Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ JSON library.

#679: Number parsing should fail for trailing dot (period)
(fix by @cowtowncoder, w/ Claude code)
#707: Add `JsonReadFeature.ALLOW_HEXADECIMAL_NUMBERS` for JSON5-style hexadecimal
integer literals (`0x` / `0X`, optional sign)
(implementation by @seonwooj0810)
#1211: Add `JsonParser.willInternPropertyNames()` to check whether
property name interning is enabled
(contributed by Max P)
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/tools/jackson/core/base/ParserBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,10 @@ protected final JsonToken reset(boolean negative, int intLen, int fractLen, int
return resetFloat(negative, intLen, fractLen, expLen);
}

protected final JsonToken resetInt(boolean negative, int intLen)
// NOTE: was `final` before 3.2; relaxed so that `JsonParserBase` can
// override to clear hex-specific state on integer reset (the sibling
// `resetFloat` / `resetAsNaN` remain `final`).
protected JsonToken resetInt(boolean negative, int intLen)
Comment thread
cowtowncoder marked this conversation as resolved.
throws JacksonException
{
// May throw StreamConstraintsException:
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/tools/jackson/core/io/BigIntegerParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,21 @@ public static BigInteger parseWithFastParser(final String valueStr, final int ra
", reason: " + nfe.getMessage());
}
}

/**
* @since 3.2
*/
public static BigInteger parseWithFastParser(final char[] ch, final int offset,
Comment thread
cowtowncoder marked this conversation as resolved.
final int length, final int radix) {
try {
return JavaBigIntegerParser.parseBigInteger(ch, offset, length, radix);
} catch (NumberFormatException nfe) {
final String reportNum = length <= MAX_CHARS_TO_REPORT
? new String(ch, offset, length)
: new String(ch, offset, MAX_CHARS_TO_REPORT) + " [truncated]";
throw new NumberFormatException("Value \"" + reportNum
+ "\" cannot be represented as `java.math.BigInteger` with radix " + radix +
", reason: " + nfe.getMessage());
}
}
}
24 changes: 24 additions & 0 deletions src/main/java/tools/jackson/core/io/NumberInput.java
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,30 @@ public static BigInteger parseBigIntegerWithRadix(final String s, final int radi
return new BigInteger(s, radix);
}

/**
* Parse a {@link BigInteger} from a {@code char[]} slice. When
* {@code useFastParser} is {@code true} the slice is handed to
* {@code FastDoubleParser} directly so no intermediate {@link String} is
* allocated; otherwise the JDK constructor is used (which requires a
* temporary String).
*
* @param ch char array containing the digits to parse
* @param offset offset of the first digit in {@code ch}
* @param length number of digits to parse
* @param radix radix to parse with
* @param useFastParser whether to use {@code FastDoubleParser} (true) or the JDK default (false)
* @return a BigInteger
* @throws NumberFormatException if the char slice cannot be represented by a BigInteger with the given radix
* @since 3.2
*/
public static BigInteger parseBigIntegerWithRadix(final char[] ch, final int offset,
final int length, final int radix, final boolean useFastParser) throws NumberFormatException {
if (useFastParser) {
return BigIntegerParser.parseWithFastParser(ch, offset, length, radix);
}
return new BigInteger(new String(ch, offset, length), radix);
}

/**
* Method called to check whether given pattern looks like a valid Java
* Number (which is bit looser definition than valid JSON Number).
Expand Down
160 changes: 159 additions & 1 deletion src/main/java/tools/jackson/core/json/JsonParserBase.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package tools.jackson.core.json;

import java.math.BigInteger;

import tools.jackson.core.*;
import tools.jackson.core.base.ParserBase;
import tools.jackson.core.exc.InputCoercionException;
import tools.jackson.core.exc.StreamReadException;
import tools.jackson.core.io.CharTypes;
import tools.jackson.core.io.IOContext;
import tools.jackson.core.io.NumberInput;
import tools.jackson.core.util.JacksonFeatureSet;
Expand Down Expand Up @@ -49,6 +52,19 @@ public abstract class JsonParserBase
*/
protected JsonToken _nextToken;

/**
* Marker for integer values read using JSON5 hexadecimal notation
* ({@code 0x} / {@code 0X} prefix), enabled via
* {@link JsonReadFeature#ALLOW_HEXADECIMAL_NUMBERS}.
* When {@code true}, the textual representation buffered for the current
* token is the original hex literal (including any sign and the
* {@code 0x}/{@code 0X} prefix) and {@link #_intLength} records the
* number of hexadecimal digits (excluding sign and prefix).
*
* @since 3.2
*/
protected boolean _numberIsHex;

