URL Encode / Decode — Zig source
Percent-encode or decode URLs and query parameters. Choose component (encodeURIComponent) or full-URI (encodeURI) mode. 100% client-side.
This is the Zig 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: Zig (0.12+). 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 —
// utf8ValidateSlice rejects overlong/surrogate sequences too) and full-URI scope leaves
// encoded reserved bytes intact.
const std = @import("std");
const HEX = "0123456789ABCDEF";
fn componentSafe(c: u8) bool {
return (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or
std.mem.indexOfScalar(u8, "-_.!~*'()", c) != null;
}
fn uriExtra(c: u8) bool {
return std.mem.indexOfScalar(u8, ";,/?:@&=+$#", c) != null;
}
fn encode(alloc: std.mem.Allocator, s: []const u8, full_uri: bool) ![]u8 {
var b = std.ArrayList(u8).init(alloc);
errdefer b.deinit();
for (s) |c| {
if (componentSafe(c) or (full_uri and uriExtra(c))) try b.append(c)
else try b.appendSlice(&[_]u8{ '%', HEX[c >> 4], HEX[c & 0xf] });
}
return b.toOwnedSlice();
}
fn unhex(c: u8) ?u8 {
return switch (c) {
'0'...'9' => c - '0',
'a'...'f' => c - 'a' + 10,
'A'...'F' => c - 'A' + 10,
else => null,
};
}
fn decode(alloc: std.mem.Allocator, s: []const u8, full_uri: bool) error{Malformed}![]u8 {
var b = std.ArrayList(u8).init(alloc);
errdefer b.deinit();
var i: usize = 0;
while (i < s.len) {
if (s[i] != '%') {
try b.append(s[i]);
i += 1;
continue;
}
if (i + 2 >= s.len) return error.Malformed; // truncated tail
const h = unhex(s[i + 1]) orelse return error.Malformed; // bad hex digit
const l = unhex(s[i + 2]) orelse return error.Malformed;
const c: u8 = h << 4 | l;
if (full_uri and c < 0x80 and uriExtra(c)) // decodeURI keeps encoded reserved
try b.appendSlice(s[i .. i + 3])
else
try b.append(c);
i += 3;
}
// strict UTF-8 — rejects overlong, surrogate and >U+10FFFF sequences
if (!std.unicode.utf8ValidateSlice(b.items)) return error.Malformed;
return b.toOwnedSlice();
}
pub fn main() !void {
const alloc = std.heap.page_allocator;
const s = "hello world & café";
std.debug.print("enc: {s}\n", .{try encode(alloc, s, false)});
std.debug.print("uri: {s}\n", .{try encode(alloc, s, true)});
const demos = [_]struct { input: []const u8, full_uri: bool, label: []const u8 }{
.{ .input = "hello%20world%20%26%20caf%C3%A9", .full_uri = false, .label = "dec: " },
.{ .input = "a%2Fb%3Fc%2Cd", .full_uri = true, .label = "duri: " }, // reserved stay escaped
.{ .input = "a%2Fb%3Fc%2Cd", .full_uri = false, .label = "dcmp: " }, // ...but component decodes
.{ .input = "100%", .full_uri = false, .label = "bad: " },
};
for (demos) |d| {
if (decode(alloc, d.input, d.full_uri)) |out| {
std.debug.print("{s}{s}\n", .{ d.label, out });
} else |err| switch (err) {
error.Malformed => std.debug.print("{s}malformed URI sequence\n", .{d.label}),
}
}
}
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 →