Repository: ktorio/ktor
File: ktor-http/ktor-http-cio/common/src/io/ktor/http/cio/ChunkedTransferEncoding.kt (Kotlin)

Code before:
private const val QUOTE = '"'.code.toByte()

@OptIn(InternalAPI::class)
private suspend fun parseChunkSize(input: ByteReadChannel): Long {
    var result = 0L
    var inExtension = false
    var inQuotes = false
    var afterCr = false
    var i = 0
    while (i++ < MAX_CHUNK_SIZE_LENGTH) {
        val byte = input.readByte()
        if (inQuotes && byte != QUOTE) continue
        try {
            when (byte) {
                CR -> continue
                LF -> {
                    if (!afterCr) {
                        throw IOException("Illegal newline character in chunk size")
                    }
                    return result
                }
                QUOTE -> inQuotes = !inQuotes
                SEMICOLON -> inExtension = true
                else -> {
                    if (inExtension) continue // always ignore extensions
                    val intValue = byte.toInt() and 0xffff
                    val digit = if (intValue < 0xff) HexTable[intValue] else -1L
                    if (digit == -1L) throw IOException("Invalid chunk size character: 0x${intValue.toString(16)}")
                    result = (result shl 4) or digit
                }
            }
        } finally {
            afterCr = byte == CR
        }
    }
    throw IOException("Chunk size limit exceeded")
}

/**
 * Encoder job type
 *
 * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.http.cio.EncoderJob)
 */
public typealias EncoderJob = ReaderJob

/**
 * Start chunked stream encoding coroutine
 *
 * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.http.cio.encodeChunked)
 */
@OptIn(DelicateCoroutinesApi::class)

Review comment (suggestion):
The bitmask operation 'byte.toInt() and 0xffff' should be 'byte.toInt() and 0xff' for a single byte. The value 0xffff is a 16-bit mask, but byte.toInt() sign-extends to 32 bits with only the lowest 8 bits containing the byte value. Using 0xff would be more semantically correct, though functionally they produce the same result for the array access since HexTable only has 256 entries.
```suggestion
                    val intValue = byte.toInt() and 0xff
```

Code after:
 * Parse a single HTTP/1.1 chunk-size line as defined by the chunked transfer coding.
 *
 * The input is expected to be positioned at the first byte of the chunk-size line and this
 * function consumes bytes up to and including the terminating CRLF. The returned [Long] is the
 * numeric size of the chunk body in bytes.
 *
 * Parsing rules and state machine:
 * - The chunk size itself is parsed as a hexadecimal number from the beginning of the line up
 *   to (but not including) the first `;` (start of chunk extensions) or CR.
 * - After the first `;` is seen, [inExtension] is set to `true` and all subsequent characters
 *   (including any additional `;`) are treated as part of the extension section and ignored for
 *   size calculation.
 * - Chunk extensions may contain quoted strings (`"..."`). When a `"` is seen, [inQuotes] is
 *   toggled. While [inQuotes] is `true`, every byte other than `"` is skipped without further
 *   interpretation so that extension parameters cannot affect the parsed size.
 * - CR/LF handling is strict: `LF` must be immediately preceded by `CR`. A bare `LF` or `LF`
 *   occurring without a prior `CR` causes an [IOException], preventing acceptance of malformed
 *   or obfuscated input.
 * - The [afterCr] flag tracks whether the last processed byte was a `CR` so that the following
 *   `LF` can be recognized as a valid line terminator.
 *
 * Security and robustness considerations (see KTOR-9263):
 * - Only standard hexadecimal digits are accepted before the first `;`. Any other character in
 *   this region results in an [IOException].
 * - Chunk extensions and their contents are always ignored for the purpose of computing the size,
 *   but are fully consumed from the stream, including quoted strings, to keep the parser in sync.
 * - A hard upper bound of [MAX_CHUNK_SIZE_LENGTH] bytes is enforced for the entire line to
 *   mitigate resource exhaustion and to reject overly long or maliciously crafted chunk-size
 *   headers.
 *
 * This function is intentionally strict and should be modified with care, as relaxing these
 * checks may reintroduce parsing ambiguities or security vulnerabilities.
 */
@OptIn(InternalAPI::class)
private suspend fun parseChunkSize(input: ByteReadChannel): Long {
    val buffer = input.readBuffer
    var result = 0L
    var inExtension = false
    var inQuotes = false
    var afterCr = false
    var i = 0
    while (i++ < MAX_CHUNK_SIZE_LENGTH) {
        if (buffer.exhausted() && !input.awaitContent()) throw EOFException()
        val byte = buffer.readByte()
        if (inQuotes && byte != QUOTE) continue
        try {
            when (byte) {
                CR -> continue // set in finally block
                LF -> {
                    if (!afterCr) throw IOException("Illegal newline character in chunk size")
                    if (i < 3) throw IOException("Empty chunk size")