• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2011 Google LLC
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.crypto.tink.internal;
18 
19 import com.google.gson.JsonArray;
20 import com.google.gson.JsonElement;
21 import com.google.gson.JsonNull;
22 import com.google.gson.JsonObject;
23 import com.google.gson.JsonPrimitive;
24 import com.google.gson.TypeAdapter;
25 import com.google.gson.stream.JsonReader;
26 import com.google.gson.stream.JsonToken;
27 import com.google.gson.stream.JsonWriter;
28 import java.io.IOException;
29 import java.io.NotSerializableException;
30 import java.io.ObjectInputStream;
31 import java.io.StringReader;
32 import java.math.BigDecimal;
33 import java.util.ArrayDeque;
34 import java.util.Deque;
35 import javax.annotation.Nullable;
36 
37 /**
38  * A JSON Parser based on the GSON JsonReader.
39  *
40  * <p>The parsing is almost identical to the normal parser provided by GSON with these changes: it
41  * never uses "lenient" mode, it rejects duplicated map keys and it rejects strings with invalid
42  * UTF16 characters.
43  *
44  * <p>The implementation is adapted from almost identical to GSON's TypeAdapters.JSON_ELEMENT.
45  */
46 public final class JsonParser {
47 
isValidString(String s)48   public static boolean isValidString(String s) {
49     int length = s.length();
50     int i = 0;
51     while (true) {
52       char ch;
53       do {
54         if (i == length) {
55           return true;
56         }
57         ch = s.charAt(i);
58         i++;
59       } while (!Character.isSurrogate(ch));
60       if (Character.isLowSurrogate(ch) || i == length || !Character.isLowSurrogate(s.charAt(i))) {
61         return false;
62       }
63       i++;
64     }
65   }
66 
67   /** This is a modified copy of Gson's internal LazilyParsedNumber. */
68   @SuppressWarnings("serial") // Serialization is not supported. Throws NotSerializableException
69   private static final class LazilyParsedNumber extends Number {
70     private final String value;
71 
LazilyParsedNumber(String value)72     public LazilyParsedNumber(String value) {
73       this.value = value;
74     }
75 
76     @Override
intValue()77     public int intValue() {
78       try {
79         return Integer.parseInt(value);
80       } catch (NumberFormatException e) {
81         try {
82           return (int) Long.parseLong(value);
83         } catch (NumberFormatException nfe) {
84           return new BigDecimal(value).intValue();
85         }
86       }
87     }
88 
89     @Override
longValue()90     public long longValue() {
91       try {
92         return Long.parseLong(value);
93       } catch (NumberFormatException e) {
94         return new BigDecimal(value).longValue();
95       }
96     }
97 
98     @Override
floatValue()99     public float floatValue() {
100       return Float.parseFloat(value);
101     }
102 
103     @Override
doubleValue()104     public double doubleValue() {
105       return Double.parseDouble(value);
106     }
107 
108     @Override
toString()109     public String toString() {
110       return value;
111     }
112 
writeReplace()113     private Object writeReplace() throws NotSerializableException {
114       throw new NotSerializableException("serialization is not supported");
115     }
116 
readObject(ObjectInputStream in)117     private void readObject(ObjectInputStream in) throws NotSerializableException {
118       throw new NotSerializableException("serialization is not supported");
119     }
120 
121     @Override
hashCode()122     public int hashCode() {
123       return value.hashCode();
124     }
125 
126     @Override
equals(Object obj)127     public boolean equals(Object obj) {
128       if (this == obj) {
129         return true;
130       }
131       if (obj instanceof LazilyParsedNumber) {
132         LazilyParsedNumber other = (LazilyParsedNumber) obj;
133         return value.equals(other.value);
134       }
135       return false;
136     }
137   }
138 
139   private static final class JsonElementTypeAdapter extends TypeAdapter<JsonElement> {
140 
141     private static final int RECURSION_LIMIT = 100;
142 
143     /**
144      * Tries to begin reading a JSON array or JSON object, returning {@code null} if the next
145      * element is neither of those.
146      */
147     @Nullable
tryBeginNesting(JsonReader in, JsonToken peeked)148     private JsonElement tryBeginNesting(JsonReader in, JsonToken peeked) throws IOException {
149       switch (peeked) {
150         case BEGIN_ARRAY:
151           in.beginArray();
152           return new JsonArray();
153         case BEGIN_OBJECT:
154           in.beginObject();
155           return new JsonObject();
156         default:
157           return null;
158       }
159     }
160 
161     /** Reads a {@link JsonElement} which cannot have any nested elements */
readTerminal(JsonReader in, JsonToken peeked)162     private JsonElement readTerminal(JsonReader in, JsonToken peeked) throws IOException {
163       switch (peeked) {
164         case STRING:
165           String value = in.nextString();
166           if (!isValidString(value)) {
167             throw new IOException("illegal characters in string");
168           }
169           return new JsonPrimitive(value);
170         case NUMBER:
171           String number = in.nextString();
172           return new JsonPrimitive(new LazilyParsedNumber(number));
173         case BOOLEAN:
174           return new JsonPrimitive(in.nextBoolean());
175         case NULL:
176           in.nextNull();
177           return JsonNull.INSTANCE;
178         default:
179           // When read(JsonReader) is called with JsonReader in invalid state
180           throw new IllegalStateException("Unexpected token: " + peeked);
181       }
182     }
183 
184     @Override
read(JsonReader in)185     public JsonElement read(JsonReader in) throws IOException {
186       // Either JsonArray or JsonObject
187       JsonElement current;
188       JsonToken peeked = in.peek();
189 
190       current = tryBeginNesting(in, peeked);
191       if (current == null) {
192         return readTerminal(in, peeked);
193       }
194 
195       Deque<JsonElement> stack = new ArrayDeque<>();
196 
197       while (true) {
198         while (in.hasNext()) {
199           String name = null;
200           // Name is only used for JSON object members
201           if (current instanceof JsonObject) {
202             name = in.nextName();
203             if (!isValidString(name)) {
204               throw new IOException("illegal characters in string");
205             }
206           }
207 
208           peeked = in.peek();
209           JsonElement value = tryBeginNesting(in, peeked);
210           boolean isNesting = value != null;
211 
212           if (value == null) {
213             value = readTerminal(in, peeked);
214           }
215 
216           if (current instanceof JsonArray) {
217             ((JsonArray) current).add(value);
218           } else {
219             if (((JsonObject) current).has(name)) {
220               throw new IOException("duplicate key: " + name);
221             }
222             ((JsonObject) current).add(name, value);
223           }
224 
225           if (isNesting) {
226             stack.addLast(current);
227             if (stack.size() > RECURSION_LIMIT) {
228                throw new IOException("too many recursions");
229             }
230             current = value;
231           }
232         }
233 
234         // End current element
235         if (current instanceof JsonArray) {
236           in.endArray();
237         } else {
238           in.endObject();
239         }
240 
241         if (stack.isEmpty()) {
242           return current;
243         } else {
244           // Continue with enclosing element
245           current = stack.removeLast();
246         }
247       }
248     }
249 
250     @Override
write(JsonWriter out, JsonElement value)251     public void write(JsonWriter out, JsonElement value) {
252       throw new UnsupportedOperationException("write is not supported");
253     }
254   }
255 
256   private static final JsonElementTypeAdapter JSON_ELEMENT = new JsonElementTypeAdapter();
257 
parse(String json)258   public static JsonElement parse(String json) throws IOException {
259     try {
260       JsonReader jsonReader = new JsonReader(new StringReader(json));
261       jsonReader.setLenient(false);
262       return JSON_ELEMENT.read(jsonReader);
263     } catch (NumberFormatException e) {
264       throw new IOException(e);
265     }
266   }
267 
268   /*
269    * Converts a parsed {@link JsonElement} into a long if it contains a valid long value.
270    *
271    * <p>Requires that {@code element} is part of a output produced by {@link #parse}.
272    *
273    * @throws NumberFormatException if {@code element} does not contain a valid long value.
274    *
275    */
getParsedNumberAsLongOrThrow(JsonElement element)276   public static long getParsedNumberAsLongOrThrow(JsonElement element) {
277     Number num = element.getAsNumber();
278     if (!(num instanceof LazilyParsedNumber)) {
279       // We restrict this function to LazilyParsedNumber because then we know that "toString" will
280       // return the unparsed number. For other implementations of Number interface, it is not
281       // clearly defined what toString will return.
282       throw new IllegalArgumentException("does not contain a parsed number.");
283     }
284     return Long.parseLong(element.getAsNumber().toString());
285   }
286 
JsonParser()287   private JsonParser() {}
288 }
289