
com.google.crypto.tink.internal.JsonParser Maven / Gradle / Ivy
Show all versions of tink-android Show documentation
/*
* Copyright 2011 Google LLC
*
* 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.google.crypto.tink.internal;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectInputStream;
import java.io.StringReader;
import java.math.BigDecimal;
import java.util.ArrayDeque;
import java.util.Deque;
import javax.annotation.Nullable;
/**
* A JSON Parser based on the GSON JsonReader.
*
* The parsing is almost identical to the normal parser provided by GSON with these changes: it
* never uses "lenient" mode, it rejects duplicated map keys and it rejects strings with invalid
* UTF16 characters.
*
*
The implementation is adapted from almost identical to GSON's TypeAdapters.JSON_ELEMENT.
*/
public final class JsonParser {
public static boolean isValidString(String s) {
int length = s.length();
int i = 0;
while (true) {
char ch;
do {
if (i == length) {
return true;
}
ch = s.charAt(i);
i++;
} while (!Character.isSurrogate(ch));
if (Character.isLowSurrogate(ch) || i == length || !Character.isLowSurrogate(s.charAt(i))) {
return false;
}
i++;
}
}
/** This is a modified copy of Gson's internal LazilyParsedNumber. */
@SuppressWarnings("serial") // Serialization is not supported. Throws NotSerializableException
private static final class LazilyParsedNumber extends Number {
private final String value;
public LazilyParsedNumber(String value) {
this.value = value;
}
@Override
public int intValue() {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
try {
return (int) Long.parseLong(value);
} catch (NumberFormatException nfe) {
return new BigDecimal(value).intValue();
}
}
}
@Override
public long longValue() {
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
return new BigDecimal(value).longValue();
}
}
@Override
public float floatValue() {
return Float.parseFloat(value);
}
@Override
public double doubleValue() {
return Double.parseDouble(value);
}
@Override
public String toString() {
return value;
}
private Object writeReplace() throws NotSerializableException {
throw new NotSerializableException("serialization is not supported");
}
private void readObject(ObjectInputStream in) throws NotSerializableException {
throw new NotSerializableException("serialization is not supported");
}
@Override
public int hashCode() {
return value.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof LazilyParsedNumber) {
LazilyParsedNumber other = (LazilyParsedNumber) obj;
return value.equals(other.value);
}
return false;
}
}
private static final class JsonElementTypeAdapter extends TypeAdapter {
private static final int RECURSION_LIMIT = 100;
/**
* Tries to begin reading a JSON array or JSON object, returning {@code null} if the next
* element is neither of those.
*/
@Nullable
private JsonElement tryBeginNesting(JsonReader in, JsonToken peeked) throws IOException {
switch (peeked) {
case BEGIN_ARRAY:
in.beginArray();
return new JsonArray();
case BEGIN_OBJECT:
in.beginObject();
return new JsonObject();
default:
return null;
}
}
/** Reads a {@link JsonElement} which cannot have any nested elements */
private JsonElement readTerminal(JsonReader in, JsonToken peeked) throws IOException {
switch (peeked) {
case STRING:
String value = in.nextString();
if (!isValidString(value)) {
throw new IOException("illegal characters in string");
}
return new JsonPrimitive(value);
case NUMBER:
String number = in.nextString();
return new JsonPrimitive(new LazilyParsedNumber(number));
case BOOLEAN:
return new JsonPrimitive(in.nextBoolean());
case NULL:
in.nextNull();
return JsonNull.INSTANCE;
default:
// When read(JsonReader) is called with JsonReader in invalid state
throw new IllegalStateException("Unexpected token: " + peeked);
}
}
@Override
public JsonElement read(JsonReader in) throws IOException {
// Either JsonArray or JsonObject
JsonElement current;
JsonToken peeked = in.peek();
current = tryBeginNesting(in, peeked);
if (current == null) {
return readTerminal(in, peeked);
}
Deque stack = new ArrayDeque<>();
while (true) {
while (in.hasNext()) {
String name = null;
// Name is only used for JSON object members
if (current instanceof JsonObject) {
name = in.nextName();
if (!isValidString(name)) {
throw new IOException("illegal characters in string");
}
}
peeked = in.peek();
JsonElement value = tryBeginNesting(in, peeked);
boolean isNesting = value != null;
if (value == null) {
value = readTerminal(in, peeked);
}
if (current instanceof JsonArray) {
((JsonArray) current).add(value);
} else {
if (((JsonObject) current).has(name)) {
throw new IOException("duplicate key: " + name);
}
((JsonObject) current).add(name, value);
}
if (isNesting) {
stack.addLast(current);
if (stack.size() > RECURSION_LIMIT) {
throw new IOException("too many recursions");
}
current = value;
}
}
// End current element
if (current instanceof JsonArray) {
in.endArray();
} else {
in.endObject();
}
if (stack.isEmpty()) {
return current;
} else {
// Continue with enclosing element
current = stack.removeLast();
}
}
}
@Override
public void write(JsonWriter out, JsonElement value) {
throw new UnsupportedOperationException("write is not supported");
}
}
private static final JsonElementTypeAdapter JSON_ELEMENT = new JsonElementTypeAdapter();
public static JsonElement parse(String json) throws IOException {
try {
JsonReader jsonReader = new JsonReader(new StringReader(json));
jsonReader.setLenient(false);
return JSON_ELEMENT.read(jsonReader);
} catch (NumberFormatException e) {
throw new IOException(e);
}
}
/**
* Converts a parsed {@link Number} into a long if it contains a valid long value.
*
* Requires that {@code number} is part of a output produced by {@link #parse}.
*
* @throws NumberFormatException if {@code number} does not contain a valid long value.
*
*/
public static long getParsedNumberAsLongOrThrow(Number number) {
if (!(number instanceof LazilyParsedNumber)) {
// We restrict this function to LazilyParsedNumber because then we know that "toString" will
// return the unparsed number. For other implementations of Number interface, it is not
// clearly defined what toString will return.
throw new IllegalArgumentException("does not contain a parsed number.");
}
// Note that we cannot use number.longValue() here, because longValue() does not throw an
// exception if number is not a valid long.
return Long.parseLong(number.toString());
}
private JsonParser() {}
}