Update Statement Service.

JSONObject parser is too lenient when parsing Json string. Security review
suggested us to use a stricter parser, which we implemented with
JsonReader in this CL.

BUG=20665035

Change-Id: I379976731a1d35ef8ec746f3a6e78be998370f00
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AbstractAsset.java b/packages/StatementService/src/com/android/statementservice/retriever/AbstractAsset.java
index bb6bdbb..8d6fd66 100644
--- a/packages/StatementService/src/com/android/statementservice/retriever/AbstractAsset.java
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AbstractAsset.java
@@ -16,6 +16,13 @@
 
 package com.android.statementservice.retriever;
 
+import android.util.JsonReader;
+
+import org.json.JSONException;
+
+import java.io.IOException;
+import java.io.StringReader;
+
 /**
  * A handle representing the identity and address of some digital asset. An asset is an online
  * entity that typically provides some service or content. Examples of assets are websites, Android
@@ -61,7 +68,14 @@
      */
     public static AbstractAsset create(String assetJson)
             throws AssociationServiceException {
-        return AssetFactory.create(assetJson);
+        JsonReader reader = new JsonReader(new StringReader(assetJson));
+        reader.setLenient(false);
+        try {
+            return AssetFactory.create(JsonParser.parse(reader));
+        } catch (JSONException | IOException e) {
+            throw new AssociationServiceException(
+                    "Input is not a well formatted asset descriptor.", e);
+        }
     }
 
     /**
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AssetFactory.java b/packages/StatementService/src/com/android/statementservice/retriever/AssetFactory.java
index 762365e..519d73a 100644
--- a/packages/StatementService/src/com/android/statementservice/retriever/AssetFactory.java
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AssetFactory.java
@@ -16,7 +16,6 @@
 
 package com.android.statementservice.retriever;
 
-import org.json.JSONException;
 import org.json.JSONObject;
 
 /**
@@ -29,25 +28,11 @@
     private AssetFactory() {}
 
     /**
-     * Creates a new Asset object from its JSON string representation.
-     *
-     * @throws AssociationServiceException if the assetJson is not well formatted.
-     */
-    public static AbstractAsset create(String assetJson) throws AssociationServiceException {
-        try {
-            return create(new JSONObject(assetJson));
-        } catch (JSONException e) {
-            throw new AssociationServiceException(
-                    "Input is not a well formatted asset descriptor.");
-        }
-    }
-
-    /**
      * Checks that the input is a valid asset with purposes.
      *
      * @throws AssociationServiceException if the asset is not well formatted.
      */
-    private static AbstractAsset create(JSONObject asset)
+    public static AbstractAsset create(JSONObject asset)
             throws AssociationServiceException {
         String namespace = asset.optString(Utils.NAMESPACE_FIELD, null);
         if (namespace == null) {
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/DirectStatementRetriever.java b/packages/StatementService/src/com/android/statementservice/retriever/DirectStatementRetriever.java
index 548149e..2ca85e9 100644
--- a/packages/StatementService/src/com/android/statementservice/retriever/DirectStatementRetriever.java
+++ b/packages/StatementService/src/com/android/statementservice/retriever/DirectStatementRetriever.java
@@ -167,7 +167,7 @@
                                 .getStatements());
             }
             return Result.create(statements, webContent.getExpireTimeMillis());
-        } catch (JSONException e) {
+        } catch (JSONException | IOException e) {
             return Result.create(statements, DO_NOT_CACHE_RESULT);
         }
     }
@@ -202,7 +202,7 @@
             }
 
             return Result.create(statements, DO_NOT_CACHE_RESULT);
