URL Encode / Decode — Kotlin source
Percent-encode or decode URLs and query parameters. Choose component (encodeURIComponent) or full-URI (encodeURI) mode. 100% client-side.
This is the Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// url-encode — percent encode/decode, component (encodeURIComponent) and full-URI (encodeURI) scope.
// Language: Kotlin (2.0, JVM, standard library only). Port of src/tools/UrlEncodeTool.tsx — same
// contract as this dir's go.go: each byte outside the safe set becomes %XX (uppercase hex);
// component keeps A-Za-z0-9-_.!~*'(), full URI also keeps ;,/?:@&=+$#; decode is strict (bad %
// pair, invalid UTF-8 -> error) and full-URI scope leaves encoded reserved bytes intact.
import java.nio.ByteBuffer
import java.nio.charset.CharacterCodingException
import java.nio.charset.CodingErrorAction
import java.nio.charset.StandardCharsets
object UrlEncode {
data class Result(val output: String, val error: String?) // error == null on success
private const val HEX = "0123456789ABCDEF"
private const val BAD = "malformed URI sequence"
private fun compSafe(c: Int) =
c in 'A'.code..'Z'.code || c in 'a'.code..'z'.code || c in '0'.code..'9'.code ||
"-_.!~*'()".indexOf(c.toChar()) >= 0
private fun uriExtra(c: Int) = ";,/?:@&=+$#".indexOf(c.toChar()) >= 0
fun encode(s: String, fullUri: Boolean = false): String = buildString {
for (cb in s.toByteArray(StandardCharsets.UTF_8)) {
val c = cb.toInt() and 0xff
if (compSafe(c) || (fullUri && uriExtra(c))) append(c.toChar())
else append('%').append(HEX[c shr 4]).append(HEX[c and 0xf])
}
}
private fun unhex(c: Char): Int = when (c) {
in '0'..'9' -> c - '0'
in 'a'..'f' -> c - 'a' + 10
in 'A'..'F' -> c - 'A' + 10
else -> -1
}
fun decode(s: String, fullUri: Boolean = false): Result {
val bytes = ByteArray(s.length) // worst case: one byte per char
var n = 0
var i = 0
while (i < s.length) {
if (s[i] != '%') { bytes[n++] = s[i].code.toByte(); i++; continue }
if (i + 2 >= s.length) return Result("", BAD) // truncated tail
val hi = unhex(s[i + 1])
val lo = unhex(s[i + 2])
if (hi < 0 || lo < 0) return Result("", BAD) // bad hex digit
val c = hi shl 4 or lo
if (fullUri && c < 0x80 && uriExtra(c)) { // decodeURI keeps encoded reserved
bytes[n++] = '%'.code.toByte()
bytes[n++] = s[i + 1].code.toByte()
bytes[n++] = s[i + 2].code.toByte()
} else {
bytes[n++] = c.toByte()
}
i += 3
}
return try {
// REPORT -> strict decoder: overlong/surrogate/truncated UTF-8 throws
val out = StandardCharsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(bytes, 0, n)).toString()
Result(out, null)
} catch (e: CharacterCodingException) {
Result("", BAD)
}
}
}
fun main() {
fun show(label: String, r: UrlEncode.Result) = println(label + (r.error ?: r.output))
println("enc: " + UrlEncode.encode("hello world & café"))
println("uri: " + UrlEncode.encode("hello world & café", fullUri = true))
show("dec: ", UrlEncode.decode("hello%20world%20%26%20caf%C3%A9"))
show("duri: ", UrlEncode.decode("a%2Fb%3Fc%2Cd", fullUri = true)) // reserved stay escaped in decodeURI
show("dcmp: ", UrlEncode.decode("a%2Fb%3Fc%2Cd")) // ...but decode in component scope
show("bad: ", UrlEncode.decode("100%"))
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →