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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The summary information stream: a property set, as [MS-OLEPS] specifies//! it, holding the properties Windows Installer reads.
const std = @import("std");const Allocator = std.mem.Allocator;
const Database = @import("../Database.zig");const codepage = @import("../codepage.zig");
/// The stream's name, which is written as it is rather than packed.pub const stream_name = [_]u16{5} ++ std.unicode.utf8ToUtf16LeStringLiteral("SummaryInformation").*;
/// FMTID_SummaryInformation, {F29F85E0-4FF9-1068-AB91-08002B27B3D9}, as it/// appears on disk.pub const fmtid = [16]u8{ 0xE0, 0x85, 0x9F, 0xF2, 0xF9, 0x4F, 0x68, 0x10, 0xAB, 0x91, 0x08, 0x00, 0x2B, 0x27, 0xB3, 0xD9 };
pub const VT_I2 = 2;pub const VT_I4 = 3;pub const VT_LPSTR = 30;pub const VT_FILETIME = 64;
/// Seconds between 1601-01-01, where a FILETIME counts from, and the Unix/// epoch.pub const filetime_epoch_offset = 11_644_473_600;
pub const Error = error{ /// A string cannot be stored in the summary information's code page. Unencodable,} || Allocator.Error;
const Value = union(enum) { i2: i16, i4: i32, string: []const u8, filetime: i64,};
/// The summary information stream for `summary`, its strings stored in its/// code page.pub fn encode(gpa: Allocator, summary: Database.SummaryInformation) Error![]u8 { return encodeOrFail(gpa, summary) catch |e| switch (e) { // Writing to memory fails only when memory runs out. error.WriteFailed => error.OutOfMemory, else => |other| other, };}
fn encodeOrFail(gpa: Allocator, summary: Database.SummaryInformation) (Error || std.Io.Writer.Error)![]u8 { var props: std.ArrayList(struct { u32, Value }) = .empty; defer props.deinit(gpa); try props.append(gpa, .{ 1, .{ .i2 = @bitCast(summary.codepage) } }); const strings = [_]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 }, }; for (strings) |s| if (s[1]) |v| try props.append(gpa, .{ s[0], .{ .string = v } }); if (summary.create_time) |t| try props.append(gpa, .{ 12, .{ .filetime = t } }); if (summary.last_save_time) |t| try props.append(gpa, .{ 13, .{ .filetime = t } }); if (summary.page_count) |v| try props.append(gpa, .{ 14, .{ .i4 = v } }); if (summary.word_count) |v| try props.append(gpa, .{ 15, .{ .i4 = v } }); if (summary.character_count) |v| try props.append(gpa, .{ 16, .{ .i4 = v } }); if (summary.creating_application) |v| try props.append(gpa, .{ 18, .{ .string = v } }); if (summary.security) |v| try props.append(gpa, .{ 19, .{ .i4 = v } });
// The values, each a multiple of four bytes, and where each begins. var values: std.Io.Writer.Allocating = .init(gpa); defer values.deinit(); const offsets = try gpa.alloc(u32, props.items.len); defer gpa.free(offsets); const table_len: u32 = @intCast(8 + 8 * props.items.len); for (props.items, 0..) |p, i| { offsets[i] = table_len + @as(u32, @intCast(values.written().len)); const w = &values.writer; switch (p[1]) { .i2 => |v| { try w.writeInt(u32, VT_I2, .little); try w.writeInt(i16, v, .little); try w.writeInt(u16, 0, .little); }, .i4 => |v| { try w.writeInt(u32, VT_I4, .little); try w.writeInt(i32, v, .little); }, .filetime => |t| { try w.writeInt(u32, VT_FILETIME, .little); const ticks: u64 = @intCast((t + filetime_epoch_offset) * 10_000_000); try w.writeInt(u64, ticks, .little); }, .string => |text| { // In the summary information's own code page. const s = codepage.encode(gpa, summary.codepage, text) catch |e| switch (e) { error.Unencodable => return error.Unencodable, error.OutOfMemory => return error.OutOfMemory, }; try w.writeInt(u32, VT_LPSTR, .little); try w.writeInt(u32, @intCast(s.len + 1), .little); try w.writeAll(s); try w.writeByte(0); try w.splatByteAll(0, std.mem.alignForward(usize, s.len + 1, 4) - (s.len + 1)); }, } }
var out: std.Io.Writer.Allocating = .init(gpa); errdefer out.deinit(); const w = &out.writer; try w.writeInt(u16, 0xFFFE, .little); // byte order try w.writeInt(u16, 0, .little); // version try w.writeInt(u32, 0x0002_0006, .little); // system identifier try w.splatByteAll(0, 16); // CLSID try w.writeInt(u32, 1, .little); // one property set try w.writeAll(&fmtid); try w.writeInt(u32, 48, .little); // where it begins try w.writeInt(u32, table_len + @as(u32, @intCast(values.written().len)), .little); try w.writeInt(u32, @intCast(props.items.len), .little); for (props.items, offsets) |p, offset| { try w.writeInt(u32, p[0], .little); try w.writeInt(u32, offset, .little); } try w.writeAll(values.written()); return out.toOwnedSlice();}
test "a codepage and a string" { const bytes = try encode(std.testing.allocator, .{ .codepage = 1252, .title = "Hi" }); defer std.testing.allocator.free(bytes); // 48 bytes of stream header, 8 of section header, 16 of property table, // 8 for the code page and 12 for the string. try std.testing.expectEqual(@as(usize, 48 + 8 + 16 + 8 + 12), bytes.len); try std.testing.expectEqual(@as(u32, 44), std.mem.readInt(u32, bytes[48..52], .little)); try std.testing.expectEqual(@as(u16, 1252), std.mem.readInt(u16, bytes[48 + 24 + 4 ..][0..2], .little)); try std.testing.expectEqualStrings("Hi\x00", bytes[48 + 24 + 8 + 8 ..][0..3]);}