Skip to content

Commit 29c171a

Browse files
authored
[CBOR] - Implement RFC compliant BigInteger bytes encoding & decoding (#578)
1 parent 75a730b commit 29c171a

File tree

7 files changed

+370
-10
lines changed

7 files changed

+370
-10
lines changed

cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/CBORGenerator.java

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ public class CBORGenerator extends GeneratorBase
2525
{
2626
private final static int[] NO_INTS = new int[0];
2727

28+
// @since 2.20
29+
private final static BigInteger BI_MINUS_ONE = BigInteger.ONE.negate();
30+
2831
/**
2932
* Let's ensure that we have big enough output buffer because of safety
3033
* margins we need for UTF-8 encoding.
@@ -115,6 +118,25 @@ public enum Feature implements FormatFeature {
115118
* @since 2.15
116119
*/
117120
WRITE_MINIMAL_DOUBLES(false),
121+
122+
/**
123+
* Feature that determines how binary tagged negative BigInteger values are
124+
* encoded: either using CBOR standard encoding logic (as per spec),
125+
* or using legacy Jackson encoding logic (encoding up to Jackson 2.19).
126+
* When enabled, uses CBOR standard specified encoding of negative values
127+
* (e.g., -1 is encoded {@code [0xC3, 0x41, 0x00]}).
128+
* When disabled, maintains backwards compatibility with existing implementations
129+
* (e.g., -1 is encoded {@code [0xC3, 0x41, 0x01]}) and uses legacy Jackson encoding.
130+
*<p>
131+
* Note that there is the counterpart
132+
* {@link CBORParser.Feature#DECODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING}
133+
* for encoding.
134+
*<p>
135+
* Default value is {@code false} for backwards-compatibility.
136+
*
137+
* @since 2.20
138+
*/
139+
ENCODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING(false)
118140
;
119141

120142
protected final boolean _defaultState;
@@ -1210,14 +1232,17 @@ public void writeNumber(BigInteger v) throws IOException {
12101232
// Main write method isolated so that it can be called directly
12111233
// in cases where that is needed (to encode BigDecimal)
12121234
protected void _write(BigInteger v) throws IOException {
1213-
/*
1214-
* Supported by using type tags, as per spec: major type for tag '6'; 5
1235+
/* Supported by using type tags, as per spec: major type for tag '6'; 5
12151236
* LSB either 2 for positive bignum or 3 for negative bignum. And then
12161237
* byte sequence that encode variable length integer.
12171238
*/
12181239
if (v.signum() < 0) {
12191240
_writeByte(BYTE_TAG_BIGNUM_NEG);
1220-
v = v.negate();
1241+
if (isEnabled(CBORGenerator.Feature.ENCODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING)) {
1242+
v = BI_MINUS_ONE.subtract(v);
1243+
} else {
1244+
v = v.negate();
1245+
}
12211246
} else {
12221247
_writeByte(BYTE_TAG_BIGNUM_POS);
12231248
}

cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/CBORParser.java

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,24 @@ public class CBORParser extends ParserMinimalBase
2828
*/
2929
public enum Feature implements FormatFeature
3030
{
31-
// BOGUS(false)
31+
/**
32+
* Feature that determines how binary tagged negative BigInteger values are
33+
* decoded: either assuming CBOR standard encoding logic (as per spec),
34+
* or the legacy Jackson encoding logic (encoding up to Jackson 2.19).
35+
* When enabled, ensures proper encoding of negative values
36+
* (e.g., {@code [0xC3, 0x41, 0x00]} is decoded as -1)
37+
* When disabled, maintains backwards compatibility with existing implementations
38+
* (e.g., {@code [0xC3, 0x41, 0x00]} is decoded as 0).
39+
*<p>
40+
* Note that there is the counterpart
41+
* {@link CBORGenerator.Feature#ENCODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING}
42+
* for encoding.
43+
*<p>
44+
* The default value is {@code false} for backwards compatibility.
45+
*
46+
* @since 2.20
47+
*/
48+
DECODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING(false)
3249
;
3350

3451
final boolean _defaultState;
@@ -147,6 +164,9 @@ public int getFirstTag() {
147164

148165
private final static int[] UTF8_UNIT_CODES = CBORConstants.sUtf8UnitLengths;
149166

167+
// @since 2.20
168+
private final static BigInteger BI_MINUS_ONE = BigInteger.ONE.negate();
169+
150170
// Constants for handling of 16-bit "mini-floats"
151171
private final static double MATH_POW_2_10 = Math.pow(2, 10);
152172
private final static double MATH_POW_2_NEG14 = Math.pow(2, -14);
@@ -165,6 +185,14 @@ public int getFirstTag() {
165185
/**********************************************************
166186
*/
167187

188+
/**
189+
* Bit flag composed of bits that indicate which
190+
* {@link CBORParser.Feature}s are enabled.
191+
*<p>
192+
* @since 2.20
193+
*/
194+
protected int _formatFeatures;
195+
168196
/**
169197
* Codec used for data binding when (if) requested.
170198
*/
@@ -515,6 +543,7 @@ public CBORParser(IOContext ctxt, int parserFeatures, int cborFeatures,
515543
boolean bufferRecyclable)
516544
{
517545
super(parserFeatures, ctxt.streamReadConstraints());
546+
_formatFeatures = cborFeatures;
518547
_ioContext = ctxt;
519548
_objectCodec = codec;
520549
_symbols = sym;
@@ -561,12 +590,15 @@ public Version version() {
561590
/**********************************************************
562591
*/
563592

564-
// public JsonParser overrideStdFeatures(int values, int mask)
593+
@Override
594+
public final JsonParser overrideFormatFeatures(int values, int mask) {
595+
_formatFeatures = (_formatFeatures & ~mask) | (values & mask);
596+
return this;
597+
}
565598

566599
@Override
567-
public int getFormatFeatures() {
568-
// No parser features, yet
569-
return 0;
600+
public final int getFormatFeatures() {
601+
return _formatFeatures;
570602
}
571603

572604
@Override // since 2.12
@@ -1123,9 +1155,15 @@ protected JsonToken _handleTaggedBinary(TagList tags) throws IOException
11231155
_numberBigInt = BigInteger.ZERO;
11241156
} else {
11251157
_streamReadConstraints.validateIntegerLength(_binaryValue.length);
1126-
BigInteger nr = new BigInteger(_binaryValue);
1158+
final BigInteger nr;
11271159
if (neg) {
1128-
nr = nr.negate();
1160+
if (Feature.DECODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING.enabledIn(_formatFeatures)) {
1161+
nr = BI_MINUS_ONE.subtract(new BigInteger(1, _binaryValue));
1162+
} else {
1163+
nr = new BigInteger(_binaryValue).negate();
1164+
}
1165+
} else {
1166+
nr = new BigInteger(_binaryValue);
11291167
}
11301168
_numberBigInt = nr;
11311169
}

cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/databind/CBORMapper.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,40 @@ public Builder(CBORMapper m) {
3636
/******************************************************************
3737
*/
3838

39+
/**
40+
* @since 2.20.0
41+
*/
42+
public Builder enable(CBORParser.Feature... features) {
43+
for (CBORParser.Feature f : features) {
44+
_streamFactory.enable(f);
45+
}
46+
return this;
47+
}
48+
49+
/**
50+
* @since 2.20.0
51+
*/
52+
public Builder disable(CBORParser.Feature... features) {
53+
for (CBORParser.Feature f : features) {
54+
_streamFactory.disable(f);
55+
}
56+
return this;
57+
}
58+
59+
/**
60+
* @since 2.20.0
61+
*/
62+
public Builder configure(CBORParser.Feature f, boolean state)
63+
{
64+
if (state) {
65+
_streamFactory.enable(f);
66+
} else {
67+
_streamFactory.disable(f);
68+
}
69+
return this;
70+
}
71+
72+
3973
/**
4074
* @since 2.14
4175
*/

cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/gen/GeneratorSimpleTest.java

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,147 @@ public void testBigDecimalValues() throws Exception
259259
assertArrayEquals(spec, b);
260260
}
261261

262+
// [dataformats-binary#431]
263+
// [https://datatracker.ietf.org/doc/html/rfc8949#section-3.4.3]
264+
@Test
265+
public void testSimpleBigIntegerEncoding() throws Exception
266+
{
267+
BigInteger minusOne = BigInteger.valueOf(-1);
268+
byte[] expectedBytes = {
269+
(byte) 0xC3, // tag 3 (negative bignum)
270+
(byte) 0x41 // byte string, length 1
271+
};
272+
273+
// Test correct encoding
274+
CBORFactory factory = CBORFactory.builder()
275+
.enable(CBORGenerator.Feature.ENCODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING)
276+
.build();
277+
ByteArrayOutputStream correctOut = new ByteArrayOutputStream();
278+
try (CBORGenerator gen1 = factory.createGenerator(correctOut)) {
279+
gen1.writeNumber(minusOne);
280+
}
281+
282+
byte[] result1 = correctOut.toByteArray();
283+
assertEquals(3, result1.length);
284+
assertEquals(expectedBytes[0], result1[0]);
285+
assertEquals(expectedBytes[1], result1[1]);
286+
assertEquals(0x00, result1[2]);
287+
288+
// Test incorrect encoding for compatibility
289+
ByteArrayOutputStream incorrectOut = new ByteArrayOutputStream();
290+
factory = CBORFactory.builder()
291+
.disable(CBORGenerator.Feature.ENCODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING)
292+
.build();
293+
try (CBORGenerator gen2 = factory.createGenerator(incorrectOut)) {
294+
gen2.writeNumber(minusOne);
295+
}
296+
297+
byte[] result2 = incorrectOut.toByteArray();
298+
assertEquals(3, result2.length);
299+
assertEquals(expectedBytes[0], result2[0]);
300+
assertEquals(expectedBytes[1], result2[1]);
301+
assertEquals(0x01, result2[2]);
302+
}
303+
304+
// [dataformats-binary#431]
305+
// [https://datatracker.ietf.org/doc/html/rfc8949#section-3.4.3]
306+
@Test
307+
public void testZeroBigIntegerEncoding() throws Exception {
308+
BigInteger zero = BigInteger.valueOf(0);
309+
byte[] expectedBytes = {
310+
(byte) 0xC2, // tag 2 (positive bignum)
311+
(byte) 0x41, // byte string, 1 byte
312+
(byte) 0x00, // 0
313+
};
314+
315+
ByteArrayOutputStream correctOut = new ByteArrayOutputStream();
316+
CBORFactory factory = CBORFactory.builder()
317+
.enable(CBORGenerator.Feature.ENCODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING)
318+
.build();
319+
try (CBORGenerator gen1 = factory.createGenerator(correctOut)) {
320+
gen1.writeNumber(zero);
321+
}
322+
323+
byte[] result = correctOut.toByteArray();
324+
assertEquals(3, result.length);
325+
assertArrayEquals(expectedBytes, result);
326+
}
327+
328+
// [dataformats-binary#431]
329+
// [https://datatracker.ietf.org/doc/html/rfc8949#section-3.4.3]
330+
@Test
331+
public void testNegativeBigIntegerEncoding() throws Exception {
332+
BigInteger negativeBigInteger = new BigInteger("-340282366920938463463374607431768211456");
333+
// correct encoding: https://cbor.me/?bytes=c35100ffffffffffffffffffffffffffffffff
334+
byte[] expectedBytes = {
335+
(byte) 0xC3,
336+
(byte) 0x51,
337+
(byte) 0x00,
338+
(byte) 0xFF,
339+
(byte) 0xFF,
340+
(byte) 0xFF,
341+
(byte) 0xFF,
342+
(byte) 0xFF,
343+
(byte) 0xFF,
344+
(byte) 0xFF,
345+
(byte) 0xFF,
346+
(byte) 0xFF,
347+
(byte) 0xFF,
348+
(byte) 0xFF,
349+
(byte) 0xFF,
350+
(byte) 0xFF,
351+
(byte) 0xFF,
352+
(byte) 0xFF,
353+
(byte) 0xFF
354+
};
355+
356+
// Test correct encoding
357+
ByteArrayOutputStream correctOut = new ByteArrayOutputStream();
358+
CBORFactory factory = CBORFactory.builder()
359+
.enable(CBORGenerator.Feature.ENCODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING)
360+
.build();
361+
try (CBORGenerator gen1 = factory.createGenerator(correctOut)) {
362+
gen1.writeNumber(negativeBigInteger);
363+
}
364+
byte[] result1 = correctOut.toByteArray();
365+
assertArrayEquals(expectedBytes, result1);
366+
367+
// Test incorrect encoding for compatibility
368+
// incorrect encoding: https://cbor.me/?bytes=c3510100000000000000000000000000000000
369+
byte[] legacyExpectedBytes = {
370+
(byte) 0xC3,
371+
(byte) 0x51,
372+
(byte) 0x01,
373+
(byte) 0x00,
374+
(byte) 0x00,
375+
(byte) 0x00,
376+
(byte) 0x00,
377+
(byte) 0x00,
378+
(byte) 0x00,
379+
(byte) 0x00,
380+
(byte) 0x00,
381+
(byte) 0x00,
382+
(byte) 0x00,
383+
(byte) 0x00,
384+
(byte) 0x00,
385+
(byte) 0x00,
386+
(byte) 0x00,
387+
(byte) 0x00,
388+
(byte) 0x00,
389+
};
390+
ByteArrayOutputStream incorrectOut = new ByteArrayOutputStream();
391+
factory = CBORFactory.builder()
392+
.disable(CBORGenerator.Feature.ENCODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING)
393+
.build();
394+
try (CBORGenerator gen2 = factory.createGenerator(incorrectOut)) {
395+
gen2.writeNumber(negativeBigInteger);
396+
}
397+
398+
byte[] result2 = incorrectOut.toByteArray();
399+
assertEquals(19, result2.length);
400+
assertArrayEquals(legacyExpectedBytes, result2);
401+
}
402+
262403
@Test
263404
public void testEmptyArray() throws Exception
264405
{

0 commit comments

Comments
 (0)