All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.io.ktor.http.Codecs.kt Maven / Gradle / Ivy

package io.ktor.http

import io.ktor.util.*
import kotlinx.io.charsets.*
import kotlinx.io.core.*

private val URL_ALPHABET = (('a'..'z') + ('A'..'Z') + ('0'..'9')).map { it.toByte() }
private val HEX_ALPHABET = ('a'..'f') + ('A'..'F') + ('0'..'9')

/**
 * https://tools.ietf.org/html/rfc3986#section-2
 */
private val URL_PROTOCOL_PART = listOf(
    ':', '/', '?', '#', '[', ']', '@',  // general
    '!', '$', '&', '\'', '(', ')', '*', ',', ';', '=',  // sub-components
    '-', '.', '_', '~', '+' // unreserved
).map { it.toByte() }

/**
 * from `pchar` in https://tools.ietf.org/html/rfc3986#section-2
 */
private val VALID_PATH_PART = listOf(
    ':', '@',
    '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=',
    '-', '.', '_', '~'
)
/**
 * Oauth specific percent encoding
 * https://tools.ietf.org/html/rfc5849#section-3.6
 */
private val OAUTH_SYMBOLS = listOf('-', '.', '_', '~').map { it.toByte() }

/**
 * Encode url part as specified in
 * https://tools.ietf.org/html/rfc3986#section-2
 */
fun String.encodeURLQueryComponent(
    encodeFull: Boolean = false,
    spaceToPlus: Boolean = false,
    charset: Charset = Charsets.UTF_8
): String = buildString {
    val content = charset.newEncoder().encode(this@encodeURLQueryComponent)
    content.forEach {
        when {
            it == ' '.toByte() -> if (spaceToPlus) append('+') else append("%20")
            it in URL_ALPHABET || (!encodeFull && it in URL_PROTOCOL_PART) -> append(it.toChar())
            else -> append(it.percentEncode())
        }
    }
}

/**
 * Encode URL path or component. It escapes all illegal or ambiguous characters
 */
fun String.encodeURLPath(): String = buildString {
    var index = 0
    while (index < [email protected]) {
        val current = this@encodeURLPath[index]
        if (current == '/' || current.toByte() in URL_ALPHABET || current in VALID_PATH_PART) {
            append(current)
            index++
            continue
        }

        if (current == '%' &&
            index + 2 < [email protected] &&
            this@encodeURLPath[index + 1] in HEX_ALPHABET &&
            this@encodeURLPath[index + 2] in HEX_ALPHABET
        ) {
            append(current)
            append(this@encodeURLPath[index + 1])
            append(this@encodeURLPath[index + 2])

            index += 3
            continue
        }

        append(current.toByte().percentEncode())
        index++
    }
}

/**
 * Encode [this] in percent encoding specified here:
 * https://tools.ietf.org/html/rfc5849#section-3.6
 */
fun String.encodeOAuth(): String = encodeURLParameter()

/**
 * Encode [this] as query parameter
 */
fun String.encodeURLParameter(
    spaceToPlus: Boolean = false
): String = buildString {
    val content = Charsets.UTF_8.newEncoder().encode(this@encodeURLParameter)
    content.forEach {
        when {
            it in URL_ALPHABET || it in OAUTH_SYMBOLS -> append(it.toChar())
            spaceToPlus && it == ' '.toByte() -> append('+')
            else -> append(it.percentEncode())
        }
    }
}

/**
 * Decode URL query component
 */
fun String.decodeURLQueryComponent(
    start: Int = 0, end: Int = length,
    plusIsSpace: Boolean = false,
    charset: Charset = Charsets.UTF_8
): String = decodeScan(start, end, plusIsSpace, charset)

/**
 * Decode percent encoded URL part within the specified range [[start], [end]).
 * This function is not intended to decode urlencoded forms so it doesn't decode plus character to space.
 */
@KtorExperimentalAPI
fun String.decodeURLPart(
    start: Int = 0, end: Int = length,
    charset: Charset = Charsets.UTF_8
): String = decodeScan(start, end, false, charset)

private fun String.decodeScan(start: Int, end: Int, plusIsSpace: Boolean, charset: Charset): String {
    for (index in start until end) {
        val ch = this[index]
        if (ch == '%' || (plusIsSpace && ch == '+')) {
            return decodeImpl(start, end, index, plusIsSpace, charset)
        }
    }
    return if (start == 0 && end == length) toString() else substring(start, end)
}

private fun CharSequence.decodeImpl(
    start: Int,
    end: Int,
    prefixEnd: Int,
    plusIsSpace: Boolean,
    charset: Charset
): String {
    val length = end - start
    // if length is big, it probably means it is encoded
    val sbSize = if (length > 255) length / 3 else length
    val sb = StringBuilder(sbSize)

    if (prefixEnd > start) {
        sb.append(this, start, prefixEnd)
    }

    var index = prefixEnd

    // reuse ByteArray for hex decoding stripes
    var bytes: ByteArray? = null

    while (index < end) {
        val c = this[index]
        when {
            plusIsSpace && c == '+' -> {
                sb.append(' ')
                index++
            }
            c == '%' -> {
                // if ByteArray was not needed before, create it with an estimate of remaining string be all hex
                if (bytes == null)
                    bytes = ByteArray((end - index) / 3)

                // fill ByteArray with all the bytes, so Charset can decode text
                var count = 0
                while (index < end && this[index] == '%') {
                    if (index + 2 >= end) {
                        throw URLDecodeException(
                            "Incomplete trailing HEX escape: ${substring(index)}, in $this at $index"
                        )
                    }

                    val digit1 = charToHexDigit(this[index + 1])
                    val digit2 = charToHexDigit(this[index + 2])
                    if (digit1 == -1 || digit2 == -1) {
                        throw URLDecodeException(
                            "Wrong HEX escape: %${this[index + 1]}${this[index + 2]}, in $this, at $index"
                        )
                    }

                    bytes[count++] = (digit1 * 16 + digit2).toByte()
                    index += 3
                }

                // Decode chars from bytes and put into StringBuilder
                // Note: Tried using ByteBuffer and using enc.decode() – it's slower
                sb.append(String(bytes, offset = 0, length = count, charset = charset))
            }
            else -> {
                sb.append(c)
                index++
            }
        }
    }

    return sb.toString()
}

/**
 * URL decoder exception
 */
class URLDecodeException(message: String) : Exception(message)

private fun Byte.percentEncode(): String {
    val code = (toInt() and 0xff).toString(radix = 16).toUpperCase()
    return "%${code.padStart(length = 2, padChar = '0')}"
}

private fun charToHexDigit(c2: Char) = when (c2) {
    in '0'..'9' -> c2 - '0'
    in 'A'..'F' -> c2 - 'A' + 10
    in 'a'..'f' -> c2 - 'a' + 10
    else -> -1
}

private fun ByteReadPacket.forEach(block: (Byte) -> Unit) {
    takeWhile { buffer ->
        while (buffer.canRead()) {
            block(buffer.readByte())
        }
        true
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy