1010import com .fasterxml .jackson .databind .ObjectMapper ;
1111import com .fasterxml .jackson .databind .SerializationFeature ;
1212import com .fasterxml .jackson .databind .json .JsonMapper ;
13+ import com .fasterxml .jackson .databind .node .ArrayNode ;
14+ import com .fasterxml .jackson .databind .node .NullNode ;
1315import com .fasterxml .jackson .databind .node .ObjectNode ;
16+ import java .util .ArrayList ;
17+ import java .util .List ;
1418import org .tron .common .parameter .CommonParameter ;
1519
1620/**
@@ -82,6 +86,113 @@ private static JsonFactory buildFactory() {
8286 private JSON () {
8387 }
8488
89+ /**
90+ * Fastjson 1.x parity: replace bare {@code NULL} tokens with {@code null}
91+ * in-place so Jackson's strict lowercase-only literal parser accepts them.
92+ * Skips contents of single- or double-quoted strings (with backslash-escape
93+ * support) and uses an identifier-aware boundary so unquoted field names
94+ * like {@code NULL_KEY} are left intact.
95+ */
96+ static String coerceUppercaseNull (String text ) {
97+ StringBuilder out = new StringBuilder (text .length ());
98+ int i = 0 ;
99+ int n = text .length ();
100+ while (i < n ) {
101+ char c = text .charAt (i );
102+ if (c == '"' || c == '\'' ) {
103+ char quote = c ;
104+ out .append (c );
105+ i ++;
106+ while (i < n ) {
107+ char ch = text .charAt (i );
108+ out .append (ch );
109+ i ++;
110+ if (ch == '\\' && i < n ) {
111+ out .append (text .charAt (i ));
112+ i ++;
113+ } else if (ch == quote ) {
114+ break ;
115+ }
116+ }
117+ continue ;
118+ }
119+ if (c == 'N' && i + 4 <= n
120+ && text .charAt (i + 1 ) == 'U'
121+ && text .charAt (i + 2 ) == 'L'
122+ && text .charAt (i + 3 ) == 'L'
123+ && (i == 0 || !isIdentChar (text .charAt (i - 1 )))
124+ && (i + 4 == n || !isIdentChar (text .charAt (i + 4 )))) {
125+ out .append ("null" );
126+ i += 4 ;
127+ continue ;
128+ }
129+ out .append (c );
130+ i ++;
131+ }
132+ return out .toString ();
133+ }
134+
135+ private static boolean isIdentChar (char c ) {
136+ return Character .isLetterOrDigit (c ) || c == '_' || c == '$' ;
137+ }
138+
139+ /**
140+ * Fast pre-check for Fastjson 1.x non-numeric coercion. Because
141+ * {@code USE_BIG_DECIMAL_FOR_FLOATS} is on, the only way a {@code Double}
142+ * {@code NaN} / {@code Infinity} can land in the tree is via the literal
143+ * tokens {@code NaN} / {@code Infinity} in the source text — large numeric
144+ * literals go to BigDecimal/BigInteger without overflow. So a substring
145+ * absence proves the tree has no offending nodes, and the O(n) walk in
146+ * {@link #coerceNonNumeric} can be skipped on the common case.
147+ */
148+ static boolean mayContainNonNumeric (String text ) {
149+ return text != null && (text .contains ("Infinity" ) || text .contains ("NaN" ));
150+ }
151+
152+ /**
153+ * Fastjson 1.x non-numeric-number parity: silently coerce {@code NaN} to JSON
154+ * {@code null}, reject {@code Infinity} / {@code -Infinity} with a
155+ * {@link JSONException} ({@code "syntax error, Infinity"} /
156+ * {@code "syntax error, -Infinity"}). Walks containers in-place.
157+ */
158+ static JsonNode coerceNonNumeric (JsonNode node ) {
159+ if (node == null || node .isNull ()) {
160+ return node ;
161+ }
162+ if (node .isFloatingPointNumber ()) {
163+ double v = node .doubleValue ();
164+ if (Double .isInfinite (v )) {
165+ throw new JSONException ("syntax error, " + (v > 0 ? "Infinity" : "-Infinity" ));
166+ }
167+ if (Double .isNaN (v )) {
168+ return NullNode .getInstance ();
169+ }
170+ return node ;
171+ }
172+ if (node .isObject ()) {
173+ ObjectNode obj = (ObjectNode ) node ;
174+ List <String > keys = new ArrayList <>();
175+ obj .fieldNames ().forEachRemaining (keys ::add );
176+ for (String k : keys ) {
177+ JsonNode child = obj .get (k );
178+ JsonNode replacement = coerceNonNumeric (child );
179+ if (replacement != child ) {
180+ obj .set (k , replacement );
181+ }
182+ }
183+ } else if (node .isArray ()) {
184+ ArrayNode arr = (ArrayNode ) node ;
185+ for (int i = 0 ; i < arr .size (); i ++) {
186+ JsonNode child = arr .get (i );
187+ JsonNode replacement = coerceNonNumeric (child );
188+ if (replacement != child ) {
189+ arr .set (i , replacement );
190+ }
191+ }
192+ }
193+ return node ;
194+ }
195+
85196 /**
86197 * Returns {@code true} when {@code text} is null, blank, or a
87198 * case-insensitive {@code "null"} literal — mirroring Fastjson's lenient
@@ -99,8 +210,12 @@ public static JSONObject parseObject(String text) {
99210 if (isNullLiteral (text )) {
100211 return null ;
101212 }
213+ String input = text .indexOf ("NULL" ) >= 0 ? coerceUppercaseNull (text ) : text ;
102214 try {
103- JsonNode node = MAPPER .readTree (text );
215+ JsonNode node = MAPPER .readTree (input );
216+ if (mayContainNonNumeric (input )) {
217+ node = coerceNonNumeric (node );
218+ }
104219 if (node == null || node .isNull ()) {
105220 return null ;
106221 }
@@ -136,12 +251,18 @@ public static JsonNode parse(String text) {
136251 if (isNullLiteral (text )) {
137252 return null ;
138253 }
254+ String input = text .indexOf ("NULL" ) >= 0 ? coerceUppercaseNull (text ) : text ;
139255 try {
140- JsonNode node = MAPPER .readTree (text );
256+ JsonNode node = MAPPER .readTree (input );
257+ if (mayContainNonNumeric (input )) {
258+ node = coerceNonNumeric (node );
259+ }
141260 if (node == null || node .isNull ()) {
142261 return null ;
143262 }
144263 return node ;
264+ } catch (JSONException e ) {
265+ throw e ;
145266 } catch (Exception e ) {
146267 throw new JSONException (e .getMessage (), e );
147268 }
0 commit comments