diff --git a/src/main/java/com/fasterxml/jackson/core/JsonParser.java b/src/main/java/com/fasterxml/jackson/core/JsonParser.java index cdfc11f7fe..56fc81e74c 100644 --- a/src/main/java/com/fasterxml/jackson/core/JsonParser.java +++ b/src/main/java/com/fasterxml/jackson/core/JsonParser.java @@ -182,6 +182,11 @@ public enum Feature { @Deprecated ALLOW_NUMERIC_LEADING_ZEROS(false), + /** + * @deprecated Use {@link com.fasterxml.jackson.core.json.JsonReadFeature#ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS} instead + */ + ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS(false), + /** * @deprecated Use {@link com.fasterxml.jackson.core.json.JsonReadFeature#ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS} instead */ diff --git a/src/main/java/com/fasterxml/jackson/core/json/JsonReadFeature.java b/src/main/java/com/fasterxml/jackson/core/json/JsonReadFeature.java index 098e496554..81396f6c57 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/JsonReadFeature.java +++ b/src/main/java/com/fasterxml/jackson/core/json/JsonReadFeature.java @@ -107,6 +107,21 @@ public enum JsonReadFeature @SuppressWarnings("deprecation") ALLOW_LEADING_ZEROS_FOR_NUMBERS(false, JsonParser.Feature.ALLOW_NUMERIC_LEADING_ZEROS), + /** + * Feature that determines whether parser will allow + * JSON decimal numbers to start with a plus sign + * (like: +123). If enabled, no exception is thrown, and the number + * is parsed as though a leading sign had not been present. + *

+ * Since JSON specification does not allow leading plus signs, + * this is a non-standard feature, and as such disabled by default. + * + * @since 2.14 + */ + @SuppressWarnings("deprecation") + ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS(false, JsonParser.Feature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS), + + /** * Feature that determines whether parser will allow * JSON decimal numbers to start with a decimal point diff --git a/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java b/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java index a7f51fa396..ab683f8624 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java +++ b/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java @@ -777,12 +777,15 @@ public final JsonToken nextToken() throws IOException break; case '-': - /* Should we have separate handling for plus? Although - * it is not allowed per se, it may be erroneously used, - * and could be indicate by a more specific error message. - */ t = _parseNegNumber(); break; + case '+': + if (!isEnabled(JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS.mappedFeature())) { + t = _handleOddValue(i); + } else { + t = _parsePosNumber(); + } + break; case '.': // [core#61]] t = _parseFloatThatStartsWithPeriod(); break; @@ -1453,24 +1456,37 @@ private final JsonToken _parseFloat(int ch, int startPtr, int ptr, boolean neg, return resetFloat(neg, intLen, fractLen, expLen); } + protected final JsonToken _parsePosNumber() throws IOException + { + return _parsePossibleNumber(false); + } + protected final JsonToken _parseNegNumber() throws IOException + { + return _parsePossibleNumber(true); + } + + private JsonToken _parsePossibleNumber(final boolean negative) throws IOException { int ptr = _inputPtr; - int startPtr = ptr-1; // to include sign/digit already read + int startPtr = negative ? ptr-1 : ptr; // to include sign/digit already read final int inputLen = _inputEnd; if (ptr >= inputLen) { - return _parseNumber2(true, startPtr); + return _parseNumber2(negative, startPtr); } int ch = _inputBuffer[ptr++]; // First check: must have a digit to follow minus sign if (ch > INT_9 || ch < INT_0) { _inputPtr = ptr; - return _handleInvalidNumberStart(ch, true); + if (ch == INT_PERIOD) { + return _parseFloatThatStartsWithPeriod(); + } + return _handleInvalidNumberStart(ch, negative); } // One special case, leading zero(es): if (ch == INT_0) { - return _parseNumber2(true, startPtr); + return _parseNumber2(negative, startPtr); } int intLen = 1; // already got one @@ -1478,7 +1494,7 @@ protected final JsonToken _parseNegNumber() throws IOException int_loop: while (true) { if (ptr >= inputLen) { - return _parseNumber2(true, startPtr); + return _parseNumber2(negative, startPtr); } ch = (int) _inputBuffer[ptr++]; if (ch < INT_0 || ch > INT_9) { @@ -1489,7 +1505,7 @@ protected final JsonToken _parseNegNumber() throws IOException if (ch == INT_PERIOD || ch == INT_e || ch == INT_E) { _inputPtr = ptr; - return _parseFloat(ch, startPtr, ptr, true, intLen); + return _parseFloat(ch, startPtr, ptr, negative, intLen); } --ptr; _inputPtr = ptr; @@ -1498,7 +1514,7 @@ protected final JsonToken _parseNegNumber() throws IOException } int len = ptr-startPtr; _textBuffer.resetWithShared(_inputBuffer, startPtr, len); - return resetInt(true, intLen); + return resetInt(negative, intLen); } /** diff --git a/src/main/java/com/fasterxml/jackson/core/json/UTF8DataInputJsonParser.java b/src/main/java/com/fasterxml/jackson/core/json/UTF8DataInputJsonParser.java index 44b9053563..1fe8c7600b 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/UTF8DataInputJsonParser.java +++ b/src/main/java/com/fasterxml/jackson/core/json/UTF8DataInputJsonParser.java @@ -660,10 +660,13 @@ public JsonToken nextToken() throws IOException case '-': t = _parseNegNumber(); break; - - // Should we have separate handling for plus? Although - // it is not allowed per se, it may be erroneously used, - // and could be indicate by a more specific error message. + case '+': + if (!isEnabled(JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS.mappedFeature())) { + t = _handleUnexpectedValue(i); + } else { + t = _parsePosNumber(); + } + break; case '.': // as per [core#611] t = _parseFloatThatStartsWithPeriod(); break; @@ -729,9 +732,11 @@ private final JsonToken _nextTokenNotInObject(int i) throws IOException return (_currToken = JsonToken.VALUE_NULL); case '-': return (_currToken = _parseNegNumber()); - // Should we have separate handling for plus? Although it is not allowed - // per se, it may be erroneously used, and could be indicated by a more - // specific error message. + case '+': + if (!isEnabled(JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS.mappedFeature())) { + return (_currToken = _handleUnexpectedValue(i)); + } + return (_currToken = _parsePosNumber()); case '.': // as per [core#611] return (_currToken = _parseFloatThatStartsWithPeriod()); case '0': @@ -1067,14 +1072,26 @@ protected JsonToken _parsePosNumber(int c) throws IOException // And there we have it! return resetInt(false, intLen); } - + + protected JsonToken _parsePosNumber() throws IOException + { + return _parsePossibleNumber(false); + } + protected JsonToken _parseNegNumber() throws IOException + { + return _parsePossibleNumber(true); + } + + private JsonToken _parsePossibleNumber(boolean negative) throws IOException { char[] outBuf = _textBuffer.emptyAndGetCurrentSegment(); int outPtr = 0; - // Need to prepend sign? - outBuf[outPtr++] = '-'; + if (negative) { + // Need to prepend sign? + outBuf[outPtr++] = '-'; + } int c = _inputData.readUnsignedByte(); outBuf[outPtr++] = (char) c; // Note: must be followed by a digit @@ -1082,12 +1099,14 @@ protected JsonToken _parseNegNumber() throws IOException // One special case: if first char is 0 need to check no leading zeroes if (c == INT_0) { c = _handleLeadingZeroes(); + } else if (c == INT_PERIOD) { + return _parseFloatThatStartsWithPeriod(); } else { - return _handleInvalidNumberStart(c, true); + return _handleInvalidNumberStart(c, negative); } } else { if (c > INT_9) { - return _handleInvalidNumberStart(c, true); + return _handleInvalidNumberStart(c, negative); } c = _inputData.readUnsignedByte(); } @@ -1101,7 +1120,7 @@ protected JsonToken _parseNegNumber() throws IOException c = _inputData.readUnsignedByte(); } if (c == '.' || c == 'e' || c == 'E') { - return _parseFloat(outBuf, outPtr, c, true, intLen); + return _parseFloat(outBuf, outPtr, c, negative, intLen); } _textBuffer.setCurrentLength(outPtr); // As per [core#105], need separating space between root values; check here @@ -1110,7 +1129,7 @@ protected JsonToken _parseNegNumber() throws IOException _verifyRootSpace(); } // And there we have it! - return resetInt(true, intLen); + return resetInt(negative, intLen); } /** diff --git a/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java b/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java index 32b50e3a13..87af9a297a 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java +++ b/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java @@ -814,9 +814,13 @@ public JsonToken nextToken() throws IOException case '-': t = _parseNegNumber(); break; - - // Should we have separate handling for plus? Although it is not allowed per se, - // it may be erroneously used, and could be indicate by a more specific error message. + case '+': + if (!isEnabled(JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS.mappedFeature())) { + t = _handleUnexpectedValue(i); + } else { + t = _parsePosNumber(); + } + break; case '.': // [core#611]: t = _parseFloatThatStartsWithPeriod(); break; @@ -882,9 +886,11 @@ private final JsonToken _nextTokenNotInObject(int i) throws IOException return (_currToken = JsonToken.VALUE_NULL); case '-': return (_currToken = _parseNegNumber()); - - // Should we have separate handling for plus? Although it is not allowed per se, - // it may be erroneously used, and could be indicate by a more specific error message. + case '+': + if (!isEnabled(JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS.mappedFeature())) { + return (_currToken = _handleUnexpectedValue(i)); + } + return (_currToken = _parsePosNumber()); case '.': // [core#611]: return (_currToken = _parseFloatThatStartsWithPeriod()); case '0': @@ -1475,14 +1481,26 @@ protected JsonToken _parsePosNumber(int c) throws IOException // And there we have it! return resetInt(false, intLen); } - + + protected JsonToken _parsePosNumber() throws IOException + { + return _parsePossibleNumber(false); + } + protected JsonToken _parseNegNumber() throws IOException + { + return _parsePossibleNumber(true); + } + + private JsonToken _parsePossibleNumber(boolean negative) throws IOException { char[] outBuf = _textBuffer.emptyAndGetCurrentSegment(); int outPtr = 0; - // Need to prepend sign? - outBuf[outPtr++] = '-'; + if (negative) { + // Need to prepend sign? + outBuf[outPtr++] = '-'; + } // Must have something after sign too if (_inputPtr >= _inputEnd) { _loadMoreGuaranteed(); @@ -1492,13 +1510,16 @@ protected JsonToken _parseNegNumber() throws IOException if (c <= INT_0) { // One special case: if first char is 0, must not be followed by a digit if (c != INT_0) { - return _handleInvalidNumberStart(c, true); + if (c == INT_PERIOD) { + return _parseFloatThatStartsWithPeriod(); + } + return _handleInvalidNumberStart(c, negative); } c = _verifyNoLeadingZeroes(); } else if (c > INT_9) { - return _handleInvalidNumberStart(c, true); + return _handleInvalidNumberStart(c, negative); } - + // Ok: we can first just add digit we saw first: outBuf[outPtr++] = (char) c; int intLen = 1; @@ -1510,7 +1531,7 @@ protected JsonToken _parseNegNumber() throws IOException while (true) { if (_inputPtr >= end) { // Long enough to be split across boundary, so: - return _parseNumber2(outBuf, outPtr, true, intLen); + return _parseNumber2(outBuf, outPtr, negative, intLen); } c = (int) _inputBuffer[_inputPtr++] & 0xFF; if (c < INT_0 || c > INT_9) { @@ -1520,9 +1541,9 @@ protected JsonToken _parseNegNumber() throws IOException outBuf[outPtr++] = (char) c; } if (c == INT_PERIOD || c == INT_e || c == INT_E) { - return _parseFloat(outBuf, outPtr, c, true, intLen); + return _parseFloat(outBuf, outPtr, c, negative, intLen); } - + --_inputPtr; // to push back trailing char (comma etc) _textBuffer.setCurrentLength(outPtr); // As per #105, need separating space between root values; check here @@ -1531,7 +1552,7 @@ protected JsonToken _parseNegNumber() throws IOException } // And there we have it! - return resetInt(true, intLen); + return resetInt(negative, intLen); } // Method called to handle parsing when input is split across buffer boundary diff --git a/src/test/java/com/fasterxml/jackson/core/read/FastParserNonStandardNumberParsingTest.java b/src/test/java/com/fasterxml/jackson/core/read/FastParserNonStandardNumberParsingTest.java index 21c8dbfa0b..ab4a64f41f 100644 --- a/src/test/java/com/fasterxml/jackson/core/read/FastParserNonStandardNumberParsingTest.java +++ b/src/test/java/com/fasterxml/jackson/core/read/FastParserNonStandardNumberParsingTest.java @@ -9,6 +9,7 @@ public class FastParserNonStandardNumberParsingTest { private final JsonFactory fastFactory = JsonFactory.builder() + .enable(JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS) .enable(JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS) .enable(JsonReadFeature.ALLOW_TRAILING_DECIMAL_POINT_FOR_NUMBERS) .enable(StreamReadFeature.USE_FAST_DOUBLE_PARSER) diff --git a/src/test/java/com/fasterxml/jackson/core/read/NonStandardNumberParsingTest.java b/src/test/java/com/fasterxml/jackson/core/read/NonStandardNumberParsingTest.java index 996412a66f..bcf6f662d3 100644 --- a/src/test/java/com/fasterxml/jackson/core/read/NonStandardNumberParsingTest.java +++ b/src/test/java/com/fasterxml/jackson/core/read/NonStandardNumberParsingTest.java @@ -7,6 +7,7 @@ public class NonStandardNumberParsingTest extends com.fasterxml.jackson.core.BaseTest { private final JsonFactory JSON_F = JsonFactory.builder() + .enable(JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS) .enable(JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS) .enable(JsonReadFeature.ALLOW_TRAILING_DECIMAL_POINT_FOR_NUMBERS) .build(); @@ -31,6 +32,22 @@ public void testLeadingDotInDecimal() throws Exception { } } + /* + * The format "+NNN" (as opposed to "NNN") is not valid JSON, so this should fail + */ + public void testLeadingPlusSignInDecimal() throws Exception { + for (int mode : ALL_MODES) { + JsonParser p = createParser(mode, " +123 "); + try { + p.nextToken(); + fail("Should not pass"); + } catch (JsonParseException e) { + verifyException(e, "expected digit (0-9) to follow minus sign, for valid numeric value"); + } + p.close(); + } + } + /** * The format "NNN." (as opposed to "NNN") is not valid JSON, so this should fail */ @@ -73,23 +90,58 @@ public void testTrailingDotInDecimalAllowedReader() throws Exception { _testTrailingDotInDecimalAllowed(jsonFactory(), MODE_READER); } + public void testLeadingPlusSignInDecimalAllowedAsync() throws Exception { + _testLeadingPlusSignInDecimalAllowed(jsonFactory(), MODE_DATA_INPUT); + } + + public void testLeadingPlusSignInDecimalAllowedBytes() throws Exception { + _testLeadingPlusSignInDecimalAllowed(jsonFactory(), MODE_INPUT_STREAM); + _testLeadingPlusSignInDecimalAllowed(jsonFactory(), MODE_INPUT_STREAM_THROTTLED); + } + + public void testLeadingPlusSignInDecimalAllowedReader() throws Exception { + _testLeadingPlusSignInDecimalAllowed(jsonFactory(), MODE_READER); + } + private void _testLeadingDotInDecimalAllowed(JsonFactory f, int mode) throws Exception { - JsonParser p = createParser(f, mode, " .125 "); - assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); - assertEquals(0.125, p.getValueAsDouble()); - assertEquals("0.125", p.getDecimalValue().toString()); - assertEquals(".125", p.getText()); - p.close(); + try (JsonParser p = createParser(f, mode, " .125 ")) { + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + assertEquals(0.125, p.getValueAsDouble()); + assertEquals("0.125", p.getDecimalValue().toString()); + assertEquals(".125", p.getText()); + } + } + + private void _testLeadingPlusSignInDecimalAllowed(JsonFactory f, int mode) throws Exception + { + try (JsonParser p = createParser(f, mode, " +125 ")) { + assertEquals(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals(125.0, p.getValueAsDouble()); + assertEquals("125", p.getDecimalValue().toString()); + assertEquals("125", p.getText()); + } + try (JsonParser p = createParser(f, mode, " +0.125 ")) { + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + assertEquals(0.125, p.getValueAsDouble()); + assertEquals("0.125", p.getDecimalValue().toString()); + assertEquals("0.125", p.getText()); + } + try (JsonParser p = createParser(f, mode, " +.125 ")) { + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + assertEquals(0.125, p.getValueAsDouble()); + assertEquals("0.125", p.getDecimalValue().toString()); + assertEquals(".125", p.getText()); + } } private void _testTrailingDotInDecimalAllowed(JsonFactory f, int mode) throws Exception { - JsonParser p = createParser(f, mode, " 125. "); - assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); - assertEquals(125.0, p.getValueAsDouble()); - assertEquals("125", p.getDecimalValue().toString()); - assertEquals("125.", p.getText()); - p.close(); + try (JsonParser p = createParser(f, mode, " 125. ")) { + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + assertEquals(125.0, p.getValueAsDouble()); + assertEquals("125", p.getDecimalValue().toString()); + assertEquals("125.", p.getText()); + } } } diff --git a/src/test/java/com/fasterxml/jackson/core/read/NonStandardParserFeaturesTest.java b/src/test/java/com/fasterxml/jackson/core/read/NonStandardParserFeaturesTest.java index 172c066f0b..3d745003a3 100644 --- a/src/test/java/com/fasterxml/jackson/core/read/NonStandardParserFeaturesTest.java +++ b/src/test/java/com/fasterxml/jackson/core/read/NonStandardParserFeaturesTest.java @@ -20,10 +20,10 @@ public void testDefaults() { public void testNonStandardAnyCharQuoting() throws Exception { - _testNonStandarBackslashQuoting(MODE_INPUT_STREAM); - _testNonStandarBackslashQuoting(MODE_INPUT_STREAM_THROTTLED); - _testNonStandarBackslashQuoting(MODE_DATA_INPUT); - _testNonStandarBackslashQuoting(MODE_READER); + _testNonStandardBackslashQuoting(MODE_INPUT_STREAM); + _testNonStandardBackslashQuoting(MODE_INPUT_STREAM_THROTTLED); + _testNonStandardBackslashQuoting(MODE_DATA_INPUT); + _testNonStandardBackslashQuoting(MODE_READER); } public void testLeadingZeroesUTF8() throws Exception { @@ -64,7 +64,7 @@ public void testAllowInfinity() throws Exception { /**************************************************************** */ - private void _testNonStandarBackslashQuoting(int mode) throws Exception + private void _testNonStandardBackslashQuoting(int mode) throws Exception { // first: verify that we get an exception final String JSON = quote("\\'");