diff options
author | Paulo Pinheiro <paulovictor.pinheiro@gmail.com> | 2021-03-30 00:57:23 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-03-29 15:57:23 -0700 |
commit | 1c26d2a1a0a24cf4050bc35a3c707dd862d34bc9 (patch) | |
tree | 684851ac87aa7b065b8a06e50fc56ad2b49bf7d2 | |
parent | 276b1bc342d23142e4b2b9b9fadbf076474deec9 (diff) | |
download | flatbuffers-1c26d2a1a0a24cf4050bc35a3c707dd862d34bc9.tar.gz flatbuffers-1c26d2a1a0a24cf4050bc35a3c707dd862d34bc9.tar.bz2 flatbuffers-1c26d2a1a0a24cf4050bc35a3c707dd862d34bc9.zip |
[Kotlin][FlexBuffers] JSON support for Flexbuffers (#6417)
* [Kotlin][FlexBuffers] Add JSON support for FlexBuffers
* [Kotlin][Flexbuffers] Re-implement JSON parser with a tokenizer.
8 files changed, 1402 insertions, 40 deletions
diff --git a/kotlin/benchmark/build.gradle.kts b/kotlin/benchmark/build.gradle.kts index 39fe5734..1b3b6301 100644 --- a/kotlin/benchmark/build.gradle.kts +++ b/kotlin/benchmark/build.gradle.kts @@ -5,6 +5,7 @@ plugins { id("org.jetbrains.kotlin.plugin.allopen") version "1.4.20" id("kotlinx.benchmark") version "0.2.0-dev-20" id("io.morethan.jmhreport") version "0.9.0" + id("de.undercouch.download") version "4.1.1" } // allOpen plugin is needed for the benchmark annotations. @@ -32,6 +33,8 @@ benchmark { iterations = 5 iterationTime = 300 iterationTimeUnit = "ms" + // uncomment for benchmarking JSON op only + // include(".*JsonBenchmark.*") } } targets { @@ -76,6 +79,11 @@ kotlin { implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.4.1") + //moshi + implementation("com.squareup.moshi:moshi-kotlin:1.11.0") + + //gson + implementation("com.google.code.gson:gson:2.8.5") } } @@ -88,3 +96,16 @@ kotlin { } } } + +// This task download all JSON files used for benchmarking +tasks.register<de.undercouch.gradle.tasks.download.Download>("downloadMultipleFiles") { + // We are downloading json benchmark samples from serdes-rs project. + // see: https://github.com/serde-rs/json-benchmark/blob/master/data + val baseUrl = "https://github.com/serde-rs/json-benchmark/raw/master/data/" + src(listOf("$baseUrl/canada.json", "$baseUrl/twitter.json", "$baseUrl/citm_catalog.json")) + dest(File("${project.projectDir.absolutePath}/src/jvmMain/resources")) +} + +project.tasks.named("compileKotlinJvm") { + dependsOn("downloadMultipleFiles") +} diff --git a/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/FlexBuffersBenchmark.kt b/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/FlexBuffersBenchmark.kt index 49f74435..ade57d95 100644 --- a/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/FlexBuffersBenchmark.kt +++ b/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/FlexBuffersBenchmark.kt @@ -35,7 +35,7 @@ import java.util.concurrent.TimeUnit @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Measurement(iterations = 20, time = 1, timeUnit = TimeUnit.NANOSECONDS) -class KotlinBenchmark { +class FlexBuffersBenchmark { var initialCapacity = 1024 var value: Double = 0.0 diff --git a/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/JsonBenchmark.kt b/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/JsonBenchmark.kt new file mode 100644 index 00000000..7d2ae507 --- /dev/null +++ b/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/JsonBenchmark.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2021 Google Inc. All rights reserved. + * + * 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.flatbuffers.kotlin.benchmark + +import com.google.flatbuffers.kotlin.ArrayReadBuffer +import com.google.flatbuffers.kotlin.JSONParser +import com.google.flatbuffers.kotlin.Reference +import com.google.flatbuffers.kotlin.toJson +import com.google.gson.Gson +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import kotlinx.benchmark.Blackhole +import okio.Buffer +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Measurement +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.OutputTimeUnit +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.State +import java.io.ByteArrayInputStream +import java.io.InputStreamReader +import java.util.concurrent.TimeUnit + +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Measurement(iterations = 100, time = 1, timeUnit = TimeUnit.MICROSECONDS) +class JsonBenchmark { + + final val moshi = Moshi.Builder() + .addLast(KotlinJsonAdapterFactory()) + .build() + final val moshiAdapter = moshi.adapter(Map::class.java) + + final val gson = Gson() + final val gsonParser = JsonParser() + + val fbParser = JSONParser() + + final val twitterData = this.javaClass.classLoader.getResourceAsStream("twitter.json")!!.readBytes() + final val canadaData = this.javaClass.classLoader.getResourceAsStream("canada.json")!!.readBytes() + final val citmData = this.javaClass.classLoader.getResourceAsStream("citm_catalog.json")!!.readBytes() + + val fbCitmRef = JSONParser().parse(ArrayReadBuffer(citmData)) + val moshiCitmRef = moshi.adapter(Map::class.java).fromJson(citmData.decodeToString()) + val gsonCitmRef = gsonParser.parse(citmData.decodeToString()) + + fun readFlexBuffers(data: ByteArray): Reference = fbParser.parse(ArrayReadBuffer(data)) + + fun readMoshi(data: ByteArray): Map<*, *>? { + val buffer = Buffer().write(data) + return moshiAdapter.fromJson(buffer) + } + + fun readGson(data: ByteArray): JsonObject { + val parser = JsonParser() + val jsonReader = InputStreamReader(ByteArrayInputStream(data)) + return parser.parse(jsonReader).asJsonObject + } + + // TWITTER + @Benchmark + fun readTwitterFlexBuffers(hole: Blackhole? = null) = hole?.consume(readFlexBuffers(twitterData)) + @Benchmark + fun readTwitterMoshi(hole: Blackhole?) = hole?.consume(readMoshi(twitterData)) + @Benchmark + fun readTwitterGson(hole: Blackhole?) = hole?.consume(readGson(twitterData)) + + @Benchmark + fun roundTripTwitterFlexBuffers(hole: Blackhole? = null) = hole?.consume(readFlexBuffers(twitterData).toJson()) + @Benchmark + fun roundTripTwitterMoshi(hole: Blackhole?) = hole?.consume(moshiAdapter.toJson(readMoshi(twitterData))) + @Benchmark + fun roundTripTwitterGson(hole: Blackhole?) = hole?.consume(gson.toJson(readGson(twitterData))) + + // CITM + @Benchmark + fun readCITMFlexBuffers(hole: Blackhole? = null) = hole?.consume(readFlexBuffers(citmData)) + @Benchmark + fun readCITMMoshi(hole: Blackhole?) = hole?.consume(moshiAdapter.toJson(readMoshi(citmData))) + @Benchmark + fun readCITMGson(hole: Blackhole?) = hole?.consume(gson.toJson(readGson(citmData))) + + @Benchmark + fun roundTripCITMFlexBuffers(hole: Blackhole? = null) = hole?.consume(readFlexBuffers(citmData).toJson()) + @Benchmark + fun roundTripCITMMoshi(hole: Blackhole?) = hole?.consume(moshiAdapter.toJson(readMoshi(citmData))) + @Benchmark + fun roundTripCITMGson(hole: Blackhole?) = hole?.consume(gson.toJson(readGson(citmData))) + + @Benchmark + fun writeCITMFlexBuffers(hole: Blackhole? = null) = hole?.consume(fbCitmRef.toJson()) + @Benchmark + fun writeCITMMoshi(hole: Blackhole?) = hole?.consume(moshiAdapter.toJson(moshiCitmRef)) + @Benchmark + fun writeCITMGson(hole: Blackhole?) = hole?.consume(gson.toJson(gsonCitmRef)) + + // CANADA + @Benchmark + fun readCanadaFlexBuffers(hole: Blackhole? = null) = hole?.consume(readFlexBuffers(canadaData)) + @Benchmark + fun readCanadaMoshi(hole: Blackhole?) = hole?.consume(readMoshi(canadaData)) + @Benchmark + fun readCanadaGson(hole: Blackhole?) = hole?.consume(readGson(canadaData)) +} diff --git a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Buffers.kt b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Buffers.kt index 998cab72..9851d90d 100644 --- a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Buffers.kt +++ b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Buffers.kt @@ -322,7 +322,8 @@ public interface ReadWriteBuffer : ReadBuffer { public fun requestCapacity(capacity: Int) } -public class ArrayReadBuffer(private val buffer: ByteArray, override var limit: Int = buffer.size) : ReadBuffer { +public open class ArrayReadBuffer(protected var buffer: ByteArray, override val limit: Int = buffer.size) : ReadBuffer { + override fun findFirst(value: Byte, start: Int, end: Int): Int { val e = min(end, limit) val s = max(0, start) @@ -369,9 +370,9 @@ public class ArrayReadBuffer(private val buffer: ByteArray, override var limit: * All operations assumes Little Endian byte order. */ public class ArrayReadWriteBuffer( - private var buffer: ByteArray, + buffer: ByteArray, override var writePosition: Int = 0 -) : ReadWriteBuffer { +) : ArrayReadBuffer(buffer, writePosition), ReadWriteBuffer { public constructor(initialCapacity: Int = 10) : this(ByteArray(initialCapacity)) @@ -379,34 +380,6 @@ public class ArrayReadWriteBuffer( override fun clear(): Unit = run { writePosition = 0 } - override fun getBoolean(index: Int): Boolean = buffer[index] != 0.toByte() - - override operator fun get(index: Int): Byte = buffer[index] - - override fun getUByte(index: Int): UByte = buffer.getUByte(index) - - override fun getShort(index: Int): Short = buffer.getShort(index) - - override fun getUShort(index: Int): UShort = buffer.getUShort(index) - - override fun getInt(index: Int): Int = buffer.getInt(index) - - override fun getUInt(index: Int): UInt = buffer.getUInt(index) - - override fun getLong(index: Int): Long = buffer.getLong(index) - - override fun getULong(index: Int): ULong = buffer.getULong(index) - - override fun getFloat(index: Int): Float = buffer.getFloat(index) - - override fun getDouble(index: Int): Double = buffer.getDouble(index) - - override fun getString(start: Int, size: Int): String = buffer.decodeToString(start, start + size) - - override fun data(): ByteArray = buffer - - override fun slice(start: Int, size: Int): ReadBuffer = ArrayReadWriteBuffer(buffer, writePosition) - override fun put(value: Boolean) { set(writePosition, value) writePosition++ @@ -509,13 +482,6 @@ public class ArrayReadWriteBuffer( buffer = buffer.copyOf(newCapacity) } - override fun findFirst(value: Byte, start: Int, end: Int): Int { - val e = min(end, buffer.size) - val s = max(0, start) - for (i in s until e) if (buffer[i] == value) return i - return -1 - } - private inline fun withCapacity(size: Int, crossinline action: ByteArray.() -> Unit) { requestCapacity(size) buffer.action() diff --git a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/JSON.kt b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/JSON.kt new file mode 100644 index 00000000..ce302ed5 --- /dev/null +++ b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/JSON.kt @@ -0,0 +1,828 @@ +/* + * Copyright 2021 Google Inc. All rights reserved. + * + * 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. + */ +@file:Suppress("NOTHING_TO_INLINE") + +package com.google.flatbuffers.kotlin + +import com.google.flatbuffers.kotlin.FlexBuffersBuilder.Companion.SHARE_KEYS_AND_STRINGS +import kotlin.experimental.and +import kotlin.math.pow + +/** + * Returns a minified version of this FlexBuffer as a JSON. + */ +public fun Reference.toJson(): String = ArrayReadWriteBuffer(1024).let { + toJson(it) + val data = it.data() // it.getString(0, it.writePosition) + return data.decodeToString(0, it.writePosition) +} + +/** + * Returns a minified version of this FlexBuffer as a JSON. + * @param out [ReadWriteBuffer] the JSON will be written. + */ +public fun Reference.toJson(out: ReadWriteBuffer) { + when (type) { + T_STRING -> { + val start = buffer.indirect(end, parentWidth) + val size = buffer.readULong(start - byteWidth, byteWidth).toInt() + out.jsonEscape(buffer, start, size) + } + T_KEY -> { + val start = buffer.indirect(end, parentWidth) + val end = buffer.findFirst(0.toByte(), start) + out.jsonEscape(buffer, start, end - start) + } + T_BLOB -> { + val blob = toBlob() + out.jsonEscape(out, blob.end, blob.size) + } + T_INT -> out.put(toLong().toString()) + T_UINT -> out.put(toULong().toString()) + T_FLOAT -> out.put(toDouble().toString()) + T_NULL -> out.put("null") + T_BOOL -> out.put(toBoolean().toString()) + T_MAP -> toMap().toJson(out) + T_VECTOR, T_VECTOR_BOOL, T_VECTOR_FLOAT, T_VECTOR_INT, + T_VECTOR_UINT, T_VECTOR_KEY, T_VECTOR_STRING_DEPRECATED -> toVector().toJson(out) + else -> error("Unable to convert type ${type.typeToString()} to JSON") + } +} + +/** + * Returns a minified version of this FlexBuffer as a JSON. + */ +public fun Map.toJson(): String = ArrayReadWriteBuffer(1024).let { toJson(it); it.toString() } + +/** + * Returns a minified version of this FlexBuffer as a JSON. + * @param out [ReadWriteBuffer] the JSON will be written. + */ +public fun Map.toJson(out: ReadWriteBuffer) { + out.put('{'.toByte()) + // key values pairs + for (i in 0 until size) { + val key = keyAt(i) + out.jsonEscape(buffer, key.start, key.sizeInBytes) + out.put(':'.toByte()) + get(i).toJson(out) + if (i != size - 1) { + out.put(','.toByte()) + } + } + // close bracket + out.put('}'.toByte()) +} + +/** + * Returns a minified version of this FlexBuffer as a JSON. + */ +public fun Vector.toJson(): String = ArrayReadWriteBuffer(1024).let { toJson(it); it.toString() } + +/** + * Returns a minified version of this FlexBuffer as a JSON. + * @param out that the JSON is being concatenated. + */ +public fun Vector.toJson(out: ReadWriteBuffer) { + out.put('['.toByte()) + for (i in 0 until size) { + get(i).toJson(out) + if (i != size - 1) { + out.put(','.toByte()) + } + } + out.put(']'.toByte()) +} + +/** + * JSONParser class is used to parse a JSON as FlexBuffers. Calling [JSONParser.parse] fiils [output] + * and returns a [Reference] ready to be used. + */ +public class JSONParser(public var output: FlexBuffersBuilder = FlexBuffersBuilder(1024, SHARE_KEYS_AND_STRINGS)) { + private var readPos = 0 + private var scopes = ScopeStack() + + /** + * Parse a json as [String] and returns a [Reference] to a FlexBuffer. + */ + public fun parse(data: String): Reference = parse(ArrayReadBuffer(data.encodeToByteArray())) + + /** + * Parse a json as [ByteArray] and returns a [Reference] to a FlexBuffer. + */ + public fun parse(data: ByteArray): Reference = parse(ArrayReadBuffer(data)) + + /** + * Parse a json as [ReadBuffer] and returns a [Reference] to a FlexBuffer. + */ + public fun parse(data: ReadBuffer): Reference { + reset() + parseValue(data, nextToken(data), null) + if (readPos < data.limit) { + val tok = skipWhitespace(data) + if (tok != CHAR_EOF) { + makeError(data, "Extraneous charaters after parse has finished", tok) + } + } + output.finish() + return getRoot(output.buffer) + } + + private fun parseValue(data: ReadBuffer, token: Token, key: String? = null): FlexBufferType { + return when (token) { + TOK_BEGIN_OBJECT -> parseObject(data, key) + TOK_BEGIN_ARRAY -> parseArray(data, key) + TOK_TRUE -> T_BOOL.also { output[key] = true } + TOK_FALSE -> T_BOOL.also { output[key] = false } + TOK_NULL -> T_NULL.also { output.putNull(key) } + TOK_BEGIN_QUOTE -> parseString(data, key) + TOK_NUMBER -> parseNumber(data, data.data(), key) + else -> makeError(data, "Unexpected Character while parsing", 'x'.toByte()) + } + } + + private fun parseObject(data: ReadBuffer, key: String? = null): FlexBufferType { + this.scopes.push(SCOPE_OBJ_EMPTY) + + val fPos = output.startMap() + val limit = data.limit + while (readPos <= limit) { + when (val tok = nextToken(data)) { + TOK_END_OBJECT -> { + this.scopes.pop() + output.endMap(fPos, key); return T_MAP + } + TOK_BEGIN_QUOTE -> { + val childKey = readString(data) + parseValue(data, nextToken(data), childKey) + } + else -> makeError(data, "Expecting start of object key", tok) + } + } + makeError(data, "Unable to parse the object", "x".toByte()) + } + + private fun parseArray(data: ReadBuffer, key: String? = null): FlexBufferType { + this.scopes.push(SCOPE_ARRAY_EMPTY) + val fPos = output.startVector() + var elementType = T_INVALID + var multiType = false + val limit = data.limit + + while (readPos <= limit) { + when (val tok = nextToken(data)) { + TOK_END_ARRAY -> { + this.scopes.pop() + return if (!multiType && elementType.isScalar()) { + output.endTypedVector(fPos, key) + elementType.toElementTypedVector() + } else { + output.endVector(key, fPos) + T_VECTOR + } + } + + else -> { + val newType = parseValue(data, tok, null) + + if (elementType == T_INVALID) { + elementType = newType + } else if (newType != elementType) { + multiType = true + } + } + } + } + makeError(data, "Unable to parse the array") + } + + private fun parseNumber(data: ReadBuffer, array: ByteArray, key: String?): FlexBufferType { + val ary = array + var cursor = readPos + var c = data[readPos++] + var useDouble = false + val limit = ary.size + var sign = 1 + var double = 0.0 + var long = 0L + var digits = 0 + + if (c == CHAR_MINUS) { + cursor++ + checkEOF(data, cursor) + c = ary[cursor] + sign = -1 + } + + // peek first byte + when (c) { + CHAR_0 -> { + cursor++ + if (cursor != limit) { + c = ary[cursor] + } + } + !in CHAR_0..CHAR_9 -> makeError(data, "Invalid Number", c) + else -> { + do { + val digit = c - CHAR_0 + // double = 10.0 * double + digit + long = 10 * long + digit + digits++ + cursor++ + if (cursor == limit) break + c = ary[cursor] + } while (c in CHAR_0..CHAR_9) + } + } + + var exponent = 0 + // If we find '.' we need to convert to double + if (c == CHAR_DOT) { + useDouble = true + checkEOF(data, cursor) + c = ary[++cursor] + if (c < CHAR_0 || c > CHAR_9) { + makeError(data, "Invalid Number", c) + } + do { + // double = double * 10 + (tok - CHAR_0) + long = 10 * long + (c - CHAR_0) + digits++ + --exponent + cursor++ + if (cursor == limit) break + c = ary[cursor] + } while (c in CHAR_0..CHAR_9) + } + + // If we find 'e' we need to convert to double + if (c == CHAR_e || c == CHAR_E) { + useDouble = true + ++cursor + checkEOF(data, cursor) + c = ary[cursor] + var negativeExponent = false + if (c == CHAR_MINUS) { + ++cursor + checkEOF(data, cursor) + negativeExponent = true + c = ary[cursor] + } else if (c == CHAR_PLUS) { + ++cursor + checkEOF(data, cursor) + c = ary[cursor] + } + if (c < CHAR_0 || c > CHAR_9) { + makeError(data, "Missing exponent", c) + } + var exp = 0 + do { + val digit = c - CHAR_0 + exp = 10 * exp + digit + ++cursor + if (cursor == limit) break + c = ary[cursor] + } while (c in CHAR_0..CHAR_9) + + exponent += if (negativeExponent) -exp else exp + } + + if (digits > 17 || exponent < -19 || exponent > 19) { + // if the float number is not simple enough + // we use language's Double parsing, which is slower but + // produce more expected results for extreme numbers. + val firstPos = readPos - 1 + val str = data.getString(firstPos, cursor - firstPos) + if (useDouble) { + double = str.toDouble() + output[key] = double + } else { + long = str.toLong() + output[key] = long + } + } else { + // this happens on single numbers outside any object + // or array + if (useDouble || exponent != 0) { + double = if (long == 0L) 0.0 else long.toDouble() * 10.0.pow(exponent) + double *= sign + output[key] = double + } else { + long *= sign + output[key] = long + } + } + readPos = cursor + return if (useDouble) T_FLOAT else T_INT + } + + private fun parseString(data: ReadBuffer, key: String?): FlexBufferType { + output[key] = readString(data) + return T_STRING + } + + private fun readString(data: ReadBuffer): String { + val limit = data.limit + if (data is ArrayReadBuffer) { + val ary = data.data() + // enables range check elimination + return readString(data, limit) { ary[it] } + } + return readString(data, limit) { data[it] } + } + + private inline fun readString(data: ReadBuffer, limit: Int, crossinline fetch: (Int) -> Byte): String { + var cursorPos = readPos + var foundEscape = false + var currentChar: Byte = 0 + // we loop over every 4 bytes until find any non-plain char + while (limit - cursorPos >= 4) { + currentChar = fetch(cursorPos) + if (!isPlainStringChar(currentChar)) { + foundEscape = true + break + } + currentChar = fetch(cursorPos + 1) + if (!isPlainStringChar(currentChar)) { + cursorPos += 1 + foundEscape = true + break + } + currentChar = fetch(cursorPos + 2) + if (!isPlainStringChar(currentChar)) { + cursorPos += 2 + foundEscape = true + break + } + currentChar = fetch(cursorPos + 3) + if (!isPlainStringChar(currentChar)) { + cursorPos += 3 + foundEscape = true + break + } + cursorPos += 4 + } + if (!foundEscape) { + // if non-plain string char is not found we loop over + // the remaining bytes + while (true) { + if (cursorPos >= limit) { + error("Unexpected end of string") + } + currentChar = fetch(cursorPos) + if (!isPlainStringChar(currentChar)) { + break + } + ++cursorPos + } + } + if (currentChar == CHAR_DOUBLE_QUOTE) { + val str = data.getString(readPos, cursorPos - readPos) + readPos = cursorPos + 1 + return str + } + if (currentChar in 0..0x1f) { + error("Illegal Codepoint") + } else { + // backslash or >0x7f + return readStringSlow(data, currentChar, cursorPos) + } + } + + private fun readStringSlow(data: ReadBuffer, first: Byte, lastPos: Int): String { + var cursorPos = lastPos + + var endOfString = lastPos + while (true) { + val pos = data.findFirst(CHAR_DOUBLE_QUOTE, endOfString) + when { + pos == -1 -> makeError(data, "Unexpected EOF, missing end of string '\"'", first) + data[pos - 1] == CHAR_BACKSLASH && data[pos - 2] != CHAR_BACKSLASH -> { + // here we are checking for double quotes preceded by backslash. eg \" + // we have to look past pos -2 to make sure that the backlash is not + // part of a previous escape, eg "\\" + endOfString = pos + 1 + } + else -> { + endOfString = pos; break + } + } + } + // copy everything before the escape + val builder = StringBuilder(data.getString(readPos, lastPos - readPos)) + while (true) { + when (val pos = data.findFirst(CHAR_BACKSLASH, cursorPos, endOfString)) { + -1 -> { + val doubleQuotePos = data.findFirst(CHAR_DOUBLE_QUOTE, cursorPos) + if (doubleQuotePos == -1) makeError(data, "Reached EOF before enclosing string", first) + val rest = data.getString(cursorPos, doubleQuotePos - cursorPos) + builder.append(rest) + readPos = doubleQuotePos + 1 + return builder.toString() + } + + else -> { + // we write everything up to \ + builder.append(data.getString(cursorPos, pos - cursorPos)) + val c = data[pos + 1] + builder.append(readEscapedChar(data, c, pos)) + cursorPos = pos + if (c == CHAR_u) 6 else 2 + } + } + } + } + + private inline fun isPlainStringChar(c: Byte): Boolean { + val flags = parseFlags + // return c in 0x20..0x7f && c != 0x22.toByte() && c != 0x5c.toByte() + return (flags[c.toInt() and 0xFF] and 1) != 0.toByte() + } + + private inline fun isWhitespace(c: Byte): Boolean { + val flags = parseFlags + // return c == '\r'.toByte() || c == '\n'.toByte() || c == '\t'.toByte() || c == ' '.toByte() + return (flags[c.toInt() and 0xFF] and 2) != 0.toByte() + } + + private fun reset() { + readPos = 0 + output.clear() + scopes.reset() + } + + private fun nextToken(data: ReadBuffer): Token { + val scope = this.scopes.last + + when (scope) { + SCOPE_ARRAY_EMPTY -> this.scopes.last = SCOPE_ARRAY_FILLED + SCOPE_ARRAY_FILLED -> { + when (val c = skipWhitespace(data)) { + CHAR_CLOSE_ARRAY -> return TOK_END_ARRAY + CHAR_COMMA -> Unit + else -> makeError(data, "Unfinished Array", c) + } + } + SCOPE_OBJ_EMPTY, SCOPE_OBJ_FILLED -> { + this.scopes.last = SCOPE_OBJ_KEY + // Look for a comma before the next element. + if (scope == SCOPE_OBJ_FILLED) { + when (val c = skipWhitespace(data)) { + CHAR_CLOSE_OBJECT -> return TOK_END_OBJECT + CHAR_COMMA -> Unit + else -> makeError(data, "Unfinished Object", c) + } + } + return when (val c = skipWhitespace(data)) { + CHAR_DOUBLE_QUOTE -> TOK_BEGIN_QUOTE + CHAR_CLOSE_OBJECT -> if (scope != SCOPE_OBJ_FILLED) { + TOK_END_OBJECT + } else { + makeError(data, "Expected Key", c) + } + else -> { + makeError(data, "Expected Key/Value", c) + } + } + } + SCOPE_OBJ_KEY -> { + this.scopes.last = SCOPE_OBJ_FILLED + when (val c = skipWhitespace(data)) { + CHAR_COLON -> Unit + else -> makeError(data, "Expect ${CHAR_COLON.print()}", c) + } + } + SCOPE_DOC_EMPTY -> this.scopes.last = SCOPE_DOC_FILLED + SCOPE_DOC_FILLED -> { + val c = skipWhitespace(data) + if (c != CHAR_EOF) + makeError(data, "Root object already finished", c) + return TOK_EOF + } + } + + val c = skipWhitespace(data) + when (c) { + CHAR_CLOSE_ARRAY -> if (scope == SCOPE_ARRAY_EMPTY) return TOK_END_ARRAY + CHAR_COLON -> makeError(data, "Unexpected character", c) + CHAR_DOUBLE_QUOTE -> return TOK_BEGIN_QUOTE + CHAR_OPEN_ARRAY -> return TOK_BEGIN_ARRAY + CHAR_OPEN_OBJECT -> return TOK_BEGIN_OBJECT + CHAR_t -> { + checkEOF(data, readPos + 2) + // 0x65757274 is equivalent to ['t', 'r', 'u', 'e' ] as a 4 byte Int + if (data.getInt(readPos - 1) != 0x65757274) { + makeError(data, "Expecting keyword \"true\"", c) + } + readPos += 3 + return TOK_TRUE + } + CHAR_n -> { + checkEOF(data, readPos + 2) + // 0x6c6c756e is equivalent to ['n', 'u', 'l', 'l' ] as a 4 byte Int + if (data.getInt(readPos - 1) != 0x6c6c756e) { + makeError(data, "Expecting keyword \"null\"", c) + } + readPos += 3 + return TOK_NULL + } + CHAR_f -> { + checkEOF(data, readPos + 3) + // 0x65736c61 is equivalent to ['a', 'l', 's', 'e' ] as a 4 byte Int + if (data.getInt(readPos) != 0x65736c61) { + makeError(data, "Expecting keyword \"false\"", c) + } + readPos += 4 + return TOK_FALSE + } + CHAR_0, CHAR_1, CHAR_2, CHAR_3, CHAR_4, CHAR_5, + CHAR_6, CHAR_7, CHAR_8, CHAR_9, CHAR_MINUS -> return TOK_NUMBER.also { + readPos-- // rewind one position so we don't lose first digit + } + } + makeError(data, "Expecting element", c) + } + + // keeps increasing [readPos] until finds a non-whitespace byte + private inline fun skipWhitespace(data: ReadBuffer): Byte { + val limit = data.limit + if (data is ArrayReadBuffer) { + // enables range check elimination + val ary = data.data() + return skipWhitespace(limit) { ary[it] } + } + return skipWhitespace(limit) { data[it] } + } + + private inline fun skipWhitespace(limit: Int, crossinline fetch: (Int) -> Byte): Byte { + var pos = readPos + while (pos < limit) { + val d = fetch(pos++) + if (!isWhitespace(d)) { + readPos = pos + return d + } + } + readPos = limit + return CHAR_EOF + } + + // byte1 is expected to be first char before `\` + private fun readEscapedChar(data: ReadBuffer, byte1: Byte, cursorPos: Int): Char { + return when (byte1) { + CHAR_u -> { + checkEOF(data, cursorPos + 1 + 4) + var result: Char = 0.toChar() + var i = cursorPos + 2 // cursorPos is on '\\', cursorPos + 1 is 'u' + val end = i + 4 + while (i < end) { + val part: Byte = data[i] + result = (result.toInt() shl 4).toChar() + result += when (part) { + in CHAR_0..CHAR_9 -> part - CHAR_0 + in CHAR_a..CHAR_f -> part - CHAR_a + 10 + in CHAR_A..CHAR_F -> part - CHAR_A + 10 + else -> makeError(data, "Invalid utf8 escaped character", -1) + } + i++ + } + result + } + CHAR_b -> '\b' + CHAR_t -> '\t' + CHAR_r -> '\r' + CHAR_n -> '\n' + CHAR_f -> 12.toChar() // '\f' + CHAR_DOUBLE_QUOTE, CHAR_BACKSLASH, CHAR_FORWARDSLASH -> byte1.toChar() + else -> makeError(data, "Invalid escape sequence.", byte1) + } + } + + private fun Byte.print(): String = when (this) { + in 0x21..0x7E -> "'${this.toChar()}'" // visible ascii chars + CHAR_EOF -> "EOF" + else -> "'0x${this.toString(16)}'" + } + + private inline fun makeError(data: ReadBuffer, msg: String, tok: Byte? = null): Nothing { + val (line, column) = calculateErrorPosition(data, readPos) + if (tok != null) { + error("Error At ($line, $column): $msg, got ${tok.print()}") + } else { + error("Error At ($line, $column): $msg") + } + } + + private inline fun makeError(data: ReadBuffer, msg: String, tok: Token): Nothing { + val (line, column) = calculateErrorPosition(data, readPos) + error("Error At ($line, $column): $msg, got ${tok.print()}") + } + + private inline fun checkEOF(data: ReadBuffer, pos: Int) { + if (pos >= data.limit) + makeError(data, "Unexpected end of file", -1) + } + + private fun calculateErrorPosition(data: ReadBuffer, endPos: Int): Pair<Int, Int> { + var line = 1 + var column = 1 + var current = 0 + while (current < endPos - 1) { + if (data[current++] == CHAR_NEWLINE) { + ++line + column = 1 + } else { + ++column + } + } + return Pair(line, column) + } +} + +internal inline fun Int.toPaddedHex(): String = "\\u${this.toString(16).padStart(4, '0')}" + +private inline fun ReadWriteBuffer.jsonEscape(data: ReadBuffer, start: Int, size: Int) { + val replacements = JSON_ESCAPE_CHARS + put(CHAR_DOUBLE_QUOTE) + var last = start + val length: Int = size + val ary = data.data() + for (i in start until start + length) { + val c = ary[i].toUByte() + var replacement: ByteArray? + if (c.toInt() < 128) { + replacement = replacements[c.toInt()] + if (replacement == null) { + continue + } + } else { + continue + } + if (last < i) { + put(ary, last, i - last) + } + put(replacement, 0, replacement.size) + last = i + 1 + } + if (last < (last + length)) { + put(ary, last, (start + length) - last) + } + put(CHAR_DOUBLE_QUOTE) +} + +// Following escape strategy defined in RFC7159. +private val JSON_ESCAPE_CHARS: Array<ByteArray?> = arrayOfNulls<ByteArray>(128).apply { + this['\n'.toInt()] = "\\n".encodeToByteArray() + this['\t'.toInt()] = "\\t".encodeToByteArray() + this['\r'.toInt()] = "\\r".encodeToByteArray() + this['\b'.toInt()] = "\\b".encodeToByteArray() + this[0x0c] = "\\f".encodeToByteArray() + this['"'.toInt()] = "\\\"".encodeToByteArray() + this['\\'.toInt()] = "\\\\".encodeToByteArray() + for (i in 0..0x1f) { + this[i] = "\\u${i.toPaddedHex()}".encodeToByteArray() + } +} + +// Scope is used to the define current space that the scanner is operating. +private inline class Scope(val id: Int) +private val SCOPE_DOC_EMPTY = Scope(0) +private val SCOPE_DOC_FILLED = Scope(1) +private val SCOPE_OBJ_EMPTY = Scope(2) +private val SCOPE_OBJ_KEY = Scope(3) +private val SCOPE_OBJ_FILLED = Scope(4) +private val SCOPE_ARRAY_EMPTY = Scope(5) +private val SCOPE_ARRAY_FILLED = Scope(6) + +// Keeps the stack state of the scopes being scanned. Currently defined to have a +// max stack size of 22, as per tests cases defined in http://json.org/JSON_checker/ +private class ScopeStack( + private val ary: IntArray = IntArray(22) { SCOPE_DOC_EMPTY.id }, + var lastPos: Int = 0 +) { + var last: Scope + get() = Scope(ary[lastPos]) + set(x) { + ary[lastPos] = x.id + } + + fun reset() { + lastPos = 0 + ary[0] = SCOPE_DOC_EMPTY.id + } + + fun pop(): Scope { + // println("Popping: ${last.print()}") + return Scope(ary[lastPos--]) + } + + fun push(scope: Scope): Scope { + if (lastPos == ary.size - 1) + error("Too much nesting reached. Max nesting is ${ary.size} levels") + // println("PUSHING : ${scope.print()}") + ary[++lastPos] = scope.id + return scope + } +} + +private inline class Token(val id: Int) { + fun print(): String = when (this) { + TOK_EOF -> "TOK_EOF" + TOK_NONE -> "TOK_NONE" + TOK_BEGIN_OBJECT -> "TOK_BEGIN_OBJECT" + TOK_END_OBJECT -> "TOK_END_OBJECT" + TOK_BEGIN_ARRAY -> "TOK_BEGIN_ARRAY" + TOK_END_ARRAY -> "TOK_END_ARRAY" + TOK_NUMBER -> "TOK_NUMBER" + TOK_TRUE -> "TOK_TRUE" + TOK_FALSE -> "TOK_FALSE" + TOK_NULL -> "TOK_NULL" + TOK_BEGIN_QUOTE -> "TOK_BEGIN_QUOTE" + else -> this.toString() + } +} + +private val TOK_EOF = Token(-1) +private val TOK_NONE = Token(0) +private val TOK_BEGIN_OBJECT = Token(1) +private val TOK_END_OBJECT = Token(2) +private val TOK_BEGIN_ARRAY = Token(3) +private val TOK_END_ARRAY = Token(4) +private val TOK_NUMBER = Token(5) +private val TOK_TRUE = Token(6) +private val TOK_FALSE = Token(7) +private val TOK_NULL = Token(8) +private val TOK_BEGIN_QUOTE = Token(9) + +private const val CHAR_NEWLINE = '\n'.toByte() +private const val CHAR_OPEN_OBJECT = '{'.toByte() +private const val CHAR_COLON = ':'.toByte() +private const val CHAR_CLOSE_OBJECT = '}'.toByte() +private const val CHAR_OPEN_ARRAY = '['.toByte() +private const val CHAR_CLOSE_ARRAY = ']'.toByte() +private const val CHAR_DOUBLE_QUOTE = '"'.toByte() +private const val CHAR_BACKSLASH = '\\'.toByte() +private const val CHAR_FORWARDSLASH = '/'.toByte() +private const val CHAR_f = 'f'.toByte() +private const val CHAR_a = 'a'.toByte() +private const val CHAR_r = 'r'.toByte() +private const val CHAR_t = 't'.toByte() +private const val CHAR_n = 'n'.toByte() +private const val CHAR_b = 'b'.toByte() +private const val CHAR_e = 'e'.toByte() +private const val CHAR_E = 'E'.toByte() +private const val CHAR_u = 'u'.toByte() +private const val CHAR_A = 'A'.toByte() +private const val CHAR_F = 'F'.toByte() +private const val CHAR_EOF = (-1).toByte() +private const val CHAR_COMMA = ','.toByte() +private const val CHAR_0 = '0'.toByte() +private const val CHAR_1 = '1'.toByte() +private const val CHAR_2 = '2'.toByte() +private const val CHAR_3 = '3'.toByte() +private const val CHAR_4 = '4'.toByte() +private const val CHAR_5 = '5'.toByte() +private const val CHAR_6 = '6'.toByte() +private const val CHAR_7 = '7'.toByte() +private const val CHAR_8 = '8'.toByte() +private const val CHAR_9 = '9'.toByte() +private const val CHAR_MINUS = '-'.toByte() +private const val CHAR_PLUS = '+'.toByte() +private const val CHAR_DOT = '.'.toByte() + +// This template utilizes the One Definition Rule to create global arrays in a +// header. As seen in: +// https://github.com/chadaustin/sajson/blob/master/include/sajson.h +// bit 0 (1) - set if: plain ASCII string character +// bit 1 (2) - set if: whitespace +// bit 4 (0x10) - set if: 0-9 e E . +private val parseFlags = byteArrayOf( +// 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 2, 0, 0, // 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1 + 3, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0x11, 1, // 2 + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 1, 1, 1, 1, 1, 1, // 3 + 1, 1, 1, 1, 1, 0x11, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 0x11, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + + // 128-255 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +) diff --git a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Utf8.kt b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Utf8.kt index 99297e62..4b02cc5c 100644 --- a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Utf8.kt +++ b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Utf8.kt @@ -338,6 +338,8 @@ public object Utf8 { // Designed to take advantage of // https://wikis.oracle.com/display/HotSpotInternals/RangeCheckElimination + if (utf16Length == 0) + return 0 var cc: Char = input[i] while (i < utf16Length && i + j < limit && input[i].also { cc = it }.toInt() < 0x80) { out[j + i] = cc.toByte() diff --git a/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/FlexBuffersTest.kt b/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/FlexBuffersTest.kt index f5aa0e47..71820b63 100644 --- a/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/FlexBuffersTest.kt +++ b/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/FlexBuffersTest.kt @@ -210,7 +210,7 @@ class FlexBuffersTest { val builder = FlexBuffersBuilder(shareFlag = FlexBuffersBuilder.SHARE_KEYS_AND_STRINGS) builder.putVector { put(10) - builder.putMap { + putMap { this["chello"] = "world" this["aint"] = 10 this["bfloat"] = 12.3 diff --git a/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/JSONTest.kt b/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/JSONTest.kt new file mode 100644 index 00000000..16039e85 --- /dev/null +++ b/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/JSONTest.kt @@ -0,0 +1,424 @@ +/* + * Copyright 2021 Google Inc. All rights reserved. + * + * 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.flatbuffers.kotlin + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class JSONTest { + + @Test + fun parse2Test() { + val dataStr = """ + { "myKey" : [1, "yay"] } + """.trimIndent() + val data = dataStr.encodeToByteArray() + val buffer = ArrayReadWriteBuffer(data, writePosition = data.size) + val parser = JSONParser() + val root = parser.parse(buffer) + println(root.toJson()) + } + + @Test + fun parseSample() { + val dataStr = """ + { + "ary" : [1, 2, 3], + "boolean_false": false, +"boolean_true": true, "double": 1.2E33, + "hello":"world" + ,"interesting": "value", + + "null_value": null, + + + "object" : { + "field1": "hello" + } + } + """ + val data = dataStr.encodeToByteArray() + val root = JSONParser().parse(ArrayReadWriteBuffer(data, writePosition = data.size)) + println(root.toJson()) + val map = root.toMap() + + val minified = data.filterNot { it == ' '.toByte() || it == '\n'.toByte() }.toByteArray().decodeToString() + assertEquals(8, map.size) + assertEquals("world", map["hello"].toString()) + assertEquals("value", map["interesting"].toString()) + assertEquals(12e32, map["double"].toDouble()) + assertArrayEquals(intArrayOf(1, 2, 3), map["ary"].toIntArray()) + assertEquals(true, map["boolean_true"].toBoolean()) + assertEquals(false, map["boolean_false"].toBoolean()) + assertEquals(true, map["null_value"].isNull) + assertEquals("hello", map["object"]["field1"].toString()) + + val obj = map["object"] + assertEquals(true, obj.isMap) + assertEquals("{\"field1\":\"hello\"}", obj.toJson()) + assertEquals(minified, root.toJson()) + } + + @Test + fun testDoubles() { + val values = arrayOf( + "-0.0", + "1.0", + "1.7976931348613157", + "0.0", + "-0.5", + "3.141592653589793", + "2.718281828459045E-3", + "2.2250738585072014E-308", + "4.9E-15", + ) + val parser = JSONParser() + assertEquals(-0.0, parser.parse(values[0]).toDouble()) + assertEquals(1.0, parser.parse(values[1]).toDouble()) + assertEquals(1.7976931348613157, parser.parse(values[2]).toDouble()) + assertEquals(0.0, parser.parse(values[3]).toDouble()) + assertEquals(-0.5, parser.parse(values[4]).toDouble()) + assertEquals(3.141592653589793, parser.parse(values[5]).toDouble()) + assertEquals(2.718281828459045e-3, parser.parse(values[6]).toDouble()) + assertEquals(2.2250738585072014E-308, parser.parse(values[7]).toDouble()) + assertEquals(4.9E-15, parser.parse(values[8]).toDouble()) + } + + @Test + fun testInts() { + val values = arrayOf( + "-0", + "0", + "-1", + "${Int.MAX_VALUE}", + "${Int.MIN_VALUE}", + "${Long.MAX_VALUE}", + "${Long.MIN_VALUE}", + ) + val parser = JSONParser() + + assertEquals(parser.parse(values[0]).toInt(), 0) + assertEquals(parser.parse(values[1]).toInt(), 0) + assertEquals(parser.parse(values[2]).toInt(), -1) + assertEquals(parser.parse(values[3]).toInt(), Int.MAX_VALUE) + assertEquals(parser.parse(values[4]).toInt(), Int.MIN_VALUE) + assertEquals(parser.parse(values[5]).toLong(), Long.MAX_VALUE) + assertEquals(parser.parse(values[6]).toLong(), Long.MIN_VALUE) + } + + @Test + fun testBooleansAndNull() { + val values = arrayOf( + "true", + "false", + "null" + ) + val parser = JSONParser() + + assertEquals(true, parser.parse(values[0]).toBoolean()) + assertEquals(false, parser.parse(values[1]).toBoolean()) + assertEquals(true, parser.parse(values[2]).isNull) + } + + @Test + fun testStrings() { + val values = arrayOf( + "\"\"", + "\"a\"", + "\"hello world\"", + "\"\\\"\\\\\\/\\b\\f\\n\\r\\t cool\"", + "\"\\u0000\"", + "\"\\u0021\"", + "\"hell\\u24AC\\n\\ro wor \\u0021 ld\"", + "\"\\/_\\\\_\\\"_\\uCAFE\\uBABE\\uAB98\\uFCDE\\ubcda\\uef4A\\b\\n\\r\\t`1~!@#\$%^&*()_+-=[]{}|;:',./<>?\"", + ) + val parser = JSONParser() + + // empty + var ref = parser.parse(values[0]) + assertEquals(true, ref.isString) + assertEquals("", ref.toString()) + // a + ref = parser.parse(values[1]) + assertEquals(true, ref.isString) + assertEquals("a", ref.toString()) + // hello world + ref = parser.parse(values[2]) + assertEquals(true, ref.isString) + assertEquals("hello world", ref.toString()) + // "\\\"\\\\\\/\\b\\f\\n\\r\\t\"" + ref = parser.parse(values[3]) + assertEquals(true, ref.isString) + assertEquals("\"\\/\b${12.toChar()}\n\r\t cool", ref.toString()) + // 0 + ref = parser.parse(values[4]) + assertEquals(true, ref.isString) + assertEquals(0.toChar().toString(), ref.toString()) + // u0021 + ref = parser.parse(values[5]) + assertEquals(true, ref.isString) + assertEquals(0x21.toChar().toString(), ref.toString()) + // "\"hell\\u24AC\\n\\ro wor \\u0021 ld\"", + ref = parser.parse(values[6]) + assertEquals(true, ref.isString) + assertEquals("hell${0x24AC.toChar()}\n\ro wor ${0x21.toChar()} ld", ref.toString()) + + ref = parser.parse(values[7]) + println(ref.toJson()) + assertEquals(true, ref.isString) + assertEquals("/_\\_\"_쫾몾ꮘﳞ볚\b\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?", ref.toString()) + } + + @Test + fun testUnicode() { + // took from test/unicode_test.json + val data = """ + { + "name": "unicode_test", + "testarrayofstring": [ + "Цлїςσδε", + "フムアムカモケモ", + "フムヤムカモケモ", + "㊀㊁㊂㊃㊄", + "☳☶☲", + "𡇙𝌆" + ], + "testarrayoftables": [ + { + "name": "Цлїςσδε" + }, + { + "name": "☳☶☲" + }, + { + "name": "フムヤムカモケモ" + }, + { + "name": "㊀㊁㊂㊃㊄" + }, + { + "name": "フムアムカモケモ" + }, + { + "name": "𡇙𝌆" + } + ] + } + """.trimIndent() + val parser = JSONParser() + val ref = parser.parse(data) + + // name + assertEquals(3, ref.toMap().size) + assertEquals("unicode_test", ref["name"].toString()) + // testarrayofstring + assertEquals(6, ref["testarrayofstring"].toVector().size) + assertEquals("Цлїςσδε", ref["testarrayofstring"][0].toString()) + assertEquals("フムアムカモケモ", ref["testarrayofstring"][1].toString()) + assertEquals("フムヤムカモケモ", ref["testarrayofstring"][2].toString()) + assertEquals("㊀㊁㊂㊃㊄", ref["testarrayofstring"][3].toString()) + assertEquals("☳☶☲", ref["testarrayofstring"][4].toString()) + assertEquals("𡇙𝌆", ref["testarrayofstring"][5].toString()) + // testarrayoftables + assertEquals(6, ref["testarrayoftables"].toVector().size) + assertEquals("Цлїςσδε", ref["testarrayoftables"][0]["name"].toString()) + assertEquals("☳☶☲", ref["testarrayoftables"][1]["name"].toString()) + assertEquals("フムヤムカモケモ", ref["testarrayoftables"][2]["name"].toString()) + assertEquals("㊀㊁㊂㊃㊄", ref["testarrayoftables"][3]["name"].toString()) + assertEquals("フムアムカモケモ", ref["testarrayoftables"][4]["name"].toString()) + assertEquals("𡇙𝌆", ref["testarrayoftables"][5]["name"].toString()) + } + + @Test + fun testArrays() { + val values = arrayOf( + "[]", + "[1]", + "[0,1, 2,3 , 4 ]", + "[1.0, 2.2250738585072014E-308, 4.9E-320]", + "[1.0, 2, \"hello world\"] ", + "[ 1.1, 2, [ \"hello\" ] ]", + "[[[1]]]" + ) + val parser = JSONParser() + + // empty + var ref = parser.parse(values[0]) + assertEquals(true, ref.isVector) + assertEquals(0, parser.parse(values[0]).toVector().size) + // single + ref = parser.parse(values[1]) + assertEquals(true, ref.isTypedVector) + assertEquals(1, ref[0].toInt()) + // ints + ref = parser.parse(values[2]) + assertEquals(true, ref.isTypedVector) + assertEquals(T_VECTOR_INT, ref.type) + assertEquals(5, ref.toVector().size) + for (i in 0..4) { + assertEquals(i, ref[i].toInt()) + } + // floats + ref = parser.parse(values[3]) + assertEquals(true, ref.isTypedVector) + assertEquals(T_VECTOR_FLOAT, ref.type) + assertEquals(3, ref.toVector().size) + assertEquals(1.0, ref[0].toDouble()) + assertEquals(2.2250738585072014E-308, ref[1].toDouble()) + assertEquals(4.9E-320, ref[2].toDouble()) + // mixed + ref = parser.parse(values[4]) + assertEquals(false, ref.isTypedVector) + assertEquals(T_VECTOR, ref.type) + assertEquals(1.0, ref[0].toDouble()) + assertEquals(2, ref[1].toInt()) + assertEquals("hello world", ref[2].toString()) + // nester array + ref = parser.parse(values[5]) + assertEquals(false, ref.isTypedVector) + assertEquals(T_VECTOR, ref.type) + assertEquals(1.1, ref[0].toDouble()) + assertEquals(2, ref[1].toInt()) + assertEquals("hello", ref[2][0].toString()) + } + + /** + * Several test cases provided by json.org + * For more details, see: http://json.org/JSON_checker/, with only + * one exception. Single strings are considered accepted, whereas on + * the test suit is should fail. + */ + @Test + fun testParseMustFail() { + val failList = listOf( + "[\"Unclosed array\"", + "{unquoted_key: \"keys must be quoted\"}", + "[\"extra comma\",]", + "[\"double extra comma\",,]", + "[ , \"<-- missing value\"]", + "[\"Comma after the close\"],", + "[\"Extra close\"]]", + "{\"Extra comma\": true,}", + "{\"Extra value after close\": true} \"misplaced quoted value\"", + "{\"Illegal expression\": 1 + 2}", + "{\"Illegal invocation\": alert()}", + "{\"Numbers cannot have leading zeroes\": 013}", + "{\"Numbers cannot be hex\": 0x14}", + "[\"Illegal backslash escape: \\x15\"]", + "[\\naked]", + "[\"Illegal backslash escape: \\017\"]", + "[[[[[[[[[[[[[[[[[[[[[[[\"Too deep\"]]]]]]]]]]]]]]]]]]]]]]]", + "{\"Missing colon\" null}", + "{\"Double colon\":: null}", + "{\"Comma instead of colon\", null}", + "[\"Colon instead of comma\": false]", + "[\"Bad value\", truth]", + "['single quote']", + "[\"\ttab\tcharacter\tin\tstring\t\"]", + "[\"tab\\ character\\ in\\ string\\ \"]", + "[\"line\nbreak\"]", + "[\"line\\\nbreak\"]", + "[0e]", + "[0e+]", + "[0e+-1]", + "{\"Comma instead if closing brace\": true,", + "[\"mismatch\"}" + ) + for (data in failList) { + try { + JSONParser().parse(ArrayReadBuffer(data.encodeToByteArray())) + assertTrue(false, "SHOULD NOT PASS: $data") + } catch (e: IllegalStateException) { + println("FAIL $e") + } + } + } + + @Test + fun testParseMustPass() { + val passList = listOf( + "[\n" + + " \"JSON Test Pattern pass1\",\n" + + " {\"object with 1 member\":[\"array with 1 element\"]},\n" + + " {},\n" + + " [],\n" + + " -42,\n" + + " true,\n" + + " false,\n" + + " null,\n" + + " {\n" + + " \"integer\": 1234567890,\n" + + " \"real\": -9876.543210,\n" + + " \"e\": 0.123456789e-12,\n" + + " \"E\": 1.234567890E+34,\n" + + " \"\": 23456789012E66,\n" + + " \"zero\": 0,\n" + + " \"one\": 1,\n" + + " \"space\": \" \",\n" + + " \"quote\": \"\\\"\",\n" + + " \"backslash\": \"\\\\\",\n" + + " \"controls\": \"\\b\\f\\n\\r\\t\",\n" + + " \"slash\": \"/ & \\/\",\n" + + " \"alpha\": \"abcdefghijklmnopqrstuvwyz\",\n" + + " \"ALPHA\": \"ABCDEFGHIJKLMNOPQRSTUVWYZ\",\n" + + " \"digit\": \"0123456789\",\n" + + " \"0123456789\": \"digit\",\n" + + " \"special\": \"`1~!@#\$%^&*()_+-={':[,]}|;.</>?\",\n" + + " \"hex\": \"\\u0123\\u4567\\u89AB\\uCDEF\\uabcd\\uef4A\",\n" + + " \"true\": true,\n" + + " \"false\": false,\n" + + " \"null\": null,\n" + + " \"array\":[ ],\n" + + " \"object\":{ },\n" + + " \"address\": \"50 St. James Street\",\n" + + " \"url\": \"http://www.JSON.org/\",\n" + + " \"comment\": \"// /* <!-- --\",\n" + + " \"# -- --> */\": \" \",\n" + + " \" s p a c e d \" :[1,2 , 3\n" + + "\n" + + ",\n" + + "\n" + + "4 , 5 , 6 ,7 ],\"compact\":[1,2,3,4,5,6,7],\n" + + " \"jsontext\": \"{\\\"object with 1 member\\\":[\\\"array with 1 element\\\"]}\",\n" + + " \"quotes\": \"" \\u0022 %22 0x22 034 "\",\n" + + " \"\\/\\\\\\\"\\uCAFE\\uBABE\\uAB98\\uFCDE\\ubcda\\uef4A\\b\\f\\n\\r\\t`1~!@#\$%^&*()_+-=[]{}|;:',./<>?\"\n" + + ": \"A key can be any string\"\n" + + " },\n" + + " 0.5 ,98.6\n" + + ",\n" + + "99.44\n" + + ",\n" + + "\n" + + "1066,\n" + + "1e1,\n" + + "0.1e1,\n" + + "1e-1,\n" + + "1e00,2e+00,2e-00\n" + + ",\"rosebud\"]", + "{\n" + + " \"JSON Test Pattern pass3\": {\n" + + " \"The outermost value\": \"must be an object or array.\",\n" + + " \"In this test\": \"It is an object.\"\n" + + " }\n" + + "}", + "[[[[[[[[[[[[[[[[[[[\"Not too deep\"]]]]]]]]]]]]]]]]]]]", + ) + for (data in passList) { + JSONParser().parse(ArrayReadBuffer(data.encodeToByteArray())) + } + } +} |