Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #632: Detect cycles when constructing a JSONObject #633

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 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
231 changes: 151 additions & 80 deletions src/main/java/org/json/JSONObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,8 @@ of this software and associated documentation files (the "Software"), to deal
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.*;
import java.util.Map.Entry;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.regex.Pattern;

/**
Expand Down Expand Up @@ -225,13 +218,13 @@ public JSONObject(JSONTokener x) throws JSONException {
for (;;) {
c = x.nextClean();
switch (c) {
case 0:
throw x.syntaxError("A JSONObject text must end with '}'");
case '}':
return;
default:
x.back();
key = x.nextValue().toString();
case 0:
throw x.syntaxError("A JSONObject text must end with '}'");
case '}':
return;
default:
x.back();
key = x.nextValue().toString();
}

// The key is followed by ':'.
Expand Down Expand Up @@ -259,17 +252,17 @@ public JSONObject(JSONTokener x) throws JSONException {
// Pairs are separated by ','.

switch (x.nextClean()) {
case ';':
case ',':
if (x.nextClean() == '}') {
case ';':
case ',':
if (x.nextClean() == '}') {
return;
}
x.back();
break;
case '}':
return;
}
x.back();
break;
case '}':
return;
default:
throw x.syntaxError("Expected a ',' or '}'");
default:
throw x.syntaxError("Expected a ',' or '}'");
}
}
}
Expand All @@ -290,12 +283,13 @@ public JSONObject(Map<?, ?> m) {
this.map = new HashMap<String, Object>();
} else {
this.map = new HashMap<String, Object>(m.size());
for (final Entry<?, ?> e : m.entrySet()) {
if(e.getKey() == null) {
throw new NullPointerException("Null key.");
}
for (final Entry<?, ?> e : m.entrySet()) {
if(e.getKey() == null) {
throw new NullPointerException("Null key.");
}
final Object value = e.getValue();
if (value != null) {
checkForCyclicDependency(value);
this.map.put(String.valueOf(e.getKey()), wrap(value));
}
}
Expand Down Expand Up @@ -614,11 +608,11 @@ public boolean getBoolean(String key) throws JSONException {
Object object = this.get(key);
if (object.equals(Boolean.FALSE)
|| (object instanceof String && ((String) object)
.equalsIgnoreCase("false"))) {
.equalsIgnoreCase("false"))) {
return false;
} else if (object.equals(Boolean.TRUE)
|| (object instanceof String && ((String) object)
.equalsIgnoreCase("true"))) {
.equalsIgnoreCase("true"))) {
return true;
}
throw wrongValueFormatException(key, "Boolean", null);
Expand Down Expand Up @@ -999,9 +993,9 @@ public boolean isEmpty() {
* is empty.
*/
public JSONArray names() {
if(this.map.isEmpty()) {
return null;
}
if(this.map.isEmpty()) {
return null;
}
return new JSONArray(this.map.keySet());
}

