Something went wrong. Try again.
Builds Windows Installer packages from WiX sources on Linux or Windows, in pure Zig; a clean-room reimplementation of msitools' wixl.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! An installer database: the contents of an installer package as tables of//! rows plus summary information, with nothing yet decided about how they are//! laid out in a file.//!//! Linking produces one from a WiX document, and writing turns one into the//! bytes of an installer package. It knows nothing about WiX, so that a reader//! of installer packages can produce the same thing.
const std = @import("std");const Allocator = std.mem.Allocator;
const Database = @This();
/// Where the tables, rows and copied strings are allocated. The database is/// meant to live in an arena and be freed all at once.gpa: Allocator,/// The code page the database's strings are stored in. 0 is the neutral code/// page, in which only ASCII can be stored.codepage: u16 = 0,tables: std.ArrayList(*Table) = .empty,summary: SummaryInformation = .{},/// The contents of each file the File table names, by the file's key. They/// go into the cabinets the Media table names.file_contents: std.StringArrayHashMapUnmanaged([]const u8) = .empty,/// How the cabinet for each disk is compressed, by the Media table's DiskId./// A disk missing from here has its files compressed with MSZIP.compression: std.AutoArrayHashMapUnmanaged(i32, Compression) = .empty,
/// How a cabinet's files are stored: as they are, or compressed with MSZIP/// and some amount of effort.pub const Compression = enum { none, low, medium, high, mszip,};
pub fn init(gpa: Allocator) Database { return .{ .gpa = gpa };}
/// The table called `name`, if the database has one.pub fn table(db: *const Database, name: []const u8) ?*Table { for (db.tables.items) |t| { if (std.mem.eql(u8, t.name, name)) return t; } return null;}
/// Adds an empty table with the given columns, or returns the existing one of/// that name. The columns are not copied, so they must outlive the database;/// a column list from `schema` always does.pub fn addTable(db: *Database, name: []const u8, columns: []const Column) Allocator.Error!*Table { if (db.table(name)) |t| return t; const t = try db.gpa.create(Table); t.* = .{ .name = name, .columns = columns }; try db.tables.append(db.gpa, t); return t;}
pub const ColumnType = enum { /// A 16-bit integer. short, /// A 32-bit integer. long, /// A string, held in the database's string pool. string, /// A stream of bytes, held in a stream of its own. binary,};
pub const Column = struct { name: []const u8, type: ColumnType, /// For a string, the most characters it may hold, with 0 meaning no limit. /// For the others, unused. size: u16 = 0, nullable: bool = false, localizable: bool = false, key: bool = false,
/// A column from Windows Installer's column definition format: a letter /// for the type (`s` string, `l` localizable string, `i` integer, `v` /// binary), upper case when the column may be null, then a size. `i2` and /// `i4` are the two integer widths; a string's size is its maximum length. pub fn parse(comptime name: []const u8, comptime definition: []const u8, comptime key: bool) Column { const letter = definition[0]; const size = std.fmt.parseInt(u16, definition[1..], 10) catch @compileError("bad column definition " ++ definition); const nullable = std.ascii.isUpper(letter); return switch (std.ascii.toLower(letter)) { 's' => .{ .name = name, .type = .string, .size = size, .nullable = nullable, .key = key }, 'l' => .{ .name = name, .type = .string, .size = size, .nullable = nullable, .localizable = true, .key = key }, 'i' => .{ .name = name, .type = switch (size) { 2 => .short, 4 => .long, else => @compileError("bad integer width in " ++ definition), }, .nullable = nullable, .key = key, }, 'v' => .{ .name = name, .type = .binary, .nullable = nullable, .key = key }, else => @compileError("bad column type in " ++ definition), }; }};
pub const Value = union(enum) { null, int: i32, string: []const u8, /// The contents of a binary cell's stream. stream: []const u8,
pub fn eql(a: Value, b: Value) bool { return switch (a) { .null => b == .null, .int => |x| b == .int and b.int == x, .string => |x| b == .string and std.mem.eql(u8, b.string, x), .stream => |x| b == .stream and std.mem.eql(u8, b.stream, x), }; }};
pub const Table = struct { name: []const u8, columns: []const Column, rows: std.ArrayList([]Value) = .empty,
pub fn columnIndex(t: *const Table, name: []const u8) ?usize { for (t.columns, 0..) |c, i| { if (std.mem.eql(u8, c.name, name)) return i; } return null; }
/// Adds a row. `values` are given in column order and copied; strings /// are not, so they must outlive the database. pub fn insert(t: *Table, gpa: Allocator, values: []const Value) Allocator.Error!void { std.debug.assert(values.len == t.columns.len); try t.rows.append(gpa, try gpa.dupe(Value, values)); }
/// The row whose leading key columns hold `key`, if there is one. pub fn find(t: *const Table, key: []const Value) ?[]Value { rows: for (t.rows.items) |row| { for (key, 0..) |k, i| { if (!k.eql(row[i])) continue :rows; } return row; } return null; }
/// The name of the stream holding the contents of a binary cell in /// `row`: the table's name and the row's key values, joined by periods. pub fn streamName(t: *const Table, gpa: Allocator, row: []const Value) Allocator.Error![]u8 { var name: std.Io.Writer.Allocating = .init(gpa); errdefer name.deinit(); const w = &name.writer; w.writeAll(t.name) catch return error.OutOfMemory; for (row, t.columns) |v, c| { if (!c.key) break; switch (v) { .string => |s| w.print(".{s}", .{s}) catch return error.OutOfMemory, .int => |i| w.print(".{d}", .{i}) catch return error.OutOfMemory, else => {}, } } return name.toOwnedSlice(); }
/// The value in column `column` of the row with key `key`, or null when /// there is no such row. A shorthand for tests and for single-key tables. pub fn get(t: *const Table, key: []const u8, column: []const u8) ?Value { const row = t.find(&.{.{ .string = key }}) orelse return null; return row[t.columnIndex(column) orelse return null]; }};
/// What Windows Installer reads from the summary information stream. A null/// field is left out of the stream.pub const SummaryInformation = struct { /// The code page the summary information's strings are stored in. This is /// separate from the database's code page. codepage: u16 = 1252, title: ?[]const u8 = null, subject: ?[]const u8 = null, author: ?[]const u8 = null, keywords: ?[]const u8 = null, comments: ?[]const u8 = null, /// The platform and languages, as `x64;1033`. template: ?[]const u8 = null, last_saved_by: ?[]const u8 = null, /// The package code. revision_number: ?[]const u8 = null, /// Seconds since the Unix epoch. create_time: ?i64 = null, /// Seconds since the Unix epoch. last_save_time: ?i64 = null, /// The minimum Windows Installer version, times 100. page_count: ?i32 = null, /// Source flags: 1 short names, 2 compressed, 4 administrative image, /// 8 no elevated privileges needed. word_count: ?i32 = null, character_count: ?i32 = null, creating_application: ?[]const u8 = null, security: ?i32 = null,};
test "column definitions" { const c: Column = comptime .parse("Value", "L255", false); try std.testing.expectEqual(ColumnType.string, c.type); try std.testing.expectEqual(@as(u16, 255), c.size); try std.testing.expect(c.nullable and c.localizable and !c.key); const i: Column = comptime .parse("Sequence", "i4", true); try std.testing.expectEqual(ColumnType.long, i.type); try std.testing.expect(!i.nullable and i.key);}
test "find a row by key" { var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); defer arena.deinit(); var db: Database = .init(arena.allocator()); const t = try db.addTable("Property", &.{ comptime Column.parse("Property", "s72", true), comptime Column.parse("Value", "l0", false), }); try t.insert(db.gpa, &.{ .{ .string = "A" }, .{ .string = "1" } }); try t.insert(db.gpa, &.{ .{ .string = "B" }, .{ .string = "2" } }); try std.testing.expectEqualStrings("2", t.get("B", "Value").?.string); try std.testing.expect(t.get("C", "Value") == null);}