From 7b419cd62c06b42dd6afef8074fd2c695ed468d1 Mon Sep 17 00:00:00 2001 From: denrase Date: Wed, 23 Jun 2021 16:37:20 +0200 Subject: [PATCH 001/157] add json writer from gson --- .../java/io/sentry/json/stream/JsonScope.java | 71 ++ .../io/sentry/json/stream/JsonWriter.java | 659 ++++++++++++++++++ .../io/sentry/json/stream/JsonWriterTest.java | 648 +++++++++++++++++ 3 files changed, 1378 insertions(+) create mode 100644 sentry/src/main/java/io/sentry/json/stream/JsonScope.java create mode 100644 sentry/src/main/java/io/sentry/json/stream/JsonWriter.java create mode 100644 sentry/src/test/java/io/sentry/json/stream/JsonWriterTest.java diff --git a/sentry/src/main/java/io/sentry/json/stream/JsonScope.java b/sentry/src/main/java/io/sentry/json/stream/JsonScope.java new file mode 100644 index 00000000000..71e7e425e99 --- /dev/null +++ b/sentry/src/main/java/io/sentry/json/stream/JsonScope.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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 io.sentry.json.stream; + +/** + * Lexical scoping elements within a JSON reader or writer. + * + * @author Jesse Wilson + * @since 1.6 + */ +final class JsonScope { + + /** + * An array with no elements requires no separators or newlines before + * it is closed. + */ + static final int EMPTY_ARRAY = 1; + + /** + * A array with at least one value requires a comma and newline before + * the next element. + */ + static final int NONEMPTY_ARRAY = 2; + + /** + * An object with no name/value pairs requires no separators or newlines + * before it is closed. + */ + static final int EMPTY_OBJECT = 3; + + /** + * An object whose most recent element is a key. The next element must + * be a value. + */ + static final int DANGLING_NAME = 4; + + /** + * An object with at least one name/value pair requires a comma and + * newline before the next element. + */ + static final int NONEMPTY_OBJECT = 5; + + /** + * No object or array has been started. + */ + static final int EMPTY_DOCUMENT = 6; + + /** + * A document with at an array or object. + */ + static final int NONEMPTY_DOCUMENT = 7; + + /** + * A document that's been closed and cannot be accessed. + */ + static final int CLOSED = 8; +} diff --git a/sentry/src/main/java/io/sentry/json/stream/JsonWriter.java b/sentry/src/main/java/io/sentry/json/stream/JsonWriter.java new file mode 100644 index 00000000000..ab1622d7d39 --- /dev/null +++ b/sentry/src/main/java/io/sentry/json/stream/JsonWriter.java @@ -0,0 +1,659 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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 io.sentry.json.stream; + +import java.io.Closeable; +import java.io.Flushable; +import java.io.IOException; +import java.io.Writer; +import java.util.Arrays; + +import static io.sentry.json.stream.JsonScope.DANGLING_NAME; +import static io.sentry.json.stream.JsonScope.EMPTY_ARRAY; +import static io.sentry.json.stream.JsonScope.EMPTY_DOCUMENT; +import static io.sentry.json.stream.JsonScope.EMPTY_OBJECT; +import static io.sentry.json.stream.JsonScope.NONEMPTY_ARRAY; +import static io.sentry.json.stream.JsonScope.NONEMPTY_DOCUMENT; +import static io.sentry.json.stream.JsonScope.NONEMPTY_OBJECT; + +/** + * Writes a JSON (RFC 7159) + * encoded value to a stream, one token at a time. The stream includes both + * literal values (strings, numbers, booleans and nulls) as well as the begin + * and end delimiters of objects and arrays. + * + *

Encoding JSON

+ * To encode your data as JSON, create a new {@code JsonWriter}. Each JSON + * document must contain one top-level array or object. Call methods on the + * writer as you walk the structure's contents, nesting arrays and objects as + * necessary: + * + * + *

Example

+ * Suppose we'd like to encode a stream of messages such as the following:
 {@code
+ * [
+ *   {
+ *     "id": 912345678901,
+ *     "text": "How do I stream JSON in Java?",
+ *     "geo": null,
+ *     "user": {
+ *       "name": "json_newb",
+ *       "followers_count": 41
+ *      }
+ *   },
+ *   {
+ *     "id": 912345678902,
+ *     "text": "@json_newb just use JsonWriter!",
+ *     "geo": [50.454722, -104.606667],
+ *     "user": {
+ *       "name": "jesse",
+ *       "followers_count": 2
+ *     }
+ *   }
+ * ]}
+ * This code encodes the above structure:
   {@code
+ *   public void writeJsonStream(OutputStream out, List messages) throws IOException {
+ *     JsonWriter writer = new JsonWriter(new OutputStreamWriter(out, "UTF-8"));
+ *     writer.setIndent("    ");
+ *     writeMessagesArray(writer, messages);
+ *     writer.close();
+ *   }
+ *
+ *   public void writeMessagesArray(JsonWriter writer, List messages) throws IOException {
+ *     writer.beginArray();
+ *     for (Message message : messages) {
+ *       writeMessage(writer, message);
+ *     }
+ *     writer.endArray();
+ *   }
+ *
+ *   public void writeMessage(JsonWriter writer, Message message) throws IOException {
+ *     writer.beginObject();
+ *     writer.name("id").value(message.getId());
+ *     writer.name("text").value(message.getText());
+ *     if (message.getGeo() != null) {
+ *       writer.name("geo");
+ *       writeDoublesArray(writer, message.getGeo());
+ *     } else {
+ *       writer.name("geo").nullValue();
+ *     }
+ *     writer.name("user");
+ *     writeUser(writer, message.getUser());
+ *     writer.endObject();
+ *   }
+ *
+ *   public void writeUser(JsonWriter writer, User user) throws IOException {
+ *     writer.beginObject();
+ *     writer.name("name").value(user.getName());
+ *     writer.name("followers_count").value(user.getFollowersCount());
+ *     writer.endObject();
+ *   }
+ *
+ *   public void writeDoublesArray(JsonWriter writer, List doubles) throws IOException {
+ *     writer.beginArray();
+ *     for (Double value : doubles) {
+ *       writer.value(value);
+ *     }
+ *     writer.endArray();
+ *   }}
+ * + *

Each {@code JsonWriter} may be used to write a single JSON stream. + * Instances of this class are not thread safe. Calls that would result in a + * malformed JSON string will fail with an {@link IllegalStateException}. + * + * @author Jesse Wilson + * @since 1.6 + */ +@SuppressWarnings({"cast", "Nopen", "MissingOverride", "OperatorPrecedence"}) // Ignore warnings to preserve original code. +public class JsonWriter implements Closeable, Flushable { + + /* + * From RFC 7159, "All Unicode characters may be placed within the + * quotation marks except for the characters that must be escaped: + * quotation mark, reverse solidus, and the control characters + * (U+0000 through U+001F)." + * + * We also escape '\u2028' and '\u2029', which JavaScript interprets as + * newline characters. This prevents eval() from failing with a syntax + * error. http://code.google.com/p/google-gson/issues/detail?id=341 + */ + private static final String[] REPLACEMENT_CHARS; + private static final String[] HTML_SAFE_REPLACEMENT_CHARS; + static { + REPLACEMENT_CHARS = new String[128]; + for (int i = 0; i <= 0x1f; i++) { + REPLACEMENT_CHARS[i] = String.format("\\u%04x", (int) i); + } + REPLACEMENT_CHARS['"'] = "\\\""; + REPLACEMENT_CHARS['\\'] = "\\\\"; + REPLACEMENT_CHARS['\t'] = "\\t"; + REPLACEMENT_CHARS['\b'] = "\\b"; + REPLACEMENT_CHARS['\n'] = "\\n"; + REPLACEMENT_CHARS['\r'] = "\\r"; + REPLACEMENT_CHARS['\f'] = "\\f"; + HTML_SAFE_REPLACEMENT_CHARS = REPLACEMENT_CHARS.clone(); + HTML_SAFE_REPLACEMENT_CHARS['<'] = "\\u003c"; + HTML_SAFE_REPLACEMENT_CHARS['>'] = "\\u003e"; + HTML_SAFE_REPLACEMENT_CHARS['&'] = "\\u0026"; + HTML_SAFE_REPLACEMENT_CHARS['='] = "\\u003d"; + HTML_SAFE_REPLACEMENT_CHARS['\''] = "\\u0027"; + } + + /** The output data, containing at most one top-level array or object. */ + private final Writer out; + + private int[] stack = new int[32]; + private int stackSize = 0; + { + push(EMPTY_DOCUMENT); + } + + /** + * A string containing a full set of spaces for a single level of + * indentation, or null for no pretty printing. + */ + private String indent; + + /** + * The name/value separator; either ":" or ": ". + */ + private String separator = ":"; + + private boolean lenient; + + private boolean htmlSafe; + + private String deferredName; + + private boolean serializeNulls = true; + + /** + * Creates a new instance that writes a JSON-encoded stream to {@code out}. + * For best performance, ensure {@link Writer} is buffered; wrapping in + * {@link java.io.BufferedWriter BufferedWriter} if necessary. + */ + public JsonWriter(Writer out) { + if (out == null) { + throw new NullPointerException("out == null"); + } + this.out = out; + } + + /** + * Sets the indentation string to be repeated for each level of indentation + * in the encoded document. If {@code indent.isEmpty()} the encoded document + * will be compact. Otherwise the encoded document will be more + * human-readable. + * + * @param indent a string containing only whitespace. + */ + public final void setIndent(String indent) { + if (indent.length() == 0) { + this.indent = null; + this.separator = ":"; + } else { + this.indent = indent; + this.separator = ": "; + } + } + + /** + * Configure this writer to relax its syntax rules. By default, this writer + * only emits well-formed JSON as specified by RFC 7159. Setting the writer + * to lenient permits the following: + *

