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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Preprocessing: turning WiX source text into plain XML.//!//! The preprocessor works on the text rather than on parsed XML. It carries//! out its directives and removes them://!//! - `<?define?>`, `<?undef?>`, `<?warning?>` and `<?error?>`;//! - `<?if?>`, `<?ifdef?>` and `<?ifndef?>`, with `<?elseif?>`, `<?else?>`//! and `<?endif?>`, keeping only the branch whose condition holds;//! - `<?include?>`, which splices in the children of another file's//! `<Include>` element;//! - `<?foreach?>` … `<?endforeach?>`, which repeats what it encloses once//! for each item of a semicolon-separated list.//!//! It replaces each variable reference, `$(var.NAME)`, `$(env.NAME)` or//! `$(sys.NAME)`, with its value, escaped for where it lands; everything//! else is copied exactly as it was written. Alongside the text it keeps a//! source map, so that a problem found in the output is still reported at//! the line and column of the file it came from, and the chain of includes//! that led there.
const std = @import("std");const Allocator = std.mem.Allocator;
const Diagnostics = @import("Diagnostics.zig");const Location = Diagnostics.Location;const FileSource = @import("FileSource.zig");const Arch = @import("link.zig").Arch;
pub const Error = error{ /// The WiX source has errors, which are in the diagnostics. Invalid,} || Allocator.Error;
/// A WiX source and the path it was read from.pub const Source = struct { path: []const u8, text: []const u8,};
/// A variable defined before preprocessing starts, as `-D NAME=VALUE` does.pub const Define = struct { name: []const u8, value: []const u8,};
/// Where `$(env.NAME)` is looked up. The library never reads the process's/// environment itself.pub const Environment = struct { context: ?*const anyopaque = null, lookupFn: *const fn (context: ?*const anyopaque, name: []const u8) ?[]const u8 = lookupNothing,
/// An environment in which nothing is defined. pub const empty: Environment = .{};
pub fn lookup(e: Environment, name: []const u8) ?[]const u8 { return e.lookupFn(e.context, name); }
fn lookupNothing(_: ?*const anyopaque, _: []const u8) ?[]const u8 { return null; }
/// The variables in `map`. pub fn fromMap(map: *const std.process.Environ.Map) Environment { return .{ .context = map, .lookupFn = lookupMap }; }
fn lookupMap(context: ?*const anyopaque, name: []const u8) ?[]const u8 { const map: *const std.process.Environ.Map = @ptrCast(@alignCast(context.?)); return map.get(name); }};
pub const Options = struct { defines: []const Define = &.{}, environment: Environment = .empty, /// Where `<?include?>` reads files from. Without one, nothing can be /// included. files: ?FileSource = null, /// Directories searched for an included file not found beside the file /// that includes it, in order. include_dirs: []const []const u8 = &.{}, /// The directory the build runs in, for `$(sys.CURRENTDIR)` and for /// making `$(sys.SOURCEFILEPATH)` absolute. current_dir: []const u8 = "", arch: Arch = .x64,};
/// Where each part of the preprocessed text came from.pub const SourceMap = struct { segments: std.ArrayList(Segment) = .empty,
pub const Segment = struct { /// Where the segment begins in the preprocessed text. output: usize, path: []const u8, /// The text of the file it came from, for counting lines. text: []const u8, /// Where the segment begins in that file. input: usize, /// True when the segment was copied as it was written, so that each /// offset into it maps to the same offset in the file. A substituted /// value maps, all of it, to the reference it replaced. copied: bool, /// The `<?include?>` that brought the file in, if it was included. included_from: ?*const Location, };
/// The location in a WiX source of an offset into the preprocessed text. pub fn locate(m: *const SourceMap, offset: usize) Location { const segs = m.segments.items; if (segs.len == 0) return .{ .path = "", .line = 1, .column = 1 }; var lo: usize = 0; var hi: usize = segs.len; while (hi - lo > 1) { const mid = (lo + hi) / 2; if (segs[mid].output <= offset) lo = mid else hi = mid; } const s = segs[lo]; const into = if (s.copied) offset -| s.output else 0; var loc: Location = .at(s.path, s.text, s.input + into); loc.included_from = s.included_from; return loc; }};
pub const Output = struct { text: []const u8, map: SourceMap,};
/// How deep includes may nest, which only a mistake would ever reach.const max_include_depth = 64;
/// The most text preprocessing may produce. Includes and loops can each/// repeat what they hold, and together they can repeat it past any machine's/// memory; this stops that with an error instead.pub const max_output = 64 << 20;
/// Preprocesses `source`, allocating the result with `arena` and adding what/// is wrong with it to `diags`.pub fn preprocess(arena: Allocator, diags: *Diagnostics, source: Source, options: Options) Error!Output { var p: Preprocessor = .{ .arena = arena, .diags = diags, .options = options, // A source in the WiX v4 namespace has WiX v4's system variables. .v4 = std.mem.find(u8, source.text, "http://wixtoolset.org/schemas/v4/wxs") != null, }; for (options.defines) |d| try p.variables.put(arena, d.name, d.value); const file: File = .{ .path = source.path, .text = source.text, .included_from = null, .depth = 0 }; p.process(&file, 0, source.text.len) catch |e| switch (e) { error.Stop => {}, error.OutOfMemory => return error.OutOfMemory, }; if (diags.hasErrors()) return error.Invalid; return .{ .text = p.out.items, .map = p.map };}
/// A file being preprocessed: the WiX source, or one it includes.const File = struct { path: []const u8, text: []const u8, included_from: ?*const Location, depth: usize, /// The file that included this one, to catch a file including itself. parent: ?*const File = null,
fn location(f: *const File, offset: usize) Location { var loc: Location = .at(f.path, f.text, offset); loc.included_from = f.included_from; return loc; }};
/// A processing instruction: where it begins and ends, its name, and its/// arguments.const Instruction = struct { start: usize, end: usize, name: []const u8, args: []const u8,};
const Preprocessor = struct { arena: Allocator, diags: *Diagnostics, options: Options, variables: std.StringHashMapUnmanaged([]const u8) = .empty, out: std.ArrayList(u8) = .empty, map: SourceMap = .{}, v4: bool = false,
/// Raised when nothing more can be read sensibly, such as at an /// instruction that is never closed. const Stop = error{Stop} || Allocator.Error;
/// Where a reference to a variable sits, which decides how its value is /// escaped. const Context = enum { text, /// Inside a tag, outside any quotes. tag, attribute_single, attribute_double, };
/// Preprocesses `file.text[start..end]` into the output. fn process(p: *Preprocessor, file: *const File, start: usize, end: usize) Stop!void { const text = file.text[0..end]; var context: Context = .text; // The start of the run of text being copied as it is. var run_start = start; var i = start; while (i < end) { const rest = text[i..]; if (context == .text and std.mem.startsWith(u8, rest, "<!--")) { i = endOf(text, i, "-->"); } else if (context == .text and std.mem.startsWith(u8, rest, "<![CDATA[")) { i = endOf(text, i, "]]>"); } else if (context == .text and std.mem.startsWith(u8, rest, "<?")) { const pi = try p.instruction(file, text, i); if (!isDirective(pi.name)) { // The XML declaration, or an instruction for something // after the preprocessor, which compiling will judge. i = pi.end; continue; } try p.copy(file, run_start, i); i = try p.directive(file, text, pi); run_start = i; } else if (std.mem.startsWith(u8, rest, "$$(")) { // An escaped reference, written out as `$(`. try p.copy(file, run_start, i + 1); i += 2; run_start = i; } else if (std.mem.startsWith(u8, rest, "$(")) { const close = std.mem.findScalarPos(u8, text, i + 2, ')') orelse { try p.diags.err(file.location(i), "a variable reference is not closed with ')'", .{}); return error.Stop; }; try p.copy(file, run_start, i); const value = try p.reference(file, i, text[i + 2 .. close]); try p.substitute(file, i, value, context); i = close + 1; run_start = i; } else { const c = text[i]; context = switch (context) { .text => if (c == '<') .tag else .text, .tag => switch (c) { '>' => .text, '"' => .attribute_double, '\'' => .attribute_single, else => .tag, }, .attribute_double => if (c == '"') .tag else .attribute_double, .attribute_single => if (c == '\'') .tag else .attribute_single, }; i += 1; } } try p.copy(file, run_start, end); }
/// The processing instruction beginning at `at`, which must start `<?`. fn instruction(p: *Preprocessor, file: *const File, text: []const u8, at: usize) Stop!Instruction { const close = std.mem.findPos(u8, text, at + 2, "?>") orelse { try p.diags.err(file.location(at), "a processing instruction is not closed", .{}); return error.Stop; }; const body = text[at + 2 .. close]; const name_end = std.mem.findAny(u8, body, " \t\r\n") orelse body.len; return .{ .start = at, .end = close + 2, .name = body[0..name_end], .args = std.mem.trim(u8, body[name_end..], " \t\r\n"), }; }
/// Just past the first `terminator` after `start`, or the end of `text`. fn endOf(text: []const u8, start: usize, terminator: []const u8) usize { const at = std.mem.findPos(u8, text, start, terminator) orelse return text.len; return at + terminator.len; }
const directives = [_][]const u8{ "define", "undef", "warning", "error", "if", "ifdef", "ifndef", "elseif", "else", "endif", "include", "foreach", "endforeach", };
fn isDirective(name: []const u8) bool { for (directives) |d| if (std.mem.eql(u8, name, d)) return true; return false; }
fn grow(p: *Preprocessor, file: *const File, at: usize, n: usize) Stop!void { if (p.out.items.len + n <= max_output) return; try p.diags.err(file.location(at), "preprocessing produces more than {d} MiB, which only includes or loops repeating without end would", .{max_output >> 20}); return error.Stop; }
/// Copies `file.text[from..to]` to the output as it is. fn copy(p: *Preprocessor, file: *const File, from: usize, to: usize) Stop!void { if (to <= from) return; try p.grow(file, from, to - from); try p.map.segments.append(p.arena, .{ .output = p.out.items.len, .path = file.path, .text = file.text, .input = from, .copied = true, .included_from = file.included_from, }); try p.out.appendSlice(p.arena, file.text[from..to]); }
/// Writes `value` in place of the reference at `at`, escaped so that it /// stays the text it is wherever it lands. fn substitute(p: *Preprocessor, file: *const File, at: usize, value: []const u8, context: Context) Stop!void { try p.grow(file, at, value.len * 6); try p.map.segments.append(p.arena, .{ .output = p.out.items.len, .path = file.path, .text = file.text, .input = at, .copied = false, .included_from = file.included_from, }); for (value) |c| { const entity: ?[]const u8 = switch (c) { '&' => "&", '<' => "<", '>' => if (context == .text) ">" else null, '"' => if (context == .attribute_double) """ else null, '\'' => if (context == .attribute_single) "'" else null, else => null, }; if (entity) |e| try p.out.appendSlice(p.arena, e) else try p.out.append(p.arena, c); } }
/// The value of the reference `$(inner)` at `at`, or "" after reporting /// why it has none. fn reference(p: *Preprocessor, file: *const File, at: usize, inner: []const u8) Allocator.Error![]const u8 { return (try p.lookup(file, at, inner, true)) orelse ""; }
/// The value of `$(inner)`, or null when it has none; that is reported /// when `required`, and also when the reference itself is malformed. fn lookup(p: *Preprocessor, file: *const File, at: usize, inner: []const u8, required: bool) Allocator.Error!?[]const u8 { const here = file.location(at); const dot = std.mem.findScalar(u8, inner, '.') orelse { try p.diags.err(here, "$({s}) names no kind of variable; write $(var.{s}), $(env.{s}) or $(sys.{s})", .{ inner, inner, inner, inner }); return null; }; const kind = inner[0..dot]; const name = inner[dot + 1 ..]; if (std.mem.eql(u8, kind, "var")) { if (p.variables.get(name)) |v| return v; if (required) try p.diags.err(here, "the variable $(var.{s}) is not defined", .{name}); return null; } else if (std.mem.eql(u8, kind, "env")) { if (p.options.environment.lookup(name)) |v| return v; if (required) try p.diags.err(here, "the environment variable $(env.{s}) is not set", .{name}); return null; } else if (std.mem.eql(u8, kind, "sys")) { return try p.system(file, here, name); } try p.diags.err(here, "$({s}): \"{s}\" is not a kind of variable; the kinds are var, env and sys", .{ inner, kind }); return null; }
fn system(p: *Preprocessor, file: *const File, here: Location, name: []const u8) Allocator.Error!?[]const u8 { // The directories end with a separator, as WiX documents. if (std.mem.eql(u8, name, "CURRENTDIR")) return try p.directory(p.options.current_dir); if (std.mem.eql(u8, name, "SOURCEFILEPATH")) return try p.absolute(file.path); if (std.mem.eql(u8, name, "SOURCEFILEDIR")) { const path = try p.absolute(file.path); return try p.directory(std.fs.path.dirname(path) orelse ""); } if (std.mem.eql(u8, name, "BUILDARCH")) return @tagName(p.options.arch); if (p.v4) { // WiX v4's, as its preprocessor documentation lists them. if (std.mem.eql(u8, name, "BUILDARCHSHORT")) return switch (p.options.arch) { .x86 => "X86", .x64 => "X64", .arm64 => "A64", }; if (std.mem.eql(u8, name, "WIXMAJORVERSION")) return "4"; try p.diags.err(here, "$(sys.{s}) is not a system variable; they are CURRENTDIR, SOURCEFILEDIR, SOURCEFILEPATH, BUILDARCH, BUILDARCHSHORT and WIXMAJORVERSION", .{name}); return null; } try p.diags.err(here, "$(sys.{s}) is not a system variable; they are CURRENTDIR, SOURCEFILEDIR, SOURCEFILEPATH and BUILDARCH", .{name}); return null; }
fn directory(p: *Preprocessor, path: []const u8) Allocator.Error![]const u8 { if (path.len == 0 or std.fs.path.isSep(path[path.len - 1])) return path; return std.fmt.allocPrint(p.arena, "{s}{c}", .{ path, std.fs.path.sep }); }
fn absolute(p: *Preprocessor, path: []const u8) Allocator.Error![]const u8 { if (std.fs.path.isAbsolute(path) or p.options.current_dir.len == 0) return path; return std.fs.path.join(p.arena, &.{ p.options.current_dir, path }); }
/// Expands every reference in `text`, as a directive's arguments are /// expanded, without escaping: the value is text, not XML. fn expand(p: *Preprocessor, file: *const File, at: usize, text: []const u8) Allocator.Error![]const u8 { var out: std.ArrayList(u8) = .empty; var i: usize = 0; while (i < text.len) { if (std.mem.startsWith(u8, text[i..], "$$(")) { try out.appendSlice(p.arena, "$("); i += 3; } else if (std.mem.startsWith(u8, text[i..], "$(")) { const close = std.mem.findScalarPos(u8, text, i + 2, ')') orelse { try p.diags.err(file.location(at), "a variable reference is not closed with ')'", .{}); return out.items; }; try out.appendSlice(p.arena, try p.reference(file, at, text[i + 2 .. close])); i = close + 1; } else { try out.append(p.arena, text[i]); i += 1; } } return out.items; }
/// Carries out the directive `pi`, returning where reading goes on. fn directive(p: *Preprocessor, file: *const File, text: []const u8, pi: Instruction) Stop!usize { const here = file.location(pi.start); const name = pi.name; if (std.mem.eql(u8, name, "define")) { try p.define(file, pi); } else if (std.mem.eql(u8, name, "undef")) { if (!p.variables.remove(pi.args)) { try p.diags.err(here, "<?undef {s}?>: the variable is not defined", .{pi.args}); } } else if (std.mem.eql(u8, name, "warning")) { try p.diags.warn(here, "{s}", .{try p.expand(file, pi.start, pi.args)}); } else if (std.mem.eql(u8, name, "error")) { try p.diags.err(here, "{s}", .{try p.expand(file, pi.start, pi.args)}); } else if (std.mem.eql(u8, name, "if") or std.mem.eql(u8, name, "ifdef") or std.mem.eql(u8, name, "ifndef")) { return p.conditional(file, text, pi); } else if (std.mem.eql(u8, name, "foreach")) { return p.foreach(file, text, pi); } else if (std.mem.eql(u8, name, "include")) { try p.include(file, pi); } else { // elseif, else, endif or endforeach, with nothing open to close. try p.diags.err(here, "<?{s}?> has no <?{s}?> before it", .{ name, if (std.mem.eql(u8, name, "endforeach")) "foreach" else "if" }); } return pi.end; }
fn define(p: *Preprocessor, file: *const File, pi: Instruction) Allocator.Error!void { const here = file.location(pi.start); // `<?define NAME?>` defines the variable with no value, which is // what `<?ifdef?>` asks about. const eq = std.mem.findScalar(u8, pi.args, '='); const variable = std.mem.trim(u8, if (eq) |e| pi.args[0..e] else pi.args, " \t\r\n"); if (!isName(variable)) { try p.diags.err(here, "\"{s}\" is not a variable name", .{variable}); return; } var value: []const u8 = ""; if (eq) |e| { value = std.mem.trim(u8, pi.args[e + 1 ..], " \t\r\n"); if (value.len >= 2 and value[0] == '"' and value[value.len - 1] == '"') value = value[1 .. value.len - 1]; value = try p.expand(file, pi.start, value); } try p.variables.put(p.arena, variable, value); }
fn isName(s: []const u8) bool { if (s.len == 0) return false; for (s) |c| if (!(std.ascii.isAlphanumeric(c) or c == '_' or c == '.')) return false; return !std.ascii.isDigit(s[0]); }
// -- conditionals and loops --------------------------------------------
/// The instructions that divide a conditional or a loop opened by /// `opening`, found by reading on and counting what else opens and /// closes, ending with the one that closes it. fn structure(p: *Preprocessor, file: *const File, text: []const u8, opening: Instruction, comptime loop: bool) Stop![]Instruction { var parts: std.ArrayList(Instruction) = .empty; var depth: usize = 0; var i = opening.end; while (i < text.len) { const rest = text[i..]; if (std.mem.startsWith(u8, rest, "<!--")) { i = endOf(text, i, "-->"); } else if (std.mem.startsWith(u8, rest, "<![CDATA[")) { i = endOf(text, i, "]]>"); } else if (std.mem.startsWith(u8, rest, "<?")) { const pi = try p.instruction(file, text, i); i = pi.end; const opens = if (loop) eql(pi.name, "foreach") else eql(pi.name, "if") or eql(pi.name, "ifdef") or eql(pi.name, "ifndef"); const closes = eql(pi.name, if (loop) "endforeach" else "endif"); if (opens) { depth += 1; } else if (closes and depth > 0) { depth -= 1; } else if (closes) { try parts.append(p.arena, pi); return parts.items; } else if (!loop and depth == 0 and (eql(pi.name, "elseif") or eql(pi.name, "else"))) { try parts.append(p.arena, pi); } } else { i += 1; } } try p.diags.err(file.location(opening.start), "<?{s}?> is never closed with <?{s}?>", .{ opening.name, if (loop) "endforeach" else "endif" }); return error.Stop; }
fn conditional(p: *Preprocessor, file: *const File, text: []const u8, opening: Instruction) Stop!usize { const parts = try p.structure(file, text, opening, false); for (parts[0 .. parts.len - 1], 0..) |part, k| { if (eql(part.name, "else") and k + 1 < parts.len - 1) { try p.diags.err(file.location(part.start), "<?else?> must be the last branch before <?endif?>", .{}); return parts[parts.len - 1].end; } } // Each branch is its condition and the text up to the next part. var condition = opening; var body_start = opening.end; for (parts) |part| { if (try p.holds(file, condition)) { try p.process(file, body_start, part.start); break; } condition = part; body_start = part.end; } return parts[parts.len - 1].end; }
/// Whether the branch opened by `pi` is the one to keep. fn holds(p: *Preprocessor, file: *const File, pi: Instruction) Stop!bool { if (eql(pi.name, "else")) return true; if (eql(pi.name, "ifdef")) return p.variables.contains(pi.args); if (eql(pi.name, "ifndef")) return !p.variables.contains(pi.args); var e: Expression = .{ .p = p, .file = file, .at = pi.start, .text = pi.args }; return e.evaluate() catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.Invalid => false, }; }
fn foreach(p: *Preprocessor, file: *const File, text: []const u8, opening: Instruction) Stop!usize { const parts = try p.structure(file, text, opening, true); const close = parts[parts.len - 1]; const here = file.location(opening.start); // <?foreach NAME in A;B;C?> var words = std.mem.tokenizeAny(u8, opening.args, " \t\r\n"); const variable = words.next() orelse ""; const in = words.next() orelse ""; if (!isName(variable) or !eql(in, "in")) { try p.diags.err(here, "<?foreach?> is written <?foreach NAME in A;B;C?>", .{}); return close.end; } const list = try p.expand(file, opening.start, std.mem.trim(u8, words.rest(), " \t\r\n")); var items = std.mem.splitScalar(u8, list, ';'); while (items.next()) |raw| { const item = std.mem.trim(u8, raw, " \t\r\n"); if (item.len == 0) continue; // Each pass works on a copy of the variables, so that nothing // defined inside the loop outlives it. const saved = try p.variables.clone(p.arena); try p.variables.put(p.arena, variable, item); try p.process(file, opening.end, close.start); p.variables = saved; } return close.end; }
// -- includes ---------------------------------------------------------
fn include(p: *Preprocessor, file: *const File, pi: Instruction) Stop!void { const here = file.location(pi.start); const files = p.options.files orelse { try p.diags.err(here, "<?include?> needs files to read, and none were given", .{}); return; }; var wanted = try p.expand(file, pi.start, pi.args); if (wanted.len >= 2 and wanted[0] == '"' and wanted[wanted.len - 1] == '"') wanted = wanted[1 .. wanted.len - 1]; const normalized = try p.arena.dupe(u8, wanted); std.mem.replaceScalar(u8, normalized, '\\', '/'); if (normalized.len == 0) { try p.diags.err(here, "<?include?> names no file", .{}); return; }
if (file.depth >= max_include_depth) { try p.diags.err(here, "includes nest more than {d} deep", .{max_include_depth}); return error.Stop; }
// Beside the including file first, then in each include directory. var candidates: std.ArrayList([]const u8) = .empty; const dir = std.fs.path.dirnamePosix(slashes(p.arena, file.path) catch file.path); try candidates.append(p.arena, if (dir) |d| try std.fmt.allocPrint(p.arena, "{s}/{s}", .{ d, normalized }) else normalized); for (p.options.include_dirs) |d| { try candidates.append(p.arena, try std.fmt.allocPrint(p.arena, "{s}/{s}", .{ std.mem.trimEnd(u8, try slashes(p.arena, d), "/"), normalized })); } var found: ?struct { []const u8, []const u8 } = null; for (candidates.items) |path| { const contents = files.read(p.arena, path) catch |e| switch (e) { error.FileNotFound => continue, error.OutOfMemory => return error.OutOfMemory, error.ReadFailed => { try p.diags.err(here, "the included file {s} cannot be read", .{path}); return; }, }; found = .{ path, contents }; break; } const path, const contents = found orelse { try p.diags.err(here, "the included file \"{s}\" is not beside {s} nor in any include directory", .{ wanted, file.path }); return; };
var ancestor: ?*const File = file; while (ancestor) |a| : (ancestor = a.parent) { if (eql(a.path, path)) { try p.diags.err(here, "{s} includes itself", .{path}); return; } }
const from = try p.arena.create(Location); from.* = here; const child: File = .{ .path = path, .text = contents, .included_from = from, .depth = file.depth + 1, .parent = file, }; const start, const end = (try p.includeBody(&child)) orelse return; try p.process(&child, start, end); }
/// Where the children of an included file's `<Include>` element begin /// and end, or null after reporting that it has none. fn includeBody(p: *Preprocessor, file: *const File) Stop!?struct { usize, usize } { const text = file.text; var i: usize = 0; // Past the XML declaration, comments and whitespace to the root. while (i < text.len) { if (std.mem.startsWith(u8, text[i..], "<?")) { i = endOf(text, i, "?>"); } else if (std.mem.startsWith(u8, text[i..], "<!--")) { i = endOf(text, i, "-->"); } else if (std.ascii.isWhitespace(text[i])) { i += 1; } else break; } const is_include = std.mem.startsWith(u8, text[i..], "<Include") and i + 8 < text.len and (text[i + 8] == '>' or text[i + 8] == '/' or std.ascii.isWhitespace(text[i + 8])); if (!is_include) { try p.diags.err(file.location(i), "an included file's root element must be <Include>", .{}); return null; } const open_end = std.mem.findScalarPos(u8, text, i, '>') orelse { try p.diags.err(file.location(i), "the <Include> element is not closed", .{}); return null; }; if (text[open_end - 1] == '/') return .{ open_end + 1, open_end + 1 }; const close = std.mem.findLast(u8, text, "</Include") orelse { try p.diags.err(file.location(i), "the <Include> element is not closed", .{}); return null; }; return .{ open_end + 1, close }; }
fn slashes(arena: Allocator, path: []const u8) Allocator.Error![]const u8 { const out = try arena.dupe(u8, path); std.mem.replaceScalar(u8, out, '\\', '/'); return out; }};
fn eql(a: []const u8, b: []const u8) bool { return std.mem.eql(u8, a, b);}
/// The condition of an `<?if?>` or `<?elseif?>`, as WiX documents it:/// read left to right, with `and` and `or` alike in precedence, below/// `not`, below the comparisons. `=`, `!=` and `~=` (which ignores case)/// compare strings; `<`, `<=`, `>` and `>=` compare integers. A literal may/// be quoted or not; a variable on its own asks whether it is defined.const Expression = struct { p: *Preprocessor, file: *const File, /// Where the directive is, for diagnostics. at: usize, text: []const u8, pos: usize = 0,
const EvalError = error{Invalid} || Allocator.Error;
const Token = union(enum) { open, close, op: []const u8, @"and", @"or", not, literal: []const u8, /// The text between `$(` and `)`. variable: []const u8, end, };
fn evaluate(e: *Expression) EvalError!bool { const v = try e.disjunction(); if (try e.peek() != .end) return e.fail("unexpected text after the condition", .{}); return v; }
fn fail(e: *Expression, comptime fmt: []const u8, args: anytype) EvalError { e.p.diags.err(e.file.location(e.at), "<?if {s}?>: " ++ fmt, .{e.text} ++ args) catch return error.OutOfMemory; return error.Invalid; }
/// `and` and `or`, left to right. fn disjunction(e: *Expression) EvalError!bool { var v = try e.negation(); while (true) { const t = try e.peek(); switch (t) { .@"and" => { _ = try e.next(); const rhs = try e.negation(); v = v and rhs; }, .@"or" => { _ = try e.next(); const rhs = try e.negation(); v = v or rhs; }, else => return v, } } }
fn negation(e: *Expression) EvalError!bool { if (try e.peek() == .not) { _ = try e.next(); return !(try e.negation()); } return e.primary(); }
fn primary(e: *Expression) EvalError!bool { const t = try e.next(); switch (t) { .open => { const v = try e.disjunction(); if (try e.next() != .close) return e.fail("a '(' is not closed", .{}); return v; }, .literal, .variable => { const following = try e.peek(); if (following != .op) { if (t == .variable) return (try e.p.lookup(e.file, e.at, t.variable, false)) != null; return e.fail("\"{s}\" is not a condition", .{t.literal}); } const op = (try e.next()).op; const rhs = try e.next(); if (rhs != .literal and rhs != .variable) return e.fail("{s} needs something to compare with", .{op}); return e.compare(try e.value(t), op, try e.value(rhs)); }, .end => return e.fail("a condition is missing", .{}), else => return e.fail("a condition cannot begin there", .{}), } }
fn value(e: *Expression, t: Token) EvalError![]const u8 { return switch (t) { .literal => |l| l, .variable => |v| (try e.p.lookup(e.file, e.at, v, true)) orelse return error.Invalid, else => unreachable, }; }
fn compare(e: *Expression, a: []const u8, op: []const u8, b: []const u8) EvalError!bool { if (eql(op, "=")) return eql(a, b); if (eql(op, "!=")) return !eql(a, b); if (eql(op, "~=")) return std.ascii.eqlIgnoreCase(a, b); const x = std.fmt.parseInt(i64, std.mem.trim(u8, a, " "), 10) catch return e.fail("{s} compares integers, and \"{s}\" is not one", .{ op, a }); const y = std.fmt.parseInt(i64, std.mem.trim(u8, b, " "), 10) catch return e.fail("{s} compares integers, and \"{s}\" is not one", .{ op, b }); if (eql(op, "<")) return x < y; if (eql(op, "<=")) return x <= y; if (eql(op, ">")) return x > y; return x >= y; }
fn peek(e: *Expression) EvalError!Token { const saved = e.pos; defer e.pos = saved; return e.next(); }
fn next(e: *Expression) EvalError!Token { const s = e.text; while (e.pos < s.len and std.ascii.isWhitespace(s[e.pos])) e.pos += 1; if (e.pos == s.len) return .end; const c = s[e.pos]; switch (c) { '(' => { e.pos += 1; return .open; }, ')' => { e.pos += 1; return .close; }, '"' => { const close = std.mem.findScalarPos(u8, s, e.pos + 1, '"') orelse return e.fail("a quoted literal is not closed", .{}); defer e.pos = close + 1; return .{ .literal = s[e.pos + 1 .. close] }; }, '=', '!', '~', '<', '>' => { for ([_][]const u8{ "!=", "~=", "<=", ">=", "=", "<", ">" }) |op| { if (std.mem.startsWith(u8, s[e.pos..], op)) { e.pos += op.len; return .{ .op = op }; } } return e.fail("'{c}' is not an operator", .{c}); }, '$' => if (std.mem.startsWith(u8, s[e.pos..], "$(")) { const close = std.mem.findScalarPos(u8, s, e.pos + 2, ')') orelse return e.fail("a variable reference is not closed", .{}); defer e.pos = close + 1; return .{ .variable = s[e.pos + 2 .. close] }; }, else => {}, } const start = e.pos; while (e.pos < s.len) : (e.pos += 1) { const d = s[e.pos]; if (std.ascii.isWhitespace(d) or std.mem.findScalar(u8, "()\"=!~<>", d) != null) break; } const word = s[start..e.pos]; if (std.ascii.eqlIgnoreCase(word, "and")) return .@"and"; if (std.ascii.eqlIgnoreCase(word, "or")) return .@"or"; if (std.ascii.eqlIgnoreCase(word, "not")) return .not; return .{ .literal = word }; }};