diff --git a/src/main/java/com/amazon/ion/bytecode/bin10/TypeIdHelper.kt b/src/main/java/com/amazon/ion/bytecode/bin10/TypeIdHelper.kt new file mode 100644 index 000000000..832be1f21 --- /dev/null +++ b/src/main/java/com/amazon/ion/bytecode/bin10/TypeIdHelper.kt @@ -0,0 +1,114 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode.bin10 + +import com.amazon.ion.IonType +import com.amazon.ion.bytecode.ir.OperationKind +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings + +internal object TypeIdHelper { + + /** + * Returns the IonType for a legal Ion 1.0 typeId. + * + * The returned value is undefined when attempting to use this function for illegal type IDs. + */ + @JvmStatic + fun ionTypeForTypeId(typeId: Int): IonType? { + return when (typeId shr 4) { + 0x0 -> if (typeId == 0x0F) IonType.NULL else null + 0x1 -> IonType.BOOL + 0x2, 0x3 -> IonType.INT + 0x4 -> IonType.FLOAT + 0x5 -> IonType.DECIMAL + 0x6 -> IonType.TIMESTAMP + 0x7 -> IonType.SYMBOL + 0x8 -> IonType.STRING + 0x9 -> IonType.CLOB + 0xA -> IonType.BLOB + 0xB -> IonType.LIST + 0xC -> IonType.SEXP + 0xD -> IonType.STRUCT + else -> null + } + } + + /** + * Returns the [OperationKind] for an Ion 1.0 typeId. + */ + @JvmStatic + fun operationKindForTypeId(typeId: Int): Int = typeToOperationKindLookup[typeId] + + private val typeToOperationKindLookup = IntArray(256) { initOperationKindForType(it) } + + private fun initOperationKindForType(state: Int): Int { + return when (state) { + in 0x00..0x0E -> OperationKind.UNSET + 0x0F -> OperationKind.NULL + 0x10, 0x11, 0x1F -> OperationKind.BOOL + in 0x20..0x2F -> OperationKind.INT + in 0x31..0x3F -> OperationKind.INT + 0x40, 0x44, 0x48, 0x4F -> OperationKind.FLOAT + in 0x50..0x5F -> OperationKind.DECIMAL + in 0x62..0x6F -> OperationKind.TIMESTAMP + in 0x70..0x7F -> OperationKind.SYMBOL + in 0x80..0x8F -> OperationKind.STRING + in 0x90..0x9F -> OperationKind.CLOB + in 0xA0..0xAF -> OperationKind.BLOB + in 0xB0..0xBF -> OperationKind.LIST + in 0xC0..0xCF -> OperationKind.SEXP + 0xD0, in 0xD2..0xDF -> OperationKind.STRUCT + 0xE0 -> OperationKind.IVM + in 0xE3..0xEE -> OperationKind.ANNOTATIONS + // Everything else: 12..1E, 30, D1, EF, F0..FF, illegal timestamp, float, and annotations sizes + else -> OperationKind.UNSET + } + } + + /** + * A lookup table containing the value length for Ion 1.0 type IDs. + * A value of -1 indicates that the length follows as a `VarUInt`. + * A value of -2 indicates that the typeId is not a valid Ion 1.0 typeId. + * + * TODO(perf): If there's not a noticeable amount of overhead, we should hide this behind a method instead of + * exposing it directly and suppressing the spotbugs error. + */ + @JvmStatic + @get:SuppressFBWarnings("MS_EXPOSE_REP", justification = "it is exposed for internal use only as a performance optimization") + val TYPE_LENGTHS = IntArray(256) { initTypeLength(it) } + + @JvmStatic + @OptIn(ExperimentalStdlibApi::class) + private fun initTypeLength(typeId: Int): Int { + return when (typeId) { + // The length of all of these is determined entirely by the low-nibble of the typeId. + 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, /* */ 0x70, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0 /* */ -> 0 + 0x01, /* */ 0x21, 0x31, /* */ 0x51, /* */ 0x71, 0x81, 0x91, 0xA1, 0xB1, 0xC1 /* */ /* */ -> 1 + 0x02, 0x12, 0x22, 0x32, /* */ 0x52, 0x62, 0x72, 0x82, 0x92, 0xA2, 0xB2, 0xC2, 0xD2 /* */ -> 2 + 0x03, 0x13, 0x23, 0x33, /* */ 0x53, 0x63, 0x73, 0x83, 0x93, 0xA3, 0xB3, 0xC3, 0xD3, 0xE3 -> 3 + 0x04, 0x14, 0x24, 0x34, 0x44, 0x54, 0x64, 0x74, 0x84, 0x94, 0xA4, 0xB4, 0xC4, 0xD4, 0xE4 -> 4 + 0x05, 0x15, 0x25, 0x35, /* */ 0x55, 0x65, 0x75, 0x85, 0x95, 0xA5, 0xB5, 0xC5, 0xD5, 0xE5 -> 5 + 0x06, 0x16, 0x26, 0x36, /* */ 0x56, 0x66, 0x76, 0x86, 0x96, 0xA6, 0xB6, 0xC6, 0xD6, 0xE6 -> 6 + 0x07, 0x17, 0x27, 0x37, /* */ 0x57, 0x67, 0x77, 0x87, 0x97, 0xA7, 0xB7, 0xC7, 0xD7, 0xE7 -> 7 + 0x08, 0x18, 0x28, 0x38, 0x48, 0x58, 0x68, 0x78, 0x88, 0x98, 0xA8, 0xB8, 0xC8, 0xD8, 0xE8 -> 8 + 0x09, 0x19, 0x29, 0x39, /* */ 0x59, 0x69, 0x79, 0x89, 0x99, 0xA9, 0xB9, 0xC9, 0xD9, 0xE9 -> 9 + 0x0A, 0x1A, 0x2A, 0x3A, /* */ 0x5A, 0x6A, 0x7A, 0x8A, 0x9A, 0xAA, 0xBA, 0xCA, 0xDA, 0xEA -> 10 + 0x0B, 0x1B, 0x2B, 0x3B, /* */ 0x5B, 0x6B, 0x7B, 0x8B, 0x9B, 0xAB, 0xBB, 0xCB, 0xDB, 0xEB -> 11 + 0x0C, 0x1C, 0x2C, 0x3C, /* */ 0x5C, 0x6C, 0x7C, 0x8C, 0x9C, 0xAC, 0xBC, 0xCC, 0xDC, 0xEC -> 12 + 0x0D, 0x1D, 0x2D, 0x3D, /* */ 0x5D, 0x6D, 0x7D, 0x8D, 0x9D, 0xAD, 0xBD, 0xCD, 0xDD, 0xED -> 13 + 0x0E, 0x1E, 0x2E, 0x3E, /* */ 0x5E, 0x6E, 0x7E, 0x8E, 0x9E, 0xAE, 0xBE, 0xCE, 0xDE, 0xEE -> -1 + // Bool True + 0x11 -> 0 + // Nulls + 0x0F, 0x1F, 0x2F, 0x3F, 0x4F, 0x5F, 0x6F, 0x7F, 0x8F, 0x9F, 0xAF, 0xBF, 0xCF, 0xDF -> 0 + // IVM + 0xE0 -> 3 // ...3 more than the typeId byte. + + // Reserved and/or illegal typeIds + 0xEF, 0x60, 0x61, 0xD1, 0xE1, 0xE2, + in 0x41..0x4E, // Illegal float lengths. 0x44 and 0x48 are trapped in the earlier condition. + in 0xF0..0xFF -> -2 + else -> TODO("This should be unreachable: ${typeId.toHexString()}") + } + } +} diff --git a/src/main/java/com/amazon/ion/bytecode/bin10/ValueHelpers.kt b/src/main/java/com/amazon/ion/bytecode/bin10/ValueHelpers.kt new file mode 100644 index 000000000..c1f86a6ec --- /dev/null +++ b/src/main/java/com/amazon/ion/bytecode/bin10/ValueHelpers.kt @@ -0,0 +1,134 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode.bin10 + +import com.amazon.ion.Decimal +import com.amazon.ion.IonException +import com.amazon.ion.Timestamp +import java.math.BigDecimal +import java.math.BigInteger +import kotlin.experimental.and + +/** + * Given a typeId in the range 0x20..0x3F, returns either -1 or 1. + * This uses some clever bit twiddling to avoid any branching. + * + * (Yes, it's obtuse looking, but it works.) + * + * TODO(perf): Once reader is implemented enough for benchmarks, benchmark this vs using if/else. + */ +internal fun signForIntTypeId(typeId: Int): Int = (((typeId shr 4) shl 31) shr 31) or 1 + +/** + * Return either -1 or 1 based on the sign bit of the given byte. This uses some bit manipulation to avoid any branching. + */ +internal fun getSignumValueFromLeadingSignBit(byte: Byte): Int = byte.toInt().shr(7).shl(1) + 1 + +/** + * Reads a timestamp value from the given byte array. + */ +internal fun readTimestampReference(valueBytes: ByteArray, position: Int, length: Int): Timestamp { + var p = position + val end = position + length + + val offset: Int? = if (valueBytes[p].toInt() and 0xFF == 0xC0) { + p++ + null + } else { + val offsetValueAndLength = VarIntHelper.readVarIntValueAndLength(valueBytes, p) + p += offsetValueAndLength.toInt() and 0xFF + (offsetValueAndLength shr 8).toInt() + } + val yearValueAndLength = VarIntHelper.readVarUIntValueAndLength(valueBytes, p) + p += yearValueAndLength.toInt() and 0xFF + val year = (yearValueAndLength shr 8).toInt() + var month = 0 + var day = 0 + var hour = 0 + var minute = 0 + var second = 0 + var fractionalSecond: BigDecimal? = null + var precision = Timestamp.Precision.YEAR + if (p < end) { + val monthValueAndLength = VarIntHelper.readVarUIntValueAndLength(valueBytes, p) + p += monthValueAndLength.toInt() and 0xFF + month = (monthValueAndLength shr 8).toInt() + precision = Timestamp.Precision.MONTH + if (p < end) { + val dayValueAndLength = VarIntHelper.readVarUIntValueAndLength(valueBytes, p) + p += dayValueAndLength.toInt() and 0xFF + day = (dayValueAndLength shr 8).toInt() + precision = Timestamp.Precision.DAY + if (p < end) { + val hourValueAndLength = VarIntHelper.readVarUIntValueAndLength(valueBytes, p) + p += hourValueAndLength.toInt() and 0xFF + hour = (hourValueAndLength shr 8).toInt() + if (p >= end) { + throw IonException("Timestamps may not specify hour without specifying minute.") + } + + val minuteValueAndLength = VarIntHelper.readVarUIntValueAndLength(valueBytes, p) + p += minuteValueAndLength.toInt() and 0xFF + minute = (minuteValueAndLength shr 8).toInt() + precision = Timestamp.Precision.MINUTE + if (p < end) { + val secondValueAndLength = VarIntHelper.readVarUIntValueAndLength(valueBytes, p) + p += secondValueAndLength.toInt() and 0xFF + second = (secondValueAndLength shr 8).toInt() + precision = Timestamp.Precision.SECOND + if (p < end) { + fractionalSecond = readDecimalReference(valueBytes, p, end) + if (fractionalSecond.scale() < 0) { + fractionalSecond = fractionalSecond.setScale(0) + } + } + } + } + } + } + try { + return Timestamp.createFromUtcFields( + precision, + year, + month, + day, + hour, + minute, + second, + fractionalSecond, + offset + ) + } catch (e: IllegalArgumentException) { + throw IonException("Illegal timestamp encoding at $position.", e) + } +} + +/** + * Reads a Decimal value from the given byte array. + */ +internal fun readDecimalReference(valueBytes: ByteArray, position: Int, end: Int): Decimal { + var p = position + val exponentValueAndLength = VarIntHelper.readVarIntValueAndLength(valueBytes, p) + p += exponentValueAndLength.toInt() and 0xFF + val scale = -(exponentValueAndLength shr 8).toInt() + + val coefficientLength = end - p + return if (coefficientLength > 0) { + // TODO: See if we can have a shared set of reusable buffers for this instead of allocating a copy. + val bytes = valueBytes.copyOfRange(p, p + coefficientLength) + + // Get the signum + val signum = getSignumValueFromLeadingSignBit(bytes[0]) + // Clear the sign bit + bytes[0] = bytes[0] and 0x7F + // Construct the BigInteger + val coefficient = BigInteger(signum, bytes) + if (coefficient == BigInteger.ZERO && signum == -1) { + Decimal.negativeZero(scale) + } else { + Decimal.valueOf(BigInteger(signum, bytes), scale) + } + } else { + Decimal.valueOf(BigInteger.ZERO, scale) + } +} diff --git a/src/main/java/com/amazon/ion/bytecode/bin10/VarIntHelper.kt b/src/main/java/com/amazon/ion/bytecode/bin10/VarIntHelper.kt new file mode 100644 index 000000000..c701cc12d --- /dev/null +++ b/src/main/java/com/amazon/ion/bytecode/bin10/VarIntHelper.kt @@ -0,0 +1,84 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode.bin10 + +import com.amazon.ion.IonException + +object VarIntHelper { + + private const val TERMINATION_BIT_MASK = 0b10000000 + private const val MASK_7_BITS = 0b01111111 + + /** + * Returns an unsigned integer up to 7 bytes, with an 1 byte integer signifying how many varuint bytes were used in its encoding. + */ + @JvmStatic + fun readVarUIntValueAndLength(source: ByteArray, position: Int): Long { + val currentByte: Int = source[position].toInt() + val result = (currentByte and MASK_7_BITS).toLong() + return if (currentByte and TERMINATION_BIT_MASK != 0) { + (result shl 8) or 1L + } else { + readVarUIntValueAndLength2(source, position + 1, result) + } + } + + @JvmStatic + private fun readVarUIntValueAndLength2(source: ByteArray, position: Int, partialResult: Long): Long { + val currentByte: Int = source.get(position).toInt() + val result = (partialResult shl 7) or (currentByte and MASK_7_BITS).toLong() + if (currentByte and TERMINATION_BIT_MASK != 0) { + return (result shl 8) or 2 + } else { + return readVarUIntValueAndLength3Plus(source, position + 1, result) + } + } + + @JvmStatic + private fun readVarUIntValueAndLength3Plus(source: ByteArray, position: Int, partialResult: Long): Long { + var currentByte: Int + var result = partialResult + var p = position + var length = 2 + do { + length++ + if (length > 7) throw IonException("VarUInt value is too large") + currentByte = source.get(p++).toInt() + result = (result shl 7) or (currentByte and MASK_7_BITS).toLong() + } while (currentByte and TERMINATION_BIT_MASK == 0) + + return (result shl 8) or length.toLong() + } + + /** + * Returns a signed integer up to 7 bytes, with an 1 byte integer signifying how many varuint bytes were used in its encoding. + */ + @JvmStatic + fun readVarIntValueAndLength(source: ByteArray, position: Int): Long { + var p = position + + var length = 1 + try { + var currentByte = source[p++].toInt() and 0xFF + var result = (currentByte and 0b00111111).toLong() + val sign = getSignumValueFromVarIntSignBit(currentByte) + while (currentByte and TERMINATION_BIT_MASK == 0) { + length++ + if (length > 7) throw IonException("VarInt value is too large") + currentByte = source[p++].toInt() + result = (result shl 7) or (currentByte and MASK_7_BITS).toLong() + } + return ((sign * result) shl 8) or length.toLong() + } catch (e: ArrayIndexOutOfBoundsException) { + throw IonException("Incomplete VarInt at position $position", e) + } + } + + /** + * Return either -1 or 1 based on the sign bit of the given byte. This uses some bit manipulation to avoid any branching. + * + * Visible only for testing. + */ + @JvmStatic + internal fun getSignumValueFromVarIntSignBit(byte: Int): Int = byte.shl(25).shr(31).shl(1) + 1 +} diff --git a/src/test/java/com/amazon/ion/TextToBinaryUtils.kt b/src/test/java/com/amazon/ion/TextToBinaryUtils.kt index 7532cc42c..c6d308eef 100644 --- a/src/test/java/com/amazon/ion/TextToBinaryUtils.kt +++ b/src/test/java/com/amazon/ion/TextToBinaryUtils.kt @@ -24,6 +24,14 @@ object TextToBinaryUtils { return bytesAsBytes } + /** + * Converts a string of binary octets, such as "10010111 00010011", to a byte array. + */ + @JvmStatic + fun String.binaryStringToByteArray(): ByteArray { + return octetStringToByteArray(this, 2) + } + /** * Converts a string of hex octets, such as "BE EF", to a byte array. */ diff --git a/src/test/java/com/amazon/ion/bytecode/bin10/TypeIdHelperTest.kt b/src/test/java/com/amazon/ion/bytecode/bin10/TypeIdHelperTest.kt new file mode 100644 index 000000000..f212465a5 --- /dev/null +++ b/src/test/java/com/amazon/ion/bytecode/bin10/TypeIdHelperTest.kt @@ -0,0 +1,143 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode.bin10 + +import com.amazon.ion.IonType +import com.amazon.ion.bytecode.ir.OperationKind +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + +class TypeIdHelperTest { + + @ParameterizedTest + @CsvSource( + // Typed values + "0x0F, NULL", + "0x10, BOOL", + "0x11, BOOL", + "0x1F, BOOL", + "0x20, INT", + "0x2F, INT", + "0x31, INT", + "0x3F, INT", + "0x40, FLOAT", + "0x48, FLOAT", + "0x50, DECIMAL", + "0x5F, DECIMAL", + "0x63, TIMESTAMP", + "0x6F, TIMESTAMP", + "0x70, SYMBOL", + "0x7F, SYMBOL", + "0x80, STRING", + "0x8F, STRING", + "0x90, CLOB", + "0x9F, CLOB", + "0xA0, BLOB", + "0xAF, BLOB", + "0xB0, LIST", + "0xBF, LIST", + "0xC0, SEXP", + "0xCF, SEXP", + "0xD0, STRUCT", + "0xDF, STRUCT", + // TypeIds without an IonType + // NOTE: Empty string for the second value gets converted to Kotlin `null`. + // NOP + "0x00, ", + // IVM + "0xE0, ", + // Annotations + "0xE1, ", + "0xEF, ", + ) + fun testIonTypeForTypeId(typeId: Int, expectedType: IonType?) { + val result = TypeIdHelper.ionTypeForTypeId(typeId) + assertEquals(expectedType, result) + } + + @ParameterizedTest + @CsvSource( + "0x00, UNSET", + "0x0E, UNSET", + "0x0F, NULL", + "0x10, BOOL", + "0x11, BOOL", + "0x1F, BOOL", + "0x20, INT", + "0x2E, INT", + "0x2F, INT", + "0x30, UNSET", + "0x31, INT", + "0x3E, INT", + "0x3F, INT", + "0x40, FLOAT", + "0x48, FLOAT", + "0x4F, FLOAT", + "0x50, DECIMAL", + "0x5E, DECIMAL", + "0x5F, DECIMAL", + "0x63, TIMESTAMP", + "0x6E, TIMESTAMP", + "0x6F, TIMESTAMP", + "0x70, SYMBOL", + "0x7E, SYMBOL", + "0x7F, SYMBOL", + "0x80, STRING", + "0x8E, STRING", + "0x8F, STRING", + "0x90, CLOB", + "0x9E, CLOB", + "0x9F, CLOB", + "0xA0, BLOB", + "0xAE, BLOB", + "0xAF, BLOB", + "0xB0, LIST", + "0xBE, LIST", + "0xBF, LIST", + "0xC0, SEXP", + "0xCE, SEXP", + "0xCF, SEXP", + "0xD0, STRUCT", + "0xD1, UNSET", + "0xDE, STRUCT", + "0xDF, STRUCT", + "0xE0, IVM", + "0xE3, ANNOTATIONS", + "0xEE, ANNOTATIONS" + ) + fun testOperationKindForTypeId(typeId: Int, expectedOperationKind: String) { + val result = TypeIdHelper.operationKindForTypeId(typeId) + assertEquals(expectedOperationKind, OperationKind.nameOf(result)) + } + + @ParameterizedTest + @CsvSource( + "0x00, 0", // Length 0 + "0x01, 1", // Length 1 + "0x02, 2", // Length 2 + "0x0D, 13", // Length 13 + "0x0E, -1", // VarUInt follows + "0x0F, 0", // NULL (length 0) + "0x10, 0", // BOOL false (length 0) + "0x11, 0", // BOOL true (length 0) + "0x12, 2", // Length 2 + "0x1E, -1", // VarUInt follows + "0x1F, 0", // NULL (length 0) + "0x20, 0", // Length 0 + "0x21, 1", // Length 1 + "0x2E, -1", // VarUInt follows + "0x2F, 0", // NULL (length 0) + "0xE0, 3", // IVM (3 bytes after typeId) + "0xE1, -2", // Reserved (invalid) + "0xE3, 3", // Annotations (length 3) + "0xEE, -1", // Annotations (VarUInt follows) + "0xEF, -2", // Reserved (invalid) + "0xF0, -2", // Reserved (invalid) + "0xFF, -2" // Reserved (invalid) + ) + fun testTypeLengths(typeId: Int, expectedLength: Int) { + val result = TypeIdHelper.TYPE_LENGTHS[typeId] + assertEquals(expectedLength, result) + } +} diff --git a/src/test/java/com/amazon/ion/bytecode/bin10/ValueHelpersTest.kt b/src/test/java/com/amazon/ion/bytecode/bin10/ValueHelpersTest.kt new file mode 100644 index 000000000..449714beb --- /dev/null +++ b/src/test/java/com/amazon/ion/bytecode/bin10/ValueHelpersTest.kt @@ -0,0 +1,109 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode.bin10 + +import com.amazon.ion.Decimal +import com.amazon.ion.IonException +import com.amazon.ion.TextToBinaryUtils.hexStringToByteArray +import com.amazon.ion.Timestamp +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + +class ValueHelpersTest { + + @ParameterizedTest + @CsvSource( + "0x20, 1", + "0x21, 1", + "0x2E, 1", + "0x31, -1", + "0x32, -1", + "0x3E, -1", + ) + fun testSignForIntTypeId(typeId: Int, expectedSign: Int) { + val result = signForIntTypeId(typeId) + assertEquals(expectedSign, result) + } + + @ParameterizedTest + @CsvSource( + // The values in the comments assume the use of the Int encoding given at: + // https://amazon-ion.github.io/ion-docs/docs/binary.html#uint-and-int-fields + "00000000, 1", // 0 + "01111111, 1", // 127 + "10000000, -1", // -0 + "11111111, -1" // -127 + ) + fun testGetSignumValueFromLeadingSignBit(byteValue: String, expectedSignum: Int) { + val result = getSignumValueFromLeadingSignBit(byteValue.toInt(radix = 2).toByte()) + assertEquals(expectedSignum, result) + } + + @ParameterizedTest + @CsvSource( + "2001T, C0 0F D1", + "2001T, 80 0F D1", + "2001-01T, 81 0F D1 81", + "2001-01-01T, 80 0F D1 81 81", + "2001-01-01T00:00Z, 80 0F D1 81 81 80 80", + "2000-01-01T00:00:00Z, 80 0F D0 81 81 80 80 80", // 2000-01-01T00:00:00Z with no fractional seconds + "2000-01-01T00:00:00Z, 80 0F D0 81 81 80 80 80 80", // The same instant with 0d0 fractional seconds and implicit zero coefficient + "2000-01-01T00:00:00Z, 80 0F D0 81 81 80 80 80 80 00", // The same instant with 0d0 fractional seconds and explicit zero coefficient + "2000-01-01T00:00:00Z, 80 0F D0 81 81 80 80 80 C0", // The same instant with 0d-0 fractional seconds + "2000-01-01T00:00:00Z, 80 0F D0 81 81 80 80 80 81", // The same instant with 0d1 fractional seconds + "2000-01-01T00:00:00.0Z, 80 0F D0 81 81 80 80 80 C1", + "2000-01-01T00:00:00.00Z, 80 0F D0 81 81 80 80 80 C2", + "2000-01-02T03:04:05.06Z, 80 0F D0 81 82 83 84 85 C2 06", + ) + fun testReadTimestampReference(expectedTimestamp: String, hexString: String) { + val bytes = hexString.hexStringToByteArray() + val expected = Timestamp.valueOf(expectedTimestamp) + val actual = readTimestampReference(bytes, 0, bytes.size) + println(actual.toString()) + assertEquals(expected, actual) + } + + @Test + fun testReadTimestampReference_ThrowsOnHourWithoutMinute() { + // 80 = offset 0, 0F D1 = year 2001 (VarUInt encoded), 81 = month 1, 81 = day 1, 80 = hour 0 (but no minute) + val bytes = "80 0F D1 81 81 80".hexStringToByteArray() + + assertThrows(IonException::class.java) { + readTimestampReference(bytes, 0, 6) + } + } + + @ParameterizedTest + @CsvSource( + "0.00, C2", + "0.0, C1", + "0., 80", + "0e1, 81", + "0.00, C2 00", + "0.0, C1 00", + "0., 80 00", + "0e1, 81 00", + "-0.00, C2 80", + "-0.0, C1 80", + "-0., 80 80", + "-0e1, 81 80", + "0.01, C2 01", + "0.1, C1 01", + "1., 80 01", + "1e1, 81 01", + "-0.01, C2 81", + "-0.1, C1 81", + "-1., 80 81", + "-1e1, 81 81", + ) + fun testReadDecimalReference(expectedDecimalString: String, hexString: String) { + val bytes = hexString.hexStringToByteArray() + val expected = Decimal.valueOf(expectedDecimalString) + val actual = readDecimalReference(bytes, 0, bytes.size) + assertEquals(expected, actual) + assertEquals(expected.isNegativeZero, actual.isNegativeZero()) + } +} diff --git a/src/test/java/com/amazon/ion/bytecode/bin10/VarIntHelperTest.kt b/src/test/java/com/amazon/ion/bytecode/bin10/VarIntHelperTest.kt new file mode 100644 index 000000000..ed75e76cc --- /dev/null +++ b/src/test/java/com/amazon/ion/bytecode/bin10/VarIntHelperTest.kt @@ -0,0 +1,95 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode.bin10 + +import com.amazon.ion.TextToBinaryUtils.binaryStringToByteArray +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + +class VarIntHelperTest { + + @ParameterizedTest + @CsvSource( + "0, 10000000", + "0, 00000000 10000000", + "0, 00000000 00000000 10000000", + "0, 00000000 00000000 00000000 10000000", + "1, 10000001", + "1, 00000000 10000001", + "1, 00000000 00000000 10000001", + "1, 00000000 00000000 00000000 10000001", + "2, 10000010", + "127, 11111111", + "128, 00000001 10000000", + "129, 00000001 10000001", + "16383, 01111111 11111111", + "16384, 00000001 00000000 10000000", + "16513, 00000001 00000001 10000001", + ) + fun testReadVarUIntValueAndLength_ByteArray(expectedValue: Long, binaryString: String) { + val bytes = binaryString.binaryStringToByteArray() + val expectedLength = bytes.size + + val result = VarIntHelper.readVarUIntValueAndLength(bytes, 0) + val actualLength = result.toInt() and 0xFF + val actualValue = result ushr 8 + + assertEquals(expectedValue, actualValue) + assertEquals(expectedLength, actualLength) + } + + @ParameterizedTest + @CsvSource( + + "0, 10000000", + "0, 00000000 10000000", + "0, 00000000 00000000 10000000", + "0, 00000000 00000000 00000000 10000000", + "1, 10000001", + "1, 00000000 10000001", + "1, 00000000 00000000 10000001", + "1, 00000000 00000000 00000000 10000001", + "-1, 11000001", + "-1, 01000000 10000001", + "-1, 01000000 00000000 10000001", + "-1, 01000000 00000000 00000000 10000001", + "63, 10111111", + "64, 00000000 11000000", + "-63, 11111111", + "-64, 01000000 11000000", + "128, 00000001 10000000", + "129, 00000001 10000001", + "-128, 01000001 10000000", + "-129, 01000001 10000001", + ) + fun testReadVarIntValueAndLength_ByteArray(expectedValue: Long, binaryString: String) { + val bytes = binaryString.binaryStringToByteArray() + val expectedLength = bytes.size + + val result = VarIntHelper.readVarIntValueAndLength(bytes, 0) + val actualLength = result.toInt() and 0xFF + val actualValue = result shr 8 + + assertEquals(expectedValue, actualValue) + assertEquals(expectedLength, actualLength) + } + + @ParameterizedTest + @CsvSource( + // The values in the comments assume the use of the Int encoding given at: + // https://amazon-ion.github.io/ion-docs/docs/binary.html#varuint-and-varint-fields + "00000000, 1", // 0 + "00111111, 1", // 63 + "01000000, -1", // -0 + "01111111, -1", // -63 + "10000000, 1", // 0 + "10111111, 1", // 63 + "11000000, -1", // -0 + "11111111, -1", // -63 + ) + fun testGetSignumValueFromVarIntSignBit(byteValue: String, expectedSignum: Int) { + val result = VarIntHelper.getSignumValueFromVarIntSignBit(byteValue.toInt(radix = 2)) + assertEquals(expectedSignum, result) + } +}