Skip to content

[CBOR] - Implement RFC compliant BigInteger bytes encoding & decoding #578

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Apr 30, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*<p>
* Note that there is the counterpart
* {@link CBORParser.Feature#DECODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING}
* for encoding.
*<p>
* Default value is {@code false} for backwards-compatibility.
*
* @since 2.20
*/
ENCODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING(false)
;

protected final boolean _defaultState;
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*<p>
* Note that there is the counterpart
* {@link CBORGenerator.Feature#ENCODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING}
* for encoding.
*<p>
* The default value is {@code false} for backwards compatibility.
*
* @since 2.20
*/
DECODE_USING_STANDARD_NEGATIVE_BIGINT_ENCODING(false)
;

final boolean _defaultState;
Expand Down Expand Up @@ -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);
Expand All @@ -165,6 +185,14 @@ public int getFirstTag() {
/**********************************************************
*/

/**
* Bit flag composed of bits that indicate which
* {@link CBORParser.Feature}s are enabled.
*<p>
* @since 2.20
*/
protected int _formatFeatures;

/**
* Codec used for data binding when (if) requested.
*/
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Loading