Skip to content

Commit 1ae30da

Browse files
committed
Fix #1557: eagerly validate separator after non-root number values
Number values inside Arrays/Objects were only checked for a valid trailing separator at root level (cf. #105 / #679). Non-root values pushed the trailing character back unchecked, so malformed input such as `[ 123true ]`, `[ 100k ]` or `[ 1.5x ]` reported a number token and only failed lazily when accessing the *following* token. Generalize the existing root-only `_verifyRootSpace` into `_verifyNumberSeparator`, applied at every number-completion site in the three blocking parsers (Reader / UTF-8 stream / UTF-8 DataInput). A non-root number must now be followed by white space, `,`, `]`, `}`, a comment start (when comments are enabled) or end-of-input; otherwise an "expected space..." error is reported at the offending character. The check reuses the trailing character already read to detect the end of the number and is a constant-time switch, so the hot path cost is negligible. Tests: add non-root verification (and comment-adjacent cases) to `NonStandardNumberParsingTest` across ALL_MODES; remove the now-fixed `tofix` repros for the blocking parsers. Async (non-blocking) parser is intentionally left as a follow-up; its `tofix` repro remains under `@JacksonTestFailureExpected`.
1 parent c7d5423 commit 1ae30da

10 files changed

Lines changed: 230 additions & 324 deletions

release-notes/VERSION

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ JSON library.
2828
#1544: Validate `read()` parameters in `MergedStream` and `UTF32Reader`
2929
(implemented by @pjfanning)
3030
#1545: Increase Android baseline from 26 to 34 in Jackson 3.2
31+
#1557: Parsing for non-root number values fails lazily
32+
(fix for blocking parsers by @seonwooj0810)
3133
#1564: Add `SimpleStreamReadContext.rollbackValueRead()` method
3234
#1575: Implement document length tracking for `DataInput`-backed
3335
JSON parsers via subclass

src/main/java/tools/jackson/core/json/ReaderBasedJsonParser.java

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,10 +1413,8 @@ protected final JsonToken _parseUnsignedNumber(int ch) throws JacksonException
14131413
// Got it all: let's add to text buffer for parsing, access
14141414
--ptr; // need to push back following separator
14151415
_inputPtr = ptr;
1416-
// As per #105, need separating space between root values; check here
1417-
if (_streamReadContext.inRoot()) {
1418-
_verifyRootSpace(ch);
1419-
}
1416+
// [core#105]/[core#1557]: verify number is properly terminated/separated
1417+
_verifyNumberSeparator(ch);
14201418
int len = ptr-startPtr;
14211419
_textBuffer.resetWithShared(_inputBuffer, startPtr, len);
14221420
return resetInt(false, intLen);
@@ -1480,10 +1478,8 @@ private final JsonToken _parseFloat(int ch, int startPtr, int ptr, boolean neg,
14801478
}
14811479
--ptr; // need to push back following separator
14821480
_inputPtr = ptr;
1483-
// As per #105, need separating space between root values; check here
1484-
if (_streamReadContext.inRoot()) {
1485-
_verifyRootSpace(ch);
1486-
}
1481+
// [core#105]/[core#1557]: verify number is properly terminated/separated
1482+
_verifyNumberSeparator(ch);
14871483
int len = ptr-startPtr;
14881484
_textBuffer.resetWithShared(_inputBuffer, startPtr, len);
14891485
// And there we have it!
@@ -1538,9 +1534,7 @@ private final JsonToken _parseSignedNumber(final boolean negative) throws Jackso
15381534
}
15391535
--ptr;
15401536
_inputPtr = ptr;
1541-
if (_streamReadContext.inRoot()) {
1542-
_verifyRootSpace(ch);
1543-
}
1537+
_verifyNumberSeparator(ch);
15441538
int len = ptr-startPtr;
15451539
_textBuffer.resetWithShared(_inputBuffer, startPtr, len);
15461540
return resetInt(negative, intLen);
@@ -1704,9 +1698,7 @@ private final JsonToken _parseNumber2(boolean neg, int startPtr) throws JacksonE
17041698
// Ok; unless we hit end-of-input, need to push last char read back
17051699
if (!eof) {
17061700
--_inputPtr;
1707-
if (_streamReadContext.inRoot()) {
1708-
_verifyRootSpace(c);
1709-
}
1701+
_verifyNumberSeparator(c);
17101702
}
17111703
_textBuffer.setCurrentLength(outPtr);
17121704

