Coverage Summary for Class: GhostJsonReaderNumbersKt (com.ghost.serialization.parser)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| GhostJsonReaderNumbersKt |
100%
(1/1)
|
100%
(18/18)
|
65.7%
(142/216)
|
87.8%
(267/304)
|
84%
(1235/1470)
|
@file:OptIn(InternalGhostApi::class)
@file:Suppress("UNNECESSARY_NOT_NULL_ASSERTION", "NOTHING_TO_INLINE")
package com.ghost.serialization.parser
import com.ghost.serialization.InternalGhostApi
import com.ghost.serialization.parser.GhostJsonConstants as C
/**
* Reads a JSON number and returns it as a Float.
* Uses a zero-allocation, register-based loop for maximum speed.
* Used by KSP-generated serializers.
*
* @throws com.ghost.serialization.exception.GhostJsonException
* if float format is invalid or overflows.
*/
fun GhostJsonReader.nextFloat(): Float {
val header = prepareNumericHeader()
val isQuoted = (header and C.NUMERIC_HEADER_QUOTED) != 0
val isNegativeValue = (header and C.NUMERIC_HEADER_NEGATIVE) != 0
validateLeadingZero()
var mantissa = 0L
var exponent = 0
var digitCount = 0
// Integer part
nextTokenByte = -1
readNumericLoop { byte ->
val digit = byte - C.ZERO_INT
if (digitCount < C.FLOAT_PRECISION_LIMIT) {
mantissa = mantissa * C.BASE_TEN + digit
digitCount++
} else {
exponent++
}
}
if (digitCount == 0) {
throwError(C.ERR_EXPECTED_INT_PART)
}
// Decimal part
val pos = position
val lim = limit
if (pos < lim && getByte(pos) == C.DOT_INT) {
val newPos = pos + 1
position = newPos
readNumericLoop { byte ->
val digit = byte - C.ZERO_INT
if (digitCount < C.FLOAT_PRECISION_LIMIT) {
mantissa = mantissa * C.BASE_TEN + digit
digitCount++
exponent--
}
}
if (position == newPos) {
throwError(C.ERR_EXPECTED_DECIMAL_DIGITS)
}
}
// Exponent part
val currentPos = position
val currentLimit = limit
if (currentPos < currentLimit && isExponentMarker(getByte(currentPos))) {
exponent += parseExponentValue()
}
var result = mantissa.toFloat()
if (exponent != 0) {
result *= getFloatPowerOfTen(exponent)
}
if (isNegativeValue) {
result = -result
}
validateNumericRangeFloat(result)
if (isQuoted) {
consumeNumericCoercionFooter()
}
return result
}
/**
* Reads a JSON number and returns it as a Double.
* Uses a zero-allocation, register-based loop for maximum speed.
* Used by KSP-generated serializers.
*
* @throws com.ghost.serialization.exception.GhostJsonException
* if double format is invalid or overflows.
*/
fun GhostJsonReader.nextDouble(): Double {
val header = prepareNumericHeader()
val isQuoted = (header and C.NUMERIC_HEADER_QUOTED) != 0
val isNegativeValue = (header and C.NUMERIC_HEADER_NEGATIVE) != 0
validateLeadingZero()
var mantissa = 0L
var exponent = 0
var digitCount = 0
// Integer part
nextTokenByte = -1
readNumericLoop { byte ->
val digit = byte - C.ZERO_INT
if (digitCount < C.DOUBLE_PRECISION_LIMIT) {
mantissa = mantissa * C.BASE_TEN + digit
digitCount++
} else {
exponent++
}
}
if (digitCount == 0) {
throwError(C.ERR_EXPECTED_INT_PART)
}
// Decimal part
val pos = position
val lim = limit
if (pos < lim && getByte(pos) == C.DOT_INT) {
val newPos = pos + 1
position = newPos
readNumericLoop { byte ->
val digitValue = byte - C.ZERO_INT
if (digitCount < C.DOUBLE_PRECISION_LIMIT) {
mantissa = mantissa * C.BASE_TEN + digitValue
digitCount++
exponent--
}
}
if (position == newPos) {
throwError(C.ERR_EXPECTED_DECIMAL_DIGITS)
}
}
// Exponent part
val currentPos = position
val currentLimit = limit
if (currentPos < currentLimit && isExponentMarker(getByte(currentPos))) {
exponent += parseExponentValue()
}
var result = mantissa.toDouble()
if (exponent != 0) {
result *= getDoublePowerOfTen(exponent)
}
if (isNegativeValue) {
result = -result
}
validateNumericRange(result)
if (isQuoted) {
consumeNumericCoercionFooter()
}
return result
}
/**
* Parses the exponent value suffix (e.g. e-5) from the stream.
*/
private inline fun GhostJsonReader.parseExponentValue(): Int {
position++
var isExpNegative = false
if (position < limit) {
val marker = getByte(position)
if (marker == C.MINUS_INT) {
isExpNegative = true
position++
} else if (marker == C.PLUS_INT) {
position++
}
}
var expValue = 0
var hasExpDigits = false
while (position < limit) {
val currentByteInt = getByte(position)
if (isDigit(currentByteInt)) {
if (expValue < C.EXPONENT_CLAMP_THRESHOLD) {
expValue = expValue * C.BASE_TEN + (currentByteInt - C.ZERO_INT)
}
hasExpDigits = true
position++
} else {
break
}
}
if (!hasExpDigits) {
throwError(C.ERR_EXPECTED_EXPONENT_DIGITS)
}
return if (isExpNegative) -expValue else expValue
}
/**
* Reads a JSON integer and returns it as an Int.
* Optimized for common small integers.
* Used by KSP-generated serializers.
*
* @throws com.ghost.serialization.exception.GhostJsonException
* if the integer is invalid or overflows.
*/
fun GhostJsonReader.nextInt(): Int {
val header = prepareNumericHeader()
val isQuoted = (header and C.NUMERIC_HEADER_QUOTED) != 0
val isNegativeValue = (header and C.NUMERIC_HEADER_NEGATIVE) != 0
val startOfNumber = position
val absoluteValue = if (
startOfNumber < limit &&
getByte(startOfNumber) == C.ZERO_INT
) {
handleLeadingZero()
0
} else {
parseIntDigits(isNegativeValue, startOfNumber)
}
val finalIntResult = if (isNegativeValue) {
-absoluteValue
} else {
absoluteValue
}
if (isQuoted) {
consumeNumericCoercionFooter()
}
return finalIntResult
}
/**
* Reads a JSON long and returns it as a Long.
* Optimized for common small longs.
* Used by KSP-generated serializers.
*
* @throws com.ghost.serialization.exception.GhostJsonException
* if the long is invalid or overflows.
*/
fun GhostJsonReader.nextLong(): Long {
val header = prepareNumericHeader()
val isQuoted = (header and C.NUMERIC_HEADER_QUOTED) != 0
val isNegativeValue = (header and C.NUMERIC_HEADER_NEGATIVE) != 0
val startOfNumber = position
val absoluteValue = if (
startOfNumber < limit &&
getByte(startOfNumber) == C.ZERO_INT
) {
handleLeadingZero()
0L
} else {
parseLongDigits(isNegativeValue, startOfNumber)
}
val finalLongResult = if (absoluteValue == Long.MIN_VALUE) {
absoluteValue
} else {
(if (isNegativeValue) -absoluteValue else absoluteValue)
}
if (isQuoted) {
consumeNumericCoercionFooter()
}
return finalLongResult
}
/**
* Prepares the numeric header by checking the negative sign and checking string coercion quote.
*/
private fun GhostJsonReader.prepareNumericHeader(): Int {
if (nextTokenByte == C.RESET_TOKEN_BYTE) {
skipWhitespace()
}
if (position >= limit) {
throwError(C.ERR_EXPECTED_NUMBER)
}
var header = 0
var token = nextTokenByte
if (token == C.QUOTE_INT) {
if (!coerceStringsToNumbers) {
throwError(C.ERR_COERCION_DISABLED)
}
position++
nextTokenByte = C.RESET_TOKEN_BYTE
skipWhitespace()
if (position >= limit) {
throwError(C.ERR_EXPECTED_NUMBER)
}
token = nextTokenByte
header = header or C.NUMERIC_HEADER_QUOTED
}
if (token == C.MINUS_INT) {
if (position + 1 >= limit) {
throwError(C.ERR_ISOLATED_MINUS)
}
position++
nextTokenByte = C.RESET_TOKEN_BYTE
header = header or C.NUMERIC_HEADER_NEGATIVE
}
return header
}
/**
* Validates and consumes a leading zero.
*/
private fun GhostJsonReader.handleLeadingZero() {
val nextCursor = position + 1
if (nextCursor < limit) {
val nextDigitByte = getByte(nextCursor)
if (nextDigitByte in C.ZERO_INT..C.NINE_INT) {
throwError(C.ERR_LEADING_ZEROS)
}
}
internalSkip(1)
}
/**
* Bitwise parsing loop for integer digits with overflow verification.
*/
private fun GhostJsonReader.parseIntDigits(
isNegative: Boolean,
startOfNumber: Int
): Int {
var accumulatedValue = 0
var digitCount = 0
var hasDigitsFound = false
nextTokenByte = C.RESET_TOKEN_BYTE
var earlyExitResult: Int? = null
readNumericLoop(
{ byte ->
val digit = byte - C.ZERO_INT
accumulatedValue = if (digitCount < C.INT_SAFE_DIGITS) {
accumulatedValue * C.BASE_TEN + digit
} else {
calculateIntWithOverflowCheck(
accumulatedValue,
digit,
isNegative
)
}
digitCount++
hasDigitsFound = true
},
{ byte ->
if (isNumericSeparator(byte)) {
position = startOfNumber
earlyExitResult = nextDouble().toInt()
}
}
)
if (earlyExitResult != null) {
return earlyExitResult!!
}
if (!hasDigitsFound) {
throwError(C.ERR_EXPECTED_INT_PART)
}
return accumulatedValue
}
/**
* Bitwise parsing loop for long digits with overflow verification.
*/
private fun GhostJsonReader.parseLongDigits(
isNegative: Boolean,
startOfNumber: Int
): Long {
var accumulatedValue = 0L
var digitCount = 0
var hasDigitsFound = false
nextTokenByte = C.RESET_TOKEN_BYTE
var earlyExitResult: Long? = null
readNumericLoop(
{ byte ->
val digit = byte - C.ZERO_INT
accumulatedValue = if (digitCount < C.LONG_SAFE_DIGITS) {
accumulatedValue * C.BASE_TEN + digit
} else {
calculateLongWithOverflowCheck(
accumulatedValue,
digit,
isNegative
)
}
digitCount++
hasDigitsFound = true
},
{ byte ->
if (isNumericSeparator(byte)) {
position = startOfNumber
earlyExitResult = nextDouble().toLong()
}
}
)
if (earlyExitResult != null) {
return earlyExitResult!!
}
if (!hasDigitsFound) {
throwError(C.ERR_EXPECTED_INT_PART)
}
return accumulatedValue
}
/**
* Consumes the closing quotation mark for coerced numeric string values.
*/
private inline fun GhostJsonReader.consumeNumericCoercionFooter() {
if (position >= limit || getByte(position) != C.QUOTE_INT) {
throwError(C.ERR_EXPECTED_COERCION_QUOTE)
}
internalSkip(1)
skipWhitespace()
}
/**
* Accumulates and checks Int bounds to throw
* [com.ghost.serialization.exception.GhostJsonException] on overflow.
*/
private inline fun GhostJsonReader.calculateIntWithOverflowCheck(
current: Int,
digitValue: Int,
isNegative: Boolean
): Int {
if (current > C.INT_OVERFLOW_LIMIT ||
(current == C.INT_OVERFLOW_LIMIT &&
digitValue > (if (isNegative) C.INT_MIN_LAST_DIGIT else C.INT_MAX_LAST_DIGIT))
) {
throwError(C.ERR_INT_OVERFLOW)
}
return current * C.BASE_TEN + digitValue
}
/**
* Accumulates and checks Long bounds to throw
* [com.ghost.serialization.exception.GhostJsonException] on overflow.
*/
private inline fun GhostJsonReader.calculateLongWithOverflowCheck(
current: Long,
digitValue: Int,
isNegative: Boolean
): Long {
if (current == Long.MIN_VALUE ||
current > C.LONG_OVERFLOW_LIMIT ||
(current == C.LONG_OVERFLOW_LIMIT &&
digitValue > C.LONG_MAX_LAST_DIGIT)
) {
if (isNegative && current == C.LONG_OVERFLOW_LIMIT &&
digitValue == C.LONG_MIN_LAST_DIGIT
) {
return Long.MIN_VALUE
}
throwError(C.ERR_LONG_OVERFLOW)
}
return current * C.BASE_TEN + digitValue
}
/**
* Asserts that leading zero doesn't precede another digit.
*/
private fun GhostJsonReader.validateLeadingZero() {
if (
position < limit &&
getByte(position) == C.ZERO_INT &&
position + 1 < limit
) {
val nextDigitByte = getByte(position + 1)
if (nextDigitByte in C.ZERO_INT..C.NINE_INT) {
throwError(C.ERR_LEADING_ZEROS)
}
}
}
/**
* Verifies that parsed Float values are finite numbers.
*/
private fun GhostJsonReader.validateNumericRangeFloat(valueToValidate: Float) {
if (valueToValidate.isInfinite() || valueToValidate.isNaN()) {
throwError(C.ERR_NUMERIC_OVERFLOW)
}
}
/**
* Verifies that parsed Double values are finite numbers.
*/
private fun GhostJsonReader.validateNumericRange(valueToValidate: Double) {
if (valueToValidate.isInfinite() || valueToValidate.isNaN()) {
throwError(C.ERR_NUMERIC_OVERFLOW)
}
}
/**
* Skips a JSON numeric token value from the source.
*/
@InternalGhostApi
fun GhostJsonReader.skipNumber() {
val header = prepareNumericHeader()
val isQuoted = (header and C.NUMERIC_HEADER_QUOTED) != 0
var hasDigits = false
// Integer part
val pos = position
val lim = limit
if (pos < lim && getByte(pos) == C.ZERO_INT) {
val newPos = pos + 1
position = newPos
hasDigits = true
if (
newPos < lim &&
isDigit(getByte(newPos))
) {
throwError(C.ERR_LEADING_ZEROS)
}
} else {
readNumericLoop { hasDigits = true }
}
if (!hasDigits) {
throwError(C.ERR_EXPECTED_INT_PART)
}
// Decimal part
val decPos = position
val decLim = limit
if (
decPos < decLim &&
getByte(decPos) == C.DOT_INT
) {
position = decPos + 1
var hasDecimalDigits = false
readNumericLoop { hasDecimalDigits = true }
if (!hasDecimalDigits) {
throwError(C.ERR_EXPECTED_DECIMAL_DIGITS)
}
}
// Exponent part
val expPos = position
val expLim = limit
if (expPos < expLim) {
val byte = getByte(expPos)
if (byte == C.EXP_LOWER_INT || byte == C.EXP_UPPER_INT) {
var newPos = expPos + 1
position = newPos
if (newPos < expLim) {
val sign = getByte(newPos)
if (sign == C.PLUS_INT || sign == C.MINUS_INT) {
newPos++
position = newPos
}
}
var hasExpDigits = false
readNumericLoop { hasExpDigits = true }
if (!hasExpDigits) {
throwError(C.ERR_EXPECTED_EXPONENT_DIGITS)
}
}
}
if (isQuoted) {
consumeNumericCoercionFooter()
}
nextTokenByte = C.RESET_TOKEN_BYTE
}
/**
* Loops and consumes digits from the stream.
*/
private inline fun GhostJsonReader.readNumericLoop(
crossinline onDigit: (byte: Int) -> Unit
) {
readNumericLoop(onDigit) {}
}
/**
* Loops and consumes digits from the stream, with optional break hook.
*/
private inline fun GhostJsonReader.readNumericLoop(
crossinline onDigit: (byte: Int) -> Unit,
crossinline onBreak: (byte: Int) -> Unit
) {
if (isStreaming) {
while (position < limit) {
val byte = source[position]
if (isDigit(byte)) {
onDigit(byte)
position++
} else {
onBreak(byte)
break
}
}
} else {
val data = rawData
while (position < limit) {
val byte = data[position].toInt() and C.BYTE_MASK
if (isDigit(byte)) {
onDigit(byte)
position++
} else {
onBreak(byte)
break
}
}
}
}