Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions release-notes/CREDITS
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ PJ Fanning (@pjfanning)
* Contributed #776: (protobuf) `ProtobufGenerator.writeString(char[],int,int)`
writes enum values twice
(3.1.7)
* Contributed #781: (smile) `SmileGenerator.writeNumber(String)` should validate
number length against configured `StreamReadConstraints`, not global defaults
(3.3.0)

DongNyoung Lee (@Dongnyoung)

Expand Down
7 changes: 5 additions & 2 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,14 @@ implementations)
#775: (avro) Update to Avro 1.12.2
#777: (avro) Properties following an `@AvroEncode`d value are silently dropped
(fix by @cowtowncoder, w/ Claude code)
- (avro) Generated `array` schemas missing `java-class` for `java.util.List`,
breaking round-trip via Apache `ReflectDatumReader`
#780: (ion) `IonFactory` leaks stream when parser/generator construction fails
for `File`/`Path`
(contributed by @Dongnyoung)
#781: (smile) `SmileGenerator.writeNumber(String)` should validate number length
against configured `StreamReadConstraints`, not global defaults
(contributed by @pjfanning)
- (avro) Generated `array` schemas missing `java-class` for `java.util.List`,
breaking round-trip via Apache `ReflectDatumReader`

3.2.3 (not yet released)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1672,11 +1672,13 @@ public JsonGenerator writeNumber(String encodedValue) throws JacksonException
protected void _writeIntegralNumber(String enc, boolean neg) throws JacksonException
{
int len = enc.length();
// 16-Dec-2023, tatu: Guard against too-big numbers
_streamReadConstraints().validateIntegerLength(len);
if (neg) {
--len;
}
// 16-Dec-2023, tatu: Guard against too-big numbers
// 18-Sep-2026: length to validate is that of digits only, without sign,
// same as what parsers pass in
_streamReadConstraints().validateIntegerLength(len);
// let's do approximate optimization
try {
if (len <= 9) {
Expand All @@ -1698,7 +1700,9 @@ protected void _writeIntegralNumber(String enc, boolean neg) throws JacksonExcep
protected void _writeDecimalNumber(String enc) throws JacksonException
{
// 16-Dec-2023, tatu: Guard against too-big numbers
_streamReadConstraints().validateFPLength(enc.length());
// 18-Sep-2026: length to validate is that of digits only, without sign,
// decimal point or exponent marker, same as what parsers pass in
_streamReadConstraints().validateFPLength(_digitCount(enc));
// ... and check basic validity too
if (NumberInput.looksLikeValidNumber(enc)) {
try {
Expand Down Expand Up @@ -2771,12 +2775,21 @@ protected UnsupportedOperationException _notSupported() {

/**
* We need access to some reader-side constraints for safety-check within
* number decoding for {@linl #writeNumber(String)}: for now we need to
* rely on global defaults; should be ok for basic safeguarding.
*
* @since 2.17
* number decoding for {@link #writeNumber(String)}: these are the ones
* configured for the underlying factory, accessed via {@link IOContext}.
*/
protected StreamReadConstraints _streamReadConstraints() {
return StreamReadConstraints.defaults();
return _ioContext.streamReadConstraints();
}

private static int _digitCount(String enc) {
int count = 0;
for (int i = 0, len = enc.length(); i < len; ++i) {
char c = enc.charAt(i);
if (c <= '9' && c >= '0') {
++count;
}
}
return count;
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
package tools.jackson.dataformat.smile.gen;

import java.io.ByteArrayOutputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;

import org.junit.jupiter.api.Test;

import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.core.StreamReadConstraints;
import tools.jackson.core.exc.StreamConstraintsException;

import tools.jackson.dataformat.smile.*;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;

public class SmileGeneratorNumbersTest
extends BaseTestForSmile
Expand Down Expand Up @@ -203,6 +211,100 @@ public void testNumbersAsString() throws Exception
assertEquals(10, out.toByteArray().length);
}

// [dataformats-binary#781]: `writeNumber(String)` should use the factory's
// `StreamReadConstraints` for its number-length guard, not global defaults
@Test
public void testNumbersAsStringLengthLimit() throws Exception
{
final int maxLen = 20;
SmileFactory f = smileFactoryBuilder(false, false, false)
.streamReadConstraints(StreamReadConstraints.builder()
.maxNumberLength(maxLen).build())
.build();
final String tooLongInt = "1".repeat(maxLen + 1);
// NOTE: limit applies to digits only, so sign/decimal point do not count
final String tooLongNegInt = "-" + "1".repeat(maxLen + 1);
final String tooLongDec = "1." + "1".repeat(maxLen);

for (String num : new String[] { tooLongInt, tooLongNegInt, tooLongDec }) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (SmileGenerator gen = (SmileGenerator) f.createGenerator(out)) {
gen.writeNumber(num);
fail("Should not pass for: "+num);
} catch (StreamConstraintsException e) {
verifyException(e, "Number value length (" + (maxLen + 1)
+ ") exceeds the maximum allowed (" + maxLen + ",");
}
}

// And conversely, one at the limit is fine
final String atLimit = "1".repeat(maxLen);
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (SmileGenerator gen = (SmileGenerator) f.createGenerator(out)) {
gen.writeNumber(atLimit);
}
try (JsonParser p = f.createParser(out.toByteArray())) {
assertEquals(JsonToken.VALUE_NUMBER_INT, p.nextToken());
assertEquals(new BigInteger(atLimit), p.getBigIntegerValue());
}

// Same for values where non-digits bring the total length past the limit:
// must match what parsers accept on read-back
final String negAtLimit = "-" + atLimit;
final String decAtLimit = "1." + "1".repeat(maxLen - 1);

out = new ByteArrayOutputStream();
try (SmileGenerator gen = (SmileGenerator) f.createGenerator(out)) {
gen.writeNumber(negAtLimit);
}
try (JsonParser p = f.createParser(out.toByteArray())) {
assertEquals(JsonToken.VALUE_NUMBER_INT, p.nextToken());
assertEquals(new BigInteger(negAtLimit), p.getBigIntegerValue());
}

out = new ByteArrayOutputStream();
try (SmileGenerator gen = (SmileGenerator) f.createGenerator(out)) {
gen.writeNumber(decAtLimit);
}
try (JsonParser p = f.createParser(out.toByteArray())) {
assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
assertEquals(new BigDecimal(decAtLimit), p.getDecimalValue());
}
}

// [dataformats-binary#781]: and the other direction -- limit configured above
// the 1000-character default must allow longer numbers than defaults would
@Test
public void testNumbersAsStringLengthLimitIncreased() throws Exception
{
final int maxLen = 2000;
SmileFactory f = smileFactoryBuilder(false, false, false)
.streamReadConstraints(StreamReadConstraints.builder()
.maxNumberLength(maxLen).build())
.build();
// Longer than the 1000-character default, but within configured limit
final String longInt = "1".repeat(1500);
final String longDec = "1." + "1".repeat(1499);

ByteArrayOutputStream out = new ByteArrayOutputStream();
try (SmileGenerator gen = (SmileGenerator) f.createGenerator(out)) {
gen.writeNumber(longInt);
}
try (JsonParser p = f.createParser(out.toByteArray())) {
assertEquals(JsonToken.VALUE_NUMBER_INT, p.nextToken());
assertEquals(new BigInteger(longInt), p.getBigIntegerValue());
}

out = new ByteArrayOutputStream();
try (SmileGenerator gen = (SmileGenerator) f.createGenerator(out)) {
gen.writeNumber(longDec);
}
try (JsonParser p = f.createParser(out.toByteArray())) {
assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
assertEquals(new BigDecimal(longDec), p.getDecimalValue());
}
}

// [dataformats-binary#608]
@Test
public void testFloat32FromSpecEncoding() throws Exception
Expand Down
Loading