Json.kt
package com.shengzhiai.yugu.internal
import java.math.BigDecimal
import java.math.BigInteger
/**
* Small JSON reader and writer used by the SDK.
*
* The SDK signs the exact text it sends, so it serializes JSON itself instead of relying on
* android's org.json (whose escaping and number formatting differ from the JVM artifact used in
* unit tests). Parsed objects are [LinkedHashMap] (key order kept), arrays are [ArrayList],
* integral numbers are [Long] (or [BigInteger] when out of range), other numbers [Double].
*/
internal object Json {
class JsonException(message: String) : RuntimeException(message)
private const val MAX_DEPTH = 512
// ------------------------------------------------------------------------------- parse
fun parse(text: String): Any? {
val p = Parser(text)
p.skipWs()
val v = p.readValue(0)
p.skipWs()
if (p.pos != text.length) throw JsonException("trailing data at ${p.pos}")
return v
}
/** Parses [text] and returns it when it is a JSON object, otherwise null. Never throws. */
fun parseObjectOrNull(text: String?): Map<String, Any?>? {
if (text.isNullOrBlank()) return null
return try {
@Suppress("UNCHECKED_CAST")
parse(text) as? Map<String, Any?>
} catch (e: JsonException) {
null
}
}
private class Parser(val s: String) {
var pos = 0
fun skipWs() {
while (pos < s.length) {
val c = s[pos]
if (c == ' ' || c == '\n' || c == '\r' || c == '\t') pos++ else break
}
}
fun fail(what: String): Nothing = throw JsonException("$what at $pos")
fun readValue(depth: Int): Any? {
if (depth > MAX_DEPTH) fail("nesting too deep")
if (pos >= s.length) fail("unexpected end")
return when (val c = s[pos]) {
'{' -> readObject(depth)
'[' -> readArray(depth)
'"' -> readString()
't' -> literal("true", true)
'f' -> literal("false", false)
'n' -> literal("null", null)
else -> if (c == '-' || c in '0'..'9') readNumber() else fail("unexpected '$c'")
}
}
fun literal(word: String, value: Any?): Any? {
if (!s.startsWith(word, pos)) fail("bad literal")
pos += word.length
return value
}
fun readObject(depth: Int): Map<String, Any?> {
val out = LinkedHashMap<String, Any?>()
pos++ // {
skipWs()
if (pos < s.length && s[pos] == '}') {
pos++
return out
}
while (true) {
skipWs()
if (pos >= s.length || s[pos] != '"') fail("expected key")
val key = readString()
skipWs()
if (pos >= s.length || s[pos] != ':') fail("expected ':'")
pos++
skipWs()
out[key] = readValue(depth + 1)
skipWs()
if (pos >= s.length) fail("unterminated object")
when (s[pos]) {
',' -> pos++
'}' -> {
pos++
return out
}
else -> fail("expected ',' or '}'")
}
}
}
fun readArray(depth: Int): List<Any?> {
val out = ArrayList<Any?>()
pos++ // [
skipWs()
if (pos < s.length && s[pos] == ']') {
pos++
return out
}
while (true) {
skipWs()
out.add(readValue(depth + 1))
skipWs()
if (pos >= s.length) fail("unterminated array")
when (s[pos]) {
',' -> pos++
']' -> {
pos++
return out
}
else -> fail("expected ',' or ']'")
}
}
}
fun readString(): String {
pos++ // opening quote
val sb = StringBuilder()
while (true) {
if (pos >= s.length) fail("unterminated string")
val c = s[pos++]
when {
c == '"' -> return sb.toString()
c == '\\' -> {
if (pos >= s.length) fail("bad escape")
when (val e = s[pos++]) {
'"' -> sb.append('"')
'\\' -> sb.append('\\')
'/' -> sb.append('/')
'b' -> sb.append('\b')
'f' -> sb.append('\u000C')
'n' -> sb.append('\n')
'r' -> sb.append('\r')
't' -> sb.append('\t')
'u' -> {
if (pos + 4 > s.length) fail("bad unicode escape")
val hex = s.substring(pos, pos + 4)
val code = hex.toIntOrNull(16) ?: fail("bad unicode escape")
sb.append(code.toChar())
pos += 4
}
else -> fail("bad escape '\\$e'")
}
}
c < ' ' -> fail("control character in string")
else -> sb.append(c)
}
}
}
fun readNumber(): Any {
val start = pos
if (s[pos] == '-') pos++
if (pos >= s.length) fail("bad number")
if (s[pos] == '0') {
pos++
} else if (s[pos] in '1'..'9') {
while (pos < s.length && s[pos] in '0'..'9') pos++
} else {
fail("bad number")
}
var integral = true
if (pos < s.length && s[pos] == '.') {
integral = false
pos++
val d = pos
while (pos < s.length && s[pos] in '0'..'9') pos++
if (pos == d) fail("bad fraction")
}
if (pos < s.length && (s[pos] == 'e' || s[pos] == 'E')) {
integral = false
pos++
if (pos < s.length && (s[pos] == '+' || s[pos] == '-')) pos++
val d = pos
while (pos < s.length && s[pos] in '0'..'9') pos++
if (pos == d) fail("bad exponent")
}
val text = s.substring(start, pos)
if (integral) {
return text.toLongOrNull() ?: BigInteger(text)
}
return text.toDouble()
}
}
// ------------------------------------------------------------------------------- write
fun write(value: Any?): String {
val sb = StringBuilder()
writeTo(sb, value, 0)
return sb.toString()
}
private fun writeTo(sb: StringBuilder, value: Any?, depth: Int) {
if (depth > MAX_DEPTH) throw JsonException("nesting too deep")
when (value) {
null -> sb.append("null")
is String -> quote(sb, value)
is Boolean -> sb.append(if (value) "true" else "false")
is Number -> sb.append(formatNumber(value))
is Char -> quote(sb, value.toString())
is Map<*, *> -> {
sb.append('{')
var first = true
for ((k, v) in value) {
if (!first) sb.append(',')
first = false
quote(sb, k.toString())
sb.append(':')
writeTo(sb, v, depth + 1)
}
sb.append('}')
}
is Iterable<*> -> {
sb.append('[')
var first = true
for (v in value) {
if (!first) sb.append(',')
first = false
writeTo(sb, v, depth + 1)
}
sb.append(']')
}
is Array<*> -> writeTo(sb, value.asList(), depth)
is IntArray -> writeTo(sb, value.asList(), depth)
is LongArray -> writeTo(sb, value.asList(), depth)
is DoubleArray -> writeTo(sb, value.asList(), depth)
else -> quote(sb, value.toString())
}
}
/**
* Number text used both in JSON bodies and in signed values: integers as is, other numbers in
* plain decimal form without exponent and without trailing zeros (`0.2`, `100`, `0.0000001`).
*/
fun formatNumber(n: Number): String = when (n) {
is Int, is Long, is Short, is Byte, is BigInteger -> n.toString()
is BigDecimal -> plain(n)
is Float -> {
if (n.isNaN() || n.isInfinite()) throw JsonException("non-finite number")
plain(BigDecimal(n.toString()))
}
else -> {
val d = n.toDouble()
if (d.isNaN() || d.isInfinite()) throw JsonException("non-finite number")
plain(BigDecimal(d.toString()))
}
}
private fun plain(b: BigDecimal): String {
if (b.signum() == 0) return "0"
val s = b.stripTrailingZeros().toPlainString()
return s
}
private fun quote(sb: StringBuilder, s: String) {
sb.append('"')
for (c in s) {
when (c) {
'"' -> sb.append("\\\"")
'\\' -> sb.append("\\\\")
'\n' -> sb.append("\\n")
'\r' -> sb.append("\\r")
'\t' -> sb.append("\\t")
'\b' -> sb.append("\\b")
'\u000C' -> sb.append("\\f")
'
' -> sb.append("\\u2028")
'
' -> sb.append("\\u2029")
else -> if (c < ' ') {
sb.append("\\u")
val h = Integer.toHexString(c.code)
for (i in h.length until 4) sb.append('0')
sb.append(h)
} else {
sb.append(c)
}
}
}
sb.append('"')
}
}
/** Typed read access to a parsed JSON object. Lenient: wrong types read as null. */
internal class JObj(val map: Map<String, Any?>) {
fun has(key: String): Boolean = map.containsKey(key)
fun raw(key: String): Any? = map[key]
fun str(key: String): String? = when (val v = map[key]) {
null -> null
is String -> v
is Number -> Json.formatNumber(v)
is Boolean -> v.toString()
else -> null
}
fun double(key: String): Double? = when (val v = map[key]) {
is Number -> v.toDouble()
is String -> v.trim().toDoubleOrNull()
else -> null
}
fun long(key: String): Long? = when (val v = map[key]) {
is Long -> v
is Int -> v.toLong()
is Number -> {
val d = v.toDouble()
if (d == Math.floor(d) && d >= -9.2e18 && d <= 9.2e18) d.toLong() else null
}
is String -> v.trim().toLongOrNull()
else -> null
}
fun int(key: String): Int? = long(key)?.let { if (it in Int.MIN_VALUE..Int.MAX_VALUE) it.toInt() else null }
fun bool(key: String): Boolean? = when (val v = map[key]) {
is Boolean -> v
is Number -> v.toDouble() != 0.0
is String -> when (v.trim().lowercase()) {
"true", "1" -> true
"false", "0" -> false
else -> null
}
else -> null
}
@Suppress("UNCHECKED_CAST")
fun obj(key: String): JObj? = (map[key] as? Map<String, Any?>)?.let { JObj(it) }
@Suppress("UNCHECKED_CAST")
fun rawObj(key: String): Map<String, Any?>? = map[key] as? Map<String, Any?>
fun list(key: String): List<Any?>? = map[key] as? List<Any?>
@Suppress("UNCHECKED_CAST")
fun objList(key: String): List<JObj> =
(map[key] as? List<Any?>)?.mapNotNull { (it as? Map<String, Any?>)?.let { m -> JObj(m) } } ?: emptyList()
companion object {
fun parseOrNull(text: String?): JObj? = Json.parseObjectOrNull(text)?.let { JObj(it) }
}
}