From 372f5caac4666cea82904cac9ce8fa2591624e1b Mon Sep 17 00:00:00 2001 From: rikkarth Date: Sat, 30 Mar 2024 10:15:10 +0000 Subject: [PATCH] feat(#871-strictMode): enhanced and simplified strictMode logic --- src/main/java/org/json/JSONObject.java | 14 +- src/main/java/org/json/JSONTokener.java | 197 ++++++++++-------------- 2 files changed, 84 insertions(+), 127 deletions(-) diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index 27bc794..f8f6e18 100644 --- a/src/main/java/org/json/JSONObject.java +++ b/src/main/java/org/json/JSONObject.java @@ -225,14 +225,7 @@ public class JSONObject { case '}': return; default: - String keyToValidate = getKeyToValidate(x, c, jsonParserConfiguration.isStrictMode()); - boolean keyHasQuotes = keyToValidate.startsWith("\"") && keyToValidate.endsWith("\""); - - if (jsonParserConfiguration.isStrictMode() && !keyHasQuotes) { - throw new JSONException("Key is not surrounded by quotes: " + keyToValidate); - } - - key = keyToValidate; + key = x.nextSimpleValue(c, jsonParserConfiguration.isStrictMode()).toString(); } // The key is followed by ':'. @@ -285,11 +278,6 @@ public class JSONObject { } return x.nextValue(); } - - private String getKeyToValidate(JSONTokener x, char c, boolean strictMode) { - return x.nextSimpleValue(c, strictMode).toString(); - } - /** * Construct a JSONObject from a Map. * diff --git a/src/main/java/org/json/JSONTokener.java b/src/main/java/org/json/JSONTokener.java index 72f0c5b..c91628a 100644 --- a/src/main/java/org/json/JSONTokener.java +++ b/src/main/java/org/json/JSONTokener.java @@ -284,62 +284,73 @@ public class JSONTokener { * Backslash processing is done. The formal JSON format does not * allow strings in single quotes, but an implementation is allowed to * accept them. + * If strictMode is true, this implementation will not accept unbalanced quotes (e.g will not accept "test') * @param quote The quoting character, either * " (double quote) or * ' (single quote). - * @return A String. - * @throws JSONException Unterminated string. + * @return A String. + * @throws JSONException Unterminated string or unbalanced quotes if strictMode == true. */ - public String nextString(char quote) throws JSONException { + public String nextString(char quote, boolean strictMode) throws JSONException { char c; StringBuilder sb = new StringBuilder(); for (;;) { c = this.next(); switch (c) { - case 0: - case '\n': - case '\r': - throw this.syntaxError("Unterminated string"); - case '\\': - c = this.next(); - switch (c) { - case 'b': - sb.append('\b'); - break; - case 't': - sb.append('\t'); - break; - case 'n': - sb.append('\n'); - break; - case 'f': - sb.append('\f'); - break; - case 'r': - sb.append('\r'); - break; - case 'u': - try { - sb.append((char)Integer.parseInt(this.next(4), 16)); - } catch (NumberFormatException e) { - throw this.syntaxError("Illegal escape.", e); + case 0: + case '\n': + case '\r': + throw this.syntaxError("Unterminated string"); + case '\\': + c = this.next(); + switch (c) { + case 'b': + sb.append('\b'); + break; + case 't': + sb.append('\t'); + break; + case 'n': + sb.append('\n'); + break; + case 'f': + sb.append('\f'); + break; + case 'r': + sb.append('\r'); + break; + case 'u': + try { + sb.append((char) Integer.parseInt(this.next(4), 16)); + } catch (NumberFormatException e) { + throw this.syntaxError("Illegal escape.", e); + } + break; + case '"': + case '\'': + case '\\': + case '/': + sb.append(c); + break; + default: + throw this.syntaxError("Illegal escape."); } break; - case '"': - case '\'': - case '\\': - case '/': - sb.append(c); - break; default: - throw this.syntaxError("Illegal escape."); - } - break; - default: - if (c == quote) { - return sb.toString(); - } - sb.append(c); + if (strictMode && c == '\"' && quote != c) { + throw this.syntaxError(String.format( + "Field contains unbalanced quotes. Starts with %s but ends with double quote.", quote)); + } + + if (strictMode && c == '\'' && quote != c) { + throw this.syntaxError(String.format( + "Field contains unbalanced quotes. Starts with %s but ends with single quote.", quote)); + } + + if (c == quote) { + return sb.toString(); + } + sb.append(c); } } } @@ -423,50 +434,10 @@ public class JSONTokener { this.back(); return getJsonArray(); default: - return getValue(c, strictMode); + return nextSimpleValue(c, strictMode); } } - /** - * This method is used to get the next value. - * - * @param c The next character in the JSONTokener. - * @param strictMode If true, the method will strictly adhere to the JSON syntax, throwing a JSONException if the - * value is not surrounded by quotes. - * @return An object which is the next value in the JSONTokener. - * @throws JSONException If the value is not surrounded by quotes when strictMode is true. - */ - private Object getValue(char c, boolean strictMode) { - if (strictMode) { - Object valueToValidate = nextSimpleValue(c, true); - - boolean isNumeric = checkIfValueIsNumeric(valueToValidate); - - if (isNumeric) { - return valueToValidate; - } - - boolean hasQuotes = valueIsWrappedByQuotes(valueToValidate); - - if (!hasQuotes) { - throw new JSONException("Value is not surrounded by quotes: " + valueToValidate); - } - - return valueToValidate; - } - - return nextSimpleValue(c); - } - - private boolean checkIfValueIsNumeric(Object valueToValidate) { - for (char c : valueToValidate.toString().toCharArray()) { - if (!Character.isDigit(c)) { - return false; - } - } - return true; - } - /** * This method is used to get a JSONObject from the JSONTokener. The strictMode parameter controls the behavior of * the method when parsing the JSONObject. @@ -502,38 +473,12 @@ public class JSONTokener { } } - /** - * This method checks if the provided value is wrapped by quotes. - * - * @param valueToValidate The value to be checked. It is converted to a string before checking. - * @return A boolean indicating whether the value is wrapped by quotes. It returns true if the value is wrapped by - * either single or double quotes. - */ - private boolean valueIsWrappedByQuotes(Object valueToValidate) { - String stringToValidate = valueToValidate.toString(); - boolean isWrappedByDoubleQuotes = isWrappedByQuotes(stringToValidate, "\""); - boolean isWrappedBySingleQuotes = isWrappedByQuotes(stringToValidate, "'"); - return isWrappedByDoubleQuotes || isWrappedBySingleQuotes; - } - - private boolean isWrappedByQuotes(String valueToValidate, String quoteType) { - return valueToValidate.startsWith(quoteType) && valueToValidate.endsWith(quoteType); - } - - Object nextSimpleValue(char c) { - return nextSimpleValue(c, false); - } - Object nextSimpleValue(char c, boolean strictMode) { if (c == '"' || c == '\'') { - String str = this.nextString(c); - if (strictMode) { - return String.format("\"%s\"", str); - } - return str; + return this.nextString(c, strictMode); } - return parsedUnquotedText(c); + return parsedUnquotedText(c, strictMode); } /** @@ -545,7 +490,7 @@ public class JSONTokener { * @return The parsed object. * @throws JSONException If the parsed string is empty. */ - private Object parsedUnquotedText(char c) { + private Object parsedUnquotedText(char c, boolean strictMode) { StringBuilder sb = new StringBuilder(); while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) { sb.append(c); @@ -556,12 +501,36 @@ public class JSONTokener { } String string = sb.toString().trim(); + + if (strictMode) { + boolean isBooleanOrNumeric = checkIfValueIsBooleanOrNumeric(string); + + if (isBooleanOrNumeric) { + return string; + } + + throw new JSONException(String.format("Value is not surrounded by quotes: %s", string)); + } + if (string.isEmpty()) { throw this.syntaxError("Missing value"); } return JSONObject.stringToValue(string); } + private boolean checkIfValueIsBooleanOrNumeric(Object valueToValidate) { + String stringToValidate = valueToValidate.toString(); + if (stringToValidate.equals("true") || stringToValidate.equals("false")) { + return true; + } + + try { + Double.parseDouble(stringToValidate); + return true; + } catch (NumberFormatException e) { + return false; + } + } /** * Skip characters until the next character is the requested character.