Something went wrong. Try again.
A Zig library and command line tool for the classic Unix uuencode format.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Command line front end for the zuucode library: a small `uuencode` and//! `uudecode` that reads a file or standard input and writes to standard//! output.
const std = @import("std");const Io = std.Io;
const zuucode = @import("zuucode");
const usage = \\usage: zuucode [options] [file] \\ \\Encodes `file` (or standard input) to standard output, and decodes it \\again with -d. Without -d the output is a complete uuencoded file with \\`begin` and `end` lines unless --raw is given. \\ \\options: \\ -d, --decode decode instead of encode \\ -n, --name NAME name for the `begin` line (default: the input file name) \\ -m, --mode MODE octal mode for the `begin` line (default: 644) \\ --charset SET encode a zero group as `backtick` (default) or `space` \\ --crlf terminate encoded lines with CRLF instead of LF \\ --raw omit (when encoding) the `begin` and `end` lines \\ --strict reject truncated lines and a body with no terminator \\ -h, --help show this help \\;
const Command = struct { decode: bool = false, raw: bool = false, name: ?[]const u8 = null, mode: u32 = 0o644, path: ?[]const u8 = null, encode_options: zuucode.EncodeOptions = .{}, decode_options: zuucode.DecodeOptions = .{},};
pub fn main(init: std.process.Init) !void { const io = init.io; const gpa = init.gpa; const args = try init.minimal.args.toSlice(init.arena.allocator());
var stderr_buffer: [512]u8 = undefined; var stderr_writer: Io.File.Writer = .init(.stderr(), io, &stderr_buffer); const stderr = &stderr_writer.interface;
const command = parseArgs(args) catch |err| switch (err) { error.HelpRequested => { try stderr.writeAll(usage); try stderr.flush(); return; }, else => |e| { try stderr.print("zuucode: {s}\n\n{s}", .{ @errorName(e), usage }); try stderr.flush(); std.process.exit(2); }, };
run(io, gpa, command) catch |err| { try stderr.print("zuucode: {s}\n", .{@errorName(err)}); try stderr.flush(); std.process.exit(1); };}
fn run(io: Io, gpa: std.mem.Allocator, command: Command) !void { const source = try readInput(io, gpa, command.path); defer gpa.free(source);
var stdout_buffer: [4096]u8 = undefined; var stdout_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer); const stdout = &stdout_writer.interface;
if (command.decode) { const decoded = if (command.raw) try zuucode.decodeAlloc(gpa, source, command.decode_options) else (try zuucode.decodeFileAlloc(gpa, source, command.decode_options)).data; defer gpa.free(decoded); try stdout.writeAll(decoded); } else if (command.raw) { try zuucode.encodeStream(stdout, source, command.encode_options); } else { const header: zuucode.Header = .{ .name = command.name orelse command.path orelse "-", .mode = command.mode, }; try zuucode.encodeFile(stdout, header, source, command.encode_options); }
try stdout.flush();}
fn parseArgs(args: []const [:0]const u8) !Command { var command: Command = .{};
var index: usize = 1; while (index < args.len) : (index += 1) { const arg = args[index]; if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) { return error.HelpRequested; } else if (std.mem.eql(u8, arg, "-d") or std.mem.eql(u8, arg, "--decode")) { command.decode = true; } else if (std.mem.eql(u8, arg, "--raw")) { command.raw = true; } else if (std.mem.eql(u8, arg, "--crlf")) { command.encode_options.line_ending = .crlf; } else if (std.mem.eql(u8, arg, "--strict")) { command.decode_options = .{ .pad_truncated_lines = false, .require_terminator = true }; } else if (std.mem.eql(u8, arg, "-n") or std.mem.eql(u8, arg, "--name")) { index += 1; if (index == args.len) return error.MissingArgument; command.name = args[index]; } else if (std.mem.eql(u8, arg, "-m") or std.mem.eql(u8, arg, "--mode")) { index += 1; if (index == args.len) return error.MissingArgument; command.mode = std.fmt.parseInt(u32, args[index], 8) catch return error.InvalidMode; } else if (std.mem.eql(u8, arg, "--charset")) { index += 1; if (index == args.len) return error.MissingArgument; command.encode_options.charset = std.meta.stringToEnum(zuucode.Charset, args[index]) orelse return error.InvalidCharset; } else if (arg.len > 1 and arg[0] == '-' and !std.mem.eql(u8, arg, "-")) { return error.UnknownOption; } else if (command.path != null) { return error.TooManyInputs; } else if (!std.mem.eql(u8, arg, "-")) { command.path = arg; } }
return command;}
fn readInput(io: Io, gpa: std.mem.Allocator, path: ?[]const u8) ![]u8 { var buffer: [8192]u8 = undefined;
if (path) |p| { const file = try Io.Dir.cwd().openFile(io, p, .{}); defer file.close(io); var file_reader: Io.File.Reader = file.reader(io, &buffer); return file_reader.interface.allocRemaining(gpa, .unlimited); }
var stdin_reader: Io.File.Reader = .initStreaming(.stdin(), io, &buffer); return stdin_reader.interface.allocRemaining(gpa, .unlimited);}
test "the CLI round trips through the library" { const gpa = std.testing.allocator; const payload = "encode me, then decode me back again";
const text = try zuucode.encodeFileAlloc(gpa, .{ .name = "round-trip" }, payload, .{}); defer gpa.free(text);
const file = try zuucode.decodeFileAlloc(gpa, text, .{ .require_terminator = true }); defer gpa.free(file.data);
try std.testing.expectEqualStrings(payload, file.data);}
test "argument parsing" { const parsed = try parseArgs(&.{ "zuucode", "-d", "--strict", "input.uu" }); try std.testing.expect(parsed.decode); try std.testing.expect(parsed.decode_options.require_terminator); try std.testing.expectEqualStrings("input.uu", parsed.path.?);
const encoding = try parseArgs(&.{ "zuucode", "-m", "600", "--charset", "space", "--crlf" }); try std.testing.expectEqual(@as(u32, 0o600), encoding.mode); try std.testing.expectEqual(zuucode.Charset.space, encoding.encode_options.charset); try std.testing.expectEqual(zuucode.LineEnding.crlf, encoding.encode_options.line_ending);
try std.testing.expectError(error.UnknownOption, parseArgs(&.{ "zuucode", "--nope" })); try std.testing.expectError(error.MissingArgument, parseArgs(&.{ "zuucode", "-n" })); try std.testing.expectError(error.InvalidCharset, parseArgs(&.{ "zuucode", "--charset", "b64" }));}