<commit_before>package org.jetbrains.ktor.http

import org.jetbrains.ktor.application.*
import java.net.*
import java.time.temporal.*
import java.util.*

public data class Cookie(
        val name: String,
        val value: String,
        val encoding: CookieEncoding = CookieEncoding.URI_ENCODING,
        val maxAge: Int = 0,
        val expires: Temporal? = null,
        val domain: String = "",
        val path: String = "",
        val secure: Boolean = false,
        val httpOnly: Boolean = false,
        val extensions: Map<String, String?> = emptyMap()
)

public enum class CookieEncoding {
    RAW, DQUOTES, URI_ENCODING, BASE64_ENCODING
}

private val loweredPartNames = setOf("max-age", "expires", "domain", "path", "secure", "httponly", "\$x-enc")
public fun parseServerSetCookieHeader(cookiesHeader: String): Cookie {
    val asMap = parseClientCookiesHeader(cookiesHeader, false)
    val first = asMap.entrySet().first { !it.key.startsWith("$") }
    val encoding = asMap["\$x-enc"]?.let { CookieEncoding.valueOf(it) } ?: CookieEncoding.URI_ENCODING
    val loweredMap = asMap.mapKeys { it.key.toLowerCase() }

    return Cookie(
            name = first.key,
            value = decodeCookieValue(first.value, encoding),
            encoding = encoding,
            maxAge = loweredMap["max-age"]?.toInt() ?: 0,
            expires = loweredMap["expires"]?.let { it.fromHttpDateString() },
            domain = loweredMap["domain"] ?: "",
            path = loweredMap["path"] ?: "",
            secure = "secure" in loweredMap,
            httpOnly = "httponly" in loweredMap,
            extensions = asMap.filterKeys {
                it.toLowerCase() !in loweredPartNames && it != first.key
            }
    )
}

public fun parseClientCookiesHeader(cookiesHeader: String, skipEscaped: Boolean = true): Map<String, String> {
    val pattern = """(^|;|,)\s*([^()<>@,;:/\\"\[\]\?=\{\}\s]+)\s*(=\s*("[^"]*"|[^;,]*))?""".toRegex()

    return pattern.matchAll(cookiesHeader)
        .map { (it.groups[2]?.value ?: "") to (it.groups[4]?.value ?: "") }
        .filter { !skipEscaped || !it.first.startsWith("$") }
        .map { when {
            it.second.startsWith("\"") && it.second.endsWith("\"") -> it.copy(second = it.second.removeSurrounding("\""))
            else -> it
        } }
        .toMap()
}

public fun renderSetCookieHeader(cookie: Cookie): String = with(cookie) {
    renderSetCookieHeader(
            name,
            value,
            encoding,
            maxAge,
            expires,
            domain,
            path,
            secure,
            httpOnly,
            extensions
    )
}

public fun renderSetCookieHeader(name: String,
                                 value: String,
                                 encoding: CookieEncoding = CookieEncoding.URI_ENCODING,
                                 maxAge: Int = 0,
                                 expires: Temporal? = null,
                                 domain: String = "",
                                 path: String = "",
                                 secure: Boolean = false,
                                 httpOnly: Boolean = false,
                                 extensions: Map<String, String?> = emptyMap()): String =
        (listOf(
                cookiePart(name.assertCookieName(), value, encoding),
                cookiePartUnencoded("Max-Age", if (maxAge > 0) maxAge else null),
                cookiePartUnencoded("Expires", expires?.toHttpDateString()),
                cookiePart("Domain", domain.nullIfEmpty(), CookieEncoding.RAW),
                cookiePart("Path", path.nullIfEmpty(), CookieEncoding.RAW),

                cookiePartFlag("Secure", secure),
                cookiePartFlag("HttpOnly", httpOnly)
        ) + extensions.map {
            cookiePartExt(it.key.assertCookieName(), it.value, encoding)
        } + cookiePartExt("\$x-enc", encoding.name(), CookieEncoding.RAW)
                ).filter { it.isNotEmpty() }
                .joinToString("; ")

public fun encodeCookieValue(value: String, encoding: CookieEncoding): String =
        when (encoding) {
            CookieEncoding.RAW -> when {
                value.any { it.shouldEscapeInCookies() } -> throw IllegalArgumentException("The cookie value contains characters that couldn't be encoded in RAW format. Consider URL_ENCODING mode")
                else -> value
            }
            CookieEncoding.DQUOTES -> when {
                value.contains('"') -> throw IllegalArgumentException("The cookie value contains characters that couldn't be encoded in RAW format. Consider URL_ENCODING mode")
                value.any { it.shouldEscapeInCookies() } -> "\"$value\""
                else -> value
            }
            CookieEncoding.BASE64_ENCODING -> Base64.getEncoder().encodeToString(value.toByteArray(Charsets.UTF_8))
            CookieEncoding.URI_ENCODING -> URLEncoder.encode(value, "UTF-8")
        }

