Skip to content

Instantly share code, notes, and snippets.

@azhai
Created December 9, 2025 07:31
Show Gist options
  • Select an option

  • Save azhai/ef42ea75f73755b505201a912f1b5505 to your computer and use it in GitHub Desktop.

Select an option

Save azhai/ef42ea75f73755b505201a912f1b5505 to your computer and use it in GitHub Desktop.
A standard base64 encoder and decoder in zig language.
/// A standard base64 encoder and decoder.
///
/// This struct provides methods for encoding and decoding data using the standard base64 encoding scheme.
/// It supports both standard and URL-safe base64 encoding.
///
/// Author:
/// Pedro Faria (https://github.com/pedropark99/)
/// Ryan Liu (https://github.com/azhai/)
/// Refer:
/// https://pedropark99.github.io/zig-book/Chapters/01-base64.html
/// https://github.com/pedropark99/zig-book/blob/main/ZigExamples/base64/base64_basic.zig
/// https://codeberg.org/ziglang/zig/src/tag/0.15.2/lib/std/base64.zig
///
/// Env: Zig 0.15.2 arm64
/// Test:
/// >>> zig test base64.zig
/// Run:
/// >>> zig run base64.zig
/// Encoded text: VGVzdGluZyBzb21lIG1vcmUgc3R1ZmY=
/// Decoded text: Testing some more stuff
/// Encoded length: 32
/// Decoded length: 23
///
/// test0: abc YWJj
/// test1: abca YWJjYQ==
/// test2: abcab YWJjYWI=
/// test3: abcabc YWJjYWJj
///
const std = @import("std");
const testing = std.testing;
const isWhitespace = std.ascii.isWhitespace;
const print = std.debug.print;
const stdout = std.fs.File.stdout();
/// Represents a standard base64 encoder and decoder.
const StandardBase64 = struct {
allocator: std.mem.Allocator,
_table: *const [65]u8,
/// Initializes a new instance of StandardBase64.
///
/// Parameters:
/// allocator: The allocator to use for memory allocation.
/// url_safe: Whether to use URL-safe characters (default is false).
///
/// Returns:
/// A new instance of StandardBase64.
pub fn init(allocator: std.mem.Allocator, url_safe: bool) StandardBase64 {
const uppers = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const lowers = "abcdefghijklmnopqrstuvwxyz";
const numbers = "0123456789";
const characters = uppers ++ lowers ++ numbers;
if (url_safe) {
return StandardBase64{
.allocator = allocator,
._table = characters ++ "-_=",
};
} else {
return StandardBase64{
.allocator = allocator,
._table = characters ++ "+/=",
};
}
}
fn _char_at(self: StandardBase64, index: usize) u8 {
if (index >= self._table.len) {
return '=';
}
return self._table[index];
}
fn _char_index(_: StandardBase64, char: u8) u8 {
if ('A' <= char and char <= 'Z') {
return char - 'A';
} else if ('a' <= char and char <= 'z') {
return char - 'a' + 26;
} else if ('0' <= char and char <= '9') {
return char - '0' + 52;
} else if (char == '+' or char == '-') {
return 62;
} else if (char == '/' or char == '_') {
return 63;
} else {
return 64;
}
}
/// Encodes the given input into a base64 string.
fn encode(self: StandardBase64, input: []const u8) ![]u8 {
const input_len: usize = input.len;
if (input_len == 0)
return "";
const n_output = try _calc_encode_length(input_len);
var output = try self.allocator.alloc(u8, n_output);
var idx: usize = 0;
var offset: usize = 0;
var part: u24 = 0;
while (offset + 3 <= input_len) : (offset += 3) {
const bits = std.mem.readInt(u24, input[offset..][0..3], .big);
inline for (0..4) |i| {
part = bits >> @intCast(24 - i * 6 - 6);
output[idx + i] = self._char_at(part & 0x3f);
}
idx += 4;
}
const count: usize = input_len - offset;
// print("remaining: {d} {d} {s}\n", .{ count, offset, input[offset..] });
if (count > 0) {
var remaining = [3]u8{ 0x03, 0x0f, 0x3f };
@memcpy(remaining[0..count], input[offset..]);
const bits = std.mem.readInt(u24, remaining[0..3], .big);
for (0..4) |i| {
if (i <= count) {
part = bits >> @intCast(24 - i * 6 - 6);
output[idx + i] = self._char_at(part & 0x3f);
} else {
output[idx + i] = self._char_at(0xff);
}
}
}
return output;
}
/// Decodes the given base64 string into a byte array.
fn decode(self: StandardBase64, input: []const u8) ![]u8 {
const input_len: usize = input.len;
if (input_len == 0)
return "";
const n_output = try _calc_decode_max_length(input_len);
var output = try self.allocator.alloc(u8, n_output);
var tmp = [4]u8{ 0, 0, 0, 0 };
var count: u8 = 0;
var written: u64 = 0;
for (0..input_len) |i| {
if (isWhitespace(input[i])) { // Ignore whitespace
continue;
}
tmp[count] = self._char_index(input[i]);
count += 1;
if (count == 4) {
output[written] = (tmp[0] << 2) + (tmp[1] >> 4);
written += 1;
if (tmp[2] != 64) {
output[written] = (tmp[1] << 4) + (tmp[2] >> 2);
written += 1;
}
if (tmp[3] != 64) {
output[written] = (tmp[2] << 6) + tmp[3];
written += 1;
}
count = 0;
}
}
// print("written: {d}, n_output: {d}, input: {s}\n", .{ written, n_output, input });
return output[0..written];
}
};
fn _calc_encode_length(input_len: usize) !usize {
return @divTrunc(input_len + 2, 3) * 4;
}
fn _calc_decode_max_length(input_len: usize) !usize {
return @divTrunc(input_len, 4) * 3;
}
pub fn main() !void {
var memory_buffer: [1000]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&memory_buffer);
const base64 = StandardBase64.init(fba.allocator(), false);
const orig_text = "Testing some more stuff";
const enc_text = "VGVzdGluZyBzb21lIG1vcmUgc3R1ZmY=";
const encoded_text = try base64.encode(orig_text);
const decoded_text = try base64.decode(enc_text);
print("Encoded text: {s}\n", .{encoded_text});
print("Decoded text: {s}\n", .{decoded_text});
print("Encoded length: {d}\n", .{encoded_text.len});
print("Decoded length: {d}\n\n", .{decoded_text.len});
print("test0: {s} {s}\n", .{ "abc", try base64.encode("abc") }); // YWJj
print("test1: {s} {s}\n", .{ "abca", try base64.encode("abca") }); // YWJjYQ==
print("test2: {s} {s}\n", .{ "abcab", try base64.encode("abcab") }); // YWJjYWI=
print("test3: {s} {s}\n", .{ "abcabc", try base64.encode("abcabc") }); // YWJjYWJj
}
test "base64" {
@setEvalBranchQuota(8000);
var memory_buffer: [1000]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&memory_buffer);
var base64 = StandardBase64.init(fba.allocator(), false);
try testEncodeAndDecode(&base64, "", "");
try testEncodeAndDecode(&base64, "f", "Zg==");
try testEncodeAndDecode(&base64, "fo", "Zm8=");
try testEncodeAndDecode(&base64, "foo", "Zm9v");
try testEncodeAndDecode(&base64, "foob", "Zm9vYg==");
try testEncodeAndDecode(&base64, "fooba", "Zm9vYmE=");
try testEncodeAndDecode(&base64, "foobar", "Zm9vYmFy");
try testEncodeAndDecode(&base64, "foobarfoobarfoo", "Zm9vYmFyZm9vYmFyZm9v");
try testEncodeAndDecode(&base64, "foobarfoobarfoob", "Zm9vYmFyZm9vYmFyZm9vYg==");
try testEncodeAndDecode(&base64, "foobarfoobarfooba", "Zm9vYmFyZm9vYmFyZm9vYmE=");
try testEncodeAndDecode(&base64, "foobarfoobarfoobar", "Zm9vYmFyZm9vYmFyZm9vYmFy");
try testDecodeIgnoreSpace(&base64, "", " ");
try testDecodeIgnoreSpace(&base64, "f", "Z g= =");
try testDecodeIgnoreSpace(&base64, "fo", " Zm8=");
try testDecodeIgnoreSpace(&base64, "foo", "Zm9v ");
try testDecodeIgnoreSpace(&base64, "foob", "Zm9vYg = = ");
try testDecodeIgnoreSpace(&base64, "fooba", "Zm9v YmE=");
try testDecodeIgnoreSpace(&base64, "foobar", " Z m 9 v Y m F y ");
}
fn testEncodeAndDecode(base64: *StandardBase64, expected_decoded: []const u8, expected_encoded: []const u8) !void {
// Encode
{
const encoded = try base64.encode(expected_decoded);
try testing.expectEqualSlices(u8, expected_encoded, encoded);
}
// Decode
{
const decoded = try base64.decode(expected_encoded);
try testing.expectEqualSlices(u8, expected_decoded, decoded);
}
}
fn testDecodeIgnoreSpace(base64: *StandardBase64, expected_decoded: []const u8, encoded: []const u8) !void {
const decoded = try base64.decode(encoded);
try testing.expectEqualSlices(u8, expected_decoded, decoded);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment