From 81ef1daae4347ae3365bd68e2490570081d7db71 Mon Sep 17 00:00:00 2001 From: Tyler Gregg Date: Wed, 12 Jun 2024 13:20:44 -0700 Subject: [PATCH] Adds binary read support for macro invocation headers. --- .../com/amazon/ion/impl/IonCursorBinary.java | 177 +++++++++++++++++- .../java/com/amazon/ion/impl/IonTypeID.java | 29 +-- src/test/java/com/amazon/ion/TestUtils.java | 10 + .../amazon/ion/impl/IonCursorBinaryTest.java | 147 ++++++++++++++- ...onReaderContinuableTopLevelBinaryTest.java | 10 - 5 files changed, 342 insertions(+), 31 deletions(-) diff --git a/src/main/java/com/amazon/ion/impl/IonCursorBinary.java b/src/main/java/com/amazon/ion/impl/IonCursorBinary.java index be11ccee1..54a0bf2f9 100644 --- a/src/main/java/com/amazon/ion/impl/IonCursorBinary.java +++ b/src/main/java/com/amazon/ion/impl/IonCursorBinary.java @@ -22,6 +22,7 @@ import static com.amazon.ion.impl.IonTypeID.DELIMITED_END_ID; import static com.amazon.ion.impl.IonTypeID.ONE_ANNOTATION_FLEX_SYM_LOWER_NIBBLE_1_1; import static com.amazon.ion.impl.IonTypeID.ONE_ANNOTATION_SID_LOWER_NIBBLE_1_1; +import static com.amazon.ion.impl.IonTypeID.SYSTEM_SYMBOL_VALUE; import static com.amazon.ion.impl.IonTypeID.TWO_ANNOTATION_FLEX_SYMS_LOWER_NIBBLE_1_1; import static com.amazon.ion.impl.IonTypeID.TWO_ANNOTATION_SIDS_LOWER_NIBBLE_1_1; import static com.amazon.ion.util.IonStreamUtils.throwAsIonException; @@ -334,6 +335,19 @@ private static class RefillableState { */ private long lastReportedByteTotal = 0; + /** + * The ID of the current macro invocation. When `isSystemInvocation` is true, a positive value indicates a system + * macro address, while a negative value indicates a system symbol ID. When `isSystemInvocation` is false, a + * positive value indicates a user macro address, while a negative value indicates that the cursor's current token + * is not a macro invocation. + */ + private long macroInvocationId = -1; + + /** + * True if the given token represents a system invocation (either a system macro invocation or a system symbol + * value). When true, `macroInvocationId` is used to retrieve the ID of the system token. + */ + private boolean isSystemInvocation = false; /** * @return the given configuration's DataHandler, or null if that DataHandler is a no-op. @@ -956,6 +970,7 @@ private boolean slowReadAnnotationWrapperHeader_1_0(IonTypeID valueTid) { if (peekIndex >= valueMarker.endIndex) { throw new IonException("Annotation wrapper must wrap a value."); } + valueMarker.typeId = valueTid; return false; } @@ -1318,6 +1333,7 @@ private boolean slowReadAnnotationWrapperHeader_1_1(IonTypeID valueTid) { } } + valueMarker.typeId = valueTid; return false; } @@ -1931,6 +1947,8 @@ private void reset() { annotationSequenceMarker.typeId = null; annotationSequenceMarker.startIndex = -1; annotationSequenceMarker.endIndex = -1; + macroInvocationId = -1; + isSystemInvocation = false; } /** @@ -2018,6 +2036,91 @@ private void validateAnnotationWrapperEndIndex(long endIndex) { } } + /** + * Sets the given marker to represent the current system token (system macro invocation or system symbol value). + * Before calling this method, `macroInvocationId` must be set from the one-byte FixedInt that represents the ID; + * positive values indicate a macro address, while negative values indicate a system symbol ID. + * @param valueTid the type ID of the system token. + * @param markerToSet the marker to set. + */ + private void setSystemTokenMarker(IonTypeID valueTid, Marker markerToSet) { + isSystemInvocation = true; + markerToSet.startIndex = peekIndex; + if (macroInvocationId < 0) { + // This is a system symbol value. + event = Event.START_SCALAR; + markerToSet.typeId = SYSTEM_SYMBOL_VALUE; + markerToSet.endIndex = peekIndex; + } else { + event = Event.NEEDS_INSTRUCTION; + markerToSet.typeId = valueTid; + markerToSet.endIndex = -1; + } + } + + /** + * Sets the given marker to represent the current user macro invocation. + * @param valueTid the type ID of the macro invocation. + * @param markerToSet the Marker to set with information parsed from the macro invocation. After return, the + * marker's type ID will be set, startIndex will point to the first byte of the invocation's + * body, and endIndex will either be -1 (when not a system symbol or prefixed invocation), or + * will be set to the end of the invocation. + * @param length the declared length of the invocation. Ignored unless this is a length-prefixed invocation + * (denoted by `valueTid.variableLength == true`). + */ + private void setUserMacroInvocationMarker(IonTypeID valueTid, Marker markerToSet, long length) { + // It's not yet known whether the invocation represents a scalar or container, or even if it is complete. + // A higher-level reader must provide additional instructions to evaluate the invocation. + event = Event.NEEDS_INSTRUCTION; + markerToSet.typeId = valueTid; + markerToSet.startIndex = peekIndex; + // Unless this is a length-prefixed invocation, the end index of the macro invocation cannot be known until + // evaluation. + markerToSet.endIndex = valueTid.variableLength ? peekIndex + length : -1; + } + + /** + * Reads a macro invocation header, ensuring enough bytes are buffered. `peekIndex` must be positioned on the + * first byte that follows the opcode. After return, `peekIndex` will be positioned after any macro address + * byte(s), and `macroInvocationId` will be set to the address of the macro being invoked. + * @param valueTid the type ID of the macro invocation. + * @param markerToSet the Marker to set with information parsed from the macro invocation. After return, the + * marker's type ID will be set, startIndex will point to the first byte of the invocation's + * body, and endIndex will either be -1 (when not a system symbol or prefixed invocation), or + * will be set to the end of the invocation. + * @param length the declared length of the invocation. Ignored unless this is a length-prefixed invocation + * (denoted by `valueTid.variableLength == true`). + */ + private void uncheckedReadMacroInvocationHeader(IonTypeID valueTid, Marker markerToSet, long length) { + if (valueTid.macroId < 0) { + if (valueTid.lowerNibble == 0xE || valueTid.variableLength) { + // Opcode 0xEE or Opcode 0xF5 (when length > 0): Read the macro ID as a FlexUInt. + long idStart = peekIndex; + macroInvocationId = uncheckedReadFlexUInt_1_1(); + // The length included the macro ID. Subtract the length of the macro ID so that the end index can + // be set correctly. + length -= peekIndex - idStart; + } else { + // Opcode 0xEF: system macro invocation or system symbol value. + macroInvocationId = buffer[(int) peekIndex++]; + setSystemTokenMarker(valueTid, markerToSet); + return; + } + } else if (valueTid.length > 0) { + // Opcodes 0x4_: the rest of the macro ID follows in a 1-byte FixedUInt. + // Opcodes 0x5_: the rest of the macro ID follows in a 2-byte FixedUInt. + int remainingId = buffer[(int) peekIndex++] & SINGLE_BYTE_MASK; + if (valueTid.length > 1) { + remainingId |= ((buffer[(int) peekIndex++] & SINGLE_BYTE_MASK) << 8); + } + macroInvocationId = valueTid.macroId + remainingId; + } else { + // Opcodes 0x00 - 0x3F -- the opcode is the macro ID. + macroInvocationId = valueTid.macroId; + } + setUserMacroInvocationMarker(valueTid, markerToSet, length); + } + /** * Reads a value header, consuming the value's annotation wrapper header, if any. Upon invocation, * `peekIndex` must be positioned on the first byte that follows the given type ID byte. After return, `peekIndex` @@ -2041,7 +2144,10 @@ private boolean uncheckedReadHeader(final int typeIdByte, final boolean isAnnota return true; } hasAnnotations = true; - return uncheckedReadHeader(buffer[(int)(peekIndex++)] & SINGLE_BYTE_MASK, true, valueMarker); + return uncheckedReadHeader(buffer[(int) (peekIndex++)] & SINGLE_BYTE_MASK, true, valueMarker); + } else if (minorVersion == 1 && valueTid.isMacroInvocation) { + uncheckedReadMacroInvocationHeader(valueTid, markerToSet, valueTid.variableLength ? uncheckedReadFlexUInt_1_1() : -1); + return true; } else { long endIndex = minorVersion == 0 ? calculateEndIndex_1_0(valueTid, isAnnotated) @@ -2105,9 +2211,8 @@ private boolean slowReadHeader(final int typeIdByte, final boolean isAnnotated, if (nullTypeIndex < 0) { return true; } - valueTid = IonTypeID.NULL_TYPE_IDS_1_1[nullTypeIndex]; + markerToSet.typeId = IonTypeID.NULL_TYPE_IDS_1_1[nullTypeIndex]; } - markerToSet.typeId = valueTid; if (checkpointLocation == CheckpointLocation.AFTER_SCALAR_HEADER) { return true; } @@ -2120,6 +2225,61 @@ private boolean slowReadHeader(final int typeIdByte, final boolean isAnnotated, return false; } + /** + * Reads a macro invocation header, ensuring enough bytes are buffered. `peekIndex` must be positioned on the + * first byte that follows the opcode. After return, `peekIndex` will be positioned after any macro address + * byte(s), and `macroInvocationId` will be set to the address of the macro being invoked. + * @param valueTid the type ID of the macro invocation. + * @param markerToSet the Marker to set with information parsed from the macro invocation. After returning `false`, + * the marker's type ID will be set, startIndex will point to the first byte of the invocation's + * body, and endIndex will either be -1 (when not a system symbol or prefixed invocation), or + * will be set to the end of the invocation. + * @param length the declared length of the invocation. Ignored unless this is a length-prefixed invocation + * (denoted by `valueTid.variableLength == true`). + * @return true if not enough data was available in the stream to complete the header; otherwise, false. + */ + private boolean slowReadMacroInvocationHeader(IonTypeID valueTid, Marker markerToSet, long length) { + if (valueTid.macroId < 0) { + if (valueTid.lowerNibble == 0xE || valueTid.variableLength) { + // Opcode 0xEE or Opcode 0xF5 (when length > 0): Read the macro ID as a FlexUInt. + long idStart = peekIndex; + macroInvocationId = slowReadFlexUInt_1_1(); + // The length included the macro ID. Subtract the length of the macro ID so that the end index can + // be set correctly. + length -= peekIndex - idStart; + if (macroInvocationId < 0) { + return true; + } + } else { + // Opcode 0xEF: system macro invocation or system symbol value. + int truncatedId = slowReadByte(); + if (truncatedId < 0) { + return true; + } + // The downcast to byte then upcast to long results in sign extension, treating the byte as a FixedInt. + macroInvocationId = (byte) truncatedId; + setSystemTokenMarker(valueTid, markerToSet); + return false; + } + } else if (valueTid.length > 0) { + // Opcode 0x4: the rest of the macro ID follows in a 1-byte FixedUInt. + // Opcode 0x5: the rest of the macro ID follows in a 2-byte FixedUInt. + if (!fillAt(peekIndex, valueTid.length)) { + return true; + } + int remainingId = slowPeekByte(); + if (valueTid.length > 1) { + remainingId |= ((byte) slowPeekByte() << 8); + } + macroInvocationId = valueTid.macroId + remainingId; + } else { + // Opcodes 0x00 - 0x3F -- the opcode is the macro ID. + macroInvocationId = valueTid.macroId; + } + setUserMacroInvocationMarker(valueTid, markerToSet, length); + return false; + } + /** * Reads a value header, ensuring enough bytes are buffered. Upon invocation, `peekIndex` must * be positioned on the first byte that follows the given type ID byte. After return, `peekIndex` @@ -2157,6 +2317,8 @@ private boolean slowReadValueHeader(IonTypeID valueTid, boolean isAnnotated, Mar return true; } valueLength = 0; + } else if (minorVersion == 1 && valueTid.isMacroInvocation) { + return slowReadMacroInvocationHeader(valueTid, markerToSet, valueLength); } else { setCheckpoint(CheckpointLocation.AFTER_SCALAR_HEADER); event = Event.START_SCALAR; @@ -2180,6 +2342,7 @@ private boolean slowReadValueHeader(IonTypeID valueTid, boolean isAnnotated, Mar validateAnnotationWrapperEndIndex(endIndex); } setMarker(endIndex, markerToSet); + markerToSet.typeId = valueTid; return false; } @@ -2689,6 +2852,14 @@ Marker getValueMarker() { return valueMarker; } + long getMacroInvocationId() { + return macroInvocationId; + } + + boolean isSystemInvocation() { + return isSystemInvocation; + } + /** * Slices the buffer using the given offset and limit. Slices are treated as if they were at the top level. This * can be used to seek the reader to a "span" of bytes that represent a value in the stream. diff --git a/src/main/java/com/amazon/ion/impl/IonTypeID.java b/src/main/java/com/amazon/ion/impl/IonTypeID.java index 4c55737c2..28554f30c 100644 --- a/src/main/java/com/amazon/ion/impl/IonTypeID.java +++ b/src/main/java/com/amazon/ion/impl/IonTypeID.java @@ -85,6 +85,7 @@ final class IonTypeID { static final IonTypeID[] NULL_TYPE_IDS_1_1; static final IonTypeID STRUCT_WITH_FLEX_SYMS_ID; static final IonTypeID DELIMITED_END_ID; + static final IonTypeID SYSTEM_SYMBOL_VALUE; static { TYPE_IDS_NO_IVM = new IonTypeID[NUMBER_OF_BYTES]; TYPE_IDS_1_0 = new IonTypeID[NUMBER_OF_BYTES]; @@ -134,6 +135,8 @@ final class IonTypeID { // This is used as a dummy ID when a delimited container reaches its end. The key here is that the type ID's // lower nibble is OpCodes.DELIMITED_END_MARKER. DELIMITED_END_ID = TYPE_IDS_1_1[DELIMITED_END_MARKER & 0xFF]; + // This is used as a dummy ID when a system symbol value is encoded using the 0xEF opcode in Ion 1.1. + SYSTEM_SYMBOL_VALUE = TYPE_IDS_1_1[SYMBOL_ADDRESS_1_BYTE & 0xFF]; } final IonType type; @@ -266,29 +269,31 @@ private IonTypeID(byte id, int minorVersion) { boolean isNull = false; int length = -1; if (isMacroInvocation) { - if (id == E_EXPRESSION_FLEX_UINT) { - variableLength = true; - macroId = -1; - } else if (upperNibble == 0x5) { - // TODO: For 0x4_ and 0x5_, the bias can be precomputed based on the lower nibble. - // Consider precomputing and adding it to the type id or some other relevant location. + if (upperNibble == 0x5) { variableLength = false; length = 2; // This isn't the whole macro ID, but it's all the relevant bits from the type ID byte (the 4 - // least-significant bits). - macroId = lowerNibble; + // least-significant bits), with pre-computed bias. + macroId = (lowerNibble << 16) + 4160; } else if (upperNibble == 0x4) { variableLength = false; length = 1; // This isn't the whole macro ID, but it's all the relevant bits from the type ID byte (the 4 - // least-significant bits). - macroId = lowerNibble; + // least-significant bits), with pre-computed bias. + macroId = (lowerNibble << 8) + 64; } else if (upperNibble < 0x4){ variableLength = false; macroId = id; + length = 0; } else { - // System or flexuint macro invocation. - variableLength = upperNibble == 0xF; + if (upperNibble == 0xF) { + // FlexUInt length-prefixed macro invocation. + variableLength = true; + } else { + // System invocation; ID follows as a 1-byte FixedInt. + variableLength = false; + length = 1; + } macroId = -1; } type = null; diff --git a/src/test/java/com/amazon/ion/TestUtils.java b/src/test/java/com/amazon/ion/TestUtils.java index 5ecd661c7..895f9d9b7 100644 --- a/src/test/java/com/amazon/ion/TestUtils.java +++ b/src/test/java/com/amazon/ion/TestUtils.java @@ -589,6 +589,16 @@ public byte[] toByteArray() { } } + /** + * Returns the given data prepended with an IVM for the requested 1.x minor version. + * @param minorVersion the IVM version to prepend. + * @param data the data. + * @return the data with an IVM prepended. + */ + public static byte[] withIvm(int minorVersion, byte[] data) throws Exception { + return new TestUtils.BinaryIonAppender(minorVersion).append(data).toByteArray(); + } + /** * Compresses the given bytes using GZIP. * @param bytes the bytes to compress. diff --git a/src/test/java/com/amazon/ion/impl/IonCursorBinaryTest.java b/src/test/java/com/amazon/ion/impl/IonCursorBinaryTest.java index 0b7a6079d..821f36d4e 100644 --- a/src/test/java/com/amazon/ion/impl/IonCursorBinaryTest.java +++ b/src/test/java/com/amazon/ion/impl/IonCursorBinaryTest.java @@ -8,6 +8,7 @@ import com.amazon.ion.IonType; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; import java.io.ByteArrayInputStream; @@ -21,6 +22,7 @@ import static com.amazon.ion.IonCursor.Event.VALUE_READY; import static com.amazon.ion.IonCursor.Event.START_CONTAINER; import static com.amazon.ion.IonCursor.Event.START_SCALAR; +import static com.amazon.ion.TestUtils.withIvm; import static com.amazon.ion.impl.IonCursorTestUtilities.STANDARD_BUFFER_CONFIGURATION; import static com.amazon.ion.impl.IonCursorTestUtilities.Expectation; import static com.amazon.ion.impl.IonCursorTestUtilities.ExpectationProvider; @@ -35,19 +37,21 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; public class IonCursorBinaryTest { - private static IonCursorBinary initializeCursor(IonBufferConfiguration configuration, boolean constructFromBytes, int... data) { + private static IonCursorBinary initializeCursor(IonBufferConfiguration configuration, boolean constructFromBytes, byte[] data) { IonCursorBinary cursor; if (constructFromBytes) { - cursor = new IonCursorBinary(configuration, bytes(data), 0, data.length); + cursor = new IonCursorBinary(configuration, data, 0, data.length); } else { cursor = new IonCursorBinary( configuration, - new ByteArrayInputStream(bytes(data)), + new ByteArrayInputStream(data), null, 0, 0 @@ -58,7 +62,49 @@ private static IonCursorBinary initializeCursor(IonBufferConfiguration configura } private static IonCursorBinary initializeCursor(boolean constructFromBytes, int... data) { - return initializeCursor(STANDARD_BUFFER_CONFIGURATION, constructFromBytes, data); + return initializeCursor(STANDARD_BUFFER_CONFIGURATION, constructFromBytes, bytes(data)); + } + + public enum InputType { + + /** + * The cursor will be constructed from a fixed byte array. + */ + FIXED_BYTES { + @Override + IonCursorBinary initializeCursor(byte[] data) { + return IonCursorBinaryTest.initializeCursor(STANDARD_BUFFER_CONFIGURATION, true, data); + } + }, + + /** + * The cursor will be constructed from an InputStream with all bytes available up front. + */ + FIXED_STREAM { + @Override + IonCursorBinary initializeCursor(byte[] data) { + return IonCursorBinaryTest.initializeCursor(STANDARD_BUFFER_CONFIGURATION, false, data); + } + }, + + /** + * The cursor will be constructed from an InputStream that is fed bytes one by one, expecting NEEDS_DATA + * after each byte except the final one. + */ + INCREMENTAL { + @Override + IonCursorBinary initializeCursor(byte[] data) { + ResizingPipedInputStream pipe = new ResizingPipedInputStream(data.length); + IonCursorBinary cursor = new IonCursorBinary(STANDARD_BUFFER_CONFIGURATION, pipe, null, 0, 0); + for (byte b : data) { + assertEquals(NEEDS_DATA, cursor.nextValue()); + pipe.receive(b); + } + return cursor; + } + }; + + abstract IonCursorBinary initializeCursor(byte[] data); } /** @@ -380,7 +426,7 @@ public void skipOversizeDelimitedContainerAtDepth1() { AtomicInteger oversizeValueCounter = new AtomicInteger(0); AtomicInteger oversizeSymbolTableCounter = new AtomicInteger(0); AtomicInteger byteCounter = new AtomicInteger(0); - int[] data = new int[] { + byte[] data = bytes( 0xE0, 0x01, 0x01, 0xEA, 0xF3, // Delimited struct 0x07, // Field SID 3 @@ -390,7 +436,7 @@ public void skipOversizeDelimitedContainerAtDepth1() { 0x09, // Field SID 4 0x61, 0x01, // Int length 1, starting at byte index 16 0x01, 0xF0 // End delimited struct - }; + ); IonCursorBinary cursor = initializeCursor( IonBufferConfiguration.Builder.standard() .withInitialBufferSize(5) @@ -636,4 +682,93 @@ public void expectIncompleteIvmToFailCleanly(boolean constructFromBytes) { } cursor.close(); } + + /** + * Asserts that the given data contains macro invocation that matches the given attributes. + * @param input the data (without IVM) to test. + * @param inputType the type of input to provide to the cursor. + * @param expectedStartIndex the expected start index of the invocation's body. + * @param expectedEndIndex the expected end index of the invocation's body, or -1 if the end index cannot be + * computed from the encoding alone. + * @param expectedId the ID of the macro being invoked. + * @param isSystemInvocation whether the invocation is of a system macro. + */ + private static void testMacroInvocation( + byte[] input, + InputType inputType, + int expectedStartIndex, + int expectedEndIndex, + int expectedId, + boolean isSystemInvocation + ) throws Exception { + try (IonCursorBinary cursor = inputType.initializeCursor(withIvm(1, input))) { + assertEquals(NEEDS_INSTRUCTION, cursor.nextValue()); + Marker invocationMarker = cursor.getValueMarker(); + assertTrue(invocationMarker.typeId.isMacroInvocation); + assertEquals(expectedStartIndex, invocationMarker.startIndex); + assertEquals(expectedEndIndex, invocationMarker.endIndex); + assertEquals(expectedId, cursor.getMacroInvocationId()); + assertEquals(isSystemInvocation, cursor.isSystemInvocation()); + } + } + + @ParameterizedTest(name = "inputType={0}") + @EnumSource(InputType.class) + public void macroInvocationWithIdInOpcode(InputType inputType) throws Exception { + // Opcode 0x13 -> macro ID 0x13 + testMacroInvocation(bytes(0x13), inputType, 5, -1, 0x13, false); + } + + @ParameterizedTest(name = "inputType={0}") + @EnumSource(InputType.class) + public void macroInvocationWithOneByteFixedUIntId(InputType inputType) throws Exception { + // Opcode 0x43; 1-byte FixedUInt 0x09 follows + testMacroInvocation(bytes(0x43, 0x09), inputType, 6, -1, 841, false); + } + + @ParameterizedTest(name = "inputType={0}") + @EnumSource(InputType.class) + public void macroInvocationWithTwoByteFixedUIntId(InputType inputType) throws Exception { + // Opcode 0x52; 2-byte FixedUInt 0x06, 0x1E follows + testMacroInvocation(bytes(0x52, 0x06, 0x1E), inputType, 7, -1, 142918, false); + } + + @ParameterizedTest(name = "inputType={0}") + @EnumSource(InputType.class) + public void macroInvocationWithFlexUIntId(InputType inputType) throws Exception { + // Opcode 0xEE; 3-byte FlexUInt 0xFC, 0xFF, 0xFF follows + testMacroInvocation(bytes(0xEE, 0xFC, 0xFF, 0xFF), inputType, 8, -1, 2097151, false); + } + + @ParameterizedTest(name = "inputType={0}") + @EnumSource(InputType.class) + public void macroInvocationLengthPrefixed(InputType inputType) throws Exception { + // Opcode 0xF5; FlexUInt length 1 followed by FlexUInt ID 2 + testMacroInvocation(bytes(0xF5, 0x03, 0x05), inputType, 7, 7, 2, false); + } + + @ParameterizedTest(name = "inputType={0}") + @EnumSource(InputType.class) + public void systemMacroInvocation(InputType inputType) throws Exception { + // Opcode 0xEF; 1-byte FixedInt follows. Positive 4 indicates system macro ID 4. + testMacroInvocation(bytes(0xEF, 0x04), inputType, 6, -1, 4, true); + } + + @ParameterizedTest(name = "inputType={0}") + @EnumSource(InputType.class) + public void systemSymbolValue(InputType inputType) throws Exception { + // Opcode 0xEF; 1-byte FixedInt follows. 0xFE (-2) indicates system symbol ID 2. + byte[] data = withIvm(1, bytes(0xEF, 0xFE)); + try (IonCursorBinary cursor = inputType.initializeCursor(data)) { + assertEquals(START_SCALAR, cursor.nextValue()); + assertTrue(cursor.isSystemInvocation()); + Marker invocationMarker = cursor.getValueMarker(); + assertFalse(invocationMarker.typeId.isMacroInvocation); + assertEquals(6, invocationMarker.startIndex); + assertEquals(6, invocationMarker.endIndex); + // Note: a higher-level reader will use the sign to direct the lookup to the system symbol table instead of + // the system macro table. + assertEquals(-2, cursor.getMacroInvocationId()); + } + } } diff --git a/src/test/java/com/amazon/ion/impl/IonReaderContinuableTopLevelBinaryTest.java b/src/test/java/com/amazon/ion/impl/IonReaderContinuableTopLevelBinaryTest.java index 94aa57e93..e9a6784e3 100644 --- a/src/test/java/com/amazon/ion/impl/IonReaderContinuableTopLevelBinaryTest.java +++ b/src/test/java/com/amazon/ion/impl/IonReaderContinuableTopLevelBinaryTest.java @@ -4143,16 +4143,6 @@ public void expectUseAfterCloseToHaveNoEffect(boolean constructFromBytes) throws reader.close(); } - /** - * Returns the given data prepended with an IVM for the requested 1.x minor version. - * @param minorVersion the IVM version to prepend. - * @param data the data. - * @return the data with an IVM prepended. - */ - private static byte[] withIvm(int minorVersion, byte[] data) throws Exception { - return new TestUtils.BinaryIonAppender(minorVersion).append(data).toByteArray(); - } - /** * Creates an IonReader over the given data, which will be prepended with a binary Ion 1.1 IVM. * @param data the data to read.