/*
/**********************************************************************
/* Helper buffer recycling
Expand Down Expand Up @@ -186,12 +202,52 @@ protected void createChildObjectContext(final int lineNr, final int colNr) throw
/**********************************************************************
*/

// Overridden to also clear the JSON-only `_numberIsHex` flag, so a
// subsequent regular integer is not mis-decoded as hex. Hex literals go
// through `resetIntHex` instead, which sets the flag.
@Override
protected JsonToken resetInt(boolean negative, int intLen)
throws JacksonException
{
_numberIsHex = false;
return super.resetInt(negative, intLen);
}

/**
* Variant of {@link #resetInt} used for integer values read in JSON5
* hexadecimal notation ({@code 0x...}). {@code hexDigitLen} is the
* number of hexadecimal digits (excluding sign and {@code 0x}/{@code 0X}
* prefix); the textual representation buffered by the caller is expected
* to contain the original literal including sign and prefix.
*
* @since 3.2
*/
protected final JsonToken resetIntHex(boolean negative, int hexDigitLen)
throws JacksonException
{
// May throw StreamConstraintsException:
_streamReadConstraints.validateIntegerLength(hexDigitLen);
Comment thread
cowtowncoder marked this conversation as resolved.
_numberNegative = negative;
_numberIsNaN = false;
_numberIsHex = true;
_intLength = hexDigitLen;
_fractLength = 0;
_expLength = 0;
_numTypesValid = NR_UNKNOWN; // to force decoding
_numberString = null;
return JsonToken.VALUE_NUMBER_INT;
}