@@ -1773,9 +1765,7 @@ private final JsonToken _finishHexNumber(boolean neg,
17731765

17741766
if (!eof) {
17751767
--_inputPtr; // push back the terminating non-hex char
1776-
if (_streamReadContext.inRoot()) {
1777-
_verifyRootSpace(c);
1778-
}
1768+
_verifyNumberSeparator(c);
17791769
}
17801770
_textBuffer.setCurrentLength(outPtr);
17811771
return resetIntHex(neg, hexLen);
@@ -1907,6 +1897,54 @@ private final void _verifyRootSpace(int ch) throws JacksonException
19071897
_reportMissingRootWS(ch);
19081898
}
19091899

1900+
/**
1901+
* Method called to verify that a just-decoded number value is followed by a
1902+
* valid separator or terminator character. For root-level values this means
1903+
* white space (as per [core#105], see {@link #_verifyRootSpace}); for non-root
1904+
* values ([core#1557]) the number must be followed by white space, a value
1905+
* separator ({@code ','}), an enclosing-structure end ({@code ']'} or
1906+
* {@code '}'}), a comment start (when enabled) or end-of-input. Without this,
1907+
* malformed content such as {@code [ 123true ]} would only fail lazily when
1908+
* accessing the following token.
1909+
*<p>
1910+
* On entry the caller has pushed the trailing character back, so {@code _inputPtr}
1911+
* points <i>at</i> it; for accepted separators this method leaves {@code _inputPtr}
1912+
* untouched so the next {@code nextToken()} call can consume them normally.
1913+
*/
1914+
private final void _verifyNumberSeparator(int ch) throws JacksonException
1915+
{
1916+
if (_streamReadContext.inRoot()) {
1917+
_verifyRootSpace(ch);
1918+
return;
1919+
}
1920+
switch (ch) {
1921+
case ' ':
1922+
case '\t':
1923+
case '\n':
1924+
case '\r':
1925+
case ',':
1926+
case ']':
1927+
case '}':
1928+
return;
1929+
case '/': // possible Java/C++ style comment
1930+
if (isEnabled(JsonReadFeature.ALLOW_JAVA_COMMENTS)) {
1931+
return;
1932+
}
1933+
break;
1934+
case '#': // possible YAML/shell style comment
1935+
if (isEnabled(JsonReadFeature.ALLOW_YAML_COMMENTS)) {
1936+
return;
1937+
}
1938+
break;
1939+
}
1940+
// Align `_inputPtr` with what `_reportUnexpectedNumberChar` ->
1941+
// `_currentLocationMinusOne()` expects (one past the offending char),
1942+
// matching `_verifyRootSpace` which advances up front.
1943+
++_inputPtr;
1944+
_reportUnexpectedNumberChar(ch,
1945+
"expected space, comma, or closing bracket/brace to separate or terminate numeric value");
1946+
}
1947+
19101948
/*
19111949
/**********************************************************************
19121950
/* Internal methods, secondary parsing

src/main/java/tools/jackson/core/json/UTF8DataInputJsonParser.java

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,11 +1092,9 @@ protected JsonToken _parseUnsignedNumber(int c) throws IOException
10921092
return _parseFloat(outBuf, outPtr, c, false, intLen);
10931093
}
10941094
_textBuffer.setCurrentLength(outPtr);
1095-
// As per [core#105], need separating space between root values; check here
1095+
// [core#105]/[core#1557]: verify number is properly terminated/separated
10961096
_nextByte = c;
1097-
if (_streamReadContext.inRoot()) {
1098-
_verifyRootSpace();
1099-
}
1097+
_verifyNumberSeparator();
11001098
// And there we have it!
11011099
return resetInt(false, intLen);
11021100
}
@@ -1160,11 +1158,9 @@ private final JsonToken _parseSignedNumber(boolean negative) throws IOException
11601158
return _parseFloat(outBuf, outPtr, c, negative, intLen);
11611159
}
11621160
_textBuffer.setCurrentLength(outPtr);
1163-
// As per [core#105], need separating space between root values; check here
1161+
// [core#105]/[core#1557]: verify number is properly terminated/separated
11641162
_nextByte = c;
1165-
if (_streamReadContext.inRoot()) {
1166-
_verifyRootSpace();
1167-
}
1163+
_verifyNumberSeparator();
11681164
// And there we have it!
11691165
return resetInt(negative, intLen);
11701166
}
@@ -1209,9 +1205,7 @@ private final JsonToken _finishHexNumber(boolean neg, char[] outBuf, int outPtr,
12091205
}
12101206
_textBuffer.setCurrentLength(outPtr);
12111207
_nextByte = c;
1212-
if (_streamReadContext.inRoot()) {
1213-
_verifyRootSpace();
1214-
}
1208+
_verifyNumberSeparator();
12151209
return resetIntHex(neg, hexLen);
12161210
}
12171211

@@ -1313,11 +1307,9 @@ private final JsonToken _parseFloat(char[] outBuf, int outPtr, int c,
13131307
}
13141308

13151309
// Ok; unless we hit end-of-input, need to push last char read back
1316-
// As per #105, need separating space between root values; check here
1310+
// [core#105]/[core#1557]: verify number is properly terminated/separated
13171311
_nextByte = c;
1318-
if (_streamReadContext.inRoot()) {
1319-
_verifyRootSpace();
1320-
}
1312+
_verifyNumberSeparator();
13211313
_textBuffer.setCurrentLength(outPtr);
13221314

13231315
// And there we have it!
@@ -1345,6 +1337,53 @@ private final void _verifyRootSpace() throws JacksonException
13451337
_reportMissingRootWS(ch);
13461338
}
13471339

1340+
/**
1341+
* Method called to verify that a just-decoded number value is followed by a
1342+
* valid separator or terminator. For root-level values this means white space
1343+
* (as per [core#105], see {@link #_verifyRootSpace}); for non-root values
1344+
* ([core#1557]) the number must be followed by white space, a value separator
1345+
* ({@code ','}), an enclosing-structure end ({@code ']'} or {@code '}'}), a
1346+
* comment start (when enabled) or end-of-input. Without this, malformed content
1347+
* such as {@code [ 123true ]} would only fail lazily when accessing the
1348+
* following token.
1349+
*<p>
1350+
* The trailing character is held in {@code _nextByte}; for accepted separators
1351+
* it is left there so the next {@code nextToken()} call can consume it normally.
1352+
*/
1353+
private final void _verifyNumberSeparator() throws JacksonException
1354+
{
1355+
if (_streamReadContext.inRoot()) {
1356+
_verifyRootSpace();
1357+
return;
1358+
}
1359+
final int ch = _nextByte;
1360+
if (ch < 0) { // end-of-input: number itself is complete
1361+
return;
1362+
}
1363+
switch (ch) {
1364+
case ' ':
1365+
case '\t':
1366+
case '\n':
1367+
case '\r':
1368+
case ',':
1369+
case ']':
1370+
case '}':
1371+
return;
1372+
case '/': // possible Java/C++ style comment
1373+
if (isEnabled(JsonReadFeature.ALLOW_JAVA_COMMENTS)) {
1374+
return;
1375+
}
1376+
break;
1377+
case '#': // possible YAML/shell style comment
1378+
if (isEnabled(JsonReadFeature.ALLOW_YAML_COMMENTS)) {
1379+
return;
1380+
}
1381+
break;
1382+
}
1383+
_reportUnexpectedNumberChar(ch,
1384+
"expected space, comma, or closing bracket/brace to separate or terminate numeric value");
1385+
}
1386+
13481387
/*
13491388
/**********************************************************************
13501389
/* Internal methods, secondary parsing

src/main/java/tools/jackson/core/json/UTF8StreamJsonParser.java

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1856,10 +1856,8 @@ protected JsonToken _parseUnsignedNumber(int c) throws JacksonException
18561856
}
18571857
--_inputPtr; // to push back trailing char (comma etc)
18581858
_textBuffer.setCurrentLength(outPtr);
1859-
// As per #105, need separating space between root values; check here
1860-
if (_streamReadContext.inRoot()) {
1861-
_verifyRootSpace(c);
1862-
}
1859+
// [core#105]/[core#1557]: verify number is properly terminated/separated
1860+
_verifyNumberSeparator(c);
18631861
// And there we have it!
18641862
return resetInt(false, intLen);
18651863
}
@@ -1925,10 +1923,8 @@ private final JsonToken _parseSignedNumber(boolean negative) throws JacksonExcep
19251923

19261924
--_inputPtr; // to push back trailing char (comma etc)
19271925
_textBuffer.setCurrentLength(outPtr);
1928-
// As per #105, need separating space between root values; check here
1929-
if (_streamReadContext.inRoot()) {
1930-
_verifyRootSpace(c);
1931-
}
1926+
// [core#105]/[core#1557]: verify number is properly terminated/separated
1927+
_verifyNumberSeparator(c);
19321928

19331929
// And there we have it!
19341930
return resetInt(negative, intLen);
@@ -1961,10 +1957,8 @@ private final JsonToken _parseNumber2(char[] outBuf, int outPtr, boolean negativ
19611957
}
19621958
--_inputPtr; // to push back trailing char (comma etc)
19631959
_textBuffer.setCurrentLength(outPtr);
1964-
// As per #105, need separating space between root values; check here
1965-
if (_streamReadContext.inRoot()) {
1966-
_verifyRootSpace(_inputBuffer[_inputPtr] & 0xFF);
1967-
}
1960+
// [core#105]/[core#1557]: verify number is properly terminated/separated
1961+
_verifyNumberSeparator(_inputBuffer[_inputPtr] & 0xFF);
19681962

19691963
// And there we have it!
19701964
return resetInt(negative, intPartLength);
@@ -2025,9 +2019,7 @@ private final JsonToken _finishHexNumber(boolean neg, char[] outBuf, int outPtr,
20252019

20262020
if (!eof) {
20272021
--_inputPtr; // push back the terminating non-hex char
2028-
if (_streamReadContext.inRoot()) {
2029-
_verifyRootSpace(c);
2030-
}
2022+
_verifyNumberSeparator(c);
20312023
}
20322024
_textBuffer.setCurrentLength(outPtr);
20332025
return resetIntHex(neg, hexLen);
@@ -2157,10 +2149,8 @@ private final JsonToken _parseFloat(char[] outBuf, int outPtr, int c,
21572149
// Ok; unless we hit end-of-input, need to push last char read back
21582150
if (!eof) {
21592151
--_inputPtr;
2160-
// As per [core#105], need separating space between root values; check here
2161-
if (_streamReadContext.inRoot()) {
2162-
_verifyRootSpace(c);
2163-
}
2152+
// [core#105]/[core#1557]: verify number is properly terminated/separated
2153+
_verifyNumberSeparator(c);
21642154
}
21652155
_textBuffer.setCurrentLength(outPtr);
21662156

@@ -2203,6 +2193,54 @@ private final void _verifyRootSpace(int ch) throws JacksonException
22032193
_reportMissingRootWS(ch);
22042194
}
22052195

2196+
/**
2197+
* Method called to verify that a just-decoded number value is followed by a
2198+
* valid separator or terminator character. For root-level values this means
2199+
* white space (as per [core#105], see {@link #_verifyRootSpace}); for non-root
2200+
* values ([core#1557]) the number must be followed by white space, a value
2201+
* separator ({@code ','}), an enclosing-structure end ({@code ']'} or
2202+
* {@code '}'}), a comment start (when enabled) or end-of-input. Without this,
2203+
* malformed content such as {@code [ 123true ]} would only fail lazily when
2204+
* accessing the following token.
2205+
*<p>
2206+
* On entry the caller has pushed the trailing character back, so {@code _inputPtr}
2207+
* points <i>at</i> it; for accepted separators this method leaves {@code _inputPtr}
2208+
* untouched so the next {@code nextToken()} call can consume them normally.
2209+
*/
2210+
private final void _verifyNumberSeparator(int ch) throws JacksonException
2211+
{
2212+
if (_streamReadContext.inRoot()) {
2213+
_verifyRootSpace(ch);
2214+
return;
2215+
}
2216+
switch (ch) {
2217+
case ' ':
2218+
case '\t':
2219+
case '\n':
2220+
case '\r':
2221+
case ',':
2222+
case ']':
2223+
case '}':
2224+
return;
2225+
case '/': // possible Java/C++ style comment
2226+
if (isEnabled(JsonReadFeature.ALLOW_JAVA_COMMENTS)) {
2227+
return;
2228+
}
2229+
break;
2230+
case '#': // possible YAML/shell style comment
2231+
if (isEnabled(JsonReadFeature.ALLOW_YAML_COMMENTS)) {
2232+
return;
2233+
}
2234+
break;
2235+
}
2236+
// Align `_inputPtr` with what `_reportUnexpectedNumberChar` ->
2237+
// `_currentLocationMinusOne()` expects (one past the offending char),
2238+
// matching `_verifyRootSpace` which advances up front.
2239+
++_inputPtr;
2240+
_reportUnexpectedNumberChar(ch,
2241+
"expected space, comma, or closing bracket/brace to separate or terminate numeric value");
2242+
}
2243+
22062244
/*
22072245
/**********************************************************************
22082246
/* Internal methods, secondary parsing

src/test/java/module-info.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
opens tools.jackson.core.unittest.jsonptr;
3535
opens tools.jackson.core.unittest.read;
3636
opens tools.jackson.core.unittest.read.loc;
37-
opens tools.jackson.core.unittest.tofix;
3837
opens tools.jackson.core.unittest.tofix.async;
3938
opens tools.jackson.core.unittest.sym;
4039
opens tools.jackson.core.unittest.type;

0 commit comments

Comments
 (0)