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

Code before:
                    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)
public fun encodeChunked(
    output: ByteWriteChannel,
    coroutineContext: CoroutineContext
): EncoderJob = GlobalScope.reader(coroutineContext, autoFlush = false) {
    encodeChunked(output, channel)
}

/**
 * Chunked stream encoding loop
 *
 * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.http.cio.encodeChunked)
 */
public suspend fun encodeChunked(output: ByteWriteChannel, input: ByteReadChannel) {
    try {
        while (!input.isClosedForRead) {
            input.read { source, startIndex, endIndex ->
                if (endIndex == startIndex) return@read 0
                output.writeChunk(source, startIndex, endIndex)
            }
        }

        input.rethrowCloseCause()
        output.writeFully(LastChunkBytes)
    } catch (cause: Throwable) {
        output.close(cause)
        input.cancel(cause)

Review comment (bug):
The parseChunkSize function doesn't validate that at least one hex digit was parsed before encountering the CRLF terminator. If the input is ";extension\r\n" (no chunk size), the function will return 0, which would be treated as the end of the chunked stream. This could lead to incorrect parsing or premature stream termination. The function should validate that result > 0 or that at least one digit was read before accepting a CRLF as valid.

Code after:
 * - 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")
                    return result
                }
                QUOTE -> inQuotes = !inQuotes
                SEMICOLON -> {
                    if (i == 1) throw IOException("Empty chunk size")
                    inExtension = true
                }
                else -> {
                    if (inExtension) continue // always ignore extensions
                    val intValue = byte.toInt() and 0xff
                    val digit = if (intValue < 0xff) HexTable[intValue] else -1L
                    if (digit == -1L) throw IOException("Invalid chunk size character: 0x${intValue.toString(16)}")
                    if ((result and -0x1000000000000000L) != 0L) throw IOException("Chunk size overflow")
                    result = (result shl 4) or digit
                }
            }
        } finally {
            afterCr = byte == CR
        }
    }
    throw IOException("Chunk size limit exceeded")
}

/**
 * Encoder job type
 *