Created
December 9, 2025 07:31
-
-
Save azhai/ef42ea75f73755b505201a912f1b5505 to your computer and use it in GitHub Desktop.
A standard base64 encoder and decoder in zig language.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// 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