diff --git a/src/main/java/com/amazon/ion/bytecode/BytecodeEmitter.kt b/src/main/java/com/amazon/ion/bytecode/BytecodeEmitter.kt index bb320365e..4b75f7ca5 100644 --- a/src/main/java/com/amazon/ion/bytecode/BytecodeEmitter.kt +++ b/src/main/java/com/amazon/ion/bytecode/BytecodeEmitter.kt @@ -84,7 +84,7 @@ internal object BytecodeEmitter { } @JvmStatic - fun emitShortTimestampReference(destination: BytecodeBuffer, precisionAndOffsetMode: Int, dataPosition: Int) { - destination.add2(Instructions.I_SHORT_TIMESTAMP_REF.packInstructionData(precisionAndOffsetMode), dataPosition) + fun emitShortTimestampReference(destination: BytecodeBuffer, opcode: Int, dataPosition: Int) { + destination.add2(Instructions.I_SHORT_TIMESTAMP_REF.packInstructionData(opcode), dataPosition) } } diff --git a/src/main/java/com/amazon/ion/bytecode/bin11/ByteArrayBytecodeGenerator11.kt b/src/main/java/com/amazon/ion/bytecode/bin11/ByteArrayBytecodeGenerator11.kt index e28c6ab48..8ad30d423 100644 --- a/src/main/java/com/amazon/ion/bytecode/bin11/ByteArrayBytecodeGenerator11.kt +++ b/src/main/java/com/amazon/ion/bytecode/bin11/ByteArrayBytecodeGenerator11.kt @@ -2,14 +2,95 @@ // SPDX-License-Identifier: Apache-2.0 package com.amazon.ion.bytecode.bin11 +import com.amazon.ion.Decimal +import com.amazon.ion.IonException +import com.amazon.ion.Timestamp +import com.amazon.ion.bytecode.BytecodeGenerator +import com.amazon.ion.bytecode.bin11.bytearray.OpcodeHandlerTable +import com.amazon.ion.bytecode.bin11.bytearray.PrimitiveDecoder.readFixedIntAsBigInteger +import com.amazon.ion.bytecode.bin11.bytearray.TimestampDecoder +import com.amazon.ion.bytecode.ir.Instructions +import com.amazon.ion.bytecode.util.AppendableConstantPoolView +import com.amazon.ion.bytecode.util.ByteSlice +import com.amazon.ion.bytecode.util.BytecodeBuffer +import com.amazon.ion.bytecode.util.unsignedToInt +import com.amazon.ion.impl.bin.utf8.Utf8StringDecoder +import com.amazon.ion.impl.bin.utf8.Utf8StringDecoderPool import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import java.math.BigInteger +import java.nio.ByteBuffer @SuppressFBWarnings("EI_EXPOSE_REP2", justification = "constructor does not make a defensive copy of source as a performance optimization") -internal class ByteArrayBytecodeGenerator11 -@SuppressFBWarnings("URF_UNREAD_FIELD", justification = "field will be read once this class is implemented") -constructor( +internal class ByteArrayBytecodeGenerator11( private val source: ByteArray, - private var i: Int, -) { - // TODO: This should implement BytecodeGenerator + private var currentPosition: Int, +) : BytecodeGenerator { + private val utf8Decoder: Utf8StringDecoder = Utf8StringDecoderPool.getInstance().orCreate + + override fun refill( + destination: BytecodeBuffer, + constantPool: AppendableConstantPoolView, + macroSrc: IntArray, + macroIndices: IntArray, + symTab: Array + ) { + var opcode = -1 + while (currentPosition < source.size && !isSystemValue(opcode)) { + opcode = source[currentPosition++].unsignedToInt() + val handler = OpcodeHandlerTable.handler(opcode) + currentPosition += handler.convertOpcodeToBytecode( + opcode, + source, + currentPosition, + destination, + constantPool, + macroSrc, + macroIndices, + symTab + ) + } + + if (currentPosition >= source.size) { + destination.add(Instructions.I_END_OF_INPUT) + } + } + + override fun readBigIntegerReference(position: Int, length: Int): BigInteger { + return readFixedIntAsBigInteger(source, position, length) + } + + override fun readDecimalReference(position: Int, length: Int): Decimal { + TODO("Not yet implemented") + } + + override fun readShortTimestampReference(position: Int, opcode: Int): Timestamp { + return TimestampDecoder.readShortTimestamp(source, position, opcode) + } + + override fun readTimestampReference(position: Int, length: Int): Timestamp { + TODO("Not yet implemented") + } + + override fun readTextReference(position: Int, length: Int): String { + val buffer = ByteBuffer.wrap(source, position, length) + return utf8Decoder.decode(buffer, length) + } + + override fun readBytesReference(position: Int, length: Int): ByteSlice { + return ByteSlice(source, position, position + length) + } + + override fun ionMinorVersion(): Int = 1 + + override fun getGeneratorForMinorVersion(minorVersion: Int): BytecodeGenerator { + return when (minorVersion) { + 1 -> ByteArrayBytecodeGenerator11(source, currentPosition) + // TODO: update with ByteArrayBytecodeGenerator10 once it implements BytecodeGenerator + else -> throw IonException("Minor version $minorVersion not yet implemented for ByteArray-backed data sources.") + } + } + + private fun isSystemValue(opcode: Int): Boolean { + return opcode in 0xE0..0xE8 + } } diff --git a/src/main/java/com/amazon/ion/bytecode/bin11/bytearray/PrimitiveDecoder.kt b/src/main/java/com/amazon/ion/bytecode/bin11/bytearray/PrimitiveDecoder.kt index ece6e9336..c2f3453aa 100644 --- a/src/main/java/com/amazon/ion/bytecode/bin11/bytearray/PrimitiveDecoder.kt +++ b/src/main/java/com/amazon/ion/bytecode/bin11/bytearray/PrimitiveDecoder.kt @@ -73,6 +73,17 @@ internal object PrimitiveDecoder { } } + @JvmStatic + fun readFixedIntAsBigInteger(source: ByteArray, start: Int, length: Int): BigInteger { + // TODO: ion-java#1114 + if (source.size < start + length) throw IonException("Incomplete data: start=$start, length=$length, limit=${source.size}") + val bytes = ByteArray(length) + for (i in 0 until length) { + bytes[i] = source[start + length - i - 1] + } + return BigInteger(bytes) + } + @JvmStatic fun readFixedUInt16(source: ByteArray, position: Int): UShort { // TODO: ion-java#1114 diff --git a/src/main/java/com/amazon/ion/bytecode/bin11/bytearray/ShortTimestampOpcodeHandler.kt b/src/main/java/com/amazon/ion/bytecode/bin11/bytearray/ShortTimestampOpcodeHandler.kt index 3d27afe8f..d75e9239b 100644 --- a/src/main/java/com/amazon/ion/bytecode/bin11/bytearray/ShortTimestampOpcodeHandler.kt +++ b/src/main/java/com/amazon/ion/bytecode/bin11/bytearray/ShortTimestampOpcodeHandler.kt @@ -40,12 +40,12 @@ internal object ShortTimestampOpcodeHandler : OpcodeToBytecodeHandler { macroIndices: IntArray, symbolTable: Array ): Int { - val precisionAndOffsetMode = opcode and 0xF BytecodeEmitter.emitShortTimestampReference( destination, - precisionAndOffsetMode, + opcode, position ) + val precisionAndOffsetMode = opcode and 0xF return serializedSizeByOpcodeTable[precisionAndOffsetMode] } } diff --git a/src/main/java/com/amazon/ion/bytecode/bin11/bytearray/TimestampDecoder.kt b/src/main/java/com/amazon/ion/bytecode/bin11/bytearray/TimestampDecoder.kt new file mode 100644 index 000000000..d2c77f763 --- /dev/null +++ b/src/main/java/com/amazon/ion/bytecode/bin11/bytearray/TimestampDecoder.kt @@ -0,0 +1,230 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode.bin11.bytearray + +import com.amazon.ion.IonException +import com.amazon.ion.Timestamp +import com.amazon.ion.bytecode.bin11.bytearray.PrimitiveDecoder.readFixedInt16 +import com.amazon.ion.bytecode.bin11.bytearray.PrimitiveDecoder.readFixedInt32 +import com.amazon.ion.bytecode.bin11.bytearray.PrimitiveDecoder.readFixedInt8AsShort +import com.amazon.ion.bytecode.bin11.bytearray.PrimitiveDecoder.readFixedIntAsLong +import com.amazon.ion.impl.bin.Ion_1_1_Constants.S_O_TIMESTAMP_FRACTION_BIT_OFFSET +import com.amazon.ion.impl.bin.Ion_1_1_Constants.S_O_TIMESTAMP_OFFSET_BIT_OFFSET +import com.amazon.ion.impl.bin.Ion_1_1_Constants.S_O_TIMESTAMP_SECOND_BIT_OFFSET +import com.amazon.ion.impl.bin.Ion_1_1_Constants.S_TIMESTAMP_DAY_BIT_OFFSET +import com.amazon.ion.impl.bin.Ion_1_1_Constants.S_TIMESTAMP_HOUR_BIT_OFFSET +import com.amazon.ion.impl.bin.Ion_1_1_Constants.S_TIMESTAMP_MINUTE_BIT_OFFSET +import com.amazon.ion.impl.bin.Ion_1_1_Constants.S_TIMESTAMP_MONTH_BIT_OFFSET +import com.amazon.ion.impl.bin.Ion_1_1_Constants.S_U_TIMESTAMP_FRACTION_BIT_OFFSET +import com.amazon.ion.impl.bin.Ion_1_1_Constants.S_U_TIMESTAMP_SECOND_BIT_OFFSET +import com.amazon.ion.impl.bin.Ion_1_1_Constants.S_U_TIMESTAMP_UTC_FLAG +import com.amazon.ion.impl.bin.Ion_1_1_Constants.S_U_TIMESTAMP_UTC_FLAG_L +import java.math.BigDecimal + +/** + * Helper class for decoding the various timestamp encoding variants from a [ByteArray]. + * + * TODO(perf): avoid auto-boxing the `0` integer for the offset when constructing the Timestamp instance. + */ +internal object TimestampDecoder { + private const val MASK_4 = 0b1111 + private const val MASK_5 = 0b11111 + private const val MASK_6 = 0b111111 + private const val MASK_7 = 0b1111111 + private const val MASK_4L = 0b1111L + private const val MASK_5L = 0b11111L + private const val MASK_6L = 0b111111L + private const val MASK_7L = 0b1111111L + private const val MASK_10L = 0b1111111111L + private const val MASK_20L = 0b11111111111111111111L + private const val MASK_30L = 0b111111111111111111111111111111L + + fun readTimestampToYear(source: ByteArray, position: Int): Timestamp { + val year = readFixedInt8AsShort(source, position).toInt() + return Timestamp.forYear(year + 1970) + } + + fun readTimestampToMonth(source: ByteArray, position: Int): Timestamp { + val yearAndMonth = readFixedInt16(source, position).toInt() + val year = yearAndMonth.and(MASK_7) + val month = yearAndMonth.shr(S_TIMESTAMP_MONTH_BIT_OFFSET) + + return Timestamp.forMonth(year + 1970, month) + } + + fun readTimestampToDay(source: ByteArray, position: Int): Timestamp { + val yearMonthAndDay = readFixedInt16(source, position).toInt() + val year = yearMonthAndDay.and(MASK_7) + val month = yearMonthAndDay.shr(S_TIMESTAMP_MONTH_BIT_OFFSET).and(MASK_4) + val day = yearMonthAndDay.shr(S_TIMESTAMP_DAY_BIT_OFFSET).and(MASK_5) + + return Timestamp.forDay(year + 1970, month, day) + } + + fun readTimestampToMinuteUTCOrUnknown(source: ByteArray, position: Int): Timestamp { + val data = readFixedInt32(source, position) + val year = data.and(MASK_7) + val month = data.shr(S_TIMESTAMP_MONTH_BIT_OFFSET).and(MASK_4) + val day = data.shr(S_TIMESTAMP_DAY_BIT_OFFSET).and(MASK_5) + val hour = data.shr(S_TIMESTAMP_HOUR_BIT_OFFSET).and(MASK_5) + val minute = data.shr(S_TIMESTAMP_MINUTE_BIT_OFFSET).and(MASK_6) + val isUTC = data.and(S_U_TIMESTAMP_UTC_FLAG) != 0 + + return Timestamp.forMinute(year + 1970, month, day, hour, minute, if (isUTC) 0 else null) + } + + fun readTimestampToSecondUTCOrUnknown(source: ByteArray, position: Int): Timestamp { + val data = readFixedIntAsLong(source, position, 5) + val year = data.and(MASK_7L).toInt() + val month = data.shr(S_TIMESTAMP_MONTH_BIT_OFFSET).and(MASK_4L).toInt() + val day = data.shr(S_TIMESTAMP_DAY_BIT_OFFSET).and(MASK_5L).toInt() + val hour = data.shr(S_TIMESTAMP_HOUR_BIT_OFFSET).and(MASK_5L).toInt() + val minute = data.shr(S_TIMESTAMP_MINUTE_BIT_OFFSET).and(MASK_6L).toInt() + val second = data.shr(S_U_TIMESTAMP_SECOND_BIT_OFFSET).and(MASK_6L).toInt() + val isUTC = data.and(S_U_TIMESTAMP_UTC_FLAG_L) != 0L + + return Timestamp.forSecond(year + 1970, month, day, hour, minute, second, if (isUTC) 0 else null) + } + + fun readTimestampToMillisecondUTCOrUnknown(source: ByteArray, position: Int): Timestamp { + val data = readFixedIntAsLong(source, position, 6) + val year = data.and(MASK_7L).toInt() + val month = data.shr(S_TIMESTAMP_MONTH_BIT_OFFSET).and(MASK_4L).toInt() + val day = data.shr(S_TIMESTAMP_DAY_BIT_OFFSET).and(MASK_5L).toInt() + val hour = data.shr(S_TIMESTAMP_HOUR_BIT_OFFSET).and(MASK_5L).toInt() + val minute = data.shr(S_TIMESTAMP_MINUTE_BIT_OFFSET).and(MASK_6L).toInt() + val second = data.shr(S_U_TIMESTAMP_SECOND_BIT_OFFSET).and(MASK_6L) + val fractionalSecond = data.shr(S_U_TIMESTAMP_FRACTION_BIT_OFFSET).and(MASK_10L) + val isUTC = data.and(S_U_TIMESTAMP_UTC_FLAG_L) != 0L + + val secondBigDecimal = BigDecimal.valueOf(second) + val fractionalSecondBigDecimal = BigDecimal.valueOf(fractionalSecond, 3) + return Timestamp.forSecond(year + 1970, month, day, hour, minute, secondBigDecimal.add(fractionalSecondBigDecimal), if (isUTC) 0 else null) + } + + fun readTimestampToMicrosecondUTCOrUnknown(source: ByteArray, position: Int): Timestamp { + val data = readFixedIntAsLong(source, position, 7) + val year = data.and(MASK_7L).toInt() + val month = data.shr(S_TIMESTAMP_MONTH_BIT_OFFSET).and(MASK_4L).toInt() + val day = data.shr(S_TIMESTAMP_DAY_BIT_OFFSET).and(MASK_5L).toInt() + val hour = data.shr(S_TIMESTAMP_HOUR_BIT_OFFSET).and(MASK_5L).toInt() + val minute = data.shr(S_TIMESTAMP_MINUTE_BIT_OFFSET).and(MASK_6L).toInt() + val second = data.shr(S_U_TIMESTAMP_SECOND_BIT_OFFSET).and(MASK_6L) + val fractionalSecond = data.shr(S_U_TIMESTAMP_FRACTION_BIT_OFFSET).and(MASK_20L) + val isUTC = data.and(S_U_TIMESTAMP_UTC_FLAG_L) != 0L + + val secondBigDecimal = BigDecimal.valueOf(second) + val fractionalSecondBigDecimal = BigDecimal.valueOf(fractionalSecond, 6) + return Timestamp.forSecond(year + 1970, month, day, hour, minute, secondBigDecimal.add(fractionalSecondBigDecimal), if (isUTC) 0 else null) + } + + fun readTimestampToNanosecondUTCOrUnknown(source: ByteArray, position: Int): Timestamp { + val data = readFixedIntAsLong(source, position, 8) + val year = data.and(MASK_7L).toInt() + val month = data.shr(S_TIMESTAMP_MONTH_BIT_OFFSET).and(MASK_4L).toInt() + val day = data.shr(S_TIMESTAMP_DAY_BIT_OFFSET).and(MASK_5L).toInt() + val hour = data.shr(S_TIMESTAMP_HOUR_BIT_OFFSET).and(MASK_5L).toInt() + val minute = data.shr(S_TIMESTAMP_MINUTE_BIT_OFFSET).and(MASK_6L).toInt() + val second = data.shr(S_U_TIMESTAMP_SECOND_BIT_OFFSET).and(MASK_6L) + val fractionalSecond = data.ushr(S_U_TIMESTAMP_FRACTION_BIT_OFFSET).and(MASK_30L) + val isUTC = data.and(S_U_TIMESTAMP_UTC_FLAG_L) != 0L + + val secondBigDecimal = BigDecimal.valueOf(second) + val fractionalSecondBigDecimal = BigDecimal.valueOf(fractionalSecond, 9) + return Timestamp.forSecond(year + 1970, month, day, hour, minute, secondBigDecimal.add(fractionalSecondBigDecimal), if (isUTC) 0 else null) + } + + fun readTimestampToMinuteWithOffset(source: ByteArray, position: Int): Timestamp { + val data = readFixedIntAsLong(source, position, 5) + val year = data.and(MASK_7L).toInt() + val month = data.shr(S_TIMESTAMP_MONTH_BIT_OFFSET).and(MASK_4L).toInt() + val day = data.shr(S_TIMESTAMP_DAY_BIT_OFFSET).and(MASK_5L).toInt() + val hour = data.shr(S_TIMESTAMP_HOUR_BIT_OFFSET).and(MASK_5L).toInt() + val minute = data.shr(S_TIMESTAMP_MINUTE_BIT_OFFSET).and(MASK_6L).toInt() + val offset = data.shr(S_O_TIMESTAMP_OFFSET_BIT_OFFSET).and(MASK_7L).toInt() + + return Timestamp.forMinute(year + 1970, month, day, hour, minute, (offset - 56) * 15) + } + + fun readTimestampToSecondWithOffset(source: ByteArray, position: Int): Timestamp { + val data = readFixedIntAsLong(source, position, 5) + val year = data.and(MASK_7L).toInt() + val month = data.shr(S_TIMESTAMP_MONTH_BIT_OFFSET).and(MASK_4L).toInt() + val day = data.shr(S_TIMESTAMP_DAY_BIT_OFFSET).and(MASK_5L).toInt() + val hour = data.shr(S_TIMESTAMP_HOUR_BIT_OFFSET).and(MASK_5L).toInt() + val minute = data.shr(S_TIMESTAMP_MINUTE_BIT_OFFSET).and(MASK_6L).toInt() + val offset = data.shr(S_O_TIMESTAMP_OFFSET_BIT_OFFSET).and(MASK_7L).toInt() + val second = data.shr(S_O_TIMESTAMP_SECOND_BIT_OFFSET).and(MASK_6L).toInt() + + return Timestamp.forSecond(year + 1970, month, day, hour, minute, second, (offset - 56) * 15) + } + + fun readTimestampToMillisecondWithOffset(source: ByteArray, position: Int): Timestamp { + val data = readFixedIntAsLong(source, position, 7) + val year = data.and(MASK_7L).toInt() + val month = data.shr(S_TIMESTAMP_MONTH_BIT_OFFSET).and(MASK_4L).toInt() + val day = data.shr(S_TIMESTAMP_DAY_BIT_OFFSET).and(MASK_5L).toInt() + val hour = data.shr(S_TIMESTAMP_HOUR_BIT_OFFSET).and(MASK_5L).toInt() + val minute = data.shr(S_TIMESTAMP_MINUTE_BIT_OFFSET).and(MASK_6L).toInt() + val offset = data.shr(S_O_TIMESTAMP_OFFSET_BIT_OFFSET).and(MASK_7L).toInt() + val second = data.shr(S_O_TIMESTAMP_SECOND_BIT_OFFSET).and(MASK_6L) + val fractionalSecond = data.shr(S_O_TIMESTAMP_FRACTION_BIT_OFFSET).and(MASK_10L) + + val secondBigDecimal = BigDecimal.valueOf(second) + val fractionalSecondBigDecimal = BigDecimal.valueOf(fractionalSecond, 3) + return Timestamp.forSecond(year + 1970, month, day, hour, minute, secondBigDecimal.add(fractionalSecondBigDecimal), (offset - 56) * 15) + } + + fun readTimestampToMicrosecondWithOffset(source: ByteArray, position: Int): Timestamp { + val data = readFixedIntAsLong(source, position, 8) + val year = data.and(MASK_7L).toInt() + val month = data.shr(S_TIMESTAMP_MONTH_BIT_OFFSET).and(MASK_4L).toInt() + val day = data.shr(S_TIMESTAMP_DAY_BIT_OFFSET).and(MASK_5L).toInt() + val hour = data.shr(S_TIMESTAMP_HOUR_BIT_OFFSET).and(MASK_5L).toInt() + val minute = data.shr(S_TIMESTAMP_MINUTE_BIT_OFFSET).and(MASK_6L).toInt() + val offset = data.shr(S_O_TIMESTAMP_OFFSET_BIT_OFFSET).and(MASK_7L).toInt() + val second = data.shr(S_O_TIMESTAMP_SECOND_BIT_OFFSET).and(MASK_6L) + val fractionalSecond = data.shr(S_O_TIMESTAMP_FRACTION_BIT_OFFSET).and(MASK_20L) + + val secondBigDecimal = BigDecimal.valueOf(second) + val fractionalSecondBigDecimal = BigDecimal.valueOf(fractionalSecond, 6) + return Timestamp.forSecond(year + 1970, month, day, hour, minute, secondBigDecimal.add(fractionalSecondBigDecimal), (offset - 56) * 15) + } + + fun readTimestampToNanosecondWithOffset(source: ByteArray, position: Int): Timestamp { + val data = readFixedIntAsLong(source, position, 8) + val highFractionalSecondByte = readFixedInt8AsShort(source, position + 8).toLong().and(MASK_6L) + val year = data.and(MASK_7L).toInt() + val month = data.shr(S_TIMESTAMP_MONTH_BIT_OFFSET).and(MASK_4L).toInt() + val day = data.shr(S_TIMESTAMP_DAY_BIT_OFFSET).and(MASK_5L).toInt() + val hour = data.shr(S_TIMESTAMP_HOUR_BIT_OFFSET).and(MASK_5L).toInt() + val minute = data.shr(S_TIMESTAMP_MINUTE_BIT_OFFSET).and(MASK_6L).toInt() + val offset = data.shr(S_O_TIMESTAMP_OFFSET_BIT_OFFSET).and(MASK_7L).toInt() + val second = data.shr(S_O_TIMESTAMP_SECOND_BIT_OFFSET).and(MASK_6L) + val fractionalSecond = data.ushr(S_O_TIMESTAMP_FRACTION_BIT_OFFSET).or(highFractionalSecondByte.shl(24)) + + val secondBigDecimal = BigDecimal.valueOf(second) + val fractionalSecondBigDecimal = BigDecimal.valueOf(fractionalSecond, 9) + return Timestamp.forSecond(year + 1970, month, day, hour, minute, secondBigDecimal.add(fractionalSecondBigDecimal), (offset - 56) * 15) + } + + @OptIn(ExperimentalStdlibApi::class) // for Byte.toHexString() + fun readShortTimestamp(source: ByteArray, position: Int, opcode: Int): Timestamp { + return when (opcode) { + 0x80 -> readTimestampToYear(source, position) + 0x81 -> readTimestampToMonth(source, position) + 0x82 -> readTimestampToDay(source, position) + 0x83 -> readTimestampToMinuteUTCOrUnknown(source, position) + 0x84 -> readTimestampToSecondUTCOrUnknown(source, position) + 0x85 -> readTimestampToMillisecondUTCOrUnknown(source, position) + 0x86 -> readTimestampToMicrosecondUTCOrUnknown(source, position) + 0x87 -> readTimestampToNanosecondUTCOrUnknown(source, position) + 0x88 -> readTimestampToMinuteWithOffset(source, position) + 0x89 -> readTimestampToSecondWithOffset(source, position) + 0x8a -> readTimestampToMillisecondWithOffset(source, position) + 0x8b -> readTimestampToMicrosecondWithOffset(source, position) + 0x8c -> readTimestampToNanosecondWithOffset(source, position) + else -> throw IonException("Unrecognized short timestamp opcode ${opcode.toByte().toHexString()}") + } + } +} diff --git a/src/main/java/com/amazon/ion/impl/bin/Ion_1_1_Constants.java b/src/main/java/com/amazon/ion/impl/bin/Ion_1_1_Constants.java index 91a764704..0d360b6ea 100644 --- a/src/main/java/com/amazon/ion/impl/bin/Ion_1_1_Constants.java +++ b/src/main/java/com/amazon/ion/impl/bin/Ion_1_1_Constants.java @@ -1,3 +1,5 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 package com.amazon.ion.impl.bin; /** @@ -6,31 +8,36 @@ public class Ion_1_1_Constants { private Ion_1_1_Constants() {} + public static final int FIRST_2_BYTE_SYMBOL_ADDRESS = 256; + public static final int FIRST_MANY_BYTE_SYMBOL_ADDRESS = 65792; + //////// Timestamp Field Constants //////// // S_TIMESTAMP_* is applicable to all short-form timestamps - static final int S_TIMESTAMP_MONTH_BIT_OFFSET = 7; - static final int S_TIMESTAMP_DAY_BIT_OFFSET = 11; - static final int S_TIMESTAMP_HOUR_BIT_OFFSET = 16; - static final int S_TIMESTAMP_MINUTE_BIT_OFFSET = 21; + public static final int S_TIMESTAMP_MONTH_BIT_OFFSET = 7; + public static final int S_TIMESTAMP_DAY_BIT_OFFSET = 11; + public static final int S_TIMESTAMP_HOUR_BIT_OFFSET = 16; + public static final int S_TIMESTAMP_MINUTE_BIT_OFFSET = 21; // S_U_TIMESTAMP_* is applicable to all short-form timestamps with a `U` bit - static final int S_U_TIMESTAMP_UTC_FLAG = 1 << 27; - static final int S_U_TIMESTAMP_SECOND_BIT_OFFSET = 28; - static final int S_U_TIMESTAMP_FRACTION_BIT_OFFSET = 34; + public static final int S_U_TIMESTAMP_UTC_FLAG = 1 << 27; + public static final long S_U_TIMESTAMP_UTC_FLAG_L = S_U_TIMESTAMP_UTC_FLAG; + public static final int S_U_TIMESTAMP_SECOND_BIT_OFFSET = 28; + public static final int S_U_TIMESTAMP_FRACTION_BIT_OFFSET = 34; // S_O_TIMESTAMP_* is applicable to all short-form timestamps with `o` (offset) bits - static final int S_O_TIMESTAMP_OFFSET_BIT_OFFSET = 27; - static final int S_O_TIMESTAMP_SECOND_BIT_OFFSET = 34; + public static final int S_O_TIMESTAMP_OFFSET_BIT_OFFSET = 27; + public static final int S_O_TIMESTAMP_SECOND_BIT_OFFSET = 34; + public static final int S_O_TIMESTAMP_FRACTION_BIT_OFFSET = 40; // L_TIMESTAMP_* is applicable to all long-form timestamps - static final int L_TIMESTAMP_MONTH_BIT_OFFSET = 14; - static final int L_TIMESTAMP_DAY_BIT_OFFSET = 18; - static final int L_TIMESTAMP_HOUR_BIT_OFFSET = 23; - static final int L_TIMESTAMP_MINUTE_BIT_OFFSET = 28; - static final int L_TIMESTAMP_OFFSET_BIT_OFFSET = 34; - static final int L_TIMESTAMP_SECOND_BIT_OFFSET = 46; - static final int L_TIMESTAMP_UNKNOWN_OFFSET_VALUE = 0b111111111111; + public static final int L_TIMESTAMP_MONTH_BIT_OFFSET = 14; + public static final int L_TIMESTAMP_DAY_BIT_OFFSET = 18; + public static final int L_TIMESTAMP_HOUR_BIT_OFFSET = 23; + public static final int L_TIMESTAMP_MINUTE_BIT_OFFSET = 28; + public static final int L_TIMESTAMP_OFFSET_BIT_OFFSET = 34; + public static final int L_TIMESTAMP_SECOND_BIT_OFFSET = 46; + public static final int L_TIMESTAMP_UNKNOWN_OFFSET_VALUE = 0b111111111111; //////// Bit masks //////// - static final long LEAST_SIGNIFICANT_7_BITS = 0b01111111L; + public static final long LEAST_SIGNIFICANT_7_BITS = 0b01111111L; } diff --git a/src/test/java/com/amazon/ion/bytecode/bin11/ByteArrayBytecodeGenerator11Test.kt b/src/test/java/com/amazon/ion/bytecode/bin11/ByteArrayBytecodeGenerator11Test.kt new file mode 100644 index 000000000..39e73ce92 --- /dev/null +++ b/src/test/java/com/amazon/ion/bytecode/bin11/ByteArrayBytecodeGenerator11Test.kt @@ -0,0 +1,142 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode.bin11 + +import com.amazon.ion.TextToBinaryUtils.cleanCommentedHexBytes +import com.amazon.ion.TextToBinaryUtils.hexStringToByteArray +import com.amazon.ion.Timestamp +import com.amazon.ion.bytecode.GeneratorTestUtil.shouldGenerate +import com.amazon.ion.bytecode.ir.Instructions +import com.amazon.ion.bytecode.ir.Instructions.packInstructionData +import com.amazon.ion.impl.bin.PrimitiveEncoder +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.junit.jupiter.params.provider.ValueSource +import java.nio.charset.StandardCharsets + +internal class ByteArrayBytecodeGenerator11Test { + + @Test + fun `generator can compile input containing multiple simple opcodes`() { + val inputBytesString = """ + 64 4F 97 21 C5 | int -987654321 + 86 35 7D CB 12 2E 22 1B | timestamp 2023-10-15T11:22:33.444555-00:00 + 8F 0C | null.struct + 6A | float 0e0 + 6D 18 2D 44 54 FB 21 09 40 | float 3.141592653589793 + FE 31 | 24-byte blob + 49 20 61 70 70 6c 61 75 | + 64 20 79 6f 75 72 20 63 | + 75 72 69 6f 73 69 74 79 | + 6F | false + """.cleanCommentedHexBytes() + val f64pi = 3.141592653589793 + val expectedBytecode = intArrayOf( + Instructions.I_INT_I32, -987654321, + Instructions.I_SHORT_TIMESTAMP_REF.packInstructionData(0x86), 6, + Instructions.I_NULL_STRUCT, + Instructions.I_FLOAT_F32, 0, + Instructions.I_FLOAT_F64, f64pi.toRawBits().ushr(32).and(0xFFFFFFFF).toInt(), f64pi.toRawBits().toInt(), + Instructions.I_BLOB_REF.packInstructionData(24), 27, + Instructions.I_BOOL.packInstructionData(0), + Instructions.I_END_OF_INPUT + ) + + val bytes = inputBytesString.hexStringToByteArray() + val generator = ByteArrayBytecodeGenerator11(bytes, 0) + generator.shouldGenerate(expectedBytecode) + } + + // TODO: add tests cases for more complicated cases like nested containers, macro compilation, annots., etc. + // once those features are implemented + + @ParameterizedTest + @CsvSource( + "80 35, 2023T", + "81 35 05, 2023-10T", + "82 35 7D, 2023-10-15T", + "83 35 7D CB 0A, 2023-10-15T11:22Z", + "84 35 7D CB 1A 02, 2023-10-15T11:22:33Z", + "84 35 7D CB 12 02, 2023-10-15T11:22:33-00:00", + "85 35 7D CB 12 F2 06, 2023-10-15T11:22:33.444-00:00", + "86 35 7D CB 12 2E 22 1B, 2023-10-15T11:22:33.444555-00:00", + "87 35 7D CB 12 4A 86 FD 69, 2023-10-15T11:22:33.444555666-00:00", + "88 35 7D CB EA 01, 2023-10-15T11:22+01:15", + "89 35 7D CB EA 85, 2023-10-15T11:22:33+01:15", + "8A 35 7D CB EA 85 BC 01, 2023-10-15T11:22:33.444+01:15", + "8B 35 7D CB EA 85 8B C8 06, 2023-10-15T11:22:33.444555+01:15", + "8C 35 7D CB EA 85 92 61 7F 1A, 2023-10-15T11:22:33.444555666+01:15", + ) + fun `generator can read short timestamp references`(inputBytesString: String, expectedTimestampString: String) { + val bytes = inputBytesString.hexStringToByteArray() + val generator = ByteArrayBytecodeGenerator11(bytes, 0) + val opcode = bytes[0].toInt().and(0xFF) + val expectedTimestamp = Timestamp.valueOf(expectedTimestampString) + val readTimestamp = generator.readShortTimestampReference(1, opcode) + assertEquals(expectedTimestamp, readTimestamp) + } + + @ParameterizedTest + @ValueSource( + strings = [ + "Hello world", + "\n\nhello\n\n", + "Love it! \uD83D\uDE0D❤\uFE0F\uD83D\uDC95\uD83D\uDE3B\uD83D\uDC96", + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`~!@#\$%^&*()-_=+[{]}\\|;:'\",<.>/?", + // A line of the Odyssey, CC BY-SA 3.0 US, from https://www.perseus.tufts.edu/hopper/text?doc=Perseus:text:1999.01.0135:book=1:card=1 + "τῶν ἁμόθεν γε, θεά, θύγατερ Διός, εἰπὲ καὶ ἡμῖν.", + "", + "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u000a\u000b\u000c\u000d\u000e\u000f\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u007f", + " \tleading and trailing whitespace\u000c\r\n" + ] + ) + fun `generator can read string references`(expectedString: String) { + val utf8Buffer = StandardCharsets.UTF_8.encode(expectedString) + val utf8Bytes = ByteArray(utf8Buffer.remaining()) + utf8Buffer.get(utf8Bytes) + val flexUIntBytes = generateFlexUIntBytes(utf8Bytes.size) + val bytes = byteArrayOf(0xF8.toByte(), *flexUIntBytes, *utf8Bytes) + + val generator = ByteArrayBytecodeGenerator11(bytes, 0) + // Size of input minus the opcode and FlexUInt length prefix + val position = flexUIntBytes.size + 1 + val readString = generator.readTextReference(position, utf8Bytes.size) + assertEquals(expectedString, readString) + } + + @ParameterizedTest + @ValueSource( + strings = [ + "00 00 00 00 00 00 00 00 00 00", + "FF FF FF FF FF FF FF FF FF FF", + "00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F 80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F 90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 DA DB DC DD DE DF E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF", + "A5", + "" + ] + ) + fun `generator can read lob references`(expectedLobBytes: String) { + val lobBytes = expectedLobBytes.hexStringToByteArray() + val flexUIntBytes = generateFlexUIntBytes(lobBytes.size) + val bytes = byteArrayOf(0xFE.toByte(), *flexUIntBytes, *lobBytes) + + val generator = ByteArrayBytecodeGenerator11(bytes, 0) + val position = flexUIntBytes.size + 1 + val readLob = generator.readBytesReference(position, lobBytes.size).newByteArray() + assertArrayEquals(lobBytes, readLob) + } + + /** + * Helper function for generating FlexUInt bytes from an unsigned integer. Useful for test + * cases that programmatically generate length-prefixed payloads. + */ + private fun generateFlexUIntBytes(value: Int): ByteArray { + val asLong = value.toLong() + val length = PrimitiveEncoder.flexUIntLength(asLong) + val bytes = ByteArray(length) + PrimitiveEncoder.writeFlexIntOrUIntInto(bytes, 0, asLong, length) + return bytes + } +} diff --git a/src/test/java/com/amazon/ion/bytecode/bin11/bytearray/PrimitiveDecoderTest.kt b/src/test/java/com/amazon/ion/bytecode/bin11/bytearray/PrimitiveDecoderTest.kt index 13ae8c482..274b8f95f 100644 --- a/src/test/java/com/amazon/ion/bytecode/bin11/bytearray/PrimitiveDecoderTest.kt +++ b/src/test/java/com/amazon/ion/bytecode/bin11/bytearray/PrimitiveDecoderTest.kt @@ -22,6 +22,7 @@ import com.amazon.ion.bytecode.bin11.bytearray.PrimitiveDecoder.readFixedInt16 import com.amazon.ion.bytecode.bin11.bytearray.PrimitiveDecoder.readFixedInt24AsInt import com.amazon.ion.bytecode.bin11.bytearray.PrimitiveDecoder.readFixedInt32 import com.amazon.ion.bytecode.bin11.bytearray.PrimitiveDecoder.readFixedInt8AsShort +import com.amazon.ion.bytecode.bin11.bytearray.PrimitiveDecoder.readFixedIntAsBigInteger import com.amazon.ion.bytecode.bin11.bytearray.PrimitiveDecoder.readFixedIntAsLong import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.TestInstance @@ -160,6 +161,20 @@ class PrimitiveDecoderTest { assertEquals(expected, actual) } + @ParameterizedTest + @MethodSource(FIXED_INT_8_CASES, FIXED_INT_16_CASES, FIXED_INT_24_CASES, FIXED_INT_32_CASES, FIXED_INT_64_CASES) + @CsvSource( + " 9223372036854775808, 00000000 00000000 00000000 00000000 00000000 00000000 00000000 10000000 00000000", + "-9223372036854775809, 11111111 11111111 11111111 11111111 11111111 11111111 11111111 01111111 11111111", + " 1, 00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000", + " -1, 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111", + ) + fun testReadFixedIntAsBigInteger(expectedValue: BigInteger, input: String) { + val data = input.binaryStringToByteArray() + val value = readFixedIntAsBigInteger(data, 0, data.size) + assertEquals(expectedValue, value) + } + @ParameterizedTest @MethodSource(FIXED_UINT_16_CASES) fun testReadFixedUInt16(expected: Int, bits: String) { diff --git a/src/test/java/com/amazon/ion/bytecode/bin11/bytearray/ShortTimestampOpcodeHandlerTest.kt b/src/test/java/com/amazon/ion/bytecode/bin11/bytearray/ShortTimestampOpcodeHandlerTest.kt index b7baa5220..a55c34d53 100644 --- a/src/test/java/com/amazon/ion/bytecode/bin11/bytearray/ShortTimestampOpcodeHandlerTest.kt +++ b/src/test/java/com/amazon/ion/bytecode/bin11/bytearray/ShortTimestampOpcodeHandlerTest.kt @@ -18,24 +18,23 @@ class ShortTimestampOpcodeHandlerTest { @ParameterizedTest @CsvSource( - "80 35, 0, 2", // 2023T - "81 35 05, 1, 3", // 2023-10T - "82 35 7D, 2, 3", // 2023-10-15T - "83 35 7D CB 0A, 3, 5", // 2023-10-15T11:22Z - "84 35 7D CB 1A 02, 4, 6", // 2023-10-15T11:22:33Z - "84 35 7D CB 12 02, 4, 6", // 2023-10-15T11:22:33-00:00 - "85 35 7D CB 12 F2 06, 5, 7", // 2023-10-15T11:22:33.444-00:00 - "86 35 7D CB 12 2E 22 1B, 6, 8", // 2023-10-15T11:22:33.444555-00:00 - "87 35 7D CB 12 4A 86 FD 69, 7, 9", // 2023-10-15T11:22:33.444555666-00:00 - "88 35 7D CB EA 01, 8, 6", // 2023-10-15T11:22+01:15 - "89 35 7D CB EA 85, 9, 6", // 2023-10-15T11:22:33+01:15 - "8A 35 7D CB EA 85 BC 01, 10, 8", // 2023-10-15T11:22:33.444+01:15 - "8B 35 7D CB EA 85 8B C8 06, 11, 9", // 2023-10-15T11:22:33.444555+01:15 - "8C 35 7D CB EA 85 92 61 7F 1A, 12, 10", // 2023-10-15T11:22:33.444555666+01:15 + "80 35, 2", // 2023T + "81 35 05, 3", // 2023-10T + "82 35 7D, 3", // 2023-10-15T + "83 35 7D CB 0A, 5", // 2023-10-15T11:22Z + "84 35 7D CB 1A 02, 6", // 2023-10-15T11:22:33Z + "84 35 7D CB 12 02, 6", // 2023-10-15T11:22:33-00:00 + "85 35 7D CB 12 F2 06, 7", // 2023-10-15T11:22:33.444-00:00 + "86 35 7D CB 12 2E 22 1B, 8", // 2023-10-15T11:22:33.444555-00:00 + "87 35 7D CB 12 4A 86 FD 69, 9", // 2023-10-15T11:22:33.444555666-00:00 + "88 35 7D CB EA 01, 6", // 2023-10-15T11:22+01:15 + "89 35 7D CB EA 85, 6", // 2023-10-15T11:22:33+01:15 + "8A 35 7D CB EA 85 BC 01, 8", // 2023-10-15T11:22:33.444+01:15 + "8B 35 7D CB EA 85 8B C8 06, 9", // 2023-10-15T11:22:33.444555+01:15 + "8C 35 7D CB EA 85 92 61 7F 1A, 10", // 2023-10-15T11:22:33.444555666+01:15 ) fun `short timestamp opcode handler emits correct bytecode`( inputString: String, - expectedPrecisionAndOffsetMode: Int, expectedEndPosition: Int ) { val inputByteArray = inputString.hexStringToByteArray() @@ -56,7 +55,7 @@ class ShortTimestampOpcodeHandlerTest { val expectedPayloadStartPosition = 1 val expectedBytecode = intArrayOf( - Instructions.I_SHORT_TIMESTAMP_REF.packInstructionData(expectedPrecisionAndOffsetMode), + Instructions.I_SHORT_TIMESTAMP_REF.packInstructionData(inputByteArray[0].unsignedToInt()), expectedPayloadStartPosition ) assertEqualBytecode(expectedBytecode, buffer.toArray()) diff --git a/src/test/java/com/amazon/ion/bytecode/bin11/bytearray/TimestampDecoderTest.kt b/src/test/java/com/amazon/ion/bytecode/bin11/bytearray/TimestampDecoderTest.kt new file mode 100644 index 000000000..45d74be6d --- /dev/null +++ b/src/test/java/com/amazon/ion/bytecode/bin11/bytearray/TimestampDecoderTest.kt @@ -0,0 +1,133 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode.bin11.bytearray + +import com.amazon.ion.TextToBinaryUtils.hexStringToByteArray +import com.amazon.ion.Timestamp +import com.amazon.ion.bytecode.util.unsignedToInt +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + +class TimestampDecoderTest { + + @ParameterizedTest + @CsvSource( + // UTC offset + "83 35 7D 01 08, 2023-10-15T01:00Z", + "83 35 7D 61 0F, 2023-10-15T01:59Z", + "83 35 7D CB 0A, 2023-10-15T11:22Z", + "83 35 7D 17 08, 2023-10-15T23:00Z", + "83 35 7D 77 0F, 2023-10-15T23:59Z", + "84 35 7D CB 0A 00, 2023-10-15T11:22:00Z", + "84 35 7D CB 1A 02, 2023-10-15T11:22:33Z", + "84 35 7D CB BA 03, 2023-10-15T11:22:59Z", + "85 35 7D CB 1A 02 00, 2023-10-15T11:22:33.000Z", + "85 35 7D CB 1A F2 06, 2023-10-15T11:22:33.444Z", + "85 35 7D CB 1A 9E 0F, 2023-10-15T11:22:33.999Z", + "86 35 7D CB 1A 02 00 00, 2023-10-15T11:22:33.000000Z", + "86 35 7D CB 1A 2E 22 1B, 2023-10-15T11:22:33.444555Z", + "86 35 7D CB 1A FE 08 3D, 2023-10-15T11:22:33.999999Z", + "87 35 7D CB 1A 02 00 00 00, 2023-10-15T11:22:33.000000000Z", + "87 35 7D CB 1A 4A 86 FD 69, 2023-10-15T11:22:33.444555666Z", + "87 35 7D CB 1A FE 27 6B EE, 2023-10-15T11:22:33.999999999Z", + + // Unknown offset + "80 35, 2023T", + "81 B5 00, 2023-01T", + "81 35 05, 2023-10T", + "81 35 06, 2023-12T", + "82 35 0D, 2023-10-01T", + "82 35 7D, 2023-10-15T", + "82 35 FD, 2023-10-31T", + "83 35 7D 01 00, 2023-10-15T01:00-00:00", + "83 35 7D 61 07, 2023-10-15T01:59-00:00", + "83 35 7D CB 02, 2023-10-15T11:22-00:00", + "83 35 7D 17 00, 2023-10-15T23:00-00:00", + "83 35 7D 77 07, 2023-10-15T23:59-00:00", + "84 35 7D CB 02 00, 2023-10-15T11:22:00-00:00", + "84 35 7D CB 12 02, 2023-10-15T11:22:33-00:00", + "84 35 7D CB B2 03, 2023-10-15T11:22:59-00:00", + "85 35 7D CB 12 02 00, 2023-10-15T11:22:33.000-00:00", + "85 35 7D CB 12 F2 06, 2023-10-15T11:22:33.444-00:00", + "85 35 7D CB 12 9E 0F, 2023-10-15T11:22:33.999-00:00", + "86 35 7D CB 12 02 00 00, 2023-10-15T11:22:33.000000-00:00", + "86 35 7D CB 12 2E 22 1B, 2023-10-15T11:22:33.444555-00:00", + "86 35 7D CB 12 FE 08 3D, 2023-10-15T11:22:33.999999-00:00", + "87 35 7D CB 12 02 00 00 00, 2023-10-15T11:22:33.000000000-00:00", + "87 35 7D CB 12 4A 86 FD 69, 2023-10-15T11:22:33.444555666-00:00", + "87 35 7D CB 12 FE 27 6B EE, 2023-10-15T11:22:33.999999999-00:00", + + // Known offset + "88 35 7D 01 00 00, 2023-10-15T01:00-14:00", // min offset + "88 35 7D 01 80 03, 2023-10-15T01:00+14:00", // max offset + "88 35 7D 01 98 01, 2023-10-15T01:00-01:15", + "88 35 7D 01 E8 01, 2023-10-15T01:00+01:15", + "88 35 7D 61 EF 01, 2023-10-15T01:59+01:15", + "88 35 7D CB EA 01, 2023-10-15T11:22+01:15", + "88 35 7D 17 E8 01, 2023-10-15T23:00+01:15", + "88 35 7D 77 EF 01, 2023-10-15T23:59+01:15", + "89 35 7D CB EA 01, 2023-10-15T11:22:00+01:15", + "89 35 7D CB EA 85, 2023-10-15T11:22:33+01:15", + "89 35 7D CB EA ED, 2023-10-15T11:22:59+01:15", + "8A 35 7D CB EA 85 00 00, 2023-10-15T11:22:33.000+01:15", + "8A 35 7D CB EA 85 BC 01, 2023-10-15T11:22:33.444+01:15", + "8A 35 7D CB EA 85 E7 03, 2023-10-15T11:22:33.999+01:15", + "8B 35 7D CB EA 85 00 00 00, 2023-10-15T11:22:33.000000+01:15", + "8B 35 7D CB EA 85 8B C8 06, 2023-10-15T11:22:33.444555+01:15", + "8B 35 7D CB EA 85 3F 42 0F, 2023-10-15T11:22:33.999999+01:15", + "8C 35 7D CB EA 85 00 00 00 00, 2023-10-15T11:22:33.000000000+01:15", + "8C 35 7D CB EA 85 92 61 7F 1A, 2023-10-15T11:22:33.444555666+01:15", + "8C 35 7D CB EA 85 FF C9 9A 3B, 2023-10-15T11:22:33.999999999+01:15", + + // Earliest possible moments in time + "80 00, 1970T", + "81 80 00, 1970-01T", + "82 80 08, 1970-01-01T", + "83 80 08 00 00, 1970-01-01T00:00-00:00", + "84 80 08 00 00 00, 1970-01-01T00:00:00-00:00", + "85 80 08 00 00 00 00, 1970-01-01T00:00:00.000-00:00", + "86 80 08 00 00 00 00 00, 1970-01-01T00:00:00.000000-00:00", + "87 80 08 00 00 00 00 00 00, 1970-01-01T00:00:00.000000000-00:00", + "88 80 08 00 80 03, 1970-01-01T00:00+14:00", + "89 80 08 00 80 03, 1970-01-01T00:00:00+14:00", + "8A 80 08 00 80 03 00 00, 1970-01-01T00:00:00.000+14:00", + "8B 80 08 00 80 03 00 00 00, 1970-01-01T00:00:00.000000+14:00", + "8C 80 08 00 80 03 00 00 00 00, 1970-01-01T00:00:00.000000000+14:00", + + // Latest possible moments in time + "80 7F, 2097T", + "81 7F 06, 2097-12T", + "82 7F FE, 2097-12-31T", + "83 7F FE 77 07, 2097-12-31T23:59-00:00", + "84 7F FE 77 B7 03, 2097-12-31T23:59:59-00:00", + "85 7F FE 77 B7 9F 0F, 2097-12-31T23:59:59.999-00:00", + "86 7F FE 77 B7 FF 08 3D, 2097-12-31T23:59:59.999999-00:00", + "87 7F FE 77 B7 FF 27 6B EE, 2097-12-31T23:59:59.999999999-00:00", + "88 7F FE 77 07 00, 2097-12-31T23:59-14:00", + "89 7F FE 77 07 EC, 2097-12-31T23:59:59-14:00", + "8A 7F FE 77 07 EC E7 03, 2097-12-31T23:59:59.999-14:00", + "8B 7F FE 77 07 EC 3F 42 0F, 2097-12-31T23:59:59.999999-14:00", + "8C 7F FE 77 07 EC FF C9 9A 3B, 2097-12-31T23:59:59.999999999-14:00", + + // Leap days + "82 3A E9, 2028-02-29T", + "83 3A E9 CA 0B, 2028-02-29T10:30Z", + "84 3A E9 CA DB 02, 2028-02-29T10:30:45Z", + "85 3A E9 CA DB EE 01, 2028-02-29T10:30:45.123Z", + "86 3A E9 CA DB 02 89 07, 2028-02-29T10:30:45.123456Z", + "87 3A E9 CA DB 56 34 6F 1D, 2028-02-29T10:30:45.123456789Z", + "88 3A E9 CA 9B 01, 2028-02-29T10:30-01:15", + "89 3A E9 CA 9B B5, 2028-02-29T10:30:45-01:15", + "8A 3A E9 CA 9B B5 7B 00, 2028-02-29T10:30:45.123-01:15", + "8B 3A E9 CA 9B B5 40 E2 01, 2028-02-29T10:30:45.123456-01:15", + "8C 3A E9 CA 9B B5 15 CD 5B 07, 2028-02-29T10:30:45.123456789-01:15", + ) + fun `short timestamps are decoded correctly`(input: String, expectedValue: String) { + val data = input.hexStringToByteArray() + val opcode = data[0].unsignedToInt() + val timestamp = TimestampDecoder.readShortTimestamp(data, 1, opcode) + val expectedTimestamp = Timestamp.valueOf(expectedValue.trim()) + assertEquals(expectedTimestamp, timestamp) + } +}