Index: components/safe_json/android/java/src/org/chromium/components/safejson/JsonSanitizer.java |
diff --git a/components/safe_json/android/java/src/org/chromium/components/safejson/JsonSanitizer.java b/components/safe_json/android/java/src/org/chromium/components/safejson/JsonSanitizer.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..1fae2fd565a13abc13c6050bfd43f8bc3c6e9eae |
--- /dev/null |
+++ b/components/safe_json/android/java/src/org/chromium/components/safejson/JsonSanitizer.java |
@@ -0,0 +1,178 @@ |
+// Copyright 2015 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package org.chromium.components.safejson; |
+ |
+import android.util.JsonReader; |
+import android.util.JsonToken; |
+import android.util.JsonWriter; |
+import android.util.MalformedJsonException; |
+ |
+import org.chromium.base.CalledByNative; |
+import org.chromium.base.JNINamespace; |
+import org.chromium.base.StreamUtil; |
+ |
+import java.io.IOException; |
+import java.io.StringReader; |
+import java.io.StringWriter; |
+ |
+/** |
+ * Sanitizes and normalizes a JSON string by parsing it, checking for wellformedness, and |
+ * serializing it again. This class is meant to be used from native code. |
+ */ |
+@JNINamespace("safe_json") |
+public class JsonSanitizer { |
+ |
+ // Disallow instantiating the class. |
+ private JsonSanitizer() { |
+ } |
+ |
+ /** |
+ * The maximum nesting depth to which the native JSON parser restricts input in order to avoid |
+ * stack overflows. |
+ */ |
+ private static final int MAX_NESTING_DEPTH = 100; |
+ |
+ @CalledByNative |
+ public static void sanitize(long nativePtr, String unsafeJson) { |
+ JsonReader reader = new JsonReader(new StringReader(unsafeJson)); |
+ StringWriter stringWriter = new StringWriter(unsafeJson.length()); |
+ JsonWriter writer = new JsonWriter(stringWriter); |
+ StackChecker stackChecker = new StackChecker(); |
+ try { |
+ boolean end = false; |
+ while (!end) { |
+ JsonToken token = reader.peek(); |
+ switch (token) { |
+ case BEGIN_ARRAY: |
+ stackChecker.increaseAndCheck(); |
+ reader.beginArray(); |
+ writer.beginArray(); |
+ break; |
+ case END_ARRAY: |
+ stackChecker.decrease(); |
+ reader.endArray(); |
+ writer.endArray(); |
+ break; |
+ case BEGIN_OBJECT: |
+ stackChecker.increaseAndCheck(); |
+ reader.beginObject(); |
+ writer.beginObject(); |
+ break; |
+ case END_OBJECT: |
+ stackChecker.decrease(); |
+ reader.endObject(); |
+ writer.endObject(); |
+ break; |
+ case NAME: |
+ writer.name(sanitizeString(reader.nextName())); |
+ break; |
+ case STRING: |
+ writer.value(sanitizeString(reader.nextString())); |
+ break; |
+ case NUMBER: { |
+ // Read the value as a string, then try to parse it first as a long, then as |
+ // a double. |
+ String value = reader.nextString(); |
+ try { |
+ writer.value(Long.parseLong(value)); |
+ } catch (NumberFormatException e) { |
+ writer.value(Double.parseDouble(value)); |
+ } |
+ break; |
+ } |
+ case BOOLEAN: |
+ writer.value(reader.nextBoolean()); |
+ break; |
+ case NULL: |
+ reader.nextNull(); |
+ writer.nullValue(); |
+ break; |
+ case END_DOCUMENT: |
+ end = true; |
+ break; |
+ } |
+ } |
+ } catch (IOException | IllegalStateException e) { |
+ nativeOnError(nativePtr, e.getMessage()); |
+ return; |
+ } finally { |
+ StreamUtil.closeQuietly(reader); |
+ StreamUtil.closeQuietly(writer); |
+ } |
+ nativeOnSuccess(nativePtr, stringWriter.toString()); |
+ } |
+ |
+ /** |
+ * Helper class to check nesting depth of JSON expressions. |
+ */ |
+ private static class StackChecker { |
+ private int mStackDepth = 0; |
+ |
+ public void increaseAndCheck() { |
+ if (++mStackDepth >= MAX_NESTING_DEPTH) { |
+ throw new IllegalStateException("Too much nesting"); |
+ } |
+ } |
+ |
+ public void decrease() { |
+ mStackDepth--; |
+ } |
+ } |
+ |
+ private static String sanitizeString(String string) throws MalformedJsonException { |
+ if (!checkString(string)) { |
+ throw new MalformedJsonException("Invalid escape sequence"); |
+ } |
+ return string; |
+ } |
+ |
+ /** |
+ * Checks whether a given String is well-formed UTF-16, i.e. all surrogates appear in high-low |
+ * pairs and each code point is a valid character. |
+ * |
+ * @param string The string to check. |
+ * @return Whether the given string is well-formed UTF-16. |
+ */ |
+ private static boolean checkString(String string) { |
+ int length = string.length(); |
+ for (int i = 0; i < length; i++) { |
+ char c = string.charAt(i); |
+ // Check that surrogates only appear in pairs of a high surrogate followed by a low |
+ // surrogate. |
+ // A lone low surrogate is not allowed. |
+ if (Character.isLowSurrogate(c)) return false; |
+ |
+ int codePoint; |
+ if (Character.isHighSurrogate(c)) { |
+ // A high surrogate has to be followed by a low surrogate. |
+ char high = c; |
+ if (++i >= length) return false; |
+ |
+ char low = string.charAt(i); |
+ if (!Character.isLowSurrogate(low)) return false; |
+ |
+ // Decode the high-low pair into a code point. |
+ codePoint = Character.toCodePoint(high, low); |
+ } else { |
+ // The code point is neither a low surrogate nor a high surrogate, so we just need |
+ // to check that it's a valid character. |
+ codePoint = c; |
+ } |
+ |
+ if (!isUnicodeCharacter(codePoint)) return false; |
+ } |
+ return true; |
+ } |
+ |
+ private static boolean isUnicodeCharacter(int codePoint) { |
+ // See the native method base::IsValidCharacter(). |
+ return codePoint < 0xD800 || (codePoint >= 0xE000 && codePoint < 0xFDD0) |
+ || (codePoint > 0xFDEF && codePoint <= 0x10FFFF && (codePoint & 0xFFFE) != 0xFFFE); |
+ } |
+ |
+ private static native void nativeOnSuccess(long id, String json); |
+ |
+ private static native void nativeOnError(long id, String error); |
+} |