URL Encode / Decode — Swift source
Percent-encode or decode URLs and query parameters. Choose component (encodeURIComponent) or full-URI (encodeURI) mode. 100% client-side.
This is the Swift 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: Swift (5.9+, 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.
enum UrlEncodeError: Error { case malformed }
enum UrlEncode {
static let hex = Array("0123456789ABCDEF".utf8)
static func componentSafe(_ c: UInt8) -> Bool {
(65...90).contains(c) || (97...122).contains(c) || (48...57).contains(c)
|| "-_.!~*'()".utf8.contains(c)
}
static func uriExtra(_ c: UInt8) -> Bool { ";,/?:@&=+$#".utf8.contains(c) }
static func encode(_ s: String, fullURI: Bool = false) -> String {
var b = [UInt8]()
b.reserveCapacity(s.utf8.count * 3)
for c in s.utf8 {
if componentSafe(c) || (fullURI && uriExtra(c)) {
b.append(c)
} else {
b.append(0x25) // '%'
b.append(hex[Int(c >> 4)])
b.append(hex[Int(c & 0xf)])
}
}
return String(decoding: b, as: UTF8.self) // encode never produces invalid UTF-8
}
static func unhex(_ c: UInt8) -> UInt8? {
switch c {
case 48...57: return c - 48
case 97...102: return c - 97 + 10
case 65...70: return c - 65 + 10
default: return nil
}
}
static func decode(_ s: String, fullURI: Bool = false) -> Result<String, UrlEncodeError> {
var bytes = [UInt8]()
bytes.reserveCapacity(s.utf8.count)
let src = Array(s.utf8)
var i = 0
while i < src.count {
if src[i] != 0x25 { bytes.append(src[i]); i += 1; continue }
guard i + 2 < src.count, let hi = unhex(src[i + 1]), let lo = unhex(src[i + 2]) else {
return .failure(.malformed) // truncated tail or bad hex digit
}
let c = hi << 4 | lo
if fullURI && c < 0x80 && uriExtra(c) { // decodeURI keeps encoded reserved
bytes.append(contentsOf: src[i...i + 2])
} else {
bytes.append(c)
}
i += 3
}
// the validating initializer is the strict decoder — nil on overlong/surrogate/truncated
guard let out = String(validating: bytes, as: UTF8.self) else { return .failure(.malformed) }
return .success(out)
}
}
func show(_ label: String, _ r: Result<String, UrlEncodeError>) {
switch r {
case .success(let out): print("\(label)\(out)")
case .failure: print("\(label)malformed URI sequence")
}
}
print("enc: \(UrlEncode.encode("hello world & café"))")
print("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 →