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