Author: yonik
Date: Wed May  4 18:54:26 2011
New Revision: 1099557

URL: http://svn.apache.org/viewvc?rev=1099557&view=rev
Log:
noggit: fix hang on bad input, fix long overflow detection, add random testing

Modified:
    labs/noggit/src/main/java/org/apache/noggit/JSONParser.java
    labs/noggit/src/test/java/org/apache/noggit/TestJSONParser.java

Modified: labs/noggit/src/main/java/org/apache/noggit/JSONParser.java
URL: 
http://svn.apache.org/viewvc/labs/noggit/src/main/java/org/apache/noggit/JSONParser.java?rev=1099557&r1=1099556&r2=1099557&view=diff
==============================================================================
--- labs/noggit/src/main/java/org/apache/noggit/JSONParser.java (original)
+++ labs/noggit/src/main/java/org/apache/noggit/JSONParser.java Wed May  4 
18:54:26 2011
@@ -57,6 +57,12 @@ public class JSONParser {
   /** Event indicating the end of input has been reached */
   public static final int EOF=11;
 
+  public static class ParseException extends RuntimeException {
+    public ParseException(String msg) {
+      super(msg);
+    }
+  }
+
   public static String getEventString( int e )
   {
     switch( e )
@@ -202,7 +208,7 @@ public class JSONParser {
     }
   }
 
-  private RuntimeException err(String msg) {
+  private ParseException err(String msg) {
     // We can't tell if EOF was hit by comparing start<=end
     // because the illegal char could have been the last in the buffer
     // or in the stream.  To deal with this, the "eof" var was introduced
@@ -214,13 +220,13 @@ public class JSONParser {
       if (start>=end) msg = "Unexpected EOF";
       else msg="JSON Parse Error";
     }
-    return new RuntimeException(msg + ": " + tot);
+    return new ParseException(msg + ": " + tot);
   }
 
   private String getContext() {
     String context = "";
     if (start>=0) {
-      context += " BEFORE='" + errEscape(Math.max(start-40,0), start+1) + "'";
+      context += " BEFORE='" + errEscape(Math.max(start-60,0), start+1) + "'";
     }
     if (start<end) {
       context += " AFTER='" + errEscape(start+1, start+40) + "'";
@@ -249,7 +255,10 @@ public class JSONParser {
     // We build up the number in the negative plane since it's larger (by one) 
than
     // the positive plane.
     long v = '0' - firstChar;
-    for (int i=0; i<22; i++) {
+     // can't overflow a long in 18 decimal digits (i.e. 17 additional after 
the first).
+     // we also need 22 additional to handle double so we'll handle in 2 
separate loops.
+    int i;
+    for (i=0; i<17; i++) {
       int ch = getChar();
       // TODO: is this switch faster as an if-then-else?
       switch(ch) {
@@ -263,7 +272,7 @@ public class JSONParser {
         case '7':
         case '8':
         case '9':
-          v = v *10 - (ch-'0');
+          v = v*10 - (ch-'0');
           out.unsafeWrite(ch);
           continue;
         case '.':
@@ -281,19 +290,56 @@ public class JSONParser {
           // for invalid chars following the number.
           if (ch!=-1) --start;   // push back last char if not EOF
 
-          // the max number of digits we are reading only allows for
-          // a long to wrap once, so we can just check if the sign is
-          // what is expected to detect an overflow.
-          if (isNeg) {
-            // -0 is allowed by the spec
-            valstate = v<=0 ? LONG : BIGNUMBER;
-          } else {
-            v=-v;
-            valstate = v>=0 ? LONG : BIGNUMBER;
-          }
-          return v;
+          valstate = LONG;
+          return isNeg ? v : -v;
       }
     }
+
+    // after this, we could overflow a long and need to do extra checking
+    boolean overflow = false;
+    long maxval = isNeg ? Long.MIN_VALUE : -Long.MAX_VALUE;
+
+    for (; i<22; i++) {
+      int ch = getChar();
+      switch(ch) {
+        case '0':
+        case '1':
+        case '2':
+        case '3':
+        case '4':
+        case '5':
+        case '6':
+        case '7':
+        case '8':
+        case '9':
+          if (v < (0x8000000000000000L/10)) overflow=true;  // can't multiply 
by 10 w/o overflowing
+          v *= 10;
+          int digit = ch - '0';
+          if (v < maxval + digit) overflow=true; // can't add digit w/o 
overflowing
+          v -= digit;
+          out.unsafeWrite(ch);
+          continue;
+        case '.':
+          out.unsafeWrite('.');
+          valstate = readFrac(out,22-i);
+          return 0;
+        case 'e':
+        case 'E':
+          out.unsafeWrite(ch);
+          nstate=0;
+          valstate = readExp(out,22-i);
+          return 0;
+        default:
+          // return the number, relying on nextEvent() to return an error
+          // for invalid chars following the number.
+          if (ch!=-1) --start;   // push back last char if not EOF
+
+          valstate = overflow ? BIGNUMBER : LONG;
+          return isNeg ? v : -v;
+      }
+    }
+
+
     nstate=0;
     valstate = BIGNUMBER;
     return 0;
@@ -445,6 +491,7 @@ public class JSONParser {
     for (;;) {
       if (middle>=end) {
         arr.write(buf,start,middle-start);
+        start=middle;
         getMore();
         middle=start;
       }
@@ -527,7 +574,7 @@ public class JSONParser {
             return valstate;
           } else if (ch>'9' || ch<'0') {
             out.unsafeWrite('0');
-            start--;
+            if (ch!=-1) start--;
             lval = 0;
             valstate=LONG;
             return LONG;

Modified: labs/noggit/src/test/java/org/apache/noggit/TestJSONParser.java
URL: 
http://svn.apache.org/viewvc/labs/noggit/src/test/java/org/apache/noggit/TestJSONParser.java?rev=1099557&r1=1099556&r2=1099557&view=diff
==============================================================================
--- labs/noggit/src/test/java/org/apache/noggit/TestJSONParser.java (original)
+++ labs/noggit/src/test/java/org/apache/noggit/TestJSONParser.java Wed May  4 
18:54:26 2011
@@ -29,13 +29,28 @@ import java.io.IOException;
  */
 public class TestJSONParser extends TestCase {
 
-  public static Random r = new Random(0);
+  public static Random r = new Random();
+
+  // these are to aid in debugging if an unexpected error occurs
+  static int parserType;
+  static int bufferSize;
+  static String parserInput;
+  static JSONParser lastParser;
+
+  public static String lastParser() {
+    return "parserType=" + parserType
+            + (parserType==1 ? " bufferSize=" + bufferSize : "")
+            + " parserInput='" + parserInput + "'";
+  }
 
   public static JSONParser getParser(String s) {
-    return getParser(s, r.nextInt(2));
+    return getParser(s, r.nextInt(2), -1);
   }
   
-  public static JSONParser getParser(String s, int type) {
+  public static JSONParser getParser(String s, int type, int bufSize) {
+    parserInput = s;
+    parserType = type;
+
     JSONParser parser=null;
     switch (type) {
       case 0:
@@ -45,11 +60,28 @@ public class TestJSONParser extends Test
       case 1:
         // test using Reader...
         // small input buffers can help find bugs on boundary conditions
-        parser = new JSONParser(new StringReader(s), new 
char[r.nextInt(25)+1]);
+
+        if (bufSize < 1) bufSize = r.nextInt(25) + 1;
+        bufferSize = bufSize;// record in case there is an error
+        parser = new JSONParser(new StringReader(s), new char[bufSize]);
+        break;
+    }
+    return lastParser = 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));
+      }
     }
-    return parser;
   }
+  **/
 
   public static byte[] events = new byte[256];
   static {
@@ -84,10 +116,87 @@ public class TestJSONParser extends Test
   public static void parse(String input, String expected) throws IOException {
     input = input.replace('\'','"');
     for (int i=0; i<Integer.MAX_VALUE; i++) {
-      JSONParser p = getParser(input,i);
+      JSONParser p = getParser(input,i,-1);
       if (p==null) break;
       parse(p,input,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 = r.nextInt(arr.length>>1) + 1;
+      for (int j=0; j<changes; j++) {
+        char ch;
+        switch (r.nextInt(30)) {
+          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;
+          default:ch = (char)r.nextInt(256);
+        }
+
+        arr[r.nextInt(arr.length)] = ch;
+      }
+
+
+      JSONParser parser = getParser(new String(arr));
+      int ret = 0;
+      try {
+        for (;;) {
+          int ev = parser.nextEvent();
+          if (r.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);
+      }
+    }
   }
 
 
@@ -165,7 +274,7 @@ public class TestJSONParser extends Test
   public static void parse(String input, Object[] expected) throws IOException 
{
     input = input.replace('\'','"');
     for (int i=0; i<Integer.MAX_VALUE; i++) {
-      JSONParser p = getParser(input,i);
+      JSONParser p = getParser(input,i,-1);
       if (p==null) break;
       parse(p,input,expected);
     }
@@ -332,6 +441,26 @@ public class TestJSONParser extends Test
     // 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 = r.nextLong();
+      String sval = Long.toString(val);
+      JSONParser parser = getParser("["+val+"]");
+      parser.nextEvent();
+      assertTrue(parser.nextEvent() == JSONParser.LONG);
+      if (r.nextBoolean()) {
+        assertEquals(val, parser.getLong());
+      } else {
+        CharArr chars = parser.getNumberChars();
+        assertEquals(sval, chars.toString());
+      }
+    }
+
   }
 
   public void testArray() throws IOException {



---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to