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

Code before:

    try {
        while (!input.exhausted()) {
            val chunkSize = parseChunkSize(input)
            if (chunkSize == 0L) {
                input.skipCrLf()
                break
            }

            input.copyTo(out, chunkSize)
            input.skipCrLf()
            out.flush()
            totalBytesCopied += chunkSize
        }
    } catch (t: Throwable) {
        out.close(t)
        throw t
    } finally {
        out.flushAndClose()
    }
}

private suspend fun ByteReadChannel.skipCrLf() {
    when (val b = readByte()) {
        CR -> if (readByte() != LF) throw IOException("Expected LF")
        LF -> return
        else -> throw IOException("Expected CRLF but found 0x${b.toString(16)}")
    }
}

private const val CR = '\r'.code.toByte()
private const val LF = '\n'.code.toByte()
private const val SEMICOLON = ';'.code.toByte()
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")

Review comment (bug):
The skipCrLf function accepts either CRLF or bare LF. According to RFC 7230 section 3.3.3, chunk-data must be followed by CRLF, not just LF. Accepting bare LF is lenient and could potentially lead to parsing ambiguities in edge cases. Consider making this stricter to only accept CRLF for better RFC compliance and security.
```suggestion
        CR -> if (readByte() != LF) throw IOException("Expected LF after CR")
```

Code after:

    try {
        while (!input.exhausted()) {
            val chunkSize = parseChunkSize(input)
            if (chunkSize == 0L) {
                input.skipCrLf()
                break
            }

            input.copyTo(out, chunkSize)
            input.skipCrLf()
            out.flush()
            totalBytesCopied += chunkSize
        }
    } catch (t: Throwable) {
        out.close(t)
        throw t
    } finally {
        out.flushAndClose()
    }
}

private suspend fun ByteReadChannel.skipCrLf() {
    if (readByte() != CR) throw IOException("Expected CR")
    if (readByte() != LF) throw IOException("Expected LF")
}

private const val CR = '\r'.code.toByte()
private const val LF = '\n'.code.toByte()
private const val SEMICOLON = ';'.code.toByte()
private const val QUOTE = '"'.code.toByte()

/**
 * 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.