public fun decodeCookieValue(encodedValue: String, encoding: CookieEncoding): String =
        when (encoding) {
            CookieEncoding.RAW, CookieEncoding.DQUOTES -> when {
                encodedValue.trimStart().startsWith("\"") && encodedValue.trimEnd().endsWith("\"") -> encodedValue.trim().removeSurrounding("\"")
                else -> encodedValue
            }
            CookieEncoding.URI_ENCODING -> URLDecoder.decode(encodedValue, "UTF-8")
            CookieEncoding.BASE64_ENCODING -> Base64.getDecoder().decode(encodedValue).toString(Charsets.UTF_8)
        }

private fun String.assertCookieName() = when {
    any { it.shouldEscapeInCookies() } -> throw IllegalArgumentException("Cookie name is not valid: $this")
    else -> this
}

private val cookieCharsShouldBeEscaped = setOf(';', ',', '=', '"')
private fun Char.shouldEscapeInCookies() = this.isWhitespace() || this < ' ' || this in cookieCharsShouldBeEscaped

@Suppress("NOTHING_TO_INLINE")
private inline fun cookiePart(name: String, value: Any?, encoding: CookieEncoding) =
        if (value != null) "$name=${encodeCookieValue(value.toString(), encoding)}" else ""

@Suppress("NOTHING_TO_INLINE")
private inline fun cookiePartUnencoded(name: String, value: Any?) =
        if (value != null) "$name=${value.toString()}" else ""


@Suppress("NOTHING_TO_INLINE")
private inline fun cookiePartFlag(name: String, value: Boolean) =
        if (value) name else ""

@Suppress("NOTHING_TO_INLINE")
private inline fun cookiePartExt(name: String, value: String?, encoding: CookieEncoding) =
        if (value == null) cookiePartFlag(name, true) else cookiePart(name, value, encoding)

@Suppress("NOTHING_TO_INLINE")
private inline fun String.nullIfEmpty() = if (this.isEmpty()) null else this

public data class CookiesKey(val encoding: CookieEncoding) : AttributeKey<Map<String, String>>()
<commit_msg>Remove unneeded CookieKey, since we store data in RequestCookies itself.<commit_after>package org.jetbrains.ktor.http

import java.net.*
import java.time.temporal.*
import java.util.*

public data class Cookie(
        val name: String,
        val value: String,
        val encoding: CookieEncoding = CookieEncoding.URI_ENCODING,
        val maxAge: Int = 0,
        val expires: Temporal? = null,
        val domain: String = "",
        val path: String = "",
        val secure: Boolean = false,
        val httpOnly: Boolean = false,
        val extensions: Map<String, String?> = emptyMap()
)

public enum class CookieEncoding {
    RAW, DQUOTES, URI_ENCODING, BASE64_ENCODING
}

private val loweredPartNames = setOf("max-age", "expires", "domain", "path", "secure", "httponly", "\$x-enc")
public fun parseServerSetCookieHeader(cookiesHeader: String): Cookie {
    val asMap = parseClientCookiesHeader(cookiesHeader, false)
    val first = asMap.entrySet().first { !it.key.startsWith("$") }
    val encoding = asMap["\$x-enc"]?.let { CookieEncoding.valueOf(it) } ?: CookieEncoding.URI_ENCODING
    val loweredMap = asMap.mapKeys { it.key.toLowerCase() }

    return Cookie(
            name = first.key,
            value = decodeCookieValue(first.value, encoding),
            encoding = encoding,
            maxAge = loweredMap["max-age"]?.toInt() ?: 0,
            expires = loweredMap["expires"]?.let { it.fromHttpDateString() },
            domain = loweredMap["domain"] ?: "",
            path = loweredMap["path"] ?: "",
            secure = "secure" in loweredMap,
            httpOnly = "httponly" in loweredMap,
            extensions = asMap.filterKeys {
                it.toLowerCase() !in loweredPartNames && it != first.key
            }
    )
}

public fun parseClientCookiesHeader(cookiesHeader: String, skipEscaped: Boolean = true): Map<String, String> {
    val pattern = """(^|;|,)\s*([^()<>@,;:/\\"\[\]\?=\{\}\s]+)\s*(=\s*("[^"]*"|[^;,]*))?""".toRegex()

    return pattern.matchAll(cookiesHeader)
        .map { (it.groups[2]?.value ?: "") to (it.groups[4]?.value ?: "") }
        .filter { !skipEscaped || !it.first.startsWith("$") }
        .map { when {
            it.second.startsWith("\"") && it.second.endsWith("\"") -> it.copy(second = it.second.removeSurrounding("\""))
            else -> it
        } }
        .toMap()
}

