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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Reading the summary information stream, a property set as [MS-OLEPS]//! specifies it, back into the properties Windows Installer reads.
const std = @import("std");const Allocator = std.mem.Allocator;
const Database = @import("../Database.zig");const codepage = @import("../codepage.zig");const written = @import("../write/summary.zig");
pub const Error = error{ /// The stream is not a property set holding summary information. Corrupt,} || Allocator.Error;
const Property = union(enum) { int: i32, /// As stored, in the summary information's code page. string: []const u8, /// Seconds since the Unix epoch. time: i64,};
/// The summary information `bytes` hold. Properties of a type Windows/// Installer does not give them, and properties it does not read, are/// passed over.pub fn decode(arena: Allocator, bytes: []const u8) Error!Database.SummaryInformation { if (bytes.len < 48 or int(u16, bytes, 0) != 0xFFFE) return error.Corrupt; const sets = int(u32, bytes, 24); // The section holding summary information, among however many there are. const section = for (0..@min(sets, 64)) |i| { const at = 28 + 20 * i; if (at + 20 > bytes.len) return error.Corrupt; if (std.mem.eql(u8, bytes[at..][0..16], &written.fmtid)) break int(u32, bytes, at + 16); } else return error.Corrupt; if (section > bytes.len -| 8) return error.Corrupt; const body = bytes[section..]; const count = int(u32, body, 4); if (count > (body.len - 8) / 8) return error.Corrupt;
var summary: Database.SummaryInformation = .{}; var strings: [20]?[]const u8 = @splat(null); for (0..count) |i| { const id = int(u32, body, 8 + 8 * i); const offset = int(u32, body, 12 + 8 * i); const property = try value(body, offset) orelse continue; switch (id) { 1 => if (property == .int) { summary.codepage = @bitCast(@as(i16, @truncate(property.int))); }, 2...9, 18 => if (property == .string) { strings[id] = property.string; }, 12 => if (property == .time) { summary.create_time = property.time; }, 13 => if (property == .time) { summary.last_save_time = property.time; }, 14 => if (property == .int) { summary.page_count = property.int; }, 15 => if (property == .int) { summary.word_count = property.int; }, 16 => if (property == .int) { summary.character_count = property.int; }, 19 => if (property == .int) { summary.security = property.int; }, else => {}, } }
// The strings, once the code page they are in is known. const fields = [_]struct { u32, *?[]const u8 }{ .{ 2, &summary.title }, .{ 3, &summary.subject }, .{ 4, &summary.author }, .{ 5, &summary.keywords }, .{ 6, &summary.comments }, .{ 7, &summary.template }, .{ 8, &summary.last_saved_by }, .{ 9, &summary.revision_number }, .{ 18, &summary.creating_application }, }; for (fields) |f| { if (strings[f[0]]) |s| f[1].* = try codepage.decode(arena, summary.codepage, s, null); } return summary;}
/// The property at `offset` in the section `body`, or null for a type/// Windows Installer does not use.fn value(body: []const u8, offset: u32) Error!?Property { if (offset > body.len -| 8) return error.Corrupt; const v = body[offset..]; return switch (int(u32, v, 0) & 0xFFFF) { written.VT_I2 => .{ .int = int(i16, v, 4) }, written.VT_I4 => .{ .int = int(i32, v, 4) }, written.VT_LPSTR => { const len = int(u32, v, 4); if (len > v.len - 8) return error.Corrupt; // The length counts a terminating NUL, which is not the string's. const s = v[8..][0..len]; return .{ .string = s[0 .. std.mem.findScalar(u8, s, 0) orelse s.len] }; }, written.VT_FILETIME => { if (v.len < 12) return error.Corrupt; const ticks = int(u64, v, 4); return .{ .time = @as(i64, @intCast(ticks / 10_000_000)) - written.filetime_epoch_offset }; }, else => null, };}
fn int(comptime T: type, bytes: []const u8, offset: usize) T { return std.mem.readInt(T, bytes[offset..][0..@sizeOf(T)], .little);}
test "what is written reads back" { var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator(); const summary: Database.SummaryInformation = .{ .codepage = 1251, .title = "Installation Database", .subject = "Привет", .template = "x64;1033", .revision_number = "{BBBBBBBB-CCCC-DDDD-EEEE-FFFFFFFFFFFF}", .create_time = 1_700_000_000, .last_save_time = 315_532_800, .page_count = 500, .word_count = 2, .creating_application = "zixl", .security = 2, }; const got = try decode(a, try written.encode(a, summary)); inline for (@typeInfo(Database.SummaryInformation).@"struct".field_names) |name| { const want = @field(summary, name); const have = @field(got, name); switch (@TypeOf(want)) { ?[]const u8 => if (want) |w| try std.testing.expectEqualStrings(w, have.?) else try std.testing.expect(have == null), else => try std.testing.expectEqual(want, have), } }}
test "what is not summary information" { var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); defer arena.deinit(); try std.testing.expectError(error.Corrupt, decode(arena.allocator(), "short")); const bytes = try written.encode(arena.allocator(), .{ .title = "x" }); try std.testing.expectError(error.Corrupt, decode(arena.allocator(), bytes[0 .. bytes.len - 6]));}