blob: 8446cf97a041ce6a1c2325d2de8d1fce36883c36 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.noggit;
import java.io.IOException;
import java.io.StringReader;
import org.apache.solr.SolrTestCaseJ4;
import org.junit.Test;
public class TestJSONParser extends SolrTestCaseJ4 {
// these are to aid in debugging if an unexpected error occurs
static int parserType;
static int bufferSize;
static String parserInput;
static JSONParser lastParser;
static int flags = JSONParser.FLAGS_DEFAULT; // the default
public static String lastParser() {
return "parserType=" + parserType
+ (parserType==1 ? " bufferSize=" + bufferSize : "")
+ " parserInput='" + parserInput + "'" + "flags : " + lastParser.flags;
}
public static JSONParser getParser(String s) {
return getParser(s, random().nextInt(2), -1);
}
public static JSONParser getParser(String s, int type, int bufSize) {
parserInput = s;
parserType = type;
JSONParser parser=null;
switch (type) {
case 0:
// test directly using input buffer
parser = new JSONParser(s.toCharArray(),0,s.length());
break;
case 1:
// test using Reader...
// small input buffers can help find bugs on boundary conditions
if (bufSize < 1) bufSize = random().nextInt(25) + 1;
bufferSize = bufSize;// record in case there is an error
parser = new JSONParser(new StringReader(s), new char[bufSize]);
break;
}
if (parser == null) return null;
lastParser = parser;
if (flags != JSONParser.FLAGS_DEFAULT) {
parser.setFlags(flags);
}
return parser;
}
/** for debugging purposes
public void testSpecific() throws Exception {
JSONParser parser = getParser("[0",1,1);
for (;;) {
int ev = parser.nextEvent();
if (ev == JSONParser.EOF) {
break;
} else {
System.out.println("got " + JSONParser.getEventString(ev));
}
}
}
**/
public static byte[] events = new byte[256];
static {
events['{'] = JSONParser.OBJECT_START;
events['}'] = JSONParser.OBJECT_END;
events['['] = JSONParser.ARRAY_START;
events[']'] = JSONParser.ARRAY_END;
events['s'] = JSONParser.STRING;
events['b'] = JSONParser.BOOLEAN;
events['l'] = JSONParser.LONG;
events['n'] = JSONParser.NUMBER;
events['N'] = JSONParser.BIGNUMBER;
events['0'] = JSONParser.NULL;
events['e'] = JSONParser.EOF;
}
// match parser states with the expected states
public static void parse(JSONParser p, String input, String expected) throws IOException {
expected += "e";
for (int i=0; i<expected.length(); i++) {
int ev = p.nextEvent();
int expect = events[expected.charAt(i)];
if (ev != expect) {
fail("Expected " + expect + ", got " + ev
+ "\n\tINPUT=" + input
+ "\n\tEXPECTED=" + expected
+ "\n\tAT=" + i + " (" + expected.charAt(i) + ")");
}
}
}
public static void parse(String input, String expected) throws IOException {
String in = input;
if ((flags & JSONParser.ALLOW_SINGLE_QUOTES)==0 || random().nextBoolean()) {
in = in.replace('\'', '"');
}
for (int i=0; i<Integer.MAX_VALUE; i++) {
JSONParser p = getParser(in,i,-1);
if (p==null) break;
parse(p,in,expected);
}
testCorruption(input, 100000);
}
public static void testCorruption(String input, int iter) {
char[] arr = new char[input.length()];
for (int i=0; i<iter; i++) {
input.getChars(0, arr.length, arr, 0);
int changes = random().nextInt(arr.length>>1) + 1;
for (int j=0; j<changes; j++) {
char ch;
switch (random().nextInt(31)) {
case 0: ch = 0; break;
case 1: ch = '['; break;
case 2: ch = ']'; break;
case 3: ch = '{'; break;
case 4: ch = '}'; break;
case 5: ch = '"'; break;
case 6: ch = '\''; break;
case 7: ch = ' '; break;
case 8: ch = '\r'; break;
case 9: ch = '\n'; break;
case 10:ch = '\t'; break;
case 11:ch = ','; break;
case 12:ch = ':'; break;
case 13:ch = '.'; break;
case 14:ch = 'a'; break;
case 15:ch = 'e'; break;
case 16:ch = '0'; break;
case 17:ch = '1'; break;
case 18:ch = '+'; break;
case 19:ch = '-'; break;
case 20:ch = 't'; break;
case 21:ch = 'f'; break;
case 22:ch = 'n'; break;
case 23:ch = '/'; break;
case 24:ch = '\\'; break;
case 25:ch = 'u'; break;
case 26:ch = '\u00a0'; break;
default:ch = (char) random().nextInt(256);
}
arr[random().nextInt(arr.length)] = ch;
}
JSONParser parser = getParser(new String(arr));
parser.setFlags( random().nextInt() ); // set random parser flags
int ret = 0;
try {
for (;;) {
int ev = parser.nextEvent();
if (random().nextBoolean()) {
// see if we can read the event
switch (ev) {
case JSONParser.STRING: ret += parser.getString().length(); break;
case JSONParser.BOOLEAN: ret += parser.getBoolean() ? 1 : 2; break;
case JSONParser.BIGNUMBER: ret += parser.getNumberChars().length(); break;
case JSONParser.NUMBER: ret += parser.getDouble(); break;
case JSONParser.LONG: ret += parser.getLong(); break;
default: ret += ev;
}
}
if (ev == JSONParser.EOF) break;
}
} catch (IOException ex) {
// shouldn't happen
System.out.println(ret); // use ret
} catch (JSONParser.ParseException ex) {
// OK
} catch (Throwable ex) {
ex.printStackTrace();
System.out.println(lastParser());
throw new RuntimeException(ex);
}
}
}
public static class Num {
public String digits;
public Num(String digits) {
this.digits = digits;
}
@Override
public String toString() { return new String("NUMBERSTRING("+digits+")"); }
@Override
public boolean equals(Object o) {
return (getClass() == o.getClass() && digits.equals(((Num) o).digits));
}
@Override
public int hashCode() {
return digits.hashCode();
}
}
public static class BigNum extends Num {
@Override
public String toString() { return new String("BIGNUM("+digits+")"); }
public BigNum(String digits) { super(digits); }
}
// Oh, what I wouldn't give for Java5 varargs and autoboxing
public static Long o(int l) { return (long) l; }
public static Long o(long l) { return l; }
public static Double o(double d) { return d; }
public static Boolean o(boolean b) { return b; }
public static Num n(String digits) { return new Num(digits); }
public static Num bn(String digits) { return new BigNum(digits); }
public static Object t = Boolean.TRUE;
public static Object f = Boolean.FALSE;
public static Object a = new Object(){@Override
public String toString() {return "ARRAY_START";}};
public static Object A = new Object(){@Override
public String toString() {return "ARRAY_END";}};
public static Object m = new Object(){@Override
public String toString() {return "OBJECT_START";}};
public static Object M = new Object(){@Override
public String toString() {return "OBJECT_END";}};
public static Object N = new Object(){@Override
public String toString() {return "NULL";}};
public static Object e = new Object(){@Override
public String toString() {return "EOF";}};
// match parser states with the expected states
public static void parse(JSONParser p, String input, Object[] expected) throws IOException {
for (int i=0; i<expected.length; i++) {
int ev = p.nextEvent();
Object exp = expected[i];
Object got = null;
switch(ev) {
case JSONParser.ARRAY_START: got=a; break;
case JSONParser.ARRAY_END: got=A; break;
case JSONParser.OBJECT_START: got=m; break;
case JSONParser.OBJECT_END: got=M; break;
case JSONParser.LONG: got=o(p.getLong()); break;
case JSONParser.NUMBER:
if (exp instanceof Double) {
got = o(p.getDouble());
} else {
got = n(p.getNumberChars().toString());
}
break;
case JSONParser.BIGNUMBER: got=bn(p.getNumberChars().toString()); break;
case JSONParser.NULL: got=N; p.getNull(); break; // optional
case JSONParser.BOOLEAN: got=o(p.getBoolean()); break;
case JSONParser.EOF: got=e; break;
case JSONParser.STRING: got=p.getString(); break;
default: got="Unexpected Event Number " + ev;
}
if (!(exp==got || exp.equals(got))) {
fail("Fail: String='" + input + "'" + "\n\tINPUT=" + got + "\n\tEXPECTED=" + exp + "\n\tAT RULE " + i);
}
}
}
public static void parse(String input, Object[] expected) throws IOException {
parse(input, (flags & JSONParser.ALLOW_SINGLE_QUOTES)==0 || random().nextBoolean(), expected);
}
public static void parse(String input, boolean changeSingleQuote, Object[] expected) throws IOException {
String in = input;
if (changeSingleQuote) {
in = in.replace('\'', '"');
}
for (int i=0; i<Integer.MAX_VALUE; i++) {
JSONParser p = getParser(in,i,-1);
if (p == null) break;
parse(p,in,expected);
}
}
public static void err(String input) throws IOException {
try {
JSONParser p = getParser(input);
while (p.nextEvent() != JSONParser.EOF) {}
} catch (Exception e) {
return;
}
fail("Input should failed:'" + input + "'");
}
@Test
public void testNull() throws IOException {
flags = JSONParser.FLAGS_STRICT;
err("nul");
err("n");
err("nullz");
err("[nullz]");
flags = JSONParser.FLAGS_DEFAULT;
parse("[null]","[0]");
parse("{'hi':null}",new Object[]{m,"hi",N,M,e});
}
@Test
public void testBool() throws IOException {
flags = JSONParser.FLAGS_STRICT;
err("[True]");
err("[False]");
err("[TRUE]");
err("[FALSE]");
err("[truex]");
err("[falsex]");
err("[tru]");
err("[fals]");
err("[tru");
err("[fals");
err("t");
err("f");
flags = JSONParser.FLAGS_DEFAULT;
parse("[false,true, false , true ]",new Object[]{a,f,t,f,t,A,e});
}
@Test
public void testString() throws IOException {
// NOTE: single quotes are converted to double quotes by this
// testsuite!
err("[']");
err("[',]");
err("{'}");
err("{',}");
err("['\\u111']");
err("['\\u11']");
err("['\\u1']");
err("['\\']");
flags = JSONParser.FLAGS_STRICT;
err("['\\ ']"); // escape of non-special char
err("['\\U1111']"); // escape of non-special char
flags = JSONParser.FLAGS_DEFAULT;
parse("['\\ ']", new Object[]{a, " ", A, e}); // escape of non-special char
parse("['\\U1111']", new Object[]{a, "U1111", A, e}); // escape of non-special char
parse("['']",new Object[]{a,"",A,e});
parse("['\\\\']",new Object[]{a,"\\",A,e});
parse("['X\\\\']",new Object[]{a,"X\\",A,e});
parse("['\\\\X']",new Object[]{a,"\\X",A,e});
parse("[\"\\\"\"]",new Object[]{a,"\"",A,e});
parse("['\\'']", true, new Object[]{a,"\"",A,e});
parse("['\\'']", false, new Object[]{a,"'",A,e});
String esc="\\n\\r\\tX\\b\\f\\/\\\\X\\\"";
String exp="\n\r\tX\b\f/\\X\"";
parse("['" + esc + "']",new Object[]{a,exp,A,e});
parse("['" + esc+esc+esc+esc+esc + "']",new Object[]{a,exp+exp+exp+exp+exp,A,e});
esc="\\u004A";
exp="\u004A";
parse("['" + esc + "']",new Object[]{a,exp,A,e});
esc="\\u0000\\u1111\\u2222\\u12AF\\u12BC\\u19DE";
exp="\u0000\u1111\u2222\u12AF\u12BC\u19DE";
parse("['" + esc + "']",new Object[]{a,exp,A,e});
}
@Test
public void testNumbers() throws IOException {
flags = JSONParser.FLAGS_STRICT;
err("[00]");
err("[003]");
err("[00.3]");
err("[1e1.1]");
err("[+1]");
err("[NaN]");
err("[Infinity]");
err("[--1]");
flags = JSONParser.FLAGS_DEFAULT;
String lmin = "-9223372036854775808";
String lminNot = "-9223372036854775809";
String lmax = "9223372036854775807";
String lmaxNot = "9223372036854775808";
String bignum="12345678987654321357975312468642099775533112244668800152637485960987654321";
parse("[0,1,-1,543,-876]", new Object[]{a,o(0),o(1),o(-1),o(543),o(-876),A,e});
parse("[-0]",new Object[]{a,o(0),A,e});
parse("["+lmin +"," + lmax+"]",
new Object[]{a,o(Long.MIN_VALUE),o(Long.MAX_VALUE),A,e});
parse("["+bignum+"]", new Object[]{a,bn(bignum),A,e});
parse("["+"-"+bignum+"]", new Object[]{a,bn("-"+bignum),A,e});
parse("["+lminNot+"]",new Object[]{a,bn(lminNot),A,e});
parse("["+lmaxNot+"]",new Object[]{a,bn(lmaxNot),A,e});
parse("["+lminNot + "," + lmaxNot + "]",
new Object[]{a,bn(lminNot),bn(lmaxNot),A,e});
// bignum many digits on either side of decimal
String t = bignum + "." + bignum;
parse("["+t+","+"-"+t+"]", new Object[]{a,bn(t),bn("-"+t),A,e});
err("[" + t+".1" + "]"); // extra decimal
err("[" + "-"+t+".1" + "]");
// bignum exponent w/o fraction
t = "1" + "e+" + bignum;
parse("["+t+","+"-"+t+"]", new Object[]{a,bn(t),bn("-"+t),A,e});
t = "1" + "E+" + bignum;
parse("["+t+","+"-"+t+"]", new Object[]{a,bn(t),bn("-"+t),A,e});
t = "1" + "e" + bignum;
parse("["+t+","+"-"+t+"]", new Object[]{a,bn(t),bn("-"+t),A,e});
t = "1" + "E" + bignum;
parse("["+t+","+"-"+t+"]", new Object[]{a,bn(t),bn("-"+t),A,e});
t = "1" + "e-" + bignum;
parse("["+t+","+"-"+t+"]", new Object[]{a,bn(t),bn("-"+t),A,e});
t = "1" + "E-" + bignum;
parse("["+t+","+"-"+t+"]", new Object[]{a,bn(t),bn("-"+t),A,e});
t = bignum + "e+" + bignum;
parse("["+t+","+"-"+t+"]", new Object[]{a,bn(t),bn("-"+t),A,e});
t = bignum + "E-" + bignum;
parse("["+t+","+"-"+t+"]", new Object[]{a,bn(t),bn("-"+t),A,e});
t = bignum + "e" + bignum;
parse("["+t+","+"-"+t+"]", new Object[]{a,bn(t),bn("-"+t),A,e});
t = bignum + "." + bignum + "e" + bignum;
parse("["+t+","+"-"+t+"]", new Object[]{a,bn(t),bn("-"+t),A,e});
err("[1E]");
err("[1E-]");
err("[1E+]");
err("[1E+.3]");
err("[1E+0.3]");
err("[1E+1e+3]");
err("["+bignum+"e"+"]");
err("["+bignum+"e-"+"]");
err("["+bignum+"e+"+"]");
err("["+bignum+"."+bignum+"."+bignum+"]");
double[] vals = new double[] {0,0.1,1.1,
Double.MAX_VALUE,
Double.MIN_VALUE,
2.2250738585072014E-308, /* Double.MIN_NORMAL */
};
for (int i=0; i<vals.length; i++) {
double d = vals[i];
parse("["+d+","+-d+"]", new Object[]{a,o(d),o(-d),A,e});
}
// MIN_NORMAL has the max number of digits (23), so check that
// adding an extra digit causes BIGNUM to be returned.
t = "2.2250738585072014E-308" + "0";
parse("["+t+","+"-"+t+"]", new Object[]{a,bn(t),bn("-"+t),A,e});
// check it works with a leading zero too
t = "0.2250738585072014E-308" + "0";
parse("["+t+","+"-"+t+"]", new Object[]{a,bn(t),bn("-"+t),A,e});
// check that overflow detection is working properly w/ numbers that don't cause a wrap to negatives
// when multiplied by 10
t = "1910151821265210155" + "0";
parse("["+t+","+"-"+t+"]", new Object[]{a,bn(t),bn("-"+t),A,e});
for (int i=0; i<1000000; i++) {
long val = random().nextLong();
String sval = Long.toString(val);
JSONParser parser = getParser("["+val+"]");
parser.nextEvent();
assertTrue(parser.nextEvent() == JSONParser.LONG);
if (random().nextBoolean()) {
assertEquals(val, parser.getLong());
} else {
CharArr chars = parser.getNumberChars();
assertEquals(sval, chars.toString());
}
}
}
@Test
public void testArray() throws IOException {
parse("[]","[]");
parse("[ ]","[]");
parse(" \r\n\t[\r\t\n ]\r\n\t ","[]");
parse("[0]","[l]");
parse("['0']","[s]");
parse("[0,'0',0.1]","[lsn]");
parse("[[[[[]]]]]","[[[[[]]]]]");
parse("[[[[[0]]]]]","[[[[[l]]]]]");
err("]");
err("[");
err("[[]");
err("[]]");
err("[}");
err("{]");
err("['a':'b']");
flags=JSONParser.FLAGS_STRICT;
err("[,]"); // test that extra commas fail
err("[[],]");
err("['a',]");
err("['a',]");
flags=JSONParser.FLAGS_DEFAULT;
parse("[,]","[]"); // test extra commas
parse("[,,]","[]");
parse("[,,,]","[]");
parse("[[],]","[[]]");
parse("[[,],]","[[]]");
parse("[[,,],,]","[[]]");
parse("[,[,,],,]","[[]]");
parse("[,5,[,,5],,]","[l[l]]");
}
@Test
public void testObject() throws IOException {
parse("{}","{}");
parse("{}","{}");
parse(" \r\n\t{\r\t\n }\r\n\t ","{}");
parse("{'':null}","{s0}");
err("}");
err("[}]");
err("{");
err("[{]");
err("{{}");
err("[{{}]");
err("{}}");
err("[{}}]");
err("{1}");
err("[{1}]");
err("{'a'}");
err("{'a','b'}");
err("{[]:'b'}");
err("{{'a':'b'}:'c'}");
err("{'a','b'}}");
// bare strings allow these to pass
flags=JSONParser.FLAGS_STRICT;
err("{null:'b'}");
err("{true:'b'}");
err("{false:'b'}");
err("{,}"); // test that extra commas fail
err("{{},}");
err("{'a':'b',}");
flags=JSONParser.FLAGS_DEFAULT;
parse("{}", new Object[]{m,M,e});
parse("{,}", new Object[]{m,M,e});
parse("{,,}", new Object[]{m,M,e});
parse("{'a':{},}", new Object[]{m,"a",m,M,M,e});
parse("{'a':{},,}", new Object[]{m,"a",m,M,M,e});
parse("{,'a':{,},,}", new Object[]{m,"a",m,M,M,e});
parse("{'a':'b'}", new Object[]{m,"a","b",M,e});
parse("{'a':5}", new Object[]{m,"a",o(5),M,e});
parse("{'a':null}", new Object[]{m,"a",N,M,e});
parse("{'a':[]}", new Object[]{m,"a",a,A,M,e});
parse("{'a':{'b':'c'}}", new Object[]{m,"a",m,"b","c",M,M,e});
String big = "Now is the time for all good men to come to the aid of their country!";
String bigger = big+big+big+big+big;
parse("{'"+bigger+"':'"+bigger+"','a':'b'}", new Object[]{m,bigger,bigger,"a","b",M,e});
flags=JSONParser.ALLOW_UNQUOTED_KEYS;
parse("{a:'b'}", new Object[]{m,"a","b",M,e});
parse("{null:'b'}", new Object[]{m,"null","b",M,e});
parse("{true: 'b'}", new Object[]{m,"true","b",M,e});
parse("{ false :'b'}", new Object[]{m,"false","b",M,e});
parse("{null:null, true : true , false : false , x:'y',a:'b'}", new Object[]{m,"null",N,"true",t,"false",f,"x","y","a","b",M,e});
flags = JSONParser.FLAGS_DEFAULT | JSONParser.ALLOW_MISSING_COLON_COMMA_BEFORE_OBJECT;
parse("{'a'{'b':'c'}}", new Object[]{m, "a", m, "b", "c", M, M, e});
parse("{'a': [{'b':'c'} {'b':'c'}]}", new Object[]{m, "a",a, m, "b", "c", M, m, "b", "c", M,A, M, e});
parse("{'a' [{'b':'c'} {'b':'c'}]}", new Object[]{m, "a", a, m, "b", "c", M, m, "b", "c", M, A, M, e});
parse("{'a':[['b']['c']]}", new Object[]{m, "a", a, a, "b", A, a, "c", A, A, M, e});
parse("{'a': {'b':'c'} 'd': {'e':'f'}}", new Object[]{m, "a", m, "b", "c",M, "d", m,"e","f", M, M, e});
parse("{'a': {'b':'c'} d: {'e':'f'}}", new Object[]{m, "a", m, "b", "c",M, "d", m,"e","f", M, M, e});
flags = JSONParser.FLAGS_DEFAULT | JSONParser.ALLOW_MISSING_COLON_COMMA_BEFORE_OBJECT | JSONParser.OPTIONAL_OUTER_BRACES;
parse("'a':{'b':'c'}", new Object[]{m, "a", m, "b", "c", M, M, e});
parse("'a':{'b':'c'}", true, new Object[]{m, "a", m, "b", "c", M, M, e});
parse("a:'b'", new Object[]{m, "a", "b", M, e});
flags = JSONParser.FLAGS_DEFAULT;
}
@Test
public void testBareString() throws Exception {
flags=JSONParser.ALLOW_UNQUOTED_STRING_VALUES | JSONParser.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER;
String[] strings = new String[] {"t","f","n","a","tru","fals","nul","abc","trueX","falseXY","nullZ","truetrue", "$true", "a.bc.def","a_b-c/d"};
for (String s : strings) {
parse(s, new Object[]{s, e});
parse("[" + s + "]", new Object[]{a, s, A, e});
parse("[ " + s + ", "+s +" ]", new Object[]{a, s, s, A, e});
parse("[" + s + ","+s +"]", new Object[]{a, s, s, A, e});
parse("\u00a0[\u00a0\r\n\t\u00a0" + s + "\u00a0,\u00a0\u00a0"+s +"\u00a0]\u00a0", new Object[]{a, s, s, A, e});
}
flags |= JSONParser.ALLOW_UNQUOTED_KEYS;
for (String s : strings) {
parse("{" + s + ":" + s + "}", new Object[]{m, s, s, M, e});
parse("{ " + s + " \t\n\r:\t\n\r " + s + "\t\n\r}", new Object[]{m, s, s, M, e});
}
parse("{true:true, false:false, null:null}",new Object[]{m,"true",t,"false",f,"null",N,M,e});
flags=JSONParser.FLAGS_DEFAULT;
}
@Test
public void testAPI() throws IOException {
JSONParser p = new JSONParser("[1,2]");
assertEquals(JSONParser.ARRAY_START, p.nextEvent());
// no nextEvent for the next objects...
assertEquals(1,p.getLong());
assertEquals(2,p.getLong());
}
@Test
public void testComments() throws IOException {
parse("#pre comment\n{//before a\n 'a' /* after a **/ #before separator\n : /* after separator */ {/*before b*/'b'#after b\n://before c\n'c'/*after c*/}/*after close*/}#post comment no EOL", new Object[]{m,"a",m,"b","c",M,M,e});
}
// rfc7159 permits any JSON values to be top level values
@Test
public void testTopLevelValues() throws Exception {
parse("\"\"", new Object[]{""});
parse("\"hello\"", new Object[]{"hello"});
parse("true", new Object[]{t});
parse("false", new Object[]{f});
parse("null", new Object[]{N});
parse("42", new Object[]{42L});
parse("1.414", new Object[]{1.414d});
parse("/*comment*/1.414/*more comment*/", new Object[]{1.414d});
}
}