Expand Down Expand Up @@ -1162,7 +1156,7 @@ public BigDecimal optBigDecimal(String key, BigDecimal defaultValue) {
static BigDecimal objectToBigDecimal(Object val, BigDecimal defaultValue) {
return objectToBigDecimal(val, defaultValue, true);
}

/**
* @param val value to convert
* @param defaultValue default value to return is the conversion doesn't work or is null.
Expand Down Expand Up @@ -1520,6 +1514,9 @@ public String optString(String key, String defaultValue) {
* the bean
*/
private void populateMap(Object bean) {

checkForCyclicDependency(bean);

Class<?> klass = bean.getClass();

// If klass is a System class then set includeSuperClass to false.
Expand Down Expand Up @@ -1961,7 +1958,7 @@ public Object query(JSONPointer jsonPointer) {
* @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax
*/
public Object optQuery(String jsonPointer) {
return optQuery(new JSONPointer(jsonPointer));
return optQuery(new JSONPointer(jsonPointer));
}

/**
Expand Down Expand Up @@ -2020,42 +2017,42 @@ public static Writer quote(String string, Writer w) throws IOException {
b = c;
c = string.charAt(i);
switch (c) {
case '\\':
case '"':
w.write('\\');
w.write(c);
break;
case '/':
if (b == '<') {
case '\\':
case '"':
w.write('\\');
}
w.write(c);
break;
case '\b':
w.write("\\b");
break;
case '\t':
w.write("\\t");
break;
case '\n':
w.write("\\n");
break;
case '\f':
w.write("\\f");
break;
case '\r':
w.write("\\r");
break;
default:
if (c < ' ' || (c >= '\u0080' && c < '\u00a0')
|| (c >= '\u2000' && c < '\u2100')) {
w.write("\\u");
hhhh = Integer.toHexString(c);
w.write("0000", 0, 4 - hhhh.length());
w.write(hhhh);
} else {
w.write(c);
}
break;
case '/':
if (b == '<') {
w.write('\\');
}
w.write(c);
break;
case '\b':
w.write("\\b");
break;
case '\t':
w.write("\\t");
break;
case '\n':
w.write("\\n");
break;
case '\f':
w.write("\\f");
break;
case '\r':
w.write("\\r");
break;
default:
if (c < ' ' || (c >= '\u0080' && c < '\u00a0')
|| (c >= '\u2000' && c < '\u2100')) {
w.write("\\u");
hhhh = Integer.toHexString(c);
w.write("0000", 0, 4 - hhhh.length());
w.write(hhhh);
} else {
w.write(c);
}
}
}
w.write('"');
Expand Down Expand Up @@ -2095,10 +2092,10 @@ public boolean similar(Object other) {
Object valueThis = entry.getValue();
Object valueOther = ((JSONObject)other).get(name);
if(valueThis == valueOther) {
continue;
continue;
}
if(valueThis == null) {
return false;
return false;
}
if (valueThis instanceof JSONObject) {
if (!((JSONObject)valueThis).similar(valueOther)) {
Expand All @@ -2110,7 +2107,7 @@ public boolean similar(Object other) {
}
} else if (valueThis instanceof Number && valueOther instanceof Number) {
if (!isNumberSimilar((Number)valueThis, (Number)valueOther)) {
return false;
return false;
};
} else if (!valueThis.equals(valueOther)) {
return false;
Expand Down Expand Up @@ -2411,10 +2408,10 @@ public String toString(int indentFactor) throws JSONException {
* If the value is or contains an invalid number.
*/
public static String valueToString(Object value) throws JSONException {
// moves the implementation to JSONWriter as:
// 1. It makes more sense to be part of the writer class
// 2. For Android support this method is not available. By implementing it in the Writer
// Android users can use the writer with the built in Android JSONObject implementation.
// moves the implementation to JSONWriter as:
// 1. It makes more sense to be part of the writer class
// 2. For Android support this method is not available. By implementing it in the Writer
// Android users can use the writer with the built in Android JSONObject implementation.
return JSONWriter.valueToString(value);
}

Expand Down Expand Up @@ -2486,7 +2483,7 @@ public Writer write(Writer writer) throws JSONException {
}

static final Writer writeValue(Writer writer, Object value,
int indentFactor, int indent) throws JSONException, IOException {
int indentFactor, int indent) throws JSONException, IOException {
if (value == null || value.equals(null)) {
writer.write("null");
} else if (value instanceof JSONString) {
Expand Down Expand Up @@ -2570,7 +2567,7 @@ public Writer write(Writer writer, int indentFactor, int indent)
writer.write('{');

if (length == 1) {
final Entry<String,?> entry = this.entrySet().iterator().next();
final Entry<String,?> entry = this.entrySet().iterator().next();
final String key = entry.getKey();
writer.write(quote(key));
writer.write(':');
Expand Down Expand Up @@ -2676,4 +2673,78 @@ private static JSONException wrongValueFormatException(
"JSONObject[" + quote(key) + "] is not a " + valueType + " (" + value + ")."
, cause);
}
}

/**
*
* @param value, the root node to check for the validity of child nodes
*/
private void checkForCyclicDependency(Object value) {
checkForCyclicDependency(value, new HashSet<Object>());
}

/**
* @param value, the root node to check for the validity of child nodes
* @param setOfInstanceVariables, the path from root to {@param value} which acts as the ancestors to the current and child nodes.
* @throws JSONException, if there exists a cyclical dependency from ancestorial root to any of the child nodes.
*/
private static void checkForCyclicDependency(Object value, Set<Object> setOfInstanceVariables) throws JSONException {
Class<?> klass = value.getClass();

// If klass is a System class then set includeSuperClass to false.
boolean includeSuperClass = klass.getClassLoader() != null;

Method[] methods = includeSuperClass ? klass.getMethods() : klass.getDeclaredMethods();
for (final Method method : methods) {
final int modifiers = method.getModifiers();
if (Modifier.isPublic(modifiers)
&& !Modifier.isStatic(modifiers)
&& method.getParameterTypes().length == 0
&& !method.isBridge()
&& method.getReturnType() != Void.TYPE
&& isValidMethodName(method.getName())) {
final String key = getKeyNameFromMethod(method);
if (key != null && !key.isEmpty()) {
try {
final Object result = method.invoke(value);
if (result != null) {
// 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
if (result instanceof Closeable) {
try {
((Closeable) result).close();
} catch (IOException ignore) {
}
}

if (setOfInstanceVariables.contains(result)) {
throw cyclicDependencyFormatException(key);
}

//adding the currently checked object to the ancestor path
setOfInstanceVariables.add(result);
//DFS type search for checking depdendency.
checkForCyclicDependency(result, setOfInstanceVariables);

//removed the currently checked object from ancestor path for different root to leaf path
setOfInstanceVariables.remove(result);
}
} catch (IllegalAccessException ignore) {
} catch (IllegalArgumentException ignore) {
} catch (InvocationTargetException ignore) {
}
}
}
}
}

/**
* Create a new JSONException in a common format for cyclic dependency.
*
* @return JSONException that can be thrown.
*/
private static JSONException cyclicDependencyFormatException(String key) {
return new JSONException(
"JSONObject[" + quote(key) + "] cannot be written because of a Cyclic Dependency");
}
}
Loading