diff --git a/src/com/amazon/ion/impl/bin/IonEncoder_1_1.java b/src/com/amazon/ion/impl/bin/IonEncoder_1_1.java new file mode 100644 index 0000000000..cca35421f7 --- /dev/null +++ b/src/com/amazon/ion/impl/bin/IonEncoder_1_1.java @@ -0,0 +1,161 @@ +package com.amazon.ion.impl.bin; + +import com.amazon.ion.Decimal; +import com.amazon.ion.IonType; +import com.amazon.ion.Timestamp; +import com.amazon.ion.impl.bin.utf8.Utf8StringEncoder; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import static java.lang.Double.doubleToRawLongBits; +import static java.lang.Float.floatToIntBits; + +/** + * Provides functions for writing various Ion values to a WriteBuffer. + * + * This class can be subsumed by IonRawBinaryWriter_1_1, when it is created. + */ +public class IonEncoder_1_1 { + + /** + * Writes an Ion Null value to the given WriteBuffer. + * @return the number of bytes written + */ + public static int writeNullValue(WriteBuffer buffer, final IonType ionType) { + if (ionType == IonType.NULL) { + buffer.writeByte(OpCodes.NULL_UNTYPED); + return 1; + } + + buffer.writeByte(OpCodes.NULL_TYPED); + switch (ionType) { + case BOOL: + buffer.writeByte((byte) 0x00); + break; + case INT: + buffer.writeByte((byte) 0x01); + break; + case FLOAT: + buffer.writeByte((byte) 0x02); + break; + case DECIMAL: + buffer.writeByte((byte) 0x03); + break; + case TIMESTAMP: + buffer.writeByte((byte) 0x04); + break; + case STRING: + buffer.writeByte((byte) 0x05); + break; + case SYMBOL: + buffer.writeByte((byte) 0x06); + break; + case BLOB: + buffer.writeByte((byte) 0x07); + break; + case CLOB: + buffer.writeByte((byte) 0x08); + break; + case LIST: + buffer.writeByte((byte) 0x09); + break; + case SEXP: + buffer.writeByte((byte) 0x0A); + break; + case STRUCT: + buffer.writeByte((byte) 0x0B); + break; + case DATAGRAM: + throw new IllegalArgumentException("Cannot write a null datagram"); + } + return 2; + } + + /** + * Writes an Ion Bool value to the given WriteBuffer. + * @return the number of bytes written + */ + public static int writeBoolValue(WriteBuffer buffer, final boolean value) { + if (value) { + buffer.writeByte(OpCodes.BOOLEAN_TRUE); + } else { + buffer.writeByte(OpCodes.BOOLEAN_FALSE); + } + return 1; + } + + /** + * Writes an Ion Integer value to the given WriteBuffer. + * @return the number of bytes written + */ + public static int writeIntValue(WriteBuffer buffer, final long value) { + if (value == 0) { + buffer.writeByte(OpCodes.INTEGER_ZERO_LENGTH); + return 1; + } + int length = WriteBuffer.fixedIntLength(value); + buffer.writeByte((byte) (OpCodes.INTEGER_ZERO_LENGTH + length)); + buffer.writeFixedInt(value); + return 1 + length; + } + + private static final BigInteger BIG_INT_LONG_MAX_VALUE = BigInteger.valueOf(Long.MAX_VALUE); + private static final BigInteger BIG_INT_LONG_MIN_VALUE = BigInteger.valueOf(Long.MIN_VALUE); + + /** + * Writes an Ion Integer value to the given WriteBuffer. + * @return the number of bytes written + */ + public static int writeIntValue(WriteBuffer buffer, final BigInteger value) { + if (value == null) { + return writeNullValue(buffer, IonType.INT); + } + if (value.compareTo(BIG_INT_LONG_MIN_VALUE) >= 0 && value.compareTo(BIG_INT_LONG_MAX_VALUE) <= 0) { + return writeIntValue(buffer, value.longValue()); + } + buffer.writeByte(OpCodes.VARIABLE_LENGTH_INTEGER); + byte[] intBytes = value.toByteArray(); + int totalBytes = 1 + intBytes.length + buffer.writeFlexUInt(intBytes.length); + for (int i = intBytes.length; i > 0; i--) { + buffer.writeByte(intBytes[i-1]); + } + return totalBytes; + } + + /** + * Writes a float to the given WriteBuffer using the Ion 1.1 encoding for Ion Floats. + * @return the number of bytes written + */ + public static int writeFloat(WriteBuffer buffer, final float value) { + // TODO: Optimization to write a 16 bit float for non-finite and possibly other values + if (value == 0.0) { + buffer.writeByte(OpCodes.FLOAT_ZERO_LENGTH); + return 1; + } else { + buffer.writeByte(OpCodes.FLOAT_32); + buffer.writeUInt32(floatToIntBits(value)); + return 5; + } + } + + /** + * Writes a double to the given WriteBuffer using the Ion 1.1 encoding for Ion Floats. + * @return the number of bytes written + */ + public static int writeFloat(WriteBuffer buffer, final double value) { + // TODO: Optimization to write a 16 bit float for non-finite and possibly other values + if (value == 0.0) { + buffer.writeByte(OpCodes.FLOAT_ZERO_LENGTH); + return 1; + } else if (!Double.isFinite(value) || value == (float) value) { + buffer.writeByte(OpCodes.FLOAT_32); + buffer.writeUInt32(floatToIntBits((float) value)); + return 5; + } else { + buffer.writeByte(OpCodes.FLOAT_64); + buffer.writeUInt64(doubleToRawLongBits(value)); + return 9; + } + } +} diff --git a/src/com/amazon/ion/impl/bin/OpCodes.java b/src/com/amazon/ion/impl/bin/OpCodes.java new file mode 100644 index 0000000000..0ea5bf843d --- /dev/null +++ b/src/com/amazon/ion/impl/bin/OpCodes.java @@ -0,0 +1,28 @@ +package com.amazon.ion.impl.bin; + +/** + * Utility class holding Ion 1.1 Op Codes. + */ +public class OpCodes { + private OpCodes() {} + + public static final byte INTEGER_ZERO_LENGTH = 0x50; + // 0x51-0x58 are additional lengths of integers. + // 0x59 Reserved + public static final byte FLOAT_ZERO_LENGTH = 0x5A; + public static final byte FLOAT_16 = 0x5B; + public static final byte FLOAT_32 = 0x5C; + public static final byte FLOAT_64 = 0x5D; + public static final byte BOOLEAN_TRUE = 0x5E; + public static final byte BOOLEAN_FALSE = 0x5F; + + public static final byte DECIMAL_ZERO_LENGTH = 0x60; + // 0x61-0x6E are additional lengths of decimals. + public static final byte NEGATIVE_ZERO_DECIMAL = 0x6F; + + + public static final byte NULL_UNTYPED = (byte) 0xEA; + public static final byte NULL_TYPED = (byte) 0xEB; + + public static final byte VARIABLE_LENGTH_INTEGER = (byte) 0xF5; +} diff --git a/src/com/amazon/ion/impl/bin/WriteBuffer.java b/src/com/amazon/ion/impl/bin/WriteBuffer.java index fba4c60d48..5a493e8ca0 100644 --- a/src/com/amazon/ion/impl/bin/WriteBuffer.java +++ b/src/com/amazon/ion/impl/bin/WriteBuffer.java @@ -18,6 +18,7 @@ import java.io.Closeable; import java.io.IOException; import java.io.OutputStream; +import java.math.BigInteger; import java.util.ArrayList; import java.util.List; @@ -1359,6 +1360,64 @@ private int writeFlexIntOrUInt(final long value, final int numBytes) { return numBytes; } + public static int flexIntLength(final BigInteger value) { + return value.bitLength() / 7 + 1; + } + + public static int flexUIntLength(final BigInteger value) { + return (value.bitLength() - 1) / 7 + 1; + } + + public int writeFlexInt(final BigInteger value) { + int numBytes = flexIntLength(value); + return writeFlexIntOrUIntForBigInteger(value, numBytes); + } + + public int writeFlexUInt(final BigInteger value) { + if (value.signum() < 0) { + throw new IllegalArgumentException("Attempted to write a FlexUInt for " + value); + } + int numBytes = flexUIntLength(value); + return writeFlexIntOrUIntForBigInteger(value, numBytes); + } + + private int writeFlexIntOrUIntForBigInteger(final BigInteger value, final int numBytes) { + // TODO: Should we branch to the implementation for long if the number is small enough? + // https://github.com/amazon-ion/ion-java/issues/614 + byte[] valueBytes = value.toByteArray(); + + int i = 0; // `i` gets incremented for every byte written. + + // Start with leading zero bytes. + // If there's 1-8 total bytes, we need no leading zero-bytes. + // If there's 9-16 total bytes, we need one zero-byte + // If there's 17-24 total bytes, we need two zero-bytes, etc. + for (; i < (numBytes - 1)/8; i++) { + writeByte((byte) 0); + } + + // Write the last length bits, possibly also containing some value bits. + int remainingLengthBits = (numBytes - 1) % 8; + byte lengthPart = (byte) (0x01 << remainingLengthBits); + int valueBitOffset = remainingLengthBits + 1; + byte valuePart = (byte) (valueBytes[valueBytes.length - 1] << valueBitOffset); + writeByte((byte) (valuePart | lengthPart)); + i++; + + for (int valueByteOffset = valueBytes.length - 1; valueByteOffset > 0; valueByteOffset--) { + // Technically it's only a nibble if the bitOffset is 4, so we call it nibble-ish + byte highNibbleIsh = (byte) (valueBytes[valueByteOffset - 1] << (valueBitOffset)); + byte lowNibbleIsh = (byte) ((valueBytes[valueByteOffset] & 0xFF) >> (8 - valueBitOffset)); + writeByte((byte) (highNibbleIsh | lowNibbleIsh)); + i++; + } + if (i < numBytes) { + writeByte((byte) ((valueBytes[0]) >> (8 - valueBitOffset))); + } + + return numBytes; + } + /** Get the length of FixedInt for the provided value. */ public static int fixedIntLength(final long value) { int numMagnitudeBitsRequired; diff --git a/test/com/amazon/ion/impl/bin/IonEncoder_1_1Test.java b/test/com/amazon/ion/impl/bin/IonEncoder_1_1Test.java new file mode 100644 index 0000000000..4d26345d77 --- /dev/null +++ b/test/com/amazon/ion/impl/bin/IonEncoder_1_1Test.java @@ -0,0 +1,225 @@ +package com.amazon.ion.impl.bin; + +import com.amazon.ion.IonType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.util.function.BiFunction; + +public class IonEncoder_1_1Test { + + private static BlockAllocator ALLOCATOR = BlockAllocatorProviders.basicProvider().vendAllocator(11); + private WriteBuffer buf; + + @BeforeEach + public void setup() { + buf = new WriteBuffer(ALLOCATOR); + } + + private byte[] bytes() { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + buf.writeTo(out); + } catch (final IOException e) { + throw new IllegalStateException(e); + } + return out.toByteArray(); + } + + /** + * Checks that the function writes the expected bytes and returns the expected + * count of written bytes for the given input value. + */ + private void assertWritingValue(String expectedBytes, T value, BiFunction writeOperation) { + int numBytes = writeOperation.apply(buf, value); + Assertions.assertEquals(expectedBytes, byteArrayToHex(bytes())); + Assertions.assertEquals(byteLengthFromHexString(expectedBytes), numBytes); + } + + @ParameterizedTest + @CsvSource({ + " NULL, EA", + " BOOL, EB 00", + " INT, EB 01", + " FLOAT, EB 02", + " DECIMAL, EB 03", + "TIMESTAMP, EB 04", + " STRING, EB 05", + " SYMBOL, EB 06", + " BLOB, EB 07", + " CLOB, EB 08", + " LIST, EB 09", + " SEXP, EB 0A", + " STRUCT, EB 0B", + }) + public void testWriteNullValue(IonType value, String expectedBytes) { + assertWritingValue(expectedBytes, value, IonEncoder_1_1::writeNullValue); + } + + @Test + public void testWriteNullValueForDatagram() { + Assertions.assertThrows(IllegalArgumentException.class, () -> IonEncoder_1_1.writeNullValue(buf, IonType.DATAGRAM)); + } + + @ParameterizedTest + @CsvSource({ + "true, 5E", + "false, 5F", + }) + public void testWriteBooleanValue(Boolean value, String expectedBytes) { + assertWritingValue(expectedBytes, value, IonEncoder_1_1::writeBoolValue); + } + + @ParameterizedTest + @CsvSource({ + " 0, 50", + " 1, 51 01", + " 17, 51 11", + " 127, 51 7F", + " 128, 52 80 00", + " 5555, 52 B3 15", + " 32767, 52 FF 7F", + " 32768, 53 00 80 00", + " 292037, 53 C5 74 04", + " 321672342, 54 96 54 2C 13", + " 64121672342, 55 96 12 F3 ED 0E", + " 1274120283167, 56 1F A4 7C A7 28 01", + " 851274120283167, 57 1F C4 8B B3 3A 06 03", + " 72624976668147840, 58 80 40 20 10 08 04 02 01", + " 9223372036854775807, 58 FF FF FF FF FF FF FF 7F", // Long.MAX_VALUE + " -1, 51 FF", + " -2, 51 FE", + " -14, 51 F2", + " -128, 51 80", + " -129, 52 7F FF", + " -944, 52 50 FC", + " -32768, 52 00 80", + " -32769, 53 FF 7F FF", + " -8388608, 53 00 00 80", + " -8388609, 54 FF FF 7F FF", + " -72624976668147841, 58 7F BF DF EF F7 FB FD FE", + "-9223372036854775808, 58 00 00 00 00 00 00 00 80", // Long.MIN_VALUE + }) + public void testWriteIntegerValue(long value, String expectedBytes) { + assertWritingValue(expectedBytes, value, IonEncoder_1_1::writeIntValue); + } + + @ParameterizedTest + @CsvSource({ + " 0, 50", + " 1, 51 01", + " 17, 51 11", + " 127, 51 7F", + " 128, 52 80 00", + " 5555, 52 B3 15", + " 32767, 52 FF 7F", + " 32768, 53 00 80 00", + " 292037, 53 C5 74 04", + " 321672342, 54 96 54 2C 13", + " 64121672342, 55 96 12 F3 ED 0E", + " 1274120283167, 56 1F A4 7C A7 28 01", + " 851274120283167, 57 1F C4 8B B3 3A 06 03", + " 72624976668147840, 58 80 40 20 10 08 04 02 01", + " 9223372036854775807, 58 FF FF FF FF FF FF FF 7F", // Long.MAX_VALUE + " 9223372036854775808, F5 13 00 00 00 00 00 00 00 80 00", + "999999999999999999999999999999, F5 1B FF FF FF 3F EA ED 74 46 D0 9C 2C 9F 0C", + " -1, 51 FF", + " -2, 51 FE", + " -14, 51 F2", + " -128, 51 80", + " -129, 52 7F FF", + " -944, 52 50 FC", + " -32768, 52 00 80", + " -32769, 53 FF 7F FF", + " -8388608, 53 00 00 80", + " -8388609, 54 FF FF 7F FF", + " -72624976668147841, 58 7F BF DF EF F7 FB FD FE", + " -9223372036854775808, 58 00 00 00 00 00 00 00 80", // Long.MIN_VALUE + " -9223372036854775809, F5 13 FF FF FF FF FF FF FF 7F FF", + "-99999999999999999999999999999, F5 1B 01 00 00 60 35 E8 8D 92 51 F0 E1 BC FE", + }) + public void testWriteIntegerValueForBigInteger(String value, String expectedBytes) { + assertWritingValue(expectedBytes, new BigInteger(value), IonEncoder_1_1::writeIntValue); + } + + @Test + public void testWriteIntegerValueForNullBigInteger() { + int numBytes = IonEncoder_1_1.writeIntValue(buf, null); + Assertions.assertEquals("EB 01", byteArrayToHex(bytes())); + Assertions.assertEquals(2, numBytes); + } + + @ParameterizedTest + @CsvSource({ + " 0.0, 5A", + " 1.0, 5C 3F 80 00 00", + " 1.5, 5C 3F C0 00 00", + " 3.1415927, 5C 40 49 0F DB", + " 4.00537109375, 5C 40 80 2C 00", + " 423542.09375, 5C 48 CE CE C3", + " 3.40282347E+38, 5C 7F 7F FF FF", // Float.MAX_VALUE + " -1.0, 5C BF 80 00 00", + " -1.5, 5C BF C0 00 00", + " -3.1415927, 5C C0 49 0F DB", + " -4.00537109375, 5C C0 80 2C 00", + " -423542.09375, 5C C8 CE CE C3", + "-3.40282347E+38, 5C FF 7F FF FF", // Float.MIN_VALUE + " NaN, 5C 7F C0 00 00", + " Infinity, 5C 7F 80 00 00", + " -Infinity, 5C FF 80 00 00", + }) + public void testWriteFloatValue(float value, String expectedBytes) { + assertWritingValue(expectedBytes, value, IonEncoder_1_1::writeFloat); + } + + @ParameterizedTest + @CsvSource({ + " 0.0, 5A", + " 1.0, 5C 3F 80 00 00", + " 1.5, 5C 3F C0 00 00", + " 3.141592653589793, 5D 40 09 21 FB 54 44 2D 18", + " 4.00537109375, 5C 40 80 2C 00", + " 4.11111111111, 5D 40 10 71 C7 1C 71 C2 39", + " 423542.09375, 5C 48 CE CE C3", + " 8236423542.09375, 5D 41 FE AE DD 97 61 80 00", + " 1.79769313486231570e+308, 5D 7F EF FF FF FF FF FF FF", // Double.MAX_VALUE + " -1.0, 5C BF 80 00 00", + " -1.5, 5C BF C0 00 00", + " -3.141592653589793, 5D C0 09 21 FB 54 44 2D 18", + " -4.00537109375, 5C C0 80 2C 00", + " -4.11111111111, 5D C0 10 71 C7 1C 71 C2 39", + " -423542.09375, 5C C8 CE CE C3", + " -8236423542.09375, 5D C1 FE AE DD 97 61 80 00", + "-1.79769313486231570e+308, 5D FF EF FF FF FF FF FF FF", // Double.MIN_VALUE + " NaN, 5C 7F C0 00 00", + " Infinity, 5C 7F 80 00 00", + " -Infinity, 5C FF 80 00 00", + }) + public void testWriteFloatValueForDouble(double value, String expectedBytes) { + assertWritingValue(expectedBytes, value, IonEncoder_1_1::writeFloat); + } + + /** + * Utility method to make it easier to write test cases that assert specific sequences of bytes. + */ + private static String byteArrayToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02X ", b)); + } + return sb.toString().trim(); + } + + /** + * Determines the number of bytes needed to represent a series of hexadecimal digits. + */ + private static int byteLengthFromHexString(String hexString) { + return (hexString.replaceAll("[^\\dA-F]", "").length() - 1) / 2 + 1; + } +} diff --git a/test/com/amazon/ion/impl/bin/WriteBufferTest.java b/test/com/amazon/ion/impl/bin/WriteBufferTest.java index 10d49f7b14..a46e4c2c75 100644 --- a/test/com/amazon/ion/impl/bin/WriteBufferTest.java +++ b/test/com/amazon/ion/impl/bin/WriteBufferTest.java @@ -24,6 +24,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.Arrays; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Assertions; @@ -1411,6 +1413,116 @@ public void testWriteFlexUIntForNegativeNumber() { Assertions.assertThrows(IllegalArgumentException.class, () -> buf.writeFlexUInt(-1)); } + @ParameterizedTest + @CsvSource({ + " 0, 00000001", + " 1, 00000011", + " 2, 00000101", + " 3, 00000111", + " 4, 00001001", + " 5, 00001011", + " 14, 00011101", + " 63, 01111111", + " 64, 00000010 00000001", + " 729, 01100110 00001011", + " 8191, 11111110 01111111", + " 8192, 00000100 00000000 00000001", + " 1048575, 11111100 11111111 01111111", + " 1048576, 00001000 00000000 00000000 00000001", + " 134217727, 11111000 11111111 11111111 01111111", + " 134217728, 00010000 00000000 00000000 00000000 00000001", + " 17179869184, 00100000 00000000 00000000 00000000 00000000 00000001", + " 2199023255552, 01000000 00000000 00000000 00000000 00000000 00000000 00000001", + " 281474976710655, 11000000 11111111 11111111 11111111 11111111 11111111 01111111", + " 281474976710656, 10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001", + " 36028797018963967, 10000000 11111111 11111111 11111111 11111111 11111111 11111111 01111111", + " 36028797018963968, 00000000 00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000001", + // Different one-bits in every byte, making it easy to see if any bytes are out of order + " 72624976668147840, 00000000 00000001 10000001 01000000 00100000 00010000 00001000 00000100 00000010", + " 4611686018427387903, 00000000 11111111 11111111 11111111 11111111 11111111 11111111 11111111 01111111", + " 4611686018427387904, 00000000 00000010 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001", + // Long.MAX_VALUE + " 9223372036854775807, 00000000 11111110 11111111 11111111 11111111 11111111 11111111 11111111 11111111 00000001", + " 9223372036854775808, 00000000 00000010 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000010", + " -1, 11111111", + " -2, 11111101", + " -3, 11111011", + " -14, 11100101", + " -64, 10000001", + " -65, 11111110 11111110", + " -729, 10011110 11110100", + " -8192, 00000010 10000000", + " -8193, 11111100 11111111 11111110", + " -1048576, 00000100 00000000 10000000", + " -1048577, 11111000 11111111 11111111 11111110", + " -134217728, 00001000 00000000 00000000 10000000", + " -134217729, 11110000 11111111 11111111 11111111 11111110", + " -17179869184, 00010000 00000000 00000000 00000000 10000000", + " -17179869185, 11100000 11111111 11111111 11111111 11111111 11111110", + " -281474976710656, 01000000 00000000 00000000 00000000 00000000 00000000 10000000", + " -281474976710657, 10000000 11111111 11111111 11111111 11111111 11111111 11111111 11111110", + " -36028797018963968, 10000000 00000000 00000000 00000000 00000000 00000000 00000000 10000000", + " -36028797018963969, 00000000 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111110", + // Different zero-bits in every byte, making it easy to see if any bytes are out of order + " -72624976668147841, 00000000 11111111 01111110 10111111 11011111 11101111 11110111 11111011 11111101", + "-4611686018427387904, 00000000 00000001 00000000 00000000 00000000 00000000 00000000 00000000 10000000", + "-4611686018427387905, 00000000 11111110 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111110", + // Long.MIN_VALUE + "-9223372036854775808, 00000000 00000010 00000000 00000000 00000000 00000000 00000000 00000000 00000000 11111110", + "-9223372036854775809, 00000000 11111110 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111101", + + }) + public void testWriteFlexIntForBigInteger(String value, String expectedBits) { + int numBytes = buf.writeFlexInt(new BigInteger(value)); + String actualBits = byteArrayToBitString(bytes()); + Assertions.assertEquals(expectedBits, actualBits); + Assertions.assertEquals((expectedBits.length() + 1)/9, numBytes); + } + + @ParameterizedTest + @CsvSource({ + " 0, 00000001", + " 1, 00000011", + " 2, 00000101", + " 3, 00000111", + " 4, 00001001", + " 5, 00001011", + " 14, 00011101", + " 63, 01111111", + " 64, 10000001", + " 127, 11111111", + " 128, 00000010 00000010", + " 729, 01100110 00001011", + " 16383, 11111110 11111111", + " 16384, 00000100 00000000 00000010", + " 2097151, 11111100 11111111 11111111", + " 2097152, 00001000 00000000 00000000 00000010", + " 268435455, 11111000 11111111 11111111 11111111", + " 268435456, 00010000 00000000 00000000 00000000 00000010", + " 34359738368, 00100000 00000000 00000000 00000000 00000000 00000010", + " 4398046511104, 01000000 00000000 00000000 00000000 00000000 00000000 00000010", + " 562949953421311, 11000000 11111111 11111111 11111111 11111111 11111111 11111111", + " 562949953421312, 10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000010", + " 72057594037927935, 10000000 11111111 11111111 11111111 11111111 11111111 11111111 11111111", + " 72057594037927936, 00000000 00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000010", + // Different one-bits in every byte, making it easy to see if any bytes are out of order + " 72624976668147840, 00000000 00000001 10000001 01000000 00100000 00010000 00001000 00000100 00000010", + // Long.MAX_VALUE + "9223372036854775807, 00000000 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111", + "9223372036854775808, 00000000 00000010 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000010", + }) + public void testWriteFlexUIntForBigInteger(String value, String expectedBits) { + int numBytes = buf.writeFlexUInt(new BigInteger(value)); + String actualBits = byteArrayToBitString(bytes()); + Assertions.assertEquals(expectedBits, actualBits); + Assertions.assertEquals((expectedBits.length() + 1)/9, numBytes); + } + + @Test + public void testWriteFlexUIntForNegativeBigInteger() { + Assertions.assertThrows(IllegalArgumentException.class, () -> buf.writeFlexUInt(BigInteger.ONE.negate())); + } + @ParameterizedTest @CsvSource({ " 0, 00000000",