fix: limit the nesting depth in JSONML

Limit the XML nesting depth for CVE-2022-45688 when using the JsonML transform.
This commit is contained in:
Tamas Perger 2023-02-10 01:46:44 +00:00
parent 2391d248cc
commit a6e412bded
3 changed files with 322 additions and 59 deletions

View File

@ -27,7 +27,32 @@ public class JSONML {
XMLTokener x, XMLTokener x,
boolean arrayForm, boolean arrayForm,
JSONArray ja, JSONArray ja,
boolean keepStrings boolean keepStrings,
int currentNestingDepth
) throws JSONException {
return parse(x,arrayForm, ja,
keepStrings ? XMLtoJSONMLParserConfiguration.KEEP_STRINGS : XMLtoJSONMLParserConfiguration.ORIGINAL,
currentNestingDepth);
}
/**
* Parse XML values and store them in a JSONArray.
* @param x The XMLTokener containing the source string.
* @param arrayForm true if array form, false if object form.
* @param ja The JSONArray that is containing the current tag or null
* if we are at the outermost level.
* @param config The XML parser configuration:
* XMLtoJSONMLParserConfiguration.ORIGINAL is the default behaviour;
* XMLtoJSONMLParserConfiguration.KEEP_STRINGS means Don't type-convert text nodes and attribute values.
* @return A JSONArray if the value is the outermost tag, otherwise null.
* @throws JSONException if a parsing error occurs
*/
private static Object parse(
XMLTokener x,
boolean arrayForm,
JSONArray ja,
XMLtoJSONMLParserConfiguration config,
int currentNestingDepth
) throws JSONException { ) throws JSONException {
String attribute; String attribute;
char c; char c;
@ -152,7 +177,7 @@ public class JSONML {
if (!(token instanceof String)) { if (!(token instanceof String)) {
throw x.syntaxError("Missing value"); throw x.syntaxError("Missing value");
} }
newjo.accumulate(attribute, keepStrings ? ((String)token) :XML.stringToValue((String)token)); newjo.accumulate(attribute, config.isKeepStrings() ? ((String)token) :XML.stringToValue((String)token));
token = null; token = null;
} else { } else {
newjo.accumulate(attribute, ""); newjo.accumulate(attribute, "");
@ -181,7 +206,12 @@ public class JSONML {
if (token != XML.GT) { if (token != XML.GT) {
throw x.syntaxError("Misshaped tag"); throw x.syntaxError("Misshaped tag");
} }
closeTag = (String)parse(x, arrayForm, newja, keepStrings);
if (currentNestingDepth == config.getMaxNestingDepth()) {
throw x.syntaxError("Maximum nesting depth of " + config.getMaxNestingDepth() + " reached");
}
closeTag = (String)parse(x, arrayForm, newja, config, currentNestingDepth + 1);
if (closeTag != null) { if (closeTag != null) {
if (!closeTag.equals(tagName)) { if (!closeTag.equals(tagName)) {
throw x.syntaxError("Mismatched '" + tagName + throw x.syntaxError("Mismatched '" + tagName +
@ -203,7 +233,7 @@ public class JSONML {
} else { } else {
if (ja != null) { if (ja != null) {
ja.put(token instanceof String ja.put(token instanceof String
? keepStrings ? XML.unescape((String)token) :XML.stringToValue((String)token) ? (config.isKeepStrings() ? XML.unescape((String)token) : XML.stringToValue((String)token))
: token); : token);
} }
} }
@ -224,7 +254,7 @@ public class JSONML {
* @throws JSONException Thrown on error converting to a JSONArray * @throws JSONException Thrown on error converting to a JSONArray
*/ */
public static JSONArray toJSONArray(String string) throws JSONException { public static JSONArray toJSONArray(String string) throws JSONException {
return (JSONArray)parse(new XMLTokener(string), true, null, false); return (JSONArray)parse(new XMLTokener(string), true, null, XMLtoJSONMLParserConfiguration.ORIGINAL, 0);
} }
@ -235,8 +265,8 @@ public class JSONML {
* attributes, then the second element will be JSONObject containing the * attributes, then the second element will be JSONObject containing the
* name/value pairs. If the tag contains children, then strings and * name/value pairs. If the tag contains children, then strings and
* JSONArrays will represent the child tags. * JSONArrays will represent the child tags.
* As opposed to toJSONArray this method does not attempt to convert * As opposed to toJSONArray this method does not attempt to convert
* any text node or attribute value to any type * any text node or attribute value to any type
* but just leaves it as a string. * but just leaves it as a string.
* Comments, prologs, DTDs, and <pre>{@code &lt;[ [ ]]>}</pre> are ignored. * Comments, prologs, DTDs, and <pre>{@code &lt;[ [ ]]>}</pre> are ignored.
* @param string The source string. * @param string The source string.
@ -246,7 +276,7 @@ public class JSONML {
* @throws JSONException Thrown on error converting to a JSONArray * @throws JSONException Thrown on error converting to a JSONArray
*/ */
public static JSONArray toJSONArray(String string, boolean keepStrings) throws JSONException { public static JSONArray toJSONArray(String string, boolean keepStrings) throws JSONException {
return (JSONArray)parse(new XMLTokener(string), true, null, keepStrings); return (JSONArray)parse(new XMLTokener(string), true, null, keepStrings, 0);
} }
@ -257,8 +287,8 @@ public class JSONML {
* attributes, then the second element will be JSONObject containing the * attributes, then the second element will be JSONObject containing the
* name/value pairs. If the tag contains children, then strings and * name/value pairs. If the tag contains children, then strings and
* JSONArrays will represent the child content and tags. * JSONArrays will represent the child content and tags.
* As opposed to toJSONArray this method does not attempt to convert * As opposed to toJSONArray this method does not attempt to convert
* any text node or attribute value to any type * any text node or attribute value to any type
* but just leaves it as a string. * but just leaves it as a string.
* Comments, prologs, DTDs, and <pre>{@code &lt;[ [ ]]>}</pre> are ignored. * Comments, prologs, DTDs, and <pre>{@code &lt;[ [ ]]>}</pre> are ignored.
* @param x An XMLTokener. * @param x An XMLTokener.
@ -268,7 +298,7 @@ public class JSONML {
* @throws JSONException Thrown on error converting to a JSONArray * @throws JSONException Thrown on error converting to a JSONArray
*/ */
public static JSONArray toJSONArray(XMLTokener x, boolean keepStrings) throws JSONException { public static JSONArray toJSONArray(XMLTokener x, boolean keepStrings) throws JSONException {
return (JSONArray)parse(x, true, null, keepStrings); return (JSONArray)parse(x, true, null, keepStrings, 0);
} }
@ -285,7 +315,7 @@ public class JSONML {
* @throws JSONException Thrown on error converting to a JSONArray * @throws JSONException Thrown on error converting to a JSONArray
*/ */
public static JSONArray toJSONArray(XMLTokener x) throws JSONException { public static JSONArray toJSONArray(XMLTokener x) throws JSONException {
return (JSONArray)parse(x, true, null, false); return (JSONArray)parse(x, true, null, false, 0);
} }
@ -303,10 +333,10 @@ public class JSONML {
* @throws JSONException Thrown on error converting to a JSONObject * @throws JSONException Thrown on error converting to a JSONObject
*/ */
public static JSONObject toJSONObject(String string) throws JSONException { public static JSONObject toJSONObject(String string) throws JSONException {
return (JSONObject)parse(new XMLTokener(string), false, null, false); return (JSONObject)parse(new XMLTokener(string), false, null, false, 0);
} }
/** /**
* Convert a well-formed (but not necessarily valid) XML string into a * Convert a well-formed (but not necessarily valid) XML string into a
* JSONObject using the JsonML transform. Each XML tag is represented as * JSONObject using the JsonML transform. Each XML tag is represented as
@ -323,10 +353,32 @@ public class JSONML {
* @throws JSONException Thrown on error converting to a JSONObject * @throws JSONException Thrown on error converting to a JSONObject
*/ */
public static JSONObject toJSONObject(String string, boolean keepStrings) throws JSONException { public static JSONObject toJSONObject(String string, boolean keepStrings) throws JSONException {
return (JSONObject)parse(new XMLTokener(string), false, null, keepStrings); return (JSONObject)parse(new XMLTokener(string), false, null, keepStrings, 0);
} }
/**
* Convert a well-formed (but not necessarily valid) XML string into a
* JSONObject using the JsonML transform. Each XML tag is represented as
* a JSONObject with a "tagName" property. If the tag has attributes, then
* the attributes will be in the JSONObject as properties. If the tag
* contains children, the object will have a "childNodes" property which
* will be an array of strings and JsonML JSONObjects.
* Comments, prologs, DTDs, and <pre>{@code &lt;[ [ ]]>}</pre> are ignored.
* @param string The XML source text.
* @param config The XML parser configuration:
* XMLtoJSONMLParserConfiguration.ORIGINAL is the default behaviour;
* XMLtoJSONMLParserConfiguration.KEEP_STRINGS means values will not be coerced into boolean
* or numeric values and will instead be left as strings
* @return A JSONObject containing the structured data from the XML string.
* @throws JSONException Thrown on error converting to a JSONObject
*/
public static JSONObject toJSONObject(String string, XMLtoJSONMLParserConfiguration config) throws JSONException {
return (JSONObject)parse(new XMLTokener(string), false, null, config, 0);
}
/** /**
* Convert a well-formed (but not necessarily valid) XML string into a * Convert a well-formed (but not necessarily valid) XML string into a
* JSONObject using the JsonML transform. Each XML tag is represented as * JSONObject using the JsonML transform. Each XML tag is represented as
@ -341,7 +393,7 @@ public class JSONML {
* @throws JSONException Thrown on error converting to a JSONObject * @throws JSONException Thrown on error converting to a JSONObject
*/ */
public static JSONObject toJSONObject(XMLTokener x) throws JSONException { public static JSONObject toJSONObject(XMLTokener x) throws JSONException {
return (JSONObject)parse(x, false, null, false); return (JSONObject)parse(x, false, null, false, 0);
} }
@ -361,7 +413,29 @@ public class JSONML {
* @throws JSONException Thrown on error converting to a JSONObject * @throws JSONException Thrown on error converting to a JSONObject
*/ */
public static JSONObject toJSONObject(XMLTokener x, boolean keepStrings) throws JSONException { public static JSONObject toJSONObject(XMLTokener x, boolean keepStrings) throws JSONException {
return (JSONObject)parse(x, false, null, keepStrings); return (JSONObject)parse(x, false, null, keepStrings, 0);
}
/**
* Convert a well-formed (but not necessarily valid) XML string into a
* JSONObject using the JsonML transform. Each XML tag is represented as
* a JSONObject with a "tagName" property. If the tag has attributes, then
* the attributes will be in the JSONObject as properties. If the tag
* contains children, the object will have a "childNodes" property which
* will be an array of strings and JsonML JSONObjects.
* Comments, prologs, DTDs, and <pre>{@code &lt;[ [ ]]>}</pre> are ignored.
* @param x An XMLTokener of the XML source text.
* @param config The XML parser configuration:
* XMLtoJSONMLParserConfiguration.ORIGINAL is the default behaviour;
* XMLtoJSONMLParserConfiguration.KEEP_STRINGS means values will not be coerced into boolean
* or numeric values and will instead be left as strings
* @return A JSONObject containing the structured data from the XML string.
* @throws JSONException Thrown on error converting to a JSONObject
*/
public static JSONObject toJSONObject(XMLTokener x, XMLtoJSONMLParserConfiguration config) throws JSONException {
return (JSONObject)parse(x, false, null, config, 0);
} }
@ -442,6 +516,7 @@ public class JSONML {
return sb.toString(); return sb.toString();
} }
/** /**
* Reverse the JSONML transformation, making an XML text from a JSONObject. * Reverse the JSONML transformation, making an XML text from a JSONObject.
* The JSONObject must contain a "tagName" property. If it has children, * The JSONObject must contain a "tagName" property. If it has children,

View File

@ -0,0 +1,128 @@
package org.json;
/*
Public Domain.
*/
/**
* Configuration object for the XML to JSONML parser. The configuration is immutable.
*/
@SuppressWarnings({""})
public class XMLtoJSONMLParserConfiguration {
/**
* Used to indicate there's no defined limit to the maximum nesting depth when parsing a XML
* document to JSONML.
*/
public static final int UNDEFINED_MAXIMUM_NESTING_DEPTH = -1;
/**
* The default maximum nesting depth when parsing a XML document to JSONML.
*/
public static final int DEFAULT_MAXIMUM_NESTING_DEPTH = 512;
/** Original Configuration of the XML to JSONML Parser. */
public static final XMLtoJSONMLParserConfiguration ORIGINAL
= new XMLtoJSONMLParserConfiguration();
/** Original configuration of the XML to JSONML Parser except that values are kept as strings. */
public static final XMLtoJSONMLParserConfiguration KEEP_STRINGS
= new XMLtoJSONMLParserConfiguration().withKeepStrings(true);
/**
* When parsing the XML into JSONML, specifies if values should be kept as strings (<code>true</code>), or if
* they should try to be guessed into JSON values (numeric, boolean, string)
*/
private boolean keepStrings;
/**
* The maximum nesting depth when parsing a XML document to JSONML.
*/
private int maxNestingDepth = DEFAULT_MAXIMUM_NESTING_DEPTH;
/**
* Default parser configuration. Does not keep strings (tries to implicitly convert values).
*/
public XMLtoJSONMLParserConfiguration() {
this.keepStrings = false;
}
/**
* Configure the parser string processing and use the default CDATA Tag Name as "content".
* @param keepStrings <code>true</code> to parse all values as string.
* <code>false</code> to try and convert XML string values into a JSON value.
* @param maxNestingDepth <code>int</code> to limit the nesting depth
*/
public XMLtoJSONMLParserConfiguration(final boolean keepStrings, final int maxNestingDepth) {
this.keepStrings = keepStrings;
this.maxNestingDepth = maxNestingDepth;
}
/**
* Provides a new instance of the same configuration.
*/
@Override
protected XMLtoJSONMLParserConfiguration clone() {
// future modifications to this method should always ensure a "deep"
// clone in the case of collections. i.e. if a Map is added as a configuration
// item, a new map instance should be created and if possible each value in the
// map should be cloned as well. If the values of the map are known to also
// be immutable, then a shallow clone of the map is acceptable.
return new XMLtoJSONMLParserConfiguration(
this.keepStrings,
this.maxNestingDepth
);
}
/**
* When parsing the XML into JSONML, specifies if values should be kept as strings (<code>true</code>), or if
* they should try to be guessed into JSON values (numeric, boolean, string)
*
* @return The <code>keepStrings</code> configuration value.
*/
public boolean isKeepStrings() {
return this.keepStrings;
}
/**
* When parsing the XML into JSONML, specifies if values should be kept as strings (<code>true</code>), or if
* they should try to be guessed into JSON values (numeric, boolean, string)
*
* @param newVal
* new value to use for the <code>keepStrings</code> configuration option.
*
* @return The existing configuration will not be modified. A new configuration is returned.
*/
public XMLtoJSONMLParserConfiguration withKeepStrings(final boolean newVal) {
XMLtoJSONMLParserConfiguration newConfig = this.clone();
newConfig.keepStrings = newVal;
return newConfig;
}
/**
* The maximum nesting depth that the parser will descend before throwing an exception
* when parsing the XML into JSONML.
* @return the maximum nesting depth set for this configuration
*/
public int getMaxNestingDepth() {
return maxNestingDepth;
}
/**
* Defines the maximum nesting depth that the parser will descend before throwing an exception
* when parsing the XML into JSONML. The default max nesting depth is 512, which means the parser
* will throw a JsonException if the maximum depth is reached.
* Using any negative value as a parameter is equivalent to setting no limit to the nesting depth,
* which means the parses will go as deep as the maximum call stack size allows.
* @param maxNestingDepth the maximum nesting depth allowed to the XML parser
* @return The existing configuration will not be modified. A new configuration is returned.
*/
public XMLtoJSONMLParserConfiguration withMaxNestingDepth(int maxNestingDepth) {
XMLtoJSONMLParserConfiguration newConfig = this.clone();
if (maxNestingDepth > UNDEFINED_MAXIMUM_NESTING_DEPTH) {
newConfig.maxNestingDepth = maxNestingDepth;
} else {
newConfig.maxNestingDepth = UNDEFINED_MAXIMUM_NESTING_DEPTH;
}
return newConfig;
}
}

View File

@ -11,19 +11,19 @@ import org.junit.Test;
/** /**
* Tests for org.json.JSONML.java * Tests for org.json.JSONML.java
* *
* Certain inputs are expected to result in exceptions. These tests are * Certain inputs are expected to result in exceptions. These tests are
* executed first. JSONML provides an API to: * executed first. JSONML provides an API to:
* Convert an XML string into a JSONArray or a JSONObject. * Convert an XML string into a JSONArray or a JSONObject.
* Convert a JSONArray or JSONObject into an XML string. * Convert a JSONArray or JSONObject into an XML string.
* Both fromstring and tostring operations operations should be symmetrical * Both fromstring and tostring operations operations should be symmetrical
* within the limits of JSONML. * within the limits of JSONML.
* It should be possible to perform the following operations, which should * It should be possible to perform the following operations, which should
* result in the original string being recovered, within the limits of the * result in the original string being recovered, within the limits of the
* underlying classes: * underlying classes:
* Convert a string -> JSONArray -> string -> JSONObject -> string * Convert a string -> JSONArray -> string -> JSONObject -> string
* Convert a string -> JSONObject -> string -> JSONArray -> string * Convert a string -> JSONObject -> string -> JSONArray -> string
* *
*/ */
public class JSONMLTest { public class JSONMLTest {
@ -56,7 +56,7 @@ public class JSONMLTest {
/** /**
* Attempts to call JSONML.toString() with a null JSONArray. * Attempts to call JSONML.toString() with a null JSONArray.
* Expects a NullPointerException. * Expects a NullPointerException.
*/ */
@Test(expected=NullPointerException.class) @Test(expected=NullPointerException.class)
public void nullJSONXMLException() { public void nullJSONXMLException() {
@ -69,7 +69,7 @@ public class JSONMLTest {
/** /**
* Attempts to call JSONML.toString() with a null JSONArray. * Attempts to call JSONML.toString() with a null JSONArray.
* Expects a JSONException. * Expects a JSONException.
*/ */
@Test @Test
public void emptyJSONXMLException() { public void emptyJSONXMLException() {
@ -125,7 +125,7 @@ public class JSONMLTest {
"[\"addresses\","+ "[\"addresses\","+
"{\"xsi:noNamespaceSchemaLocation\":\"test.xsd\","+ "{\"xsi:noNamespaceSchemaLocation\":\"test.xsd\","+
"\"xmlns:xsi\":\"http://www.w3.org/2001/XMLSchema-instance\"},"+ "\"xmlns:xsi\":\"http://www.w3.org/2001/XMLSchema-instance\"},"+
// this array has no name // this array has no name
"["+ "["+
"[\"name\"],"+ "[\"name\"],"+
"[\"nocontent\"],"+ "[\"nocontent\"],"+
@ -180,7 +180,7 @@ public class JSONMLTest {
} }
/** /**
* Attempts to transform a malformed XML document * Attempts to transform a malformed XML document
* (element tag has a frontslash) to a JSONArray.\ * (element tag has a frontslash) to a JSONArray.\
* Expects a JSONException * Expects a JSONException
*/ */
@ -191,7 +191,7 @@ public class JSONMLTest {
* In this case, the XML is invalid because the 'name' element * In this case, the XML is invalid because the 'name' element
* contains an invalid frontslash. * contains an invalid frontslash.
*/ */
String xmlStr = String xmlStr =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+ "<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+
" xsi:noNamespaceSchemaLocation='test.xsd'>\n"+ " xsi:noNamespaceSchemaLocation='test.xsd'>\n"+
@ -216,7 +216,7 @@ public class JSONMLTest {
*/ */
@Test @Test
public void invalidBangInTagException() { public void invalidBangInTagException() {
String xmlStr = String xmlStr =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+ "<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+
" xsi:noNamespaceSchemaLocation='test.xsd'>\n"+ " xsi:noNamespaceSchemaLocation='test.xsd'>\n"+
@ -246,7 +246,7 @@ public class JSONMLTest {
* In this case, the XML is invalid because an element * In this case, the XML is invalid because an element
* starts with '!' and has no closing tag * starts with '!' and has no closing tag
*/ */
String xmlStr = String xmlStr =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+ "<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+
" xsi:noNamespaceSchemaLocation='test.xsd'>\n"+ " xsi:noNamespaceSchemaLocation='test.xsd'>\n"+
@ -276,7 +276,7 @@ public class JSONMLTest {
* In this case, the XML is invalid because an element * In this case, the XML is invalid because an element
* has no closing '>'. * has no closing '>'.
*/ */
String xmlStr = String xmlStr =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+ "<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+
" xsi:noNamespaceSchemaLocation='test.xsd'>\n"+ " xsi:noNamespaceSchemaLocation='test.xsd'>\n"+
@ -306,7 +306,7 @@ public class JSONMLTest {
* In this case, the XML is invalid because an element * In this case, the XML is invalid because an element
* has no name after the closing tag '</'. * has no name after the closing tag '</'.
*/ */
String xmlStr = String xmlStr =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+ "<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+
" xsi:noNamespaceSchemaLocation='test.xsd'>\n"+ " xsi:noNamespaceSchemaLocation='test.xsd'>\n"+
@ -336,7 +336,7 @@ public class JSONMLTest {
* In this case, the XML is invalid because an element * In this case, the XML is invalid because an element
* has '>' after the closing tag '</' and name. * has '>' after the closing tag '</' and name.
*/ */
String xmlStr = String xmlStr =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+ "<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+
" xsi:noNamespaceSchemaLocation=\"test.xsd\">\n"+ " xsi:noNamespaceSchemaLocation=\"test.xsd\">\n"+
@ -364,9 +364,9 @@ public class JSONMLTest {
/** /**
* xmlStr contains XML text which is transformed into a JSONArray. * xmlStr contains XML text which is transformed into a JSONArray.
* In this case, the XML is invalid because an element * In this case, the XML is invalid because an element
* does not have a complete CDATA string. * does not have a complete CDATA string.
*/ */
String xmlStr = String xmlStr =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+ "<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+
" xsi:noNamespaceSchemaLocation='test.xsd'>\n"+ " xsi:noNamespaceSchemaLocation='test.xsd'>\n"+
@ -388,7 +388,7 @@ public class JSONMLTest {
/** /**
* Convert an XML document into a JSONArray, then use JSONML.toString() * Convert an XML document into a JSONArray, then use JSONML.toString()
* to convert it into a string. This string is then converted back into * to convert it into a string. This string is then converted back into
* a JSONArray. Both JSONArrays are compared against a control to * a JSONArray. Both JSONArrays are compared against a control to
* confirm the contents. * confirm the contents.
*/ */
@Test @Test
@ -405,7 +405,7 @@ public class JSONMLTest {
* which is used to create a final JSONArray, which is also compared * which is used to create a final JSONArray, which is also compared
* against the expected JSONArray. * against the expected JSONArray.
*/ */
String xmlStr = String xmlStr =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+ "<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+
"xsi:noNamespaceSchemaLocation='test.xsd'>\n"+ "xsi:noNamespaceSchemaLocation='test.xsd'>\n"+
@ -414,7 +414,7 @@ public class JSONMLTest {
"<nocontent/>>\n"+ "<nocontent/>>\n"+
"</address>\n"+ "</address>\n"+
"</addresses>"; "</addresses>";
String expectedStr = String expectedStr =
"[\"addresses\","+ "[\"addresses\","+
"{\"xsi:noNamespaceSchemaLocation\":\"test.xsd\","+ "{\"xsi:noNamespaceSchemaLocation\":\"test.xsd\","+
"\"xmlns:xsi\":\"http://www.w3.org/2001/XMLSchema-instance\"},"+ "\"xmlns:xsi\":\"http://www.w3.org/2001/XMLSchema-instance\"},"+
@ -434,12 +434,12 @@ public class JSONMLTest {
} }
/** /**
* Convert an XML document into a JSONObject. Use JSONML.toString() to * Convert an XML document into a JSONObject. Use JSONML.toString() to
* convert it back into a string, and then re-convert it into a JSONObject. * convert it back into a string, and then re-convert it into a JSONObject.
* Both JSONObjects are compared against a control JSONObject to confirm * Both JSONObjects are compared against a control JSONObject to confirm
* the contents. * the contents.
* <p> * <p>
* Next convert the XML document into a JSONArray. Use JSONML.toString() to * Next convert the XML document into a JSONArray. Use JSONML.toString() to
* convert it back into a string, and then re-convert it into a JSONArray. * convert it back into a string, and then re-convert it into a JSONArray.
* Both JSONArrays are compared against a control JSONArray to confirm * Both JSONArrays are compared against a control JSONArray to confirm
* the contents. * the contents.
@ -452,23 +452,23 @@ public class JSONMLTest {
/** /**
* xmlStr contains XML text which is transformed into a JSONObject, * xmlStr contains XML text which is transformed into a JSONObject,
* restored to XML, transformed into a JSONArray, and then restored * restored to XML, transformed into a JSONArray, and then restored
* to XML again. Both JSONObject and JSONArray should contain the same * to XML again. Both JSONObject and JSONArray should contain the same
* information and should produce the same XML, allowing for non-ordered * information and should produce the same XML, allowing for non-ordered
* attributes. * attributes.
* *
* Transformation to JSONObject: * Transformation to JSONObject:
* The elementName is stored as a string where key="tagName" * The elementName is stored as a string where key="tagName"
* Attributes are simply stored as key/value pairs * Attributes are simply stored as key/value pairs
* If the element has either content or child elements, they are stored * If the element has either content or child elements, they are stored
* in a jsonArray with key="childNodes". * in a jsonArray with key="childNodes".
* *
* Transformation to JSONArray: * Transformation to JSONArray:
* 1st entry = elementname * 1st entry = elementname
* 2nd entry = attributes object (if present) * 2nd entry = attributes object (if present)
* 3rd entry = content (if present) * 3rd entry = content (if present)
* 4th entry = child element JSONArrays (if present) * 4th entry = child element JSONArrays (if present)
*/ */
String xmlStr = String xmlStr =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+ "<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+
"xsi:noNamespaceSchemaLocation='test.xsd'>\n"+ "xsi:noNamespaceSchemaLocation='test.xsd'>\n"+
@ -585,7 +585,7 @@ public class JSONMLTest {
"\"tagName\":\"addresses\""+ "\"tagName\":\"addresses\""+
"}"; "}";
String expectedJSONArrayStr = String expectedJSONArrayStr =
"["+ "["+
"\"addresses\","+ "\"addresses\","+
"{"+ "{"+
@ -645,12 +645,12 @@ public class JSONMLTest {
JSONObject finalJsonObject = JSONML.toJSONObject(jsonObjectXmlToStr); JSONObject finalJsonObject = JSONML.toJSONObject(jsonObjectXmlToStr);
Util.compareActualVsExpectedJsonObjects(finalJsonObject, expectedJsonObject); Util.compareActualVsExpectedJsonObjects(finalJsonObject, expectedJsonObject);
// create a JSON array from the original string and make sure it // create a JSON array from the original string and make sure it
// looks as expected // looks as expected
JSONArray jsonArray = JSONML.toJSONArray(xmlStr); JSONArray jsonArray = JSONML.toJSONArray(xmlStr);
JSONArray expectedJsonArray = new JSONArray(expectedJSONArrayStr); JSONArray expectedJsonArray = new JSONArray(expectedJSONArrayStr);
Util.compareActualVsExpectedJsonArrays(jsonArray,expectedJsonArray); Util.compareActualVsExpectedJsonArrays(jsonArray,expectedJsonArray);
// restore the XML, then make another JSONArray and make sure it // restore the XML, then make another JSONArray and make sure it
// looks as expected // looks as expected
String jsonArrayXmlToStr = JSONML.toString(jsonArray); String jsonArrayXmlToStr = JSONML.toString(jsonArray);
@ -668,14 +668,14 @@ public class JSONMLTest {
* Convert an XML document which contains embedded comments into * Convert an XML document which contains embedded comments into
* a JSONArray. Use JSONML.toString() to turn it into a string, then * a JSONArray. Use JSONML.toString() to turn it into a string, then
* reconvert it into a JSONArray. Compare both JSONArrays to a control * reconvert it into a JSONArray. Compare both JSONArrays to a control
* JSONArray to confirm the contents. * JSONArray to confirm the contents.
* <p> * <p>
* This test shows how XML comments are handled. * This test shows how XML comments are handled.
*/ */
@Test @Test
public void commentsInXML() { public void commentsInXML() {
String xmlStr = String xmlStr =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<!-- this is a comment -->\n"+ "<!-- this is a comment -->\n"+
"<addresses>\n"+ "<addresses>\n"+
@ -734,7 +734,7 @@ public class JSONMLTest {
final String expectedJsonString = "[\"root\",[\"id\",\"01\"],[\"id\",\"1\"],[\"id\",\"00\"],[\"id\",\"0\"],[\"item\",{\"id\":\"01\"}],[\"title\",\"True\"]]"; final String expectedJsonString = "[\"root\",[\"id\",\"01\"],[\"id\",\"1\"],[\"id\",\"00\"],[\"id\",\"0\"],[\"item\",{\"id\":\"01\"}],[\"title\",\"True\"]]";
final JSONArray json = JSONML.toJSONArray(originalXml,true); final JSONArray json = JSONML.toJSONArray(originalXml,true);
assertEquals(expectedJsonString, json.toString()); assertEquals(expectedJsonString, json.toString());
final String reverseXml = JSONML.toString(json); final String reverseXml = JSONML.toString(json);
assertEquals(originalXml, reverseXml); assertEquals(originalXml, reverseXml);
} }
@ -749,7 +749,7 @@ public class JSONMLTest {
final String revertedXml = JSONML.toString(jsonArray); final String revertedXml = JSONML.toString(jsonArray);
assertEquals(revertedXml, originalXml); assertEquals(revertedXml, originalXml);
} }
/** /**
* JSON string cannot be reverted to original xml. See test result in * JSON string cannot be reverted to original xml. See test result in
* comment below. * comment below.
@ -770,7 +770,7 @@ public class JSONMLTest {
// 1. Our XML parser does not handle generic HTML entities, only valid XML entities. Hence &nbsp; // 1. Our XML parser does not handle generic HTML entities, only valid XML entities. Hence &nbsp;
// or other HTML specific entities would fail on reversability // or other HTML specific entities would fail on reversability
// 2. Our JSON implementation for storing the XML attributes uses the standard unordered map. // 2. Our JSON implementation for storing the XML attributes uses the standard unordered map.
// This means that <tag attr1="v1" attr2="v2" /> can not be reversed reliably. // This means that <tag attr1="v1" attr2="v2" /> can not be reversed reliably.
// //
// /** // /**
// * Test texts taken from jsonml.org. Currently our implementation FAILS this conversion but shouldn't. // * Test texts taken from jsonml.org. Currently our implementation FAILS this conversion but shouldn't.
@ -783,13 +783,13 @@ public class JSONMLTest {
// final String expectedJsonString = "[\"table\",{\"class\" : \"MyTable\",\"style\" : \"background-color:yellow\"},[\"tr\",[\"td\",{\"class\" : \"MyTD\",\"style\" : \"border:1px solid black\"},\"#550758\"],[\"td\",{\"class\" : \"MyTD\",\"style\" : \"background-color:red\"},\"Example text here\"]],[\"tr\",[\"td\",{\"class\" : \"MyTD\",\"style\" : \"border:1px solid black\"},\"#993101\"],[\"td\",{\"class\" : \"MyTD\",\"style\" : \"background-color:green\"},\"127624015\"]],[\"tr\",[\"td\",{\"class\" : \"MyTD\",\"style\" : \"border:1px solid black\"},\"#E33D87\"],[\"td\",{\"class\" : \"MyTD\",\"style\" : \"background-color:blue\"},\"\u00A0\",[\"span\",{ \"style\" : \"background-color:maroon\" },\"\u00A9\"],\"\u00A0\"]]]"; // final String expectedJsonString = "[\"table\",{\"class\" : \"MyTable\",\"style\" : \"background-color:yellow\"},[\"tr\",[\"td\",{\"class\" : \"MyTD\",\"style\" : \"border:1px solid black\"},\"#550758\"],[\"td\",{\"class\" : \"MyTD\",\"style\" : \"background-color:red\"},\"Example text here\"]],[\"tr\",[\"td\",{\"class\" : \"MyTD\",\"style\" : \"border:1px solid black\"},\"#993101\"],[\"td\",{\"class\" : \"MyTD\",\"style\" : \"background-color:green\"},\"127624015\"]],[\"tr\",[\"td\",{\"class\" : \"MyTD\",\"style\" : \"border:1px solid black\"},\"#E33D87\"],[\"td\",{\"class\" : \"MyTD\",\"style\" : \"background-color:blue\"},\"\u00A0\",[\"span\",{ \"style\" : \"background-color:maroon\" },\"\u00A9\"],\"\u00A0\"]]]";
// final JSONArray json = JSONML.toJSONArray(originalXml,true); // final JSONArray json = JSONML.toJSONArray(originalXml,true);
// final String actualJsonString = json.toString(); // final String actualJsonString = json.toString();
// //
// final String reverseXml = JSONML.toString(json); // final String reverseXml = JSONML.toString(json);
// assertNotEquals(originalXml, reverseXml); // assertNotEquals(originalXml, reverseXml);
// //
// assertNotEquals(expectedJsonString, actualJsonString); // assertNotEquals(expectedJsonString, actualJsonString);
// } // }
// //
// /** // /**
// * Test texts taken from jsonml.org but modified to have XML entities only. // * Test texts taken from jsonml.org but modified to have XML entities only.
// */ // */
@ -799,15 +799,15 @@ public class JSONMLTest {
// final String expectedJsonString = "[\"table\",{\"class\" : \"MyTable\",\"style\" : \"background-color:yellow\"},[\"tr\",[\"td\",{\"class\" : \"MyTD\",\"style\" : \"border:1px solid black\"},\"#550758\"],[\"td\",{\"class\" : \"MyTD\",\"style\" : \"background-color:red\"},\"Example text here\"]],[\"tr\",[\"td\",{\"class\" : \"MyTD\",\"style\" : \"border:1px solid black\"},\"#993101\"],[\"td\",{\"class\" : \"MyTD\",\"style\" : \"background-color:green\"},\"127624015\"]],[\"tr\",[\"td\",{\"class\" : \"MyTD\",\"style\" : \"border:1px solid black\"},\"#E33D87\"],[\"td\",{\"class\" : \"MyTD\",\"style\" : \"background-color:blue\"},\"&\",[\"span\",{ \"style\" : \"background-color:maroon\" },\">\"],\"<\"]]]"; // final String expectedJsonString = "[\"table\",{\"class\" : \"MyTable\",\"style\" : \"background-color:yellow\"},[\"tr\",[\"td\",{\"class\" : \"MyTD\",\"style\" : \"border:1px solid black\"},\"#550758\"],[\"td\",{\"class\" : \"MyTD\",\"style\" : \"background-color:red\"},\"Example text here\"]],[\"tr\",[\"td\",{\"class\" : \"MyTD\",\"style\" : \"border:1px solid black\"},\"#993101\"],[\"td\",{\"class\" : \"MyTD\",\"style\" : \"background-color:green\"},\"127624015\"]],[\"tr\",[\"td\",{\"class\" : \"MyTD\",\"style\" : \"border:1px solid black\"},\"#E33D87\"],[\"td\",{\"class\" : \"MyTD\",\"style\" : \"background-color:blue\"},\"&\",[\"span\",{ \"style\" : \"background-color:maroon\" },\">\"],\"<\"]]]";
// final JSONArray jsonML = JSONML.toJSONArray(originalXml,true); // final JSONArray jsonML = JSONML.toJSONArray(originalXml,true);
// final String actualJsonString = jsonML.toString(); // final String actualJsonString = jsonML.toString();
// //
// final String reverseXml = JSONML.toString(jsonML); // final String reverseXml = JSONML.toString(jsonML);
// // currently not equal because the hashing of the attribute objects makes the attribute // // currently not equal because the hashing of the attribute objects makes the attribute
// // order not happen the same way twice // // order not happen the same way twice
// assertEquals(originalXml, reverseXml); // assertEquals(originalXml, reverseXml);
// //
// assertEquals(expectedJsonString, actualJsonString); // assertEquals(expectedJsonString, actualJsonString);
// } // }
@Test (timeout = 6000) @Test (timeout = 6000)
public void testIssue484InfinteLoop1() { public void testIssue484InfinteLoop1() {
try { try {
@ -819,11 +819,11 @@ public class JSONMLTest {
ex.getMessage()); ex.getMessage());
} }
} }
@Test (timeout = 6000) @Test (timeout = 6000)
public void testIssue484InfinteLoop2() { public void testIssue484InfinteLoop2() {
try { try {
String input = "??*\n" + String input = "??*\n" +
"??|?CglR??`??>?w??PIlr??D?$?-?o??O?*??{OD?Y??`2a????NM?bq?:O?>S$ ?J?B.gUK?m\b??zE???!v]???????c??????h???s???g???`?qbi??:Zl?)?}1^??k?0??:$V?$?Ovs(}J??????2;gQ????Tg?K?`?h%c?hmGA?<!C*?9?~?t?)??,zA???S}?Q??.q?j????]"; "??|?CglR??`??>?w??PIlr??D?$?-?o??O?*??{OD?Y??`2a????NM?bq?:O?>S$ ?J?B.gUK?m\b??zE???!v]???????c??????h???s???g???`?qbi??:Zl?)?}1^??k?0??:$V?$?Ovs(}J??????2;gQ????Tg?K?`?h%c?hmGA?<!C*?9?~?t?)??,zA???S}?Q??.q?j????]";
JSONML.toJSONObject(input); JSONML.toJSONObject(input);
fail("Exception expected for invalid JSON."); fail("Exception expected for invalid JSON.");
@ -833,4 +833,64 @@ public class JSONMLTest {
ex.getMessage()); ex.getMessage());
} }
} }
@Test
public void testMaxNestingDepthOf42IsRespected() {
final String wayTooLongMalformedXML = new String(new char[6000]).replace("\0", "<a>");
final int maxNestingDepth = 42;
try {
JSONML.toJSONObject(wayTooLongMalformedXML, XMLtoJSONMLParserConfiguration.ORIGINAL.withMaxNestingDepth(maxNestingDepth));
fail("Expecting a JSONException");
} catch (JSONException e) {
assertTrue("Wrong throwable thrown: not expecting message <" + e.getMessage() + ">",
e.getMessage().startsWith("Maximum nesting depth of " + maxNestingDepth));
}
}
@Test
public void testMaxNestingDepthIsRespectedWithValidXML() {
final String perfectlyFineXML = "<Test>\n" +
" <employee>\n" +
" <name>sonoo</name>\n" +
" <salary>56000</salary>\n" +
" <married>true</married>\n" +
" </employee>\n" +
"</Test>\n";
final int maxNestingDepth = 1;
try {
JSONML.toJSONObject(perfectlyFineXML, XMLtoJSONMLParserConfiguration.ORIGINAL.withMaxNestingDepth(maxNestingDepth));
fail("Expecting a JSONException");
} catch (JSONException e) {
assertTrue("Wrong throwable thrown: not expecting message <" + e.getMessage() + ">",
e.getMessage().startsWith("Maximum nesting depth of " + maxNestingDepth));
}
}
@Test
public void testMaxNestingDepthWithValidFittingXML() {
final String perfectlyFineXML = "<Test>\n" +
" <employee>\n" +
" <name>sonoo</name>\n" +
" <salary>56000</salary>\n" +
" <married>true</married>\n" +
" </employee>\n" +
"</Test>\n";
final int maxNestingDepth = 3;
try {
JSONML.toJSONObject(perfectlyFineXML, XMLtoJSONMLParserConfiguration.ORIGINAL.withMaxNestingDepth(maxNestingDepth));
} catch (JSONException e) {
e.printStackTrace();
fail("XML document should be parsed as its maximum depth fits the maxNestingDepth " +
"parameter of the XMLtoJSONMLParserConfiguration used");
}
}
} }