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

commonMain.aws.smithy.kotlin.runtime.util.text.Text.kt Maven / Gradle / Ivy

/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

package aws.smithy.kotlin.runtime.util.text

import aws.smithy.kotlin.runtime.util.InternalApi

/**
 * URL encode a string component according to
 * https://tools.ietf.org/html/rfc3986#section-2
 */
@InternalApi
fun String.urlEncodeComponent(
    formUrlEncode: Boolean = false
): String {
    val sb = StringBuilder(this.length)
    val data = this.encodeToByteArray()
    for (cbyte in data) {
        val chr = cbyte.toInt().toChar()
        when (chr) {
            ' ' -> if (formUrlEncode) sb.append("+") else sb.append("%20")
            // $2.3 Unreserved characters
            in 'a'..'z', in 'A'..'Z', in '0'..'9', '-', '_', '.', '~' -> sb.append(chr)
            else -> sb.append(cbyte.percentEncode())
        }
    }

    return sb.toString()
}

/**
 * The set of reserved delimiters from section-2.2 allowed as a path character that do not require
 * percent encoding.
 * See 'pchar': https://tools.ietf.org/html/rfc3986#section-3.3
 */
@InternalApi
val VALID_PCHAR_DELIMS = setOf(
    '/',
    ':', '@',
    // sub-delims from section-2.2
    '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=',
    // unreserved section-2.3
    '-', '.', '_', '~'
)

// test if a string encoded to a ByteArray is already percent encoded starting at index [i]
private fun isPercentEncodedAt(d: ByteArray, i: Int): Boolean =
    i + 2 < d.size &&
        d[i].toInt().toChar() == '%' &&
        i + 2 < d.size &&
        d[i + 1].toInt().toChar().uppercaseChar() in upperHexSet &&
        d[i + 2].toInt().toChar().uppercaseChar() in upperHexSet

/**
 * Encode a string that represents a *raw* URL path according to
 * https://tools.ietf.org/html/rfc3986#section-3.3
 */
@InternalApi
fun String.encodeUrlPath() = encodeUrlPath(VALID_PCHAR_DELIMS, checkPercentEncoded = true)

/**
 * Encode a string that represents a raw URL path component. Everything EXCEPT alphanumeric characters
 * and all delimiters in the [validDelimiters] set will be percent encoded.
 *
 * @param validDelimiters the set of allowed delimiters that need not be percent-encoded
 * @param checkPercentEncoded flag indicating if the encoding process should check for already percent-encoded
 * characters and pass them through as is or not.
 */
@InternalApi
fun String.encodeUrlPath(validDelimiters: Set, checkPercentEncoded: Boolean): String {
    val sb = StringBuilder(this.length)
    val data = this.encodeToByteArray()

    var i = 0
    while (i < data.size) {
        // 3.3 pchar: pct-encoded
        if (checkPercentEncoded && isPercentEncodedAt(data, i)) {
            sb.append(data[i++].toInt().toChar())
            sb.append(data[i++].toInt().toChar())
            sb.append(data[i++].toInt().toChar())
            continue
        }

        val cbyte = data[i]
        when (val chr = cbyte.toInt().toChar()) {
            // unreserved
            in 'a'..'z', in 'A'..'Z', in '0'..'9', in validDelimiters -> sb.append(chr)
            else -> sb.append(cbyte.percentEncode())
        }
        i++
    }

    return sb.toString()
}

private const val upperHex: String = "0123456789ABCDEF"
private val upperHexSet = upperHex.toSet()

// $2.1 Percent-Encoding
private fun Byte.percentEncode(): String = buildString(3) {
    val code = toInt() and 0xff
    append('%')
    append(upperHex[code shr 4])
    append(upperHex[code and 0x0f])
}

/**
 * Split a (decoded) query string "foo=baz&bar=quux" into it's component parts
 */
@InternalApi
public fun String.splitAsQueryString(): Map> {
    val entries = mutableMapOf>()
    split("&")
        .forEach { pair ->
            val parts = pair.split("=", limit = 2)
            val key = parts[0]
            val value = when (parts.size) {
                1 -> ""
                2 -> parts[1]
                else -> throw IllegalArgumentException("invalid query string: $parts")
            }
            if (entries.containsKey(key)) {
                entries[key]!!.add(value)
            } else {
                entries[key] = mutableListOf(value)
            }
        }
    return entries
}

private val percentEncodedParam = """%([A-Fa-f0-9]{2})""".toRegex()

/**
 * Decode a URL's query string, resolving percent-encoding (e.g., "%3B" → ";").
 */
@InternalApi
public fun String.urlDecodeComponent(): String = this
    .replace('+', ' ')
    .replace(percentEncodedParam) { it.groupValues[1].toInt(radix = 16).toChar().toString() }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy