Coverage Summary for Class: GhostParserUtilsKt (com.ghost.serialization.parser)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| GhostParserUtilsKt |
100%
(1/1)
|
100%
(8/8)
|
66.1%
(80/121)
|
87.7%
(93/106)
|
84.7%
(454/536)
|
@file:Suppress("NOTHING_TO_INLINE", "UNNECESSARY_NOT_NULL_ASSERTION")
package com.ghost.serialization.parser
import kotlin.math.pow
import com.ghost.serialization.parser.GhostJsonConstants as C
/**
* Checks if the byte code matches dot, lower 'e', or upper 'E'.
*/
internal inline fun isNumericSeparator(byteCode: Int): Boolean {
return byteCode == C.DOT_INT || byteCode == C.EXP_LOWER_INT || byteCode == C.EXP_UPPER_INT
}
/**
* Checks if the byte code matches lowercase or uppercase 'e'.
*/
internal inline fun isExponentMarker(markerByte: Int): Boolean {
return (markerByte or C.CASE_INSENSITIVE_MASK) == C.EXP_LOWER_INT
}
/**
* Helper to verify if the given byte is a valid JSON numeric digit.
*/
internal inline fun isDigit(byteCode: Int): Boolean {
return (byteCode xor C.ZERO_INT) < C.BASE_TEN
}
/**
* Returns 10.0f raised to the power of exponent using a lookup table or pow fallback.
*/
internal inline fun getFloatPowerOfTen(exponent: Int): Float {
return if (exponent > 0) {
if (exponent < C.POWERS_OF_TEN_FLOAT.size) {
C.POWERS_OF_TEN_FLOAT[exponent]
} else {
10.0f.pow(exponent.toFloat())
}
} else {
val absExp = -exponent
if (absExp < C.INVERSE_POWERS_OF_TEN_FLOAT.size) {
C.INVERSE_POWERS_OF_TEN_FLOAT[absExp]
} else {
10.0f.pow(exponent.toFloat())
}
}
}
/**
* Returns 10.0 raised to the power of exponent using a lookup table or pow fallback.
*/
internal inline fun getDoublePowerOfTen(exponent: Int): Double {
return if (exponent > 0) {
if (exponent < C.POWERS_OF_TEN.size) {
C.POWERS_OF_TEN[exponent]
} else {
10.0.pow(exponent.toDouble())
}
} else {
val absExp = -exponent
if (absExp < C.INVERSE_POWERS_OF_TEN.size) {
C.INVERSE_POWERS_OF_TEN[absExp]
} else {
10.0.pow(exponent.toDouble())
}
}
}
/**
* Pure, reader-agnostic boolean coercion byte matcher.
*
* Compares raw bytes at [start]..[start+length] against the known truthy/falsy
* coercion strings. No String is allocated. Case-insensitive via bitwise
* `or CASE_INSENSITIVE_MASK` (= or 32), which collapses ASCII upper/lower pairs.
*
* Truthy (case-insensitive): "true", "yes", "on", "y", "1".
* Falsy (case-insensitive): "false", "no", "off", "n", "0".
*
* @param start Index of the first content byte (after the opening `"`).
* @param length Number of content bytes (excluding quotes).
* @param getByte Reads byte at absolute index as an unsigned int (0-255).
* @param onError Called when no coercion match — must throw. The [Nothing] return
* type keeps every branch exhaustive without a dead-code stub.
*/
internal inline fun matchCoerceBooleanBytes(
start: Int,
length: Int,
onError: () -> Nothing,
getByte: (Int) -> Int,
): Boolean = when (length) {
C.BOOL_STR_LEN_1 -> {
val b0 = getByte(start)
when (b0 or C.CASE_INSENSITIVE_MASK) {
C.FOLD_Y -> true // "y" / "Y"
C.FOLD_N -> false // "n" / "N"
else -> when (b0) {
C.ONE_INT -> true
// "1"
C.ZERO_INT -> false
// "0"
else -> onError()
}
}
}
C.BOOL_STR_LEN_2 -> {
val b0 = getByte(start)
val b1 = getByte(start + 1)
when {
// "on" / "ON"
(b0 or C.CASE_INSENSITIVE_MASK) == C.FOLD_O &&
(b1 or C.CASE_INSENSITIVE_MASK) == C.FOLD_N -> true
// "no" / "NO"
(b0 or C.CASE_INSENSITIVE_MASK) == C.FOLD_N &&
(b1 or C.CASE_INSENSITIVE_MASK) == C.FOLD_O -> false
else -> onError()
}
}
C.BOOL_STR_LEN_3 -> {
val b0 = getByte(start)
val b1 = getByte(start + 1)
val b2 = getByte(start + 2)
when {
// "yes" / "YES"
(b0 or C.CASE_INSENSITIVE_MASK) == C.FOLD_Y &&
(b1 or C.CASE_INSENSITIVE_MASK) == C.FOLD_E &&
(b2 or C.CASE_INSENSITIVE_MASK) == C.FOLD_S -> true
// "off" / "OFF"
(b0 or C.CASE_INSENSITIVE_MASK) == C.FOLD_O &&
(b1 or C.CASE_INSENSITIVE_MASK) == C.FOLD_F &&
(b2 or C.CASE_INSENSITIVE_MASK) == C.FOLD_F -> false
else -> onError()
}
}
C.BOOL_STR_LEN_4 -> {
val b0 = getByte(start)
val b1 = getByte(start + 1)
val b2 = getByte(start + 2)
val b3 = getByte(start + 3)
// "true" / "TRUE"
if ((b0 or C.CASE_INSENSITIVE_MASK) == C.FOLD_T &&
(b1 or C.CASE_INSENSITIVE_MASK) == C.FOLD_R &&
(b2 or C.CASE_INSENSITIVE_MASK) == C.FOLD_U &&
(b3 or C.CASE_INSENSITIVE_MASK) == C.FOLD_E) true
else onError()
}
C.BOOL_STR_LEN_5 -> {
val b0 = getByte(start)
val b1 = getByte(start + 1)
val b2 = getByte(start + 2)
val b3 = getByte(start + 3)
val b4 = getByte(start + 4)
// "false" / "FALSE"
if ((b0 or C.CASE_INSENSITIVE_MASK) == C.FOLD_F &&
(b1 or C.CASE_INSENSITIVE_MASK) == C.FOLD_A &&
(b2 or C.CASE_INSENSITIVE_MASK) == C.FOLD_L &&
(b3 or C.CASE_INSENSITIVE_MASK) == C.FOLD_S &&
(b4 or C.CASE_INSENSITIVE_MASK) == C.FOLD_E) false
else onError()
}
else -> onError()
}
/**
* Maps a char-based position in a Kotlin [String] to its corresponding byte offset in
* the equivalent UTF-8 representation, without allocating a [ByteArray].
*
* Used by [GhostJsonStringReader] to bridge char-indexed positions (String reader)
* to byte-indexed positions expected by custom decoders generated by the KSP compiler.
*
* Surrogate pairs (supplementary plane code points, U+10000..U+10FFFF) consume two
* `Char` slots in a Kotlin String but encode as four UTF-8 bytes.
*
* @param s The source Kotlin String.
* @param charPos The char-indexed position to convert (0 = start of string).
* @return The UTF-8 byte offset corresponding to [charPos].
*/
@com.ghost.serialization.InternalGhostApi
fun charToBytePosition(s: String, charPos: Int): Int {
var bytePos = 0
var i = 0
while (i < charPos && i < s.length) {
val code = s[i].code
when {
code <= C.UTF8_1BYTE_MAX -> {
bytePos += C.UTF8_1BYTE_SIZE
i++
}
code <= C.UTF8_2BYTE_MAX -> {
bytePos += C.UTF8_2BYTE_SIZE
i++
}
code in C.HIGH_SURROGATE_START..C.HIGH_SURROGATE_END &&
i + 1 < s.length &&
s[i + 1].code in C.LOW_SURROGATE_START..C.LOW_SURROGATE_END -> {
bytePos += C.UTF8_4BYTE_SIZE
i += 2
}
else -> {
bytePos += C.UTF8_3BYTE_SIZE
i++
}
}
}
return bytePos
}
/**
* Maps a UTF-8 byte offset back to its corresponding char-indexed position in a Kotlin [String].
*
* This is the inverse of [charToBytePosition]. Used by the KSP compiler bridge to translate
* byte offsets returned by custom decoders back into char positions for the String reader.
*
* @param s The source Kotlin String.
* @param targetBytePos The UTF-8 byte offset to convert.
* @return The char-indexed position in [s] corresponding to [targetBytePos].
*/
@com.ghost.serialization.InternalGhostApi
fun byteToCharPosition(s: String, targetBytePos: Int): Int {
var bytePos = 0
var i = 0
while (bytePos < targetBytePos && i < s.length) {
val code = s[i].code
when {
code <= C.UTF8_1BYTE_MAX -> {
bytePos += C.UTF8_1BYTE_SIZE
i++
}
code <= C.UTF8_2BYTE_MAX -> {
bytePos += C.UTF8_2BYTE_SIZE
i++
}
code in C.HIGH_SURROGATE_START..C.HIGH_SURROGATE_END &&
i + 1 < s.length &&
s[i + 1].code in C.LOW_SURROGATE_START..C.LOW_SURROGATE_END -> {
bytePos += C.UTF8_4BYTE_SIZE
i += 2
}
else -> {
bytePos += C.UTF8_3BYTE_SIZE
i++
}
}
}
return i
}