public fun renderSetCookieHeader(cookie: Cookie): String = with(cookie) {
    renderSetCookieHeader(
            name,
            value,
            encoding,
            maxAge,
            expires,
            domain,
            path,
            secure,
            httpOnly,
            extensions
    )
}

public fun renderSetCookieHeader(name: String,
                                 value: String,
                                 encoding: CookieEncoding = CookieEncoding.URI_ENCODING,
                                 maxAge: Int = 0,
                                 expires: Temporal? = null,
                                 domain: String = "",
                                 path: String = "",
                                 secure: Boolean = false,
                                 httpOnly: Boolean = false,
                                 extensions: Map<String, String?> = emptyMap()): String =
        (listOf(
                cookiePart(name.assertCookieName(), value, encoding),
                cookiePartUnencoded("Max-Age", if (maxAge > 0) maxAge else null),
                cookiePartUnencoded("Expires", expires?.toHttpDateString()),
                cookiePart("Domain", domain.nullIfEmpty(), CookieEncoding.RAW),
                cookiePart("Path", path.nullIfEmpty(), CookieEncoding.RAW),

                cookiePartFlag("Secure", secure),
                cookiePartFlag("HttpOnly", httpOnly)
        ) + extensions.map {
            cookiePartExt(it.key.assertCookieName(), it.value, encoding)
        } + cookiePartExt("\$x-enc", encoding.name(), CookieEncoding.RAW)
                ).filter { it.isNotEmpty() }
                .joinToString("; ")

public fun encodeCookieValue(value: String, encoding: CookieEncoding): String =
        when (encoding) {
            CookieEncoding.RAW -> when {
                value.any { it.shouldEscapeInCookies() } -> throw IllegalArgumentException("The cookie value contains characters that couldn't be encoded in RAW format. Consider URL_ENCODING mode")
                else -> value
            }
            CookieEncoding.DQUOTES -> when {
                value.contains('"') -> throw IllegalArgumentException("The cookie value contains characters that couldn't be encoded in RAW format. Consider URL_ENCODING mode")
                value.any { it.shouldEscapeInCookies() } -> "\"$value\""
                else -> value
            }
            CookieEncoding.BASE64_ENCODING -> Base64.getEncoder().encodeToString(value.toByteArray(Charsets.UTF_8))
            CookieEncoding.URI_ENCODING -> URLEncoder.encode(value, "UTF-8")
        }

public fun decodeCookieValue(encodedValue: String, encoding: CookieEncoding): String =
        when (encoding) {
            CookieEncoding.RAW, CookieEncoding.DQUOTES -> when {
                encodedValue.trimStart().startsWith("\"") && encodedValue.trimEnd().endsWith("\"") -> encodedValue.trim().removeSurrounding("\"")
                else -> encodedValue
            }
            CookieEncoding.URI_ENCODING -> URLDecoder.decode(encodedValue, "UTF-8")
            CookieEncoding.BASE64_ENCODING -> Base64.getDecoder().decode(encodedValue).toString(Charsets.UTF_8)
        }

private fun String.assertCookieName() = when {
    any { it.shouldEscapeInCookies() } -> throw IllegalArgumentException("Cookie name is not valid: $this")
    else -> this
}

private val cookieCharsShouldBeEscaped = setOf(';', ',', '=', '"')
private fun Char.shouldEscapeInCookies() = this.isWhitespace() || this < ' ' || this in cookieCharsShouldBeEscaped

@Suppress("NOTHING_TO_INLINE")
private inline fun cookiePart(name: String, value: Any?, encoding: CookieEncoding) =
        if (value != null) "$name=${encodeCookieValue(value.toString(), encoding)}" else ""

@Suppress("NOTHING_TO_INLINE")
private inline fun cookiePartUnencoded(name: String, value: Any?) =
        if (value != null) "$name=${value.toString()}" else ""


@Suppress("NOTHING_TO_INLINE")
private inline fun cookiePartFlag(name: String, value: Boolean) =
        if (value) name else ""

@Suppress("NOTHING_TO_INLINE")
private inline fun cookiePartExt(name: String, value: String?, encoding: CookieEncoding) =
        if (value == null) cookiePartFlag(name, true) else cookiePart(name, value, encoding)

@Suppress("NOTHING_TO_INLINE")
private inline fun String.nullIfEmpty() = if (this.isEmpty()) null else this
{'fraction_non_alphanumeric': 0.13275379401797555, 'fraction_numerical': 0.002946810078090467, 'mean_word_length': 2.4159536990437847, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 51, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 18, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5706312', 'n_tokens_mistral': 3884, 'n_tokens_neox': 3652, 'n_words': 843}