Something went wrong. Try again.
A Zig library and small command line tool for compressing and decompressing Uxn LZ Format (ULZ) things.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376pub fn main(init: std.process.Init) u8 { const arena = init.arena.allocator(); const io = init.io;
var args_iter = std.process.Args.Iterator.initAllocator(init.minimal.args, arena) catch return 1; _ = args_iter.skip();
var args_list: std.ArrayList([:0]const u8) = .empty; args_list.append(arena, "ulz") catch return 1; while (args_iter.next()) |arg| { args_list.append(arena, arg) catch return 1; } const args = args_list.items;
var stdout_buffer: [4096]u8 = undefined; var stdout_writer = std.Io.File.stdout().writerStreaming(io, &stdout_buffer);
run( io, arena, .{ .compressFn = compressFile, .decompressFn = decompressFile, }, &stdout_writer.interface, args, ) catch |err| { switch (err) { error.NotEnoughArguments => { stdout_writer.interface.writeAll("Not enough arguments.\n" ++ usage) catch return 1; stdout_writer.interface.flush() catch return 1; }, else => {}, } return 1; };
return 0;}
fn run(io: std.Io, arena: std.mem.Allocator, ops: Operations, stdout: *std.Io.Writer, args: []const [:0]const u8) !void { if (args.len < 2) { return error.NotEnoughArguments; } var i: usize = 1; while (i < args.len) : (i += 1) { const arg = args[i]; if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) { try stdout.writeAll(usage); try stdout.flush(); return; } }
const options = try Options.fromArgs(args);
if (options.compress) { ops.compressFn(io, arena, options); } else { ops.decompressFn(io, arena, options); }}
var visits: std.ArrayList(Options) = undefined;
test run { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const io = std.testing.io;
visits = .empty; defer visits.deinit(arena);
const mockOperation = struct { pub fn call(_: std.Io, alloc: Allocator, options: Options) void { visits.append(alloc, options) catch return; } }.call;
var discard_buf: [64]u8 = undefined; var writer = Io.Writer.Discarding.init(&discard_buf);
const help_args = [_][:0]const u8{ "ulz", "-h" }; try run( io, arena, .{ .compressFn = mockOperation, .decompressFn = mockOperation, }, &writer.writer, &help_args, );
try expectEqual(0, visits.items.len);
const args_with_output = [_][:0]const u8{ "ulz", "-o", "out.ulz", "tests/test.txt" }; try run( io, arena, .{ .compressFn = mockOperation, .decompressFn = mockOperation, }, &writer.writer, &args_with_output, ); try expectEqual(1, visits.items.len); try expectEqualStrings("out.ulz", visits.items[0].output.?); try expectEqualStrings("tests/test.txt", visits.items[0].file.?);
visits = .empty; const min_args = [_][:0]const u8{ "ulz", "tests/test.txt" }; try run( io, arena, .{ .compressFn = mockOperation, .decompressFn = mockOperation, }, &writer.writer, &min_args, ); try expectEqual(1, visits.items.len); try expectEqual(true, visits.items[0].compress); try expectEqual(null, visits.items[0].output); try expectEqualStrings("tests/test.txt", visits.items[0].file.?);
visits = .empty; const decompress_args = [_][:0]const u8{ "ulz", "-d", "tests/test.txt.ulz" }; try run( io, arena, .{ .compressFn = mockOperation, .decompressFn = mockOperation, }, &writer.writer, &decompress_args, ); try expectEqual(1, visits.items.len); try expectEqual(false, visits.items[0].compress); try expectEqualStrings("tests/test.txt.ulz", visits.items[0].file.?);
visits = .empty; const no_args = [_][:0]const u8{"ulz"}; try std.testing.expectError(error.NotEnoughArguments, run( io, arena, .{ .compressFn = mockOperation, .decompressFn = mockOperation, }, &writer.writer, &no_args, ));
const missing_output_args = [_][:0]const u8{ "ulz", "-o" }; try std.testing.expectError(error.NotEnoughArguments, run( io, arena, .{ .compressFn = mockOperation, .decompressFn = mockOperation, }, &writer.writer, &missing_output_args, ));
const missing_file_args = [_][:0]const u8{ "ulz", "-d" }; try std.testing.expectError(error.NotEnoughArguments, run( io, arena, .{ .compressFn = mockOperation, .decompressFn = mockOperation, }, &writer.writer, &missing_file_args, )); try expectEqual(0, visits.items.len);
const file_before_flag_args = [_][:0]const u8{ "ulz", "tests/test.txt.ulz", "-d" }; try run( io, arena, .{ .compressFn = mockOperation, .decompressFn = mockOperation, }, &writer.writer, &file_before_flag_args, ); try expectEqual(1, visits.items.len); try expectEqual(false, visits.items[0].compress); try expectEqualStrings("tests/test.txt.ulz", visits.items[0].file.?);}
const usage = \\ \\Usage: ulz [-d | --decompress] [-o | --output <output>] \\ [-h | --help] FILE \\ \\Compress and decompress files using ULZ. \\ \\Options: \\ \\ -d, --decompress Decompress \\ -o, --output Write output to a single file \\ -h, --help Show this help and exit \\ \\Arguments: \\ \\ FILE Input file \\;
fn compressFile(io: std.Io, arena: std.mem.Allocator, options: Options) void { const filename = options.file.?; const output = options.output;
const input = Io.Dir.cwd().readFileAlloc(io, filename, arena, .unlimited) catch |err| { fatal("unable to read '{s}': {s}", .{ filename, @errorName(err) }); };
const compressed = ulz.encode(arena, input) catch |err| { fatal("unable to compress '{s}': {s}", .{ filename, @errorName(err) }); };
const output_filename = if (output) |out_filename| out_filename else std.fmt.allocPrint(arena, "{s}.ulz", .{filename}) catch @panic("OOM");
var output_file = Io.Dir.cwd().createFile(io, output_filename, .{}) catch |err| { fatal("unable to open '{s}' for writing: {s}", .{ output_filename, @errorName(err) }); }; defer output_file.close(io);
output_file.writeStreamingAll(io, compressed[0..]) catch |err| { fatal("unable to write to '{s}': {s}", .{ output_filename, @errorName(err) }); };}
test "compressFile writes compressed output" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const io = std.testing.io;
const test_filename = "test_compress_input.txt"; const test_content = "hello world"; { var file = try Io.Dir.cwd().createFile(io, test_filename, .{}); defer file.close(io); try file.writeStreamingAll(io, test_content); } defer Io.Dir.cwd().deleteFile(io, test_filename) catch {};
const output_filename = "test_compress_output.ulz"; defer Io.Dir.cwd().deleteFile(io, output_filename) catch {};
compressFile(io, allocator, .{ .file = test_filename, .output = output_filename });
const compressed = try Io.Dir.cwd().readFileAlloc(io, output_filename, allocator, .unlimited); try std.testing.expect(compressed.len > 0);}
fn decompressFile(io: std.Io, arena: std.mem.Allocator, options: Options) void { const filename = options.file.?; const output = options.output;
const input = Io.Dir.cwd().readFileAlloc(io, filename, arena, .unlimited) catch |err| { fatal("unable to read '{s}': {s}", .{ filename, @errorName(err) }); };
const decompressed = ulz.decode(arena, input) catch |err| { fatal("unable to decompress '{s}': {s}", .{ filename, @errorName(err) }); };
const output_file_path = if (output) |out| out else if (std.mem.endsWith(u8, filename, ".ulz")) filename[0 .. filename.len - 4] else std.fmt.allocPrint(arena, "{s}.unlz", .{filename}) catch @panic("OOM");
var output_file = Io.Dir.cwd().createFile(io, output_file_path, .{}) catch |err| { fatal("unable to open '{s}' for writing: {s}", .{ output_file_path, @errorName(err) }); }; defer output_file.close(io);
output_file.writeStreamingAll(io, decompressed[0..]) catch |err| { fatal("unable to write to '{s}': {s}", .{ output_file_path, @errorName(err) }); };}
test "decompressFile writes decompressed output" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const io = std.testing.io;
const test_filename = "test_decompress_input.txt"; const test_content = "zig is fun"; { var file = try Io.Dir.cwd().createFile(io, test_filename, .{}); defer file.close(io); try file.writeStreamingAll(io, test_content); } defer Io.Dir.cwd().deleteFile(io, test_filename) catch {};
const compressed_filename = "test_decompress_input.txt.ulz"; defer Io.Dir.cwd().deleteFile(io, compressed_filename) catch {};
compressFile(io, allocator, .{ .file = test_filename, .output = compressed_filename });
const output_filename = "test_decompress_output.txt"; defer Io.Dir.cwd().deleteFile(io, output_filename) catch {};
decompressFile(io, allocator, .{ .file = compressed_filename, .output = output_filename });
const decompressed = try Io.Dir.cwd().readFileAlloc(io, output_filename, allocator, .unlimited); try expectEqualStrings(test_content, decompressed);
decompressFile(io, allocator, .{ .file = compressed_filename }); const default_output_filename = "test_decompress_output.txt"; defer Io.Dir.cwd().deleteFile(io, default_output_filename) catch {};}
fn fatal(comptime format: []const u8, args: anytype) noreturn { std.log.err(format, args);
std.process.exit(1);}
const Options = struct { compress: bool = true, output: ?[]const u8 = null, file: ?[]const u8 = null,
pub fn fromArgs(args: []const [:0]const u8) !Options { var flags: Options = .default;
var i: usize = 1; while (i < args.len) : (i += 1) { const arg = args[i]; if (std.mem.eql(u8, "-d", arg) or std.mem.eql(u8, "--decompress", arg)) { flags.compress = false; } else if (std.mem.eql(u8, "-o", arg) or std.mem.eql(u8, "--output", arg)) { i += 1; if (i >= args.len) return error.NotEnoughArguments; if (flags.output != null) fatal("duplicated {s} argument", .{arg}); flags.output = args[i]; } else { flags.file = arg; } }
if (flags.file == null) return error.NotEnoughArguments;
return flags; }
pub const default: Options = .{ .compress = true, .output = null, .file = null };};
const Operations = struct { compressFn: *const fn (std.Io, Allocator, Options) void, decompressFn: *const fn (std.Io, Allocator, Options) void,};
const ulz = @import("ulz");
const std = @import("std");const Io = std.Io;const expectEqual = std.testing.expectEqual;const expectEqualStrings = std.testing.expectEqualStrings;const Allocator = std.mem.Allocator;