Skip to content

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 →