@Override
protected void _parseNumericValue(int expType)
throws JacksonException, InputCoercionException
{
// Int or float?
if (_currToken == JsonToken.VALUE_NUMBER_INT) {
if (_numberIsHex) {
_parseHexInt(expType);
return;
}
int len = _intLength;
// First: optimization for simple int
if (len <= 9) {
Expand Down Expand Up @@ -250,7 +306,9 @@ protected int _parseIntValue() throws JacksonException
{
// Inlined variant of: _parseNumericValue(NR_INT)
if (_currToken == JsonToken.VALUE_NUMBER_INT) {
if (_intLength <= 9) {
// Hex integers go through the generic path so the base-16 decode is
// applied (the base-10 fast path below would mis-read the literal):
if (_intLength <= 9 && !_numberIsHex) {
int i = _textBuffer.contentsAsInt(_numberNegative);
_numberInt = i;
_numTypesValid = NR_INT;
Expand Down Expand Up @@ -297,6 +355,106 @@ private void _parseSlowFloat(int expType) throws JacksonException
}
}

/**
* Decode a JSON5 hexadecimal integer that was buffered as the original
* textual literal (sign + {@code 0x}/{@code 0X} prefix + hex digits).
* {@link #_intLength} holds the count of hex digits.
*
* @since 3.2
*/
private void _parseHexInt(int expType) throws JacksonException
{
final int hexLen = _intLength;
final char[] buf = _textBuffer.getTextBuffer();
Comment thread
cowtowncoder marked this conversation as resolved.
// Locate the first hex digit: skip optional sign and "0x" / "0X" prefix
int idx = _textBuffer.getTextOffset();
final char first = buf[idx];
if (first == '-' || first == '+') {
++idx;
}
idx += 2; // skip "0x" / "0X"

// Up to 7 hex digits always fit in a positive signed int (<= 0x0FFFFFFF).
// 8 hex digits may overflow signed int (e.g. 0x80000000), so we defer to
// the long path which handles range checks uniformly.
if (hexLen <= 7) {
int v = 0;
for (int i = 0; i < hexLen; ++i) {
v = (v << 4) | CharTypes.charToHex(buf[idx + i]);
}
_numberInt = _numberNegative ? -v : v;
_numTypesValid = NR_INT;
return;
}
// 9..15 hex digits always fit in a positive long (63 bits used at most)
if (hexLen <= 15) {
long v = 0L;
for (int i = 0; i < hexLen; ++i) {
v = (v << 4) | CharTypes.charToHex(buf[idx + i]);
}
_numberLong = _numberNegative ? -v : v;
_numTypesValid = NR_LONG;
return;
}
// 16 hex digits: may or may not fit in signed long, depending on top bit
if (hexLen == 16) {
int topNibble = CharTypes.charToHex(buf[idx]);
if (topNibble < 0x8) { // fits in positive signed long
long v = topNibble;
for (int i = 1; i < 16; ++i) {
v = (v << 4) | CharTypes.charToHex(buf[idx + i]);
}
_numberLong = _numberNegative ? -v : v;
_numTypesValid = NR_LONG;
return;
}
// else fall through to BigInteger path
}
// Larger values -> BigInteger. We must eagerly decode here (the lazy
// base-10 path via _numberString would mis-read hex digits). Pass the
// char[] slice directly so the fast path avoids an intermediate String.
BigInteger bi = NumberInput.parseBigIntegerWithRadix(buf, idx, hexLen, 16,
isEnabled(StreamReadFeature.USE_FAST_BIG_NUMBER_PARSER));
if (_numberNegative) {
bi = bi.negate();
}
_numberBigInt = bi;
_numberString = null;
_numTypesValid = NR_BIGINT;
if ((expType == NR_INT) || (expType == NR_LONG)) {
// Force the overflow path to surface a meaningful error
_reportTooLongIntegral(expType, _textBuffer.contentsAsString());
}
}

/**
* Standard error message used by all JSON parser variants when a
* {@code 0x}/{@code 0X} hex prefix is not followed by any hex digit.
*
* @since 3.2
*/
protected static String _hexPrefixNotFollowedMessage(char prefixChar) {
return "Hexadecimal number prefix '0" + prefixChar
+ "' must be followed by at least one hex digit (0-9, a-f, A-F)";
}

/**
* Called after seeing the {@code 'x'} or {@code 'X'} that follows a leading
* {@code '0'} in a number literal. Returns silently if
* {@link JsonReadFeature#ALLOW_HEXADECIMAL_NUMBERS} is enabled; otherwise
* throws a {@link StreamReadException} naming the feature that must be
* enabled, so the user gets a specific actionable error instead of a
* generic "unexpected character".
*
* @since 3.2
*/
protected void _checkHexNumbersAllowed(int prefixChar) throws StreamReadException {
if (!isEnabled(JsonReadFeature.ALLOW_HEXADECIMAL_NUMBERS)) {
_reportUnexpectedChar(prefixChar,
"hexadecimal number literals require enabling `JsonReadFeature.ALLOW_HEXADECIMAL_NUMBERS`");
}
}

private void _parseSlowInt(int expType) throws JacksonException
{
final String numStr = _textBuffer.contentsAsString();
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/tools/jackson/core/json/JsonReadFeature.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,38 @@ public enum JsonReadFeature

// // // Support for non-standard data format constructs: number representations

/**
* Feature that determines whether parser will allow
* JSON integer numbers to be expressed in hexadecimal
* notation as defined by the
* <a href="https://spec.json5.org/#numbers">JSON5 specification</a>:
* a {@code 0x} or {@code 0X} prefix followed by one or more
* hexadecimal digits ({@code [0-9a-fA-F]}), optionally preceded
* by a single {@code +} or {@code -} sign
* (with {@link #ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS} additionally
* required for the {@code +} variant).
* When enabled, tokens such as {@code 0xC0FFEE} or {@code -0x10} are
* accepted as {@link JsonToken#VALUE_NUMBER_INT}. The textual
* representation returned by {@link JsonParser#getString()} preserves
* the original literal (including the {@code 0x} / {@code 0X} prefix
* and any sign), while numeric accessors such as
* {@link JsonParser#getIntValue()}, {@link JsonParser#getLongValue()}
* and {@link JsonParser#getBigIntegerValue()} return the decoded value.
*<p>
* This feature is independent of
* {@link #ALLOW_LEADING_ZEROS_FOR_NUMBERS}: leading zeros in the
* hexadecimal digit sequence (for example {@code 0x007F}) are always
* permitted when this feature is enabled, regardless of the state of
* {@code ALLOW_LEADING_ZEROS_FOR_NUMBERS}, since the JSON5 grammar
* allows them.
*<p>
* Since JSON specification does not allow hexadecimal numbers,
* this is a non-standard feature, and disabled by default.
*
* @since 3.2
*/
ALLOW_HEXADECIMAL_NUMBERS(false),

/**
* Feature that determines whether parser will allow
* JSON decimal numbers to start with a decimal point
Expand Down
Loading
Loading