From 6507252e59e9828d13902f8dbe2705fdbf0d7956 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Sat, 12 Sep 2026 21:07:46 +0100 Subject: [PATCH 1/2] Use configured StreamReadConstraints in SmileGenerator.writeNumber(String) (#781) `_streamReadConstraints()` returned `StreamReadConstraints.defaults()`, so a `maxNumberLength` configured on the `SmileFactory` was ignored by the number-length guard in `writeNumber(String)`. Use the factory's constraints via `IOContext` instead. Co-Authored-By: Claude Opus 5 (1M context) --- release-notes/CREDITS | 3 ++ release-notes/VERSION | 3 ++ .../dataformat/smile/SmileGenerator.java | 8 ++-- .../smile/gen/SmileGeneratorNumbersTest.java | 43 +++++++++++++++++++ 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/release-notes/CREDITS b/release-notes/CREDITS index 6f5d21a88..8e8f4b199 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -76,3 +76,6 @@ PJ Fanning (@pjfanning) * Contributed #763: (protobuf) Use `VarHandle` for multi-byte primitive reads and writes in `ProtobufParser` / `ProtobufGenerator` (3.3.0) +* Contributed #781: (smile) `SmileGenerator.writeNumber(String)` should validate + number length against configured `StreamReadConstraints`, not global defaults + (3.3.0) diff --git a/release-notes/VERSION b/release-notes/VERSION index 34c5dfd62..d498ab9cc 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -38,6 +38,9 @@ implementations) #767: (smile) Use more efficient `String` construction wrt "Compact Strings" for "short" ASCII text values of async parser (fix by @cowtowncoder, w/ Claude code) +#781: (smile) `SmileGenerator.writeNumber(String)` should validate number length + against configured `StreamReadConstraints`, not global defaults + (contributed by @pjfanning) 3.2.3 (not yet released) diff --git a/smile/src/main/java/tools/jackson/dataformat/smile/SmileGenerator.java b/smile/src/main/java/tools/jackson/dataformat/smile/SmileGenerator.java index fce5d2b4e..ca5361f09 100644 --- a/smile/src/main/java/tools/jackson/dataformat/smile/SmileGenerator.java +++ b/smile/src/main/java/tools/jackson/dataformat/smile/SmileGenerator.java @@ -2761,12 +2761,10 @@ 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(); } } diff --git a/smile/src/test/java/tools/jackson/dataformat/smile/gen/SmileGeneratorNumbersTest.java b/smile/src/test/java/tools/jackson/dataformat/smile/gen/SmileGeneratorNumbersTest.java index 2b27bd1ee..e0b28b171 100644 --- a/smile/src/test/java/tools/jackson/dataformat/smile/gen/SmileGeneratorNumbersTest.java +++ b/smile/src/test/java/tools/jackson/dataformat/smile/gen/SmileGeneratorNumbersTest.java @@ -1,14 +1,21 @@ package tools.jackson.dataformat.smile.gen; import java.io.ByteArrayOutputStream; +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 @@ -203,6 +210,42 @@ 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); + final String tooLongDec = "1." + "1".repeat(maxLen - 1); + + for (String num : new String[] { tooLongInt, 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()); + } + } + // [dataformats-binary#608] @Test public void testFloat32FromSpecEncoding() throws Exception From 3f4e5aabe1c20f7f35381062aa1bc52832085eef Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 18 Sep 2026 09:33:15 -0700 Subject: [PATCH 2/2] Minor tweaking --- release-notes/VERSION | 6 +- .../dataformat/smile/SmileGenerator.java | 21 ++++++- .../smile/gen/SmileGeneratorNumbersTest.java | 63 ++++++++++++++++++- 3 files changed, 82 insertions(+), 8 deletions(-) diff --git a/release-notes/VERSION b/release-notes/VERSION index dc6ee5bfe..5fdf7e742 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -41,14 +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) +#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` -#780: (ion) `IonFactory` leaks stream when parser/generator construction fails - for `File`/`Path` - (contributed by @Dongnyoung) 3.2.3 (not yet released) diff --git a/smile/src/main/java/tools/jackson/dataformat/smile/SmileGenerator.java b/smile/src/main/java/tools/jackson/dataformat/smile/SmileGenerator.java index 97dd08064..b3e93b90d 100644 --- a/smile/src/main/java/tools/jackson/dataformat/smile/SmileGenerator.java +++ b/smile/src/main/java/tools/jackson/dataformat/smile/SmileGenerator.java @@ -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) { @@ -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 { @@ -2777,4 +2781,15 @@ protected UnsupportedOperationException _notSupported() { protected StreamReadConstraints _streamReadConstraints() { 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; + } } diff --git a/smile/src/test/java/tools/jackson/dataformat/smile/gen/SmileGeneratorNumbersTest.java b/smile/src/test/java/tools/jackson/dataformat/smile/gen/SmileGeneratorNumbersTest.java index e0b28b171..faeee4613 100644 --- a/smile/src/test/java/tools/jackson/dataformat/smile/gen/SmileGeneratorNumbersTest.java +++ b/smile/src/test/java/tools/jackson/dataformat/smile/gen/SmileGeneratorNumbersTest.java @@ -1,6 +1,7 @@ package tools.jackson.dataformat.smile.gen; import java.io.ByteArrayOutputStream; +import java.math.BigDecimal; import java.math.BigInteger; import java.util.Arrays; @@ -221,9 +222,11 @@ public void testNumbersAsStringLengthLimit() throws Exception .maxNumberLength(maxLen).build()) .build(); final String tooLongInt = "1".repeat(maxLen + 1); - final String tooLongDec = "1." + "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, tooLongDec }) { + for (String num : new String[] { tooLongInt, tooLongNegInt, tooLongDec }) { ByteArrayOutputStream out = new ByteArrayOutputStream(); try (SmileGenerator gen = (SmileGenerator) f.createGenerator(out)) { gen.writeNumber(num); @@ -244,6 +247,62 @@ public void testNumbersAsStringLengthLimit() throws Exception 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]