diff --git a/.github/workflows/xdr-generator-snapshot-test.yml b/.github/workflows/xdr-generator-snapshot-test.yml new file mode 100644 index 000000000..918f8f466 --- /dev/null +++ b/.github/workflows/xdr-generator-snapshot-test.yml @@ -0,0 +1,22 @@ +name: XDR Generator Snapshot Test + +on: + push: + branches: [master] + paths: + - "xdr-generator/**" + - "Makefile" + - ".github/workflows/xdr-generator-snapshot-test.yml" + pull_request: + paths: + - "xdr-generator/**" + - "Makefile" + - ".github/workflows/xdr-generator-snapshot-test.yml" + +jobs: + snapshot-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run XDR generator snapshot tests + run: make xdr-generator-test diff --git a/Makefile b/Makefile index 5c0000dd4..dfe9b29a2 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ xdr/Stellar-exporter.x # stellar-xdr commit to use, see https://github.com/stellar/stellar-xdr XDR_COMMIT=4b7a2ef7931ab2ca2499be68d849f38190b443ca -.PHONY: xdr xdr-clean xdr-update +.PHONY: xdr xdr-clean xdr-update xdr-generator-test xdr-generator-update-snapshots xdr-generate: $(XDRS) docker run --rm -v $$PWD:/wd -w /wd ruby:3.4 /bin/bash -c '\ @@ -33,3 +33,15 @@ xdr-clean: find src/main/java/org/stellar/sdk/xdr -type f -name "*.java" ! -name "package-info.java" -delete xdr-update: xdr-clean xdr-generate + +xdr-generator-test: + docker run --rm -v $$PWD:/wd -w /wd ruby:3.4 /bin/bash -c '\ + cd xdr-generator && \ + bundle install --quiet && \ + bundle exec ruby test/generator_snapshot_test.rb' + +xdr-generator-update-snapshots: + docker run --rm -v $$PWD:/wd -w /wd ruby:3.4 /bin/bash -c '\ + cd xdr-generator && \ + bundle install --quiet && \ + UPDATE_SNAPSHOTS=1 bundle exec ruby test/generator_snapshot_test.rb' diff --git a/xdr-generator/Gemfile b/xdr-generator/Gemfile index 6a4e677ea..972cbc209 100644 --- a/xdr-generator/Gemfile +++ b/xdr-generator/Gemfile @@ -8,3 +8,5 @@ gem "benchmark" gem "bigdecimal" gem "logger" gem "mutex_m" + +gem "minitest" diff --git a/xdr-generator/Gemfile.lock b/xdr-generator/Gemfile.lock index 0c393e0a7..85012cafb 100644 --- a/xdr-generator/Gemfile.lock +++ b/xdr-generator/Gemfile.lock @@ -48,6 +48,7 @@ DEPENDENCIES benchmark bigdecimal logger + minitest mutex_m xdrgen! diff --git a/xdr-generator/test/fixtures/xdrgen/block_comments.x b/xdr-generator/test/fixtures/xdrgen/block_comments.x new file mode 100644 index 000000000..e58ce95a9 --- /dev/null +++ b/xdr-generator/test/fixtures/xdrgen/block_comments.x @@ -0,0 +1,13 @@ +enum AccountFlags +{ // masks for each flag + AUTH_REQUIRED_FLAG = 0x1 +}; + +/* AccountEntry + + Main entry representing a user in Stellar. All transactions are performed + using an account. + + Other ledger entries created require an account. + +*/ diff --git a/xdr-generator/test/fixtures/xdrgen/const.x b/xdr-generator/test/fixtures/xdrgen/const.x new file mode 100644 index 000000000..48fdb2157 --- /dev/null +++ b/xdr-generator/test/fixtures/xdrgen/const.x @@ -0,0 +1,4 @@ +const FOO = 1; + +typedef int TestArray[FOO]; +typedef int TestArray2; diff --git a/xdr-generator/test/fixtures/xdrgen/enum.x b/xdr-generator/test/fixtures/xdrgen/enum.x new file mode 100644 index 000000000..17c898f45 --- /dev/null +++ b/xdr-generator/test/fixtures/xdrgen/enum.x @@ -0,0 +1,42 @@ +enum MessageType +{ + ERROR_MSG, + HELLO, + DONT_HAVE, + + GET_PEERS, // gets a list of peers this guy knows about + PEERS, + + GET_TX_SET, // gets a particular txset by hash + TX_SET, + + GET_VALIDATIONS, // gets validations for a given ledger hash + VALIDATIONS, + + TRANSACTION, //pass on a tx you have heard about + JSON_TRANSACTION, + + // FBA + GET_FBA_QUORUMSET, + FBA_QUORUMSET, + FBA_MESSAGE +}; + +enum Color { + RED=0, + GREEN=1, + BLUE=2 +}; + + +enum Color2 { + RED2=RED, + GREEN2=1, + BLUE2=2 +}; + +enum Color3 { + RED_1=1, + RED_2_TWO=2, + RED_3=3 +}; diff --git a/xdr-generator/test/fixtures/xdrgen/nesting.x b/xdr-generator/test/fixtures/xdrgen/nesting.x new file mode 100644 index 000000000..990bdd9db --- /dev/null +++ b/xdr-generator/test/fixtures/xdrgen/nesting.x @@ -0,0 +1,24 @@ +enum UnionKey { + ONE = 1, + TWO = 2, + OFFER = 3 +}; + +typedef int Foo; + +union MyUnion switch (UnionKey type) +{ + case ONE: + struct { + int someInt; + } one; + + case TWO: + struct { + int someInt; + Foo foo; + } two; + + case OFFER: + void; +}; diff --git a/xdr-generator/test/fixtures/xdrgen/optional.x b/xdr-generator/test/fixtures/xdrgen/optional.x new file mode 100644 index 000000000..24669a3da --- /dev/null +++ b/xdr-generator/test/fixtures/xdrgen/optional.x @@ -0,0 +1,8 @@ +typedef int Arr[2]; + +struct HasOptions +{ + int* firstOption; + int *secondOption; + Arr *thirdOption; +}; diff --git a/xdr-generator/test/fixtures/xdrgen/struct.x b/xdr-generator/test/fixtures/xdrgen/struct.x new file mode 100644 index 000000000..2a984357f --- /dev/null +++ b/xdr-generator/test/fixtures/xdrgen/struct.x @@ -0,0 +1,10 @@ +typedef hyper int64; + +struct MyStruct +{ + int someInt; + int64 aBigInt; + opaque someOpaque[10]; + string someString<>; + string maxString<100>; +}; diff --git a/xdr-generator/test/fixtures/xdrgen/test.x b/xdr-generator/test/fixtures/xdrgen/test.x new file mode 100644 index 000000000..8f78e84c5 --- /dev/null +++ b/xdr-generator/test/fixtures/xdrgen/test.x @@ -0,0 +1,77 @@ +%#include "generated/FBAXDR.h" + +namespace MyNamespace { + +// messages +typedef opaque uint512[64]; +typedef opaque uint513<64>; +typedef opaque uint514<>; +typedef string str<64>; +typedef string str2<>; + +typedef opaque Hash[32]; +typedef Hash Hashes1[12]; +typedef Hash Hashes2<12>; +typedef Hash Hashes3<>; + + +typedef Hash *optHash1; +typedef Hash* optHash2; + +typedef int int1; +typedef hyper int2; +typedef unsigned int int3; +typedef unsigned hyper int4; + +struct MyStruct +{ + uint512 field1; + optHash1 field2; + int1 field3; + unsigned int field4; + float field5; + double field6; + bool field7; +}; + +struct LotsOfMyStructs +{ + MyStruct members<>; +}; + +struct HasStuff +{ + LotsOfMyStructs data; +}; + +enum Color { + RED, + BLUE = 5, + GREEN +}; + +const FOO = 1244; +const BAR = FOO; + +struct Nester +{ + enum { + BLAH_1, + BLAH_2 + } nestedEnum; + + struct { + int blah; + } nestedStruct; + + union switch (Color color) { + case RED: + void; + default: + int blah2; + } nestedUnion; + + +}; + +} diff --git a/xdr-generator/test/fixtures/xdrgen/union.x b/xdr-generator/test/fixtures/xdrgen/union.x new file mode 100644 index 000000000..6c02ba621 --- /dev/null +++ b/xdr-generator/test/fixtures/xdrgen/union.x @@ -0,0 +1,28 @@ +typedef int Error; +typedef int Multi; + +enum UnionKey { + ERROR, + MULTI +}; + +union MyUnion switch (UnionKey type) +{ + case ERROR: + Error error; + case MULTI: + Multi things<>; + + +}; + +union IntUnion switch (int type) +{ + case 0: + Error error; + case 1: + Multi things<>; + +}; + +typedef IntUnion IntUnion2; diff --git a/xdr-generator/test/generator_snapshot_test.rb b/xdr-generator/test/generator_snapshot_test.rb new file mode 100644 index 000000000..870478854 --- /dev/null +++ b/xdr-generator/test/generator_snapshot_test.rb @@ -0,0 +1,89 @@ +require "fileutils" +require "minitest/autorun" +require "tmpdir" + +require "xdrgen" +require_relative "../generator/generator" + +class GeneratorSnapshotTest < Minitest::Test + FIXTURES_DIR = File.expand_path("fixtures/xdrgen", __dir__) + SNAPSHOTS_DIR = File.expand_path("snapshots", __dir__) + UPDATE_SNAPSHOTS = ENV["UPDATE_SNAPSHOTS"] == "1" + + def test_fixture_snapshots + fixtures = fixture_paths + refute_empty fixtures, "No fixture files were found in #{FIXTURES_DIR}" + + fixtures.each do |fixture_path| + assert_fixture_snapshot(fixture_path) + end + end + + private + + def fixture_paths + Dir[File.join(FIXTURES_DIR, "*.x")].sort + end + + def assert_fixture_snapshot(fixture_path) + fixture_name = File.basename(fixture_path, ".x") + snapshot_dir = File.join(SNAPSHOTS_DIR, fixture_name) + + Dir.mktmpdir("xdrgen-#{fixture_name}-") do |tmp_dir| + generated_dir = File.join(tmp_dir, "generated") + compile_fixture(fixture_path, generated_dir) + + if UPDATE_SNAPSHOTS + FileUtils.rm_rf(snapshot_dir) + FileUtils.mkdir_p(snapshot_dir) + FileUtils.cp_r("#{generated_dir}/.", snapshot_dir) + next + end + + assert File.directory?(snapshot_dir), <<~MSG + Missing snapshot for #{fixture_name}. + Run UPDATE_SNAPSHOTS=1 bundle exec ruby test/generator_snapshot_test.rb + MSG + + assert_files_match(snapshot_dir, generated_dir, fixture_name) + end + end + + def compile_fixture(fixture_path, output_dir) + FileUtils.mkdir_p(output_dir) + Xdrgen::Compilation.new( + [fixture_path], + output_dir: output_dir, + generator: Generator, + namespace: "org.stellar.sdk.xdr", + ).compile + end + + def assert_files_match(expected_dir, actual_dir, fixture_name) + expected_files = collect_relative_files(expected_dir) + actual_files = collect_relative_files(actual_dir) + + assert_equal expected_files, actual_files, <<~MSG + Generated file list changed for fixture #{fixture_name}. + Expected: #{expected_files.join(", ")} + Actual: #{actual_files.join(", ")} + MSG + + expected_files.each do |relative_path| + expected_path = File.join(expected_dir, relative_path) + actual_path = File.join(actual_dir, relative_path) + expected_content = File.binread(expected_path) + actual_content = File.binread(actual_path) + + assert_equal expected_content, actual_content, <<~MSG + Generated content changed for fixture #{fixture_name}: #{relative_path} + MSG + end + end + + def collect_relative_files(root_dir) + Dir.chdir(root_dir) do + Dir.glob("**/*", File::FNM_DOTMATCH).sort.select { |path| File.file?(path) } + end + end +end diff --git a/xdr-generator/test/snapshots/block_comments/AccountFlags.java b/xdr-generator/test/snapshots/block_comments/AccountFlags.java new file mode 100644 index 000000000..a8f242a9b --- /dev/null +++ b/xdr-generator/test/snapshots/block_comments/AccountFlags.java @@ -0,0 +1,62 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +/** + * AccountFlags's original definition in the XDR file is: + *
+ * enum AccountFlags
+ * { // masks for each flag
+ *     AUTH_REQUIRED_FLAG = 0x1
+ * };
+ * 
+ */ +public enum AccountFlags implements XdrElement { + AUTH_REQUIRED_FLAG(1); + + private final int value; + + AccountFlags(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static AccountFlags 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) { + case 1: return AUTH_REQUIRED_FLAG; + default: + throw new IllegalArgumentException("Unknown enum value: " + value); + } + } + + public static AccountFlags decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(value); + } + public static AccountFlags fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static AccountFlags fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/block_comments/Constants.java b/xdr-generator/test/snapshots/block_comments/Constants.java new file mode 100644 index 000000000..3bb7224f8 --- /dev/null +++ b/xdr-generator/test/snapshots/block_comments/Constants.java @@ -0,0 +1,10 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +public final class Constants { + private Constants() {} +} diff --git a/xdr-generator/test/snapshots/block_comments/XdrDataInputStream.java b/xdr-generator/test/snapshots/block_comments/XdrDataInputStream.java new file mode 100644 index 000000000..0dd3098d9 --- /dev/null +++ b/xdr-generator/test/snapshots/block_comments/XdrDataInputStream.java @@ -0,0 +1,226 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/block_comments/XdrDataOutputStream.java b/xdr-generator/test/snapshots/block_comments/XdrDataOutputStream.java new file mode 100644 index 000000000..9bb857a64 --- /dev/null +++ b/xdr-generator/test/snapshots/block_comments/XdrDataOutputStream.java @@ -0,0 +1,96 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/block_comments/XdrElement.java b/xdr-generator/test/snapshots/block_comments/XdrElement.java new file mode 100644 index 000000000..fba637542 --- /dev/null +++ b/xdr-generator/test/snapshots/block_comments/XdrElement.java @@ -0,0 +1,21 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/block_comments/XdrString.java b/xdr-generator/test/snapshots/block_comments/XdrString.java new file mode 100644 index 000000000..f0a0c478a --- /dev/null +++ b/xdr-generator/test/snapshots/block_comments/XdrString.java @@ -0,0 +1,78 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/block_comments/XdrUnsignedHyperInteger.java b/xdr-generator/test/snapshots/block_comments/XdrUnsignedHyperInteger.java new file mode 100644 index 000000000..82a21dcab --- /dev/null +++ b/xdr-generator/test/snapshots/block_comments/XdrUnsignedHyperInteger.java @@ -0,0 +1,74 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/block_comments/XdrUnsignedInteger.java b/xdr-generator/test/snapshots/block_comments/XdrUnsignedInteger.java new file mode 100644 index 000000000..d5a79959d --- /dev/null +++ b/xdr-generator/test/snapshots/block_comments/XdrUnsignedInteger.java @@ -0,0 +1,62 @@ +package org.stellar.sdk.xdr; + +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); + } +} diff --git a/xdr-generator/test/snapshots/const/Constants.java b/xdr-generator/test/snapshots/const/Constants.java new file mode 100644 index 000000000..9d069d4f9 --- /dev/null +++ b/xdr-generator/test/snapshots/const/Constants.java @@ -0,0 +1,11 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +public final class Constants { + private Constants() {} + public static final int FOO = 1; +} diff --git a/xdr-generator/test/snapshots/const/TestArray.java b/xdr-generator/test/snapshots/const/TestArray.java new file mode 100644 index 000000000..59834f8ad --- /dev/null +++ b/xdr-generator/test/snapshots/const/TestArray.java @@ -0,0 +1,64 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * TestArray's original definition in the XDR file is: + *
+ * typedef int TestArray[FOO];
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TestArray implements XdrElement { + private Integer[] TestArray; + public void encode(XdrDataOutputStream stream) throws IOException { + int TestArraySize = getTestArray().length; + if (TestArraySize != Constants.FOO) { + throw new IOException("TestArray size " + TestArraySize + " does not match fixed size FOO"); + } + for (int i = 0; i < TestArraySize; i++) { + stream.writeInt(TestArray[i]); + } + } + + public static TestArray decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + TestArray decodedTestArray = new TestArray(); + int TestArraySize = Constants.FOO; + decodedTestArray.TestArray = new Integer[TestArraySize]; + for (int i = 0; i < TestArraySize; i++) { + decodedTestArray.TestArray[i] = stream.readInt(); + } + return decodedTestArray; + } + public static TestArray decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static TestArray fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static TestArray fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/const/TestArray2.java b/xdr-generator/test/snapshots/const/TestArray2.java new file mode 100644 index 000000000..9e56e1456 --- /dev/null +++ b/xdr-generator/test/snapshots/const/TestArray2.java @@ -0,0 +1,75 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * TestArray2's original definition in the XDR file is: + *
+ * typedef int TestArray2<FOO>;
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TestArray2 implements XdrElement { + private Integer[] TestArray2; + public void encode(XdrDataOutputStream stream) throws IOException { + int TestArray2Size = getTestArray2().length; + if (TestArray2Size > 1) { + throw new IOException("TestArray2 size " + TestArray2Size + " exceeds max size 1"); + } + stream.writeInt(TestArray2Size); + for (int i = 0; i < TestArray2Size; i++) { + stream.writeInt(TestArray2[i]); + } + } + + public static TestArray2 decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + TestArray2 decodedTestArray2 = new TestArray2(); + int TestArray2Size = stream.readInt(); + if (TestArray2Size < 0) { + throw new IOException("TestArray2 size " + TestArray2Size + " is negative"); + } + if (TestArray2Size > 1) { + throw new IOException("TestArray2 size " + TestArray2Size + " exceeds max size 1"); + } + int TestArray2RemainingInputLen = stream.getRemainingInputLen(); + if (TestArray2RemainingInputLen >= 0 && TestArray2RemainingInputLen < TestArray2Size) { + throw new IOException("TestArray2 size " + TestArray2Size + " exceeds remaining input length " + TestArray2RemainingInputLen); + } + decodedTestArray2.TestArray2 = new Integer[TestArray2Size]; + for (int i = 0; i < TestArray2Size; i++) { + decodedTestArray2.TestArray2[i] = stream.readInt(); + } + return decodedTestArray2; + } + public static TestArray2 decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static TestArray2 fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static TestArray2 fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/const/XdrDataInputStream.java b/xdr-generator/test/snapshots/const/XdrDataInputStream.java new file mode 100644 index 000000000..0dd3098d9 --- /dev/null +++ b/xdr-generator/test/snapshots/const/XdrDataInputStream.java @@ -0,0 +1,226 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/const/XdrDataOutputStream.java b/xdr-generator/test/snapshots/const/XdrDataOutputStream.java new file mode 100644 index 000000000..9bb857a64 --- /dev/null +++ b/xdr-generator/test/snapshots/const/XdrDataOutputStream.java @@ -0,0 +1,96 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/const/XdrElement.java b/xdr-generator/test/snapshots/const/XdrElement.java new file mode 100644 index 000000000..fba637542 --- /dev/null +++ b/xdr-generator/test/snapshots/const/XdrElement.java @@ -0,0 +1,21 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/const/XdrString.java b/xdr-generator/test/snapshots/const/XdrString.java new file mode 100644 index 000000000..f0a0c478a --- /dev/null +++ b/xdr-generator/test/snapshots/const/XdrString.java @@ -0,0 +1,78 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/const/XdrUnsignedHyperInteger.java b/xdr-generator/test/snapshots/const/XdrUnsignedHyperInteger.java new file mode 100644 index 000000000..82a21dcab --- /dev/null +++ b/xdr-generator/test/snapshots/const/XdrUnsignedHyperInteger.java @@ -0,0 +1,74 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/const/XdrUnsignedInteger.java b/xdr-generator/test/snapshots/const/XdrUnsignedInteger.java new file mode 100644 index 000000000..d5a79959d --- /dev/null +++ b/xdr-generator/test/snapshots/const/XdrUnsignedInteger.java @@ -0,0 +1,62 @@ +package org.stellar.sdk.xdr; + +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); + } +} diff --git a/xdr-generator/test/snapshots/enum/Color.java b/xdr-generator/test/snapshots/enum/Color.java new file mode 100644 index 000000000..0d9c985a9 --- /dev/null +++ b/xdr-generator/test/snapshots/enum/Color.java @@ -0,0 +1,67 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +/** + * Color's original definition in the XDR file is: + *
+ * enum Color {
+ *     RED=0,
+ *     GREEN=1,
+ *     BLUE=2
+ * };
+ * 
+ */ +public enum Color implements XdrElement { + RED(0), + GREEN(1), + BLUE(2); + + private final int value; + + Color(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static Color 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) { + case 0: return RED; + case 1: return GREEN; + case 2: return BLUE; + default: + throw new IllegalArgumentException("Unknown enum value: " + value); + } + } + + public static Color decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(value); + } + public static Color fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Color fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/enum/Color2.java b/xdr-generator/test/snapshots/enum/Color2.java new file mode 100644 index 000000000..d451853cc --- /dev/null +++ b/xdr-generator/test/snapshots/enum/Color2.java @@ -0,0 +1,67 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +/** + * Color2's original definition in the XDR file is: + *
+ * enum Color2 {
+ *     RED2=RED,
+ *     GREEN2=1,
+ *     BLUE2=2
+ * };
+ * 
+ */ +public enum Color2 implements XdrElement { + RED2(0), + GREEN2(1), + BLUE2(2); + + private final int value; + + Color2(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static Color2 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) { + case 0: return RED2; + case 1: return GREEN2; + case 2: return BLUE2; + default: + throw new IllegalArgumentException("Unknown enum value: " + value); + } + } + + public static Color2 decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(value); + } + public static Color2 fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Color2 fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/enum/Color3.java b/xdr-generator/test/snapshots/enum/Color3.java new file mode 100644 index 000000000..762898f59 --- /dev/null +++ b/xdr-generator/test/snapshots/enum/Color3.java @@ -0,0 +1,67 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +/** + * Color3's original definition in the XDR file is: + *
+ * enum Color3 {
+ *     RED_1=1,
+ *     RED_2_TWO=2,
+ *     RED_3=3
+ * };
+ * 
+ */ +public enum Color3 implements XdrElement { + RED_1(1), + RED_2_TWO(2), + RED_3(3); + + private final int value; + + Color3(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static Color3 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) { + case 1: return RED_1; + case 2: return RED_2_TWO; + case 3: return RED_3; + default: + throw new IllegalArgumentException("Unknown enum value: " + value); + } + } + + public static Color3 decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(value); + } + public static Color3 fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Color3 fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/enum/Constants.java b/xdr-generator/test/snapshots/enum/Constants.java new file mode 100644 index 000000000..3bb7224f8 --- /dev/null +++ b/xdr-generator/test/snapshots/enum/Constants.java @@ -0,0 +1,10 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +public final class Constants { + private Constants() {} +} diff --git a/xdr-generator/test/snapshots/enum/MessageType.java b/xdr-generator/test/snapshots/enum/MessageType.java new file mode 100644 index 000000000..c181ec96f --- /dev/null +++ b/xdr-generator/test/snapshots/enum/MessageType.java @@ -0,0 +1,107 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +/** + * MessageType's original definition in the XDR file is: + *
+ * enum MessageType
+ * {
+ *     ERROR_MSG,
+ *     HELLO,
+ *     DONT_HAVE,
+ * 
+ *     GET_PEERS,   // gets a list of peers this guy knows about
+ *     PEERS,
+ * 
+ *     GET_TX_SET,  // gets a particular txset by hash
+ *     TX_SET,
+ * 
+ *     GET_VALIDATIONS, // gets validations for a given ledger hash
+ *     VALIDATIONS,
+ * 
+ *     TRANSACTION, //pass on a tx you have heard about
+ *     JSON_TRANSACTION,
+ * 
+ *     // FBA
+ *     GET_FBA_QUORUMSET,
+ *     FBA_QUORUMSET,
+ *     FBA_MESSAGE
+ * };
+ * 
+ */ +public enum MessageType implements XdrElement { + ERROR_MSG(0), + HELLO(1), + DONT_HAVE(2), + GET_PEERS(3), + PEERS(4), + GET_TX_SET(5), + TX_SET(6), + GET_VALIDATIONS(7), + VALIDATIONS(8), + TRANSACTION(9), + JSON_TRANSACTION(10), + GET_FBA_QUORUMSET(11), + FBA_QUORUMSET(12), + FBA_MESSAGE(13); + + private final int value; + + MessageType(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static MessageType 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) { + case 0: return ERROR_MSG; + case 1: return HELLO; + case 2: return DONT_HAVE; + case 3: return GET_PEERS; + case 4: return PEERS; + case 5: return GET_TX_SET; + case 6: return TX_SET; + case 7: return GET_VALIDATIONS; + case 8: return VALIDATIONS; + case 9: return TRANSACTION; + case 10: return JSON_TRANSACTION; + case 11: return GET_FBA_QUORUMSET; + case 12: return FBA_QUORUMSET; + case 13: return FBA_MESSAGE; + default: + throw new IllegalArgumentException("Unknown enum value: " + value); + } + } + + public static MessageType decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(value); + } + public static MessageType fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static MessageType fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/enum/XdrDataInputStream.java b/xdr-generator/test/snapshots/enum/XdrDataInputStream.java new file mode 100644 index 000000000..0dd3098d9 --- /dev/null +++ b/xdr-generator/test/snapshots/enum/XdrDataInputStream.java @@ -0,0 +1,226 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/enum/XdrDataOutputStream.java b/xdr-generator/test/snapshots/enum/XdrDataOutputStream.java new file mode 100644 index 000000000..9bb857a64 --- /dev/null +++ b/xdr-generator/test/snapshots/enum/XdrDataOutputStream.java @@ -0,0 +1,96 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/enum/XdrElement.java b/xdr-generator/test/snapshots/enum/XdrElement.java new file mode 100644 index 000000000..fba637542 --- /dev/null +++ b/xdr-generator/test/snapshots/enum/XdrElement.java @@ -0,0 +1,21 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/enum/XdrString.java b/xdr-generator/test/snapshots/enum/XdrString.java new file mode 100644 index 000000000..f0a0c478a --- /dev/null +++ b/xdr-generator/test/snapshots/enum/XdrString.java @@ -0,0 +1,78 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/enum/XdrUnsignedHyperInteger.java b/xdr-generator/test/snapshots/enum/XdrUnsignedHyperInteger.java new file mode 100644 index 000000000..82a21dcab --- /dev/null +++ b/xdr-generator/test/snapshots/enum/XdrUnsignedHyperInteger.java @@ -0,0 +1,74 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/enum/XdrUnsignedInteger.java b/xdr-generator/test/snapshots/enum/XdrUnsignedInteger.java new file mode 100644 index 000000000..d5a79959d --- /dev/null +++ b/xdr-generator/test/snapshots/enum/XdrUnsignedInteger.java @@ -0,0 +1,62 @@ +package org.stellar.sdk.xdr; + +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); + } +} diff --git a/xdr-generator/test/snapshots/nesting/Constants.java b/xdr-generator/test/snapshots/nesting/Constants.java new file mode 100644 index 000000000..3bb7224f8 --- /dev/null +++ b/xdr-generator/test/snapshots/nesting/Constants.java @@ -0,0 +1,10 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +public final class Constants { + private Constants() {} +} diff --git a/xdr-generator/test/snapshots/nesting/Foo.java b/xdr-generator/test/snapshots/nesting/Foo.java new file mode 100644 index 000000000..238e80d08 --- /dev/null +++ b/xdr-generator/test/snapshots/nesting/Foo.java @@ -0,0 +1,54 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Foo's original definition in the XDR file is: + *
+ * typedef int Foo;
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Foo implements XdrElement { + private Integer Foo; + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(Foo); + } + + public static Foo decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + Foo decodedFoo = new Foo(); + decodedFoo.Foo = stream.readInt(); + return decodedFoo; + } + public static Foo decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static Foo fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Foo fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/nesting/MyUnion.java b/xdr-generator/test/snapshots/nesting/MyUnion.java new file mode 100644 index 000000000..b8c6cba18 --- /dev/null +++ b/xdr-generator/test/snapshots/nesting/MyUnion.java @@ -0,0 +1,184 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Builder; + +/** + * MyUnion's original definition in the XDR file is: + *
+ * union MyUnion switch (UnionKey type)
+ * {
+ *     case ONE:
+ *         struct {
+ *             int someInt;
+ *         } one;
+ * 
+ *     case TWO:
+ *         struct {
+ *             int someInt;
+ *             Foo foo;
+ *         } two;
+ * 
+ *     case OFFER:
+ *         void;
+ * };
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) +public class MyUnion implements XdrElement { + private UnionKey discriminant; + private MyUnionOne one; + private MyUnionTwo two; + + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(discriminant.getValue()); + switch (discriminant) { + case ONE: + one.encode(stream); + break; + case TWO: + two.encode(stream); + break; + case OFFER: + break; + } + } + public static MyUnion decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + MyUnion decodedMyUnion = new MyUnion(); + UnionKey discriminant = UnionKey.decode(stream, maxDepth); + decodedMyUnion.setDiscriminant(discriminant); + switch (decodedMyUnion.getDiscriminant()) { + case ONE: + decodedMyUnion.one = MyUnionOne.decode(stream, maxDepth); + break; + case TWO: + decodedMyUnion.two = MyUnionTwo.decode(stream, maxDepth); + break; + case OFFER: + break; + default: + throw new IOException("Unknown discriminant value: " + discriminant); + } + return decodedMyUnion; + } + public static MyUnion decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + public static MyUnion fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static MyUnion fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } + + /** + * MyUnionOne's original definition in the XDR file is: + *
+   * struct {
+   *             int someInt;
+   *         }
+   * 
+ */ + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder(toBuilder = true) + public static class MyUnionOne implements XdrElement { + private Integer someInt; + public void encode(XdrDataOutputStream stream) throws IOException{ + stream.writeInt(someInt); + } + public static MyUnionOne decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + MyUnionOne decodedMyUnionOne = new MyUnionOne(); + decodedMyUnionOne.someInt = stream.readInt(); + return decodedMyUnionOne; + } + public static MyUnionOne decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + public static MyUnionOne fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static MyUnionOne fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } + + } + /** + * MyUnionTwo's original definition in the XDR file is: + *
+   * struct {
+   *             int someInt;
+   *             Foo foo;
+   *         }
+   * 
+ */ + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder(toBuilder = true) + public static class MyUnionTwo implements XdrElement { + private Integer someInt; + private Foo foo; + public void encode(XdrDataOutputStream stream) throws IOException{ + stream.writeInt(someInt); + foo.encode(stream); + } + public static MyUnionTwo decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + MyUnionTwo decodedMyUnionTwo = new MyUnionTwo(); + decodedMyUnionTwo.someInt = stream.readInt(); + decodedMyUnionTwo.foo = Foo.decode(stream, maxDepth); + return decodedMyUnionTwo; + } + public static MyUnionTwo decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + public static MyUnionTwo fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static MyUnionTwo fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } + + } +} diff --git a/xdr-generator/test/snapshots/nesting/UnionKey.java b/xdr-generator/test/snapshots/nesting/UnionKey.java new file mode 100644 index 000000000..48da38380 --- /dev/null +++ b/xdr-generator/test/snapshots/nesting/UnionKey.java @@ -0,0 +1,67 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +/** + * UnionKey's original definition in the XDR file is: + *
+ * enum UnionKey {
+ *   ONE = 1,
+ *   TWO = 2,
+ *   OFFER = 3
+ * };
+ * 
+ */ +public enum UnionKey implements XdrElement { + ONE(1), + TWO(2), + OFFER(3); + + private final int value; + + UnionKey(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static UnionKey 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) { + case 1: return ONE; + case 2: return TWO; + case 3: return OFFER; + default: + throw new IllegalArgumentException("Unknown enum value: " + value); + } + } + + public static UnionKey decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(value); + } + public static UnionKey fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static UnionKey fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/nesting/XdrDataInputStream.java b/xdr-generator/test/snapshots/nesting/XdrDataInputStream.java new file mode 100644 index 000000000..0dd3098d9 --- /dev/null +++ b/xdr-generator/test/snapshots/nesting/XdrDataInputStream.java @@ -0,0 +1,226 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/nesting/XdrDataOutputStream.java b/xdr-generator/test/snapshots/nesting/XdrDataOutputStream.java new file mode 100644 index 000000000..9bb857a64 --- /dev/null +++ b/xdr-generator/test/snapshots/nesting/XdrDataOutputStream.java @@ -0,0 +1,96 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/nesting/XdrElement.java b/xdr-generator/test/snapshots/nesting/XdrElement.java new file mode 100644 index 000000000..fba637542 --- /dev/null +++ b/xdr-generator/test/snapshots/nesting/XdrElement.java @@ -0,0 +1,21 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/nesting/XdrString.java b/xdr-generator/test/snapshots/nesting/XdrString.java new file mode 100644 index 000000000..f0a0c478a --- /dev/null +++ b/xdr-generator/test/snapshots/nesting/XdrString.java @@ -0,0 +1,78 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/nesting/XdrUnsignedHyperInteger.java b/xdr-generator/test/snapshots/nesting/XdrUnsignedHyperInteger.java new file mode 100644 index 000000000..82a21dcab --- /dev/null +++ b/xdr-generator/test/snapshots/nesting/XdrUnsignedHyperInteger.java @@ -0,0 +1,74 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/nesting/XdrUnsignedInteger.java b/xdr-generator/test/snapshots/nesting/XdrUnsignedInteger.java new file mode 100644 index 000000000..d5a79959d --- /dev/null +++ b/xdr-generator/test/snapshots/nesting/XdrUnsignedInteger.java @@ -0,0 +1,62 @@ +package org.stellar.sdk.xdr; + +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); + } +} diff --git a/xdr-generator/test/snapshots/optional/Arr.java b/xdr-generator/test/snapshots/optional/Arr.java new file mode 100644 index 000000000..8ba42e404 --- /dev/null +++ b/xdr-generator/test/snapshots/optional/Arr.java @@ -0,0 +1,64 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Arr's original definition in the XDR file is: + *
+ * typedef int Arr[2];
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Arr implements XdrElement { + private Integer[] Arr; + public void encode(XdrDataOutputStream stream) throws IOException { + int ArrSize = getArr().length; + if (ArrSize != 2) { + throw new IOException("Arr size " + ArrSize + " does not match fixed size 2"); + } + for (int i = 0; i < ArrSize; i++) { + stream.writeInt(Arr[i]); + } + } + + public static Arr decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + Arr decodedArr = new Arr(); + int ArrSize = 2; + decodedArr.Arr = new Integer[ArrSize]; + for (int i = 0; i < ArrSize; i++) { + decodedArr.Arr[i] = stream.readInt(); + } + return decodedArr; + } + public static Arr decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static Arr fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Arr fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/optional/Constants.java b/xdr-generator/test/snapshots/optional/Constants.java new file mode 100644 index 000000000..3bb7224f8 --- /dev/null +++ b/xdr-generator/test/snapshots/optional/Constants.java @@ -0,0 +1,10 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +public final class Constants { + private Constants() {} +} diff --git a/xdr-generator/test/snapshots/optional/HasOptions.java b/xdr-generator/test/snapshots/optional/HasOptions.java new file mode 100644 index 000000000..c69c3d022 --- /dev/null +++ b/xdr-generator/test/snapshots/optional/HasOptions.java @@ -0,0 +1,89 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Builder; + +/** + * HasOptions's original definition in the XDR file is: + *
+ * struct HasOptions
+ * {
+ *   int* firstOption;
+ *   int *secondOption;
+ *   Arr *thirdOption;
+ * };
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) +public class HasOptions implements XdrElement { + private Integer firstOption; + private Integer secondOption; + private Arr thirdOption; + public void encode(XdrDataOutputStream stream) throws IOException{ + if (firstOption != null) { + stream.writeInt(1); + stream.writeInt(firstOption); + } else { + stream.writeInt(0); + } + if (secondOption != null) { + stream.writeInt(1); + stream.writeInt(secondOption); + } else { + stream.writeInt(0); + } + if (thirdOption != null) { + stream.writeInt(1); + thirdOption.encode(stream); + } else { + stream.writeInt(0); + } + } + public static HasOptions decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + HasOptions decodedHasOptions = new HasOptions(); + boolean firstOptionPresent = stream.readXdrBoolean(); + if (firstOptionPresent) { + decodedHasOptions.firstOption = stream.readInt(); + } + boolean secondOptionPresent = stream.readXdrBoolean(); + if (secondOptionPresent) { + decodedHasOptions.secondOption = stream.readInt(); + } + boolean thirdOptionPresent = stream.readXdrBoolean(); + if (thirdOptionPresent) { + decodedHasOptions.thirdOption = Arr.decode(stream, maxDepth); + } + return decodedHasOptions; + } + public static HasOptions decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + public static HasOptions fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static HasOptions fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/optional/XdrDataInputStream.java b/xdr-generator/test/snapshots/optional/XdrDataInputStream.java new file mode 100644 index 000000000..0dd3098d9 --- /dev/null +++ b/xdr-generator/test/snapshots/optional/XdrDataInputStream.java @@ -0,0 +1,226 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/optional/XdrDataOutputStream.java b/xdr-generator/test/snapshots/optional/XdrDataOutputStream.java new file mode 100644 index 000000000..9bb857a64 --- /dev/null +++ b/xdr-generator/test/snapshots/optional/XdrDataOutputStream.java @@ -0,0 +1,96 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/optional/XdrElement.java b/xdr-generator/test/snapshots/optional/XdrElement.java new file mode 100644 index 000000000..fba637542 --- /dev/null +++ b/xdr-generator/test/snapshots/optional/XdrElement.java @@ -0,0 +1,21 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/optional/XdrString.java b/xdr-generator/test/snapshots/optional/XdrString.java new file mode 100644 index 000000000..f0a0c478a --- /dev/null +++ b/xdr-generator/test/snapshots/optional/XdrString.java @@ -0,0 +1,78 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/optional/XdrUnsignedHyperInteger.java b/xdr-generator/test/snapshots/optional/XdrUnsignedHyperInteger.java new file mode 100644 index 000000000..82a21dcab --- /dev/null +++ b/xdr-generator/test/snapshots/optional/XdrUnsignedHyperInteger.java @@ -0,0 +1,74 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/optional/XdrUnsignedInteger.java b/xdr-generator/test/snapshots/optional/XdrUnsignedInteger.java new file mode 100644 index 000000000..d5a79959d --- /dev/null +++ b/xdr-generator/test/snapshots/optional/XdrUnsignedInteger.java @@ -0,0 +1,62 @@ +package org.stellar.sdk.xdr; + +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); + } +} diff --git a/xdr-generator/test/snapshots/struct/Constants.java b/xdr-generator/test/snapshots/struct/Constants.java new file mode 100644 index 000000000..3bb7224f8 --- /dev/null +++ b/xdr-generator/test/snapshots/struct/Constants.java @@ -0,0 +1,10 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +public final class Constants { + private Constants() {} +} diff --git a/xdr-generator/test/snapshots/struct/Int64.java b/xdr-generator/test/snapshots/struct/Int64.java new file mode 100644 index 000000000..ac278b74e --- /dev/null +++ b/xdr-generator/test/snapshots/struct/Int64.java @@ -0,0 +1,54 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Int64's original definition in the XDR file is: + *
+ * typedef hyper int64;
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Int64 implements XdrElement { + private Long int64; + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeLong(int64); + } + + public static Int64 decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + Int64 decodedInt64 = new Int64(); + decodedInt64.int64 = stream.readLong(); + return decodedInt64; + } + public static Int64 decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static Int64 fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Int64 fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/struct/MyStruct.java b/xdr-generator/test/snapshots/struct/MyStruct.java new file mode 100644 index 000000000..70f84d5c2 --- /dev/null +++ b/xdr-generator/test/snapshots/struct/MyStruct.java @@ -0,0 +1,83 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Builder; + +/** + * MyStruct's original definition in the XDR file is: + *
+ * struct MyStruct
+ * {
+ *     int    someInt;
+ *     int64  aBigInt;
+ *     opaque someOpaque[10];
+ *     string someString<>;
+ *     string maxString<100>;
+ * };
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) +public class MyStruct implements XdrElement { + private Integer someInt; + private Int64 aBigInt; + private byte[] someOpaque; + private XdrString someString; + private XdrString maxString; + public void encode(XdrDataOutputStream stream) throws IOException{ + stream.writeInt(someInt); + aBigInt.encode(stream); + int someOpaqueSize = someOpaque.length; + if (someOpaqueSize != 10) { + throw new IOException("someOpaque size " + someOpaqueSize + " does not match fixed size 10"); + } + stream.write(getSomeOpaque(), 0, someOpaqueSize); + someString.encode(stream); + int maxStringSize = maxString.getBytes().length; + if (maxStringSize > 100) { + throw new IOException("maxString size " + maxStringSize + " exceeds max size 100"); + } + maxString.encode(stream); + } + public static MyStruct decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + MyStruct decodedMyStruct = new MyStruct(); + decodedMyStruct.someInt = stream.readInt(); + decodedMyStruct.aBigInt = Int64.decode(stream, maxDepth); + int someOpaqueSize = 10; + decodedMyStruct.someOpaque = new byte[someOpaqueSize]; + stream.readPaddedData(decodedMyStruct.someOpaque, 0, someOpaqueSize); + decodedMyStruct.someString = XdrString.decode(stream, maxDepth, Integer.MAX_VALUE); + decodedMyStruct.maxString = XdrString.decode(stream, maxDepth, 100); + return decodedMyStruct; + } + public static MyStruct decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + public static MyStruct fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static MyStruct fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/struct/XdrDataInputStream.java b/xdr-generator/test/snapshots/struct/XdrDataInputStream.java new file mode 100644 index 000000000..0dd3098d9 --- /dev/null +++ b/xdr-generator/test/snapshots/struct/XdrDataInputStream.java @@ -0,0 +1,226 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/struct/XdrDataOutputStream.java b/xdr-generator/test/snapshots/struct/XdrDataOutputStream.java new file mode 100644 index 000000000..9bb857a64 --- /dev/null +++ b/xdr-generator/test/snapshots/struct/XdrDataOutputStream.java @@ -0,0 +1,96 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/struct/XdrElement.java b/xdr-generator/test/snapshots/struct/XdrElement.java new file mode 100644 index 000000000..fba637542 --- /dev/null +++ b/xdr-generator/test/snapshots/struct/XdrElement.java @@ -0,0 +1,21 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/struct/XdrString.java b/xdr-generator/test/snapshots/struct/XdrString.java new file mode 100644 index 000000000..f0a0c478a --- /dev/null +++ b/xdr-generator/test/snapshots/struct/XdrString.java @@ -0,0 +1,78 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/struct/XdrUnsignedHyperInteger.java b/xdr-generator/test/snapshots/struct/XdrUnsignedHyperInteger.java new file mode 100644 index 000000000..82a21dcab --- /dev/null +++ b/xdr-generator/test/snapshots/struct/XdrUnsignedHyperInteger.java @@ -0,0 +1,74 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/struct/XdrUnsignedInteger.java b/xdr-generator/test/snapshots/struct/XdrUnsignedInteger.java new file mode 100644 index 000000000..d5a79959d --- /dev/null +++ b/xdr-generator/test/snapshots/struct/XdrUnsignedInteger.java @@ -0,0 +1,62 @@ +package org.stellar.sdk.xdr; + +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); + } +} diff --git a/xdr-generator/test/snapshots/test/Color.java b/xdr-generator/test/snapshots/test/Color.java new file mode 100644 index 000000000..dd4a49b30 --- /dev/null +++ b/xdr-generator/test/snapshots/test/Color.java @@ -0,0 +1,67 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +/** + * Color's original definition in the XDR file is: + *
+ * enum Color {
+ *   RED,
+ *   BLUE = 5,
+ *   GREEN
+ * };
+ * 
+ */ +public enum Color implements XdrElement { + RED(0), + BLUE(5), + GREEN(6); + + private final int value; + + Color(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static Color 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) { + case 0: return RED; + case 5: return BLUE; + case 6: return GREEN; + default: + throw new IllegalArgumentException("Unknown enum value: " + value); + } + } + + public static Color decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(value); + } + public static Color fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Color fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/test/Constants.java b/xdr-generator/test/snapshots/test/Constants.java new file mode 100644 index 000000000..3b453493a --- /dev/null +++ b/xdr-generator/test/snapshots/test/Constants.java @@ -0,0 +1,12 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +public final class Constants { + private Constants() {} + public static final int BAR = FOO; + public static final int FOO = 1244; +} diff --git a/xdr-generator/test/snapshots/test/HasStuff.java b/xdr-generator/test/snapshots/test/HasStuff.java new file mode 100644 index 000000000..b45485ae1 --- /dev/null +++ b/xdr-generator/test/snapshots/test/HasStuff.java @@ -0,0 +1,57 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Builder; + +/** + * HasStuff's original definition in the XDR file is: + *
+ * struct HasStuff
+ * {
+ *   LotsOfMyStructs data;
+ * };
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) +public class HasStuff implements XdrElement { + private LotsOfMyStructs data; + public void encode(XdrDataOutputStream stream) throws IOException{ + data.encode(stream); + } + public static HasStuff decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + HasStuff decodedHasStuff = new HasStuff(); + decodedHasStuff.data = LotsOfMyStructs.decode(stream, maxDepth); + return decodedHasStuff; + } + public static HasStuff decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + public static HasStuff fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static HasStuff fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/test/Hash.java b/xdr-generator/test/snapshots/test/Hash.java new file mode 100644 index 000000000..369a341cf --- /dev/null +++ b/xdr-generator/test/snapshots/test/Hash.java @@ -0,0 +1,60 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Hash's original definition in the XDR file is: + *
+ * typedef opaque Hash[32];
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Hash implements XdrElement { + private byte[] Hash; + public void encode(XdrDataOutputStream stream) throws IOException { + int HashSize = Hash.length; + if (HashSize != 32) { + throw new IOException("Hash size " + HashSize + " does not match fixed size 32"); + } + stream.write(getHash(), 0, HashSize); + } + + public static Hash decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + Hash decodedHash = new Hash(); + int HashSize = 32; + decodedHash.Hash = new byte[HashSize]; + stream.readPaddedData(decodedHash.Hash, 0, HashSize); + return decodedHash; + } + public static Hash decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static Hash fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Hash fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/test/Hashes1.java b/xdr-generator/test/snapshots/test/Hashes1.java new file mode 100644 index 000000000..480cf6625 --- /dev/null +++ b/xdr-generator/test/snapshots/test/Hashes1.java @@ -0,0 +1,64 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Hashes1's original definition in the XDR file is: + *
+ * typedef Hash Hashes1[12];
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Hashes1 implements XdrElement { + private Hash[] Hashes1; + public void encode(XdrDataOutputStream stream) throws IOException { + int Hashes1Size = getHashes1().length; + if (Hashes1Size != 12) { + throw new IOException("Hashes1 size " + Hashes1Size + " does not match fixed size 12"); + } + for (int i = 0; i < Hashes1Size; i++) { + Hashes1[i].encode(stream); + } + } + + public static Hashes1 decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + Hashes1 decodedHashes1 = new Hashes1(); + int Hashes1Size = 12; + decodedHashes1.Hashes1 = new Hash[Hashes1Size]; + for (int i = 0; i < Hashes1Size; i++) { + decodedHashes1.Hashes1[i] = Hash.decode(stream, maxDepth); + } + return decodedHashes1; + } + public static Hashes1 decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static Hashes1 fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Hashes1 fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/test/Hashes2.java b/xdr-generator/test/snapshots/test/Hashes2.java new file mode 100644 index 000000000..de53fe61d --- /dev/null +++ b/xdr-generator/test/snapshots/test/Hashes2.java @@ -0,0 +1,75 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Hashes2's original definition in the XDR file is: + *
+ * typedef Hash Hashes2<12>;
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Hashes2 implements XdrElement { + private Hash[] Hashes2; + public void encode(XdrDataOutputStream stream) throws IOException { + int Hashes2Size = getHashes2().length; + if (Hashes2Size > 12) { + throw new IOException("Hashes2 size " + Hashes2Size + " exceeds max size 12"); + } + stream.writeInt(Hashes2Size); + for (int i = 0; i < Hashes2Size; i++) { + Hashes2[i].encode(stream); + } + } + + public static Hashes2 decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + Hashes2 decodedHashes2 = new Hashes2(); + int Hashes2Size = stream.readInt(); + if (Hashes2Size < 0) { + throw new IOException("Hashes2 size " + Hashes2Size + " is negative"); + } + if (Hashes2Size > 12) { + throw new IOException("Hashes2 size " + Hashes2Size + " exceeds max size 12"); + } + int Hashes2RemainingInputLen = stream.getRemainingInputLen(); + if (Hashes2RemainingInputLen >= 0 && Hashes2RemainingInputLen < Hashes2Size) { + throw new IOException("Hashes2 size " + Hashes2Size + " exceeds remaining input length " + Hashes2RemainingInputLen); + } + decodedHashes2.Hashes2 = new Hash[Hashes2Size]; + for (int i = 0; i < Hashes2Size; i++) { + decodedHashes2.Hashes2[i] = Hash.decode(stream, maxDepth); + } + return decodedHashes2; + } + public static Hashes2 decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static Hashes2 fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Hashes2 fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/test/Hashes3.java b/xdr-generator/test/snapshots/test/Hashes3.java new file mode 100644 index 000000000..010a3b1ba --- /dev/null +++ b/xdr-generator/test/snapshots/test/Hashes3.java @@ -0,0 +1,69 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Hashes3's original definition in the XDR file is: + *
+ * typedef Hash Hashes3<>;
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Hashes3 implements XdrElement { + private Hash[] Hashes3; + public void encode(XdrDataOutputStream stream) throws IOException { + int Hashes3Size = getHashes3().length; + stream.writeInt(Hashes3Size); + for (int i = 0; i < Hashes3Size; i++) { + Hashes3[i].encode(stream); + } + } + + public static Hashes3 decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + Hashes3 decodedHashes3 = new Hashes3(); + int Hashes3Size = stream.readInt(); + if (Hashes3Size < 0) { + throw new IOException("Hashes3 size " + Hashes3Size + " is negative"); + } + int Hashes3RemainingInputLen = stream.getRemainingInputLen(); + if (Hashes3RemainingInputLen >= 0 && Hashes3RemainingInputLen < Hashes3Size) { + throw new IOException("Hashes3 size " + Hashes3Size + " exceeds remaining input length " + Hashes3RemainingInputLen); + } + decodedHashes3.Hashes3 = new Hash[Hashes3Size]; + for (int i = 0; i < Hashes3Size; i++) { + decodedHashes3.Hashes3[i] = Hash.decode(stream, maxDepth); + } + return decodedHashes3; + } + public static Hashes3 decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static Hashes3 fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Hashes3 fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/test/Int1.java b/xdr-generator/test/snapshots/test/Int1.java new file mode 100644 index 000000000..34bd2b703 --- /dev/null +++ b/xdr-generator/test/snapshots/test/Int1.java @@ -0,0 +1,54 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Int1's original definition in the XDR file is: + *
+ * typedef int             int1;
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Int1 implements XdrElement { + private Integer int1; + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(int1); + } + + public static Int1 decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + Int1 decodedInt1 = new Int1(); + decodedInt1.int1 = stream.readInt(); + return decodedInt1; + } + public static Int1 decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static Int1 fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Int1 fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/test/Int2.java b/xdr-generator/test/snapshots/test/Int2.java new file mode 100644 index 000000000..34a768eac --- /dev/null +++ b/xdr-generator/test/snapshots/test/Int2.java @@ -0,0 +1,54 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Int2's original definition in the XDR file is: + *
+ * typedef hyper           int2;
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Int2 implements XdrElement { + private Long int2; + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeLong(int2); + } + + public static Int2 decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + Int2 decodedInt2 = new Int2(); + decodedInt2.int2 = stream.readLong(); + return decodedInt2; + } + public static Int2 decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static Int2 fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Int2 fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/test/Int3.java b/xdr-generator/test/snapshots/test/Int3.java new file mode 100644 index 000000000..d55e5a3fd --- /dev/null +++ b/xdr-generator/test/snapshots/test/Int3.java @@ -0,0 +1,54 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Int3's original definition in the XDR file is: + *
+ * typedef unsigned int    int3;
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Int3 implements XdrElement { + private XdrUnsignedInteger int3; + public void encode(XdrDataOutputStream stream) throws IOException { + int3.encode(stream); + } + + public static Int3 decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + Int3 decodedInt3 = new Int3(); + decodedInt3.int3 = XdrUnsignedInteger.decode(stream, maxDepth); + return decodedInt3; + } + public static Int3 decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static Int3 fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Int3 fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/test/Int4.java b/xdr-generator/test/snapshots/test/Int4.java new file mode 100644 index 000000000..b3d3fd678 --- /dev/null +++ b/xdr-generator/test/snapshots/test/Int4.java @@ -0,0 +1,54 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Int4's original definition in the XDR file is: + *
+ * typedef unsigned hyper  int4;
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Int4 implements XdrElement { + private XdrUnsignedHyperInteger int4; + public void encode(XdrDataOutputStream stream) throws IOException { + int4.encode(stream); + } + + public static Int4 decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + Int4 decodedInt4 = new Int4(); + decodedInt4.int4 = XdrUnsignedHyperInteger.decode(stream, maxDepth); + return decodedInt4; + } + public static Int4 decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static Int4 fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Int4 fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/test/LotsOfMyStructs.java b/xdr-generator/test/snapshots/test/LotsOfMyStructs.java new file mode 100644 index 000000000..ab074600e --- /dev/null +++ b/xdr-generator/test/snapshots/test/LotsOfMyStructs.java @@ -0,0 +1,72 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Builder; + +/** + * LotsOfMyStructs's original definition in the XDR file is: + *
+ * struct LotsOfMyStructs
+ * {
+ *     MyStruct members<>;
+ * };
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) +public class LotsOfMyStructs implements XdrElement { + private MyStruct[] members; + public void encode(XdrDataOutputStream stream) throws IOException{ + int membersSize = getMembers().length; + stream.writeInt(membersSize); + for (int i = 0; i < membersSize; i++) { + members[i].encode(stream); + } + } + public static LotsOfMyStructs decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + LotsOfMyStructs decodedLotsOfMyStructs = new LotsOfMyStructs(); + int membersSize = stream.readInt(); + if (membersSize < 0) { + throw new IOException("members size " + membersSize + " is negative"); + } + int membersRemainingInputLen = stream.getRemainingInputLen(); + if (membersRemainingInputLen >= 0 && membersRemainingInputLen < membersSize) { + throw new IOException("members size " + membersSize + " exceeds remaining input length " + membersRemainingInputLen); + } + decodedLotsOfMyStructs.members = new MyStruct[membersSize]; + for (int i = 0; i < membersSize; i++) { + decodedLotsOfMyStructs.members[i] = MyStruct.decode(stream, maxDepth); + } + return decodedLotsOfMyStructs; + } + public static LotsOfMyStructs decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + public static LotsOfMyStructs fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static LotsOfMyStructs fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/test/MyStruct.java b/xdr-generator/test/snapshots/test/MyStruct.java new file mode 100644 index 000000000..536a52b4d --- /dev/null +++ b/xdr-generator/test/snapshots/test/MyStruct.java @@ -0,0 +1,81 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Builder; + +/** + * MyStruct's original definition in the XDR file is: + *
+ * struct MyStruct
+ * {
+ *     uint512 field1;
+ *     optHash1 field2;
+ *     int1 field3;
+ *     unsigned int field4;
+ *     float field5;
+ *     double field6;
+ *     bool field7;
+ * };
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) +public class MyStruct implements XdrElement { + private Uint512 field1; + private OptHash1 field2; + private Int1 field3; + private XdrUnsignedInteger field4; + private Float field5; + private Double field6; + private Boolean field7; + public void encode(XdrDataOutputStream stream) throws IOException{ + field1.encode(stream); + field2.encode(stream); + field3.encode(stream); + field4.encode(stream); + stream.writeFloat(field5); + stream.writeDouble(field6); + stream.writeInt(field7 ? 1 : 0); + } + public static MyStruct decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + MyStruct decodedMyStruct = new MyStruct(); + decodedMyStruct.field1 = Uint512.decode(stream, maxDepth); + decodedMyStruct.field2 = OptHash1.decode(stream, maxDepth); + decodedMyStruct.field3 = Int1.decode(stream, maxDepth); + decodedMyStruct.field4 = XdrUnsignedInteger.decode(stream, maxDepth); + decodedMyStruct.field5 = stream.readFloat(); + decodedMyStruct.field6 = stream.readDouble(); + decodedMyStruct.field7 = stream.readXdrBoolean(); + return decodedMyStruct; + } + public static MyStruct decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + public static MyStruct fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static MyStruct fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/test/Nester.java b/xdr-generator/test/snapshots/test/Nester.java new file mode 100644 index 000000000..4c2b04426 --- /dev/null +++ b/xdr-generator/test/snapshots/test/Nester.java @@ -0,0 +1,238 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Builder; + +/** + * Nester's original definition in the XDR file is: + *
+ * struct Nester
+ * {
+ *   enum {
+ *     BLAH_1,
+ *     BLAH_2
+ *   } nestedEnum;
+ * 
+ *   struct {
+ *     int blah;
+ *   } nestedStruct;
+ * 
+ *   union switch (Color color) {
+ *     case RED:
+ *       void;
+ *     default:
+ *       int blah2;
+ *   } nestedUnion;
+ * 
+ * 
+ * };
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) +public class Nester implements XdrElement { + private NesterNestedEnum nestedEnum; + private NesterNestedStruct nestedStruct; + private NesterNestedUnion nestedUnion; + public void encode(XdrDataOutputStream stream) throws IOException{ + nestedEnum.encode(stream); + nestedStruct.encode(stream); + nestedUnion.encode(stream); + } + public static Nester decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + Nester decodedNester = new Nester(); + decodedNester.nestedEnum = NesterNestedEnum.decode(stream, maxDepth); + decodedNester.nestedStruct = NesterNestedStruct.decode(stream, maxDepth); + decodedNester.nestedUnion = NesterNestedUnion.decode(stream, maxDepth); + return decodedNester; + } + public static Nester decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + public static Nester fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Nester fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } + + /** + * NesterNestedEnum's original definition in the XDR file is: + *
+   * enum {
+   *     BLAH_1,
+   *     BLAH_2
+   *   }
+   * 
+ */ + public static enum NesterNestedEnum implements XdrElement { + BLAH_1(0), + BLAH_2(1); + + private final int value; + + NestedEnum(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static NestedEnum 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) { + case 0: return BLAH_1; + case 1: return BLAH_2; + default: + throw new IllegalArgumentException("Unknown enum value: " + value); + } + } + + public static NestedEnum decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(value); + } + public static NestedEnum fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static NestedEnum fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } + + } + /** + * NesterNestedStruct's original definition in the XDR file is: + *
+   * struct {
+   *     int blah;
+   *   }
+   * 
+ */ + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder(toBuilder = true) + public static class NesterNestedStruct implements XdrElement { + private Integer blah; + public void encode(XdrDataOutputStream stream) throws IOException{ + stream.writeInt(blah); + } + public static NesterNestedStruct decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + NesterNestedStruct decodedNesterNestedStruct = new NesterNestedStruct(); + decodedNesterNestedStruct.blah = stream.readInt(); + return decodedNesterNestedStruct; + } + public static NesterNestedStruct decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + public static NesterNestedStruct fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static NesterNestedStruct fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } + + } + /** + * NesterNestedUnion's original definition in the XDR file is: + *
+   * union switch (Color color) {
+   *     case RED:
+   *       void;
+   *     default:
+   *       int blah2;
+   *   }
+   * 
+ */ + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder(toBuilder = true) + public static class NesterNestedUnion implements XdrElement { + private Color discriminant; + private Integer blah2; + + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(discriminant.getValue()); + switch (discriminant) { + case RED: + break; + default: + stream.writeInt(blah2); + break; + } + } + public static NesterNestedUnion decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + NesterNestedUnion decodedNesterNestedUnion = new NesterNestedUnion(); + Color discriminant = Color.decode(stream, maxDepth); + decodedNesterNestedUnion.setDiscriminant(discriminant); + switch (decodedNesterNestedUnion.getDiscriminant()) { + case RED: + break; + default: + decodedNesterNestedUnion.blah2 = stream.readInt(); + break; + } + return decodedNesterNestedUnion; + } + public static NesterNestedUnion decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + public static NesterNestedUnion fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static NesterNestedUnion fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } + + } +} diff --git a/xdr-generator/test/snapshots/test/OptHash1.java b/xdr-generator/test/snapshots/test/OptHash1.java new file mode 100644 index 000000000..163e062d7 --- /dev/null +++ b/xdr-generator/test/snapshots/test/OptHash1.java @@ -0,0 +1,62 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * OptHash1's original definition in the XDR file is: + *
+ * typedef Hash *optHash1;
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OptHash1 implements XdrElement { + private Hash optHash1; + public void encode(XdrDataOutputStream stream) throws IOException { + if (optHash1 != null) { + stream.writeInt(1); + optHash1.encode(stream); + } else { + stream.writeInt(0); + } + } + + public static OptHash1 decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + OptHash1 decodedOptHash1 = new OptHash1(); + boolean optHash1Present = stream.readXdrBoolean(); + if (optHash1Present) { + decodedOptHash1.optHash1 = Hash.decode(stream, maxDepth); + } + return decodedOptHash1; + } + public static OptHash1 decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static OptHash1 fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static OptHash1 fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/test/OptHash2.java b/xdr-generator/test/snapshots/test/OptHash2.java new file mode 100644 index 000000000..05c36d889 --- /dev/null +++ b/xdr-generator/test/snapshots/test/OptHash2.java @@ -0,0 +1,62 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * OptHash2's original definition in the XDR file is: + *
+ * typedef Hash* optHash2;
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OptHash2 implements XdrElement { + private Hash optHash2; + public void encode(XdrDataOutputStream stream) throws IOException { + if (optHash2 != null) { + stream.writeInt(1); + optHash2.encode(stream); + } else { + stream.writeInt(0); + } + } + + public static OptHash2 decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + OptHash2 decodedOptHash2 = new OptHash2(); + boolean optHash2Present = stream.readXdrBoolean(); + if (optHash2Present) { + decodedOptHash2.optHash2 = Hash.decode(stream, maxDepth); + } + return decodedOptHash2; + } + public static OptHash2 decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static OptHash2 fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static OptHash2 fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/test/Str.java b/xdr-generator/test/snapshots/test/Str.java new file mode 100644 index 000000000..7200a8bf3 --- /dev/null +++ b/xdr-generator/test/snapshots/test/Str.java @@ -0,0 +1,58 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Str's original definition in the XDR file is: + *
+ * typedef string str<64>;
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Str implements XdrElement { + private XdrString str; + public void encode(XdrDataOutputStream stream) throws IOException { + int strSize = str.getBytes().length; + if (strSize > 64) { + throw new IOException("str size " + strSize + " exceeds max size 64"); + } + str.encode(stream); + } + + public static Str decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + Str decodedStr = new Str(); + decodedStr.str = XdrString.decode(stream, maxDepth, 64); + return decodedStr; + } + public static Str decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static Str fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Str fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/test/Str2.java b/xdr-generator/test/snapshots/test/Str2.java new file mode 100644 index 000000000..ffaef6e8a --- /dev/null +++ b/xdr-generator/test/snapshots/test/Str2.java @@ -0,0 +1,54 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Str2's original definition in the XDR file is: + *
+ * typedef string str2<>;
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Str2 implements XdrElement { + private XdrString str2; + public void encode(XdrDataOutputStream stream) throws IOException { + str2.encode(stream); + } + + public static Str2 decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + Str2 decodedStr2 = new Str2(); + decodedStr2.str2 = XdrString.decode(stream, maxDepth, Integer.MAX_VALUE); + return decodedStr2; + } + public static Str2 decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static Str2 fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Str2 fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/test/Uint512.java b/xdr-generator/test/snapshots/test/Uint512.java new file mode 100644 index 000000000..8c02ce4fb --- /dev/null +++ b/xdr-generator/test/snapshots/test/Uint512.java @@ -0,0 +1,60 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Uint512's original definition in the XDR file is: + *
+ * typedef opaque uint512[64];
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Uint512 implements XdrElement { + private byte[] uint512; + public void encode(XdrDataOutputStream stream) throws IOException { + int uint512Size = uint512.length; + if (uint512Size != 64) { + throw new IOException("uint512 size " + uint512Size + " does not match fixed size 64"); + } + stream.write(getUint512(), 0, uint512Size); + } + + public static Uint512 decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + Uint512 decodedUint512 = new Uint512(); + int uint512Size = 64; + decodedUint512.uint512 = new byte[uint512Size]; + stream.readPaddedData(decodedUint512.uint512, 0, uint512Size); + return decodedUint512; + } + public static Uint512 decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static Uint512 fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Uint512 fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/test/Uint513.java b/xdr-generator/test/snapshots/test/Uint513.java new file mode 100644 index 000000000..d4a550657 --- /dev/null +++ b/xdr-generator/test/snapshots/test/Uint513.java @@ -0,0 +1,71 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Uint513's original definition in the XDR file is: + *
+ * typedef opaque uint513<64>;
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Uint513 implements XdrElement { + private byte[] uint513; + public void encode(XdrDataOutputStream stream) throws IOException { + int uint513Size = uint513.length; + if (uint513Size > 64) { + throw new IOException("uint513 size " + uint513Size + " exceeds max size 64"); + } + stream.writeInt(uint513Size); + stream.write(getUint513(), 0, uint513Size); + } + + public static Uint513 decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + Uint513 decodedUint513 = new Uint513(); + int uint513Size = stream.readInt(); + if (uint513Size < 0) { + throw new IOException("uint513 size " + uint513Size + " is negative"); + } + if (uint513Size > 64) { + throw new IOException("uint513 size " + uint513Size + " exceeds max size 64"); + } + int uint513RemainingInputLen = stream.getRemainingInputLen(); + if (uint513RemainingInputLen >= 0 && uint513RemainingInputLen < uint513Size) { + throw new IOException("uint513 size " + uint513Size + " exceeds remaining input length " + uint513RemainingInputLen); + } + decodedUint513.uint513 = new byte[uint513Size]; + stream.readPaddedData(decodedUint513.uint513, 0, uint513Size); + return decodedUint513; + } + public static Uint513 decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static Uint513 fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Uint513 fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/test/Uint514.java b/xdr-generator/test/snapshots/test/Uint514.java new file mode 100644 index 000000000..ad843a890 --- /dev/null +++ b/xdr-generator/test/snapshots/test/Uint514.java @@ -0,0 +1,65 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Uint514's original definition in the XDR file is: + *
+ * typedef opaque uint514<>;
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Uint514 implements XdrElement { + private byte[] uint514; + public void encode(XdrDataOutputStream stream) throws IOException { + int uint514Size = uint514.length; + stream.writeInt(uint514Size); + stream.write(getUint514(), 0, uint514Size); + } + + public static Uint514 decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + Uint514 decodedUint514 = new Uint514(); + int uint514Size = stream.readInt(); + if (uint514Size < 0) { + throw new IOException("uint514 size " + uint514Size + " is negative"); + } + int uint514RemainingInputLen = stream.getRemainingInputLen(); + if (uint514RemainingInputLen >= 0 && uint514RemainingInputLen < uint514Size) { + throw new IOException("uint514 size " + uint514Size + " exceeds remaining input length " + uint514RemainingInputLen); + } + decodedUint514.uint514 = new byte[uint514Size]; + stream.readPaddedData(decodedUint514.uint514, 0, uint514Size); + return decodedUint514; + } + public static Uint514 decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static Uint514 fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Uint514 fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/test/XdrDataInputStream.java b/xdr-generator/test/snapshots/test/XdrDataInputStream.java new file mode 100644 index 000000000..0dd3098d9 --- /dev/null +++ b/xdr-generator/test/snapshots/test/XdrDataInputStream.java @@ -0,0 +1,226 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/test/XdrDataOutputStream.java b/xdr-generator/test/snapshots/test/XdrDataOutputStream.java new file mode 100644 index 000000000..9bb857a64 --- /dev/null +++ b/xdr-generator/test/snapshots/test/XdrDataOutputStream.java @@ -0,0 +1,96 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/test/XdrElement.java b/xdr-generator/test/snapshots/test/XdrElement.java new file mode 100644 index 000000000..fba637542 --- /dev/null +++ b/xdr-generator/test/snapshots/test/XdrElement.java @@ -0,0 +1,21 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/test/XdrString.java b/xdr-generator/test/snapshots/test/XdrString.java new file mode 100644 index 000000000..f0a0c478a --- /dev/null +++ b/xdr-generator/test/snapshots/test/XdrString.java @@ -0,0 +1,78 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/test/XdrUnsignedHyperInteger.java b/xdr-generator/test/snapshots/test/XdrUnsignedHyperInteger.java new file mode 100644 index 000000000..82a21dcab --- /dev/null +++ b/xdr-generator/test/snapshots/test/XdrUnsignedHyperInteger.java @@ -0,0 +1,74 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/test/XdrUnsignedInteger.java b/xdr-generator/test/snapshots/test/XdrUnsignedInteger.java new file mode 100644 index 000000000..d5a79959d --- /dev/null +++ b/xdr-generator/test/snapshots/test/XdrUnsignedInteger.java @@ -0,0 +1,62 @@ +package org.stellar.sdk.xdr; + +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); + } +} diff --git a/xdr-generator/test/snapshots/union/Constants.java b/xdr-generator/test/snapshots/union/Constants.java new file mode 100644 index 000000000..3bb7224f8 --- /dev/null +++ b/xdr-generator/test/snapshots/union/Constants.java @@ -0,0 +1,10 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +public final class Constants { + private Constants() {} +} diff --git a/xdr-generator/test/snapshots/union/Error.java b/xdr-generator/test/snapshots/union/Error.java new file mode 100644 index 000000000..97adc85f5 --- /dev/null +++ b/xdr-generator/test/snapshots/union/Error.java @@ -0,0 +1,54 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Error's original definition in the XDR file is: + *
+ * typedef int Error;
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Error implements XdrElement { + private Integer Error; + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(Error); + } + + public static Error decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + Error decodedError = new Error(); + decodedError.Error = stream.readInt(); + return decodedError; + } + public static Error decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static Error fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Error fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/union/IntUnion.java b/xdr-generator/test/snapshots/union/IntUnion.java new file mode 100644 index 000000000..b7d5d6675 --- /dev/null +++ b/xdr-generator/test/snapshots/union/IntUnion.java @@ -0,0 +1,98 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Builder; + +/** + * IntUnion's original definition in the XDR file is: + *
+ * union IntUnion switch (int type)
+ * {
+ *     case 0:
+ *         Error error;
+ *     case 1:
+ *         Multi things<>;
+ * 
+ * };
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) +public class IntUnion implements XdrElement { + private Integer discriminant; + private Error error; + private Multi[] things; + + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(discriminant); + switch (discriminant) { + case 0: + error.encode(stream); + break; + case 1: + int thingsSize = getThings().length; + stream.writeInt(thingsSize); + for (int i = 0; i < thingsSize; i++) { + things[i].encode(stream); + } + break; + } + } + public static IntUnion decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + IntUnion decodedIntUnion = new IntUnion(); + Integer discriminant = stream.readInt(); + decodedIntUnion.setDiscriminant(discriminant); + switch (decodedIntUnion.getDiscriminant()) { + case 0: + decodedIntUnion.error = Error.decode(stream, maxDepth); + break; + case 1: + int thingsSize = stream.readInt(); + if (thingsSize < 0) { + throw new IOException("things size " + thingsSize + " is negative"); + } + int thingsRemainingInputLen = stream.getRemainingInputLen(); + if (thingsRemainingInputLen >= 0 && thingsRemainingInputLen < thingsSize) { + throw new IOException("things size " + thingsSize + " exceeds remaining input length " + thingsRemainingInputLen); + } + decodedIntUnion.things = new Multi[thingsSize]; + for (int i = 0; i < thingsSize; i++) { + decodedIntUnion.things[i] = Multi.decode(stream, maxDepth); + } + break; + default: + throw new IOException("Unknown discriminant value: " + discriminant); + } + return decodedIntUnion; + } + public static IntUnion decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + public static IntUnion fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static IntUnion fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/union/IntUnion2.java b/xdr-generator/test/snapshots/union/IntUnion2.java new file mode 100644 index 000000000..6350969dd --- /dev/null +++ b/xdr-generator/test/snapshots/union/IntUnion2.java @@ -0,0 +1,54 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * IntUnion2's original definition in the XDR file is: + *
+ * typedef IntUnion IntUnion2;
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IntUnion2 implements XdrElement { + private IntUnion IntUnion2; + public void encode(XdrDataOutputStream stream) throws IOException { + IntUnion2.encode(stream); + } + + public static IntUnion2 decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + IntUnion2 decodedIntUnion2 = new IntUnion2(); + decodedIntUnion2.IntUnion2 = IntUnion.decode(stream, maxDepth); + return decodedIntUnion2; + } + public static IntUnion2 decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static IntUnion2 fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static IntUnion2 fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/union/Multi.java b/xdr-generator/test/snapshots/union/Multi.java new file mode 100644 index 000000000..c7374b3a0 --- /dev/null +++ b/xdr-generator/test/snapshots/union/Multi.java @@ -0,0 +1,54 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Multi's original definition in the XDR file is: + *
+ * typedef int Multi;
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Multi implements XdrElement { + private Integer Multi; + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(Multi); + } + + public static Multi decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + Multi decodedMulti = new Multi(); + decodedMulti.Multi = stream.readInt(); + return decodedMulti; + } + public static Multi decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static Multi fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static Multi fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/union/MyUnion.java b/xdr-generator/test/snapshots/union/MyUnion.java new file mode 100644 index 000000000..078a7c4fb --- /dev/null +++ b/xdr-generator/test/snapshots/union/MyUnion.java @@ -0,0 +1,99 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Builder; + +/** + * MyUnion's original definition in the XDR file is: + *
+ * union MyUnion switch (UnionKey type)
+ * {
+ *     case ERROR:
+ *         Error error;
+ *     case MULTI:
+ *         Multi things<>;
+ * 
+ * 
+ * };
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) +public class MyUnion implements XdrElement { + private UnionKey discriminant; + private Error error; + private Multi[] things; + + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(discriminant.getValue()); + switch (discriminant) { + case ERROR: + error.encode(stream); + break; + case MULTI: + int thingsSize = getThings().length; + stream.writeInt(thingsSize); + for (int i = 0; i < thingsSize; i++) { + things[i].encode(stream); + } + break; + } + } + public static MyUnion decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + MyUnion decodedMyUnion = new MyUnion(); + UnionKey discriminant = UnionKey.decode(stream, maxDepth); + decodedMyUnion.setDiscriminant(discriminant); + switch (decodedMyUnion.getDiscriminant()) { + case ERROR: + decodedMyUnion.error = Error.decode(stream, maxDepth); + break; + case MULTI: + int thingsSize = stream.readInt(); + if (thingsSize < 0) { + throw new IOException("things size " + thingsSize + " is negative"); + } + int thingsRemainingInputLen = stream.getRemainingInputLen(); + if (thingsRemainingInputLen >= 0 && thingsRemainingInputLen < thingsSize) { + throw new IOException("things size " + thingsSize + " exceeds remaining input length " + thingsRemainingInputLen); + } + decodedMyUnion.things = new Multi[thingsSize]; + for (int i = 0; i < thingsSize; i++) { + decodedMyUnion.things[i] = Multi.decode(stream, maxDepth); + } + break; + default: + throw new IOException("Unknown discriminant value: " + discriminant); + } + return decodedMyUnion; + } + public static MyUnion decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + public static MyUnion fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static MyUnion fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/union/UnionKey.java b/xdr-generator/test/snapshots/union/UnionKey.java new file mode 100644 index 000000000..ef4bc5f7a --- /dev/null +++ b/xdr-generator/test/snapshots/union/UnionKey.java @@ -0,0 +1,64 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.IOException; + +import org.stellar.sdk.Base64Factory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +/** + * UnionKey's original definition in the XDR file is: + *
+ * enum UnionKey {
+ *   ERROR,
+ *   MULTI
+ * };
+ * 
+ */ +public enum UnionKey implements XdrElement { + ERROR(0), + MULTI(1); + + private final int value; + + UnionKey(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static UnionKey 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) { + case 0: return ERROR; + case 1: return MULTI; + default: + throw new IllegalArgumentException("Unknown enum value: " + value); + } + } + + public static UnionKey decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(value); + } + public static UnionKey fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static UnionKey fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } +} diff --git a/xdr-generator/test/snapshots/union/XdrDataInputStream.java b/xdr-generator/test/snapshots/union/XdrDataInputStream.java new file mode 100644 index 000000000..0dd3098d9 --- /dev/null +++ b/xdr-generator/test/snapshots/union/XdrDataInputStream.java @@ -0,0 +1,226 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/union/XdrDataOutputStream.java b/xdr-generator/test/snapshots/union/XdrDataOutputStream.java new file mode 100644 index 000000000..9bb857a64 --- /dev/null +++ b/xdr-generator/test/snapshots/union/XdrDataOutputStream.java @@ -0,0 +1,96 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/union/XdrElement.java b/xdr-generator/test/snapshots/union/XdrElement.java new file mode 100644 index 000000000..fba637542 --- /dev/null +++ b/xdr-generator/test/snapshots/union/XdrElement.java @@ -0,0 +1,21 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/union/XdrString.java b/xdr-generator/test/snapshots/union/XdrString.java new file mode 100644 index 000000000..f0a0c478a --- /dev/null +++ b/xdr-generator/test/snapshots/union/XdrString.java @@ -0,0 +1,78 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/union/XdrUnsignedHyperInteger.java b/xdr-generator/test/snapshots/union/XdrUnsignedHyperInteger.java new file mode 100644 index 000000000..82a21dcab --- /dev/null +++ b/xdr-generator/test/snapshots/union/XdrUnsignedHyperInteger.java @@ -0,0 +1,74 @@ +package org.stellar.sdk.xdr; + +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/test/snapshots/union/XdrUnsignedInteger.java b/xdr-generator/test/snapshots/union/XdrUnsignedInteger.java new file mode 100644 index 000000000..d5a79959d --- /dev/null +++ b/xdr-generator/test/snapshots/union/XdrUnsignedInteger.java @@ -0,0 +1,62 @@ +package org.stellar.sdk.xdr; + +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); + } +}