commonMain.org.json.JsonTokener.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ktx-compose-constraint-layout Show documentation
Show all versions of ktx-compose-constraint-layout Show documentation
Extensions for the Kotlin standard library and third-party libraries.
The newest version!
/*
* Copyright (C) 2010 The Android Open Source Project
*
* 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 org.json
// Note: this class was written without inspecting the non-free org.Json sourcecode.
/**
* Parses a Json ([RFC 4627](http://www.ietf.org/rfc/rfc4627.txt))
* encoded string into the corresponding object. Most clients of
* this class will use only need the [constructor][.JsonTokener]
* and [.nextValue] method. Example usage:
* String Json = "{"
* + " \"query\": \"Pizza\", "
* + " \"locations\": [ 94043, 90210 ] "
* + "}";
*
* JsonObject object = (JsonObject) new JsonTokener(Json).nextValue();
* String query = object.getString("query");
* JsonArray locations = object.getJsonArray("locations");
*
*
* For best interoperability and performance use Json that complies with
* RFC 4627, such as that generated by [JsonStringer]. For legacy reasons
* this parser is lenient, so a successful parse does not indicate that the
* input string was valid Json. All of the following syntax errors will be
* ignored:
*
* * End of line comments starting with `//` or `#` and ending
* with a newline character.
* * C-style comments starting with `/ *` and ending with
* `*``/`. Such comments may not be nested.
* * Strings that are unquoted or `'single quoted'`.
* * Hexadecimal integers prefixed with `0x` or `0X`.
* * Octal integers prefixed with `0`.
* * Array elements separated by `;`.
* * Unnecessary array separators. These are interpreted as if null was the
* omitted value.
* * Key-value pairs separated by `=` or `=>`.
* * Key-value pairs separated by `;`.
*
*
*
* Each tokener may be used to parse a single Json string. Instances of this
* class are not thread safe. Although this class is nonfinal, it was not
* designed for inheritance and should not be subclassed. In particular,
* self-use by overrideable methods is not specified. See *Effective Java*
* Item 17, "Design and Document or inheritance or else prohibit it" for further
* information.
*/
open class JsonTokener(`in`: String?) {
/** The input Json. */
private val `in`: String?
/**
* The index of the next character to be returned by [.next]. When
* the input is exhausted, this equals the input's length.
*/
private var pos = 0
/**
* Returns the next value from the input.
*
* @return a [JsonObject], [JsonArray], String, Boolean,
* Integer, Long, Double or [JsonObject.NULL].
* @throws JsonException if the input is malformed.
*/
@Throws(JsonException::class)
fun nextValue(): Any {
val c = nextCleanInternal()
when (c) {
-1 -> throw syntaxError("End of input")
'{'.code -> return readObject()
'['.code -> return readArray()
'\''.code, '"'.code -> return nextString(c.toChar())
else -> {
pos--
return readLiteral()
}
}
}
@Throws(JsonException::class)
private fun nextCleanInternal(): Int {
while (pos < `in`!!.length) {
val c = `in`[pos++].code
when (c) {
'\t'.code, ' '.code, '\n'.code, '\r'.code -> continue
'/'.code -> {
if (pos == `in`.length) {
return c
}
val peek = `in`[pos]
when (peek) {
'*' -> {
// skip a /* c-style comment */
pos++
val commentEnd = `in`.indexOf("*/", pos)
if (commentEnd == -1) {
throw syntaxError("Unterminated comment")
}
pos = commentEnd + 2
continue
}
'/' -> {
// skip a // end-of-line comment
pos++
skipToEndOfLine()
continue
}
else -> return c
}
}
'#'.code -> {
/*
* Skip a # hash end-of-line comment. The Json RFC doesn't
* specify this behavior, but it's required to parse
* existing documents. See http://b/2571423.
*/skipToEndOfLine()
continue
}
else -> return c
}
}
return -1
}
/**
* Advances the position until after the next newline character. If the line
* is terminated by "\r\n", the '\n' must be consumed as whitespace by the
* caller.
*/
private fun skipToEndOfLine() {
while (pos < `in`!!.length) {
val c = `in`[pos]
if (c == '\r' || c == '\n') {
pos++
break
}
pos++
}
}
/**
* Returns the string up to but not including `quote`, unescaping any
* character escape sequences encountered along the way. The opening quote
* should have already been read. This consumes the closing quote, but does
* not include it in the returned string.
*
* @param quote either ' or ".
*/
@Throws(JsonException::class)
fun nextString(quote: Char): String {
/*
* For strings that are free of escape sequences, we can just extract
* the result as a substring of the input. But if we encounter an escape
* sequence, we need to use a StringBuilder to compose the result.
*/
var builder: StringBuilder? = null
/* the index of the first character not yet appended to the builder. */
var start = pos
while (pos < `in`!!.length) {
val c = `in`[pos++].code
if (c == quote.code) {
if (builder == null) {
// a new string avoids leaking memory
return `in`.substring(start, pos - 1)
} else {
builder.append(`in`, start, pos - 1)
return builder.toString()
}
}
if (c == '\\'.toInt()) {
if (pos == `in`.length) {
throw syntaxError("Unterminated escape sequence")
}
if (builder == null) {
builder = StringBuilder()
}
builder.append(`in`, start, pos - 1)
builder.append(readEscapeCharacter())
start = pos
}
}
throw syntaxError("Unterminated string")
}
/**
* Unescapes the character identified by the character or characters that
* immediately follow a backslash. The backslash '\' should have already
* been read. This supports both unicode escapes "u000A" and two-character
* escapes "\n".
*/
@Throws(JsonException::class)
private fun readEscapeCharacter(): Char {
val escaped = `in`!![pos++]
when (escaped) {
'u' -> {
if (pos + 4 > `in`.length) {
throw syntaxError("Unterminated escape sequence")
}
val hex = `in`.substring(pos, pos + 4)
pos += 4
try {
return hex.toInt(16).toChar()
} catch (nfe: NumberFormatException) {
throw syntaxError("Invalid escape sequence: $hex")
}
return '\t'
}
't' -> return '\t'
'b' -> return '\b'
'n' -> return '\n'
'r' -> return '\r'
// TODO form feed character not supported 'f' -> return '\f'
'\'', '"', '\\' -> return escaped
else -> return escaped
}
}
/**
* Reads a null, boolean, numeric or unquoted string literal value. Numeric
* values will be returned as an Integer, Long, or Double, in that order of
* preference.
*/
@Throws(JsonException::class)
private fun readLiteral(): Any {
val literal = nextToInternal("{}[]/\\:,=;# \t")// TODO form feed \f
if (literal.length == 0) {
throw syntaxError("Expected literal value")
} else if ("null".equals(literal, ignoreCase = true)) {
return JsonObject.NULL
} else if ("true".equals(literal, ignoreCase = true)) {
return true
} else if ("false".equals(literal, ignoreCase = true)) {
return false
}
/* try to parse as an integral type... */if (literal.indexOf('.') == -1) {
var base = 10
var number = literal
if (number.startsWith("0x") || number.startsWith("0X")) {
number = number.substring(2)
base = 16
} else if (number.startsWith("0") && number.length > 1) {
number = number.substring(1)
base = 8
}
try {
val longValue = number.toLong(base)
return if (longValue <= Int.MAX_VALUE && longValue >= Int.MIN_VALUE) {
longValue.toInt()
} else {
longValue
}
} catch (e: NumberFormatException) {
/*
* This only happens for integral numbers greater than
* Long.MAX_VALUE, numbers in exponential form (5e-10) and
* unquoted strings. Fall through to try floating point.
*/
}
}
/* ...next try to parse as a floating point... */try {
return literal.toDouble()
} catch (ignored: NumberFormatException) {
}
/* ... finally give up. We have an unquoted string */return literal.toCharArray() // a new string avoids leaking memory
}
/**
* Returns the string up to but not including any of the given characters or
* a newline character. This does not consume the excluded character.
*/
private fun nextToInternal(excluded: String): String {
val start = pos
while (pos < `in`!!.length) {
val c = `in`[pos]
if (c == '\r' || c == '\n' || excluded.indexOf(c) != -1) {
return `in`.substring(start, pos)
}
pos++
}
return `in`.substring(start)
}
/**
* Reads a sequence of key/value pairs and the trailing closing brace '}' of
* an object. The opening brace '{' should have already been read.
*/
@Throws(JsonException::class)
private fun readObject(): JsonObject {
val result = JsonObject()
/* Peek to see if this is the empty object. */
val first = nextCleanInternal()
if (first == '}'.toInt()) {
return result
} else if (first != -1) {
pos--
}
while (true) {
val name: Any = nextValue()
if (name !is String) {
if (name == null) {
throw syntaxError("Names cannot be null")
} else {
throw syntaxError(
"Names must be strings, but " + name
+ " is of type " + name::class.qualifiedName
)
}
}
/*
* Expect the name/value separator to be either a colon ':', an
* equals sign '=', or an arrow "=>". The last two are bogus but we
* include them because that's what the original implementation did.
*/
val separator = nextCleanInternal()
if (separator != ':'.toInt() && separator != '='.toInt()) {
throw syntaxError("Expected ':' after $name")
}
if (pos < `in`!!.length && `in`[pos] == '>') {
pos++
}
result.put(name as String?, nextValue())
when (nextCleanInternal()) {
'}'.code -> return result
';'.code, ','.code -> continue
else -> throw syntaxError("Unterminated object")
}
}
}
/**
* Reads a sequence of values and the trailing closing brace ']' of an
* array. The opening brace '[' should have already been read. Note that
* "[]" yields an empty array, but "[,]" returns a two-element array
* equivalent to "[null,null]".
*/
@Throws(JsonException::class)
private fun readArray(): JsonArray {
val result = JsonArray()
/* to cover input that ends with ",]". */
var hasTrailingSeparator = false
while (true) {
when (nextCleanInternal()) {
-1 -> throw syntaxError("Unterminated array")
']'.code -> {
if (hasTrailingSeparator) {
result.put(null)
}
return result
}
','.code, ';'.code -> {
/* A separator without a value first means "null". */result.put(null)
hasTrailingSeparator = true
continue
}
else -> pos--
}
result.put(nextValue())
when (nextCleanInternal()) {
']'.code -> return result
','.code, ';'.code -> {
hasTrailingSeparator = true
continue
}
else -> throw syntaxError("Unterminated array")
}
}
}
/**
* Returns an exception containing the given message plus the current
* position and the entire input string.
*/
fun syntaxError(message: String): JsonException {
return JsonException(message + this)
}
/**
* Returns the current position and the entire input string.
*/
override fun toString(): String {
// consistent with the original implementation
return " at character $pos of $`in`"
}
/*
* Legacy APIs.
*
* None of the methods below are on the critical path of parsing Json
* documents. They exist only because they were exposed by the original
* implementation and may be used by some clients.
*/
/**
* Returns true until the input has been exhausted.
*/
fun more(): Boolean {
return pos < `in`!!.length
}
/**
* Returns the next available character, or the null character '\0' if all
* input has been exhausted. The return value of this method is ambiguous
* for Json strings that contain the character '\0'.
*/
operator fun next(): Char {
return if (pos < `in`!!.length) `in`[pos++] else '\u0000'
}
/**
* Returns the next available character if it equals `c`. Otherwise an
* exception is thrown.
*/
@Throws(JsonException::class)
fun next(c: Char): Char {
val result = next()
if (result != c) {
throw syntaxError("Expected $c but was $result")
}
return result
}
/**
* Returns the next character that is not whitespace and does not belong to
* a comment. If the input is exhausted before such a character can be
* found, the null character '\0' is returned. The return value of this
* method is ambiguous for Json strings that contain the character '\0'.
*/
@Throws(JsonException::class)
fun nextClean(): Char {
val nextCleanInt = nextCleanInternal()
return if (nextCleanInt == -1) '\u0000' else nextCleanInt.toChar()
}
/**
* Returns the next `length` characters of the input.
*
*
* The returned string shares its backing character array with this
* tokener's input string. If a reference to the returned string may be held
* indefinitely, you should use `new String(result)` to copy it first
* to avoid memory leaks.
*
* @throws JsonException if the remaining input is not long enough to
* satisfy this request.
*/
@Throws(JsonException::class)
fun next(length: Int): String {
if (pos + length > `in`!!.length) {
throw syntaxError("$length is out of bounds")
}
val result = `in`.substring(pos, pos + length)
pos += length
return result
}
/**
* Returns the [trimmed][String.trim] string holding the characters up
* to but not including the first of:
*
* * any character in `excluded`
* * a newline character '\n'
* * a carriage return '\r'
*
*
*
* The returned string shares its backing character array with this
* tokener's input string. If a reference to the returned string may be held
* indefinitely, you should use `new String(result)` to copy it first
* to avoid memory leaks.
*
* @return a possibly-empty string
*/
fun nextTo(excluded: String?): String {
if (excluded == null) {
throw NullPointerException("excluded == null")
}
return nextToInternal(excluded).trim { it <= ' ' }
}
/**
* Equivalent to `nextTo(String.valueOf(excluded))`.
*/
fun nextTo(excluded: Char): String {
return nextToInternal(excluded.toString()).trim { it <= ' ' }
}
/**
* Advances past all input up to and including the next occurrence of
* `thru`. If the remaining input doesn't contain `thru`, the
* input is exhausted.
*/
open fun skipPast(thru: String) {
val thruStart = `in`!!.indexOf(thru, pos)
pos = if (thruStart == -1) `in`.length else (thruStart + thru.length)
}
/**
* Advances past all input up to but not including the next occurrence of
* `to`. If the remaining input doesn't contain `to`, the input
* is unchanged.
*/
fun skipTo(to: Char): Char {
val index = `in`!!.indexOf(to, pos)
if (index != -1) {
pos = index
return to
} else {
return '\u0000'
}
}
/**
* Unreads the most recent character of input. If no input characters have
* been read, the input is unchanged.
*/
fun back() {
if (--pos == -1) {
pos = 0
}
}
companion object {
/**
* Returns the integer [0..15] value for the given hex character, or -1
* for non-hex input.
*
* @param hex a character in the ranges [0-9], [A-F] or [a-f]. Any other
* character will yield a -1 result.
*/
fun dehexchar(hex: Char): Int {
if (hex >= '0' && hex <= '9') {
return hex - '0'
} else if (hex >= 'A' && hex <= 'F') {
return hex - 'A' + 10
} else return if (hex >= 'a' && hex <= 'f') {
hex - 'a' + 10
} else {
-1
}
}
}
/**
* @param in Json encoded string. Null is not permitted and will yield a
* tokener that throws `NullPointerExceptions` when methods are
* called.
*/
init {
// consume an optional byte order mark (BOM) if it exists
var `in` = `in`
if (`in` != null && `in`.startsWith("\ufeff")) {
`in` = `in`.substring(1)
}
this.`in` = `in`
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy