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..5cfc608180 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()); @@ -349,15 +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: - case MINOR_NUMBER_MINUSZERO: - // NOTE: does NOT retain possible leading minus-sign (can change if - // absolutely needs be) return _valueCompleteInt(0, "0"); + case MINOR_NUMBER_MINUSZERO: + _numberNegative = true; + _valueCompleteInt(0, "-0"); + _intLength = 1; + return _currToken; + case MINOR_NUMBER_PLUSZERO: + _valueCompleteInt(0, "+0"); + _intLength = 1; + return _currToken; case MINOR_NUMBER_INTEGER_DIGITS: // Fine: just need to ensure we have value fully defined { @@ -1643,7 +1651,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..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 @@ -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(); } @@ -256,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 ]";