JWT Debugger — Zig source
Decode a JSON Web Token and inspect its header and payload as pretty JSON, with exp/iat claim awareness and an expiry badge. Runs entirely in your browser.
This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.
//! JWT base64url decoding helper — Zig port of the jwt tool.
const std = @import("std");
pub const B64Error = error{ InvalidCharacter, InvalidLength, OutOfMemory };
fn indexOf(c: u8) ?u8 {
return switch (c) {
'A'...'Z' => c - 'A',
'a'...'z' => c - 'a' + 26,
'0'...'9' => c - '0' + 52,
'+' => 62,
'/' => 63,
else => null, // '=' is only legal as the final 1-2 chars
};
}
/// Decode a base64url segment (JWT header/payload) into bytes. Zig's stdlib
/// has url-safe decoders, but the canonical steps are spelled out here so
/// they stay visible. The returned slice is byte-faithful (the caller reads
/// it Latin-1, like JS atob); errors play the role of the TS throw.
pub fn b64urlDecode(alloc: std.mem.Allocator, str: []const u8) B64Error![]u8 {
// 1. URL-safe alphabet -> standard base64 alphabet.
const mapped = try alloc.alloc(u8, str.len);
defer alloc.free(mapped);
for (str, 0..) |c, i| {
mapped[i] = switch (c) {
'-' => '+',
'_' => '/',
else => c,
};
}
// 2. Restore stripped '=' padding so the length is a multiple of 4.
if (mapped.len % 4 == 1) return error.InvalidLength;
const padded_len = mapped.len + (4 - mapped.len % 4) % 4;
const buf = try alloc.alloc(u8, padded_len);
defer alloc.free(buf);
@memcpy(buf[0..mapped.len], mapped);
@memset(buf[mapped.len..], '=');
// 3. Strict decode: 4-char blocks -> 3 bytes, validating every char and
// every '=' (legal only as the final 1-2 chars of the final block).
const out = try alloc.alloc(u8, padded_len / 4 * 3);
errdefer alloc.free(out);
var o: usize = 0;
var q: usize = 0;
while (q < padded_len) : (q += 4) {
var v: [4]u8 = .{ 0, 0, 0, 0 };
var pad: usize = 0;
for (0..4) |k| {
const c = buf[q + k];
if (c == '=') {
pad += 1;
continue;
}
if (pad != 0) return error.InvalidCharacter; // data after '='
v[k] = indexOf(c) orelse return error.InvalidCharacter;
}
if (pad > 2 or (pad != 0 and q + 4 != padded_len)) return error.InvalidCharacter;
const triple: u32 = (@as(u32, v[0]) << 18) | (@as(u32, v[1]) << 12) |
(@as(u32, v[2]) << 6) | @as(u32, v[3]);
out[o] = @truncate(triple >> 16);
if (pad < 2) out[o + 1] = @truncate(triple >> 8);
if (pad < 1) out[o + 2] = @truncate(triple);
o += 3 - pad; // a valid block emits 3, 2 or 1 bytes (pad = 0, 1, 2)
}
// 4. Shrink to the decoded size; the bytes are the result.
return alloc.realloc(out, o);
}
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
// eyJhbGciOiJIUzI1NiJ9 decodes to {"alg":"HS256"}
const text = try b64urlDecode(arena.allocator(), "eyJhbGciOiJIUzI1NiJ9");
std.debug.print("{s}\n", .{text});
}
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 →