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);
+ }
+}