1 /*
2 * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3 */
4
5 package kotlinx.serialization.json.internal
6
7 import kotlinx.serialization.json.internal.CharMappings.CHAR_TO_TOKEN
8 import kotlinx.serialization.json.internal.CharMappings.ESCAPE_2_CHAR
9 import kotlin.js.*
10 import kotlin.jvm.*
11 import kotlin.math.*
12
13 internal const val lenientHint = "Use 'isLenient = true' in 'Json {}' builder to accept non-compliant JSON."
14 internal const val coerceInputValuesHint = "Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls if property has a default value."
15 internal const val specialFlowingValuesHint =
16 "It is possible to deserialize them using 'JsonBuilder.allowSpecialFloatingPointValues = true'"
17 internal const val ignoreUnknownKeysHint = "Use 'ignoreUnknownKeys = true' in 'Json {}' builder to ignore unknown keys."
18 internal const val allowStructuredMapKeysHint =
19 "Use 'allowStructuredMapKeys = true' in 'Json {}' builder to convert such maps to [key1, value1, key2, value2,...] arrays."
20
21 // special strings
22 internal const val NULL = "null"
23
24 // special chars
25 internal const val COMMA = ','
26 internal const val COLON = ':'
27 internal const val BEGIN_OBJ = '{'
28 internal const val END_OBJ = '}'
29 internal const val BEGIN_LIST = '['
30 internal const val END_LIST = ']'
31 internal const val STRING = '"'
32 internal const val STRING_ESC = '\\'
33
34 internal const val INVALID = 0.toChar()
35 internal const val UNICODE_ESC = 'u'
36
37 // token classes
38 internal const val TC_OTHER: Byte = 0
39 internal const val TC_STRING: Byte = 1
40 internal const val TC_STRING_ESC: Byte = 2
41 internal const val TC_WHITESPACE: Byte = 3
42 internal const val TC_COMMA: Byte = 4
43 internal const val TC_COLON: Byte = 5
44 internal const val TC_BEGIN_OBJ: Byte = 6
45 internal const val TC_END_OBJ: Byte = 7
46 internal const val TC_BEGIN_LIST: Byte = 8
47 internal const val TC_END_LIST: Byte = 9
48 internal const val TC_EOF: Byte = 10
49 internal const val TC_INVALID: Byte = Byte.MAX_VALUE
50
51 // mapping from chars to token classes
52 private const val CTC_MAX = 0x7e
53
54 // mapping from escape chars real chars
55 private const val ESC2C_MAX = 0x75
56
57 internal const val asciiCaseMask = 1 shl 5
58
tokenDescriptionnull59 internal fun tokenDescription(token: Byte) = when (token) {
60 TC_STRING -> "quotation mark '\"'"
61 TC_STRING_ESC -> "string escape sequence '\\'"
62 TC_COMMA -> "comma ','"
63 TC_COLON -> "colon ':'"
64 TC_BEGIN_OBJ -> "start of the object '{'"
65 TC_END_OBJ -> "end of the object '}'"
66 TC_BEGIN_LIST -> "start of the array '['"
67 TC_END_LIST -> "end of the array ']'"
68 TC_EOF -> "end of the input"
69 TC_INVALID -> "invalid token"
70 else -> "valid token" // should never happen
71 }
72
73 // object instead of @SharedImmutable because there is mutual initialization in [initC2ESC] and [initC2TC]
74 internal object CharMappings {
75 @JvmField
76 val ESCAPE_2_CHAR = CharArray(ESC2C_MAX)
77
78 @JvmField
79 val CHAR_TO_TOKEN = ByteArray(CTC_MAX)
80
81 init {
82 initEscape()
83 initCharToToken()
84 }
85
initEscapenull86 private fun initEscape() {
87 for (i in 0x00..0x1f) {
88 initC2ESC(i, UNICODE_ESC)
89 }
90
91 initC2ESC(0x08, 'b')
92 initC2ESC(0x09, 't')
93 initC2ESC(0x0a, 'n')
94 initC2ESC(0x0c, 'f')
95 initC2ESC(0x0d, 'r')
96 initC2ESC('/', '/')
97 initC2ESC(STRING, STRING)
98 initC2ESC(STRING_ESC, STRING_ESC)
99 }
100
initCharToTokennull101 private fun initCharToToken() {
102 for (i in 0..0x20) {
103 initC2TC(i, TC_INVALID)
104 }
105
106 initC2TC(0x09, TC_WHITESPACE)
107 initC2TC(0x0a, TC_WHITESPACE)
108 initC2TC(0x0d, TC_WHITESPACE)
109 initC2TC(0x20, TC_WHITESPACE)
110 initC2TC(COMMA, TC_COMMA)
111 initC2TC(COLON, TC_COLON)
112 initC2TC(BEGIN_OBJ, TC_BEGIN_OBJ)
113 initC2TC(END_OBJ, TC_END_OBJ)
114 initC2TC(BEGIN_LIST, TC_BEGIN_LIST)
115 initC2TC(END_LIST, TC_END_LIST)
116 initC2TC(STRING, TC_STRING)
117 initC2TC(STRING_ESC, TC_STRING_ESC)
118 }
119
initC2ESCnull120 private fun initC2ESC(c: Int, esc: Char) {
121 if (esc != UNICODE_ESC) ESCAPE_2_CHAR[esc.code] = c.toChar()
122 }
123
initC2ESCnull124 private fun initC2ESC(c: Char, esc: Char) = initC2ESC(c.code, esc)
125
126 private fun initC2TC(c: Int, cl: Byte) {
127 CHAR_TO_TOKEN[c] = cl
128 }
129
initC2TCnull130 private fun initC2TC(c: Char, cl: Byte) = initC2TC(c.code, cl)
131 }
132
133 internal fun charToTokenClass(c: Char) = if (c.code < CTC_MAX) CHAR_TO_TOKEN[c.code] else TC_OTHER
134
135 internal fun escapeToChar(c: Int): Char = if (c < ESC2C_MAX) ESCAPE_2_CHAR[c] else INVALID
136
137 /**
138 * The base class that reads the JSON from the given char sequence source.
139 * It has two implementations: one over the raw [String] instance, [StringJsonLexer],
140 * and one over an arbitrary stream of data, [ReaderJsonLexer] (JVM-only).
141 *
142 * [AbstractJsonLexer] contains base implementation for cold or not performance-sensitive
143 * methods on top of [CharSequence], but [StringJsonLexer] overrides some
144 * of them for the performance reasons (devirtualization of [CharSequence] and avoid
145 * of additional spills).
146 */
147 internal abstract class AbstractJsonLexer {
148
149 protected abstract val source: CharSequence
150
151 @JvmField
152 internal var currentPosition: Int = 0 // position in source
153
154 @JvmField
155 val path = JsonPath()
156
157 open fun ensureHaveChars() {}
158
159 fun isNotEof(): Boolean = peekNextToken() != TC_EOF
160
161 // Used as bound check in loops
162 abstract fun prefetchOrEof(position: Int): Int
163
164 abstract fun tryConsumeComma(): Boolean
165
166 abstract fun canConsumeValue(): Boolean
167
168 abstract fun consumeNextToken(): Byte
169
170 protected fun isValidValueStart(c: Char): Boolean {
171 return when (c) {
172 '}', ']', ':', ',' -> false
173 else -> true
174 }
175 }
176
177 fun expectEof() {
178 val nextToken = consumeNextToken()
179 if (nextToken != TC_EOF)
180 fail("Expected EOF after parsing, but had ${source[currentPosition - 1]} instead")
181 }
182
183 /*
184 * Peeked string for coerced enums.
185 * If the value was picked, 'consumeString' will take it without scanning the source.
186 */
187 private var peekedString: String? = null
188 protected var escapedString = StringBuilder()
189
190 // TODO consider replacing usages of this method in JsonParser with char overload
191 fun consumeNextToken(expected: Byte): Byte {
192 val token = consumeNextToken()
193 if (token != expected) {
194 fail(expected)
195 }
196 return token
197 }
198
199 open fun consumeNextToken(expected: Char) {
200 ensureHaveChars()
201 val source = source
202 var cpos = currentPosition
203 while (true) {
204 cpos = prefetchOrEof(cpos)
205 if (cpos == -1) break // could be inline function but KT-1436
206 val c = source[cpos++]
207 if (c == ' ' || c == '\n' || c == '\r' || c == '\t') continue
208 currentPosition = cpos
209 if (c == expected) return
210 unexpectedToken(expected)
211 }
212 currentPosition = cpos
213 unexpectedToken(expected) // EOF
214 }
215
216 protected fun unexpectedToken(expected: Char) {
217 if (currentPosition > 0 && expected == STRING) {
218 val inputLiteral = withPositionRollback {
219 currentPosition--
220 consumeStringLenient()
221 }
222 if (inputLiteral == NULL)
223 fail("Expected string literal but 'null' literal was found", currentPosition - 1, coerceInputValuesHint)
224 }
225 fail(charToTokenClass(expected))
226 }
227
228 internal fun fail(expectedToken: Byte, wasConsumed: Boolean = true): Nothing {
229 // Slow path, never called in normal code, can avoid optimizing it
230 val expected = tokenDescription(expectedToken)
231 val position = if (wasConsumed) currentPosition - 1 else currentPosition
232 val s = if (currentPosition == source.length || position < 0) "EOF" else source[position].toString()
233 fail("Expected $expected, but had '$s' instead", position)
234 }
235
236 fun peekNextToken(): Byte {
237 val source = source
238 var cpos = currentPosition
239 while (true) {
240 cpos = prefetchOrEof(cpos)
241 if (cpos == -1) break
242 val ch = source[cpos]
243 if (ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t') {
244 ++cpos
245 continue
246 }
247 currentPosition = cpos
248 return charToTokenClass(ch)
249 }
250 currentPosition = cpos
251 return TC_EOF
252 }
253
254 /**
255 * Tries to consume `null` token from input.
256 * Returns `false` if the next 4 chars in input are not `null`,
257 * `true` otherwise and consumes it if [doConsume] is `true`.
258 */
259 fun tryConsumeNull(doConsume: Boolean = true): Boolean {
260 var current = skipWhitespaces()
261 current = prefetchOrEof(current)
262 // Cannot consume null due to EOF, maybe something else
263 val len = source.length - current
264 if (len < 4 || current == -1) return false
265 for (i in 0..3) {
266 if (NULL[i] != source[current + i]) return false
267 }
268 /*
269 * If we're in lenient mode, this might be the string with 'null' prefix,
270 * distinguish it from 'null'
271 */
272 if (len > 4 && charToTokenClass(source[current + 4]) == TC_OTHER) return false
273
274 if (doConsume) {
275 currentPosition = current + 4
276 }
277 return true
278 }
279
280 open fun skipWhitespaces(): Int {
281 var current = currentPosition
282 // Skip whitespaces
283 while (true) {
284 current = prefetchOrEof(current)
285 if (current == -1) break
286 val c = source[current]
287 // Faster than char2TokenClass actually
288 if (c == ' ' || c == '\n' || c == '\r' || c == '\t') {
289 ++current
290 } else {
291 break
292 }
293 }
294 currentPosition = current
295 return current
296 }
297
298 abstract fun peekLeadingMatchingValue(keyToMatch: String, isLenient: Boolean): String?
299
300 fun peekString(isLenient: Boolean): String? {
301 val token = peekNextToken()
302 val string = if (isLenient) {
303 if (token != TC_STRING && token != TC_OTHER) return null
304 consumeStringLenient()
305 } else {
306 if (token != TC_STRING) return null
307 consumeString()
308 }
309 peekedString = string
310 return string
311 }
312
313 fun discardPeeked() {
314 peekedString = null
315 }
316
317 open fun indexOf(char: Char, startPos: Int) = source.indexOf(char, startPos)
318 open fun substring(startPos: Int, endPos: Int) = source.substring(startPos, endPos)
319
320 /*
321 * This method is a copy of consumeString, but used for key of json objects, so there
322 * is no need to lookup peeked string.
323 */
324 abstract fun consumeKeyString(): String
325
326 private fun insideString(isLenient: Boolean, char: Char): Boolean = if (isLenient) {
327 charToTokenClass(char) == TC_OTHER
328 } else {
329 char != STRING
330 }
331
332 open fun consumeStringChunked(isLenient: Boolean, consumeChunk: (stringChunk: String) -> Unit) { // open to allow simpler implementations (i.e. StringJsonLexer)
333 val nextToken = peekNextToken()
334 if (isLenient && nextToken != TC_OTHER) return // noting to consume
335
336 if (!isLenient) {
337 consumeNextToken(STRING)
338 }
339 var currentPosition = this.currentPosition
340 var lastPosition = currentPosition
341 var char = source[currentPosition] // Avoid two range checks visible in the profiler
342 var usedAppend = false
343 while (insideString(isLenient, char)) {
344 if (!isLenient && char == STRING_ESC) { // handle escaping only in non-lenient mode
345 usedAppend = true
346 currentPosition = prefetchOrEof(appendEscape(lastPosition, currentPosition))
347 lastPosition = currentPosition
348 } else {
349 currentPosition++
350 }
351 if (currentPosition >= source.length) {
352 // end of chunk
353 writeRange(lastPosition, currentPosition, usedAppend, consumeChunk)
354 usedAppend = false
355 currentPosition = prefetchOrEof(currentPosition)
356 if (currentPosition == -1)
357 fail("EOF", currentPosition)
358 lastPosition = currentPosition
359 }
360 char = source[currentPosition]
361 }
362 writeRange(lastPosition, currentPosition, usedAppend, consumeChunk)
363 this.currentPosition = currentPosition
364 if (!isLenient) {
365 consumeNextToken(STRING)
366 }
367 }
368
369 private fun writeRange(fromIndex: Int, toIndex: Int, currentChunkHasEscape: Boolean, consumeChunk: (stringChunk: String) -> Unit) {
370 if (currentChunkHasEscape) {
371 consumeChunk(decodedString(fromIndex, toIndex))
372 } else {
373 consumeChunk(substring(fromIndex, toIndex))
374 }
375 }
376
377
378 fun consumeString(): String {
379 if (peekedString != null) {
380 return takePeeked()
381 }
382
383 return consumeKeyString()
384 }
385
386 @JsName("consumeString2") // WA for JS issue
387 protected fun consumeString(source: CharSequence, startPosition: Int, current: Int): String {
388 var currentPosition = current
389 var lastPosition = startPosition
390 var char = source[currentPosition] // Avoid two range checks visible in the profiler
391 var usedAppend = false
392 while (char != STRING) {
393 if (char == STRING_ESC) {
394 usedAppend = true
395 currentPosition = prefetchOrEof(appendEscape(lastPosition, currentPosition))
396 if (currentPosition == -1)
397 fail("Unexpected EOF", currentPosition)
398 lastPosition = currentPosition
399 } else if (++currentPosition >= source.length) {
400 usedAppend = true
401 // end of chunk
402 appendRange(lastPosition, currentPosition)
403 currentPosition = prefetchOrEof(currentPosition)
404 if (currentPosition == -1)
405 fail("Unexpected EOF", currentPosition)
406 lastPosition = currentPosition
407 }
408 char = source[currentPosition]
409 }
410
411 val string = if (!usedAppend) {
412 // there was no escaped chars
413 substring(lastPosition, currentPosition)
414 } else {
415 // some escaped chars were there
416 decodedString(lastPosition, currentPosition)
417 }
418 this.currentPosition = currentPosition + 1
419 return string
420 }
421
422 private fun appendEscape(lastPosition: Int, current: Int): Int {
423 appendRange(lastPosition, current)
424 return appendEsc(current + 1)
425 }
426
427 private fun decodedString(lastPosition: Int, currentPosition: Int): String {
428 appendRange(lastPosition, currentPosition)
429 val result = escapedString.toString()
430 escapedString.setLength(0)
431 return result
432 }
433
434 private fun takePeeked(): String {
435 return peekedString!!.also { peekedString = null }
436 }
437
438 fun consumeStringLenientNotNull(): String {
439 val result = consumeStringLenient()
440 /*
441 * Check if lenient value is 'null' _without_ quotation marks and fail for non-nullable read if so.
442 */
443 if (result == NULL && wasUnquotedString()) {
444 fail("Unexpected 'null' value instead of string literal")
445 }
446 return result
447 }
448
449 private fun wasUnquotedString(): Boolean {
450 // Is invoked _only_ when the 'null' string was read, thus 'cP - 1' is always within bounds
451 return source[currentPosition - 1] != STRING
452 }
453
454 // Allows consuming unquoted string
455 fun consumeStringLenient(): String {
456 if (peekedString != null) {
457 return takePeeked()
458 }
459 var current = skipWhitespaces()
460 if (current >= source.length || current == -1) fail("EOF", current)
461 val token = charToTokenClass(source[current])
462 if (token == TC_STRING) {
463 return consumeString()
464 }
465
466 if (token != TC_OTHER) {
467 fail("Expected beginning of the string, but got ${source[current]}")
468 }
469 var usedAppend = false
470 while (charToTokenClass(source[current]) == TC_OTHER) {
471 ++current
472 if (current >= source.length) {
473 usedAppend = true
474 appendRange(currentPosition, current)
475 val eof = prefetchOrEof(current)
476 if (eof == -1) {
477 // to handle plain lenient strings, such as top-level
478 currentPosition = current
479 return decodedString(0, 0)
480 } else {
481 current = eof
482 }
483 }
484 }
485 val result = if (!usedAppend) {
486 substring(currentPosition, current)
487 } else {
488 decodedString(currentPosition, current)
489 }
490 currentPosition = current
491 return result
492 }
493
494 // initializes buf usage upon the first encountered escaped char
495 protected open fun appendRange(fromIndex: Int, toIndex: Int) {
496 escapedString.append(source, fromIndex, toIndex)
497 }
498
499 private fun appendEsc(startPosition: Int): Int {
500 var currentPosition = startPosition
501 currentPosition = prefetchOrEof(currentPosition)
502 if (currentPosition == -1) fail("Expected escape sequence to continue, got EOF")
503 val currentChar = source[currentPosition++]
504 if (currentChar == UNICODE_ESC) {
505 return appendHex(source, currentPosition)
506 }
507
508 val c = escapeToChar(currentChar.code)
509 if (c == INVALID) fail("Invalid escaped char '$currentChar'")
510 escapedString.append(c)
511 return currentPosition
512 }
513
514 private fun appendHex(source: CharSequence, startPos: Int): Int {
515 if (startPos + 4 >= source.length) {
516 currentPosition = startPos
517 ensureHaveChars()
518 if (currentPosition + 4 >= source.length)
519 fail("Unexpected EOF during unicode escape")
520 return appendHex(source, currentPosition)
521 }
522 escapedString.append(
523 ((fromHexChar(source, startPos) shl 12) +
524 (fromHexChar(source, startPos + 1) shl 8) +
525 (fromHexChar(source, startPos + 2) shl 4) +
526 fromHexChar(source, startPos + 3)).toChar()
527 )
528 return startPos + 4
529 }
530
531 internal inline fun require(condition: Boolean, position: Int = currentPosition, message: () -> String) {
532 if (!condition) fail(message(), position)
533 }
534
535 private fun fromHexChar(source: CharSequence, currentPosition: Int): Int {
536 return when (val character = source[currentPosition]) {
537 in '0'..'9' -> character.code - '0'.code
538 in 'a'..'f' -> character.code - 'a'.code + 10
539 in 'A'..'F' -> character.code - 'A'.code + 10
540 else -> fail("Invalid toHexChar char '$character' in unicode escape")
541 }
542 }
543
544 fun skipElement(allowLenientStrings: Boolean) {
545 val tokenStack = mutableListOf<Byte>()
546 var lastToken = peekNextToken()
547 if (lastToken != TC_BEGIN_LIST && lastToken != TC_BEGIN_OBJ) {
548 consumeStringLenient()
549 return
550 }
551 while (true) {
552 lastToken = peekNextToken()
553 if (lastToken == TC_STRING) {
554 if (allowLenientStrings) consumeStringLenient() else consumeKeyString()
555 continue
556 }
557 when (lastToken) {
558 TC_BEGIN_LIST, TC_BEGIN_OBJ -> {
559 tokenStack.add(lastToken)
560 }
561 TC_END_LIST -> {
562 if (tokenStack.last() != TC_BEGIN_LIST) throw JsonDecodingException(
563 currentPosition,
564 "found ] instead of } at path: $path",
565 source
566 )
567 tokenStack.removeLast()
568 }
569 TC_END_OBJ -> {
570 if (tokenStack.last() != TC_BEGIN_OBJ) throw JsonDecodingException(
571 currentPosition,
572 "found } instead of ] at path: $path",
573 source
574 )
575 tokenStack.removeLast()
576 }
577 TC_EOF -> fail("Unexpected end of input due to malformed JSON during ignoring unknown keys")
578 }
579 consumeNextToken()
580 if (tokenStack.size == 0) return
581 }
582 }
583
584 override fun toString(): String {
585 return "JsonReader(source='$source', currentPosition=$currentPosition)"
586 }
587
588 fun failOnUnknownKey(key: String) {
589 // At this moment we already have both key and semicolon (and whitespaces! consumed),
590 // but still would like an error to point to the beginning of the key, so we are backtracking it
591 val processed = substring(0, currentPosition)
592 val lastIndexOf = processed.lastIndexOf(key)
593 fail("Encountered an unknown key '$key'", lastIndexOf, ignoreUnknownKeysHint)
594 }
595
596 fun fail(message: String, position: Int = currentPosition, hint: String = ""): Nothing {
597 val hintMessage = if (hint.isEmpty()) "" else "\n$hint"
598 throw JsonDecodingException(position, message + " at path: " + path.getPath() + hintMessage, source)
599 }
600
601 fun consumeNumericLiteral(): Long {
602 /*
603 * This is an optimized (~40% for numbers) version of consumeString().toLong()
604 * that doesn't allocate and also doesn't support any radix but 10
605 */
606 var current = skipWhitespaces()
607 current = prefetchOrEof(current)
608 if (current >= source.length || current == -1) fail("EOF")
609 val hasQuotation = if (source[current] == STRING) {
610 // Check it again
611 // not sure if should call ensureHaveChars() because threshold is far greater than chars count in MAX_LONG
612 if (++current == source.length) fail("EOF")
613 true
614 } else {
615 false
616 }
617 var accumulator = 0L
618 var exponentAccumulator = 0L
619 var isNegative = false
620 var isExponentPositive = false
621 var hasExponent = false
622 val start = current
623 while (current != source.length) {
624 val ch: Char = source[current]
625 if ((ch == 'e' || ch == 'E') && !hasExponent) {
626 if (current == start) fail("Unexpected symbol $ch in numeric literal")
627 isExponentPositive = true
628 hasExponent = true
629 ++current
630 continue
631 }
632 if (ch == '-' && hasExponent) {
633 if (current == start) fail("Unexpected symbol '-' in numeric literal")
634 isExponentPositive = false
635 ++current
636 continue
637 }
638 if (ch == '+' && hasExponent) {
639 if (current == start) fail("Unexpected symbol '+' in numeric literal")
640 isExponentPositive = true
641 ++current
642 continue
643 }
644 if (ch == '-') {
645 if (current != start) fail("Unexpected symbol '-' in numeric literal")
646 isNegative = true
647 ++current
648 continue
649 }
650 val token = charToTokenClass(ch)
651 if (token != TC_OTHER) break
652 ++current
653 val digit = ch - '0'
654 if (digit !in 0..9) fail("Unexpected symbol '$ch' in numeric literal")
655 if (hasExponent) {
656 exponentAccumulator = exponentAccumulator * 10 + digit
657 continue
658 }
659 accumulator = accumulator * 10 - digit
660 if (accumulator > 0) fail("Numeric value overflow")
661 }
662 val hasChars = current != start
663 if (start == current || (isNegative && start == current - 1)) {
664 fail("Expected numeric literal")
665 }
666 if (hasQuotation) {
667 if (!hasChars) fail("EOF")
668 if (source[current] != STRING) fail("Expected closing quotation mark")
669 ++current
670 }
671 currentPosition = current
672
673 fun calculateExponent(exponentAccumulator: Long, isExponentPositive: Boolean): Double = when (isExponentPositive) {
674 false -> 10.0.pow(-exponentAccumulator.toDouble())
675 true -> 10.0.pow(exponentAccumulator.toDouble())
676 }
677
678 if (hasExponent) {
679 val doubleAccumulator = accumulator.toDouble() * calculateExponent(exponentAccumulator, isExponentPositive)
680 if (doubleAccumulator > Long.MAX_VALUE || doubleAccumulator < Long.MIN_VALUE) fail("Numeric value overflow")
681 if (floor(doubleAccumulator) != doubleAccumulator) fail("Can't convert $doubleAccumulator to Long")
682 accumulator = doubleAccumulator.toLong()
683 }
684
685 return when {
686 isNegative -> accumulator
687 accumulator != Long.MIN_VALUE -> -accumulator
688 else -> fail("Numeric value overflow")
689 }
690 }
691
692
693 fun consumeBoolean(): Boolean {
694 return consumeBoolean(skipWhitespaces())
695 }
696
697 fun consumeBooleanLenient(): Boolean {
698 var current = skipWhitespaces()
699 if (current == source.length) fail("EOF")
700 val hasQuotation = if (source[current] == STRING) {
701 ++current
702 true
703 } else {
704 false
705 }
706 val result = consumeBoolean(current)
707 if (hasQuotation) {
708 if (currentPosition == source.length) fail("EOF")
709 if (source[currentPosition] != STRING)
710 fail("Expected closing quotation mark")
711 ++currentPosition
712 }
713 return result
714 }
715
716 @JsName("consumeBoolean2") // WA for JS issue
717 private fun consumeBoolean(start: Int): Boolean {
718 /*
719 * In ASCII representation, upper and lower case letters are different
720 * in 6-th bit and we leverage this fact, our implementation consumes boolean literals
721 * in a case-insensitive manner.
722 */
723 var current = prefetchOrEof(start)
724 if (current >= source.length || current == -1) fail("EOF")
725 return when (source[current++].code or asciiCaseMask) {
726 't'.code -> {
727 consumeBooleanLiteral("rue", current)
728 true
729 }
730 'f'.code -> {
731 consumeBooleanLiteral("alse", current)
732 false
733 }
734 else -> {
735 fail("Expected valid boolean literal prefix, but had '${consumeStringLenient()}'")
736 }
737 }
738 }
739
740 private fun consumeBooleanLiteral(literalSuffix: String, current: Int) {
741 if (source.length - current < literalSuffix.length) {
742 fail("Unexpected end of boolean literal")
743 }
744
745 for (i in literalSuffix.indices) {
746 val expected = literalSuffix[i]
747 val actual = source[current + i]
748 if (expected.code != actual.code or asciiCaseMask) {
749 fail("Expected valid boolean literal prefix, but had '${consumeStringLenient()}'")
750 }
751 }
752
753 currentPosition = current + literalSuffix.length
754 }
755
756 private inline fun <T> withPositionRollback(action: () -> T): T {
757 val snapshot = currentPosition
758 try {
759 return action()
760 } finally {
761 currentPosition = snapshot
762 }
763 }
764 }
765