Coverage Summary for Class: GhostJsonReader (com.ghost.serialization.parser)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| GhostJsonReader |
100%
(1/1)
|
89.5%
(34/38)
|
67.2%
(117/174)
|
87.6%
(275/314)
|
83.3%
(1338/1606)
|
@file:OptIn(InternalGhostApi::class)
@file:Suppress("FunctionName", "unused")
package com.ghost.serialization.parser
import com.ghost.serialization.parser.GhostJsonConstants as C
import com.ghost.serialization.InternalGhostApi
import com.ghost.serialization.acquireScratchBuffer
import com.ghost.serialization.exception.GhostJsonException
import com.ghost.serialization.releaseScratchBuffer
import okio.BufferedSource
import okio.ByteString
import okio.ByteString.Companion.encodeUtf8
/**
* A high-performance, zero-allocation JSON parser for Kotlin Multiplatform.
*
* This reader is optimized for generated code and raw speed by:
* - Using an [Int]-based (0-255) data access pattern to eliminate sign-extension overhead.
* - Implementing a "Fast-Path" that operates directly on [ByteArray] when possible.
* - Reusing strings via an internal [stringPool] to reduce memory pressure.
* - Providing a "Discriminator Peeker" for ultra-fast polymorphic deserialization.
* - Minimizing virtual dispatch by caching the raw source data.
*/
class GhostJsonReader(
@PublishedApi internal var source: GhostSource,
@PublishedApi internal var limit: Int = source.size,
var maxDepth: Int = C.MAX_DEPTH,
/**
* When true, enables strict JSON validation: rejects unknown/unmapped fields
* and performs strict bitwise syntax validation on missing or duplicate commas.
* Defaults to false for maximum lenient parsing performance.
*/
var strictMode: Boolean = false,
var coerceStringsToNumbers: Boolean = false,
var coerceBooleans: Boolean = false,
var maxCollectionSize: Int = GhostHeuristics.maxCollectionSize
) {
/** Cached raw byte array for fast-path access. Eliminates interface dispatch. */
@PublishedApi
internal var rawData: ByteArray = source.rawSourceData
@PublishedApi
internal val isStreaming: Boolean = source is StreamingGhostSource
@PublishedApi
internal var position: Int = 0
@PublishedApi
internal var nextTokenByte: Int = -1
@InternalGhostApi
fun _getPosition(): Int = position
@InternalGhostApi
fun _setPosition(p: Int) {
position = p
}
@InternalGhostApi
fun _getRawData(): ByteArray = rawData
@InternalGhostApi
fun _setNextTokenByte(t: Int) {
nextTokenByte = t
}
internal val stringPool = arrayOfNulls<String>(C.STR_POOL_SIZE)
/**
* Set during [GhostSource.scanString] fast path: false if any content byte had bit 7 set
* (UTF-8 multibyte); true if only ASCII bytes were scanned (including empty string).
*/
internal var lastScanContentWas7BitOnly: Boolean = false
/** Current nesting depth (object/array).
* Incremented on begin*, decremented on end*. */
var depth: Int = 0
@PublishedApi internal var needsCommaMask: Long = 0L
@PublishedApi internal var commaConsumedMask: Long = 0L
/** Convenience constructor for ByteArray —
* used by KSP-generated serializers and tests. */
constructor(
bytes: ByteArray,
maxDepth: Int = C.MAX_DEPTH,
strictMode: Boolean = false,
coerceStringsToNumbers: Boolean = false,
coerceBooleans: Boolean = false,
maxCollectionSize: Int = GhostHeuristics.maxCollectionSize
) : this(
createByteArraySource(bytes),
bytes.size,
maxDepth,
strictMode,
coerceStringsToNumbers,
coerceBooleans,
maxCollectionSize
)
/** New Streaming Constructor for Okio Source */
constructor(
okioSource: BufferedSource,
maxDepth: Int = C.MAX_DEPTH,
strictMode: Boolean = false,
coerceStringsToNumbers: Boolean = false,
coerceBooleans: Boolean = false,
maxCollectionSize: Int = GhostHeuristics.maxCollectionSize
) : this(
createSourceBridge(okioSource),
Int.MAX_VALUE, // Limit is unknown for streaming
maxDepth,
strictMode,
coerceStringsToNumbers,
coerceBooleans,
maxCollectionSize
)
/**
* Optimized byte access.
* Uses hardware-level zero-extension if [rawData] is available,
* bypassing interface overhead.
*/
@PublishedApi
@Suppress("NOTHING_TO_INLINE")
internal inline fun getByte(index: Int): Int {
if (isStreaming) {
return source[index]
}
return rawData[index].toInt() and C.BYTE_MASK
}
/**
* Throws a structured [GhostJsonException] with exact position, line, and column numbers.
*/
fun throwError(message: String): Nothing {
val errorPosition = position
val sourceRef = source
val errorEnd = if (errorPosition > sourceRef.size) {
sourceRef.size
} else {
errorPosition
}
throw GhostJsonException(
baseMessage = "$message at position $errorPosition",
computeLineCol = {
var columnNumber = 0
var lineNumber = 0
var byteIndex = 0
while (byteIndex < errorEnd) {
if (sourceRef[byteIndex] == C.NEWLINE_INT) {
lineNumber++
columnNumber = 0
} else {
columnNumber++
}
byteIndex++
}
intArrayOf(lineNumber, columnNumber)
}
)
}
/**
* Consumes the next non-whitespace byte and validates it against [expected].
* Primarily used for manual parsing and testing.
*/
fun expectByte(expected: Int) {
if (peekNextToken() != expected) {
throwError(
"Expected '${Char(expected)}' but found '${Char(nextTokenByte)}'"
)
}
if (expected == C.COMMA_INT) {
if (depth < C.MAX_BITMASK_DEPTH) {
val bit = C.BITMASK_UNIT shl depth
commaConsumedMask = commaConsumedMask or bit
needsCommaMask = needsCommaMask and bit.inv()
}
}
internalSkip(1)
}
/**
* Skips [n] bytes and resets the cached [nextTokenByte].
*/
fun internalSkip(n: Int) {
position += n
nextTokenByte = -1
}
/**
* Advances the position past any whitespace and caches the next non-whitespace token byte.
*/
fun skipWhitespace() {
val nextPos = if (isStreaming) {
source.findNextNonWhitespace(position, limit)
} else {
val localData = rawData
findNextNonWhitespaceImpl(position, limit) { localData[it].toInt() and C.BYTE_MASK }
}
if (nextPos != -1) {
position = nextPos
nextTokenByte = getByte(position)
} else {
position = limit
nextTokenByte = C.MATCH_END
}
}
/**
* Attempts to peek at the discriminator value (e.g. "type") of the current object.
* Does not advance the reader's position.
* Returns null if not found or if the current token is not an object start.
* Used by KSP-generated serializers for polymorphic deserialization.
*/
fun peekDiscriminator(key: String = C.DEFAULT_DISCRIMINATOR_KEY): String? {
if (key == C.DEFAULT_DISCRIMINATOR_KEY) {
return peekDiscriminator(C.TYPE_BS)
}
return peekDiscriminator(key.encodeUtf8())
}
/**
* Internal version that takes a [ByteString] for maximum performance.
* Used by KSP-generated serializers for polymorphic deserialization.
*/
fun peekDiscriminator(key: ByteString): String? {
return GhostDiscriminatorPeeker.peek(
source,
rawData,
isStreaming,
position,
limit,
key
)
}
/**
* Peeks and returns the next token byte in the stream, skipping preceding whitespaces.
*/
fun peekNextToken(): Int {
val cached = nextTokenByte
if (cached != -1) return cached
skipWhitespace()
return nextTokenByte
}
/**
* Peeks and returns the next token byte as a [Byte].
*/
fun peekByte(): Byte = peekNextToken().toByte()
/**
* Consumes and returns the next non-whitespace token byte in the stream.
*/
fun nextNonWhitespace(): Int {
val nextToken = peekNextToken()
if (nextToken == -1) {
throwError(C.ERR_UNEXPECTED_EOF)
}
internalSkip(1)
return nextToken
}
/**
* Skips and validates that the next characters in the stream match the [expected] byte sequence.
*/
@InternalGhostApi
fun skipAndValidateLiteral(expected: ByteString) {
val size = expected.size
val isValid = if (isStreaming) {
source.contentEquals(position, expected)
} else {
position + size <= limit && expected.rangeEquals(0, rawData, position, size)
}
if (!isValid) {
throwError("Expected literal ${expected.utf8()}")
}
position += size
nextTokenByte = -1
}
/**
* Reads a quoted JSON string.
*
* This implementation features:
* 1. **Fast-path**: Direct decoding if no escapes are present.
* 2. **String Pooling**: Checks [stringPool] to reuse existing String instances.
* 3. **Slow-path**: StringBuilder-like approach using a pooled char buffer for escapes.
*/
fun readQuotedString(): String {
if (nextNonWhitespace() != C.QUOTE_INT) {
throwError(C.ERR_EXPECTED_QUOTE)
}
val start = position
val scanResult = if (isStreaming) {
source.scanString(start, limit)
} else {
val localData = rawData
scanStringImpl(start, limit) { localData[it].toInt() and C.BYTE_MASK }
}
if (scanResult != -1L) {
val length = ((scanResult and C.SCAN_LENGTH_MASK) ushr C.SCAN_LENGTH_SHIFT).toInt()
val rollingHash = scanResult.toInt()
val only7Bit = (scanResult and C.SCAN_7BIT_BIT) != 0L
lastScanContentWas7BitOnly = only7Bit
val end = start + length
if (length <= 0) {
position = end + 1
nextTokenByte = -1
return ""
}
if (length > GhostHeuristics.maxStringPoolLength) {
val result = source.decodeJsonStringRange(start, end, only7Bit)
position = end + 1
nextTokenByte = -1
return result
}
val poolBucketIndex = rollingHash and (C.STR_POOL_SIZE - 1)
val cachedString = stringPool[poolBucketIndex]
val isMatch = if (only7Bit && cachedString != null) {
if (isStreaming) {
source.contentEqualsString(start, length, cachedString)
} else {
val localData = rawData
contentEqualsStringImpl(start, length, cachedString) { localData[it].toInt() and C.BYTE_MASK }
}
} else {
false
}
if (isMatch) {
position = end + 1
nextTokenByte = -1
return cachedString!!
}
val decodedString = source.decodeJsonStringRange(start, end, only7Bit)
if (only7Bit) {
stringPool[poolBucketIndex] = decodedString
}
position = end + 1
nextTokenByte = -1
return decodedString
}
return readQuotedStringSlow(start)
}
private fun GhostJsonReader.readQuotedStringSlow(start: Int): String {
// Slow path: manual string building for escapes (Bitwise & Zero-Allocation approach)
var outBuffer = acquireScratchBuffer(C.TIER_SMALL_INT)
var outPos = 0
try {
var pos = start
while (pos < limit) {
val byteValue = getByte(pos++)
if (byteValue == C.QUOTE_INT) {
position = pos
nextTokenByte = -1
return outBuffer.decodeToString(0, outPos)
}
if (byteValue == C.BACKSLASH_INT) {
if (pos >= limit) {
position = pos
throwError(C.UNTERMINATED_ESCAPE_ERROR)
}
when (val escaped = getByte(pos++)) {
C.UNICODE_PREFIX_U_INT -> {
if (pos + C.UNICODE_HEX_LENGTH > limit) {
position = pos
throwError(C.UNTERMINATED_UNICODE_ERROR)
}
var code = parseUnicodeHex(pos)
pos += C.UNICODE_HEX_LENGTH
if (code in C.HIGH_SURROGATE_START..C.HIGH_SURROGATE_END) {
if (pos + C.SURROGATE_OFFSET <= limit &&
getByte(pos) == C.BACKSLASH_INT &&
getByte(pos + C.SINGLE_CHAR_SIZE) == C.UNICODE_PREFIX_U_INT
) {
// Valid surrogate pair check
pos += C.UNICODE_ESCAPE_PREFIX_SIZE
val lowCode = parseUnicodeHex(pos)
if (lowCode in C.LOW_SURROGATE_START..C.LOW_SURROGATE_END) {
pos += C.UNICODE_HEX_LENGTH
code = C.UNICODE_BASE +
((code - C.HIGH_SURROGATE_START) shl C.SHIFT_10) +
(lowCode - C.LOW_SURROGATE_START)
} else {
position = pos
throwError(C.ERR_HIGH_SURROGATE)
}
} else {
position = pos
throwError(C.ERR_HIGH_SURROGATE)
}
}
// Encode code point to UTF-8 bytes in outBuffer
if (code <= C.UTF8_1BYTE_MAX) {
if (outPos + 1 > outBuffer.size) {
outBuffer = growBuffer(outBuffer, outPos)
}
outBuffer[outPos++] = code.toByte()
} else if (code <= C.UTF8_2BYTE_MAX) {
if (outPos + 2 > outBuffer.size) {
outBuffer = growBuffer(outBuffer, outPos)
}
outBuffer[outPos++] = (C.UTF8_2BYTE_PREFIX or (code shr C.UTF8_SHIFT_6)).toByte()
outBuffer[outPos++] = (C.UTF8_CONT_PREFIX or (code and C.UTF8_CONT_MASK)).toByte()
} else if (code <= C.BMP_LIMIT) {
if (outPos + 3 > outBuffer.size) {
outBuffer = growBuffer(outBuffer, outPos)
}
outBuffer[outPos++] = (C.UTF8_3BYTE_PREFIX or (code shr C.UTF8_SHIFT_12)).toByte()
outBuffer[outPos++] = (C.UTF8_CONT_PREFIX or ((code shr C.UTF8_SHIFT_6) and C.UTF8_CONT_MASK)).toByte()
outBuffer[outPos++] = (C.UTF8_CONT_PREFIX or (code and C.UTF8_CONT_MASK)).toByte()
} else {
if (outPos + 4 > outBuffer.size) {
outBuffer = growBuffer(outBuffer, outPos)
}
outBuffer[outPos++] = (C.UTF8_4BYTE_PREFIX or (code shr C.UTF8_SHIFT_18)).toByte()
outBuffer[outPos++] = (C.UTF8_CONT_PREFIX or ((code shr C.UTF8_SHIFT_12) and C.UTF8_CONT_MASK)).toByte()
outBuffer[outPos++] = (C.UTF8_CONT_PREFIX or ((code shr C.UTF8_SHIFT_6) and C.UTF8_CONT_MASK)).toByte()
outBuffer[outPos++] = (C.UTF8_CONT_PREFIX or (code and C.UTF8_CONT_MASK)).toByte()
}
}
C.N_BYTE_INT -> {
if (outPos + 1 > outBuffer.size) {
outBuffer = growBuffer(outBuffer, outPos)
}
outBuffer[outPos++] = C.LF_INT.toByte()
}
C.R_BYTE_INT -> {
if (outPos + 1 > outBuffer.size) {
outBuffer = growBuffer(outBuffer, outPos)
}
outBuffer[outPos++] = C.CR_INT.toByte()
}
C.T_BYTE_INT -> {
if (outPos + 1 > outBuffer.size) {
outBuffer = growBuffer(outBuffer, outPos)
}
outBuffer[outPos++] = C.TAB_INT.toByte()
}
C.B_BYTE_INT -> {
if (outPos + 1 > outBuffer.size) {
outBuffer = growBuffer(outBuffer, outPos)
}
outBuffer[outPos++] = C.BS_INT.toByte()
}
C.F_BYTE_INT -> {
if (outPos + 1 > outBuffer.size) {
outBuffer = growBuffer(outBuffer, outPos)
}
outBuffer[outPos++] = C.FF_INT.toByte()
}
else -> {
if (outPos + 1 > outBuffer.size) {
outBuffer = growBuffer(outBuffer, outPos)
}
outBuffer[outPos++] = escaped.toByte()
}
}
} else if (byteValue < C.SPACE_INT) {
position = pos
throwError(C.UNESCAPED_CONTROL_CHAR_ERROR)
} else {
if (outPos + 1 > outBuffer.size) {
outBuffer = growBuffer(outBuffer, outPos)
}
outBuffer[outPos++] = byteValue.toByte()
}
}
position = pos
} finally {
releaseScratchBuffer(outBuffer)
}
throwError(C.UNTERMINATED_STRING_ERROR)
}
/**
* Skips a double-quoted JSON string in the raw source without decoding its content.
*/
fun skipQuotedString() {
if (nextNonWhitespace() != C.QUOTE_INT) {
throwError(C.ERR_EXPECTED_QUOTE)
}
val start = position
val end = if (isStreaming) {
source.findClosingQuote(start, limit)
} else {
val localData = rawData
findClosingQuoteImpl(start, limit) { localData[it].toInt() and C.BYTE_MASK }
}
if (end != -1) {
position = end + 1
return
}
var pos = start
while (pos < limit) {
val byteValue = getByte(pos++)
if (byteValue == C.QUOTE_INT) {
position = pos
nextTokenByte = -1
return
}
if (byteValue == C.BACKSLASH_INT) {
if (pos >= limit) {
position = pos
throwError(C.UNTERMINATED_ESCAPE_ERROR)
}
val escaped = getByte(pos++)
if (escaped == C.UNICODE_PREFIX_U_INT) {
if (pos + C.UNICODE_HEX_LENGTH > limit) {
position = pos
throwError(C.UNTERMINATED_UNICODE_ERROR)
}
parseUnicodeHex(pos)
pos += C.UNICODE_HEX_LENGTH
}
} else if (byteValue < C.SPACE_INT) {
position = pos
throwError(C.UNESCAPED_CONTROL_CHAR_ERROR)
}
}
position = pos
throwError(C.UNTERMINATED_STRING_ERROR)
}
/**
* Parses 4 hex digits from the byte array at the given position and returns the resulting code point.
*/
private fun parseUnicodeHex(currentPosition: Int): Int {
val hexByte0 = getByte(currentPosition)
val hexByte1 = getByte(currentPosition + 1)
val hexByte2 = getByte(currentPosition + 2)
val hexByte3 = getByte(currentPosition + 3)
val hexLookupTable = C.HEX_LUT
val digitValue0 = hexLookupTable[hexByte0]
val digitValue1 = hexLookupTable[hexByte1]
val digitValue2 = hexLookupTable[hexByte2]
val digitValue3 = hexLookupTable[hexByte3]
if ((digitValue0 or digitValue1 or digitValue2 or digitValue3) < 0) {
throwError("Invalid unicode escape at $currentPosition")
}
return (digitValue0 shl C.SHIFT_12) or
(digitValue1 shl C.SHIFT_8) or
(digitValue2 shl C.SHIFT_4) or
digitValue3
}
/**
* Utility helper to grow a temporary byte array buffer.
*/
private fun growBuffer(outBuffer: ByteArray, outPos: Int): ByteArray {
val newBuffer = acquireScratchBuffer(outBuffer.size * C.BUFFER_SCALE_FACTOR)
outBuffer.copyInto(newBuffer, 0, 0, outPos)
releaseScratchBuffer(outBuffer)
return newBuffer
}
/**
* Resets the reader's state to process a new flat byte array payload.
*/
fun reset(newData: ByteArray, newLimit: Int = newData.size) {
val currentSource = this.source
if (currentSource is ByteArrayGhostSource) {
currentSource.data = newData
reset(currentSource, newLimit)
} else {
reset(createByteArraySource(newData), newLimit)
}
}
/**
* Resets the reader's state to process a new streaming Okio [BufferedSource] payload.
*/
fun reset(okioSource: BufferedSource) {
reset(createSourceBridge(okioSource), Int.MAX_VALUE)
}
/**
* Resets the reader state with a new [GhostSource].
* Clears cached tokens, positions, and depth for reuse.
*/
fun reset(newSource: GhostSource, newLimit: Int = newSource.size) {
this.source = newSource
this.rawData = newSource.rawSourceData
this.position = 0
this.limit = newLimit
this.nextTokenByte = -1
this.depth = 0
this.needsCommaMask = 0L
this.commaConsumedMask = 0L
this.strictMode = false
this.coerceStringsToNumbers = false
this.coerceBooleans = false
this.maxDepth = C.MAX_DEPTH
this.maxCollectionSize = GhostHeuristics.maxCollectionSize
this.lastScanContentWas7BitOnly = false
}
}