// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! The `zixl-dump` command line: an installer package written out as files. //! Its options follow those of msitools' `msidump`. Tables are written as //! text archive (`.idt`) files in the format Microsoft documents, a binary //! cell's contents as `TABLE/KEY.ibd` //! (`docs/adr/0006-text-archives-follow-microsofts-format.md`), and streams //! into `_Streams`. const std = @import("std"); const Io = std.Io; const zixl = @import("zixl"); const usage = \\zixl-dump dumps MSI tables as idt text and streams \\Usage: zixl-dump [OPTION]... MSI-FILE \\ \\Options: \\ -t, --tables Dump tables. This is the default. \\ -s, --streams Dump streams \\ -d, --directory DIR Dump to given directory DIR \\ -h, --help Print help message and exit. \\ -v, --version Print version information and exit. \\ \\More than one of -t or -s may be specified. \\ ; pub fn main(init: std.process.Init) !u8 { const arena = init.arena.allocator(); const io = init.io; const args = try init.minimal.args.toSlice(arena); var stdout_buffer: [4096]u8 = undefined; var stdout_writer = Io.File.stdout().writer(io, &stdout_buffer); const stdout = &stdout_writer.interface; var stderr_buffer: [4096]u8 = undefined; var stderr_writer = Io.File.stderr().writer(io, &stderr_buffer); const stderr = &stderr_writer.interface; var tables = false; var streams = false; var directory: []const u8 = "."; var file: ?[]const u8 = null; var i: usize = 1; while (i < args.len) : (i += 1) { const arg: []const u8 = args[i]; if (eql(arg, "-h") or eql(arg, "--help")) { try stdout.writeAll(usage); try stdout.flush(); return 0; } else if (eql(arg, "-v") or eql(arg, "--version")) { try stdout.print("zixl-dump {s}\n", .{zixl.version}); try stdout.flush(); return 0; } else if (eql(arg, "-t") or eql(arg, "--tables")) { tables = true; } else if (eql(arg, "-s") or eql(arg, "--streams")) { streams = true; } else if (eql(arg, "-S") or eql(arg, "--signature")) { return fail(stderr, "{s} is not supported", .{arg}); } else if (eql(arg, "-d") or eql(arg, "--directory")) { i += 1; if (i == args.len) return fail(stderr, "{s} needs a directory", .{arg}); directory = args[i]; } else if (std.mem.startsWith(u8, arg, "--directory=")) { directory = arg["--directory=".len..]; } else if (arg.len > 1 and arg[0] == '-') { return fail(stderr, "unknown option {s}", .{arg}); } else if (file != null) { return fail(stderr, "only one MSI-FILE may be given", .{}); } else { file = arg; } } const path = file orelse { try stderr.writeAll(usage); try stderr.flush(); return 1; }; if (!tables and !streams) tables = true; const bytes = Io.Dir.cwd().readFileAlloc(io, path, arena, .unlimited) catch |e| return fail(stderr, "{s}: cannot be read: {s}", .{ path, @errorName(e) }); var diags: zixl.Diagnostics = .init(arena); const package = zixl.read(arena, &diags, path, bytes) catch |e| switch (e) { error.Invalid => { try diags.render(stderr); try stderr.flush(); return 1; }, else => |other| return other, }; try diags.render(stderr); var out = Io.Dir.cwd().createDirPathOpen(io, directory, .{}) catch |e| return fail(stderr, "{s}: cannot be made: {s}", .{ directory, @errorName(e) }); defer out.close(io); var dumper: Dumper = .{ .arena = arena, .io = io, .out = out, .stdout = stdout, .stderr = stderr, .package = &package }; if (tables) dumper.tables() catch |e| return fail(stderr, "{s}: {s}", .{ directory, @errorName(e) }); if (streams) dumper.streams() catch |e| return fail(stderr, "{s}: {s}", .{ directory, @errorName(e) }); try stdout.flush(); try stderr.flush(); return 0; } const Dumper = struct { arena: std.mem.Allocator, io: Io, out: Io.Dir, stdout: *Io.Writer, stderr: *Io.Writer, package: *const zixl.Package, fn write(d: *Dumper, sub_path: []const u8, data: []const u8) !void { try d.out.writeFile(d.io, .{ .sub_path = sub_path, .data = data }); } /// Every table as `TABLE.idt`, starting with the two that are not /// stored as tables, and each binary cell's contents as `TABLE/KEY.ibd`. fn tables(d: *Dumper) !void { const db = &d.package.database; var text: Io.Writer.Allocating = .init(d.arena); try d.stdout.writeAll("Exporting table _SummaryInformation...\n"); try zixl.archive.writeSummary(d.arena, &text.writer, db.summary); try d.write("_SummaryInformation.idt", text.written()); text.clearRetainingCapacity(); try d.stdout.writeAll("Exporting table _ForceCodepage...\n"); try zixl.archive.writeForceCodepage(&text.writer, db.codepage); try d.write("_ForceCodepage.idt", text.written()); for (db.tables.items) |t| { try d.stdout.print("Exporting table {s}...\n", .{t.name}); text.clearRetainingCapacity(); try zixl.archive.writeTable(d.arena, &text.writer, db.codepage, t); try d.write(try std.fmt.allocPrint(d.arena, "{s}.idt", .{t.name}), text.written()); for (t.rows.items) |row| { for (row) |v| { if (v != .stream) continue; try d.out.createDirPath(d.io, t.name); const name = try zixl.archive.binaryFileName(d.arena, t, row); try d.write(try std.fs.path.join(d.arena, &.{ t.name, safe(d.arena, name) }), v.stream); } } } } /// Every stream that is not a table, into `_Streams`. fn streams(d: *Dumper) !void { try d.out.createDirPath(d.io, "_Streams"); for (d.package.streams) |s| { const name = safe(d.arena, s.name); try d.stdout.print("Exporting stream {s}...\n", .{name}); try d.write(try std.fs.path.join(d.arena, &.{ "_Streams", name }), s.data); } } }; /// A stream or key name made fit to be a file name everywhere: without the /// control characters the summary information's name begins with, which /// Windows does not allow, and with no path separators. fn safe(arena: std.mem.Allocator, name: []const u8) []const u8 { var out: std.ArrayList(u8) = .empty; for (name) |c| { if (c < 0x20) continue; out.append(arena, if (c == '/' or c == '\\') '_' else c) catch return name; } return out.items; } fn eql(a: []const u8, b: []const u8) bool { return std.mem.eql(u8, a, b); } fn fail(stderr: *Io.Writer, comptime fmt: []const u8, args: anytype) !u8 { try stderr.print("zixl-dump: " ++ fmt ++ "\n", args); try stderr.flush(); return 1; } test "a stream name made a file name" { var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); defer arena.deinit(); try std.testing.expectEqualStrings("SummaryInformation", safe(arena.allocator(), "\x05SummaryInformation")); try std.testing.expectEqualStrings("a_b_c", safe(arena.allocator(), "a/b\\c")); }