+ */ + public final void setLenient(boolean lenient) { + this.lenient = lenient; + } + + /** + * Returns true if this writer has relaxed syntax rules. + */ + public boolean isLenient() { + return lenient; + } + + /** + * Configure this writer to emit JSON that's safe for direct inclusion in HTML + * and XML documents. This escapes the HTML characters {@code <}, {@code >}, + * {@code &} and {@code =} before writing them to the stream. Without this + * setting, your XML/HTML encoder should replace these characters with the + * corresponding escape sequences. + */ + public final void setHtmlSafe(boolean htmlSafe) { + this.htmlSafe = htmlSafe; + } + + /** + * Returns true if this writer writes JSON that's safe for inclusion in HTML + * and XML documents. + */ + public final boolean isHtmlSafe() { + return htmlSafe; + } + + /** + * Sets whether object members are serialized when their value is null. + * This has no impact on array elements. The default is true. + */ + public final void setSerializeNulls(boolean serializeNulls) { + this.serializeNulls = serializeNulls; + } + + /** + * Returns true if object members are serialized when their value is null. + * This has no impact on array elements. The default is true. + */ + public final boolean getSerializeNulls() { + return serializeNulls; + } + + /** + * Begins encoding a new array. Each call to this method must be paired with + * a call to {@link #endArray}. + * + * @return this writer. + */ + public JsonWriter beginArray() throws IOException { + writeDeferredName(); + return open(EMPTY_ARRAY, '['); + } + + /** + * Ends encoding the current array. + * + * @return this writer. + */ + public JsonWriter endArray() throws IOException { + return close(EMPTY_ARRAY, NONEMPTY_ARRAY, ']'); + } + + /** + * Begins encoding a new object. Each call to this method must be paired + * with a call to {@link #endObject}. + * + * @return this writer. + */ + public JsonWriter beginObject() throws IOException { + writeDeferredName(); + return open(EMPTY_OBJECT, '{'); + } + + /** + * Ends encoding the current object. + * + * @return this writer. + */ + public JsonWriter endObject() throws IOException { + return close(EMPTY_OBJECT, NONEMPTY_OBJECT, '}'); + } + + /** + * Enters a new scope by appending any necessary whitespace and the given + * bracket. + */ + private JsonWriter open(int empty, char openBracket) throws IOException { + beforeValue(); + push(empty); + out.write(openBracket); + return this; + } + + /** + * Closes the current scope by appending any necessary whitespace and the + * given bracket. + */ + private JsonWriter close(int empty, int nonempty, char closeBracket) + throws IOException { + int context = peek(); + if (context != nonempty && context != empty) { + throw new IllegalStateException("Nesting problem."); + } + if (deferredName != null) { + throw new IllegalStateException("Dangling name: " + deferredName); + } + + stackSize--; + if (context == nonempty) { + newline(); + } + out.write(closeBracket); + return this; + } + + private void push(int newTop) { + if (stackSize == stack.length) { + stack = Arrays.copyOf(stack, stackSize * 2); + } + stack[stackSize++] = newTop; + } + + /** + * Returns the value on the top of the stack. + */ + private int peek() { + if (stackSize == 0) { + throw new IllegalStateException("JsonWriter is closed."); + } + return stack[stackSize - 1]; + } + + /** + * Replace the value on the top of the stack with the given value. + */ + private void replaceTop(int topOfStack) { + stack[stackSize - 1] = topOfStack; + } + + /** + * Encodes the property name. + * + * @param name the name of the forthcoming value. May not be null. + * @return this writer. + */ + public JsonWriter name(String name) throws IOException { + if (name == null) { + throw new NullPointerException("name == null"); + } + if (deferredName != null) { + throw new IllegalStateException(); + } + if (stackSize == 0) { + throw new IllegalStateException("JsonWriter is closed."); + } + deferredName = name; + return this; + } + + private void writeDeferredName() throws IOException { + if (deferredName != null) { + beforeName(); + string(deferredName); + deferredName = null; + } + } + + /** + * Encodes {@code value}. + * + * @param value the literal string value, or null to encode a null literal. + * @return this writer. + */ + public JsonWriter value(String value) throws IOException { + if (value == null) { + return nullValue(); + } + writeDeferredName(); + beforeValue(); + string(value); + return this; + } + + /** + * Writes {@code value} directly to the writer without quoting or + * escaping. + * + * @param value the literal string value, or null to encode a null literal. + * @return this writer. + */ + public JsonWriter jsonValue(String value) throws IOException { + if (value == null) { + return nullValue(); + } + writeDeferredName(); + beforeValue(); + out.append(value); + return this; + } + + /** + * Encodes {@code null}. + * + * @return this writer. + */ + public JsonWriter nullValue() throws IOException { + if (deferredName != null) { + if (serializeNulls) { + writeDeferredName(); + } else { + deferredName = null; + return this; // skip the name and the value + } + } + beforeValue(); + out.write("null"); + return this; + } + + /** + * Encodes {@code value}. + * + * @return this writer. + */ + public JsonWriter value(boolean value) throws IOException { + writeDeferredName(); + beforeValue(); + out.write(value ? "true" : "false"); + return this; + } + + /** + * Encodes {@code value}. + * + * @return this writer. + */ + public JsonWriter value(Boolean value) throws IOException { + if (value == null) { + return nullValue(); + } + writeDeferredName(); + beforeValue(); + out.write(value ? "true" : "false"); + return this; + } + + /** + * Encodes {@code value}. + * + * @param value a finite value. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + * @return this writer. + */ + public JsonWriter value(double value) throws IOException { + writeDeferredName(); + if (!lenient && (Double.isNaN(value) || Double.isInfinite(value))) { + throw new IllegalArgumentException("Numeric values must be finite, but was " + value); + } + beforeValue(); + out.append(Double.toString(value)); + return this; + } + + /** + * Encodes {@code value}. + * + * @return this writer. + */ + public JsonWriter value(long value) throws IOException { + writeDeferredName(); + beforeValue(); + out.write(Long.toString(value)); + return this; + } + + /** + * Encodes {@code value}. + * + * @param value a finite value. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + * @return this writer. + */ + public JsonWriter value(Number value) throws IOException { + if (value == null) { + return nullValue(); + } + + writeDeferredName(); + String string = value.toString(); + if (!lenient + && (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) { + throw new IllegalArgumentException("Numeric values must be finite, but was " + value); + } + beforeValue(); + out.append(string); + return this; + } + + /** + * Ensures all buffered data is written to the underlying {@link Writer} + * and flushes that writer. + */ + public void flush() throws IOException { + if (stackSize == 0) { + throw new IllegalStateException("JsonWriter is closed."); + } + out.flush(); + } + + /** + * Flushes and closes this writer and the underlying {@link Writer}. + * + * @throws IOException if the JSON document is incomplete. + */ + public void close() throws IOException { + out.close(); + + int size = stackSize; + if (size > 1 || size == 1 && stack[size - 1] != NONEMPTY_DOCUMENT) { + throw new IOException("Incomplete document"); + } + stackSize = 0; + } + + private void string(String value) throws IOException { + String[] replacements = htmlSafe ? HTML_SAFE_REPLACEMENT_CHARS : REPLACEMENT_CHARS; + out.write('\"'); + int last = 0; + int length = value.length(); + for (int i = 0; i < length; i++) { + char c = value.charAt(i); + String replacement; + if (c < 128) { + replacement = replacements[c]; + if (replacement == null) { + continue; + } + } else if (c == '\u2028') { + replacement = "\\u2028"; + } else if (c == '\u2029') { + replacement = "\\u2029"; + } else { + continue; + } + if (last < i) { + out.write(value, last, i - last); + } + out.write(replacement); + last = i + 1; + } + if (last < length) { + out.write(value, last, length - last); + } + out.write('\"'); + } + + private void newline() throws IOException { + if (indent == null) { + return; + } + + out.write('\n'); + for (int i = 1, size = stackSize; i < size; i++) { + out.write(indent); + } + } + + /** + * Inserts any necessary separators and whitespace before a name. Also + * adjusts the stack to expect the name's value. + */ + private void beforeName() throws IOException { + int context = peek(); + if (context == NONEMPTY_OBJECT) { // first in object + out.write(','); + } else if (context != EMPTY_OBJECT) { // not in an object! + throw new IllegalStateException("Nesting problem."); + } + newline(); + replaceTop(DANGLING_NAME); + } + + /** + * Inserts any necessary separators and whitespace before a literal value, + * inline array, or inline object. Also adjusts the stack to expect either a + * closing bracket or another element. + */ + @SuppressWarnings("fallthrough") + private void beforeValue() throws IOException { + switch (peek()) { + case NONEMPTY_DOCUMENT: + if (!lenient) { + throw new IllegalStateException( + "JSON must have only one top-level value."); + } + // fall-through + case EMPTY_DOCUMENT: // first in document + replaceTop(NONEMPTY_DOCUMENT); + break; + + case EMPTY_ARRAY: // first in array + replaceTop(NONEMPTY_ARRAY); + newline(); + break; + + case NONEMPTY_ARRAY: // another in array + out.append(','); + newline(); + break; + + case DANGLING_NAME: // value for name + out.append(separator); + replaceTop(NONEMPTY_OBJECT); + break; + + default: + throw new IllegalStateException("Nesting problem."); + } + } +} diff --git a/sentry/src/test/java/io/sentry/json/stream/JsonWriterTest.java b/sentry/src/test/java/io/sentry/json/stream/JsonWriterTest.java new file mode 100644 index 00000000000..0608134b515 --- /dev/null +++ b/sentry/src/test/java/io/sentry/json/stream/JsonWriterTest.java @@ -0,0 +1,648 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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 io.sentry.json.stream; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.io.StringWriter; +import java.math.BigDecimal; +import java.math.BigInteger; + +@SuppressWarnings("resource") +public final class JsonWriterTest extends TestCase { + + public void testTopLevelValueTypes() throws IOException { + StringWriter string1 = new StringWriter(); + JsonWriter writer1 = new JsonWriter(string1); + writer1.value(true); + writer1.close(); + assertEquals("true", string1.toString()); + + StringWriter string2 = new StringWriter(); + JsonWriter writer2 = new JsonWriter(string2); + writer2.nullValue(); + writer2.close(); + assertEquals("null", string2.toString()); + + StringWriter string3 = new StringWriter(); + JsonWriter writer3 = new JsonWriter(string3); + writer3.value(123); + writer3.close(); + assertEquals("123", string3.toString()); + + StringWriter string4 = new StringWriter(); + JsonWriter writer4 = new JsonWriter(string4); + writer4.value(123.4); + writer4.close(); + assertEquals("123.4", string4.toString()); + + StringWriter string5 = new StringWriter(); + JsonWriter writert = new JsonWriter(string5); + writert.value("a"); + writert.close(); + assertEquals("\"a\"", string5.toString()); + } + + public void testInvalidTopLevelTypes() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.name("hello"); + try { + jsonWriter.value("world"); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testTwoNames() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginObject(); + jsonWriter.name("a"); + try { + jsonWriter.name("a"); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testNameWithoutValue() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginObject(); + jsonWriter.name("a"); + try { + jsonWriter.endObject(); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testValueWithoutName() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginObject(); + try { + jsonWriter.value(true); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testMultipleTopLevelValues() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray().endArray(); + try { + jsonWriter.beginArray(); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testBadNestingObject() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.beginObject(); + try { + jsonWriter.endArray(); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testBadNestingArray() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.beginArray(); + try { + jsonWriter.endObject(); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testNullName() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginObject(); + try { + jsonWriter.name(null); + fail(); + } catch (NullPointerException expected) { + } + } + + public void testNullStringValue() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginObject(); + jsonWriter.name("a"); + jsonWriter.value((String) null); + jsonWriter.endObject(); + assertEquals("{\"a\":null}", stringWriter.toString()); + } + + public void testJsonValue() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginObject(); + jsonWriter.name("a"); + jsonWriter.jsonValue("{\"b\":true}"); + jsonWriter.name("c"); + jsonWriter.value(1); + jsonWriter.endObject(); + assertEquals("{\"a\":{\"b\":true},\"c\":1}", stringWriter.toString()); + } + + public void testNonFiniteDoubles() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + try { + jsonWriter.value(Double.NaN); + fail(); + } catch (IllegalArgumentException expected) { + } + try { + jsonWriter.value(Double.NEGATIVE_INFINITY); + fail(); + } catch (IllegalArgumentException expected) { + } + try { + jsonWriter.value(Double.POSITIVE_INFINITY); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + public void testNonFiniteBoxedDoubles() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + try { + jsonWriter.value(Double.valueOf(Double.NaN)); + fail(); + } catch (IllegalArgumentException expected) { + } + try { + jsonWriter.value(Double.valueOf(Double.NEGATIVE_INFINITY)); + fail(); + } catch (IllegalArgumentException expected) { + } + try { + jsonWriter.value(Double.valueOf(Double.POSITIVE_INFINITY)); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + public void testNonFiniteDoublesWhenLenient() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.setLenient(true); + jsonWriter.beginArray(); + jsonWriter.value(Double.NaN); + jsonWriter.value(Double.NEGATIVE_INFINITY); + jsonWriter.value(Double.POSITIVE_INFINITY); + jsonWriter.endArray(); + assertEquals("[NaN,-Infinity,Infinity]", stringWriter.toString()); + } + + public void testNonFiniteBoxedDoublesWhenLenient() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.setLenient(true); + jsonWriter.beginArray(); + jsonWriter.value(Double.valueOf(Double.NaN)); + jsonWriter.value(Double.valueOf(Double.NEGATIVE_INFINITY)); + jsonWriter.value(Double.valueOf(Double.POSITIVE_INFINITY)); + jsonWriter.endArray(); + assertEquals("[NaN,-Infinity,Infinity]", stringWriter.toString()); + } + + public void testDoubles() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.value(-0.0); + jsonWriter.value(1.0); + jsonWriter.value(Double.MAX_VALUE); + jsonWriter.value(Double.MIN_VALUE); + jsonWriter.value(0.0); + jsonWriter.value(-0.5); + jsonWriter.value(2.2250738585072014E-308); + jsonWriter.value(Math.PI); + jsonWriter.value(Math.E); + jsonWriter.endArray(); + jsonWriter.close(); + assertEquals("[-0.0," + + "1.0," + + "1.7976931348623157E308," + + "4.9E-324," + + "0.0," + + "-0.5," + + "2.2250738585072014E-308," + + "3.141592653589793," + + "2.718281828459045]", stringWriter.toString()); + } + + public void testLongs() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.value(0); + jsonWriter.value(1); + jsonWriter.value(-1); + jsonWriter.value(Long.MIN_VALUE); + jsonWriter.value(Long.MAX_VALUE); + jsonWriter.endArray(); + jsonWriter.close(); + assertEquals("[0," + + "1," + + "-1," + + "-9223372036854775808," + + "9223372036854775807]", stringWriter.toString()); + } + + public void testNumbers() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.value(new BigInteger("0")); + jsonWriter.value(new BigInteger("9223372036854775808")); + jsonWriter.value(new BigInteger("-9223372036854775809")); + jsonWriter.value(new BigDecimal("3.141592653589793238462643383")); + jsonWriter.endArray(); + jsonWriter.close(); + assertEquals("[0," + + "9223372036854775808," + + "-9223372036854775809," + + "3.141592653589793238462643383]", stringWriter.toString()); + } + + public void testBooleans() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.value(true); + jsonWriter.value(false); + jsonWriter.endArray(); + assertEquals("[true,false]", stringWriter.toString()); + } + + public void testBoxedBooleans() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.value((Boolean) true); + jsonWriter.value((Boolean) false); + jsonWriter.value((Boolean) null); + jsonWriter.endArray(); + assertEquals("[true,false,null]", stringWriter.toString()); + } + + public void testNulls() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.nullValue(); + jsonWriter.endArray(); + assertEquals("[null]", stringWriter.toString()); + } + + public void testStrings() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.value("a"); + jsonWriter.value("a\""); + jsonWriter.value("\""); + jsonWriter.value(":"); + jsonWriter.value(","); + jsonWriter.value("\b"); + jsonWriter.value("\f"); + jsonWriter.value("\n"); + jsonWriter.value("\r"); + jsonWriter.value("\t"); + jsonWriter.value(" "); + jsonWriter.value("\\"); + jsonWriter.value("{"); + jsonWriter.value("}"); + jsonWriter.value("["); + jsonWriter.value("]"); + jsonWriter.value("\0"); + jsonWriter.value("\u0019"); + jsonWriter.endArray(); + assertEquals("[\"a\"," + + "\"a\\\"\"," + + "\"\\\"\"," + + "\":\"," + + "\",\"," + + "\"\\b\"," + + "\"\\f\"," + + "\"\\n\"," + + "\"\\r\"," + + "\"\\t\"," + + "\" \"," + + "\"\\\\\"," + + "\"{\"," + + "\"}\"," + + "\"[\"," + + "\"]\"," + + "\"\\u0000\"," + + "\"\\u0019\"]", stringWriter.toString()); + } + + public void testUnicodeLineBreaksEscaped() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.value("\u2028 \u2029"); + jsonWriter.endArray(); + assertEquals("[\"\\u2028 \\u2029\"]", stringWriter.toString()); + } + + public void testEmptyArray() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.endArray(); + assertEquals("[]", stringWriter.toString()); + } + + public void testEmptyObject() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginObject(); + jsonWriter.endObject(); + assertEquals("{}", stringWriter.toString()); + } + + public void testObjectsInArrays() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.beginObject(); + jsonWriter.name("a").value(5); + jsonWriter.name("b").value(false); + jsonWriter.endObject(); + jsonWriter.beginObject(); + jsonWriter.name("c").value(6); + jsonWriter.name("d").value(true); + jsonWriter.endObject(); + jsonWriter.endArray(); + assertEquals("[{\"a\":5,\"b\":false}," + + "{\"c\":6,\"d\":true}]", stringWriter.toString()); + } + + public void testArraysInObjects() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginObject(); + jsonWriter.name("a"); + jsonWriter.beginArray(); + jsonWriter.value(5); + jsonWriter.value(false); + jsonWriter.endArray(); + jsonWriter.name("b"); + jsonWriter.beginArray(); + jsonWriter.value(6); + jsonWriter.value(true); + jsonWriter.endArray(); + jsonWriter.endObject(); + assertEquals("{\"a\":[5,false]," + + "\"b\":[6,true]}", stringWriter.toString()); + } + + public void testDeepNestingArrays() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + for (int i = 0; i < 20; i++) { + jsonWriter.beginArray(); + } + for (int i = 0; i < 20; i++) { + jsonWriter.endArray(); + } + assertEquals("[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]", stringWriter.toString()); + } + + public void testDeepNestingObjects() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginObject(); + for (int i = 0; i < 20; i++) { + jsonWriter.name("a"); + jsonWriter.beginObject(); + } + for (int i = 0; i < 20; i++) { + jsonWriter.endObject(); + } + jsonWriter.endObject(); + assertEquals("{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":" + + "{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{" + + "}}}}}}}}}}}}}}}}}}}}}", stringWriter.toString()); + } + + public void testRepeatedName() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginObject(); + jsonWriter.name("a").value(true); + jsonWriter.name("a").value(false); + jsonWriter.endObject(); + // JsonWriter doesn't attempt to detect duplicate names + assertEquals("{\"a\":true,\"a\":false}", stringWriter.toString()); + } + + public void testPrettyPrintObject() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.setIndent(" "); + + jsonWriter.beginObject(); + jsonWriter.name("a").value(true); + jsonWriter.name("b").value(false); + jsonWriter.name("c").value(5.0); + jsonWriter.name("e").nullValue(); + jsonWriter.name("f").beginArray(); + jsonWriter.value(6.0); + jsonWriter.value(7.0); + jsonWriter.endArray(); + jsonWriter.name("g").beginObject(); + jsonWriter.name("h").value(8.0); + jsonWriter.name("i").value(9.0); + jsonWriter.endObject(); + jsonWriter.endObject(); + + String expected = "{\n" + + " \"a\": true,\n" + + " \"b\": false,\n" + + " \"c\": 5.0,\n" + + " \"e\": null,\n" + + " \"f\": [\n" + + " 6.0,\n" + + " 7.0\n" + + " ],\n" + + " \"g\": {\n" + + " \"h\": 8.0,\n" + + " \"i\": 9.0\n" + + " }\n" + + "}"; + assertEquals(expected, stringWriter.toString()); + } + + public void testPrettyPrintArray() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.setIndent(" "); + + jsonWriter.beginArray(); + jsonWriter.value(true); + jsonWriter.value(false); + jsonWriter.value(5.0); + jsonWriter.nullValue(); + jsonWriter.beginObject(); + jsonWriter.name("a").value(6.0); + jsonWriter.name("b").value(7.0); + jsonWriter.endObject(); + jsonWriter.beginArray(); + jsonWriter.value(8.0); + jsonWriter.value(9.0); + jsonWriter.endArray(); + jsonWriter.endArray(); + + String expected = "[\n" + + " true,\n" + + " false,\n" + + " 5.0,\n" + + " null,\n" + + " {\n" + + " \"a\": 6.0,\n" + + " \"b\": 7.0\n" + + " },\n" + + " [\n" + + " 8.0,\n" + + " 9.0\n" + + " ]\n" + + "]"; + assertEquals(expected, stringWriter.toString()); + } + + public void testLenientWriterPermitsMultipleTopLevelValues() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.setLenient(true); + writer.beginArray(); + writer.endArray(); + writer.beginArray(); + writer.endArray(); + writer.close(); + assertEquals("[][]", stringWriter.toString()); + } + + public void testStrictWriterDoesNotPermitMultipleTopLevelValues() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginArray(); + writer.endArray(); + try { + writer.beginArray(); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testClosedWriterThrowsOnStructure() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginArray(); + writer.endArray(); + writer.close(); + try { + writer.beginArray(); + fail(); + } catch (IllegalStateException expected) { + } + try { + writer.endArray(); + fail(); + } catch (IllegalStateException expected) { + } + try { + writer.beginObject(); + fail(); + } catch (IllegalStateException expected) { + } + try { + writer.endObject(); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testClosedWriterThrowsOnName() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginArray(); + writer.endArray(); + writer.close(); + try { + writer.name("a"); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testClosedWriterThrowsOnValue() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginArray(); + writer.endArray(); + writer.close(); + try { + writer.value("a"); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testClosedWriterThrowsOnFlush() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginArray(); + writer.endArray(); + writer.close(); + try { + writer.flush(); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testWriterCloseIsIdempotent() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginArray(); + writer.endArray(); + writer.close(); + writer.close(); + } +} From 4dd911b5a0568202a099150cd1a8fe1553d6fe4a Mon Sep 17 00:00:00 2001 From: denrase Date: Wed, 23 Jun 2021 16:38:58 +0200 Subject: [PATCH 002/157] ignore nullaway warnings --- sentry/src/main/java/io/sentry/json/stream/JsonWriter.java | 2 +- sentry/src/test/java/io/sentry/json/stream/JsonWriterTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry/src/main/java/io/sentry/json/stream/JsonWriter.java b/sentry/src/main/java/io/sentry/json/stream/JsonWriter.java index ab1622d7d39..a1d80859161 100644 --- a/sentry/src/main/java/io/sentry/json/stream/JsonWriter.java +++ b/sentry/src/main/java/io/sentry/json/stream/JsonWriter.java @@ -128,7 +128,7 @@ * @author Jesse Wilson * @since 1.6 */ -@SuppressWarnings({"cast", "Nopen", "MissingOverride", "OperatorPrecedence"}) // Ignore warnings to preserve original code. +@SuppressWarnings({"cast", "NullAway", "Nopen", "MissingOverride", "OperatorPrecedence"}) // Ignore warnings to preserve original code. public class JsonWriter implements Closeable, Flushable { /* diff --git a/sentry/src/test/java/io/sentry/json/stream/JsonWriterTest.java b/sentry/src/test/java/io/sentry/json/stream/JsonWriterTest.java index 0608134b515..50d9cc79f93 100644 --- a/sentry/src/test/java/io/sentry/json/stream/JsonWriterTest.java +++ b/sentry/src/test/java/io/sentry/json/stream/JsonWriterTest.java @@ -23,7 +23,7 @@ import java.math.BigDecimal; import java.math.BigInteger; -@SuppressWarnings("resource") +@SuppressWarnings({"resource", "NullAway"}) // Ignore warnings to preserve original code. public final class JsonWriterTest extends TestCase { public void testTopLevelValueTypes() throws IOException { From ca1d203f9f02416a6b49d1e1857a3a7ddc24c97a Mon Sep 17 00:00:00 2001 From: denrase Date: Wed, 23 Jun 2021 17:14:58 +0200 Subject: [PATCH 003/157] add JsonSerializable and implement serialization for UserFeedback --- .../src/main/java/io/sentry/UserFeedback.java | 30 ++++++++++++- .../java/io/sentry/json/JsonSerializable.java | 10 +++++ .../sentry/UserFeedbackSerializationTest.kt | 43 +++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 sentry/src/main/java/io/sentry/json/JsonSerializable.java create mode 100644 sentry/src/test/java/io/sentry/UserFeedbackSerializationTest.kt diff --git a/sentry/src/main/java/io/sentry/UserFeedback.java b/sentry/src/main/java/io/sentry/UserFeedback.java index f3ea42c5c0a..cda1cd61a9b 100644 --- a/sentry/src/main/java/io/sentry/UserFeedback.java +++ b/sentry/src/main/java/io/sentry/UserFeedback.java @@ -1,10 +1,16 @@ package io.sentry; +import io.sentry.json.JsonSerializable; +import io.sentry.json.stream.JsonWriter; import io.sentry.protocol.SentryId; + +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.io.IOException; + /** Adds additional information about what happened to an event. */ -public final class UserFeedback { +public final class UserFeedback implements JsonSerializable { private final SentryId eventId; private @Nullable String name; @@ -115,4 +121,26 @@ public String toString() { + '\'' + '}'; } + + // JsonSerializable + + @Override + public void toJson(@NotNull JsonWriter jsonWriter) throws IOException { + jsonWriter.beginObject(); + jsonWriter.name("event_id"); + jsonWriter.value(eventId.toString()); + if (name != null) { + jsonWriter.name("name"); + jsonWriter.value(name); + } + if (email != null) { + jsonWriter.name("email"); + jsonWriter.value(email); + } + if (comments != null) { + jsonWriter.name("comments"); + jsonWriter.value(comments); + } + jsonWriter.endObject(); + } } diff --git a/sentry/src/main/java/io/sentry/json/JsonSerializable.java b/sentry/src/main/java/io/sentry/json/JsonSerializable.java new file mode 100644 index 00000000000..5183e20f833 --- /dev/null +++ b/sentry/src/main/java/io/sentry/json/JsonSerializable.java @@ -0,0 +1,10 @@ +package io.sentry.json; + +import org.jetbrains.annotations.NotNull; +import io.sentry.json.stream.JsonWriter; +import java.io.IOException; + +public interface JsonSerializable { + void toJson(@NotNull JsonWriter jsonWriter) throws IOException; +} + diff --git a/sentry/src/test/java/io/sentry/UserFeedbackSerializationTest.kt b/sentry/src/test/java/io/sentry/UserFeedbackSerializationTest.kt new file mode 100644 index 00000000000..61ad43897d9 --- /dev/null +++ b/sentry/src/test/java/io/sentry/UserFeedbackSerializationTest.kt @@ -0,0 +1,43 @@ +package io.sentry + +import io.sentry.json.JsonSerializable +import io.sentry.json.stream.JsonWriter +import io.sentry.protocol.SentryId +import org.junit.Test +import java.io.StringWriter +import kotlin.test.assertEquals + +class UserFeedbackSerializationTest { + + private val userFeedback: UserFeedback get() { + val eventId = SentryId("c2fb8fee2e2b49758bcb67cda0f713c7") + return UserFeedback(eventId).apply { + name = "John" + email = "john@me.com" + comments = "comment" + } + } + + @Test + fun `serializing user feedback`() { + val actual = serializeToString(userFeedback) + + val expected = "{\"event_id\":\"${userFeedback.eventId}\",\"name\":\"${userFeedback.name}\"," + + "\"email\":\"${userFeedback.email}\",\"comments\":\"${userFeedback.comments}\"}" + + assertEquals(expected, actual) + } + + // Helper + + private fun serializeToString(jsonSerializable: JsonSerializable): String { + return this.serializeToString { wrt -> jsonSerializable.toJson(wrt) } + } + + private fun serializeToString(serialize: (JsonWriter) -> Unit): String { + val wrt = StringWriter() + val jsonWrt = JsonWriter(wrt); + serialize(jsonWrt) + return wrt.toString() + } +} From 10a518234352a8e032cc17c0ebcc57b4a5196feb Mon Sep 17 00:00:00 2001 From: denrase Date: Wed, 23 Jun 2021 17:43:17 +0200 Subject: [PATCH 004/157] play around with JsonDeserializable stati method protocol --- sentry/src/main/java/io/sentry/UserFeedback.java | 16 ++++++++++++++++ .../java/io/sentry/json/JsonDeserializable.java | 7 +++++++ 2 files changed, 23 insertions(+) create mode 100644 sentry/src/main/java/io/sentry/json/JsonDeserializable.java diff --git a/sentry/src/main/java/io/sentry/UserFeedback.java b/sentry/src/main/java/io/sentry/UserFeedback.java index cda1cd61a9b..2a88e71e666 100644 --- a/sentry/src/main/java/io/sentry/UserFeedback.java +++ b/sentry/src/main/java/io/sentry/UserFeedback.java @@ -1,9 +1,11 @@ package io.sentry; +import io.sentry.json.JsonDeserializable; import io.sentry.json.JsonSerializable; import io.sentry.json.stream.JsonWriter; import io.sentry.protocol.SentryId; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -143,4 +145,18 @@ public void toJson(@NotNull JsonWriter jsonWriter) throws IOException { } jsonWriter.endObject(); } + + // JsonDeserializable + + public static JsonDeserializable deserializer = json -> { + + String test = "test"; + + return new UserFeedback( + SentryId.EMPTY_ID, + null, + null, + null + ); + }; } diff --git a/sentry/src/main/java/io/sentry/json/JsonDeserializable.java b/sentry/src/main/java/io/sentry/json/JsonDeserializable.java new file mode 100644 index 00000000000..dea861d42b5 --- /dev/null +++ b/sentry/src/main/java/io/sentry/json/JsonDeserializable.java @@ -0,0 +1,7 @@ +package io.sentry.json; + +import org.jetbrains.annotations.NotNull; + +public interface JsonDeserializable { + @NotNull T fromJson(String json) throws Exception; +} From 325447df775e3f45a4a2330c49c7f4906647f583 Mon Sep 17 00:00:00 2001 From: denrase Date: Wed, 23 Jun 2021 18:19:24 +0200 Subject: [PATCH 005/157] Add gsjon JsonReader and implement UserFeedback deserialization --- .../src/main/java/io/sentry/UserFeedback.java | 58 +- .../io/sentry/json/stream/JsonReader.java | 1612 +++++++++++++++ .../java/io/sentry/json/stream/JsonToken.java | 85 + .../json/stream/MalformedJsonException.java | 44 + .../sentry/UserFeedbackSerializationTest.kt | 14 + .../io/sentry/json/stream/JsonReaderTest.java | 1788 +++++++++++++++++ 6 files changed, 3595 insertions(+), 6 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/json/stream/JsonReader.java create mode 100644 sentry/src/main/java/io/sentry/json/stream/JsonToken.java create mode 100644 sentry/src/main/java/io/sentry/json/stream/MalformedJsonException.java create mode 100644 sentry/src/test/java/io/sentry/json/stream/JsonReaderTest.java diff --git a/sentry/src/main/java/io/sentry/UserFeedback.java b/sentry/src/main/java/io/sentry/UserFeedback.java index 2a88e71e666..792caff6c5c 100644 --- a/sentry/src/main/java/io/sentry/UserFeedback.java +++ b/sentry/src/main/java/io/sentry/UserFeedback.java @@ -2,6 +2,8 @@ import io.sentry.json.JsonDeserializable; import io.sentry.json.JsonSerializable; +import io.sentry.json.stream.JsonReader; +import io.sentry.json.stream.JsonToken; import io.sentry.json.stream.JsonWriter; import io.sentry.protocol.SentryId; @@ -10,6 +12,7 @@ import org.jetbrains.annotations.Nullable; import java.io.IOException; +import java.io.StringReader; /** Adds additional information about what happened to an event. */ public final class UserFeedback implements JsonSerializable { @@ -149,14 +152,57 @@ public void toJson(@NotNull JsonWriter jsonWriter) throws IOException { // JsonDeserializable public static JsonDeserializable deserializer = json -> { - - String test = "test"; + JsonReader reader = new JsonReader(new StringReader(json)); + reader.beginObject(); + + SentryId sentryId = null; + String name = null; + String email = null; + String comments = null; + + do { + String nextName = reader.nextName(); + switch (nextName) { + case "event_id": + sentryId = new SentryId(reader.nextString()); + break; + case "name": + if (reader.peek() == JsonToken.STRING) { + name = reader.nextString(); + } else { + name = null; + } + break; + case "email": + if (reader.peek() == JsonToken.STRING) { + email = reader.nextString(); + } else { + email = null; + } + break; + case "comments": + if (reader.peek() == JsonToken.STRING) { + comments = reader.nextString(); + } else { + comments = null; + } + break; + default: + break; + } + } while (reader.hasNext()); + + reader.endObject(); + + if (sentryId == null) { + throw new IllegalStateException("Missing required field \"sentryId\""); + } return new UserFeedback( - SentryId.EMPTY_ID, - null, - null, - null + sentryId, + name, + email, + comments ); }; } diff --git a/sentry/src/main/java/io/sentry/json/stream/JsonReader.java b/sentry/src/main/java/io/sentry/json/stream/JsonReader.java new file mode 100644 index 00000000000..31adc869ab6 --- /dev/null +++ b/sentry/src/main/java/io/sentry/json/stream/JsonReader.java @@ -0,0 +1,1612 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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 io.sentry.json.stream; + +//import com.google.gson.internal.JsonReaderInternalAccess; +//import com.google.gson.internal.bind.JsonTreeReader; +import java.io.Closeable; +import java.io.EOFException; +import java.io.IOException; +import java.io.Reader; +import java.util.Arrays; + +/** + * Reads a JSON (RFC 7159) + * encoded value as a stream of tokens. This stream includes both literal + * values (strings, numbers, booleans, and nulls) as well as the begin and + * end delimiters of objects and arrays. The tokens are traversed in + * depth-first order, the same order that they appear in the JSON document. + * Within JSON objects, name/value pairs are represented by a single token. + * + *

Parsing JSON

+ * To create a recursive descent parser for your own JSON streams, first create + * an entry point method that creates a {@code JsonReader}. + * + *

Next, create handler methods for each structure in your JSON text. You'll + * need a method for each object type and for each array type. + *

    + *
  • Within array handling methods, first call {@link + * #beginArray} to consume the array's opening bracket. Then create a + * while loop that accumulates values, terminating when {@link #hasNext} + * is false. Finally, read the array's closing bracket by calling {@link + * #endArray}. + *
  • Within object handling methods, first call {@link + * #beginObject} to consume the object's opening brace. Then create a + * while loop that assigns values to local variables based on their name. + * This loop should terminate when {@link #hasNext} is false. Finally, + * read the object's closing brace by calling {@link #endObject}. + *
+ *

When a nested object or array is encountered, delegate to the + * corresponding handler method. + * + *

When an unknown name is encountered, strict parsers should fail with an + * exception. Lenient parsers should call {@link #skipValue()} to recursively + * skip the value's nested tokens, which may otherwise conflict. + * + *

If a value may be null, you should first check using {@link #peek()}. + * Null literals can be consumed using either {@link #nextNull()} or {@link + * #skipValue()}. + * + *

Example

+ * Suppose we'd like to parse a stream of messages such as the following:
 {@code
+ * [
+ *   {
+ *     "id": 912345678901,
+ *     "text": "How do I read a JSON stream in Java?",
+ *     "geo": null,
+ *     "user": {
+ *       "name": "json_newb",
+ *       "followers_count": 41
+ *      }
+ *   },
+ *   {
+ *     "id": 912345678902,
+ *     "text": "@json_newb just use JsonReader!",
+ *     "geo": [50.454722, -104.606667],
+ *     "user": {
+ *       "name": "jesse",
+ *       "followers_count": 2
+ *     }
+ *   }
+ * ]}
+ * This code implements the parser for the above structure:
   {@code
+ *
+ *   public List readJsonStream(InputStream in) throws IOException {
+ *     JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
+ *     try {
+ *       return readMessagesArray(reader);
+ *     } finally {
+ *       reader.close();
+ *     }
+ *   }
+ *
+ *   public List readMessagesArray(JsonReader reader) throws IOException {
+ *     List messages = new ArrayList();
+ *
+ *     reader.beginArray();
+ *     while (reader.hasNext()) {
+ *       messages.add(readMessage(reader));
+ *     }
+ *     reader.endArray();
+ *     return messages;
+ *   }
+ *
+ *   public Message readMessage(JsonReader reader) throws IOException {
+ *     long id = -1;
+ *     String text = null;
+ *     User user = null;
+ *     List geo = null;
+ *
+ *     reader.beginObject();
+ *     while (reader.hasNext()) {
+ *       String name = reader.nextName();
+ *       if (name.equals("id")) {
+ *         id = reader.nextLong();
+ *       } else if (name.equals("text")) {
+ *         text = reader.nextString();
+ *       } else if (name.equals("geo") && reader.peek() != JsonToken.NULL) {
+ *         geo = readDoublesArray(reader);
+ *       } else if (name.equals("user")) {
+ *         user = readUser(reader);
+ *       } else {
+ *         reader.skipValue();
+ *       }
+ *     }
+ *     reader.endObject();
+ *     return new Message(id, text, user, geo);
+ *   }
+ *
+ *   public List readDoublesArray(JsonReader reader) throws IOException {
+ *     List doubles = new ArrayList();
+ *
+ *     reader.beginArray();
+ *     while (reader.hasNext()) {
+ *       doubles.add(reader.nextDouble());
+ *     }
+ *     reader.endArray();
+ *     return doubles;
+ *   }
+ *
+ *   public User readUser(JsonReader reader) throws IOException {
+ *     String username = null;
+ *     int followersCount = -1;
+ *
+ *     reader.beginObject();
+ *     while (reader.hasNext()) {
+ *       String name = reader.nextName();
+ *       if (name.equals("name")) {
+ *         username = reader.nextString();
+ *       } else if (name.equals("followers_count")) {
+ *         followersCount = reader.nextInt();
+ *       } else {
+ *         reader.skipValue();
+ *       }
+ *     }
+ *     reader.endObject();
+ *     return new User(username, followersCount);
+ *   }}
+ * + *

Number Handling

+ * This reader permits numeric values to be read as strings and string values to + * be read as numbers. For example, both elements of the JSON array {@code + * [1, "1"]} may be read using either {@link #nextInt} or {@link #nextString}. + * This behavior is intended to prevent lossy numeric conversions: double is + * JavaScript's only numeric type and very large values like {@code + * 9007199254740993} cannot be represented exactly on that platform. To minimize + * precision loss, extremely large values should be written and read as strings + * in JSON. + * + *

Non-Execute Prefix

+ * Web servers that serve private data using JSON may be vulnerable to
Cross-site + * request forgery attacks. In such an attack, a malicious site gains access + * to a private JSON file by executing it with an HTML {@code