diff --git a/cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/CBORGenerator.java b/cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/CBORGenerator.java index e2052d1cc..0863ec0ec 100644 --- a/cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/CBORGenerator.java +++ b/cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/CBORGenerator.java @@ -25,6 +25,9 @@ public class CBORGenerator extends GeneratorBase { private final static int[] NO_INTS = new int[0]; + // @since 2.20 + private final static BigInteger BI_MINUS_ONE = BigInteger.ONE.negate(); + /** * Let's ensure that we have big enough output buffer because of safety * margins we need for UTF-8 encoding. @@ -115,6 +118,25 @@ public enum Feature implements FormatFeature { * @since 2.15 */ WRITE_MINIMAL_DOUBLES(false), + + /** + * Feature that determines how binary tagged negative BigInteger values are + * encoded: either using CBOR standard encoding logic (as per spec), + * or using legacy Jackson encoding logic (encoding up to Jackson 2.19). + * When enabled, uses CBOR standard specified encoding of negative values + * (e.g., -1 is encoded {@code [0xC3, 0x41, 0x00]}). + * When disabled, maintains backwards compatibility with existing implementations + * (e.g., -1 is encoded {@code [0xC3, 0x41, 0x01]}) and uses legacy Jackson encoding. + *
+ * Note that there is the counterpart + * {@link CBORParser.Feature#DECODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING} + * for encoding. + *
+ * Default value is {@code false} for backwards-compatibility. + * + * @since 2.20 + */ + ENCODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING(false) ; protected final boolean _defaultState; @@ -1210,14 +1232,17 @@ public void writeNumber(BigInteger v) throws IOException { // Main write method isolated so that it can be called directly // in cases where that is needed (to encode BigDecimal) protected void _write(BigInteger v) throws IOException { - /* - * Supported by using type tags, as per spec: major type for tag '6'; 5 + /* Supported by using type tags, as per spec: major type for tag '6'; 5 * LSB either 2 for positive bignum or 3 for negative bignum. And then * byte sequence that encode variable length integer. */ if (v.signum() < 0) { _writeByte(BYTE_TAG_BIGNUM_NEG); - v = v.negate(); + if (isEnabled(CBORGenerator.Feature.ENCODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING)) { + v = BI_MINUS_ONE.subtract(v); + } else { + v = v.negate(); + } } else { _writeByte(BYTE_TAG_BIGNUM_POS); } 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 5bc01fda4..01ab6184d 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 @@ -28,7 +28,24 @@ public class CBORParser extends ParserMinimalBase */ public enum Feature implements FormatFeature { -// BOGUS(false) + /** + * Feature that determines how binary tagged negative BigInteger values are + * decoded: either assuming CBOR standard encoding logic (as per spec), + * or the legacy Jackson encoding logic (encoding up to Jackson 2.19). + * When enabled, ensures proper encoding of negative values + * (e.g., {@code [0xC3, 0x41, 0x00]} is decoded as -1) + * When disabled, maintains backwards compatibility with existing implementations + * (e.g., {@code [0xC3, 0x41, 0x00]} is decoded as 0). + *
+ * Note that there is the counterpart + * {@link CBORGenerator.Feature#ENCODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING} + * for encoding. + *
+ * The default value is {@code false} for backwards compatibility. + * + * @since 2.20 + */ + DECODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING(false) ; final boolean _defaultState; @@ -147,6 +164,9 @@ public int getFirstTag() { private final static int[] UTF8_UNIT_CODES = CBORConstants.sUtf8UnitLengths; + // @since 2.20 + private final static BigInteger BI_MINUS_ONE = BigInteger.ONE.negate(); + // Constants for handling of 16-bit "mini-floats" private final static double MATH_POW_2_10 = Math.pow(2, 10); private final static double MATH_POW_2_NEG14 = Math.pow(2, -14); @@ -165,6 +185,14 @@ public int getFirstTag() { /********************************************************** */ + /** + * Bit flag composed of bits that indicate which + * {@link CBORParser.Feature}s are enabled. + *
+ * @since 2.20 + */ + protected int _formatFeatures; + /** * Codec used for data binding when (if) requested. */ @@ -515,6 +543,7 @@ public CBORParser(IOContext ctxt, int parserFeatures, int cborFeatures, boolean bufferRecyclable) { super(parserFeatures, ctxt.streamReadConstraints()); + _formatFeatures = cborFeatures; _ioContext = ctxt; _objectCodec = codec; _symbols = sym; @@ -561,12 +590,15 @@ public Version version() { /********************************************************** */ -// public JsonParser overrideStdFeatures(int values, int mask) + @Override + public final JsonParser overrideFormatFeatures(int values, int mask) { + _formatFeatures = (_formatFeatures & ~mask) | (values & mask); + return this; + } @Override - public int getFormatFeatures() { - // No parser features, yet - return 0; + public final int getFormatFeatures() { + return _formatFeatures; } @Override // since 2.12 @@ -1123,9 +1155,15 @@ protected JsonToken _handleTaggedBinary(TagList tags) throws IOException _numberBigInt = BigInteger.ZERO; } else { _streamReadConstraints.validateIntegerLength(_binaryValue.length); - BigInteger nr = new BigInteger(_binaryValue); + final BigInteger nr; if (neg) { - nr = nr.negate(); + if (Feature.DECODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING.enabledIn(_formatFeatures)) { + nr = BI_MINUS_ONE.subtract(new BigInteger(1, _binaryValue)); + } else { + nr = new BigInteger(_binaryValue).negate(); + } + } else { + nr = new BigInteger(_binaryValue); } _numberBigInt = nr; } diff --git a/cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/databind/CBORMapper.java b/cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/databind/CBORMapper.java index c0b7734a9..5117cef64 100644 --- a/cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/databind/CBORMapper.java +++ b/cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/databind/CBORMapper.java @@ -36,6 +36,40 @@ public Builder(CBORMapper m) { /****************************************************************** */ + /** + * @since 2.20.0 + */ + public Builder enable(CBORParser.Feature... features) { + for (CBORParser.Feature f : features) { + _streamFactory.enable(f); + } + return this; + } + + /** + * @since 2.20.0 + */ + public Builder disable(CBORParser.Feature... features) { + for (CBORParser.Feature f : features) { + _streamFactory.disable(f); + } + return this; + } + + /** + * @since 2.20.0 + */ + public Builder configure(CBORParser.Feature f, boolean state) + { + if (state) { + _streamFactory.enable(f); + } else { + _streamFactory.disable(f); + } + return this; + } + + /** * @since 2.14 */ diff --git a/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/gen/GeneratorSimpleTest.java b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/gen/GeneratorSimpleTest.java index a3626f86e..e40f052cf 100644 --- a/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/gen/GeneratorSimpleTest.java +++ b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/gen/GeneratorSimpleTest.java @@ -259,6 +259,147 @@ public void testBigDecimalValues() throws Exception assertArrayEquals(spec, b); } + // [dataformats-binary#431] + // [https://datatracker.ietf.org/doc/html/rfc8949#section-3.4.3] + @Test + public void testSimpleBigIntegerEncoding() throws Exception + { + BigInteger minusOne = BigInteger.valueOf(-1); + byte[] expectedBytes = { + (byte) 0xC3, // tag 3 (negative bignum) + (byte) 0x41 // byte string, length 1 + }; + + // Test correct encoding + CBORFactory factory = CBORFactory.builder() + .enable(CBORGenerator.Feature.ENCODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING) + .build(); + ByteArrayOutputStream correctOut = new ByteArrayOutputStream(); + try (CBORGenerator gen1 = factory.createGenerator(correctOut)) { + gen1.writeNumber(minusOne); + } + + byte[] result1 = correctOut.toByteArray(); + assertEquals(3, result1.length); + assertEquals(expectedBytes[0], result1[0]); + assertEquals(expectedBytes[1], result1[1]); + assertEquals(0x00, result1[2]); + + // Test incorrect encoding for compatibility + ByteArrayOutputStream incorrectOut = new ByteArrayOutputStream(); + factory = CBORFactory.builder() + .disable(CBORGenerator.Feature.ENCODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING) + .build(); + try (CBORGenerator gen2 = factory.createGenerator(incorrectOut)) { + gen2.writeNumber(minusOne); + } + + byte[] result2 = incorrectOut.toByteArray(); + assertEquals(3, result2.length); + assertEquals(expectedBytes[0], result2[0]); + assertEquals(expectedBytes[1], result2[1]); + assertEquals(0x01, result2[2]); + } + + // [dataformats-binary#431] + // [https://datatracker.ietf.org/doc/html/rfc8949#section-3.4.3] + @Test + public void testZeroBigIntegerEncoding() throws Exception { + BigInteger zero = BigInteger.valueOf(0); + byte[] expectedBytes = { + (byte) 0xC2, // tag 2 (positive bignum) + (byte) 0x41, // byte string, 1 byte + (byte) 0x00, // 0 + }; + + ByteArrayOutputStream correctOut = new ByteArrayOutputStream(); + CBORFactory factory = CBORFactory.builder() + .enable(CBORGenerator.Feature.ENCODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING) + .build(); + try (CBORGenerator gen1 = factory.createGenerator(correctOut)) { + gen1.writeNumber(zero); + } + + byte[] result = correctOut.toByteArray(); + assertEquals(3, result.length); + assertArrayEquals(expectedBytes, result); + } + + // [dataformats-binary#431] + // [https://datatracker.ietf.org/doc/html/rfc8949#section-3.4.3] + @Test + public void testNegativeBigIntegerEncoding() throws Exception { + BigInteger negativeBigInteger = new BigInteger("-340282366920938463463374607431768211456"); + // correct encoding: https://cbor.me/?bytes=c35100ffffffffffffffffffffffffffffffff + byte[] expectedBytes = { + (byte) 0xC3, + (byte) 0x51, + (byte) 0x00, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF + }; + + // Test correct encoding + ByteArrayOutputStream correctOut = new ByteArrayOutputStream(); + CBORFactory factory = CBORFactory.builder() + .enable(CBORGenerator.Feature.ENCODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING) + .build(); + try (CBORGenerator gen1 = factory.createGenerator(correctOut)) { + gen1.writeNumber(negativeBigInteger); + } + byte[] result1 = correctOut.toByteArray(); + assertArrayEquals(expectedBytes, result1); + + // Test incorrect encoding for compatibility + // incorrect encoding: https://cbor.me/?bytes=c3510100000000000000000000000000000000 + byte[] legacyExpectedBytes = { + (byte) 0xC3, + (byte) 0x51, + (byte) 0x01, + (byte) 0x00, + (byte) 0x00, + (byte) 0x00, + (byte) 0x00, + (byte) 0x00, + (byte) 0x00, + (byte) 0x00, + (byte) 0x00, + (byte) 0x00, + (byte) 0x00, + (byte) 0x00, + (byte) 0x00, + (byte) 0x00, + (byte) 0x00, + (byte) 0x00, + (byte) 0x00, + }; + ByteArrayOutputStream incorrectOut = new ByteArrayOutputStream(); + factory = CBORFactory.builder() + .disable(CBORGenerator.Feature.ENCODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING) + .build(); + try (CBORGenerator gen2 = factory.createGenerator(incorrectOut)) { + gen2.writeNumber(negativeBigInteger); + } + + byte[] result2 = incorrectOut.toByteArray(); + assertEquals(19, result2.length); + assertArrayEquals(legacyExpectedBytes, result2); + } + @Test public void testEmptyArray() throws Exception { diff --git a/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/mapper/CBORMapperTest.java b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/mapper/CBORMapperTest.java index 4ab56b821..5567e4fcd 100644 --- a/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/mapper/CBORMapperTest.java +++ b/cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/mapper/CBORMapperTest.java @@ -6,6 +6,8 @@ import com.fasterxml.jackson.dataformat.cbor.*; import com.fasterxml.jackson.dataformat.cbor.databind.CBORMapper; +import java.math.BigInteger; + import static org.junit.jupiter.api.Assertions.*; public class CBORMapperTest extends CBORTestBase @@ -54,4 +56,112 @@ public void testMapperCopy() throws Exception assertNotSame(src, m2); assertSame(streamingF, m2.tokenStreamFactory()); } + + // [dataformats-binary#431] + @Test + public void testSimpleNegativeBigInteger() throws Exception { + byte[] encodedNegativeOne = { + (byte) 0xC3, // tag 3 (negative big integer) + (byte) 0x41, // byte string, length 1 + (byte) 0x00 // value 0 (become -1 after decoding) + }; + + // Test correct decoding + CBORMapper mapper1 = CBORMapper.builder() + .enable(CBORParser.Feature.DECODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING) + .build(); + assertEquals(BigInteger.valueOf(-1), + mapper1.readValue(encodedNegativeOne, BigInteger.class)); + + // Test incorrect decoding for compatibility + CBORMapper mapper2 = CBORMapper.builder() + .disable(CBORParser.Feature.DECODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING) + .build(); + assertEquals(BigInteger.ZERO, + mapper2.readValue(encodedNegativeOne, BigInteger.class)); + } + + + // [dataformats-binary#431] + @Test + public void testNegativeBigInteger() throws Exception { + // correct encoding: https://cbor.me/?bytes=c35100ffffffffffffffffffffffffffffffff + byte[] encodedNegative = { + (byte) 0xC3, + (byte) 0x51, + (byte) 0x00, // leading zero + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF + }; + + // Test correct decoding + CBORMapper mapper1 = CBORMapper.builder() + .enable(CBORParser.Feature.DECODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING) + .build(); + assertEquals(new BigInteger("-340282366920938463463374607431768211456"), + mapper1.readValue(encodedNegative, BigInteger.class)); + + + // Test incorrect decoding for compatibility + CBORMapper mapper2 = CBORMapper.builder() + .disable(CBORParser.Feature.DECODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING) + .build(); + assertEquals(new BigInteger("-340282366920938463463374607431768211455"), + mapper2.readValue(encodedNegative, BigInteger.class)); + } + + // [dataformats-binary#431] + @Test + public void testNegativeBigIntegerWithoutLeadingZero() throws Exception { + // correct encoding: https://cbor.me/?bytes=c350ffffffffffffffffffffffffffffffff + byte[] encodedNegative = { + (byte) 0xC3, + (byte) 0x50, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF + }; + + // Test correct decoding + CBORMapper mapper1 = CBORMapper.builder() + .enable(CBORParser.Feature.DECODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING) + .build(); + assertEquals(new BigInteger("-340282366920938463463374607431768211456"), + mapper1.readValue(encodedNegative, BigInteger.class)); + + + // Test incorrect decoding for compatibility + CBORMapper mapper2 = CBORMapper.builder() + .disable(CBORParser.Feature.DECODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING) + .build(); + assertEquals(BigInteger.ONE, + mapper2.readValue(encodedNegative, BigInteger.class)); + } } diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 8dec9da09..008bf45ac 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -385,3 +385,12 @@ Manuel Sugawara (@sugmanue) Josh Curry (@seadbrane) * Reported, contributed fix for #571: Unable to deserialize a pojo with IonStruct (2.19.0) + +Brian Gruber (@bgruber) + * Reported #431: (cbor) Negative `BigInteger` values not encoded/decoded correctly + (2.20.0) + +Fawzi Essam (@iifawzi) + * Contributed fix for #431: (cbor) Negative `BigInteger` values not encoded/decoded + correctly + (2.20.0) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 750168c00..e36f11800 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -16,6 +16,9 @@ Active maintainers: 2.20.0 (not yet released) +#431: (cbor) Negative `BigInteger` values not encoded/decoded correctly + (reported by Brian G) + (fix contributed by Fawzi E) - Generate SBOMs [JSTEP-14] 2.19.0 (24-Apr-2025)