Something went wrong. Try again.
A Zig library and command line tool for the classic Unix uuencode format.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Encoding side of the classic Unix `uuencode` format.//!//! A uuencoded stream is a sequence of lines. Each line begins with a single//! character giving the number of payload bytes it carries, followed by that//! payload written as groups of three bytes expanded into four printable//! characters. Every six-bit group is emitted as `group + 0x20`, which keeps//! the whole encoding inside the printable ASCII range. The body ends with a//! line whose length character is zero.//!//! A complete uuencoded file additionally carries a `begin <mode> <name>`//! header line and a trailing `end` line, which is what `Encoder.begin` and//! `Encoder.end` write.
const std = @import("std");const Io = std.Io;const Allocator = std.mem.Allocator;const assert = std.debug.assert;
const simd = @import("simd.zig");
/// Maximum number of payload bytes a single uuencoded line can carry. This is/// the value every historical implementation uses: 45 bytes expand to exactly/// 60 characters, which together with the length character makes a 61 column/// line.pub const bytes_per_line: usize = 45;
/// Number of characters in a full data line, excluding the line terminator.pub const chars_per_line: usize = lineLen(bytes_per_line);
/// Which character represents a six-bit group whose value is zero.////// The two choices decode identically, because decoders mask the character/// down to six bits and `' ' & 0o77 == '`' & 0o77 == 0`.pub const Charset = enum { /// Emit a backtick for zero, as BSD `uuencode` does. This is the default /// because it survives transports that strip trailing whitespace. backtick, /// Emit a space for zero, as the original 1980 implementation does. space,
/// The character this charset uses for a six-bit group of zero. pub fn zeroChar(charset: Charset) u8 { return switch (charset) { .backtick => '`', .space => ' ', }; }
fn encodeSix(charset: Charset, six: u6) u8 { return if (six == 0) charset.zeroChar() else @as(u8, six) + ' '; }};
/// Line terminator written after each encoded line.pub const LineEnding = enum { lf, crlf,
pub fn bytes(line_ending: LineEnding) []const u8 { return switch (line_ending) { .lf => "\n", .crlf => "\r\n", }; }};
pub const Options = struct { charset: Charset = .backtick, line_ending: LineEnding = .lf,};
/// The metadata carried by a `begin` line.pub const Header = struct { /// File name. May contain spaces; it runs to the end of the `begin` line. name: []const u8, /// Unix permission bits, written in octal. mode: u32 = 0o644,};
/// Number of characters, excluding the line terminator, produced for a data/// line carrying `payload_len` bytes.pub fn lineLen(payload_len: usize) usize { assert(payload_len <= bytes_per_line); return 1 + 4 * ((payload_len + 2) / 3);}
/// Number of bytes `encode` writes for a payload of `source_len` bytes,/// including line terminators and the zero-length terminator line, but/// excluding any `begin` and `end` lines.pub fn encodedLen(source_len: usize, options: Options) usize { const eol = options.line_ending.bytes().len; const full_lines = source_len / bytes_per_line; const rest = source_len % bytes_per_line; var total = full_lines * (chars_per_line + eol); if (rest != 0) total += lineLen(rest) + eol; return total + 1 + eol;}
/// Encodes up to `bytes_per_line` payload bytes as one data line, without a/// line terminator. Returns the portion of `dest` that was written.////// A full line is handed to the vector kernel in `simd.zig` on targets that/// have the registers for it; everything else, including every short final/// line, takes the scalar path. Both produce the same bytes.////// Asserts `source.len <= bytes_per_line` and that `dest` has room for/// `lineLen(source.len)` bytes.pub fn encodeLine(dest: []u8, source: []const u8, options: Options) []u8 { if (simd.available and source.len == bytes_per_line) { assert(dest.len >= chars_per_line); dest[0] = options.charset.encodeSix(@intCast(source.len)); switch (options.charset) { inline else => |charset| simd.encodeFullLine( dest[1..][0..simd.chars_per_full_line], source[0..bytes_per_line], charset, ), } return dest[0..chars_per_line]; } return encodeLineScalar(dest, source, options);}
/// The scalar reference implementation of `encodeLine`, always available and/// always taken for short lines. Kept separate so the vector kernel has/// something to be checked and benchmarked against.pub fn encodeLineScalar(dest: []u8, source: []const u8, options: Options) []u8 { const len = lineLen(source.len); assert(dest.len >= len);
const charset = options.charset; dest[0] = charset.encodeSix(@intCast(source.len));
var read: usize = 0; var written: usize = 1; while (read < source.len) : (read += 3) { // Bytes past the end of `source` are encoded as zero, which is what // makes the last line of a payload round-trip: the length character // tells the decoder how much of the final group to keep. const b0: u32 = source[read]; const b1: u32 = if (read + 1 < source.len) source[read + 1] else 0; const b2: u32 = if (read + 2 < source.len) source[read + 2] else 0; const group = (b0 << 16) | (b1 << 8) | b2;
dest[written + 0] = charset.encodeSix(@truncate(group >> 18)); dest[written + 1] = charset.encodeSix(@truncate(group >> 12)); dest[written + 2] = charset.encodeSix(@truncate(group >> 6)); dest[written + 3] = charset.encodeSix(@truncate(group)); written += 4; }
assert(written == len); return dest[0..len];}
/// Encodes `source` as a uuencode body: data lines followed by the/// zero-length terminator line. No `begin` or `end` line is written.////// Asserts `dest` has room for `encodedLen(source.len, options)` bytes.pub fn encode(dest: []u8, source: []const u8, options: Options) []u8 { const total = encodedLen(source.len, options); assert(dest.len >= total);
const eol = options.line_ending.bytes(); var read: usize = 0; var written: usize = 0; while (read < source.len) : (read += bytes_per_line) { const chunk = source[read..@min(read + bytes_per_line, source.len)]; written += encodeLine(dest[written..], chunk, options).len; @memcpy(dest[written..][0..eol.len], eol); written += eol.len; }
dest[written] = options.charset.zeroChar(); written += 1; @memcpy(dest[written..][0..eol.len], eol); written += eol.len;
assert(written == total); return dest[0..written];}
/// Same as `encode`, allocating the result. Caller owns the returned memory.pub fn encodeAlloc(gpa: Allocator, source: []const u8, options: Options) Allocator.Error![]u8 { const dest = try gpa.alloc(u8, encodedLen(source.len, options)); return encode(dest, source, options);}
/// Encodes `source` as a uuencode body directly to `writer`.pub fn encodeStream(writer: *Io.Writer, source: []const u8, options: Options) Io.Writer.Error!void { var encoder: Encoder = .init(writer, options); try encoder.write(source); try encoder.flush();}
/// Encodes `source` as a complete uuencoded file: a `begin` line, the body,/// and an `end` line.pub fn encodeFile( writer: *Io.Writer, header: Header, source: []const u8, options: Options,) Io.Writer.Error!void { var encoder: Encoder = .init(writer, options); try encoder.begin(header); try encoder.write(source); try encoder.end();}
/// Same as `encodeFile`, allocating the result. Caller owns the returned/// memory.pub fn encodeFileAlloc( gpa: Allocator, header: Header, source: []const u8, options: Options,) Allocator.Error![]u8 { var allocating: Io.Writer.Allocating = .init(gpa); defer allocating.deinit(); encodeFile(&allocating.writer, header, source, options) catch return error.OutOfMemory; return allocating.toOwnedSlice();}
/// Incremental encoder. Payload can be handed over in arbitrarily sized/// chunks; the encoder buffers whatever does not fill a complete line.////// To produce a bare body call `write` as many times as needed and then/// `flush`. To produce a complete file, call `begin` first and finish with/// `end` instead of `flush`.pub const Encoder = struct { writer: *Io.Writer, options: Options, /// Payload bytes left over from previous `write` calls, not yet enough to /// fill a line. pending: [bytes_per_line]u8, pending_len: usize,
pub const Error = Io.Writer.Error;
pub fn init(writer: *Io.Writer, options: Options) Encoder { return .{ .writer = writer, .options = options, .pending = undefined, .pending_len = 0, }; }
/// Writes the `begin <mode> <name>` header line. pub fn begin(encoder: *Encoder, header: Header) Error!void { try encoder.writer.print("begin {o} {s}{s}", .{ header.mode, header.name, encoder.options.line_ending.bytes(), }); }
/// Encodes more payload. Bytes that do not complete a line are buffered /// until a later `write`, `flush`, or `end` call. pub fn write(encoder: *Encoder, payload: []const u8) Error!void { var rest = payload;
if (encoder.pending_len != 0) { const take = @min(bytes_per_line - encoder.pending_len, rest.len); @memcpy(encoder.pending[encoder.pending_len..][0..take], rest[0..take]); encoder.pending_len += take; rest = rest[take..]; if (encoder.pending_len < bytes_per_line) return; try encoder.writeLine(&encoder.pending); encoder.pending_len = 0; }
while (rest.len >= bytes_per_line) { try encoder.writeLine(rest[0..bytes_per_line]); rest = rest[bytes_per_line..]; }
@memcpy(encoder.pending[0..rest.len], rest); encoder.pending_len = rest.len; }
/// Writes any buffered payload followed by the zero-length terminator /// line, completing the body. Call at most once per encoder; it does not /// flush the underlying `Io.Writer`. pub fn flush(encoder: *Encoder) Error!void { if (encoder.pending_len != 0) { try encoder.writeLine(encoder.pending[0..encoder.pending_len]); encoder.pending_len = 0; } try encoder.writer.writeByte(encoder.options.charset.zeroChar()); try encoder.writer.writeAll(encoder.options.line_ending.bytes()); }
/// `flush` followed by the `end` trailer line. pub fn end(encoder: *Encoder) Error!void { try encoder.flush(); try encoder.writer.writeAll("end"); try encoder.writer.writeAll(encoder.options.line_ending.bytes()); }
fn writeLine(encoder: *Encoder, chunk: []const u8) Error!void { var buf: [chars_per_line]u8 = undefined; try encoder.writer.writeAll(encodeLine(&buf, chunk, encoder.options)); try encoder.writer.writeAll(encoder.options.line_ending.bytes()); }};