Skip to content

Basic Auth Generator — Zig source

Build HTTP Basic Access Authentication headers from a username and password, or decode an existing Authorization header back to its credentials. UTF-8 safe and fully client-side, with a shareable link.

This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.

//! basic-auth-generator — HTTP Basic-Auth header encode/decode.
//!
//! Language: Zig (0.13, standard library only)
//! Source:   CosmoDev polyglot showcase port of the Basic Auth Generator tool,
//!           ported from src/lib/basic-auth.ts (the canonical TypeScript
//!           implementation) and kept in lock-step with cli/basic-auth-generator.
//! License:  display source — part of CosmoDev's polyglot tool pages.
//!
//! Design goals:
//!   - Pure + deterministic; never panics (parseHeader returns null on
//!     malformed input — the only error path is allocator failure).
//!   - Functionally equivalent to the TS reference + Go twin: same inputs ->
//!     same outputs, same reject behavior on malformed headers.
//!   - Self-contained: std only — std.base64.standard is the RFC 4648 §4
//!     codec, the direct equivalent of Go's encoding/base64 StdEncoding.
//!
//! Zig is a dependency-rich polyglot here, like the Python port: the standard
//! library ships the base64 codec, so unlike the Rust/C/C++ ports no codec is
//! hand-rolled. One quirk is compensated for below — the standard decoder
//! rejects whitespace instead of skipping it, so interior whitespace is
//! stripped first, mirroring the TS b64decode(token.replace(/\s+/g, ''))
//! helper. All returned/returned-through slices are owned by the caller's
//! allocator.

const std = @import("std");

/// Credentials hold a parsed username/password pair. Mirrors the Go twin's
/// `Credentials` struct and the TS `BasicAuthCredentials` interface. The two
/// slices are separately owned by the allocator passed to parseHeader.
pub const Credentials = struct {
    user: []u8,
    pass: []u8,

    pub fn deinit(self: Credentials, alloc: std.mem.Allocator) void {
        alloc.free(self.user);
        alloc.free(self.pass);
    }
};

/// Build an HTTP Basic-Auth header value: "Basic " ++ base64(user ++ ":" ++ pass).
/// Mirrors `BuildHeader` in the Go twin and `buildBasicAuth` in the TS source.
/// The encoder operates on the UTF-8 byte representation, so multibyte
/// credentials (e.g. "café:päss") encode correctly. The returned slice is
/// owned by `alloc`.
pub fn buildHeader(alloc: std.mem.Allocator, user: []const u8, pass: []const u8) ![]u8 {
    const raw = try std.mem.concat(alloc, u8, &.{ user, ":", pass });
    defer alloc.free(raw);

    const enc = std.base64.standard.Encoder;
    const out = try alloc.alloc(u8, "Basic ".len + enc.calcSize(raw.len));
    @memcpy(out[0.."Basic ".len], "Basic ");
    _ = enc.encode(out["Basic ".len..], raw);
    return out;
}

/// Parse a "Basic <token>" header back into credentials, returning null if
/// the header is malformed or the credentials have no colon separator. Logic
/// mirrors `ParseHeader` in the Go twin exactly: trim the header, require a
/// case-insensitive "basic " prefix, trim the remaining token, base64-decode
/// it (rejecting invalid characters), then split on the FIRST colon so a
/// password containing colons round-trips.
pub fn parseHeader(alloc: std.mem.Allocator, header: []const u8) !?Credentials {
    const ws = " \t\n\r\x0b\x0c";
    const h = std.mem.trim(u8, header, ws);

    const prefix = "basic ";
    if (h.len < prefix.len) return null;
    for (prefix, 0..) |c, i| {
        if (std.ascii.toLower(h[i]) != c) return null;
    }
    const token = std.mem.trim(u8, h[prefix.len..], ws);

    // Strip interior whitespace: the standard decoder rejects it, Go's
    // StdEncoding and the TS helper skip it.
    const buf = try alloc.alloc(u8, token.len);
    defer alloc.free(buf);
    var n: usize = 0;
    for (token) |c| {
        switch (c) {
            ' ', '\t', '\n', '\r' => {},
            else => {
                buf[n] = c;
                n += 1;
            },
        }
    }
    const cleaned = buf[0..n];

    // calcSizeForSlice enforces the multiple-of-4 length (error.InvalidPadding
    // otherwise) and the trailing-padding arithmetic, exactly like Go.
    const dec = std.base64.standard.Decoder;
    const decoded_len = dec.calcSizeForSlice(cleaned) catch return null;
    const decoded = try alloc.alloc(u8, decoded_len);
    dec.decode(decoded, cleaned) catch {
        alloc.free(decoded);
        return null;
    };
    defer alloc.free(decoded);

    // UTF-8 validation, mirroring Rust's String::from_utf8 check.
    if (!std.unicode.utf8ValidateSlice(decoded)) return null;

    // Split on the FIRST colon only — the rest stays in the password.
    const colon = std.mem.indexOfScalar(u8, decoded, ':') orelse return null;
    const user = try alloc.dupe(u8, decoded[0..colon]);
    const pass = alloc.dupe(u8, decoded[colon + 1 ..]) catch |e| {
        alloc.free(user);
        return e;
    };
    return .{ .user = user, .pass = pass };
}

// ---------- tests (showcase-only; the canonical suite lives in src/lib) ----------
test "build known vector" {
    const h = try buildHeader(std.testing.allocator, "user", "pass");
    defer std.testing.allocator.free(h);
    try std.testing.expectEqualStrings("Basic dXNlcjpwYXNz", h);
}

test "round trip (incl. empty halves and colon-bearing password)" {
    const cases = [_][2][]const u8{
        .{ "alice", "s3cr3t" },
        .{ "", "x" },
        .{ "x", "" },
        .{ "u", "a:b:c" },
    };
    for (cases) |case| {
        const h = try buildHeader(std.testing.allocator, case[0], case[1]);
        defer std.testing.allocator.free(h);
        const creds = (try parseHeader(std.testing.allocator, h)).?;
        defer creds.deinit(std.testing.allocator);
        try std.testing.expectEqualStrings(case[0], creds.user);
        try std.testing.expectEqualStrings(case[1], creds.pass);
    }
}

test "rejects malformed headers" {
    const bad = [_][]const u8{
        "Bearer xyz",
        "",
        "Basic !!!not-base64!!!",
        "Basic bm9jb2xvbg==", // "nocolon" — no colon separator
    };
    for (bad) |s| {
        try std.testing.expect((try parseHeader(std.testing.allocator, s)) == null);
    }
}

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 →