diff --git a/cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/CBORParser.java b/cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/CBORParser.java index fd3266bbb..ff4cfe114 100644 --- a/cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/CBORParser.java +++ b/cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/CBORParser.java @@ -60,7 +60,21 @@ public enum Feature implements FormatFeature * * @since 2.20 */ - READ_UNDEFINED_AS_EMBEDDED_OBJECT(false) + READ_UNDEFINED_AS_EMBEDDED_OBJECT(false), + + /** + * Feature that determines how a CBOR "simple value" of major type 7 is exposed by parser. + *

+ * When enabled, the parser returns {@link JsonToken#VALUE_EMBEDDED_OBJECT} with + * an embedded value of type {@link CBORSimpleValue}, allowing the caller to distinguish + * these values from actual {@link JsonToken#VALUE_NUMBER_INT}s. + * When disabled, simple values are returned as {@link JsonToken#VALUE_NUMBER_INT}. + *

+ * The default value is {@code false} for backwards compatibility (with versions prior to 2.20). + * + * @since 2.20 + */ + READ_SIMPLE_VALUE_AS_EMBEDDED_OBJECT(false) ; private final boolean _defaultState; @@ -363,6 +377,14 @@ public int getFirstTag() { */ protected TagList _tagValues = new TagList(); + /** + * When major type 7 value is encountered and exposed as {@link JsonToken#VALUE_EMBEDDED_OBJECT}, + * the value will be stored here. + * + * @since 2.20 + */ + protected CBORSimpleValue _simpleValue; + /** * Flag that indicates that the current token has not yet * been fully processed, and needs to be finished for @@ -824,9 +846,9 @@ public JsonToken nextToken() throws IOException _skipIncomplete(); } _tokenInputTotal = _currInputProcessed + _inputPtr; - // also: clear any data retained so far - _numTypesValid = NR_UNKNOWN; - _binaryValue = null; + + // also: clear any data retained for previous token + clearRetainedValues(); // First: need to keep track of lengths of defined-length Arrays and // Objects (to materialize END_ARRAY/END_OBJECT as necessary); @@ -1453,12 +1475,12 @@ public boolean nextFieldName(SerializableString str) throws IOException { // Two parsing modes; can only succeed if expecting field name, so handle that first: if (_streamReadContext.inObject() && _currToken != JsonToken.FIELD_NAME) { - _numTypesValid = NR_UNKNOWN; if (_tokenIncomplete) { _skipIncomplete(); } _tokenInputTotal = _currInputProcessed + _inputPtr; - _binaryValue = null; + // need to clear retained values for previous token + clearRetainedValues(); _tagValues.clear(); // completed the whole Object? if (!_streamReadContext.expectMoreValues()) { @@ -1506,19 +1528,19 @@ public boolean nextFieldName(SerializableString str) throws IOException } } // otherwise just fall back to default handling; should occur rarely - return (nextToken() == JsonToken.FIELD_NAME) && str.getValue().equals(getCurrentName()); + return (nextToken() == JsonToken.FIELD_NAME) && str.getValue().equals(currentName()); } @Override public String nextFieldName() throws IOException { if (_streamReadContext.inObject() && _currToken != JsonToken.FIELD_NAME) { - _numTypesValid = NR_UNKNOWN; if (_tokenIncomplete) { _skipIncomplete(); } _tokenInputTotal = _currInputProcessed + _inputPtr; - _binaryValue = null; + // need to clear retained values for previous token + clearRetainedValues(); _tagValues.clear(); // completed the whole Object? if (!_streamReadContext.expectMoreValues()) { @@ -1843,7 +1865,10 @@ public Object getEmbeddedObject() throws IOException if (_tokenIncomplete) { _finishToken(); } - if (_currToken == JsonToken.VALUE_EMBEDDED_OBJECT ) { + if (_currToken == JsonToken.VALUE_EMBEDDED_OBJECT) { + if (_simpleValue != null) { + return _simpleValue; + } return _binaryValue; } return null; @@ -1933,11 +1958,11 @@ private final byte[] _getBinaryFromString(Base64Variant variant) throws IOExcept /** * Checking whether the current token represents an `undefined` value (0xF7). *

- * This method allows distinguishing between real {@code null} and `undefined`, + * This method allows distinguishing between real {@code null} and {@code undefined}, * even if {@link CBORParser.Feature#READ_UNDEFINED_AS_EMBEDDED_OBJECT} is disabled * and the token is reported as {@link JsonToken#VALUE_NULL}. * - * @return {@code true} if current token is an `undefined`, {@code false} otherwise + * @return {@code true} if current token is an {@code undefined}, {@code false} otherwise * * @since 2.20 */ @@ -3713,9 +3738,10 @@ protected JsonToken _decodeUndefinedValue() { * Helper method that deals with details of decoding unallocated "simple values" * and exposing them as expected token. *

- * As of Jackson 2.12, simple values are exposed as - * {@link JsonToken#VALUE_NUMBER_INT}s, - * but in later versions this is planned to be changed to separate value type. + * Starting with Jackson 2.20, this behavior can be changed by enabling the + * {@link CBORParser.Feature#READ_SIMPLE_VALUE_AS_EMBEDDED_OBJECT} + * feature, in which case simple values are returned as {@link JsonToken#VALUE_EMBEDDED_OBJECT} with an + * embedded {@link CBORSimpleValue} instance. * * @since 2.12 */ @@ -3723,28 +3749,39 @@ public JsonToken _decodeSimpleValue(int lowBits, int ch) throws IOException { if (lowBits > 24) { _invalidToken(ch); } + final boolean simpleAsEmbedded = Feature.READ_SIMPLE_VALUE_AS_EMBEDDED_OBJECT.enabledIn(_formatFeatures); if (lowBits < 24) { - _numberInt = lowBits; + if (simpleAsEmbedded) { + _simpleValue = new CBORSimpleValue(lowBits); + } else { + _numberInt = lowBits; + } } else { // need another byte if (_inputPtr >= _inputEnd) { loadMoreGuaranteed(); } - _numberInt = _inputBuffer[_inputPtr++] & 0xFF; + // As per CBOR spec, values below 32 not allowed to avoid // confusion (as well as guarantee uniqueness of encoding) - if (_numberInt < 32) { + int value = _inputBuffer[_inputPtr++] & 0xFF; + if (value < 32) { throw _constructError("Invalid second byte for simple value: 0x" - +Integer.toHexString(_numberInt)+" (only values 0x20 - 0xFF allowed)"); + +Integer.toHexString(value)+" (only values 0x20 - 0xFF allowed)"); + } + + if (simpleAsEmbedded) { + _simpleValue = new CBORSimpleValue(value); + } else { + _numberInt = value; } } - // 25-Nov-2020, tatu: Although ideally we should report these - // as `JsonToken.VALUE_EMBEDDED_OBJECT`, due to late addition - // of handling in 2.12, simple value in 2.12 will be reported - // as simple ints. + if (simpleAsEmbedded) { + return JsonToken.VALUE_EMBEDDED_OBJECT; + } _numTypesValid = NR_INT; - return (JsonToken.VALUE_NUMBER_INT); + return JsonToken.VALUE_NUMBER_INT; } /* @@ -4101,4 +4138,11 @@ private void createChildObjectContext(final int len) throws IOException { _streamReadContext = _streamReadContext.createChildObjectContext(len); _streamReadConstraints.validateNestingDepth(_streamReadContext.getNestingDepth()); } + + // @since 2.20 + private void clearRetainedValues() { + _numTypesValid = NR_UNKNOWN; + _binaryValue = null; + _simpleValue = null; + } } diff --git a/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/SimpleValuesTest.java b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/SimpleValuesTest.java index d72f0175c..e1eed4ea2 100644 --- a/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/SimpleValuesTest.java +++ b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/parse/SimpleValuesTest.java @@ -30,6 +30,23 @@ public void testTinySimpleValues() throws Exception } } + @Test + public void testTinySimpleValuesAsEmbeddedObjectWhenEnabled() throws Exception + { + CBORFactory f = CBORFactory.builder() + .enable(CBORParser.Feature.READ_SIMPLE_VALUE_AS_EMBEDDED_OBJECT) + .build(); + // Values 0..19 are unassigned, valid to encounter + for (int v = 0; v <= 19; ++v) { + byte[] doc = new byte[1]; + doc[0] = (byte) (CBORConstants.PREFIX_TYPE_MISC + v); + try (CBORParser p = cborParser(f, doc)) { + assertToken(JsonToken.VALUE_EMBEDDED_OBJECT, p.nextToken()); + assertEquals(new CBORSimpleValue(v), p.getEmbeddedObject()); + } + } + } + @Test public void testValidByteLengthMinimalValues() throws Exception { // Values 32..255 are unassigned, valid to encounter @@ -44,6 +61,21 @@ public void testValidByteLengthMinimalValues() throws Exception { } } + @Test + public void testValidByteLengthMinimalValuesAsEmbeddedObjectWhenEnabled() throws Exception { + // Values 32..255 are unassigned, valid to encounter + CBORFactory f = CBORFactory.builder() + .enable(CBORParser.Feature.READ_SIMPLE_VALUE_AS_EMBEDDED_OBJECT) + .build(); + for (int v = 32; v <= 255; ++v) { + byte[] doc = { (byte) (CBORConstants.PREFIX_TYPE_MISC + 24), (byte) v }; + try (CBORParser p = cborParser(f, doc)) { + assertToken(JsonToken.VALUE_EMBEDDED_OBJECT, p.nextToken()); + assertEquals(new CBORSimpleValue(v), p.getEmbeddedObject()); + } + } + } + @Test public void testInvalidByteLengthMinimalValues() throws Exception { // Values 0..31 are invalid for variant that takes 2 bytes... diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index f407d82b0..4214e8634 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -397,3 +397,7 @@ Fawzi Essam (@iifawzi) * Contributed fix for #431: (cbor) Negative `BigInteger` values not encoded/decoded correctly (2.20.0) + * Contributed implementation of #587: (cbor) Allow exposing CBOR Simple values as + `JsonToken.VALUE_EMBEDDED_OBJECT` with a feature flag + (2.20.0) + diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index b4c66a319..79b9e4c4c 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -22,6 +22,9 @@ Active maintainers: #431: (cbor) Negative `BigInteger` values not encoded/decoded correctly (reported by Brian G) (fix contributed by Fawzi E) +#587: (cbor) Allow exposing CBOR Simple values as `JsonToken.VALUE_EMBEDDED_OBJECT` + with a feature flag + (implementation contributed by Fawzi E) - Generate SBOMs [JSTEP-14] 2.19.0 (24-Apr-2025)