diff --git a/Makefile b/Makefile index 50857a5e9..5c0000dd4 100644 --- a/Makefile +++ b/Makefile @@ -13,22 +13,16 @@ xdr/Stellar-internal.x \ xdr/Stellar-contract-config-setting.x \ xdr/Stellar-exporter.x -# xdrgen commit to use, see https://github.com/stellar/xdrgen -XDRGEN_COMMIT=621d042b824f67ac65cc53d0e5e381e24aed4583 # stellar-xdr commit to use, see https://github.com/stellar/stellar-xdr XDR_COMMIT=4b7a2ef7931ab2ca2499be68d849f38190b443ca .PHONY: xdr xdr-clean xdr-update xdr-generate: $(XDRS) - docker run -it --rm -v $$PWD:/wd -w /wd ruby /bin/bash -c '\ - gem install specific_install -v 0.3.8 && \ - gem specific_install https://github.com/lightsail-network/xdrgen.git -b $(XDRGEN_COMMIT) && \ - xdrgen \ - --language java \ - --namespace org.stellar.sdk.xdr \ - --output src/main/java/org/stellar/sdk/xdr/ \ - $(XDRS)' + docker run --rm -v $$PWD:/wd -w /wd ruby:3.4 /bin/bash -c '\ + cd xdr-generator && \ + bundle install --quiet && \ + bundle exec ruby generate.rb' ./gradlew :spotlessApply xdr/%.x: @@ -38,4 +32,4 @@ xdr-clean: rm xdr/*.x || true find src/main/java/org/stellar/sdk/xdr -type f -name "*.java" ! -name "package-info.java" -delete -xdr-update: xdr-clean xdr-generate \ No newline at end of file +xdr-update: xdr-clean xdr-generate diff --git a/src/main/java/org/stellar/sdk/xdr/Constants.java b/src/main/java/org/stellar/sdk/xdr/Constants.java index 36c0ab556..e0f8f7110 100644 --- a/src/main/java/org/stellar/sdk/xdr/Constants.java +++ b/src/main/java/org/stellar/sdk/xdr/Constants.java @@ -6,21 +6,21 @@ public final class Constants { private Constants() {} + public static final int AUTH_MSG_FLAG_FLOW_CONTROL_BYTES_REQUESTED = 200; + public static final int CONTRACT_COST_COUNT_LIMIT = 1024; + public static final int LIQUIDITY_POOL_FEE_V18 = 30; public static final int MASK_ACCOUNT_FLAGS = 0x7; public static final int MASK_ACCOUNT_FLAGS_V17 = 0xF; - public static final int MAX_SIGNERS = 20; + public static final int MASK_CLAIMABLE_BALANCE_FLAGS = 0x1; + public static final int MASK_LEDGER_HEADER_FLAGS = 0x7; + public static final int MASK_OFFERENTRY_FLAGS = 1; public static final int MASK_TRUSTLINE_FLAGS = 1; public static final int MASK_TRUSTLINE_FLAGS_V13 = 3; public static final int MASK_TRUSTLINE_FLAGS_V17 = 7; - public static final int MASK_OFFERENTRY_FLAGS = 1; - public static final int MASK_CLAIMABLE_BALANCE_FLAGS = 0x1; - public static final int MASK_LEDGER_HEADER_FLAGS = 0x7; - public static final int AUTH_MSG_FLAG_FLOW_CONTROL_BYTES_REQUESTED = 200; - public static final int TX_ADVERT_VECTOR_MAX_SIZE = 1000; - public static final int TX_DEMAND_VECTOR_MAX_SIZE = 1000; public static final int MAX_OPS_PER_TX = 100; - public static final int LIQUIDITY_POOL_FEE_V18 = 30; - public static final int SC_SPEC_DOC_LIMIT = 1024; + public static final int MAX_SIGNERS = 20; public static final int SCSYMBOL_LIMIT = 32; - public static final int CONTRACT_COST_COUNT_LIMIT = 1024; + public static final int SC_SPEC_DOC_LIMIT = 1024; + public static final int TX_ADVERT_VECTOR_MAX_SIZE = 1000; + public static final int TX_DEMAND_VECTOR_MAX_SIZE = 1000; } diff --git a/xdr-generator/Gemfile b/xdr-generator/Gemfile new file mode 100644 index 000000000..6a4e677ea --- /dev/null +++ b/xdr-generator/Gemfile @@ -0,0 +1,10 @@ +source "https://rubygems.org" + +gem "xdrgen", git: "https://github.com/stellar/xdrgen", branch: "master" + +# Required for Ruby 3.4+ compatibility +gem "base64" +gem "benchmark" +gem "bigdecimal" +gem "logger" +gem "mutex_m" diff --git a/xdr-generator/Gemfile.lock b/xdr-generator/Gemfile.lock new file mode 100644 index 000000000..0c393e0a7 --- /dev/null +++ b/xdr-generator/Gemfile.lock @@ -0,0 +1,55 @@ +GIT + remote: https://github.com/stellar/xdrgen + revision: 9796d627a739533d0c158ce87fde349b5f764f8b + branch: master + specs: + xdrgen (0.1.3) + activesupport (~> 6) + concurrent-ruby (<= 1.3.4) + memoist (~> 0.11.0) + slop (~> 3.4) + treetop (~> 1.5.3) + +GEM + remote: https://rubygems.org/ + specs: + activesupport (6.1.7.10) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (4.0.1) + concurrent-ruby (1.3.4) + i18n (1.14.8) + concurrent-ruby (~> 1.0) + logger (1.7.0) + memoist (0.11.0) + minitest (6.0.1) + prism (~> 1.5) + mutex_m (0.3.0) + polyglot (0.3.5) + prism (1.9.0) + slop (3.6.0) + treetop (1.5.3) + polyglot (~> 0.3) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + zeitwerk (2.7.4) + +PLATFORMS + aarch64-linux + ruby + +DEPENDENCIES + base64 + benchmark + bigdecimal + logger + mutex_m + xdrgen! + +BUNDLED WITH + 2.6.9 diff --git a/xdr-generator/generate.rb b/xdr-generator/generate.rb new file mode 100644 index 000000000..116a93c68 --- /dev/null +++ b/xdr-generator/generate.rb @@ -0,0 +1,17 @@ +require 'xdrgen' +require_relative 'generator/generator' + +puts "Generating Java XDR classes..." + +# Operate on the root directory of the repo. +Dir.chdir("..") + +# Compile the XDR files into Java. +Xdrgen::Compilation.new( + Dir.glob("xdr/*.x"), + output_dir: "src/main/java/org/stellar/sdk/xdr/", + generator: Generator, + namespace: "org.stellar.sdk.xdr", +).compile + +puts "Done!" diff --git a/xdr-generator/generator/generator.rb b/xdr-generator/generator/generator.rb new file mode 100644 index 000000000..7ef9ec9fc --- /dev/null +++ b/xdr-generator/generator/generator.rb @@ -0,0 +1,761 @@ +require 'set' + +class Generator < Xdrgen::Generators::Base + AST = Xdrgen::AST + + def generate + constants_container = Set[] + render_lib + render_definitions(@top, constants_container) + render_constants constants_container + end + + def render_lib + template = IO.read(__dir__ + "/templates/XdrDataInputStream.erb") + result = ERB.new(template).result binding + @output.write "XdrDataInputStream.java", result + + template = IO.read(__dir__ + "/templates/XdrDataOutputStream.erb") + result = ERB.new(template).result binding + @output.write "XdrDataOutputStream.java", result + + template = IO.read(__dir__ + "/templates/XdrElement.erb") + result = ERB.new(template).result binding + @output.write "XdrElement.java", result + + template = IO.read(__dir__ + "/templates/XdrString.erb") + result = ERB.new(template).result binding + @output.write "XdrString.java", result + + template = IO.read(__dir__ + "/templates/XdrUnsignedHyperInteger.erb") + result = ERB.new(template).result binding + @output.write "XdrUnsignedHyperInteger.java", result + + template = IO.read(__dir__ + "/templates/XdrUnsignedInteger.erb") + result = ERB.new(template).result binding + @output.write "XdrUnsignedInteger.java", result + end + + def render_definitions(node, constants_container) + node.namespaces.each{|n| render_definitions n, constants_container } + node.definitions.each { |defn| render_definition(defn, constants_container) } + end + + def add_imports_for_definition(defn, imports) + imports.add("org.stellar.sdk.Base64Factory") + imports.add("java.io.ByteArrayInputStream") + imports.add("java.io.ByteArrayOutputStream") + + case defn + when AST::Definitions::Struct, AST::Definitions::Union + imports.add("lombok.Data") + imports.add("lombok.NoArgsConstructor") + imports.add("lombok.AllArgsConstructor") + imports.add("lombok.Builder") + when AST::Definitions::Typedef + imports.add("lombok.Data") + imports.add("lombok.NoArgsConstructor") + imports.add("lombok.AllArgsConstructor") + end + + if defn.respond_to? :nested_definitions + defn.nested_definitions.each{ |child_defn| add_imports_for_definition(child_defn, imports) } + end + end + + def render_definition(defn, constants_container) + imports = Set[] + add_imports_for_definition(defn, imports) + + case defn + when AST::Definitions::Struct ; + render_element defn, imports, defn do |out| + render_struct defn, out + render_nested_definitions defn, out + end + when AST::Definitions::Enum ; + render_element defn, imports, defn do |out| + render_enum defn, out + end + when AST::Definitions::Union ; + render_element defn, imports, defn do |out| + render_union defn, out + render_nested_definitions defn, out + end + when AST::Definitions::Typedef ; + render_element defn, imports, defn do |out| + render_typedef defn, out + end + when AST::Definitions::Const ; + const_name = defn.name + const_value = defn.value + constants_container.add([const_name, const_value]) + end + end + + def render_nested_definitions(defn, out, post_name="implements XdrElement") + return unless defn.respond_to? :nested_definitions + defn.nested_definitions.each{|ndefn| + render_source_comment out, ndefn + case ndefn + when AST::Definitions::Struct ; + name = name ndefn + out.puts "@Data" + out.puts "@NoArgsConstructor" + out.puts "@AllArgsConstructor" + out.puts "@Builder(toBuilder = true)" + out.puts "public static class #{name} #{post_name} {" + out.indent do + render_struct ndefn, out + render_nested_definitions ndefn , out + end + out.puts "}" + when AST::Definitions::Enum ; + name = name ndefn + out.puts "public static enum #{name} #{post_name} {" + out.indent do + render_enum ndefn, out + end + out.puts "}" + when AST::Definitions::Union ; + name = name ndefn + out.puts "@Data" + out.puts "@NoArgsConstructor" + out.puts "@AllArgsConstructor" + out.puts "@Builder(toBuilder = true)" + out.puts "public static class #{name} #{post_name} {" + out.indent do + render_union ndefn, out + render_nested_definitions ndefn, out + end + out.puts "}" + when AST::Definitions::Typedef ; + name = name ndefn + out.puts "@Data" + out.puts "@NoArgsConstructor" + out.puts "@AllArgsConstructor" + out.puts "public static class #{name} #{post_name} {" + out.indent do + render_typedef ndefn, out + end + out.puts "}" + end + } + end + + def render_element(defn, imports, element, post_name="implements XdrElement") + path = element.name.camelize + ".java" + name = name_string element.name + out = @output.open(path) + render_top_matter out + imports.each do |import| + out.puts "import #{import};" + end + out.puts "\n" + render_source_comment out, element + case defn + when AST::Definitions::Struct, AST::Definitions::Union + out.puts "@Data" + out.puts "@NoArgsConstructor" + out.puts "@AllArgsConstructor" + out.puts "@Builder(toBuilder = true)" + out.puts "public class #{name} #{post_name} {" + when AST::Definitions::Enum + out.puts "public enum #{name} #{post_name} {" + when AST::Definitions::Typedef + out.puts "@Data" + out.puts "@NoArgsConstructor" + out.puts "@AllArgsConstructor" + out.puts "public class #{name} #{post_name} {" + end + out.indent do + yield out + out.unbreak + end + out.puts "}" + end + + def render_constants(constants_container) + out = @output.open("Constants.java") + render_top_matter out + out.puts "public final class Constants {" + out.indent do + out.puts "private Constants() {}" + # Sort constants by name for consistent output + constants_container.sort_by { |const_name, _| const_name }.each do |const_name, const_value| + out.puts "public static final int #{const_name} = #{const_value};" + end + end + out.puts "}" + end + + def render_enum(enum, out) + out.balance_after /,[\s]*/ do + enum.members.each_with_index do |em, index| + out.puts "#{em.name}(#{em.value})#{index == enum.members.size - 1 ? ';' : ','}" + end + end + out.break + out.puts <<-EOS.strip_heredoc + private final int value; + + #{name_string enum.name}(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static #{name_string enum.name} decode(XdrDataInputStream stream, int maxDepth) throws IOException { + // maxDepth is intentionally not checked - enums are leaf types with no recursive decoding + int value = stream.readInt(); + switch (value) { + EOS + out.indent 2 do + enum.members.each do |em| + out.puts "case #{em.value}: return #{em.name};" + end + end + out.puts <<-EOS.strip_heredoc + default: + throw new IllegalArgumentException("Unknown enum value: " + value); + } + } + + public static #{name_string enum.name} decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(value); + } + EOS + render_base64((name_string enum.name), out) + out.break + end + + def render_struct(struct, out) + struct.members.each do |m| + out.puts "private #{decl_string(m.declaration)} #{m.name};" + end + + out.puts "public void encode(XdrDataOutputStream stream) throws IOException{" + struct.members.each do |m| + out.indent do + encode_member m, out + end + end + out.puts "}" + + # decode with maxDepth parameter + out.puts <<-EOS.strip_heredoc + public static #{name struct} decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + #{name struct} decoded#{name struct} = new #{name struct}(); + EOS + struct.members.each do |m| + out.indent do + decode_member "decoded#{name struct}", m, out, "maxDepth" + end + end + out.indent do + out.puts "return decoded#{name struct};" + end + out.puts "}" + + # decode without maxDepth parameter (uses default) + out.puts <<-EOS.strip_heredoc + public static #{name struct} decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + EOS + + render_base64((name struct), out) + out.break + end + + def render_typedef(typedef, out) + out.puts "private #{decl_string typedef.declaration} #{typedef.name};" + out.puts "public void encode(XdrDataOutputStream stream) throws IOException {" + out.indent do + encode_member typedef, out + end + out.puts "}" + out.break + + # decode with maxDepth parameter + out.puts <<-EOS.strip_heredoc + public static #{name typedef} decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + #{name typedef} decoded#{name typedef} = new #{name typedef}(); + EOS + out.indent do + decode_member "decoded#{name typedef}", typedef, out, "maxDepth" + out.puts "return decoded#{name typedef};" + end + out.puts "}" + + # decode without maxDepth parameter (uses default) + out.puts <<-EOS.strip_heredoc + public static #{name typedef} decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + EOS + out.break + render_base64(typedef.name.camelize, out) + end + + def render_union(union, out) + out.puts "private #{type_string union.discriminant.type} discriminant;" + union.arms.each do |arm| + next if arm.void? + out.puts "private #{decl_string(arm.declaration)} #{arm.name};" + end + out.break + + out.puts "public void encode(XdrDataOutputStream stream) throws IOException {" + if union.discriminant.type.is_a?(AST::Typespecs::Int) + out.puts "stream.writeInt(discriminant);" + elsif type_string(union.discriminant.type) == "Uint32" + # ugly workaround for compile error after generating source for AuthenticatedMessage in stellar-core + out.puts "stream.writeInt(discriminant.getUint32().getNumber().intValue());" + else + out.puts "stream.writeInt(discriminant.getValue());" + end + if type_string(union.discriminant.type) == "Uint32" + # ugly workaround for compile error after generating source for AuthenticatedMessage in stellar-core + out.puts "switch (discriminant.getUint32().getNumber().intValue()) {" + else + out.puts "switch (discriminant) {" + end + union.arms.each do |arm| + case arm + when AST::Definitions::UnionDefaultArm ; + out.puts "default:" + else + arm.cases.each do |kase| + if kase.value.is_a?(AST::Identifier) + if type_string(union.discriminant.type) == "Integer" + member = union.resolved_case(kase) + out.puts "case #{member.value}:" + else + out.puts "case #{kase.value.name}:" + end + else + out.puts "case #{kase.value.value}:" + end + end + end + encode_member arm, out + out.puts "break;" + end + out.puts "}\n}" + + # decode with maxDepth parameter + out.puts "public static #{name union} decode(XdrDataInputStream stream, int maxDepth) throws IOException {" + out.puts "if (maxDepth <= 0) {" + out.puts " throw new IOException(\"Maximum decoding depth reached\");" + out.puts "}" + out.puts "maxDepth -= 1;" + out.puts "#{name union} decoded#{name union} = new #{name union}();" + if union.discriminant.type.is_a?(AST::Typespecs::Int) + out.puts "Integer discriminant = stream.readInt();" + else + out.puts "#{name union.discriminant.type} discriminant = #{name union.discriminant.type}.decode(stream, maxDepth);" + end + out.puts "decoded#{name union}.setDiscriminant(discriminant);" + + if type_string(union.discriminant.type) == "Uint32" + # ugly workaround for compile error after generating source for AuthenticatedMessage in stellar-core + out.puts "switch (decoded#{name union}.getDiscriminant().getUint32().getNumber().intValue()) {" + else + out.puts "switch (decoded#{name union}.getDiscriminant()) {" + end + + has_default_arm = union.arms.any? { |arm| arm.is_a?(AST::Definitions::UnionDefaultArm) } + union.arms.each do |arm| + case arm + when AST::Definitions::UnionDefaultArm ; + out.puts "default:" + else + arm.cases.each do |kase| + if kase.value.is_a?(AST::Identifier) + if type_string(union.discriminant.type) == "Integer" + member = union.resolved_case(kase) + out.puts "case #{member.value}:" + else + out.puts "case #{kase.value.name}:" + end + else + out.puts "case #{kase.value.value}:" + end + end + end + decode_member "decoded#{name union}", arm, out, "maxDepth" + out.puts "break;" + end + unless has_default_arm + out.puts "default:" + out.puts " throw new IOException(\"Unknown discriminant value: \" + discriminant);" + end + out.puts "}\n" + out.indent do + out.puts "return decoded#{name union};" + end + out.puts "}" + + # decode without maxDepth parameter (uses default) + out.puts <<-EOS.strip_heredoc + public static #{name union} decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + EOS + render_base64((name union), out) + out.break + end + + def render_top_matter(out) + out.puts <<-EOS.strip_heredoc + // Automatically generated by xdrgen + // DO NOT EDIT or your changes may be overwritten + + package #{@namespace}; + + import java.io.IOException; + EOS + out.break + end + + def render_source_comment(out, defn) + return if defn.is_a?(AST::Definitions::Namespace) + + out.puts "/**" + out.puts " * #{name defn}'s original definition in the XDR file is:" + out.puts " *
"
+    out.puts " * " + escape_html(defn.text_value).split("\n").join("\n * ")
+    out.puts " * 
" + out.puts " */" + end + + def render_base64(return_type, out) + out.puts <<-EOS.strip_heredoc + public static #{return_type} fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static #{return_type} fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } + EOS + end + + def encode_member(member, out) + case member.declaration + when AST::Declarations::Void + return + end + + if member.type.sub_type == :optional + out.puts "if (#{member.name} != null) {" + out.puts "stream.writeInt(1);" + end + case member.declaration + when AST::Declarations::Opaque ; + out.puts "int #{member.name}Size = #{member.name}.length;" + if member.declaration.fixed? + out.puts "if (#{member.name}Size != #{convert_constant member.declaration.size}) {" + out.puts " throw new IOException(\"#{member.name} size \" + #{member.name}Size + \" does not match fixed size #{member.declaration.size}\");" + out.puts "}" + else + max_size = member.declaration.resolved_size + if max_size + out.puts "if (#{member.name}Size > #{convert_constant max_size}) {" + out.puts " throw new IOException(\"#{member.name} size \" + #{member.name}Size + \" exceeds max size #{max_size}\");" + out.puts "}" + end + out.puts "stream.writeInt(#{member.name}Size);" + end + out.puts <<-EOS.strip_heredoc + stream.write(get#{member.name.slice(0,1).capitalize+member.name.slice(1..-1)}(), 0, #{member.name}Size); + EOS + when AST::Declarations::Array ; + out.puts "int #{member.name}Size = get#{member.name.slice(0,1).capitalize+member.name.slice(1..-1)}().length;" + if member.declaration.fixed? + out.puts "if (#{member.name}Size != #{convert_constant member.declaration.size}) {" + out.puts " throw new IOException(\"#{member.name} size \" + #{member.name}Size + \" does not match fixed size #{member.declaration.size}\");" + out.puts "}" + else + max_size = member.declaration.resolved_size + if max_size + out.puts "if (#{member.name}Size > #{convert_constant max_size}) {" + out.puts " throw new IOException(\"#{member.name} size \" + #{member.name}Size + \" exceeds max size #{max_size}\");" + out.puts "}" + end + out.puts "stream.writeInt(#{member.name}Size);" + end + out.puts <<-EOS.strip_heredoc + for (int i = 0; i < #{member.name}Size; i++) { + #{encode_type member.declaration.type, "#{member.name}[i]"}; + } + EOS + when AST::Declarations::String ; + max_size = member.declaration.resolved_size + if max_size + out.puts "int #{member.name}Size = #{member.name}.getBytes().length;" + out.puts "if (#{member.name}Size > #{convert_constant max_size}) {" + out.puts " throw new IOException(\"#{member.name} size \" + #{member.name}Size + \" exceeds max size #{max_size}\");" + out.puts "}" + end + out.puts "#{member.name}.encode(stream);" + else + out.puts "#{encode_type member.declaration.type, "#{member.name}"};" + end + if member.type.sub_type == :optional + out.puts "} else {" + out.puts "stream.writeInt(0);" + out.puts "}" + end + end + + def encode_type(type, value) + case type + when AST::Typespecs::Int ; + "stream.writeInt(#{value})" + when AST::Typespecs::UnsignedInt ; + "#{value}.encode(stream)" + when AST::Typespecs::Hyper ; + "stream.writeLong(#{value})" + when AST::Typespecs::UnsignedHyper ; + "#{value}.encode(stream)" + when AST::Typespecs::Float ; + "stream.writeFloat(#{value})" + when AST::Typespecs::Double ; + "stream.writeDouble(#{value})" + when AST::Typespecs::Quadruple ; + raise "cannot render quadruple in java" + when AST::Typespecs::Bool ; + "stream.writeInt(#{value} ? 1 : 0)" + when AST::Typespecs::String ; + "#{value}.encode(stream)" + when AST::Typespecs::Simple ; + "#{value}.encode(stream)" + when AST::Concerns::NestedDefinition ; + "#{value}.encode(stream)" + else + raise "Unknown typespec: #{type.class.name}" + end + end + + def decode_member(value, member, out, depth_var = nil) + case member.declaration + when AST::Declarations::Void ; + return + end + if member.type.sub_type == :optional + out.puts <<-EOS.strip_heredoc + boolean #{member.name}Present = stream.readXdrBoolean(); + if (#{member.name}Present) { + EOS + end + case member.declaration + when AST::Declarations::Opaque ; + if (member.declaration.fixed?) + out.puts "int #{member.name}Size = #{convert_constant member.declaration.size};" + else + out.puts "int #{member.name}Size = stream.readInt();" + # Add size validation for variable-length opaque + out.puts "if (#{member.name}Size < 0) {" + out.puts " throw new IOException(\"#{member.name} size \" + #{member.name}Size + \" is negative\");" + out.puts "}" + max_size = member.declaration.resolved_size + if max_size + out.puts "if (#{member.name}Size > #{convert_constant max_size}) {" + out.puts " throw new IOException(\"#{member.name} size \" + #{member.name}Size + \" exceeds max size #{max_size}\");" + out.puts "}" + end + # Add input length check to prevent DoS + out.puts "int #{member.name}RemainingInputLen = stream.getRemainingInputLen();" + out.puts "if (#{member.name}RemainingInputLen >= 0 && #{member.name}RemainingInputLen < #{member.name}Size) {" + out.puts " throw new IOException(\"#{member.name} size \" + #{member.name}Size + \" exceeds remaining input length \" + #{member.name}RemainingInputLen);" + out.puts "}" + end + out.puts <<-EOS.strip_heredoc + #{value}.#{member.name} = new byte[#{member.name}Size]; + stream.readPaddedData(#{value}.#{member.name}, 0, #{member.name}Size); + EOS + when AST::Declarations::Array ; + if (member.declaration.fixed?) + out.puts "int #{member.name}Size = #{convert_constant member.declaration.size};" + else + out.puts "int #{member.name}Size = stream.readInt();" + # Add size validation for variable-length array + out.puts "if (#{member.name}Size < 0) {" + out.puts " throw new IOException(\"#{member.name} size \" + #{member.name}Size + \" is negative\");" + out.puts "}" + max_size = member.declaration.resolved_size + if max_size + out.puts "if (#{member.name}Size > #{convert_constant max_size}) {" + out.puts " throw new IOException(\"#{member.name} size \" + #{member.name}Size + \" exceeds max size #{max_size}\");" + out.puts "}" + end + # Add input length check to prevent DoS + out.puts "int #{member.name}RemainingInputLen = stream.getRemainingInputLen();" + out.puts "if (#{member.name}RemainingInputLen >= 0 && #{member.name}RemainingInputLen < #{member.name}Size) {" + out.puts " throw new IOException(\"#{member.name} size \" + #{member.name}Size + \" exceeds remaining input length \" + #{member.name}RemainingInputLen);" + out.puts "}" + end + out.puts <<-EOS.strip_heredoc + #{value}.#{member.name} = new #{type_string member.type}[#{member.name}Size]; + for (int i = 0; i < #{member.name}Size; i++) { + #{value}.#{member.name}[i] = #{decode_type member.declaration, depth_var}; + } + EOS + else + out.puts "#{value}.#{member.name} = #{decode_type member.declaration, depth_var};" + end + if member.type.sub_type == :optional + out.puts "}" + end + end + + def decode_type(decl, depth_var = nil) + case decl.type + when AST::Typespecs::Int ; + "stream.readInt()" + when AST::Typespecs::UnsignedInt ; + depth_var ? "XdrUnsignedInteger.decode(stream, #{depth_var})" : "XdrUnsignedInteger.decode(stream)" + when AST::Typespecs::Hyper ; + "stream.readLong()" + when AST::Typespecs::UnsignedHyper ; + depth_var ? "XdrUnsignedHyperInteger.decode(stream, #{depth_var})" : "XdrUnsignedHyperInteger.decode(stream)" + when AST::Typespecs::Float ; + "stream.readFloat()" + when AST::Typespecs::Double ; + "stream.readDouble()" + when AST::Typespecs::Quadruple ; + raise "cannot render quadruple in java" + when AST::Typespecs::Bool ; + "stream.readXdrBoolean()" + when AST::Typespecs::String ; + depth_var ? "XdrString.decode(stream, #{depth_var}, #{(convert_constant decl.size) || 'Integer.MAX_VALUE'})" : "XdrString.decode(stream, #{(convert_constant decl.size) || 'Integer.MAX_VALUE'})" + when AST::Typespecs::Simple ; + depth_var ? "#{name decl.type.resolved_type}.decode(stream, #{depth_var})" : "#{name decl.type.resolved_type}.decode(stream)" + when AST::Concerns::NestedDefinition ; + depth_var ? "#{name decl.type}.decode(stream, #{depth_var})" : "#{name decl.type}.decode(stream)" + else + raise "Unknown typespec: #{decl.type.class.name}" + end + end + + def decl_string(decl) + case decl + when AST::Declarations::Opaque ; + "byte[]" + when AST::Declarations::String ; + "XdrString" + when AST::Declarations::Array ; + "#{type_string decl.type}[]" + when AST::Declarations::Optional ; + "#{type_string(decl.type)}" + when AST::Declarations::Simple ; + type_string(decl.type) + else + raise "Unknown declaration type: #{decl.class.name}" + end + end + + def is_decl_array(decl) + case decl + when AST::Declarations::Opaque ; + true + when AST::Declarations::Array ; + true + when AST::Declarations::Optional ; + is_type_array(decl.type) + when AST::Declarations::Simple ; + is_type_array(decl.type) + else + false + end + end + + def is_type_array(type) + case type + when AST::Typespecs::Opaque ; + true + else + false + end + end + + def type_string(type) + case type + when AST::Typespecs::Int ; + "Integer" + when AST::Typespecs::UnsignedInt ; + "XdrUnsignedInteger" + when AST::Typespecs::Hyper ; + "Long" + when AST::Typespecs::UnsignedHyper ; + "XdrUnsignedHyperInteger" + when AST::Typespecs::Float ; + "Float" + when AST::Typespecs::Double ; + "Double" + when AST::Typespecs::Quadruple ; + raise "cannot render quadruple in java" + when AST::Typespecs::Bool ; + "Boolean" + when AST::Typespecs::Opaque ; + "Byte[#{convert_constant type.size}]" + when AST::Typespecs::String ; + "XdrString" + when AST::Typespecs::Simple ; + name type.resolved_type + when AST::Concerns::NestedDefinition ; + name type + else + raise "Unknown typespec: #{type.class.name}" + end + end + + def name(named) + parent = name named.parent_defn if named.is_a?(AST::Concerns::NestedDefinition) + result = named.name.camelize + + "#{parent}#{result}" + end + + def name_string(name) + name.camelize + end + + def escape_html(value) + value.to_s + .gsub('&', '&') + .gsub('<', '<') + .gsub('>', '>') + .gsub('*', '*') # to avoid encountering`*/` + end + + def convert_constant(str) + if str.nil? || str.empty? + str + elsif str =~ /\A\d+\z/ + str + else + "Constants.#{str}" + end + end +end diff --git a/xdr-generator/generator/templates/XdrDataInputStream.erb b/xdr-generator/generator/templates/XdrDataInputStream.erb new file mode 100644 index 000000000..1975f8402 --- /dev/null +++ b/xdr-generator/generator/templates/XdrDataInputStream.erb @@ -0,0 +1,226 @@ +package <%= @namespace %>; + +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import lombok.Setter; + +public class XdrDataInputStream extends DataInputStream { + + /** Default maximum decoding depth to prevent stack overflow from deeply nested structures. */ + public static final int DEFAULT_MAX_DEPTH = 200; + + // The underlying input stream + private final XdrInputStream mIn; + + /** + * Maximum input length, -1 if unknown. + * This is used to validate that the declared size of variable-length + * arrays/opaques doesn't exceed the remaining input length, preventing DoS attacks. + */ + @Setter + private int maxInputLen = -1; + + /** + * Creates a XdrDataInputStream that uses the specified + * underlying InputStream. + * + * @param in the specified input stream + */ + public XdrDataInputStream(InputStream in) { + super(new XdrInputStream(in)); + mIn = (XdrInputStream) super.in; + } + + /** + * Returns the remaining input length if known, -1 otherwise. + * This can be used to validate sizes before allocating memory. + * + * @return remaining input length, or -1 if unknown + */ + public int getRemainingInputLen() { + if (maxInputLen < 0) { + return -1; + } + return maxInputLen - mIn.getCount(); + } + + /** + * Reads an XDR boolean value from the stream. + * Per RFC 4506, a boolean is encoded as an integer that must be 0 (FALSE) or 1 (TRUE). + * + * @return the boolean value + * @throws IOException if the value is not 0 or 1, or if an I/O error occurs + */ + public boolean readXdrBoolean() throws IOException { + int value = readInt(); + if (value == 0) { + return false; + } else if (value == 1) { + return true; + } else { + throw new IOException("Invalid boolean value: " + value + ", must be 0 or 1 per RFC 4506"); + } + } + + /** + * @deprecated This method does not validate the array length and may cause + * OutOfMemoryError or NegativeArraySizeException with untrusted input. + * Use generated XDR type decoders instead which include proper validation. + */ + @Deprecated + public int[] readIntArray() throws IOException { + int l = readInt(); + return readIntArray(l); + } + + private int[] readIntArray(int l) throws IOException { + int[] arr = new int[l]; + for (int i = 0; i < l; i++) { + arr[i] = readInt(); + } + return arr; + } + + /** + * @deprecated This method does not validate the array length and may cause + * OutOfMemoryError or NegativeArraySizeException with untrusted input. + * Use generated XDR type decoders instead which include proper validation. + */ + @Deprecated + public float[] readFloatArray() throws IOException { + int l = readInt(); + return readFloatArray(l); + } + + private float[] readFloatArray(int l) throws IOException { + float[] arr = new float[l]; + for (int i = 0; i < l; i++) { + arr[i] = readFloat(); + } + return arr; + } + + /** + * @deprecated This method does not validate the array length and may cause + * OutOfMemoryError or NegativeArraySizeException with untrusted input. + * Use generated XDR type decoders instead which include proper validation. + */ + @Deprecated + public double[] readDoubleArray() throws IOException { + int l = readInt(); + return readDoubleArray(l); + } + + private double[] readDoubleArray(int l) throws IOException { + double[] arr = new double[l]; + for (int i = 0; i < l; i++) { + arr[i] = readDouble(); + } + return arr; + } + + @Override + public int read() throws IOException { + return super.read(); + } + + /** + * Reads exactly len bytes of XDR opaque/string data, handling short reads, + * then reads and validates padding bytes to maintain 4-byte alignment. + * This method must be used instead of read(byte[], int, int) for opaque data + * to correctly handle short reads from the underlying stream. + * + * @param b the buffer into which the data is read + * @param off the start offset in array b at which the data is written + * @param len the number of bytes to read + * @throws IOException if an I/O error occurs or EOF is reached before reading len bytes + */ + public void readPaddedData(byte[] b, int off, int len) throws IOException { + mIn.readFullyNoPad(b, off, len); + mIn.pad(); + } + + /** + * Need to provide a custom impl of InputStream as DataInputStream's read methods + * are final and we need to keep track of the count for padding purposes. + */ + private static final class XdrInputStream extends InputStream { + + // The underlying input stream + private final InputStream mIn; + + // The amount of bytes read so far. + private int mCount; + + public XdrInputStream(InputStream in) { + mIn = in; + mCount = 0; + } + + public int getCount() { + return mCount; + } + + @Override + public int read() throws IOException { + int read = mIn.read(); + if (read >= 0) { + mCount++; + } + return read; + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int read = mIn.read(b, off, len); + if (read > 0) { + mCount += read; + // Note: padding is NOT automatically applied here. + // For opaque/string data, use XdrDataInputStream.readPaddedData() which + // handles short reads correctly and applies padding after all data is read. + // Primitive types (int, long, float, double) are naturally 4/8-byte aligned + // and don't need padding between reads. + } + return read; + } + + public void pad() throws IOException { + int pad = 0; + int mod = mCount % 4; + if (mod > 0) { + pad = 4-mod; + } + + while (pad-- > 0) { + int b = read(); + if (b != 0) { + throw new IOException("non-zero padding"); + } + } + } + + /** + * Reads exactly len bytes into the buffer, handling short reads. + * Does not apply padding - caller must call pad() after this. + */ + void readFullyNoPad(byte[] b, int off, int len) throws IOException { + int totalRead = 0; + while (totalRead < len) { + int read = mIn.read(b, off + totalRead, len - totalRead); + if (read < 0) { + throw new EOFException("Unexpected end of stream while reading XDR data"); + } + mCount += read; + totalRead += read; + } + } + } +} diff --git a/xdr-generator/generator/templates/XdrDataOutputStream.erb b/xdr-generator/generator/templates/XdrDataOutputStream.erb new file mode 100644 index 000000000..6069833a9 --- /dev/null +++ b/xdr-generator/generator/templates/XdrDataOutputStream.erb @@ -0,0 +1,96 @@ +package <%= @namespace %>; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; + +public class XdrDataOutputStream extends DataOutputStream { + + private final XdrOutputStream mOut; + + public XdrDataOutputStream(OutputStream out) { + super(new XdrOutputStream(out)); + mOut = (XdrOutputStream) super.out; + } + + public void writeIntArray(int[] a) throws IOException { + writeInt(a.length); + writeIntArray(a, a.length); + } + + private void writeIntArray(int[] a, int l) throws IOException { + for (int i = 0; i < l; i++) { + writeInt(a[i]); + } + } + + public void writeFloatArray(float[] a) throws IOException { + writeInt(a.length); + writeFloatArray(a, a.length); + } + + private void writeFloatArray(float[] a, int l) throws IOException { + for (int i = 0; i < l; i++) { + writeFloat(a[i]); + } + } + + public void writeDoubleArray(double[] a) throws IOException { + writeInt(a.length); + writeDoubleArray(a, a.length); + } + + private void writeDoubleArray(double[] a, int l) throws IOException { + for (int i = 0; i < l; i++) { + writeDouble(a[i]); + } + } + + private static final class XdrOutputStream extends OutputStream { + + private final OutputStream mOut; + + // Number of bytes written + private int mCount; + + public XdrOutputStream(OutputStream out) { + mOut = out; + mCount = 0; + } + + @Override + public void write(int b) throws IOException { + mOut.write(b); + // https://docs.oracle.com/javase/7/docs/api/java/io/OutputStream.html#write(int): + // > The byte to be written is the eight low-order bits of the argument b. + // > The 24 high-order bits of b are ignored. + mCount++; + } + + @Override + public void write(byte[] b) throws IOException { + // https://docs.oracle.com/javase/7/docs/api/java/io/OutputStream.html#write(byte[]): + // > The general contract for write(b) is that it should have exactly the same effect + // > as the call write(b, 0, b.length). + write(b, 0, b.length); + } + + public void write(byte[] b, int offset, int length) throws IOException { + mOut.write(b, offset, length); + mCount += length; + pad(); + } + + public void pad() throws IOException { + int pad = 0; + int mod = mCount % 4; + if (mod > 0) { + pad = 4-mod; + } + while (pad-- > 0) { + write(0); + } + } + } +} diff --git a/xdr-generator/generator/templates/XdrElement.erb b/xdr-generator/generator/templates/XdrElement.erb new file mode 100644 index 000000000..0194cf7e1 --- /dev/null +++ b/xdr-generator/generator/templates/XdrElement.erb @@ -0,0 +1,21 @@ +package <%= @namespace %>; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import org.stellar.sdk.Base64Factory; + +/** Common parent interface for all generated classes. */ +public interface XdrElement { + void encode(XdrDataOutputStream stream) throws IOException; + + default String toXdrBase64() throws IOException { + return Base64Factory.getInstance().encodeToString(toXdrByteArray()); + } + + default byte[] toXdrByteArray() throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + XdrDataOutputStream xdrDataOutputStream = new XdrDataOutputStream(byteArrayOutputStream); + encode(xdrDataOutputStream); + return byteArrayOutputStream.toByteArray(); + } +} diff --git a/xdr-generator/generator/templates/XdrString.erb b/xdr-generator/generator/templates/XdrString.erb new file mode 100644 index 000000000..f0ff6e9d5 --- /dev/null +++ b/xdr-generator/generator/templates/XdrString.erb @@ -0,0 +1,78 @@ +package <%= @namespace %>; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InvalidClassException; +import java.nio.charset.StandardCharsets; +import lombok.Value; +import org.stellar.sdk.Base64Factory; + +@Value +public class XdrString implements XdrElement { + byte[] bytes; + + public XdrString(byte[] bytes) { + this.bytes = bytes; + } + + public XdrString(String text) { + this.bytes = text.getBytes(StandardCharsets.UTF_8); + } + + @Override + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(this.bytes.length); + stream.write(this.bytes, 0, this.bytes.length); + } + + public static XdrString decode(XdrDataInputStream stream, int maxDepth, int maxSize) throws IOException { + // maxDepth is intentionally not checked - XdrString is a leaf type with no recursive decoding + int size = stream.readInt(); + if (size < 0) { + throw new IOException("String length " + size + " is negative"); + } + if (size > maxSize) { + throw new IOException("String length " + size + " exceeds max size " + maxSize); + } + int remainingInputLen = stream.getRemainingInputLen(); + if (remainingInputLen >= 0 && remainingInputLen < size) { + throw new IOException("String length " + size + " exceeds remaining input length " + remainingInputLen); + } + byte[] bytes = new byte[size]; + stream.readPaddedData(bytes, 0, size); + return new XdrString(bytes); + } + + public static XdrString decode(XdrDataInputStream stream, int maxSize) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH, maxSize); + } + + public static XdrString fromXdrBase64(String xdr, int maxSize) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes, maxSize); + } + + public static XdrString fromXdrBase64(String xdr) throws IOException { + return fromXdrBase64(xdr, Integer.MAX_VALUE); + } + + public static XdrString fromXdrByteArray(byte[] xdr, int maxSize) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream, maxSize); + } + + public static XdrString fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream, Integer.MAX_VALUE); + } + + @Override + public String toString() { + return new String(bytes, StandardCharsets.UTF_8); + } +} diff --git a/xdr-generator/generator/templates/XdrUnsignedHyperInteger.erb b/xdr-generator/generator/templates/XdrUnsignedHyperInteger.erb new file mode 100644 index 000000000..de585c8cf --- /dev/null +++ b/xdr-generator/generator/templates/XdrUnsignedHyperInteger.erb @@ -0,0 +1,74 @@ +package <%= @namespace %>; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import lombok.Value; +import org.stellar.sdk.Base64Factory; + +/** + * Represents XDR Unsigned Hyper Integer. + * + * @see XDR: External Data + * Representation Standard + */ +@Value +public class XdrUnsignedHyperInteger implements XdrElement { + public static final BigInteger MAX_VALUE = new BigInteger("18446744073709551615"); + public static final BigInteger MIN_VALUE = BigInteger.ZERO; + BigInteger number; + + public XdrUnsignedHyperInteger(BigInteger number) { + if (number.compareTo(MIN_VALUE) < 0 || number.compareTo(MAX_VALUE) > 0) { + throw new IllegalArgumentException("number must be between 0 and 2^64 - 1 inclusive"); + } + this.number = number; + } + + public XdrUnsignedHyperInteger(Long number) { + if (number < 0) { + throw new IllegalArgumentException( + "number must be greater than or equal to 0 if you want to construct it from Long"); + } + this.number = BigInteger.valueOf(number); + } + + @Override + public void encode(XdrDataOutputStream stream) throws IOException { + stream.write(getBytes()); + } + + public static XdrUnsignedHyperInteger decode(XdrDataInputStream stream, int maxDepth) throws IOException { + // maxDepth is intentionally not checked - XdrUnsignedHyperInteger is a leaf type with no recursive decoding + byte[] bytes = new byte[8]; + stream.readFully(bytes); + BigInteger uint64 = new BigInteger(1, bytes); + return new XdrUnsignedHyperInteger(uint64); + } + + public static XdrUnsignedHyperInteger decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + private byte[] getBytes() { + byte[] bytes = number.toByteArray(); + byte[] paddedBytes = new byte[8]; + + int numBytesToCopy = Math.min(bytes.length, 8); + int copyStartIndex = bytes.length - numBytesToCopy; + System.arraycopy(bytes, copyStartIndex, paddedBytes, 8 - numBytesToCopy, numBytesToCopy); + return paddedBytes; + } + + public static XdrUnsignedHyperInteger fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static XdrUnsignedHyperInteger fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/generator/templates/XdrUnsignedInteger.erb b/xdr-generator/generator/templates/XdrUnsignedInteger.erb new file mode 100644 index 000000000..824730b2e --- /dev/null +++ b/xdr-generator/generator/templates/XdrUnsignedInteger.erb @@ -0,0 +1,62 @@ +package <%= @namespace %>; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import lombok.Value; +import org.stellar.sdk.Base64Factory; + +/** + * Represents XDR Unsigned Integer. + * + * @see XDR: External Data + * Representation Standard + */ +@Value +public class XdrUnsignedInteger implements XdrElement { + public static final long MAX_VALUE = (1L << 32) - 1; + public static final long MIN_VALUE = 0; + Long number; + + public XdrUnsignedInteger(Long number) { + if (number < MIN_VALUE || number > MAX_VALUE) { + throw new IllegalArgumentException("number must be between 0 and 2^32 - 1 inclusive"); + } + this.number = number; + } + + public XdrUnsignedInteger(Integer number) { + if (number < 0) { + throw new IllegalArgumentException( + "number must be greater than or equal to 0 if you want to construct it from Integer"); + } + this.number = number.longValue(); + } + + public static XdrUnsignedInteger decode(XdrDataInputStream stream, int maxDepth) throws IOException { + // maxDepth is intentionally not checked - XdrUnsignedInteger is a leaf type with no recursive decoding + int intValue = stream.readInt(); + long uint32Value = Integer.toUnsignedLong(intValue); + return new XdrUnsignedInteger(uint32Value); + } + + public static XdrUnsignedInteger decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + @Override + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(number.intValue()); + } + + public static XdrUnsignedInteger fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static XdrUnsignedInteger fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + return decode(xdrDataInputStream); + } +}