Skip to content

Commit 522a0c8

Browse files
committed
Merge branch '2.x' into 3.x
2 parents d30ed95 + f5f6889 commit 522a0c8

File tree

4 files changed

+151
-22
lines changed

4 files changed

+151
-22
lines changed

release-notes/VERSION-2.x

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ No changes since 2.19.1
108108
(reported by @ventusfortis)
109109
#1548: `StreamReadConstraints.maxDocumentLength` not checked when
110110
creating parser with fixed buffer
111+
#1555: Enforce `StreamReadConstraints.maxNumberLength` for
112+
non-blocking (async) parser
113+
(fix by @pjfanning)
111114

112115
2.18.5 (27-Oct-2025)
113116
(same as 2.18.4.1 on 10-June-2025)

src/main/java/tools/jackson/core/json/async/NonBlockingUtf8JsonParserBase.java

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
import tools.jackson.core.JacksonException;
44
import tools.jackson.core.JsonToken;
55
import tools.jackson.core.ObjectReadContext;
6+
import tools.jackson.core.exc.StreamConstraintsException;
67
import tools.jackson.core.io.CharTypes;
78
import tools.jackson.core.io.IOContext;
89
import tools.jackson.core.json.JsonReadFeature;
910
import tools.jackson.core.sym.ByteQuadsCanonicalizer;
1011
import tools.jackson.core.util.InternalJacksonUtil;
1112
import tools.jackson.core.util.VersionUtil;
1213

