Something went wrong. Try again.
A Zig library and command line tool for the classic Unix uuencode format.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! zuucode: encoding and decoding of the classic Unix `uuencode` format.//!//! The two halves live in `uuencode` and `uudecode`; the names that matter are//! re-exported here, so `@import("zuucode")` is normally all a consumer needs.//!//! Encoding a complete file, `begin` and `end` lines included://!//! ```//! const text = try zuucode.encodeFileAlloc(gpa, .{ .name = "cat.bin" }, payload, .{});//! defer gpa.free(text);//! ```//!//! Decoding it again://!//! ```//! const file = try zuucode.decodeFileAlloc(gpa, text, .{});//! defer gpa.free(file.data);//! ```//!//! Neither direction allocates unless asked to: `encode` and `decode` write//! into a caller-provided buffer, `Encoder` streams to an `Io.Writer`, and//! `decodeStream` streams from an `Io.Reader`.
const std = @import("std");
pub const uuencode = @import("uuencode.zig");pub const uudecode = @import("uudecode.zig");pub const simd = @import("simd.zig");
/// Maximum number of payload bytes carried by one uuencoded line.pub const bytes_per_line = uuencode.bytes_per_line;/// Number of characters in a full data line, excluding the line terminator.pub const chars_per_line = uuencode.chars_per_line;/// Whether the vectorised kernels are in use on this target.pub const simd_available = simd.available;
pub const Charset = uuencode.Charset;pub const LineEnding = uuencode.LineEnding;pub const Header = uuencode.Header;pub const Encoder = uuencode.Encoder;pub const EncodeOptions = uuencode.Options;
pub const lineLen = uuencode.lineLen;pub const encodedLen = uuencode.encodedLen;pub const encodeLine = uuencode.encodeLine;pub const encodeLineScalar = uuencode.encodeLineScalar;pub const encode = uuencode.encode;pub const encodeAlloc = uuencode.encodeAlloc;pub const encodeStream = uuencode.encodeStream;pub const encodeFile = uuencode.encodeFile;pub const encodeFileAlloc = uuencode.encodeFileAlloc;
pub const DecodeError = uudecode.Error;pub const DecodeOptions = uudecode.Options;pub const File = uudecode.File;pub const StreamHeader = uudecode.StreamHeader;
pub const decodeChar = uudecode.decodeChar;pub const decodedLen = uudecode.decodedLen;pub const decodedLenUpperBound = uudecode.decodedLenUpperBound;pub const decodeLine = uudecode.decodeLine;pub const decodeLineScalar = uudecode.decodeLineScalar;pub const decode = uudecode.decode;pub const decodeAlloc = uudecode.decodeAlloc;pub const decodeFile = uudecode.decodeFile;pub const decodeFileAlloc = uudecode.decodeFileAlloc;pub const decodeStream = uudecode.decodeStream;pub const decodeFileStream = uudecode.decodeFileStream;pub const parseBeginLine = uudecode.parseBeginLine;pub const isEndLine = uudecode.isEndLine;
const testing = std.testing;const Io = std.Io;
test { _ = uuencode; _ = uudecode; _ = simd; _ = @import("fuzz.zig");}
// Expected values below come from Python's `binascii.b2a_uu`, which uses the// historic space charset.
test "encodeLine matches the reference encoder" { var buf: [chars_per_line]u8 = undefined; const options: EncodeOptions = .{ .charset = .space };
try testing.expectEqualStrings("#0V%T", encodeLine(&buf, "Cat", options)); try testing.expectEqualStrings(" ", encodeLine(&buf, "", options)); try testing.expectEqualStrings("!80 ", encodeLine(&buf, "a", options)); try testing.expectEqualStrings("\"86( ", encodeLine(&buf, "ab", options)); try testing.expectEqualStrings("#86)C", encodeLine(&buf, "abc", options)); try testing.expectEqualStrings("# ", encodeLine(&buf, "\x00\x00\x00", options)); try testing.expectEqualStrings( "::'1T<#HO+W=W=RYW:6MI<&5D:6$N;W)G#0H ", encodeLine(&buf, "http://www.wikipedia.org\r\n", options), );
var full: [bytes_per_line]u8 = undefined; for (&full, 0..) |*byte, i| byte.* = @intCast(i); try testing.expectEqualStrings( "M $\" P0%!@<(\"0H+# T.#Q 1$A,4%187&!D:&QP='A\\@(2(C)\"4F)R@I*BLL", encodeLine(&buf, &full, options), );}
test "backtick charset replaces every encoded zero" { var buf: [chars_per_line]u8 = undefined; try testing.expectEqualStrings("!80``", encodeLine(&buf, "a", .{})); try testing.expectEqualStrings("#````", encodeLine(&buf, "\x00\x00\x00", .{})); // A space in the payload is not an encoded zero, so it is left alone. try testing.expectEqualStrings("!(```", encodeLine(&buf, " ", .{}));}
test "lineLen and encodedLen agree with what is written" { for (0..bytes_per_line + 1) |n| { var buf: [chars_per_line]u8 = undefined; const source: [bytes_per_line]u8 = @splat('x'); try testing.expectEqual(lineLen(n), encodeLine(&buf, source[0..n], .{}).len); }
const gpa = testing.allocator; for ([_]usize{ 0, 1, 44, 45, 46, 90, 91, 1000 }) |n| { for ([_]LineEnding{ .lf, .crlf }) |line_ending| { const options: EncodeOptions = .{ .line_ending = line_ending }; const source = try gpa.alloc(u8, n); defer gpa.free(source); @memset(source, 'z');
const text = try encodeAlloc(gpa, source, options); defer gpa.free(text); try testing.expectEqual(encodedLen(n, options), text.len); } }}
test "encodeFile produces a complete uuencoded file" { const gpa = testing.allocator; const text = try encodeFileAlloc(gpa, .{ .name = "cat.txt", .mode = 0o644 }, "Cat", .{}); defer gpa.free(text); try testing.expectEqualStrings("begin 644 cat.txt\n#0V%T\n`\nend\n", text);}
test "encodeFile honours mode, name, charset, and line ending" { const gpa = testing.allocator; const text = try encodeFileAlloc( gpa, .{ .name = "my file.bin", .mode = 0o600 }, "a", .{ .charset = .space, .line_ending = .crlf }, ); defer gpa.free(text); try testing.expectEqualStrings("begin 600 my file.bin\r\n!80 \r\n \r\nend\r\n", text);}
test "round trip across every payload length up to several lines" { const gpa = testing.allocator;
var source: [200]u8 = undefined; for (&source, 0..) |*byte, i| byte.* = @truncate(i *% 7 +% 13);
for (0..source.len + 1) |n| { for ([_]Charset{ .backtick, .space }) |charset| { for ([_]LineEnding{ .lf, .crlf }) |line_ending| { const options: EncodeOptions = .{ .charset = charset, .line_ending = line_ending };
const text = try encodeAlloc(gpa, source[0..n], options); defer gpa.free(text);
const decoded = try decodeAlloc(gpa, text, .{ .require_terminator = true }); defer gpa.free(decoded);
try testing.expectEqualSlices(u8, source[0..n], decoded); } } }}
test "round trip of a payload covering every byte value" { const gpa = testing.allocator;
var source: [256]u8 = undefined; for (&source, 0..) |*byte, i| byte.* = @intCast(i);
const text = try encodeFileAlloc(gpa, .{ .name = "all-bytes" }, &source, .{}); defer gpa.free(text);
const file = try decodeFileAlloc(gpa, text, .{ .require_terminator = true }); defer gpa.free(file.data);
try testing.expectEqualStrings("all-bytes", file.name); try testing.expectEqual(@as(u32, 0o644), file.mode); try testing.expectEqualSlices(u8, &source, file.data);}
test "Encoder buffers payload handed over in uneven chunks" { const gpa = testing.allocator;
var source: [512]u8 = undefined; for (&source, 0..) |*byte, i| byte.* = @truncate(i *% 31);
for ([_]usize{ 1, 2, 7, 44, 45, 46, 128 }) |chunk_len| { var allocating: Io.Writer.Allocating = .init(gpa); defer allocating.deinit();
var encoder: Encoder = .init(&allocating.writer, .{}); try encoder.begin(.{ .name = "chunks" }); var offset: usize = 0; while (offset < source.len) : (offset += chunk_len) { try encoder.write(source[offset..@min(offset + chunk_len, source.len)]); } try encoder.end();
const expected = try encodeFileAlloc(gpa, .{ .name = "chunks" }, &source, .{}); defer gpa.free(expected); try testing.expectEqualStrings(expected, allocating.written()); }}
test "decode into a caller buffer reports an exact length" { const gpa = testing.allocator; const payload = "the quick brown fox jumps over the lazy dog, twice over in fact";
const text = try encodeAlloc(gpa, payload, .{}); defer gpa.free(text);
try testing.expectEqual(payload.len, try decodedLen(text, .{})); try testing.expect(decodedLenUpperBound(text.len) >= payload.len);
var buf: [128]u8 = undefined; try testing.expectEqualStrings(payload, try decode(&buf, text, .{}));
var too_small: [10]u8 = undefined; try testing.expectError(error.NoSpaceLeft, decode(&too_small, text, .{}));}
test "decoder accepts both charsets and both line endings" { var buf: [64]u8 = undefined; try testing.expectEqualStrings("Cat", try decode(&buf, "#0V%T\n \n", .{})); try testing.expectEqualStrings("Cat", try decode(&buf, "#0V%T\r\n`\r\n", .{})); try testing.expectEqualStrings("a", try decode(&buf, "!80 \n \n", .{})); try testing.expectEqualStrings("a", try decode(&buf, "!80``\n`\n", .{}));}
test "decoder tolerates lines whose trailing spaces were stripped" { var buf: [64]u8 = undefined; // "!80 " lost both padding spaces, and the terminator lost its space. try testing.expectEqualStrings("a", try decode(&buf, "!80\n\n", .{})); try testing.expectError( error.TruncatedLine, decode(&buf, "!80\n\n", .{ .pad_truncated_lines = false }), );}
test "strict mode pins how many characters a line must carry" { var buf: [64]u8 = undefined; const strict: DecodeOptions = .{ .pad_truncated_lines = false };
// "a" encodes to a length character plus one full group of four. try testing.expectEqualStrings("a", try decode(&buf, "!80``\n`\n", strict)); // Exactly one character short must be rejected rather than quietly padded, // which is the boundary the padding option turns on and off. try testing.expectError(error.TruncatedLine, decode(&buf, "!80`\n`\n", strict)); try testing.expectEqualStrings("a", try decode(&buf, "!80`\n`\n", .{}));}
test "decoder ignores characters past the declared length" { var buf: [64]u8 = undefined; // A line padded out to the full 61 columns still decodes to three bytes. try testing.expectEqualStrings("Cat", try decode(&buf, "#0V%T" ++ &@as([56]u8, @splat(' ')) ++ "\n`\n", .{})); try testing.expectEqual(@as(usize, 3), try decodeLine(&buf, "#0V%Textra", .{})); try testing.expectEqualStrings("Cat", buf[0..3]);}
test "decode stops at the terminator and ignores what follows" { var buf: [64]u8 = undefined; try testing.expectEqualStrings("Cat", try decode(&buf, "#0V%T\n`\n#86)C\n`\n", .{})); try testing.expectEqualStrings("Cat", try decode(&buf, "#0V%T\nend\ntrailing junk\n", .{}));}
test "decode rejects malformed input" { var buf: [64]u8 = undefined; // 0x7f is outside the 0x20...0x60 alphabet. try testing.expectError(error.InvalidCharacter, decode(&buf, "#0V%\x7f\n`\n", .{})); // '`' + 1 would be a length character of 65, which no line can carry. try testing.expectError(error.InvalidCharacter, decode(&buf, "a0V%T\n`\n", .{})); // 'X' is a length character of 56, above the 45 byte maximum. try testing.expectError(error.InvalidLength, decode(&buf, "X0V%T\n`\n", .{})); try testing.expectError( error.MissingTerminator, decode(&buf, "#0V%T\n", .{ .require_terminator = true }), );}
test "parseBeginLine and isEndLine" { try testing.expect(parseBeginLine("not a header") == null); try testing.expect(parseBeginLine("begin") == null); try testing.expect(parseBeginLine("begin 644") == null); try testing.expect(parseBeginLine("begin 9x9 name") == null); try testing.expect(parseBeginLine("beginning 644 name") == null);
const simple = parseBeginLine("begin 644 cat.txt").?; try testing.expectEqual(@as(u32, 0o644), simple.mode); try testing.expectEqualStrings("cat.txt", simple.name);
// Names may contain spaces, and stray whitespace is forgiven. const spaced = parseBeginLine("begin 0600 my file.bin \r").?; try testing.expectEqual(@as(u32, 0o600), spaced.mode); try testing.expectEqualStrings("my file.bin", spaced.name);
try testing.expect(isEndLine("end")); try testing.expect(isEndLine("end\r")); try testing.expect(isEndLine(" end ")); try testing.expect(!isEndLine("ending"));}
test "decodeFile skips whatever precedes the begin line" { const gpa = testing.allocator; const text = \\Here is the file you asked for. \\ \\begin 755 script.sh \\#0V%T \\` \\end \\ ; const file = try decodeFileAlloc(gpa, text, .{ .require_terminator = true }); defer gpa.free(file.data);
try testing.expectEqualStrings("script.sh", file.name); try testing.expectEqual(@as(u32, 0o755), file.mode); try testing.expectEqualStrings("Cat", file.data);
try testing.expectError(error.MissingBeginLine, decodeFileAlloc(gpa, "#0V%T\n`\n", .{}));}
test "streaming decode from a reader" { const gpa = testing.allocator;
var source: [1000]u8 = undefined; for (&source, 0..) |*byte, i| byte.* = @truncate(i *% 11 +% 3);
const text = try encodeFileAlloc(gpa, .{ .name = "streamed", .mode = 0o600 }, &source, .{}); defer gpa.free(text);
var reader: Io.Reader = .fixed(text); var allocating: Io.Writer.Allocating = .init(gpa); defer allocating.deinit();
const header = try decodeFileStream(&reader, &allocating.writer, .{ .require_terminator = true }); try testing.expectEqualStrings("streamed", header.name()); try testing.expectEqual(@as(u32, 0o600), header.mode); try testing.expectEqualSlices(u8, &source, allocating.written());}