From 8260a9c4aada94dde3da7c9271ee32c6072da23f Mon Sep 17 00:00:00 2001 From: seonwooj0810 Date: Wed, 27 May 2026 12:50:52 +0900 Subject: [PATCH 1/2] Preserve explicit '+' across buffer boundaries for "+0..." on async parser The non-blocking parser reused MINOR_NUMBER_ZERO for both bare-zero ("0...") and explicit-plus-zero ("+0...") suspension paths, so the leading '+' was lost when input arrived byte-by-byte. Add a dedicated MINOR_NUMBER_PLUSZERO state alongside MINOR_NUMBER_ZERO/MINOR_NUMBER_MINUSZERO; _finishNumberLeadingPosNegZeroes now stores MINOR_NUMBER_PLUSZERO when negative=false, restoring symmetry with the blocking parsers and the sibling "+123" path. Update AsyncNonStandardNumberParsingTest#leadingPlusSignInDecimalEnabled2 to assert "+0.123" -- the previous "0.123" assertion contradicted the sibling test for "+123" and was masked by the bug. Noticed while working on hex-literal support (#707); split out per maintainer request. --- .../core/json/async/NonBlockingJsonParserBase.java | 6 ++++++ .../core/json/async/NonBlockingUtf8JsonParserBase.java | 8 +++++++- .../json/async/AsyncNonStandardNumberParsingTest.java | 6 +++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/java/tools/jackson/core/json/async/NonBlockingJsonParserBase.java b/src/main/java/tools/jackson/core/json/async/NonBlockingJsonParserBase.java index 35754f88eb..1a3d12ddb8 100644 --- a/src/main/java/tools/jackson/core/json/async/NonBlockingJsonParserBase.java +++ b/src/main/java/tools/jackson/core/json/async/NonBlockingJsonParserBase.java @@ -110,6 +110,12 @@ public abstract class NonBlockingJsonParserBase protected final static int MINOR_NUMBER_EXPONENT_MARKER = 31; protected final static int MINOR_NUMBER_EXPONENT_DIGITS = 32; + // Resumption state for an explicit '+' followed by '0' (and possibly more + // zeros). Companion to {@link #MINOR_NUMBER_ZERO} and + // {@link #MINOR_NUMBER_MINUSZERO}; needed so that the buffered text on + // resume correctly retains the leading '+' character. + protected final static int MINOR_NUMBER_PLUSZERO = 33; + protected final static int MINOR_VALUE_STRING = 40; protected final static int MINOR_VALUE_STRING_ESCAPE = 41; protected final static int MINOR_VALUE_STRING_UTF8_2 = 42; diff --git a/src/main/java/tools/jackson/core/json/async/NonBlockingUtf8JsonParserBase.java b/src/main/java/tools/jackson/core/json/async/NonBlockingUtf8JsonParserBase.java index 4a51f9608c..6abeecd775 100644 --- a/src/main/java/tools/jackson/core/json/async/NonBlockingUtf8JsonParserBase.java +++ b/src/main/java/tools/jackson/core/json/async/NonBlockingUtf8JsonParserBase.java @@ -244,6 +244,8 @@ protected final JsonToken _finishToken() throws JacksonException return _finishNumberLeadingZeroes(); case MINOR_NUMBER_MINUSZERO: return _finishNumberLeadingNegZeroes(); + case MINOR_NUMBER_PLUSZERO: + return _finishNumberLeadingPosZeroes(); case MINOR_NUMBER_INTEGER_DIGITS: return _finishNumberIntegralPart(_textBuffer.getBufferWithoutReset(), _textBuffer.getCurrentSegmentSize()); @@ -355,6 +357,7 @@ protected final JsonToken _finishTokenWithEOF() throws JacksonException // Number-parsing states; valid stopping points, more explicit errors case MINOR_NUMBER_ZERO: case MINOR_NUMBER_MINUSZERO: + case MINOR_NUMBER_PLUSZERO: // NOTE: does NOT retain possible leading minus-sign (can change if // absolutely needs be) return _valueCompleteInt(0, "0"); @@ -1643,7 +1646,10 @@ protected JsonToken _finishNumberLeadingPosNegZeroes(final boolean negative) thr // numeric characters; likely legal separators, or, known illegal (letters). while (true) { if (_inputPtr >= _inputEnd) { - _minorState = negative ? MINOR_NUMBER_MINUSZERO : MINOR_NUMBER_ZERO; + // Use MINOR_NUMBER_PLUSZERO for explicit-plus paths so the sign is + // preserved on resumption (callers reach here via + // _finishNumberLeadingPosZeroes / _finishNumberLeadingNegZeroes only). + _minorState = negative ? MINOR_NUMBER_MINUSZERO : MINOR_NUMBER_PLUSZERO; return _updateTokenToNA(); } int ch = getNextUnsignedByteFromBuffer(); diff --git a/src/test/java/tools/jackson/core/unittest/json/async/AsyncNonStandardNumberParsingTest.java b/src/test/java/tools/jackson/core/unittest/json/async/AsyncNonStandardNumberParsingTest.java index 4a01fabf9f..0c94ff3df0 100644 --- a/src/test/java/tools/jackson/core/unittest/json/async/AsyncNonStandardNumberParsingTest.java +++ b/src/test/java/tools/jackson/core/unittest/json/async/AsyncNonStandardNumberParsingTest.java @@ -232,7 +232,11 @@ void leadingPlusSignInDecimalEnabled2() throws Exception { assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); assertEquals(0.123, p.getDoubleValue()); assertEquals("0.123", p.getDecimalValue().toString()); - assertEquals("0.123", p.currentText()); + // The async parser now retains the leading '+' here too, matching the + // blocking parsers and the sibling leadingPlusSignInDecimalEnabled test + // for "+123". Previously the explicit '+' followed by leading zero was + // lost on the byte-by-byte resumption path. + assertEquals("+0.123", p.currentText()); } finally { p.close(); } From d262a8cbfcc01b752e5d34fdcc6b5aedf631f555 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Tue, 26 May 2026 21:31:42 -0700 Subject: [PATCH 2/2] Add a fix for +0/-0 at EOF case --- .../async/NonBlockingUtf8JsonParserBase.java | 13 ++++-- .../AsyncNonStandardNumberParsingTest.java | 42 +++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/main/java/tools/jackson/core/json/async/NonBlockingUtf8JsonParserBase.java b/src/main/java/tools/jackson/core/json/async/NonBlockingUtf8JsonParserBase.java index 6abeecd775..5cfc608180 100644 --- a/src/main/java/tools/jackson/core/json/async/NonBlockingUtf8JsonParserBase.java +++ b/src/main/java/tools/jackson/core/json/async/NonBlockingUtf8JsonParserBase.java @@ -351,16 +351,21 @@ protected final JsonToken _finishTokenWithEOF() throws JacksonException return _finishKeywordTokenWithEOF("false", _pending32, JsonToken.VALUE_FALSE); case MINOR_VALUE_TOKEN_NON_STD: return _finishNonStdTokenWithEOF(_nonStdTokenType, _pending32); - case MINOR_VALUE_TOKEN_ERROR: // case of "almost token", just need tokenize for error + case MINOR_VALUE_TOKEN_ERROR: // case of "almost token", just need to tokenize for error return _finishErrorTokenWithEOF(); // Number-parsing states; valid stopping points, more explicit errors case MINOR_NUMBER_ZERO: + return _valueCompleteInt(0, "0"); case MINOR_NUMBER_MINUSZERO: + _numberNegative = true; + _valueCompleteInt(0, "-0"); + _intLength = 1; + return _currToken; case MINOR_NUMBER_PLUSZERO: - // NOTE: does NOT retain possible leading minus-sign (can change if - // absolutely needs be) - return _valueCompleteInt(0, "0"); + _valueCompleteInt(0, "+0"); + _intLength = 1; + return _currToken; case MINOR_NUMBER_INTEGER_DIGITS: // Fine: just need to ensure we have value fully defined { diff --git a/src/test/java/tools/jackson/core/unittest/json/async/AsyncNonStandardNumberParsingTest.java b/src/test/java/tools/jackson/core/unittest/json/async/AsyncNonStandardNumberParsingTest.java index 0c94ff3df0..51dd6dbe72 100644 --- a/src/test/java/tools/jackson/core/unittest/json/async/AsyncNonStandardNumberParsingTest.java +++ b/src/test/java/tools/jackson/core/unittest/json/async/AsyncNonStandardNumberParsingTest.java @@ -260,6 +260,48 @@ void leadingPlusSignInDecimalEnabled3() throws Exception { } } + // Root-level "+0" / "-0" / "0" terminated by end-of-input: exercises the + // MINOR_NUMBER_PLUSZERO / MINOR_NUMBER_MINUSZERO / MINOR_NUMBER_ZERO branches + // of `_finishTokenWithEOF`. Previously the sign was discarded here even when + // the byte-by-byte resumption path preserved it. + @Test + void rootPlusZeroAtEOF() throws Exception { + JsonFactory jsonFactory = JsonFactory.builder() + .enable(JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS).build(); + AsyncReaderWrapper p = createParser(jsonFactory, "+0", 1); + try { + assertEquals(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals(0, p.getIntValue()); + assertEquals("+0", p.currentText()); + } finally { + p.close(); + } + } + + @Test + void rootMinusZeroAtEOF() throws Exception { + AsyncReaderWrapper p = createParser(DEFAULT_F, "-0", 1); + try { + assertEquals(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals(0, p.getIntValue()); + assertEquals("-0", p.currentText()); + } finally { + p.close(); + } + } + + @Test + void rootPlainZeroAtEOF() throws Exception { + AsyncReaderWrapper p = createParser(DEFAULT_F, "0", 1); + try { + assertEquals(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals(0, p.getIntValue()); + assertEquals("0", p.currentText()); + } finally { + p.close(); + } + } + @Test void leadingPlusSignNoLeadingZeroDisabled() throws Exception { final String JSON = "[ +.123 ]";