14+
1315
/**
1416
* Non-blocking parser base implementation for JSON content.
1517
*<p>
@@ -363,7 +365,7 @@ protected final JsonToken _finishTokenWithEOF() throws JacksonException
363365
if (_numberNegative) {
364366
--len;
365367
}
366-
_intLength = len;
368+
_setIntLength(len);
367369
}
368370
return _valueComplete(JsonToken.VALUE_NUMBER_INT);
369371

@@ -1301,15 +1303,15 @@ protected JsonToken _startPositiveNumber(int ch) throws JacksonException
13011303
while (true) {
13021304
if (ch < INT_0) {
13031305
if (ch == INT_PERIOD) {
1304-
_intLength = outPtr;
1306+
_setIntLength(outPtr);
13051307
++_inputPtr;
13061308
return _startFloat(outBuf, outPtr, ch);
13071309
}
13081310
break;
13091311
}
13101312
if (ch > INT_9) {
13111313
if ((ch | 0x20) == INT_e) { // ~ 'eE'
1312-
_intLength = outPtr;
1314+
_setIntLength(outPtr);
13131315
++_inputPtr;
13141316
return _startFloat(outBuf, outPtr, ch);
13151317
}
@@ -1328,7 +1330,7 @@ protected JsonToken _startPositiveNumber(int ch) throws JacksonException
13281330
}
13291331
ch = getByteFromBuffer(_inputPtr) & 0xFF;
13301332
}
1331-
_intLength = outPtr;
1333+
_setIntLength(outPtr);
13321334
_textBuffer.setCurrentLength(outPtr);
13331335
return _valueComplete(JsonToken.VALUE_NUMBER_INT);
13341336
}
@@ -1368,15 +1370,15 @@ protected JsonToken _startNegativeNumber() throws JacksonException
13681370
while (true) {
13691371
if (ch < INT_0) {
13701372
if (ch == INT_PERIOD) {
1371-
_intLength = outPtr-1;
1373+
_setIntLength(outPtr-1);
13721374
++_inputPtr;
13731375
return _startFloat(outBuf, outPtr, ch);
13741376
}
13751377
break;
13761378
}
13771379
if (ch > INT_9) {
13781380
if ((ch | 0x20) == INT_e) { // ~ 'eE'
1379-
_intLength = outPtr-1;
1381+
_setIntLength(outPtr-1);
13801382
++_inputPtr;
13811383
return _startFloat(outBuf, outPtr, ch);
13821384
}
@@ -1394,7 +1396,7 @@ protected JsonToken _startNegativeNumber() throws JacksonException
13941396
}
13951397
ch = getByteFromBuffer(_inputPtr) & 0xFF;
13961398
}
1397-
_intLength = outPtr-1;
1399+
_setIntLength(outPtr-1);
13981400
_textBuffer.setCurrentLength(outPtr);
13991401
return _valueComplete(JsonToken.VALUE_NUMBER_INT);
14001402
}
@@ -1440,15 +1442,15 @@ protected JsonToken _startPositiveNumber() throws JacksonException
14401442
while (true) {
14411443
if (ch < INT_0) {
14421444
if (ch == INT_PERIOD) {
1443-
_intLength = outPtr-1;
1445+
_setIntLength(outPtr-1);
14441446
++_inputPtr;
14451447
return _startFloat(outBuf, outPtr, ch);
14461448
}
14471449
break;
14481450
}
14491451
if (ch > INT_9) {
14501452
if ((ch | 0x20) == INT_e) { // ~ 'eE'
1451-
_intLength = outPtr-1;
1453+
_setIntLength(outPtr-1);
14521454
++_inputPtr;
14531455
return _startFloat(outBuf, outPtr, ch);
14541456
}
@@ -1466,7 +1468,7 @@ protected JsonToken _startPositiveNumber() throws JacksonException
14661468
}
14671469
ch = getByteFromBuffer(_inputPtr) & 0xFF;
14681470
}
1469-
_intLength = outPtr-1;
1471+
_setIntLength(outPtr-1);
14701472
_textBuffer.setCurrentLength(outPtr);
14711473
return _valueComplete(JsonToken.VALUE_NUMBER_INT);
14721474
}
@@ -1699,15 +1701,15 @@ protected JsonToken _finishNumberIntegralPart(char[] outBuf, int outPtr) throws
16991701
ch = getByteFromBuffer(_inputPtr) & 0xFF;
17001702
if (ch < INT_0) {
17011703
if (ch == INT_PERIOD) {
1702-
_intLength = outPtr+negMod;
1704+
_setIntLength(outPtr+negMod);
17031705
++_inputPtr;
17041706
return _startFloat(outBuf, outPtr, ch);
17051707
}
17061708
break;
17071709
}
17081710
if (ch > INT_9) {
17091711
if ((ch | 0x20) == INT_e) { // ~ 'eE'
1710-
_intLength = outPtr+negMod;
1712+
_setIntLength(outPtr+negMod);
17111713
++_inputPtr;
17121714
return _startFloat(outBuf, outPtr, ch);
17131715
}
@@ -1721,7 +1723,7 @@ protected JsonToken _finishNumberIntegralPart(char[] outBuf, int outPtr) throws
17211723
}
17221724
outBuf[outPtr++] = (char) ch;
17231725
}
1724-
_intLength = outPtr+negMod;
1726+
_setIntLength(outPtr+negMod);
17251727
_textBuffer.setCurrentLength(outPtr);
17261728
// As per #105, need separating space between root values; check here
17271729
if (_streamReadContext.inRoot()) {
@@ -1742,7 +1744,7 @@ protected JsonToken _startFloat(char[] outBuf, int outPtr, int ch) throws Jackso
17421744
if (_inputPtr >= _inputEnd) {
17431745
_textBuffer.setCurrentLength(outPtr);
17441746
_minorState = MINOR_NUMBER_FRACTION_DIGITS;
1745-
_fractLength = fractLen;
1747+
_setFractLength(fractLen);
17461748
return _updateTokenToNA();
17471749
}
17481750
ch = getNextSignedByteFromBuffer(); // ok to have sign extension for now
@@ -1763,7 +1765,7 @@ protected JsonToken _startFloat(char[] outBuf, int outPtr, int ch) throws Jackso
17631765
++fractLen;
17641766
}
17651767
}
1766-
_fractLength = fractLen;
1768+
_setFractLength(fractLen);
17671769
int expLen = 0;
17681770
if ((ch | 0x20) == INT_e) { // ~ 'eE' exponent?
17691771
if (outPtr >= outBuf.length) {
@@ -1799,7 +1801,7 @@ protected JsonToken _startFloat(char[] outBuf, int outPtr, int ch) throws Jackso
17991801
if (_inputPtr >= _inputEnd) {
18001802
_textBuffer.setCurrentLength(outPtr);
18011803
_minorState = MINOR_NUMBER_EXPONENT_DIGITS;
1802-
_expLength = expLen;
1804+
_setExpLength(expLen);
18031805
return _updateTokenToNA();
18041806
}
18051807
ch = getNextSignedByteFromBuffer();
@@ -1814,11 +1816,11 @@ protected JsonToken _startFloat(char[] outBuf, int outPtr, int ch) throws Jackso
18141816
--_inputPtr;
18151817
_textBuffer.setCurrentLength(outPtr);
18161818
// negative, int-length, fract-length already set, so...
1817-
_expLength = expLen;
18181819
// As per #105, need separating space between root values; check here
18191820
if (_streamReadContext.inRoot()) {
18201821
_verifyRootSpace(ch);
18211822
}
1823+
_setExpLength(expLen);
18221824
return _valueComplete(JsonToken.VALUE_NUMBER_FLOAT);
18231825
}
18241826

@@ -1840,7 +1842,7 @@ protected JsonToken _finishFloatFraction() throws JacksonException
18401842
outBuf[outPtr++] = (char) ch;
18411843
if (_inputPtr >= _inputEnd) {
18421844
_textBuffer.setCurrentLength(outPtr);
1843-
_fractLength = fractLen;
1845+
_setFractLength(fractLen);
18441846
return JsonToken.NOT_AVAILABLE;
18451847
}
18461848
ch = getNextSignedByteFromBuffer();
@@ -1866,7 +1868,7 @@ protected JsonToken _finishFloatFraction() throws JacksonException
18661868
_reportUnexpectedNumberChar(ch, "Decimal point not followed by a digit");
18671869
}
18681870
}
1869-
_fractLength = fractLen;
1871+
_setFractLength(fractLen);
18701872
_textBuffer.setCurrentLength(outPtr);
18711873

18721874
// Ok: end of floating point number or exponent?
@@ -1920,7 +1922,7 @@ protected JsonToken _finishFloatExponent(boolean checkSign, int ch) throws Jacks
19201922
outBuf[outPtr++] = (char) ch;
19211923
if (_inputPtr >= _inputEnd) {
19221924
_textBuffer.setCurrentLength(outPtr);
1923-
_expLength = expLen;
1925+
_setExpLength(expLen);
19241926
return JsonToken.NOT_AVAILABLE;
19251927
}
19261928
ch = getNextSignedByteFromBuffer();
@@ -1934,11 +1936,11 @@ protected JsonToken _finishFloatExponent(boolean checkSign, int ch) throws Jacks
19341936
--_inputPtr;
19351937
_textBuffer.setCurrentLength(outPtr);
19361938
// negative, int-length, fract-length already set, so...
1937-
_expLength = expLen;
19381939
// As per #105, need separating space between root values; check here
19391940
if (_streamReadContext.inRoot()) {
19401941
_verifyRootSpace(ch);
19411942
}
1943+
_setExpLength(expLen);
19421944
return _valueComplete(JsonToken.VALUE_NUMBER_FLOAT);
19431945
}
19441946

@@ -3214,4 +3216,21 @@ private final void _verifyRootSpace(int ch) throws JacksonException
32143216
/* Internal methods, other
32153217
/**********************************************************************
32163218
*/
3219+
3220+
private void _setIntLength(final int len) throws StreamConstraintsException {
3221+
_streamReadConstraints.validateIntegerLength(len);
3222+
_intLength = len;
3223+
}
3224+
3225+
private void _setFractLength(final int len) throws StreamConstraintsException {
3226+
// assumes that the _intLength has been updated first
3227+
_streamReadConstraints.validateFPLength(_intLength + len);
3228+
_fractLength = len;
3229+
}
3230+
3231+
private void _setExpLength(final int len) throws StreamConstraintsException {
3232+
// assumes that the _intLength and _fractLength have been updated already
3233+
_streamReadConstraints.validateFPLength(_intLength + _fractLength + len);
3234+
_expLength = len;
3235+
}
32173236
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package tools.jackson.core.unittest.constraints;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import tools.jackson.core.*;
6+
import tools.jackson.core.async.ByteArrayFeeder;
7+
import tools.jackson.core.exc.StreamConstraintsException;
8+
import tools.jackson.core.json.JsonFactory;
9+
import tools.jackson.core.unittest.JacksonCoreTestBase;
10+
11+
import static org.junit.jupiter.api.Assertions.*;
12+
13+
/**
14+
* Number Length Constraint Bypass in Non-Blocking (Async) JSON Parsers
15+
*/
16+
class AsyncLargeNumberReadTest
17+
extends JacksonCoreTestBase
18+
{
19+
private static final int TEST_NUMBER_LENGTH = StreamReadConstraints.DEFAULT_MAX_NUM_LEN * 2;
20+
21+
private final JsonFactory JSON_F = newStreamFactory();
22+
23+
@Test
24+
void asyncParserFailsTooLongInt() throws Exception {
25+
byte[] payload = buildPayloadWithLongInteger(TEST_NUMBER_LENGTH);
26+
27+
try (JsonParser p = JSON_F.createNonBlockingByteArrayParser(ObjectReadContext.empty())) {
28+
ByteArrayFeeder byteArrayFeeder = (ByteArrayFeeder) p;
29+
byteArrayFeeder.feedInput(payload, 0, payload.length);
30+
byteArrayFeeder.endOfInput();
31+
32+
_asyncParserFailsTooLongNumber(p, JsonToken.VALUE_NUMBER_INT);
33+
}
34+
}
35+
36+
@Test
37+
void asyncParserFailsTooLongDecimal() throws Exception {
38+
byte[] payload = buildPayloadWithLongDecimal(TEST_NUMBER_LENGTH);
39+
40+
try (JsonParser p = JSON_F.createNonBlockingByteArrayParser(ObjectReadContext.empty())) {
41+
ByteArrayFeeder byteArrayFeeder = (ByteArrayFeeder) p;
42+
byteArrayFeeder.feedInput(payload, 0, payload.length);
43+
byteArrayFeeder.endOfInput();
44+
45+
_asyncParserFailsTooLongNumber(p, JsonToken.VALUE_NUMBER_FLOAT);
46+
}
47+
}
48+
49+
@Test
50+
void asyncParserFailsTooLongDecimalWithExponent() throws Exception {
51+
byte[] payload = buildPayloadWithLongExponent(TEST_NUMBER_LENGTH);
52+
53+
try (JsonParser p = JSON_F.createNonBlockingByteArrayParser(ObjectReadContext.empty())) {
54+
ByteArrayFeeder byteArrayFeeder = (ByteArrayFeeder) p;
55+
byteArrayFeeder.feedInput(payload, 0, payload.length);
56+
byteArrayFeeder.endOfInput();
57+
58+
_asyncParserFailsTooLongNumber(p, JsonToken.VALUE_NUMBER_FLOAT);
59+
}
60+
}
61+
62+
private void _asyncParserFailsTooLongNumber(JsonParser p, JsonToken tokenMatch) throws Exception {
63+
boolean foundNumber = false;
64+
try {
65+
while (p.nextToken() != null) {
66+
if (p.currentToken() == tokenMatch) {
67+
foundNumber = true;
68+
String numberText = p.getString();
69+
assertEquals(TEST_NUMBER_LENGTH, numberText.length(),
70+
"Async parser silently accepted all " + TEST_NUMBER_LENGTH + " digits");
71+
}
72+
}
73+
fail("Async parser must reject a " + TEST_NUMBER_LENGTH + "-digit number (number found? "+foundNumber+")");
74+
} catch (StreamConstraintsException e) {
75+
verifyException(e, "Number value length (");
76+
verifyException(e, " exceeds the maximum allowed");
77+
}
78+
}
79+
80+
private byte[] buildPayloadWithLongInteger(int numDigits) {
81+
StringBuilder sb = new StringBuilder(numDigits + 10);
82+
sb.append("{\"v\":");
83+
for (int i = 0; i < numDigits; i++) {
84+
sb.append((char) ('1' + (i % 9)));
85+
}
86+
sb.append('}');
87+
return utf8Bytes(sb.toString());
88+
}
89+
90+
private byte[] buildPayloadWithLongDecimal(int numDigits) {
91+
StringBuilder sb = new StringBuilder(numDigits + 10);
92+
sb.append("{\"v\":0.");
93+
for (int i = 0; i < numDigits; i++) {
94+
sb.append((char) ('1' + (i % 9)));
95+
}
96+
sb.append('}');
97+
return utf8Bytes(sb.toString());
98+
}
99+
100+
private byte[] buildPayloadWithLongExponent(int numDigits) {
101+
StringBuilder sb = new StringBuilder(numDigits + 10);
102+
sb.append("{\"v\":1.1E");
103+
for (int i = 0; i < numDigits; i++) {
104+
sb.append((char) ('1' + (i % 9)));
105+
}
106+
return utf8Bytes(sb.toString());
107+
}
108+
}

src/test/java/tools/jackson/core/unittest/constraints/LargeNumberReadTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
* Set of basic unit tests for verifying that "too big" number constraints
1818
* are caught by various JSON parser backends.
1919
*/
20-
2120
@SuppressWarnings("resource")
2221
class LargeNumberReadTest
2322
extends tools.jackson.core.unittest.JacksonCoreTestBase

0 commit comments

Comments
 (0)