-        } catch (JSONException | NameNotFoundException e) {
+        } catch (JSONException | IOException | NameNotFoundException e) {
             Log.w(DirectStatementRetriever.class.getSimpleName(), e);
             return Result.create(Collections.<Statement>emptyList(), DO_NOT_CACHE_RESULT);
         }
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/JsonParser.java b/packages/StatementService/src/com/android/statementservice/retriever/JsonParser.java
new file mode 100644
index 0000000..ce063ea
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/JsonParser.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed 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 com.android.statementservice.retriever;
+
+import android.util.JsonReader;
+import android.util.JsonToken;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A helper class that creates a {@link JSONObject} from a {@link JsonReader}.
+ */
+public final class JsonParser {
+
+    private JsonParser() {}
+
+    /**
+     * Consumes and parses exactly one JSON object from the {@link JsonReader}.
+     * The object's fields can only be objects, strings or arrays of strings.
+     */
+    public static JSONObject parse(JsonReader reader) throws IOException, JSONException {
+        JSONObject output = new JSONObject();
+        String errorMsg = null;
+
+        reader.beginObject();
+        while (reader.hasNext()) {
+            String fieldName = reader.nextName();
+
+            if (output.has(fieldName)) {
+                errorMsg = "Duplicate field name.";
+                reader.skipValue();
+                continue;
+            }
+
+            JsonToken token = reader.peek();
+            if (token.equals(JsonToken.BEGIN_ARRAY)) {
+                output.put(fieldName, new JSONArray(parseArray(reader)));
+            } else if (token.equals(JsonToken.STRING)) {
+                output.put(fieldName, reader.nextString());
+            } else if (token.equals(JsonToken.BEGIN_OBJECT)) {
+                try {
+                    output.put(fieldName, parse(reader));
+                } catch (JSONException e) {
+                    errorMsg = e.getMessage();
+                }
+            } else {
+                reader.skipValue();
+                errorMsg = "Unsupported value type.";
+            }
+        }
+        reader.endObject();
+
+        if (errorMsg != null) {
+            throw new JSONException(errorMsg);
+        }
+
+        return output;
+    }
+
+    /**
+     * Parses one string array from the {@link JsonReader}.
+     */
+    public static List<String> parseArray(JsonReader reader) throws IOException {
+        ArrayList<String> output = new ArrayList<>();
+
+        reader.beginArray();
+        while (reader.hasNext()) {
+            output.add(reader.nextString());
+        }
+        reader.endArray();
+
+        return output;
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/StatementParser.java b/packages/StatementService/src/com/android/statementservice/retriever/StatementParser.java
index bcd91bd..0369718 100644
--- a/packages/StatementService/src/com/android/statementservice/retriever/StatementParser.java
+++ b/packages/StatementService/src/com/android/statementservice/retriever/StatementParser.java
@@ -16,10 +16,14 @@
 
 package com.android.statementservice.retriever;
 
+import android.util.JsonReader;
+
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import java.io.IOException;
+import java.io.StringReader;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -28,20 +32,33 @@
  */
 /* package private */ final class StatementParser {
 
+    private static final String FIELD_NOT_STRING_FORMAT_STRING = "Expected %s to be string.";
+    private static final String FIELD_NOT_ARRAY_FORMAT_STRING = "Expected %s to be array.";
+
     /**
      * Parses a JSON array of statements.
      */
     static ParsedStatement parseStatementList(String statementList, AbstractAsset source)
-            throws JSONException, AssociationServiceException {
+            throws JSONException, IOException {
         List<Statement> statements = new ArrayList<Statement>();
         List<String> delegates = new ArrayList<String>();
 
-        JSONArray statementsJson = new JSONArray(statementList);
-        for (int i = 0; i < statementsJson.length(); i++) {
-            ParsedStatement result = parseStatement(statementsJson.getString(i), source);
+        JsonReader reader = new JsonReader(new StringReader(statementList));
+        reader.setLenient(false);
+
+        reader.beginArray();
+        while (reader.hasNext()) {
+            ParsedStatement result;
+            try {
+                result = parseStatement(reader, source);
+            } catch (AssociationServiceException e) {
+                // The element in the array is well formatted Json but not a well-formed Statement.
+                continue;
+            }
             statements.addAll(result.getStatements());
             delegates.addAll(result.getDelegates());
         }
+        reader.endArray();
 
         return new ParsedStatement(statements, delegates);
     }
@@ -50,17 +67,39 @@
      * Parses a single JSON statement.
      */
     static ParsedStatement parseStatement(String statementString, AbstractAsset source)
-            throws JSONException, AssociationServiceException {
+            throws AssociationServiceException, IOException, JSONException {
+        JsonReader reader = new JsonReader(new StringReader(statementString));
+        reader.setLenient(false);
+        return parseStatement(reader, source);
+    }
+
+    /**
+     * Parses a single JSON statement. This method guarantees that exactly one JSON object
+     * will be consumed.
+     */
+    static ParsedStatement parseStatement(JsonReader reader, AbstractAsset source)
+            throws JSONException, AssociationServiceException, IOException {
         List<Statement> statements = new ArrayList<Statement>();
         List<String> delegates = new ArrayList<String>();
-        JSONObject statement = new JSONObject(statementString);
+
+        JSONObject statement = JsonParser.parse(reader);
+
         if (statement.optString(Utils.DELEGATE_FIELD_DELEGATE, null) != null) {
             delegates.add(statement.optString(Utils.DELEGATE_FIELD_DELEGATE));
         } else {
-            AbstractAsset target = AssetFactory
-                    .create(statement.getString(Utils.ASSET_DESCRIPTOR_FIELD_TARGET));
-            JSONArray relations = statement.getJSONArray(
-                    Utils.ASSET_DESCRIPTOR_FIELD_RELATION);
+            JSONObject targetObject = statement.optJSONObject(Utils.ASSET_DESCRIPTOR_FIELD_TARGET);
+            if (targetObject == null) {
+                throw new AssociationServiceException(String.format(
+                        FIELD_NOT_STRING_FORMAT_STRING, Utils.ASSET_DESCRIPTOR_FIELD_TARGET));
+            }
+
+            JSONArray relations = statement.optJSONArray(Utils.ASSET_DESCRIPTOR_FIELD_RELATION);
+            if (relations == null) {
+                throw new AssociationServiceException(String.format(
+                        FIELD_NOT_ARRAY_FORMAT_STRING, Utils.ASSET_DESCRIPTOR_FIELD_RELATION));
+            }
+
+            AbstractAsset target = AssetFactory.create(targetObject);
             for (int i = 0; i < relations.length(); i++) {
                 statements.add(Statement
                         .create(source, target, Relation.create(relations.getString(i))));