diff --git a/README.md b/README.md index e999230ba..9f0134206 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ JSON in Java [package org.json] [![Maven Central](https://img.shields.io/maven-central/v/org.json/json.svg)](https://mvnrepository.com/artifact/org.json/json) -**[Click here if you just want the latest release jar file.](https://search.maven.org/remotecontent?filepath=org/json/json/20230618/json-20230618.jar)** +**[Click here if you just want the latest release jar file.](https://search.maven.org/remotecontent?filepath=org/json/json/20231013/json-20231013.jar)** # Overview diff --git a/build.gradle b/build.gradle index 8a3708a74..fbc2ff1f1 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ repositories { } dependencies { - testImplementation 'junit:junit:4.13.1' + testImplementation 'junit:junit:4.13.2' testImplementation 'com.jayway.jsonpath:json-path:2.1.0' testImplementation 'org.mockito:mockito-core:4.2.0' } diff --git a/docs/RELEASES.md b/docs/RELEASES.md index ae439831c..2b8aaa267 100644 --- a/docs/RELEASES.md +++ b/docs/RELEASES.md @@ -5,6 +5,8 @@ and artifactId "json". For example: [https://search.maven.org/search?q=g:org.json%20AND%20a:json&core=gav](https://search.maven.org/search?q=g:org.json%20AND%20a:json&core=gav) ~~~ +20231013 First release with minimum Java version 1.8. Recent commits, including fixes for CVE-2023-5072. + 20230618 Final release with Java 1.6 compatibility. Future releases will require Java 1.8 or greater. 20230227 Fix for CVE-2022-45688 and recent commits diff --git a/pom.xml b/pom.xml index 720529c50..77bbdaca3 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.json json - 20230618 + 20231013 bundle JSON in Java @@ -15,7 +15,7 @@ It also includes the capability to convert between JSON and XML, HTTP headers, Cookies, and CDL. - This is a reference implementation. There is a large number of JSON packages + This is a reference implementation. There are a large number of JSON packages in Java. Perhaps someday the Java community will standardize on one. Until then, choose carefully. @@ -57,7 +57,7 @@ junit junit - 4.13.1 + 4.13.2 test @@ -159,35 +159,17 @@ false - - org.moditect - moditect-maven-plugin - 1.0.0.Final - - - add-module-infos - package - - add-module-info - - - 9 - - - org.json - - org.json; - - - - - - - org.apache.maven.plugins maven-jar-plugin 3.3.0 + + + + org.json + + + diff --git a/src/main/java/org/json/JSONArray.java b/src/main/java/org/json/JSONArray.java index 375d03888..b0c912d1f 100644 --- a/src/main/java/org/json/JSONArray.java +++ b/src/main/java/org/json/JSONArray.java @@ -924,30 +924,57 @@ public BigDecimal optBigDecimal(int index, BigDecimal defaultValue) { } /** - * Get the optional JSONArray associated with an index. + * Get the optional JSONArray associated with an index. Null is returned if + * there is no value at that index or if the value is not a JSONArray. * * @param index - * subscript - * @return A JSONArray value, or null if the index has no value, or if the - * value is not a JSONArray. + * The index must be between 0 and length() - 1. + * @return A JSONArray value. */ public JSONArray optJSONArray(int index) { - Object o = this.opt(index); - return o instanceof JSONArray ? (JSONArray) o : null; + return this.optJSONArray(index, null); + } + + /** + * Get the optional JSONArray associated with an index. The defaultValue is returned if + * there is no value at that index or if the value is not a JSONArray. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default. + * @return A JSONArray value. + */ + public JSONArray optJSONArray(int index, JSONArray defaultValue) { + Object object = this.opt(index); + return object instanceof JSONArray ? (JSONArray) object : defaultValue; } /** * Get the optional JSONObject associated with an index. Null is returned if - * the key is not found, or null if the index has no value, or if the value - * is not a JSONObject. + * there is no value at that index or if the value is not a JSONObject. * * @param index * The index must be between 0 and length() - 1. * @return A JSONObject value. */ public JSONObject optJSONObject(int index) { - Object o = this.opt(index); - return o instanceof JSONObject ? (JSONObject) o : null; + return this.optJSONObject(index, null); + } + + /** + * Get the optional JSONObject associated with an index. The defaultValue is returned if + * there is no value at that index or if the value is not a JSONObject. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default. + * @return A JSONObject value. + */ + public JSONObject optJSONObject(int index, JSONObject defaultValue) { + Object object = this.opt(index); + return object instanceof JSONObject ? (JSONObject) object : defaultValue; } /** @@ -1619,9 +1646,7 @@ public String toString() { @SuppressWarnings("resource") public String toString(int indentFactor) throws JSONException { StringWriter sw = new StringWriter(); - synchronized (sw.getBuffer()) { - return this.write(sw, indentFactor, 0).toString(); - } + return this.write(sw, indentFactor, 0).toString(); } /** diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index 5e00eb9a3..fbf225e9f 100644 --- a/src/main/java/org/json/JSONObject.java +++ b/src/main/java/org/json/JSONObject.java @@ -208,22 +208,14 @@ public JSONObject(JSONTokener x) throws JSONException { throw x.syntaxError("A JSONObject text must begin with '{'"); } for (;;) { - char prev = x.getPrevious(); c = x.nextClean(); switch (c) { case 0: throw x.syntaxError("A JSONObject text must end with '}'"); case '}': return; - case '{': - case '[': - if(prev=='{') { - throw x.syntaxError("A JSON Object can not directly nest another JSON Object or JSON Array."); - } - // fall through default: - x.back(); - key = x.nextValue().toString(); + key = x.nextSimpleValue(c).toString(); } // The key is followed by ':'. @@ -291,6 +283,7 @@ public JSONObject(Map m) { } final Object value = e.getValue(); if (value != null) { + testValidity(value); this.map.put(String.valueOf(e.getKey()), wrap(value)); } } @@ -354,6 +347,8 @@ public JSONObject(Map m) { * @param bean * An object that has getter methods that should be used to make * a JSONObject. + * @throws JSONException + * If a getter returned a non-finite number. */ public JSONObject(Object bean) { this(); @@ -1512,8 +1507,22 @@ public Integer optIntegerObject(String key, Integer defaultValue) { * @return A JSONArray which is the value. */ public JSONArray optJSONArray(String key) { - Object o = this.opt(key); - return o instanceof JSONArray ? (JSONArray) o : null; + return this.optJSONArray(key, null); + } + + /** + * Get an optional JSONArray associated with a key, or the default if there + * is no such key, or if its value is not a JSONArray. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return A JSONArray which is the value. + */ + public JSONArray optJSONArray(String key, JSONArray defaultValue) { + Object object = this.opt(key); + return object instanceof JSONArray ? (JSONArray) object : defaultValue; } /** @@ -1685,6 +1694,8 @@ public String optString(String key, String defaultValue) { * * @param bean * the bean + * @throws JSONException + * If a getter returned a non-finite number. */ private void populateMap(Object bean) { populateMap(bean, Collections.newSetFromMap(new IdentityHashMap())); @@ -1712,21 +1723,22 @@ && isValidMethodName(method.getName())) { final Object result = method.invoke(bean); if (result != null) { // check cyclic dependency and throw error if needed - // the wrap and populateMap combination method is + // the wrap and populateMap combination method is // itself DFS recursive if (objectsRecord.contains(result)) { throw recursivelyDefinedObjectException(key); } - + objectsRecord.add(result); + testValidity(result); this.map.put(key, wrap(result, objectsRecord)); objectsRecord.remove(result); // we don't use the result anywhere outside of wrap // if it's a resource we should be sure to close it - // after calling toString + // after calling toString if (result instanceof Closeable) { try { ((Closeable) result).close(); @@ -2177,13 +2189,11 @@ public Object optQuery(JSONPointer jsonPointer) { @SuppressWarnings("resource") public static String quote(String string) { StringWriter sw = new StringWriter(); - synchronized (sw.getBuffer()) { - try { - return quote(string, sw).toString(); - } catch (IOException ignored) { - // will never happen - we are writing to a string writer - return ""; - } + try { + return quote(string, sw).toString(); + } catch (IOException ignored) { + // will never happen - we are writing to a string writer + return ""; } } @@ -2375,14 +2385,21 @@ protected static boolean isDecimalNotation(final String val) { * returns for this function are BigDecimal, Double, BigInteger, Long, and Integer. * When a Double is returned, it should always be a valid Double and not NaN or +-infinity. * - * @param val value to convert + * @param input value to convert * @return Number representation of the value. * @throws NumberFormatException thrown if the value is not a valid number. A public * caller should catch this and wrap it in a {@link JSONException} if applicable. */ - protected static Number stringToNumber(final String val) throws NumberFormatException { + protected static Number stringToNumber(final String input) throws NumberFormatException { + String val = input; + if (val.startsWith(".")){ + val = "0"+val; + } + if (val.startsWith("-.")){ + val = "-0."+val.substring(2); + } char initial = val.charAt(0); - if ((initial >= '0' && initial <= '9') || initial == '-') { + if ((initial >= '0' && initial <= '9') || initial == '-' ) { // decimal representation if (isDecimalNotation(val)) { // Use a BigDecimal all the time so we keep the original @@ -2399,25 +2416,26 @@ protected static Number stringToNumber(final String val) throws NumberFormatExce try { Double d = Double.valueOf(val); if(d.isNaN() || d.isInfinite()) { - throw new NumberFormatException("val ["+val+"] is not a valid number."); + throw new NumberFormatException("val ["+input+"] is not a valid number."); } return d; } catch (NumberFormatException ignore) { - throw new NumberFormatException("val ["+val+"] is not a valid number."); + throw new NumberFormatException("val ["+input+"] is not a valid number."); } } } - // block items like 00 01 etc. Java number parsers treat these as Octal. + val = removeLeadingZerosOfNumber(input); + initial = val.charAt(0); if(initial == '0' && val.length() > 1) { char at1 = val.charAt(1); if(at1 >= '0' && at1 <= '9') { - throw new NumberFormatException("val ["+val+"] is not a valid number."); + throw new NumberFormatException("val ["+input+"] is not a valid number."); } } else if (initial == '-' && val.length() > 2) { char at1 = val.charAt(1); char at2 = val.charAt(2); if(at1 == '0' && at2 >= '0' && at2 <= '9') { - throw new NumberFormatException("val ["+val+"] is not a valid number."); + throw new NumberFormatException("val ["+input+"] is not a valid number."); } } // integer representation. @@ -2437,7 +2455,7 @@ protected static Number stringToNumber(final String val) throws NumberFormatExce } return bi; } - throw new NumberFormatException("val ["+val+"] is not a valid number."); + throw new NumberFormatException("val ["+input+"] is not a valid number."); } /** @@ -2473,8 +2491,7 @@ public static Object stringToValue(String string) { * produced, then the value will just be a string. */ - char initial = string.charAt(0); - if ((initial >= '0' && initial <= '9') || initial == '-') { + if (potentialNumber(string)) { try { return stringToNumber(string); } catch (Exception ignore) { @@ -2483,6 +2500,28 @@ public static Object stringToValue(String string) { return string; } + + private static boolean potentialNumber(String value){ + if (value == null || value.isEmpty()){ + return false; + } + return potentialPositiveNumberStartingAtIndex(value, (value.charAt(0)=='-'?1:0)); + } + + private static boolean potentialPositiveNumberStartingAtIndex(String value,int index){ + if (index >= value.length()){ + return false; + } + return digitAtIndex(value, (value.charAt(index)=='.'?index+1:index)); + } + + private static boolean digitAtIndex(String value, int index){ + if (index >= value.length()){ + return false; + } + return value.charAt(index) >= '0' && value.charAt(index) <= '9'; + } + /** * Throw an exception if the object is a NaN or infinite number. * @@ -2570,9 +2609,7 @@ public String toString() { @SuppressWarnings("resource") public String toString(int indentFactor) throws JSONException { StringWriter w = new StringWriter(); - synchronized (w.getBuffer()) { - return this.write(w, indentFactor, 0).toString(); - } + return this.write(w, indentFactor, 0).toString(); } /** @@ -2884,4 +2921,24 @@ private static JSONException recursivelyDefinedObjectException(String key) { "JavaBean object contains recursively defined member variable of key " + quote(key) ); } + + /** + * For a prospective number, remove the leading zeros + * @param value prospective number + * @return number without leading zeros + */ + private static String removeLeadingZerosOfNumber(String value){ + if (value.equals("-")){return value;} + boolean negativeFirstChar = (value.charAt(0) == '-'); + int counter = negativeFirstChar ? 1:0; + while (counter < value.length()){ + if (value.charAt(counter) != '0'){ + if (negativeFirstChar) {return "-".concat(value.substring(counter));} + return value.substring(counter); + } + ++counter; + } + if (negativeFirstChar) {return "-0";} + return "0"; + } } diff --git a/src/main/java/org/json/JSONTokener.java b/src/main/java/org/json/JSONTokener.java index 5dc8ae85a..4a83a6971 100644 --- a/src/main/java/org/json/JSONTokener.java +++ b/src/main/java/org/json/JSONTokener.java @@ -402,12 +402,7 @@ public String nextTo(String delimiters) throws JSONException { */ public Object nextValue() throws JSONException { char c = this.nextClean(); - String string; - switch (c) { - case '"': - case '\'': - return this.nextString(c); case '{': this.back(); try { @@ -423,6 +418,17 @@ public Object nextValue() throws JSONException { throw new JSONException("JSON Array or Object depth too large to process.", e); } } + return nextSimpleValue(c); + } + + Object nextSimpleValue(char c) { + String string; + + switch (c) { + case '"': + case '\'': + return this.nextString(c); + } /* * Handle unquoted text. This could be the values true, false, or diff --git a/src/test/java/org/json/junit/JSONArrayTest.java b/src/test/java/org/json/junit/JSONArrayTest.java index ad938cf50..e877070d0 100644 --- a/src/test/java/org/json/junit/JSONArrayTest.java +++ b/src/test/java/org/json/junit/JSONArrayTest.java @@ -118,7 +118,7 @@ public void nullException() { * Expects a JSONException. */ @Test - public void emptStr() { + public void emptyStr() { String str = ""; try { assertNull("Should throw an exception", new JSONArray(str)); @@ -460,6 +460,20 @@ public void failedGetArrayValues() { Util.checkJSONArrayMaps(jsonArray); } + /** + * The JSON parser is permissive of unambiguous unquoted keys and values. + * Such JSON text should be allowed, even if it does not strictly conform + * to the spec. However, after being parsed, toString() should emit strictly + * conforming JSON text. + */ + @Test + public void unquotedText() { + String str = "[value1, something!, (parens), foo@bar.com, 23, 23+45]"; + JSONArray jsonArray = new JSONArray(str); + List expected = Arrays.asList("value1", "something!", "(parens)", "foo@bar.com", 23, "23+45"); + assertEquals(expected, jsonArray.toList()); + } + /** * Exercise JSONArray.join() by converting a JSONArray into a * comma-separated string. Since this is very nearly a JSON document, @@ -595,13 +609,17 @@ public void opt() { JSONArray nestedJsonArray = jsonArray.optJSONArray(9); assertTrue("Array opt JSONArray", nestedJsonArray != null); - assertTrue("Array opt JSONArray default", + assertTrue("Array opt JSONArray null", null == jsonArray.optJSONArray(99)); + assertTrue("Array opt JSONArray default", + "value".equals(jsonArray.optJSONArray(99, new JSONArray("[\"value\"]")).getString(0))); JSONObject nestedJsonObject = jsonArray.optJSONObject(10); assertTrue("Array opt JSONObject", nestedJsonObject != null); - assertTrue("Array opt JSONObject default", + assertTrue("Array opt JSONObject null", null == jsonArray.optJSONObject(99)); + assertTrue("Array opt JSONObject default", + "value".equals(jsonArray.optJSONObject(99, new JSONObject("{\"key\":\"value\"}")).getString("key"))); assertTrue("Array opt long", 0 == jsonArray.optLong(11)); @@ -1369,7 +1387,7 @@ public void jsonArrayClearMethodTest() { @Test(expected = JSONException.class) public void issue654StackOverflowInputWellFormed() { //String input = new String(java.util.Base64.getDecoder().decode(base64Bytes)); - final InputStream resourceAsStream = JSONObjectTest.class.getClassLoader().getResourceAsStream("Issue654WellFormedArray.json"); + final InputStream resourceAsStream = JSONArrayTest.class.getClassLoader().getResourceAsStream("Issue654WellFormedArray.json"); JSONTokener tokener = new JSONTokener(resourceAsStream); JSONArray json_input = new JSONArray(tokener); assertNotNull(json_input); diff --git a/src/test/java/org/json/junit/JSONObjectDecimalTest.java b/src/test/java/org/json/junit/JSONObjectDecimalTest.java new file mode 100644 index 000000000..3302f0a24 --- /dev/null +++ b/src/test/java/org/json/junit/JSONObjectDecimalTest.java @@ -0,0 +1,100 @@ +package org.json.junit; + +import org.json.JSONObject; +import org.junit.Test; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import static org.junit.Assert.assertEquals; + +public class JSONObjectDecimalTest { + + @Test + public void shouldParseDecimalNumberThatStartsWithDecimalPoint(){ + JSONObject jsonObject = new JSONObject("{value:0.50}"); + assertEquals("Float not recognized", 0.5f, jsonObject.getFloat("value"), 0.0f); + assertEquals("Float not recognized", 0.5f, jsonObject.optFloat("value"), 0.0f); + assertEquals("Float not recognized", 0.5f, jsonObject.optFloatObject("value"), 0.0f); + assertEquals("Double not recognized", 0.5d, jsonObject.optDouble("value"), 0.0f); + assertEquals("Double not recognized", 0.5d, jsonObject.optDoubleObject("value"), 0.0f); + assertEquals("Double not recognized", 0.5d, jsonObject.getDouble("value"), 0.0f); + assertEquals("Long not recognized", 0, jsonObject.optLong("value"), 0); + assertEquals("Long not recognized", 0, jsonObject.getLong("value"), 0); + assertEquals("Long not recognized", 0, jsonObject.optLongObject("value"), 0); + assertEquals("Integer not recognized", 0, jsonObject.optInt("value"), 0); + assertEquals("Integer not recognized", 0, jsonObject.getInt("value"), 0); + assertEquals("Integer not recognized", 0, jsonObject.optIntegerObject("value"), 0); + assertEquals("Number not recognized", 0, jsonObject.getNumber("value").intValue(), 0); + assertEquals("Number not recognized", 0, jsonObject.getNumber("value").longValue(), 0); + assertEquals("BigDecimal not recognized", 0, BigDecimal.valueOf(.5).compareTo(jsonObject.getBigDecimal("value"))); + assertEquals("BigInteger not recognized",0, BigInteger.valueOf(0).compareTo(jsonObject.getBigInteger("value"))); + } + + + + @Test + public void shouldParseNegativeDecimalNumberThatStartsWithDecimalPoint(){ + JSONObject jsonObject = new JSONObject("{value:-.50}"); + assertEquals("Float not recognized", -0.5f, jsonObject.getFloat("value"), 0.0f); + assertEquals("Float not recognized", -0.5f, jsonObject.optFloat("value"), 0.0f); + assertEquals("Float not recognized", -0.5f, jsonObject.optFloatObject("value"), 0.0f); + assertEquals("Double not recognized", -0.5d, jsonObject.optDouble("value"), 0.0f); + assertEquals("Double not recognized", -0.5d, jsonObject.optDoubleObject("value"), 0.0f); + assertEquals("Double not recognized", -0.5d, jsonObject.getDouble("value"), 0.0f); + assertEquals("Long not recognized", 0, jsonObject.optLong("value"), 0); + assertEquals("Long not recognized", 0, jsonObject.getLong("value"), 0); + assertEquals("Long not recognized", 0, jsonObject.optLongObject("value"), 0); + assertEquals("Integer not recognized", 0, jsonObject.optInt("value"), 0); + assertEquals("Integer not recognized", 0, jsonObject.getInt("value"), 0); + assertEquals("Integer not recognized", 0, jsonObject.optIntegerObject("value"), 0); + assertEquals("Number not recognized", 0, jsonObject.getNumber("value").intValue(), 0); + assertEquals("Number not recognized", 0, jsonObject.getNumber("value").longValue(), 0); + assertEquals("BigDecimal not recognized", 0, BigDecimal.valueOf(-.5).compareTo(jsonObject.getBigDecimal("value"))); + assertEquals("BigInteger not recognized",0, BigInteger.valueOf(0).compareTo(jsonObject.getBigInteger("value"))); + } + + @Test + public void shouldParseDecimalNumberThatHasZeroBeforeWithDecimalPoint(){ + JSONObject jsonObject = new JSONObject("{value:00.050}"); + assertEquals("Float not recognized", 0.05f, jsonObject.getFloat("value"), 0.0f); + assertEquals("Float not recognized", 0.05f, jsonObject.optFloat("value"), 0.0f); + assertEquals("Float not recognized", 0.05f, jsonObject.optFloatObject("value"), 0.0f); + assertEquals("Double not recognized", 0.05d, jsonObject.optDouble("value"), 0.0f); + assertEquals("Double not recognized", 0.05d, jsonObject.optDoubleObject("value"), 0.0f); + assertEquals("Double not recognized", 0.05d, jsonObject.getDouble("value"), 0.0f); + assertEquals("Long not recognized", 0, jsonObject.optLong("value"), 0); + assertEquals("Long not recognized", 0, jsonObject.getLong("value"), 0); + assertEquals("Long not recognized", 0, jsonObject.optLongObject("value"), 0); + assertEquals("Integer not recognized", 0, jsonObject.optInt("value"), 0); + assertEquals("Integer not recognized", 0, jsonObject.getInt("value"), 0); + assertEquals("Integer not recognized", 0, jsonObject.optIntegerObject("value"), 0); + assertEquals("Number not recognized", 0, jsonObject.getNumber("value").intValue(), 0); + assertEquals("Number not recognized", 0, jsonObject.getNumber("value").longValue(), 0); + assertEquals("BigDecimal not recognized", 0, BigDecimal.valueOf(.05).compareTo(jsonObject.getBigDecimal("value"))); + assertEquals("BigInteger not recognized",0, BigInteger.valueOf(0).compareTo(jsonObject.getBigInteger("value"))); + } + + @Test + public void shouldParseNegativeDecimalNumberThatHasZeroBeforeWithDecimalPoint(){ + JSONObject jsonObject = new JSONObject("{value:-00.050}"); + assertEquals("Float not recognized", -0.05f, jsonObject.getFloat("value"), 0.0f); + assertEquals("Float not recognized", -0.05f, jsonObject.optFloat("value"), 0.0f); + assertEquals("Float not recognized", -0.05f, jsonObject.optFloatObject("value"), 0.0f); + assertEquals("Double not recognized", -0.05d, jsonObject.optDouble("value"), 0.0f); + assertEquals("Double not recognized", -0.05d, jsonObject.optDoubleObject("value"), 0.0f); + assertEquals("Double not recognized", -0.05d, jsonObject.getDouble("value"), 0.0f); + assertEquals("Long not recognized", 0, jsonObject.optLong("value"), 0); + assertEquals("Long not recognized", 0, jsonObject.getLong("value"), 0); + assertEquals("Long not recognized", 0, jsonObject.optLongObject("value"), 0); + assertEquals("Integer not recognized", 0, jsonObject.optInt("value"), 0); + assertEquals("Integer not recognized", 0, jsonObject.getInt("value"), 0); + assertEquals("Integer not recognized", 0, jsonObject.optIntegerObject("value"), 0); + assertEquals("Number not recognized", 0, jsonObject.getNumber("value").intValue(), 0); + assertEquals("Number not recognized", 0, jsonObject.getNumber("value").longValue(), 0); + assertEquals("BigDecimal not recognized", 0, BigDecimal.valueOf(-.05).compareTo(jsonObject.getBigDecimal("value"))); + assertEquals("BigInteger not recognized",0, BigInteger.valueOf(0).compareTo(jsonObject.getBigInteger("value"))); + } + + +} diff --git a/src/test/java/org/json/junit/JSONObjectNumberTest.java b/src/test/java/org/json/junit/JSONObjectNumberTest.java index 43173a288..14e68d66b 100644 --- a/src/test/java/org/json/junit/JSONObjectNumberTest.java +++ b/src/test/java/org/json/junit/JSONObjectNumberTest.java @@ -23,7 +23,10 @@ public class JSONObjectNumberTest { @Parameters(name = "{index}: {0}") public static Collection data() { return Arrays.asList(new Object[][]{ - {"{value:50}", 1}, + {"{value:0050}", 1}, + {"{value:0050.0000}", 1}, + {"{value:-0050}", -1}, + {"{value:-0050.0000}", -1}, {"{value:50.0}", 1}, {"{value:5e1}", 1}, {"{value:5E1}", 1}, @@ -32,6 +35,7 @@ public static Collection data() { {"{value:-50}", -1}, {"{value:-50.0}", -1}, {"{value:-5e1}", -1}, + {"{value:-0005e1}", -1}, {"{value:-5E1}", -1}, {"{value:-5e1}", -1}, {"{value:'-50'}", -1} diff --git a/src/test/java/org/json/junit/JSONObjectTest.java b/src/test/java/org/json/junit/JSONObjectTest.java index 2de8f815c..5adb6ce51 100644 --- a/src/test/java/org/json/junit/JSONObjectTest.java +++ b/src/test/java/org/json/junit/JSONObjectTest.java @@ -4,11 +4,13 @@ Public Domain. */ +import static java.lang.Double.NaN; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; @@ -205,13 +207,17 @@ public void jsonObjectByNullBean() { */ @Test public void unquotedText() { - String str = "{key1:value1, key2:42}"; + String str = "{key1:value1, key2:42, 1.2 : 3.4, -7e5 : something!}"; JSONObject jsonObject = new JSONObject(str); String textStr = jsonObject.toString(); assertTrue("expected key1", textStr.contains("\"key1\"")); assertTrue("expected value1", textStr.contains("\"value1\"")); assertTrue("expected key2", textStr.contains("\"key2\"")); assertTrue("expected 42", textStr.contains("42")); + assertTrue("expected 1.2", textStr.contains("\"1.2\"")); + assertTrue("expected 3.4", textStr.contains("3.4")); + assertTrue("expected -7E+5", textStr.contains("\"-7E+5\"")); + assertTrue("expected something!", textStr.contains("\"something!\"")); Util.checkJSONObjectMaps(jsonObject); } @@ -777,7 +783,7 @@ public void jsonObjectAccumulate() { jsonObject.accumulate("myArray", -23.45e7); // include an unsupported object for coverage try { - jsonObject.accumulate("myArray", Double.NaN); + jsonObject.accumulate("myArray", NaN); fail("Expected exception"); } catch (JSONException ignored) {} @@ -809,7 +815,7 @@ public void jsonObjectAppend() { jsonObject.append("myArray", -23.45e7); // include an unsupported object for coverage try { - jsonObject.append("myArray", Double.NaN); + jsonObject.append("myArray", NaN); fail("Expected exception"); } catch (JSONException ignored) {} @@ -834,7 +840,7 @@ public void jsonObjectAppend() { public void jsonObjectDoubleToString() { String [] expectedStrs = {"1", "1", "-23.4", "-2.345E68", "null", "null" }; Double [] doubles = { 1.0, 00001.00000, -23.4, -23.45e67, - Double.NaN, Double.NEGATIVE_INFINITY }; + NaN, Double.NEGATIVE_INFINITY }; for (int i = 0; i < expectedStrs.length; ++i) { String actualStr = JSONObject.doubleToString(doubles[i]); assertTrue("value expected ["+expectedStrs[i]+ @@ -889,11 +895,11 @@ public void jsonObjectValues() { assertTrue("opt doubleKey should be double", jsonObject.optDouble("doubleKey") == -23.45e7); assertTrue("opt doubleKey with Default should be double", - jsonObject.optDouble("doubleStrKey", Double.NaN) == 1); + jsonObject.optDouble("doubleStrKey", NaN) == 1); assertTrue("opt doubleKey should be Double", Double.valueOf(-23.45e7).equals(jsonObject.optDoubleObject("doubleKey"))); assertTrue("opt doubleKey with Default should be Double", - Double.valueOf(1).equals(jsonObject.optDoubleObject("doubleStrKey", Double.NaN))); + Double.valueOf(1).equals(jsonObject.optDoubleObject("doubleStrKey", NaN))); assertTrue("opt negZeroKey should be a Double", jsonObject.opt("negZeroKey") instanceof Double); assertTrue("get negZeroKey should be a Double", @@ -1059,12 +1065,21 @@ public void jsonInvalidNumberValues() { "\"tooManyZeros\":00,"+ "\"negativeInfinite\":-Infinity,"+ "\"negativeNaN\":-NaN,"+ + "\"negativeNaNWithLeadingZeros\":-00NaN,"+ "\"negativeFraction\":-.01,"+ "\"tooManyZerosFraction\":00.001,"+ "\"negativeHexFloat\":-0x1.fffp1,"+ "\"hexFloat\":0x1.0P-1074,"+ "\"floatIdentifier\":0.1f,"+ - "\"doubleIdentifier\":0.1d"+ + "\"doubleIdentifier\":0.1d,"+ + "\"doubleIdentifierWithMultipleLeadingZerosBeforeDecimal\":0000000.1d,"+ + "\"negativeDoubleIdentifierWithMultipleLeadingZerosBeforeDecimal\":-0000000.1d,"+ + "\"doubleIdentifierWithMultipleLeadingZerosAfterDecimal\":0000000.0001d,"+ + "\"negativeDoubleIdentifierWithMultipleLeadingZerosAfterDecimal\":-0000000.0001d,"+ + "\"integerWithLeadingZeros\":000900,"+ + "\"integerWithAllZeros\":00000,"+ + "\"compositeWithLeadingZeros\":00800.90d,"+ + "\"decimalPositiveWithoutNumberBeforeDecimalPoint\":.90,"+ "}"; JSONObject jsonObject = new JSONObject(str); Object obj; @@ -1074,17 +1089,24 @@ public void jsonInvalidNumberValues() { assertTrue("hexNumber currently evaluates to string", obj.equals("-0x123")); assertTrue( "tooManyZeros currently evaluates to string", - jsonObject.get( "tooManyZeros" ).equals("00")); + jsonObject.get( "tooManyZeros" ).equals(0)); obj = jsonObject.get("negativeInfinite"); assertTrue( "negativeInfinite currently evaluates to string", obj.equals("-Infinity")); obj = jsonObject.get("negativeNaN"); assertTrue( "negativeNaN currently evaluates to string", obj.equals("-NaN")); + obj = jsonObject.get("negativeNaNWithLeadingZeros"); + assertTrue( "negativeNaNWithLeadingZeros currently evaluates to string", + obj.equals("-00NaN")); assertTrue( "negativeFraction currently evaluates to double -0.01", jsonObject.get( "negativeFraction" ).equals(BigDecimal.valueOf(-0.01))); assertTrue( "tooManyZerosFraction currently evaluates to double 0.001", jsonObject.get( "tooManyZerosFraction" ).equals(BigDecimal.valueOf(0.001))); + assertTrue( "tooManyZerosFraction currently evaluates to double 0.001", + jsonObject.getLong( "tooManyZerosFraction" )==0); + assertTrue( "tooManyZerosFraction currently evaluates to double 0.001", + jsonObject.optLong( "tooManyZerosFraction" )==0); assertTrue( "negativeHexFloat currently evaluates to double -3.99951171875", jsonObject.get( "negativeHexFloat" ).equals(Double.valueOf(-3.99951171875))); assertTrue("hexFloat currently evaluates to double 4.9E-324", @@ -1093,6 +1115,53 @@ public void jsonInvalidNumberValues() { jsonObject.get("floatIdentifier").equals(Double.valueOf(0.1))); assertTrue("doubleIdentifier currently evaluates to double 0.1", jsonObject.get("doubleIdentifier").equals(Double.valueOf(0.1))); + assertTrue("doubleIdentifierWithMultipleLeadingZerosBeforeDecimal currently evaluates to double 0.1", + jsonObject.get("doubleIdentifierWithMultipleLeadingZerosBeforeDecimal").equals(Double.valueOf(0.1))); + assertTrue("negativeDoubleIdentifierWithMultipleLeadingZerosBeforeDecimal currently evaluates to double -0.1", + jsonObject.get("negativeDoubleIdentifierWithMultipleLeadingZerosBeforeDecimal").equals(Double.valueOf(-0.1))); + assertTrue("doubleIdentifierWithMultipleLeadingZerosAfterDecimal currently evaluates to double 0.0001", + jsonObject.get("doubleIdentifierWithMultipleLeadingZerosAfterDecimal").equals(Double.valueOf(0.0001))); + assertTrue("doubleIdentifierWithMultipleLeadingZerosAfterDecimal currently evaluates to double 0.0001", + jsonObject.get("doubleIdentifierWithMultipleLeadingZerosAfterDecimal").equals(Double.valueOf(0.0001))); + assertTrue("negativeDoubleIdentifierWithMultipleLeadingZerosAfterDecimal currently evaluates to double -0.0001", + jsonObject.get("negativeDoubleIdentifierWithMultipleLeadingZerosAfterDecimal").equals(Double.valueOf(-0.0001))); + assertTrue("Integer does not evaluate to 900", + jsonObject.get("integerWithLeadingZeros").equals(900)); + assertTrue("Integer does not evaluate to 900", + jsonObject.getInt("integerWithLeadingZeros")==900); + assertTrue("Integer does not evaluate to 900", + jsonObject.optInt("integerWithLeadingZeros")==900); + assertTrue("Integer does not evaluate to 0", + jsonObject.get("integerWithAllZeros").equals(0)); + assertTrue("Integer does not evaluate to 0", + jsonObject.getInt("integerWithAllZeros")==0); + assertTrue("Integer does not evaluate to 0", + jsonObject.optInt("integerWithAllZeros")==0); + assertTrue("Double does not evaluate to 800.90", + jsonObject.get("compositeWithLeadingZeros").equals(800.90)); + assertTrue("Double does not evaluate to 800.90", + jsonObject.getDouble("compositeWithLeadingZeros")==800.9d); + assertTrue("Integer does not evaluate to 800", + jsonObject.optInt("compositeWithLeadingZeros")==800); + assertTrue("Long does not evaluate to 800.90", + jsonObject.getLong("compositeWithLeadingZeros")==800); + assertTrue("Long does not evaluate to 800.90", + jsonObject.optLong("compositeWithLeadingZeros")==800); + assertEquals("Get long of decimalPositiveWithoutNumberBeforeDecimalPoint does not match", + 0.9d,jsonObject.getDouble("decimalPositiveWithoutNumberBeforeDecimalPoint"), 0.0d); + assertEquals("Get long of decimalPositiveWithoutNumberBeforeDecimalPoint does not match", + 0.9d,jsonObject.optDouble("decimalPositiveWithoutNumberBeforeDecimalPoint"), 0.0d); + assertEquals("Get long of decimalPositiveWithoutNumberBeforeDecimalPoint does not match", + 0.0d,jsonObject.optLong("decimalPositiveWithoutNumberBeforeDecimalPoint"), 0.0d); + + assertEquals("Get long of doubleIdentifierWithMultipleLeadingZerosAfterDecimal does not match", + 0.0001d,jsonObject.getDouble("doubleIdentifierWithMultipleLeadingZerosAfterDecimal"), 0.0d); + assertEquals("Get long of doubleIdentifierWithMultipleLeadingZerosAfterDecimal does not match", + 0.0001d,jsonObject.optDouble("doubleIdentifierWithMultipleLeadingZerosAfterDecimal"), 0.0d); + assertEquals("Get long of doubleIdentifierWithMultipleLeadingZerosAfterDecimal does not match", + 0.0d, jsonObject.getLong("doubleIdentifierWithMultipleLeadingZerosAfterDecimal") , 0.0d); + assertEquals("Get long of doubleIdentifierWithMultipleLeadingZerosAfterDecimal does not match", + 0.0d,jsonObject.optLong("doubleIdentifierWithMultipleLeadingZerosAfterDecimal"), 0.0d); Util.checkJSONObjectMaps(jsonObject); } @@ -1968,7 +2037,7 @@ public void jsonObjectToStringIndent() { @Test public void jsonObjectToStringSuppressWarningOnCastToMap() { JSONObject jsonObject = new JSONObject(); - Map map = new HashMap(); + Map map = new HashMap<>(); map.put("abc", "def"); jsonObject.put("key", map); @@ -2224,6 +2293,42 @@ public void jsonObjectParsingErrors() { "Expected a ',' or '}' at 15 [character 16 line 1]", e.getMessage()); } + try { + // key is a nested map + String str = "{{\"foo\": \"bar\"}: \"baz\"}"; + assertNull("Expected an exception",new JSONObject(str)); + } catch (JSONException e) { + assertEquals("Expecting an exception message", + "Missing value at 1 [character 2 line 1]", + e.getMessage()); + } + try { + // key is a nested array containing a map + String str = "{\"a\": 1, [{\"foo\": \"bar\"}]: \"baz\"}"; + assertNull("Expected an exception",new JSONObject(str)); + } catch (JSONException e) { + assertEquals("Expecting an exception message", + "Missing value at 9 [character 10 line 1]", + e.getMessage()); + } + try { + // key contains } + String str = "{foo}: 2}"; + assertNull("Expected an exception",new JSONObject(str)); + } catch (JSONException e) { + assertEquals("Expecting an exception message", + "Expected a ':' after a key at 5 [character 6 line 1]", + e.getMessage()); + } + try { + // key contains ] + String str = "{foo]: 2}"; + assertNull("Expected an exception",new JSONObject(str)); + } catch (JSONException e) { + assertEquals("Expecting an exception message", + "Expected a ':' after a key at 5 [character 6 line 1]", + e.getMessage()); + } try { // \0 after , String str = "{\"myKey\":true, \0\"myOtherKey\":false}"; @@ -2287,7 +2392,7 @@ public void jsonObjectParsingErrors() { } try { // test validity of invalid double - JSONObject.testValidity(Double.NaN); + JSONObject.testValidity(NaN); fail("Expected an exception"); } catch (JSONException e) { assertTrue("", true); @@ -2510,6 +2615,8 @@ public void jsonObjectOptDefault() { MyEnum.VAL1.equals(jsonObject.optEnum(MyEnum.class, "myKey", MyEnum.VAL1))); assertTrue("optJSONArray() should return null ", null==jsonObject.optJSONArray("myKey")); + assertTrue("optJSONArray() should return default JSONArray", + "value".equals(jsonObject.optJSONArray("myKey", new JSONArray("[\"value\"]")).getString(0))); assertTrue("optJSONObject() should return default JSONObject ", jsonObject.optJSONObject("myKey", new JSONObject("{\"testKey\":\"testValue\"}")).getString("testKey").equals("testValue")); assertTrue("optLong() should return default long", @@ -2555,6 +2662,8 @@ public void jsonObjectOptNoKey() { Integer.valueOf(42).equals(jsonObject.optIntegerObject("myKey", 42))); assertTrue("optEnum() should return default Enum", MyEnum.VAL1.equals(jsonObject.optEnum(MyEnum.class, "myKey", MyEnum.VAL1))); + assertTrue("optJSONArray() should return default JSONArray", + "value".equals(jsonObject.optJSONArray("myKey", new JSONArray("[\"value\"]")).getString(0))); assertTrue("optJSONArray() should return null ", null==jsonObject.optJSONArray("myKey")); assertTrue("optJSONObject() should return default JSONObject ", @@ -3239,7 +3348,7 @@ public void testSingletonEnumBean() { @SuppressWarnings("boxing") @Test public void testGenericBean() { - GenericBean bean = new GenericBean(42); + GenericBean bean = new GenericBean<>(42); final JSONObject jo = new JSONObject(bean); assertEquals(jo.keySet().toString(), 8, jo.length()); assertEquals(42, jo.get("genericValue")); @@ -3288,6 +3397,7 @@ public void testWierdListBean() { * Sample test case from https://github.com/stleary/JSON-java/issues/531 * which verifies that no regression in double/BigDecimal support is present. */ + @Test public void testObjectToBigDecimal() { double value = 1412078745.01074; Reader reader = new StringReader("[{\"value\": " + value + "}]"); @@ -3582,4 +3692,25 @@ public String toJSONString() { .put("b", 2); assertFalse(jo1.similar(jo3)); } + + private static final Number[] NON_FINITE_NUMBERS = { Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NaN, + Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.NaN }; + + @Test + public void issue713MapConstructorWithNonFiniteNumbers() { + for (Number nonFinite : NON_FINITE_NUMBERS) { + Map map = new HashMap<>(); + map.put("a", nonFinite); + + assertThrows(JSONException.class, () -> new JSONObject(map)); + } + } + + @Test + public void issue713BeanConstructorWithNonFiniteNumbers() { + for (Number nonFinite : NON_FINITE_NUMBERS) { + GenericBean bean = new GenericBean<>(nonFinite); + assertThrows(JSONException.class, () -> new JSONObject(bean)); + } + } } diff --git a/src/test/java/org/json/junit/JsonNumberZeroTest.java b/src/test/java/org/json/junit/JsonNumberZeroTest.java new file mode 100644 index 000000000..bfa4ca9d8 --- /dev/null +++ b/src/test/java/org/json/junit/JsonNumberZeroTest.java @@ -0,0 +1,55 @@ +package org.json.junit; + +import org.json.JSONObject; +import org.junit.Test; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import static org.junit.Assert.assertEquals; + +public class JsonNumberZeroTest { + + @Test + public void shouldParseNegativeZeroValueWithMultipleZeroDigit(){ + JSONObject jsonObject = new JSONObject("{value:-0000}"); + assertEquals("Float not recognized", -0f, jsonObject.getFloat("value"), 0.0f); + assertEquals("Float not recognized", -0f, jsonObject.optFloat("value"), 0.0f); + assertEquals("Float not recognized", -0f, jsonObject.optFloatObject("value"), 0.0f); + assertEquals("Double not recognized", -0d, jsonObject.optDouble("value"), 0.0f); + assertEquals("Double not recognized", -0.0d, jsonObject.optDoubleObject("value"), 0.0f); + assertEquals("Double not recognized", -0.0d, jsonObject.getDouble("value"), 0.0f); + assertEquals("Long not recognized", 0, jsonObject.optLong("value"), 0); + assertEquals("Long not recognized", 0, jsonObject.getLong("value"), 0); + assertEquals("Long not recognized", 0, jsonObject.optLongObject("value"), 0); + assertEquals("Integer not recognized", 0, jsonObject.optInt("value"), 0); + assertEquals("Integer not recognized", 0, jsonObject.getInt("value"), 0); + assertEquals("Integer not recognized", 0, jsonObject.optIntegerObject("value"), 0); + assertEquals("Number not recognized", 0, jsonObject.getNumber("value").intValue(), 0); + assertEquals("Number not recognized", 0, jsonObject.getNumber("value").longValue(), 0); + assertEquals("BigDecimal not recognized", 0, BigDecimal.valueOf(-0).compareTo(jsonObject.getBigDecimal("value"))); + assertEquals("BigInteger not recognized",0, BigInteger.valueOf(0).compareTo(jsonObject.getBigInteger("value"))); + } + + @Test + public void shouldParseZeroValueWithMultipleZeroDigit(){ + JSONObject jsonObject = new JSONObject("{value:0000}"); + assertEquals("Float not recognized", 0f, jsonObject.getFloat("value"), 0.0f); + assertEquals("Float not recognized", 0f, jsonObject.optFloat("value"), 0.0f); + assertEquals("Float not recognized", 0f, jsonObject.optFloatObject("value"), 0.0f); + assertEquals("Double not recognized", 0d, jsonObject.optDouble("value"), 0.0f); + assertEquals("Double not recognized", 0.0d, jsonObject.optDoubleObject("value"), 0.0f); + assertEquals("Double not recognized", 0.0d, jsonObject.getDouble("value"), 0.0f); + assertEquals("Long not recognized", 0, jsonObject.optLong("value"), 0); + assertEquals("Long not recognized", 0, jsonObject.getLong("value"), 0); + assertEquals("Long not recognized", 0, jsonObject.optLongObject("value"), 0); + assertEquals("Integer not recognized", 0, jsonObject.optInt("value"), 0); + assertEquals("Integer not recognized", 0, jsonObject.getInt("value"), 0); + assertEquals("Integer not recognized", 0, jsonObject.optIntegerObject("value"), 0); + assertEquals("Number not recognized", 0, jsonObject.getNumber("value").intValue(), 0); + assertEquals("Number not recognized", 0, jsonObject.getNumber("value").longValue(), 0); + assertEquals("BigDecimal not recognized", 0, BigDecimal.valueOf(-0).compareTo(jsonObject.getBigDecimal("value"))); + assertEquals("BigInteger not recognized",0, BigInteger.valueOf(0).compareTo(jsonObject.getBigInteger("value"))); + } + +} diff --git a/src/test/java/org/json/junit/XMLTest.java b/src/test/java/org/json/junit/XMLTest.java index e940032e0..22d6131cb 100644 --- a/src/test/java/org/json/junit/XMLTest.java +++ b/src/test/java/org/json/junit/XMLTest.java @@ -1223,32 +1223,18 @@ public void testIndentSimpleJsonArray(){ @Test public void testIndentComplicatedJsonObjectWithArrayAndWithConfig(){ - try { - InputStream jsonStream = null; - try { - jsonStream = XMLTest.class.getClassLoader().getResourceAsStream("Issue593.json"); - final JSONObject object = new JSONObject(new JSONTokener(jsonStream)); - String actualString = XML.toString(object, null, XMLParserConfiguration.KEEP_STRINGS,2); - InputStream xmlStream = null; - try { - xmlStream = XMLTest.class.getClassLoader().getResourceAsStream("Issue593.xml"); - int bufferSize = 1024; - char[] buffer = new char[bufferSize]; - StringBuilder expected = new StringBuilder(); - Reader in = new InputStreamReader(xmlStream, "UTF-8"); - for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) { - expected.append(buffer, 0, numRead); - } - assertEquals(expected.toString(), actualString.replaceAll("\\n|\\r\\n", System.getProperty("line.separator"))); - } finally { - if (xmlStream != null) { - xmlStream.close(); - } - } - } finally { - if (jsonStream != null) { - jsonStream.close(); + try (InputStream jsonStream = XMLTest.class.getClassLoader().getResourceAsStream("Issue593.json")) { + final JSONObject object = new JSONObject(new JSONTokener(jsonStream)); + String actualString = XML.toString(object, null, XMLParserConfiguration.KEEP_STRINGS, 2); + try (InputStream xmlStream = XMLTest.class.getClassLoader().getResourceAsStream("Issue593.xml")) { + int bufferSize = 1024; + char[] buffer = new char[bufferSize]; + StringBuilder expected = new StringBuilder(); + Reader in = new InputStreamReader(xmlStream, "UTF-8"); + for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0; ) { + expected.append(buffer, 0, numRead); } + assertEquals(expected.toString(), actualString); } } catch (IOException e) { fail("file writer error: " +e.getMessage()); diff --git a/src/test/java/org/json/junit/data/GenericBean.java b/src/test/java/org/json/junit/data/GenericBean.java index da6370d48..dd46b88e6 100644 --- a/src/test/java/org/json/junit/data/GenericBean.java +++ b/src/test/java/org/json/junit/data/GenericBean.java @@ -9,7 +9,7 @@ * @param * generic number value */ -public class GenericBean> implements MyBean { +public class GenericBean implements MyBean { /** * @param genericValue * value to initiate with