From 015c6a0f53987179057921299782d14937b96358 Mon Sep 17 00:00:00 2001 From: Matthew Pope <81593196+popematt@users.noreply.github.com> Date: Thu, 7 Dec 2023 14:18:23 -0800 Subject: [PATCH] Adds annotations to IonRawBinaryWriter_1_1 (#661) --- .../java/com/amazon/ion/impl/IonWriter_1_1.kt | 8 +- .../ion/impl/bin/IonRawBinaryWriter_1_1.kt | 214 +++++++++++--- .../com/amazon/ion/impl/bin/WriteBuffer.java | 30 ++ .../impl/bin/IonRawBinaryWriterTest_1_1.kt | 271 +++++++++++++++--- .../amazon/ion/impl/bin/WriteBufferTest.java | 39 +++ 5 files changed, 489 insertions(+), 73 deletions(-) diff --git a/src/main/java/com/amazon/ion/impl/IonWriter_1_1.kt b/src/main/java/com/amazon/ion/impl/IonWriter_1_1.kt index f85d526c7f..b83856a42e 100644 --- a/src/main/java/com/amazon/ion/impl/IonWriter_1_1.kt +++ b/src/main/java/com/amazon/ion/impl/IonWriter_1_1.kt @@ -60,10 +60,10 @@ interface IonWriter_1_1 { fun writeAnnotations(annotation0: Int, annotation1: Int) /** - * Writes three or more annotations for the next value. + * Writes any number of annotations for the next value. * [writeAnnotations] may be called more than once to build up a list of annotations. */ - fun writeAnnotations(annotation0: Int, annotation1: Int, vararg annotations: Int) + fun writeAnnotations(annotations: IntArray) /** * Writes one annotation for the next value. @@ -78,10 +78,10 @@ interface IonWriter_1_1 { fun writeAnnotations(annotation0: CharSequence, annotation1: CharSequence) /** - * Writes three or more annotations for the next value. + * Writes any number of annotations for the next value. * [writeAnnotations] may be called more than once to build up a list of annotations. */ - fun writeAnnotations(annotation0: CharSequence, annotation1: CharSequence, vararg annotations: CharSequence) + fun writeAnnotations(annotations: Array) /** * Writes the field name for the next value. Must be called while in a struct and must be called before [writeAnnotations]. diff --git a/src/main/java/com/amazon/ion/impl/bin/IonRawBinaryWriter_1_1.kt b/src/main/java/com/amazon/ion/impl/bin/IonRawBinaryWriter_1_1.kt index af082ee156..005b1eacfc 100644 --- a/src/main/java/com/amazon/ion/impl/bin/IonRawBinaryWriter_1_1.kt +++ b/src/main/java/com/amazon/ion/impl/bin/IonRawBinaryWriter_1_1.kt @@ -5,6 +5,7 @@ package com.amazon.ion.impl.bin import com.amazon.ion.* import com.amazon.ion.impl.* import com.amazon.ion.impl.bin.IonRawBinaryWriter_1_1.ContainerType.* +import com.amazon.ion.impl.bin.utf8.* import com.amazon.ion.util.* import java.io.ByteArrayOutputStream import java.math.BigDecimal @@ -33,18 +34,53 @@ class IonRawBinaryWriter_1_1 internal constructor( * TODO: Test if performance is better if we just check currentContainer for nullness. */ Top, + /** + * Represents a group of annotations. May only contain FlexSyms or FlexUInt symbol IDs. + */ + Annotations, } private data class ContainerInfo( var type: ContainerType? = null, var isDelimited: Boolean = false, var position: Long = -1, - var length: Long = -1, + var length: Long = 0, // TODO: Test if performance is better with an Object Reference or an index into the PatchPoint queue. var patchPoint: PatchPoint? = null, - ) + ) { + fun clear() { + type = null + isDelimited = false + position = -1 + length = 0 + patchPoint = null + } + } + companion object { + /** Flag to indicate that annotations need to be written using FlexSyms */ + private const val FLEX_SYMS_REQUIRED = -1 + + /** + * Annotations container always requires at least one length prefix byte. In practice, it's almost certain to + * never require more than one byte for SID annotations. We assume that it will infrequently require more than + * one byte for FlexSym annotations. + */ + private const val ANNOTATIONS_LENGTH_PREFIX_ALLOCATION_SIZE = 1 + } + + private val utf8StringEncoder = Utf8StringEncoderPool.getInstance().getOrCreate() + + private var annotationsTextBuffer = arrayOfNulls(8) + private var annotationsIdBuffer = IntArray(8) private var numAnnotations = 0 + /** + * Flag indicating whether to use FlexSyms to write the annotations. When FlexSyms are required, the flag should be + * set to `-1` so that we can `xor` it with [numAnnotations] to get a distinct integer that represents the number + * and type of annotations required. + */ + private var annotationFlexSymFlag = 0 + private var hasFieldName = false private var closed = false @@ -57,6 +93,7 @@ class IonRawBinaryWriter_1_1 internal constructor( override fun finish() { if (closed) return confirm(depth() == 0) { "Cannot call finish() while in a container" } + confirm(numAnnotations == 0) { "Cannot call finish with dangling annotations" } if (patchPoints.isEmpty) { // nothing to patch--write 'em out! @@ -114,50 +151,155 @@ class IonRawBinaryWriter_1_1 internal constructor( buffer.writeBytes(_Private_IonConstants.BINARY_VERSION_MARKER_1_1) } + /** + * Ensures that there is enough space in the annotation buffers for [n] annotations. + * If more space is needed, it over-allocates by 8 to ensure that we're not continually allocating when annotations + * are being added one by one. + */ + private inline fun ensureAnnotationSpace(n: Int) { + if (annotationsIdBuffer.size < n || annotationsTextBuffer.size < n) { + val oldIds = annotationsIdBuffer + annotationsIdBuffer = IntArray(n + 8) + oldIds.copyInto(annotationsIdBuffer) + val oldText = annotationsTextBuffer + annotationsTextBuffer = arrayOfNulls(n + 8) + oldText.copyInto(annotationsTextBuffer) + } + } + override fun writeAnnotations(annotation0: Int) { - TODO("Not yet implemented") + ensureAnnotationSpace(numAnnotations + 1) + annotationsIdBuffer[numAnnotations++] = annotation0 } override fun writeAnnotations(annotation0: Int, annotation1: Int) { - TODO("Not yet implemented") + ensureAnnotationSpace(numAnnotations + 2) + annotationsIdBuffer[numAnnotations++] = annotation0 + annotationsIdBuffer[numAnnotations++] = annotation1 } - override fun writeAnnotations(annotation0: Int, annotation1: Int, vararg annotations: Int) { - TODO("Not yet implemented") + override fun writeAnnotations(annotations: IntArray) { + ensureAnnotationSpace(numAnnotations + annotations.size) + annotations.copyInto(annotationsIdBuffer, numAnnotations) + numAnnotations += annotations.size } override fun writeAnnotations(annotation0: CharSequence) { - TODO("Not yet implemented") + ensureAnnotationSpace(numAnnotations + 1) + annotationsTextBuffer[numAnnotations++] = annotation0 + annotationFlexSymFlag = FLEX_SYMS_REQUIRED } override fun writeAnnotations(annotation0: CharSequence, annotation1: CharSequence) { - TODO("Not yet implemented") + ensureAnnotationSpace(numAnnotations + 2) + annotationsTextBuffer[numAnnotations++] = annotation0 + annotationsTextBuffer[numAnnotations++] = annotation1 + annotationFlexSymFlag = FLEX_SYMS_REQUIRED } - override fun writeAnnotations(annotation0: CharSequence, annotation1: CharSequence, vararg annotations: CharSequence) { - TODO("Not yet implemented") + override fun writeAnnotations(annotations: Array) { + if (annotations.isEmpty()) return + ensureAnnotationSpace(numAnnotations + annotations.size) + annotations.copyInto(annotationsTextBuffer, numAnnotations) + numAnnotations += annotations.size + annotationFlexSymFlag = FLEX_SYMS_REQUIRED } /** * Helper function for handling annotations and field names when starting a value. */ private inline fun openValue(valueWriterExpression: () -> Unit) { - if (numAnnotations > 0) { - TODO("Actually write out the annotations.") + + // Start at 1, assuming there's an annotations OpCode byte. + // We'll clear this if there are no annotations. + var annotationsTotalLength = 1L + + // Effect of the xor: if annotationsFlexSymFlag is -1, then we're matching `-1 * numAnnotations - 1` + when (numAnnotations xor annotationFlexSymFlag) { + 0, -1 -> annotationsTotalLength = 0 + 1 -> { + buffer.writeByte(OpCodes.ANNOTATIONS_1_SYMBOL_ADDRESS) + annotationsTotalLength += buffer.writeFlexUInt(annotationsIdBuffer[0]) + } + 2 -> { + buffer.writeByte(OpCodes.ANNOTATIONS_2_SYMBOL_ADDRESS) + annotationsTotalLength += buffer.writeFlexUInt(annotationsIdBuffer[0]) + annotationsTotalLength += buffer.writeFlexUInt(annotationsIdBuffer[1]) + } + -2 -> { + // If there's only one annotation, and we know that at least one has text, we don't need to check + // whether this is SID. + buffer.writeByte(OpCodes.ANNOTATIONS_1_FLEX_SYM) + annotationsTotalLength += buffer.writeFlexSym(utf8StringEncoder.encode(annotationsTextBuffer[0].toString())) + annotationsTextBuffer[0] = null + } + -3 -> { + buffer.writeByte(OpCodes.ANNOTATIONS_2_FLEX_SYM) + annotationsTotalLength += writeFlexSymFromAnnotationsBuffer(0) + annotationsTotalLength += writeFlexSymFromAnnotationsBuffer(1) + } + else -> annotationsTotalLength += writeManyAnnotations() } + currentContainer.length += annotationsTotalLength + numAnnotations = 0 + annotationFlexSymFlag = 0 hasFieldName = false valueWriterExpression() } + /** + * Writes a FlexSym annotation for the specified position in the annotations buffers. + */ + private fun writeFlexSymFromAnnotationsBuffer(i: Int): Int { + val annotationText = annotationsTextBuffer[i] + return if (annotationText != null) { + annotationsTextBuffer[i] = null + buffer.writeFlexSym(utf8StringEncoder.encode(annotationText.toString())) + } else { + buffer.writeFlexSym(annotationsIdBuffer[i]) + } + } + + /** + * Writes 3 or more annotations for SIDs or FlexSyms + */ + private fun writeManyAnnotations(): Long { + currentContainer = containerStack.push { + it.clear() + it.type = Annotations + it.position = buffer.position() + } + if (annotationFlexSymFlag == FLEX_SYMS_REQUIRED) { + buffer.writeByte(OpCodes.ANNOTATIONS_MANY_FLEX_SYM) + buffer.reserve(ANNOTATIONS_LENGTH_PREFIX_ALLOCATION_SIZE) + for (i in 0 until numAnnotations) { + currentContainer.length += writeFlexSymFromAnnotationsBuffer(i) + } + } else { + buffer.writeByte(OpCodes.ANNOTATIONS_MANY_SYMBOL_ADDRESS) + buffer.reserve(ANNOTATIONS_LENGTH_PREFIX_ALLOCATION_SIZE) + for (i in 0 until numAnnotations) { + currentContainer.length += buffer.writeFlexUInt(annotationsIdBuffer[i]) + } + } + + val numAnnotationsBytes = currentContainer.length + val numLengthPrefixBytes = writeCurrentContainerLength(ANNOTATIONS_LENGTH_PREFIX_ALLOCATION_SIZE) + + // Set the new current container + containerStack.pop() + currentContainer = containerStack.peek() + + return numLengthPrefixBytes + numAnnotationsBytes + } + /** * Helper function for finishing scalar values. Similar concerns for containers are handled in [stepOut]. */ private inline fun closeScalar(valueWriterExpression: () -> Int) { val numBytesWritten = valueWriterExpression() - - // Update the container length (unless it's Top) - if (currentContainer.type != Top) currentContainer.length += numBytesWritten + currentContainer.length += numBytesWritten } /** @@ -244,10 +386,10 @@ class IonRawBinaryWriter_1_1 internal constructor( override fun stepInList(delimited: Boolean) { openValue { currentContainer = containerStack.push { + it.clear() it.type = List it.position = buffer.position() it.isDelimited = delimited - it.length = 0 } if (delimited) { buffer.writeByte(OpCodes.DELIMITED_LIST) @@ -301,27 +443,14 @@ class IonRawBinaryWriter_1_1 internal constructor( buffer.shiftBytesLeft(currentContainer.length.toInt(), lengthPrefixPreallocation) buffer.writeUInt8At(currentContainer.position, OpCodes.LIST_ZERO_LENGTH + contentLength) } else { - val lengthPrefixBytesRequired = FlexInt.flexUIntLength(contentLength) - thisContainerTotalLength += lengthPrefixBytesRequired - - if (lengthPrefixBytesRequired == lengthPrefixPreallocation) { - // We have enough space, so write in the correct length. - buffer.writeFlexIntOrUIntAt(currentContainer.position + 1, currentContainer.length, lengthPrefixBytesRequired) - } else { - addPatchPointsToStack() - // currentContainer is in containerStack, so we know that its patchPoint is non-null. - currentContainer.patchPoint.assumeNotNull().apply { - oldPosition = currentContainer.position + 1 - oldLength = lengthPrefixPreallocation - length = currentContainer.length - } - } + thisContainerTotalLength += writeCurrentContainerLength(lengthPrefixPreallocation) } } SExp -> TODO() Struct -> TODO() Macro -> TODO() Stream -> TODO() + Annotations -> TODO("Unreachable.") Top -> throw IonException("Nothing to step out of.") } } @@ -330,7 +459,28 @@ class IonRawBinaryWriter_1_1 internal constructor( containerStack.pop() currentContainer = containerStack.peek() // Update the length of the new current container to include the length of the container that we just stepped out of. - if (currentContainer.type != Top) currentContainer.length += thisContainerTotalLength + currentContainer.length += thisContainerTotalLength + } + + /** + * Writes the length of the current container and returns the number of bytes needed to do so. + * Transparently handles PatchPoints as necessary. + */ + private fun writeCurrentContainerLength(numPreAllocatedLengthPrefixBytes: Int): Int { + val lengthPrefixBytesRequired = FlexInt.flexUIntLength(currentContainer.length) + if (lengthPrefixBytesRequired == numPreAllocatedLengthPrefixBytes) { + // We have enough space, so write in the correct length. + buffer.writeFlexIntOrUIntAt(currentContainer.position + 1, currentContainer.length, lengthPrefixBytesRequired) + } else { + addPatchPointsToStack() + // All ContainerInfos are in the stack, so we know that its patchPoint is non-null. + currentContainer.patchPoint.assumeNotNull().apply { + oldPosition = currentContainer.position + 1 + oldLength = numPreAllocatedLengthPrefixBytes + length = currentContainer.length + } + } + return lengthPrefixBytesRequired } private fun addPatchPointsToStack() { diff --git a/src/main/java/com/amazon/ion/impl/bin/WriteBuffer.java b/src/main/java/com/amazon/ion/impl/bin/WriteBuffer.java index 1a03ba118d..b4cb3c2aed 100644 --- a/src/main/java/com/amazon/ion/impl/bin/WriteBuffer.java +++ b/src/main/java/com/amazon/ion/impl/bin/WriteBuffer.java @@ -15,6 +15,8 @@ package com.amazon.ion.impl.bin; +import com.amazon.ion.impl.bin.utf8.Utf8StringEncoder; + import java.io.Closeable; import java.io.IOException; import java.io.OutputStream; @@ -1519,6 +1521,34 @@ public int writeFixedIntOrUInt(final byte[] value) { return value.length; } + /** + * Writes a FlexSym with a symbol id. + */ + public int writeFlexSym(int sid) { + if (sid != 0) { + return writeFlexInt(sid); + } else { + writeByte((byte) 0x01); + writeByte((byte) 0x90); + return 2; + } + } + + /** + * Writes a FlexSym with inline text. + */ + public int writeFlexSym(Utf8StringEncoder.Result text) { + if (text.getEncodedLength() == 0) { + writeByte((byte) 0x01); + writeByte((byte) 0x80); + return 2; + } else { + int numLengthBytes = writeFlexInt(-text.getEncodedLength()); + writeBytes(text.getBuffer(), 0, text.getEncodedLength()); + return numLengthBytes + text.getEncodedLength(); + } +} + /** Write the entire buffer to output stream. */ public void writeTo(final OutputStream out) throws IOException { diff --git a/src/test/java/com/amazon/ion/impl/bin/IonRawBinaryWriterTest_1_1.kt b/src/test/java/com/amazon/ion/impl/bin/IonRawBinaryWriterTest_1_1.kt index f8f2a687f0..0862c69d45 100644 --- a/src/test/java/com/amazon/ion/impl/bin/IonRawBinaryWriterTest_1_1.kt +++ b/src/test/java/com/amazon/ion/impl/bin/IonRawBinaryWriterTest_1_1.kt @@ -14,7 +14,7 @@ import org.junit.jupiter.params.provider.CsvSource class IonRawBinaryWriterTest_1_1 { - private inline fun writeAsHexString(block: IonRawBinaryWriter_1_1.() -> Unit): String { + private inline fun writeAsHexString(autoClose: Boolean = true, block: IonRawBinaryWriter_1_1.() -> Unit): String { val baos = ByteArrayOutputStream() val rawWriter = IonRawBinaryWriter_1_1( out = baos, @@ -22,56 +22,86 @@ class IonRawBinaryWriterTest_1_1 { lengthPrefixPreallocation = 1, ) block.invoke(rawWriter) + if (autoClose) rawWriter.close() @OptIn(ExperimentalStdlibApi::class) return baos.toByteArray().joinToString(" ") { it.toHexString(HexFormat.UpperCase) } } - private inline fun assertWriterOutputEquals(hexBytes: String, block: IonRawBinaryWriter_1_1.() -> Unit) { - assertEquals(hexBytes, writeAsHexString(block)) + private inline fun assertWriterOutputEquals(hexBytes: String, autoClose: Boolean = true, block: IonRawBinaryWriter_1_1.() -> Unit) { + assertEquals(hexBytes, writeAsHexString(autoClose, block)) + } + + private inline fun assertWriterThrows(block: IonRawBinaryWriter_1_1.() -> Unit) { + val baos = ByteArrayOutputStream() + val rawWriter = IonRawBinaryWriter_1_1( + out = baos, + buffer = WriteBuffer(BlockAllocatorProviders.basicProvider().vendAllocator(32)), + lengthPrefixPreallocation = 1, + ) + assertThrows { + block.invoke(rawWriter) + } } @Test fun `calling close while in a container should throw IonException`() { - assertThrows { - writeAsHexString { - stepInList(false) - close() - } + assertWriterThrows { + stepInList(false) + close() } } @Test fun `calling finish while in a container should throw IonException`() { - assertThrows { - writeAsHexString { - stepInList(true) - finish() - } + assertWriterThrows { + stepInList(true) + finish() + } + } + + @Test + fun `calling finish with a dangling annotation should throw IonException`() { + assertWriterThrows { + writeAnnotations(10) + finish() } } @Test fun `calling stepOut while not in a container should throw IonException`() { - assertThrows { - writeAsHexString { - stepOut() - } + assertWriterThrows { + stepOut() + } + } + + @Test + fun `calling stepOut with a dangling annotation should throw IonException`() { + assertWriterThrows { + stepInList(true) + writeAnnotations(10) + stepOut() } } @Test fun `calling writeIVM when in a container should throw IonException`() { - assertThrows { - writeAsHexString { - stepInList(false) - writeIVM() - } + assertWriterThrows { + stepInList(false) + writeIVM() + } + } + + @Test + fun `calling writeIVM with a dangling annotation should throw IonException`() { + assertWriterThrows { + writeAnnotations(10) + writeIVM() } } @Test fun `calling finish should cause the buffered data to be written to the output stream`() { - val actual = writeAsHexString { + val actual = writeAsHexString(autoClose = false) { writeIVM() finish() } @@ -84,7 +114,6 @@ class IonRawBinaryWriterTest_1_1 { val actual = writeAsHexString { finish() writeIVM() - close() } // Just checking that data is written, not asserting the content. assertTrue(actual.isNotBlank()) @@ -92,7 +121,7 @@ class IonRawBinaryWriterTest_1_1 { @Test fun `calling close should cause the buffered data to be written to the output stream`() { - val actual = writeAsHexString { + val actual = writeAsHexString(autoClose = false) { writeIVM() close() } @@ -118,14 +147,12 @@ class IonRawBinaryWriterTest_1_1 { fun `write the IVM`() { assertWriterOutputEquals("E0 01 01 EA") { writeIVM() - close() } } @Test fun `write nothing`() { assertWriterOutputEquals("") { - close() } } @@ -133,7 +160,6 @@ class IonRawBinaryWriterTest_1_1 { fun `write a null`() { assertWriterOutputEquals("EA") { writeNull() - close() } } @@ -142,7 +168,6 @@ class IonRawBinaryWriterTest_1_1 { // Just checking one type. The full range of types are checked in IonEncoder_1_1Test assertWriterOutputEquals("EB 00") { writeNull(IonType.BOOL) - close() } } @@ -151,7 +176,6 @@ class IonRawBinaryWriterTest_1_1 { fun `write a boolean`(value: Boolean, hexBytes: String) { assertWriterOutputEquals(hexBytes) { writeBool(value) - close() } } @@ -162,7 +186,6 @@ class IonRawBinaryWriterTest_1_1 { writeBool(true) writeBool(false) stepOut() - close() } } @@ -173,7 +196,6 @@ class IonRawBinaryWriterTest_1_1 { writeBool(true) writeBool(false) stepOut() - close() } } @@ -184,7 +206,6 @@ class IonRawBinaryWriterTest_1_1 { repeat(16) { writeBool(true) } stepOut() finish() - close() } } @@ -194,7 +215,6 @@ class IonRawBinaryWriterTest_1_1 { stepInList(false) repeat(128) { writeBool(true) } stepOut() - close() } } @@ -203,7 +223,6 @@ class IonRawBinaryWriterTest_1_1 { assertWriterOutputEquals("A4 A3 A2 A1 A0") { repeat(5) { stepInList(false) } repeat(5) { stepOut() } - close() } } @@ -212,7 +231,6 @@ class IonRawBinaryWriterTest_1_1 { assertWriterOutputEquals("F1 F1 F1 F1 F0 F0 F0 F0") { repeat(4) { stepInList(true) } repeat(4) { stepOut() } - close() } } @@ -224,7 +242,186 @@ class IonRawBinaryWriterTest_1_1 { stepInList(false) } repeat(8) { stepOut() } - close() + } + } + + @Test + fun `writeAnnotations with empty int array should write no annotations`() { + assertWriterOutputEquals("5E") { + writeAnnotations(intArrayOf()) + writeBool(true) + } + } + + @Test + fun `write one sid annotation`() { + val expectedBytes = "E4 07 5E" + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(3) + writeBool(true) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(3) + writeAnnotations(intArrayOf()) + writeAnnotations(arrayOf()) + writeBool(true) + } + } + + @Test + fun `write two sid annotations`() { + val expectedBytes = "E5 07 09 5E" + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(3) + writeAnnotations(4) + writeBool(true) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(3, 4) + writeBool(true) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(intArrayOf(3, 4)) + writeBool(true) + } + } + + @Test + fun `write three sid annotations`() { + val expectedBytes = "E6 09 07 09 02 04 5E" + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(3) + writeAnnotations(4) + writeAnnotations(256) + writeBool(true) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(3) + writeAnnotations(4, 256) + writeBool(true) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(intArrayOf(3, 4)) + writeAnnotations(256) + writeBool(true) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(intArrayOf(3, 4, 256)) + writeBool(true) + } + } + + @Test + fun `write sid 0 annotation`() { + assertWriterOutputEquals("E4 01 5E") { + writeAnnotations(0) + writeBool(true) + } + } + + @Test + fun `write one inline annotation`() { + val expectedBytes = "E7 FB 66 6F 6F 5F" + assertWriterOutputEquals(expectedBytes) { + writeAnnotations("foo") + writeBool(false) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations("foo") + writeAnnotations(intArrayOf()) + writeBool(false) + } + } + + @Test + fun `write two inline annotations`() { + val expectedBytes = "E8 FB 66 6F 6F FB 62 61 72 5F" + assertWriterOutputEquals(expectedBytes) { + writeAnnotations("foo") + writeAnnotations("bar") + writeBool(false) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(arrayOf("foo", "bar")) + writeBool(false) + } + } + + @Test + fun `write three inline annotations`() { + val expectedBytes = "E9 19 FB 66 6F 6F FB 62 61 72 FB 62 61 7A 5F" + assertWriterOutputEquals(expectedBytes) { + writeAnnotations("foo") + writeAnnotations("bar") + writeAnnotations("baz") + writeBool(false) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations("foo") + writeAnnotations("bar", "baz") + writeBool(false) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(arrayOf("foo", "bar")) + writeAnnotations("baz") + writeBool(false) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(arrayOf("foo", "bar", "baz")) + writeBool(false) + } + } + + @Test + fun `write empty text and sid 0 annotations`() { + assertWriterOutputEquals("E8 01 90 01 80 5E") { + writeAnnotations(0) + writeAnnotations("") + writeBool(true) + } + } + + @Test + fun `write two mixed sid and inline annotations`() { + val expectedBytes = "E8 15 FB 66 6F 6F 5F" + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(10) + writeAnnotations("foo") + writeBool(false) + } + } + + @Test + fun `write three mixed sid and inline annotations`() { + val expectedBytes = "E9 13 15 FB 66 6F 6F FB 62 61 72 5F" + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(10) + writeAnnotations("foo") + writeAnnotations("bar") + writeBool(false) + } + assertWriterOutputEquals(expectedBytes) { + writeAnnotations(10) + writeAnnotations(arrayOf("foo", "bar")) + writeBool(false) + } + } + + @Test + fun `write annotations that are long enough to need a patch point`() { + val opCode = "E7" + val length = "C6 FD" + val text = "41 6D 61 7A 6F 6E 20 49 6F 6E 20 69 73 20 61 20 72 69 63 68 6C 79 2D 74 79 70 65 64 2C 20 73 65 " + + "6C 66 2D 64 65 73 63 72 69 62 69 6E 67 2C 20 68 69 65 72 61 72 63 68 69 63 61 6C 20 64 61 74 61 20 " + + "73 65 72 69 61 6C 69 7A 61 74 69 6F 6E 20 66 6F 72 6D 61 74 20 6F 66 66 65 72 69 6E 67 20 69 6E 74 " + + "65 72 63 68 61 6E 67 65 61 62 6C 65 20 62 69 6E 61 72 79 20 61 6E 64 20 74 65 78 74 20 72 65 70 72 " + + "65 73 65 6E 74 61 74 69 6F 6E 73 2E 5F" + assertWriterOutputEquals("$opCode $length $text") { + writeAnnotations( + "Amazon Ion is a richly-typed, self-describing, hierarchical data serialization " + + "format offering interchangeable binary and text representations." + ) + writeBool(false) } } } diff --git a/src/test/java/com/amazon/ion/impl/bin/WriteBufferTest.java b/src/test/java/com/amazon/ion/impl/bin/WriteBufferTest.java index b57d6d56d7..20cb8c471d 100644 --- a/src/test/java/com/amazon/ion/impl/bin/WriteBufferTest.java +++ b/src/test/java/com/amazon/ion/impl/bin/WriteBufferTest.java @@ -23,7 +23,10 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.util.Arrays; + +import com.amazon.ion.impl.bin.utf8.Utf8StringEncoder; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -1755,6 +1758,42 @@ public void testWriteFixedIntOrUIntThrowsExceptionWhenNumBytesIsOutOfBounds() { Assertions.assertThrows(IllegalArgumentException.class, () -> buf.writeFixedIntOrUInt(0, 9)); } + @ParameterizedTest + @CsvSource({ + " 0, 00000001 10010000", + " 1, 00000011", + " 2, 00000101", + "63, 01111111", + "64, 00000010 00000001", + }) + public void testWriteSidFlexSym(int value, String expectedBits) { + int numBytes = buf.writeFlexSym(value); + String actualBits = byteArrayToBitString(bytes()); + Assertions.assertEquals(expectedBits, actualBits); + Assertions.assertEquals((expectedBits.length() + 1)/9, numBytes); + } + + @ParameterizedTest + @CsvSource({ + "'', 00000001 10000000", + "a, 11111111 01100001", + "abc, 11111011 01100001 01100010 01100011", + "this is a very very very very very long symbol, " + + "10100101 01110100 01101000 01101001 01110011 00100000 01101001 01110011 00100000 01100001 00100000 " + + "01110110 01100101 01110010 01111001 00100000 01110110 01100101 01110010 01111001 00100000 01110110 " + + "01100101 01110010 01111001 00100000 01110110 01100101 01110010 01111001 00100000 01110110 01100101 " + + "01110010 01111001 00100000 01101100 01101111 01101110 01100111 00100000 01110011 01111001 01101101 " + + "01100010 01101111 01101100", + }) + public void testWriteTextFlexSym(String value, String expectedBits) { + // This is a sloppy way to construct a Result, but it works for this test because we only have ascii characters. + Utf8StringEncoder.Result encoded = new Utf8StringEncoder.Result(value.length(), value.getBytes(StandardCharsets.US_ASCII)); + int numBytes = buf.writeFlexSym(encoded); + String actualBits = byteArrayToBitString(bytes()); + Assertions.assertEquals(expectedBits, actualBits); + Assertions.assertEquals((expectedBits.length() + 1)/9, numBytes); + } + /** * Converts a byte array to a string of bits, such as "00110110 10001001". * The purpose of this method is to make it easier to read and write test assertions.