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.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Compiling: turning a preprocessed WiX source into a WiX document.//!//! Every element and attribute zixl does not implement is an error, never a//! warning, so that an installer package is never quietly missing something//! its WiX source asked for.
const std = @import("std");const Allocator = std.mem.Allocator;const xml = @import("zxml");const UUID = @import("uuid").UUID;
const Diagnostics = @import("Diagnostics.zig");const Location = Diagnostics.Location;const FileSource = @import("FileSource.zig");const SourceMap = @import("preprocess.zig").SourceMap;const codepage_ = @import("codepage.zig");const wix = @import("wix.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 preprocessed WiX source and the path it was read from, for diagnostics.pub const Source = struct { path: []const u8, text: []const u8, /// Where each part of `text` came from, when it was preprocessed; /// without one, offsets into `text` are offsets into the file at `path`. map: ?*const SourceMap = null,};
/// Compiles `sources` into one WiX document allocated with `arena`, adding/// what is wrong with them to `diags`. The files their source paths name are/// read through `files`. `arch` is the architecture the package is for,/// which decides what a WiX v4 `...6432Folder` stands for.////// Each source holds sections -- a <Product>, or <Fragment>s -- and the/// document is the one product's section with every fragment it refers to,/// directly or through other fragments. A fragment nothing refers to/// contributes nothing.pub fn compile(arena: Allocator, diags: *Diagnostics, files: FileSource, sources: []const Source, arch: Arch) Error!wix.Document { var sections: std.ArrayList(*Section) = .empty; var text: std.ArrayList(Text) = .empty; // The first source's language, which every other must share. var first: ?struct { wix.Language, Location } = null; for (sources) |source| { var c: Compiler = .{ .arena = arena, .diags = diags, .files = files, .source = source, .reader = .init(source.text), .text = &text, }; c.document(§ions) catch |e| switch (e) { error.Stop => {}, error.OutOfMemory => return error.OutOfMemory, }; if (c.wix_location) |here| { if (first) |f| { if (f[0] != c.language) try diags.err(here, "this source is in WiX {s}, and {s}:{d} is in WiX {s}; one build reads one language", .{ @tagName(c.language), f[1].path, f[1].line, @tagName(f[0]), }); } else first = .{ c.language, here }; } } if (diags.hasErrors()) return error.Invalid; var product = linkSections(arena, diags, sections.items) catch |e| switch (e) { error.Stop => return error.Invalid, error.OutOfMemory => return error.OutOfMemory, }; const v4 = for (sections.items) |sec| { if (sec.language == .v4) break true; } else false; // Before the GUIDs are made, since a component's comes from where it is // installed, and that differs by architecture. if (v4) { product = try resolveBitness(arena, product, arch); try bindProperties(arena, diags, &product); } try generateGuids(arena, diags, sources, &product); try checkEncodable(arena, diags, text.items, product); if (diags.hasErrors()) return error.Invalid; return .{ .product = product, .language = if (v4) .v4 else .v3 };}
/// Text read from a WiX source that goes into the installer package, with/// where it was written.const Text = struct { element: []const u8, /// The attribute, or the element itself for its content. attribute: []const u8, value: []const u8, location: Location,};
/// Attributes whose values name files on the build machine, and never go/// into the installer package.const not_stored = [_][]const u8{ "Source", "SourceFile" };
/// Text that also goes into the summary information, which has a code page/// of its own.const summary_text = [_]struct { []const u8, []const u8 }{ .{ "Product", "Name" }, .{ "Product", "Manufacturer" }, .{ "Package", "Manufacturer" }, .{ "Package", "Description" }, .{ "Package", "Comments" }, .{ "Package", "Keywords" },};
/// Reports each piece of text the code page it is stored in cannot hold,/// where it was written.fn checkEncodable(arena: Allocator, diags: *Diagnostics, text: []const Text, product: wix.Product) Allocator.Error!void { const database = product.codepage orelse 1252; const summary = product.package.summary_codepage orelse 1252; for (text) |t| { _ = codepage_.encode(arena, database, t.value) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.Unencodable => { try diags.err(t.location, "{s} of <{s}>, \"{s}\", cannot be stored in code page {d}; choose another with <Product Codepage>", .{ t.attribute, t.element, t.value, database }); continue; }, }; for (summary_text) |st| { if (!std.mem.eql(u8, st[0], t.element) or !std.mem.eql(u8, st[1], t.attribute)) continue; _ = codepage_.encode(arena, summary, t.value) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.Unencodable => try diags.err(t.location, "{s} of <{s}>, \"{s}\", cannot be stored in the summary information's code page {d}; choose another with <Package SummaryCodepage>", .{ t.attribute, t.element, t.value, summary }), }; } }}
/// What a reference refers to, and what a section defines.const Kind = enum { directory, component, component_group, feature, property, icon,
fn noun(k: Kind) []const u8 { return switch (k) { .directory => "directory", .component => "component", .component_group => "component group", .feature => "feature", .property => "property", .icon => "icon", }; }};
const SectionRef = struct { kind: Kind, ref: wix.Reference,};
const Group = struct { location: Location, id: []const u8, components: std.ArrayList(wix.Reference) = .empty, groups: std.ArrayList(wix.Reference) = .empty,};
/// A WiX v4 <MediaTemplate>: how the files are laid out on disks, when no/// <Media> says.const MediaTemplate = struct { location: Location, /// A cabinet's name, with `{0}` for its disk's number. cabinet: []const u8 = "cab{0}.cab", embed: bool = false, compression: wix.Compression = .medium, /// The most a cabinet holds before the next is begun, in bytes of /// files. max_size: u64 = 200 * 1024 * 1024,};
/// A <Product> or a <Fragment>: what it defines, and what it refers to.const Section = struct { location: Location, product: ?wix.Product = null, package: ?wix.Package = null, properties: std.ArrayList(wix.Property) = .empty, features: std.ArrayList(wix.Feature) = .empty, directories: std.ArrayList(wix.Directory) = .empty, components: std.ArrayList(wix.Component) = .empty, groups: std.ArrayList(Group) = .empty, media: std.ArrayList(wix.Media) = .empty, icons: std.ArrayList(wix.Icon) = .empty, launch_conditions: std.ArrayList(wix.LaunchCondition) = .empty, major_upgrade: ?wix.MajorUpgrade = null, refs: std.ArrayList(SectionRef) = .empty, language: wix.Language = .v3, /// Directories made for a Subdirectory or a standard directory, which /// any number of sections may make alike, and so define no symbol. implicit_directories: std.ArrayList(wix.Directory) = .empty, /// Components put in features by their Feature attribute. memberships: std.ArrayList(struct { feature: wix.Reference, component: wix.Reference }) = .empty, media_template: ?MediaTemplate = null,
/// Every symbol the section defines, as its kind, Id and location. fn define(s: *const Section, arena: Allocator, out: *std.ArrayList(struct { Kind, []const u8, Location })) Allocator.Error!void { for (s.directories.items) |d| try out.append(arena, .{ .directory, d.id, d.location }); for (s.components.items) |c| try out.append(arena, .{ .component, c.id, c.location }); for (s.groups.items) |g| try out.append(arena, .{ .component_group, g.id, g.location }); for (s.features.items) |f| try out.append(arena, .{ .feature, f.id, f.location }); for (s.properties.items) |p| try out.append(arena, .{ .property, p.id, p.location }); for (s.icons.items) |i| try out.append(arena, .{ .icon, i.id, i.location }); }};
/// Joins the product's section and every section it reaches through its/// references into one product.fn linkSections(arena: Allocator, diags: *Diagnostics, sections: []const *Section) Compiler.Stop!wix.Product { // The one product. var product_index: ?usize = null; for (sections, 0..) |s, i| { if (s.product == null) continue; if (product_index) |first| { try diags.err(s.location, "there may be only one <Product>, and one is already at {s}:{d}:{d}", .{ sections[first].location.path, sections[first].location.line, sections[first].location.column, }); } else product_index = i; } const root = product_index orelse { const where: ?Location = if (sections.len > 0) sections[0].location else null; try diags.err(where, "there is no <Product>", .{}); return error.Stop; };
// Every symbol, and the section that defines it; a symbol defined twice // is an error wherever it is. var symbols: std.StringHashMapUnmanaged(struct { usize, Location }) = .empty; for (sections, 0..) |s, i| { var defined: std.ArrayList(struct { Kind, []const u8, Location }) = .empty; try s.define(arena, &defined); for (defined.items) |d| { const key = try std.fmt.allocPrint(arena, "{s}:{s}", .{ @tagName(d[0]), d[1] }); const gop = try symbols.getOrPut(arena, key); if (gop.found_existing) { const first = gop.value_ptr[1]; try diags.err(d[2], "{s} \"{s}\" is already defined at {s}:{d}:{d}", .{ capitalized(d[0]), d[1], first.path, first.line, first.column }); } else gop.value_ptr.* = .{ i, d[2] }; } }
// The sections reached from the product, by following references. const included = try arena.alloc(bool, sections.len); @memset(included, false); included[root] = true; var queue: std.ArrayList(usize) = .empty; try queue.append(arena, root); while (queue.pop()) |i| { for (sections[i].refs.items) |r| { const key = try std.fmt.allocPrint(arena, "{s}:{s}", .{ @tagName(r.kind), r.ref.id }); const target = symbols.get(key) orelse { // A standard folder, which no section defines, is still a // directory to refer to; in WiX v4, any standard directory. if (r.kind == .directory and std.mem.eql(u8, r.ref.id, "TARGETDIR")) continue; if (r.kind == .directory and sections[i].language == .v4 and wix.isStandardDirectory(r.ref.id)) continue; // WiX v5 supplies an INSTALLFOLDER referred to and not defined. if (r.kind == .directory and sections[i].language == .v4 and std.mem.eql(u8, r.ref.id, "INSTALLFOLDER")) continue; try diags.err(r.ref.location, "no {s} has the Id \"{s}\"", .{ r.kind.noun(), r.ref.id }); continue; }; if (!included[target[0]]) { included[target[0]] = true; try queue.append(arena, target[0]); } } }
var product = sections[root].product.?; var properties: std.ArrayList(wix.Property) = .empty; var features: std.ArrayList(wix.Feature) = .empty; var directories: std.ArrayList(wix.Directory) = .empty; var components: std.ArrayList(wix.Component) = .empty; var media: std.ArrayList(wix.Media) = .empty; var icons: std.ArrayList(wix.Icon) = .empty; var launch_conditions: std.ArrayList(wix.LaunchCondition) = .empty; var groups: std.StringHashMapUnmanaged(*const Group) = .empty; var implicit: std.ArrayList(wix.Directory) = .empty; var memberships: std.ArrayList(struct { feature: wix.Reference, component: wix.Reference }) = .empty; var template: ?MediaTemplate = null; const v4 = sections[root].language == .v4; for (sections, included) |s, in| { if (!in) continue; try implicit.appendSlice(arena, s.implicit_directories.items); for (s.memberships.items) |m| try memberships.append(arena, .{ .feature = m.feature, .component = m.component }); if (s.media_template) |t| { if (template != null) try diags.err(t.location, "there may be only one <MediaTemplate>", .{}); template = t; } try properties.appendSlice(arena, s.properties.items); try features.appendSlice(arena, s.features.items); try directories.appendSlice(arena, s.directories.items); try components.appendSlice(arena, s.components.items); try media.appendSlice(arena, s.media.items); try icons.appendSlice(arena, s.icons.items); try launch_conditions.appendSlice(arena, s.launch_conditions.items); for (s.groups.items) |*g| try groups.put(arena, g.id, g); }
// A component's Feature attribute puts it in that feature. for (memberships.items) |m| { for (features.items) |*f| { if (!std.mem.eql(u8, f.id, m.feature.id)) continue; const list = try arena.alloc(wix.Reference, f.components.len + 1); @memcpy(list[0..f.components.len], f.components); list[f.components.len] = m.component; f.components = list; break; } }
// Each feature installs its components and every component in the // groups it names, groups within groups included. for (features.items) |*f| { var all: std.ArrayList(wix.Reference) = .empty; try all.appendSlice(arena, f.components); var seen: std.StringHashMapUnmanaged(void) = .empty; for (f.component_groups) |g| try expandGroup(arena, groups, g.id, &all, &seen); f.components = all.items; }
if (v4) { // WiX v5's default INSTALLFOLDER, for a package that uses one // without defining it: the manufacturer and product name, in the // program files folder of the package's bitness. const defined = for (directories.items) |d| { if (std.mem.eql(u8, d.id, "INSTALLFOLDER")) break true; } else false; if (!defined and (directoryUsed(directories.items, components.items, features.items, "INSTALLFOLDER") or directoryUsed(implicit.items, &.{}, &.{}, "INSTALLFOLDER"))) { try directories.append(arena, .{ .location = sections[root].location, .id = "INSTALLFOLDER", .parent = "ProgramFiles6432Folder", .name = "!(bind.Property.Manufacturer) !(bind.Property.ProductName)", }); }
// WiX v5's default feature, for a package that has none, holding // every component. Its Id is zixl's (ADR-0007). if (features.items.len == 0) { const all = try arena.alloc(wix.Reference, components.items.len); for (components.items, all) |comp, *r| r.* = .{ .location = comp.location, .id = comp.id }; try features.append(arena, .{ .location = sections[root].location, .id = "DefaultFeature", .components = all }); }
// The directories made for Subdirectory attributes, once each, and // TARGETDIR and the standard directories that WiX v4 sources use // without defining, ahead of the rest so that parents come first. for (implicit.items) |d| { for (directories.items) |existing| { if (std.mem.eql(u8, existing.id, d.id)) break; } else try directories.append(arena, d); } var standard: std.ArrayList(wix.Directory) = .empty; const target_dir: wix.Directory = .{ .location = sections[root].location, .id = "TARGETDIR", .name = "SourceDir" }; for (wix.standard_directories) |id| { if (std.mem.eql(u8, id, "TARGETDIR")) continue; for (directories.items) |d| { if (std.mem.eql(u8, d.id, id)) break; } else if (directoryUsed(directories.items, components.items, features.items, id)) { try standard.append(arena, .{ .location = sections[root].location, .id = id, .parent = "TARGETDIR" }); } } // TARGETDIR, the root, only when there is anything to root. const any = standard.items.len > 0 or directories.items.len > 0 or directoryUsed(directories.items, components.items, features.items, "TARGETDIR"); if (any) for (directories.items) |d| { if (std.mem.eql(u8, d.id, "TARGETDIR")) break; } else try standard.insert(arena, 0, target_dir); try standard.appendSlice(arena, directories.items); directories = standard;
if (media.items.len > 0) { if (template) |t| try diags.err(t.location, "a package laid out by <Media> cannot have a <MediaTemplate> too", .{}); } else { // With neither <Media> nor <MediaTemplate>, the defaults a // <MediaTemplate> documents (ADR-0007). try layOut(arena, template orelse .{ .location = sections[root].location }, components.items, &media); } }
product.properties = properties.items; product.features = features.items; product.directories = directories.items; product.components = components.items; product.media = media.items; product.icons = icons.items; product.launch_conditions = launch_conditions.items; product.major_upgrade = sections[root].major_upgrade;
var files: std.ArrayList(wix.File) = .empty; for (product.components) |component| try files.appendSlice(arena, component.files); try checkUnique(diags, wix.File, "File", files.items); return product;}
/// The 32-bit or 64-bit folder a WiX v4 `...6432Folder` stands for, by the/// package's architecture, or `id` itself for any other directory.fn standardFolder(id: []const u8, arch: Arch) []const u8 { const folders = [_]struct { []const u8, []const u8, []const u8 }{ .{ "ProgramFiles6432Folder", "ProgramFilesFolder", "ProgramFiles64Folder" }, .{ "CommonFiles6432Folder", "CommonFilesFolder", "CommonFiles64Folder" }, .{ "System6432Folder", "SystemFolder", "System64Folder" }, }; for (folders) |f| if (std.mem.eql(u8, id, f[0])) return if (arch == .x86) f[1] else f[2]; return id;}
/// `product` with every `...6432Folder` replaced by the folder it stands/// for in a package for `arch`.fn resolveBitness(arena: Allocator, product: wix.Product, arch: Arch) Allocator.Error!wix.Product { var p = product; const directories = try arena.dupe(wix.Directory, product.directories); for (directories) |*d| { d.id = standardFolder(d.id, arch); if (d.parent) |parent| d.parent = standardFolder(parent, arch); } // A package may name both a 6432 folder and the folder it becomes; // the two are one directory. var unique: std.ArrayList(wix.Directory) = .empty; for (directories) |d| { for (unique.items) |u| { if (std.mem.eql(u8, u.id, d.id)) break; } else try unique.append(arena, d); } p.directories = unique.items; const components = try arena.dupe(wix.Component, product.components); for (components) |*c| { c.directory = standardFolder(c.directory, arch); const folders = try arena.dupe(wix.Reference, c.create_folders); for (folders) |*f| f.id = standardFolder(f.id, arch); c.create_folders = folders; const shortcuts = try arena.dupe(wix.Shortcut, c.shortcuts); for (shortcuts) |*sc| { sc.directory = standardFolder(sc.directory, arch); if (sc.working_directory) |w| sc.working_directory = standardFolder(w, arch); } c.shortcuts = shortcuts; const removals = try arena.dupe(wix.Removal, c.removals); for (removals) |*r| r.directory = standardFolder(r.directory, arch); c.removals = removals; } p.components = components; const features = try arena.dupe(wix.Feature, product.features); for (features) |*f| if (f.configurable_directory) |d| { f.configurable_directory = standardFolder(d, arch); }; p.features = features; return p;}
/// Replaces each `!(bind.Property.NAME)` in the text of a WiX v4 product/// with the property's value, as WiX's binder does. Other binder/// variables, and localization strings, are refused by name.fn bindProperties(arena: Allocator, diags: *Diagnostics, product: *wix.Product) Allocator.Error!void { const Binder = struct { arena: Allocator, diags: *Diagnostics, product: *const wix.Product,
fn property(b: @This(), name: []const u8) ?[]const u8 { const p = b.product; const fixed = [_]struct { []const u8, ?[]const u8 }{ .{ "ProductName", p.name }, .{ "Manufacturer", p.manufacturer }, .{ "ProductVersion", p.version }, .{ "ProductLanguage", p.language }, .{ "UpgradeCode", p.upgrade_code }, }; for (fixed) |f| if (std.mem.eql(u8, f[0], name)) return f[1]; for (p.properties) |prop| if (std.mem.eql(u8, prop.id, name)) return prop.value; return null; }
fn text(b: @This(), location: Location, s: []const u8) Allocator.Error![]const u8 { if (std.mem.find(u8, s, "!(") == null) return s; var out: std.ArrayList(u8) = .empty; var rest = s; while (std.mem.find(u8, rest, "!(")) |start| { try out.appendSlice(b.arena, rest[0..start]); const end = std.mem.findScalarPos(u8, rest, start, ')') orelse { try out.appendSlice(b.arena, rest[start..]); rest = ""; break; }; const variable = rest[start + 2 .. end]; const prefix = "bind.Property."; if (std.mem.startsWith(u8, variable, prefix)) { const name = variable[prefix.len..]; if (b.property(name)) |v| { try out.appendSlice(b.arena, v); } else { try b.diags.err(location, "!({s}) names a property that has no value", .{variable}); } } else { try b.diags.err(location, "the variable !({s}) is not supported; of the binder's variables, zixl knows only !(bind.Property.NAME)", .{variable}); } rest = rest[end + 1 ..]; } try out.appendSlice(b.arena, rest); return out.items; }
fn maybe(b: @This(), location: Location, s: ?[]const u8) Allocator.Error!?[]const u8 { return if (s) |v| try b.text(location, v) else null; } }; const b: Binder = .{ .arena = arena, .diags = diags, .product = product };
const properties = try arena.dupe(wix.Property, product.properties); for (properties) |*p| p.value = try b.maybe(p.location, p.value); const directories = try arena.dupe(wix.Directory, product.directories); for (directories) |*d| d.name = try b.maybe(d.location, d.name); const features = try arena.dupe(wix.Feature, product.features); for (features) |*f| { f.title = try b.maybe(f.location, f.title); f.description = try b.maybe(f.location, f.description); } const launch = try arena.dupe(wix.LaunchCondition, product.launch_conditions); for (launch) |*l| { l.condition = try b.text(l.location, l.condition); l.message = try b.text(l.location, l.message); } const components = try arena.dupe(wix.Component, product.components); for (components) |*c| { c.condition = try b.maybe(c.location, c.condition); const registry = try arena.dupe(wix.RegistryRow, c.registry); for (registry) |*r| { r.key = try b.text(r.location, r.key); r.name = try b.maybe(r.location, r.name); r.value = try b.maybe(r.location, r.value); } c.registry = registry; const environment = try arena.dupe(wix.Environment, c.environment); for (environment) |*e| e.value = try b.maybe(e.location, e.value); c.environment = environment; const shortcuts = try arena.dupe(wix.Shortcut, c.shortcuts); for (shortcuts) |*sc| { sc.name = try b.text(sc.location, sc.name); sc.description = try b.maybe(sc.location, sc.description); sc.arguments = try b.maybe(sc.location, sc.arguments); } c.shortcuts = shortcuts; } var package = product.package; package.description = try b.maybe(package.location, package.description); package.comments = try b.maybe(package.location, package.comments); package.keywords = try b.maybe(package.location, package.keywords); package.author = try b.maybe(package.location, package.author);
product.properties = properties; product.directories = directories; product.features = features; product.launch_conditions = launch; product.components = components; product.package = package;}
/// Whether directory `id` is anyone's parent, or any component's directory.fn directoryUsed(directories: []const wix.Directory, components: []const wix.Component, features: []const wix.Feature, id: []const u8) bool { for (directories) |d| if (d.parent) |p| if (std.mem.eql(u8, p, id)) return true; for (components) |c| { if (std.mem.eql(u8, c.directory, id)) return true; for (c.create_folders) |f| if (std.mem.eql(u8, f.id, id)) return true; for (c.shortcuts) |s| if (std.mem.eql(u8, s.directory, id)) return true; for (c.removals) |r| if (std.mem.eql(u8, r.directory, id)) return true; } for (features) |f| if (f.configurable_directory) |d| if (std.mem.eql(u8, d, id)) return true; return false;}
/// Puts each component with files on a disk of `template`'s, a new one/// begun whenever the next component's files would take the last past its/// size. A component's files stay together on one disk.fn layOut(arena: Allocator, template: MediaTemplate, components: []wix.Component, media: *std.ArrayList(wix.Media)) Allocator.Error!void { var size: u64 = 0; for (components) |*c| { if (c.files.len == 0) continue; var bytes: u64 = 0; for (c.files) |f| bytes += f.contents.len; if (media.items.len == 0 or (size > 0 and size + bytes > template.max_size)) { const disk: i32 = @intCast(media.items.len + 1); const name = try std.mem.replaceOwned(u8, arena, template.cabinet, "{0}", try std.fmt.allocPrint(arena, "{d}", .{disk})); try media.append(arena, .{ .location = template.location, .disk_id = disk, .cabinet = name, .embed = template.embed, .compression = template.compression, }); size = 0; } c.disk_id = @intCast(media.items.len); size += bytes; }}
fn expandGroup( arena: Allocator, groups: std.StringHashMapUnmanaged(*const Group), id: []const u8, out: *std.ArrayList(wix.Reference), seen: *std.StringHashMapUnmanaged(void),) Allocator.Error!void { // A group already expanded, or one that includes itself, adds nothing // more. if ((try seen.getOrPut(arena, id)).found_existing) return; const g = groups.get(id) orelse return; try out.appendSlice(arena, g.components.items); for (g.groups.items) |inner| try expandGroup(arena, groups, inner.id, out, seen);}
fn capitalized(k: Kind) []const u8 { return switch (k) { .directory => "Directory", .component => "Component", .component_group => "Component group", .feature => "Feature", .property => "Property", .icon => "Icon", };}
fn checkUnique(diags: *Diagnostics, comptime T: type, comptime kind: []const u8, items: []const T) Allocator.Error!void { for (items, 0..) |item, i| { for (items[0..i]) |earlier| { if (std.mem.eql(u8, item.id, earlier.id)) { try diags.err(item.location, "{s} \"{s}\" is already defined at {s}:{d}:{d}", .{ kind, item.id, earlier.location.path, earlier.location.line, earlier.location.column, }); break; } } }}
/// The namespace zixl's generated GUIDs are made in: a name-based UUID/// of zixl's own URL.fn zixlNamespace() UUID { return .fromName(.v5, .namespace_url, &.{"https://git.jcollie.dev/jeff/zixl"});}
fn named(arena: Allocator, parts: []const []const u8) Allocator.Error![]const u8 { return arena.dupe(u8, &UUID.fromName(.v5, zixlNamespace(), parts).serializeRegistry());}
/// Replaces each "*" with a GUID derived from what it identifies, as/// ADR-0003 decides. The product and package codes come from every/// input -- the preprocessed WiX sources and every file it names -- so/// that a change to any of them changes both; a component GUID comes/// from its key path alone, so that it stays the same from one version/// to the next.fn generateGuids(arena: Allocator, diags: *Diagnostics, sources: []const Source, product: *wix.Product) Allocator.Error!void { var hash: std.crypto.hash.sha2.Sha256 = .init(.{}); for (sources) |source| { var len: [8]u8 = undefined; std.mem.writeInt(u64, &len, source.text.len, .little); hash.update(&len); hash.update(source.text); } for (product.components) |component| { for (component.files) |f| { var len: [8]u8 = undefined; std.mem.writeInt(u64, &len, f.contents.len, .little); hash.update(&len); hash.update(f.contents); } } var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined; hash.final(&digest); const inputs = std.fmt.bytesToHex(digest, .lower);
if (std.mem.eql(u8, product.id, "*")) product.id = try named(arena, &.{ "product", &inputs }); if (std.mem.eql(u8, product.package.id, "*")) product.package.id = try named(arena, &.{ "package", &inputs });
const components = try arena.dupe(wix.Component, product.components); for (components) |*component| { const g = component.guid orelse continue; if (!std.mem.eql(u8, g, "*")) continue; const key = component.key_path orelse { try diags.err(component.location, "component \"{s}\" asks for a generated GUID, which is derived from its key path, but its key path is a directory; give it a file, or write the GUID out", .{component.id}); continue; }; const path = if (component.key_path_is_registry) try registryKeyPath(arena, component.*, key) else try keyPath(arena, product.*, component.*, key); component.guid = try named(arena, &.{ "component", path }); } product.components = components;}
/// Where a component's key registry value is written: its root, key and/// name, in lower case, since the registry ignores case.fn registryKeyPath(arena: Allocator, component: wix.Component, id: []const u8) Allocator.Error![]const u8 { for (component.registry) |r| { if (!std.mem.eql(u8, r.id, id)) continue; const path = try std.fmt.allocPrint(arena, "registry:{d}:{s}:{s}", .{ @backingInt(r.root), r.key, r.name orelse "" }); return std.ascii.lowerString(path, path); } return id;}
/// Where a component's key file is installed, as the directories and/// the file's name, in lower case since Windows ignores case there. A/// standard folder, which has no name of its own, is named by its Id.fn keyPath(arena: Allocator, product: wix.Product, component: wix.Component, file_id: []const u8) Allocator.Error![]const u8 { var parts: std.ArrayList([]const u8) = .empty; for (component.files) |f| { if (std.mem.eql(u8, f.id, file_id)) try parts.append(arena, f.name); } var dir: ?[]const u8 = component.directory; var depth: usize = 0; while (dir) |id| : (depth += 1) { if (depth > product.directories.len) break; // a cycle, reported elsewhere const d = for (product.directories) |d| { if (std.mem.eql(u8, d.id, id)) break d; } else break; if (d.parent == null) break; try parts.append(arena, d.name orelse d.id); dir = d.parent; } std.mem.reverse([]const u8, parts.items); const joined = try std.mem.join(arena, "/", parts.items); return std.ascii.lowerString(try arena.alloc(u8, joined.len), joined);}
const Compiler = struct { arena: Allocator, diags: *Diagnostics, files: FileSource, source: Source, reader: xml.Reader, /// Every piece of text read that goes into the package, for checking /// once its code page is known. text: *std.ArrayList(Text), /// The language the source is written in, which its <Wix> element's /// namespace says. language: wix.Language = .v3, /// Where its <Wix> element is, once read. wix_location: ?Location = null,
/// Raised once the XML itself cannot be read any further. const Stop = error{Stop} || Allocator.Error;
const Located = struct { event: xml.Event, offset: usize, };
fn location(c: *const Compiler, offset: usize) Location { if (c.source.map) |m| return m.locate(offset); return .at(c.source.path, c.source.text, offset); }
/// Where a slice borrowed from the source text begins. fn offsetOf(c: *const Compiler, slice: []const u8) usize { return @intFromPtr(slice.ptr) - @intFromPtr(c.source.text.ptr); }
fn next(c: *Compiler) Stop!Located { // Whatever precedes a token has already been reported as an event of // its own, so the position before `next` is where the token starts. const offset = c.reader.pos; const event = c.reader.next() catch |e| { try c.diags.err(c.location(c.reader.pos), "malformed XML: {s}", .{@errorName(e)}); return error.Stop; }; return .{ .event = event, .offset = offset }; }
fn skip(c: *Compiler) Stop!void { c.reader.skipElement() catch |e| { try c.diags.err(c.location(c.reader.pos), "malformed XML: {s}", .{@errorName(e)}); return error.Stop; }; }
/// Reads the source's <Wix> element, adding a section to `sections` for /// its <Product> and for each <Fragment>. fn document(c: *Compiler, sections: *std.ArrayList(*Section)) Stop!void { while (true) { const ev = try c.next(); switch (ev.event) { .start_element => |e| { if (!std.mem.eql(u8, e.name, "Wix")) { try c.diags.err(c.location(ev.offset), "the root element is <{s}>, not <Wix>", .{e.name}); return error.Stop; } return c.wixElement(e, ev.offset, sections); }, .eof => { try c.diags.err(c.location(ev.offset), "the document has no <Wix> element", .{}); return error.Stop; }, else => {}, } } }
fn wixElement(c: *Compiler, e: xml.Element, offset: usize, sections: *std.ArrayList(*Section)) Stop!void { const here = c.location(offset); var namespace: ?[]const u8 = null; for (e.attributes) |a| { if (std.mem.eql(u8, a.name, "xmlns")) { namespace = a.raw_value; } else if (!std.mem.startsWith(u8, a.name, "xmlns:")) { try c.unsupportedAttribute(e, a); } } if (namespace != null and std.mem.eql(u8, namespace.?, wix.namespace_v4)) { c.language = .v4; } else if (namespace == null or !std.mem.eql(u8, namespace.?, wix.namespace_v3)) { try c.diags.err(here, "<Wix> must be in the WiX v3 namespace, \"{s}\", or the WiX v4 one, \"{s}\"", .{ wix.namespace_v3, wix.namespace_v4 }); } c.wix_location = here;
while (true) { const ev = try c.next(); switch (ev.event) { .start_element => |child| { if (c.language == .v3 and std.mem.eql(u8, child.name, "Product")) { const section = try c.newSection(sections, ev.offset); try c.productElement(child, ev.offset, section); } else if (c.language == .v4 and std.mem.eql(u8, child.name, "Package")) { const section = try c.newSection(sections, ev.offset); try c.packageV4Element(child, ev.offset, section); } else if (std.mem.eql(u8, child.name, "Fragment")) { const section = try c.newSection(sections, ev.offset); _ = try c.attributes(child, &.{"Id"}); try c.sectionChildren(child, section); } else { try c.unsupportedElement(child, ev.offset); } }, .end_element => return, else => try c.ignorable(ev), } } }
fn newSection(c: *Compiler, sections: *std.ArrayList(*Section), offset: usize) Stop!*Section { const section = try c.arena.create(Section); section.* = .{ .location = c.location(offset), .language = c.language }; try sections.append(c.arena, section); return section; }
fn productElement(c: *Compiler, e: xml.Element, offset: usize, section: *Section) Stop!void { const here = c.location(offset); const a = try c.attributes(e, &.{ "Id", "Name", "Language", "Version", "Manufacturer", "UpgradeCode", "Codepage" }); var product: wix.Product = .{ .location = here, .id = try c.required(e, offset, a[0], "Id"), .name = try c.required(e, offset, a[1], "Name"), .language = try c.required(e, offset, a[2], "Language"), .version = try c.required(e, offset, a[3], "Version"), .manufacturer = try c.required(e, offset, a[4], "Manufacturer"), .upgrade_code = if (a[5]) |v| try c.guid(v, false) else null, .codepage = if (a[6]) |v| try c.codepage(v) else null, .package = .{ .location = here }, .properties = &.{}, .features = &.{}, .directories = &.{}, .components = &.{}, .media = &.{}, }; if (a[0]) |v| product.id = try c.guid(v, true); section.product = product; try c.sectionChildren(e, section); if (section.package) |package| { section.product.?.package = package; } else { try c.diags.err(here, "<Product> has no <Package>", .{}); } }
/// A WiX v4 <Package>: the product and its package in one, with v4's /// defaults, as the WiX schema reference gives them. fn packageV4Element(c: *Compiler, e: xml.Element, offset: usize, section: *Section) Stop!void { const here = c.location(offset); const a = try c.attributes(e, &.{ "Name", "Manufacturer", "Version", "Language", "UpgradeCode", "ProductCode", "Codepage", "Compressed", "InstallerVersion", "Scope", "UpgradeStrategy", "Id", "ShortNames", }); if (a[11]) |v| try c.diags.err(v.location, "Package/@Id, a readable upgrade code, is not supported: WiX does not document how it makes a GUID of it, and a GUID made otherwise would not upgrade WiX's packages (ADR-0007); write an UpgradeCode instead", .{}); if (a[12]) |v| if (try c.yesNo(v)) try c.diags.err(v.location, "installing files by their short names is not supported", .{}); var product: wix.Product = .{ .location = here, // A product code left out is generated, as WiX v4 does. .id = "*", .name = try c.required(e, offset, a[0], "Name"), .manufacturer = try c.required(e, offset, a[1], "Manufacturer"), .version = try c.required(e, offset, a[2], "Version"), // Not documented: Windows Installer's language-neutral 0 // (ADR-0007). .language = if (a[3]) |v| v.value else "0", .upgrade_code = if (a[4]) |v| try c.guid(v, false) else null, // UTF-8, unless the source says otherwise. .codepage = if (a[6]) |v| try c.codepage(v) else codepage_.utf8, .package = .{ .location = here, .id = "*", .compressed = if (a[7]) |v| try c.yesNo(v) else true, .installer_version = if (a[8]) |v| try c.integer(i32, v) else 500, .installer_version_location = if (a[8]) |v| v.location else here, .install_scope = .per_machine, .summary_codepage = codepage_.utf8, }, .properties = &.{}, .features = &.{}, .directories = &.{}, .components = &.{}, .media = &.{}, }; if (a[5]) |v| product.id = try c.guid(v, true); if (a[9]) |v| product.package.install_scope = switch (try c.oneOf(enum { machine, user, dual }, v, &.{ .{ "perMachine", .machine }, .{ "perUser", .user }, .{ "perUserOrMachine", .dual }, .{ "perMachineOrUser", .dual }, })) { .machine => .per_machine, .user => .per_user, .dual => blk: { try c.diags.err(v.location, "Scope=\"{s}\", a package that installs either per user or per machine, is not supported yet", .{v.value}); break :blk .per_machine; }, }; const expects_upgrade = if (a[10]) |v| try c.oneOf(bool, v, &.{ .{ "majorUpgrade", true }, .{ "none", false } }) else true; if (expects_upgrade and product.upgrade_code == null) { try c.diags.err(here, "<Package> needs an UpgradeCode, since its UpgradeStrategy is majorUpgrade; say UpgradeStrategy=\"none\" if it is never to be upgraded", .{}); } section.product = product; try c.sectionChildren(e, section); if (section.package) |summary| { // What <SummaryInformation> said. var package = §ion.product.?.package; package.description = summary.description; package.comments = summary.comments; package.keywords = summary.keywords; package.author = summary.author; if (summary.summary_codepage) |cp| package.summary_codepage = cp; } if (expects_upgrade and section.major_upgrade == null) { try c.diags.warn(here, "WiX gives a package whose UpgradeStrategy is majorUpgrade a default <MajorUpgrade>, and zixl does not, since its message is not documented (ADR-0007); write a <MajorUpgrade>, or say UpgradeStrategy=\"none\"", .{}); } }
/// A WiX v4 <SummaryInformation>, as a package holding only what it /// says of the summary information. fn summaryInformationElement(c: *Compiler, e: xml.Element, offset: usize) Stop!wix.Package { const a = try c.attributes(e, &.{ "Codepage", "Comments", "Description", "Keywords", "Manufacturer" }); var package: wix.Package = .{ .location = c.location(offset) }; if (a[0]) |v| package.summary_codepage = try c.codepage(v); if (a[1]) |v| package.comments = v.value; if (a[2]) |v| package.description = v.value; if (a[3]) |v| package.keywords = v.value; if (a[4]) |v| package.author = v.value; try c.noChildren(e); return package; }
/// The children of a <Product> or a <Fragment>. fn sectionChildren(c: *Compiler, e: xml.Element, section: *Section) Stop!void { if (e.self_closing) { _ = try c.next(); return; } while (true) { const ev = try c.next(); switch (ev.event) { .start_element => |child| { const name = child.name; if (c.language == .v3 and std.mem.eql(u8, name, "Package") and section.product != null) { if (section.package != null) { try c.diags.err(c.location(ev.offset), "<Product> may have only one <Package>", .{}); try c.skip(); } else { section.package = try c.packageElement(child, ev.offset); } } else if (c.language == .v4 and std.mem.eql(u8, name, "SummaryInformation") and section.product != null) { if (section.package != null) { try c.diags.err(c.location(ev.offset), "<Package> may have only one <SummaryInformation>", .{}); try c.skip(); } else { section.package = try c.summaryInformationElement(child, ev.offset); } } else if (c.language == .v4 and std.mem.eql(u8, name, "Launch")) { const a = try c.attributes(child, &.{ "Condition", "Message" }); try section.launch_conditions.append(c.arena, .{ .location = c.location(ev.offset), .condition = try c.required(child, ev.offset, a[0], "Condition"), .message = try c.required(child, ev.offset, a[1], "Message"), }); try c.noChildren(child); } else if (std.mem.eql(u8, name, "Property")) { try c.propertyElement(child, ev.offset, §ion.properties); } else if (std.mem.eql(u8, name, "Feature")) { try c.featureElement(child, ev.offset, null, section); } else if (std.mem.eql(u8, name, "Directory")) { // In WiX v4, a directory at the top is in TARGETDIR. try c.directoryElement(child, ev.offset, if (c.language == .v4) "TARGETDIR" else null, section); } else if (c.language == .v4 and std.mem.eql(u8, name, "StandardDirectory")) { try c.standardDirectoryElement(child, ev.offset, section); } else if (c.language == .v4 and std.mem.eql(u8, name, "MediaTemplate")) { if (section.media_template != null) try c.diags.err(c.location(ev.offset), "there may be only one <MediaTemplate>", .{}); section.media_template = try c.mediaTemplateElement(child, ev.offset); } else if (c.language == .v4 and std.mem.eql(u8, name, "Component")) { _ = try c.componentElement(child, ev.offset, null, section); } else if (c.language == .v4 and std.mem.eql(u8, name, "File")) { _ = try c.nakedFileElement(child, ev.offset, null, section); } else if (std.mem.eql(u8, name, "DirectoryRef")) { try c.directoryRefElement(child, ev.offset, section); } else if (std.mem.eql(u8, name, "Media")) { try c.mediaElement(child, ev.offset, §ion.media); } else if (std.mem.eql(u8, name, "ComponentGroup")) { try c.componentGroupElement(child, ev.offset, section); } else if (std.mem.eql(u8, name, "Icon")) { try c.iconElement(child, ev.offset, section); } else if (c.language == .v3 and std.mem.eql(u8, name, "Condition")) { const a = try c.attributes(child, &.{"Message"}); const message = try c.required(child, ev.offset, a[0], "Message"); try section.launch_conditions.append(c.arena, .{ .location = c.location(ev.offset), .condition = try c.conditionText(child, ev.offset), .message = message, }); } else if (std.mem.eql(u8, name, "MajorUpgrade") and section.product != null) { if (section.major_upgrade != null) { try c.diags.err(c.location(ev.offset), "<{s}> may have only one <MajorUpgrade>", .{if (c.language == .v3) "Product" else "Package"}); } section.major_upgrade = try c.majorUpgradeElement(child, ev.offset); } else if (refKind(name)) |kind| { _ = try c.reference(child, ev.offset, section, kind); try c.noChildren(child); } else { try c.unsupportedElement(child, ev.offset); } }, .end_element => return, else => try c.ignorable(ev), } } }
/// What a reference element refers to. fn refKind(name: []const u8) ?Kind { const map = [_]struct { []const u8, Kind }{ .{ "ComponentRef", .component }, .{ "ComponentGroupRef", .component_group }, .{ "FeatureRef", .feature }, .{ "PropertyRef", .property }, }; for (map) |m| if (std.mem.eql(u8, name, m[0])) return m[1]; return null; }
/// Records the reference a `...Ref` element makes, by its Id, and /// returns it. fn reference(c: *Compiler, e: xml.Element, offset: usize, section: *Section, kind: Kind) Stop!wix.Reference { const a = try c.attributes(e, &.{"Id"}); const ref: wix.Reference = .{ .location = c.location(offset), .id = try c.required(e, offset, a[0], "Id") }; try section.refs.append(c.arena, .{ .kind = kind, .ref = ref }); return ref; }
fn packageElement(c: *Compiler, e: xml.Element, offset: usize) Stop!wix.Package { const a = try c.attributes(e, &.{ "Id", "Description", "Comments", "Keywords", "InstallerVersion", "Compressed", "InstallScope", "SummaryCodepage", "Manufacturer", }); // A package code left out is generated, as one written "*" is. var package: wix.Package = .{ .location = c.location(offset), .id = "*" }; if (a[0]) |v| package.id = try c.guid(v, true); if (a[1]) |v| package.description = v.value; if (a[2]) |v| package.comments = v.value; if (a[3]) |v| package.keywords = v.value; if (a[4]) |v| { package.installer_version = try c.integer(i32, v); package.installer_version_location = v.location; } if (a[5]) |v| package.compressed = try c.yesNo(v); if (a[6]) |v| package.install_scope = try c.oneOf(wix.InstallScope, v, &.{ .{ "perMachine", .per_machine }, .{ "perUser", .per_user }, }); if (a[7]) |v| package.summary_codepage = try c.codepage(v); // The summary information's author, which is otherwise the // product's manufacturer. if (a[8]) |v| package.author = v.value; try c.noChildren(e); return package; }
fn propertyElement(c: *Compiler, e: xml.Element, offset: usize, list: *std.ArrayList(wix.Property)) Stop!void { const a = try c.attributes(e, &.{ "Id", "Value" }); const id = try c.required(e, offset, a[0], "Id"); try list.append(c.arena, .{ .location = c.location(offset), .id = id, .value = if (a[1]) |v| v.value else null, }); try c.noChildren(e); }
fn featureElement(c: *Compiler, e: xml.Element, offset: usize, parent: ?[]const u8, section: *Section) Stop!void { if (c.language == .v4) return c.featureV4Element(e, offset, parent, section); const a = try c.attributes(e, &.{ "Id", "Title", "Description", "Level", "Display", "ConfigurableDirectory", "Absent", "AllowAdvertise", "InstallDefault", "TypicalDefault", }); var feature: wix.Feature = .{ .location = c.location(offset), .id = try c.required(e, offset, a[0], "Id"), .parent = parent, }; if (a[1]) |v| feature.title = v.value; if (a[2]) |v| feature.description = v.value; if (a[3]) |v| feature.level = try c.integer(i32, v); if (a[4]) |v| feature.display = try c.oneOf(wix.Display, v, &.{ .{ "collapse", .collapse }, .{ "expand", .expand }, .{ "hidden", .hidden }, }); if (a[5]) |v| { feature.configurable_directory = v.value; try section.refs.append(c.arena, .{ .kind = .directory, .ref = .{ .location = v.location, .id = v.value } }); } // The Feature table's attribute bits, as the Windows Installer SDK // defines them: favor source 1, follow parent 2, favor advertise 4, // disallow advertise 8, disallow absent 16, no unsupported // advertise 32. if (a[6]) |v| feature.attributes |= try c.oneOf(i32, v, &.{ .{ "allow", 0 }, .{ "disallow", 16 } }); if (a[7]) |v| feature.attributes |= try c.oneOf(i32, v, &.{ .{ "yes", 0 }, .{ "no", 8 }, .{ "system", 32 } }); if (a[8]) |v| feature.attributes |= try c.oneOf(i32, v, &.{ .{ "local", 0 }, .{ "source", 1 }, .{ "followParent", 2 } }); if (a[9]) |v| feature.attributes |= try c.oneOf(i32, v, &.{ .{ "install", 0 }, .{ "advertise", 4 } }); try c.featureChildren(e, feature, section); }
/// A WiX v4 <Feature>, whose advertising and absence are yes or no, and /// whose conditions are <Level> children. fn featureV4Element(c: *Compiler, e: xml.Element, offset: usize, parent: ?[]const u8, section: *Section) Stop!void { const a = try c.attributes(e, &.{ "Id", "Title", "Description", "Level", "Display", "ConfigurableDirectory", "AllowAbsent", "AllowAdvertise", "InstallDefault", "TypicalDefault", }); var feature: wix.Feature = .{ .location = c.location(offset), .id = try c.identifier(e, offset, a[0], "Id"), .parent = parent, }; if (a[1]) |v| feature.title = v.value; if (a[2]) |v| feature.description = v.value; if (a[3]) |v| feature.level = try c.integer(i32, v); if (a[4]) |v| feature.display = try c.oneOf(wix.Display, v, &.{ .{ "collapse", .collapse }, .{ "expand", .expand }, .{ "hidden", .hidden }, }); if (a[5]) |v| { feature.configurable_directory = v.value; try section.refs.append(c.arena, .{ .kind = .directory, .ref = .{ .location = v.location, .id = v.value } }); } // The Feature table's attribute bits, as for WiX v3. if (a[6]) |v| if (!try c.yesNo(v)) { feature.attributes |= 16; }; const allow_advertise = if (a[7]) |v| try c.yesNo(v) else true; if (!allow_advertise) feature.attributes |= 8; if (a[8]) |v| feature.attributes |= try c.oneOf(i32, v, &.{ .{ "local", 0 }, .{ "source", 1 }, .{ "followParent", 2 } }); if (a[9]) |v| { const advertise = try c.oneOf(bool, v, &.{ .{ "install", false }, .{ "advertise", true } }); if (advertise and !allow_advertise) { try c.diags.err(v.location, "a feature that may not be advertised cannot favor being advertised", .{}); } if (advertise) feature.attributes |= 4; } try c.featureChildren(e, feature, section); }
/// Records `feature` and reads what it holds: features within it, the /// components it installs, and its conditions. fn featureChildren(c: *Compiler, e: xml.Element, feature: wix.Feature, section: *Section) Stop!void { const index = section.features.items.len; try section.features.append(c.arena, feature);
if (e.self_closing) { _ = try c.next(); return; } var components: std.ArrayList(wix.Reference) = .empty; var groups: std.ArrayList(wix.Reference) = .empty; var conditions: std.ArrayList(wix.FeatureCondition) = .empty; defer { section.features.items[index].components = components.items; section.features.items[index].component_groups = groups.items; section.features.items[index].conditions = conditions.items; } while (true) { const ev = try c.next(); switch (ev.event) { .start_element => |child| { if (std.mem.eql(u8, child.name, "Feature")) { try c.featureElement(child, ev.offset, feature.id, section); } else if (std.mem.eql(u8, child.name, "ComponentRef")) { try components.append(c.arena, try c.reference(child, ev.offset, section, .component)); try c.noChildren(child); } else if (std.mem.eql(u8, child.name, "ComponentGroupRef")) { try groups.append(c.arena, try c.reference(child, ev.offset, section, .component_group)); try c.noChildren(child); } else if (c.language == .v4 and std.mem.eql(u8, child.name, "Level")) { const la = try c.attributes(child, &.{ "Condition", "Value" }); try conditions.append(c.arena, .{ .location = c.location(ev.offset), .level = if (la[1]) |v| try c.integer(i32, v) else blk: { _ = try c.required(child, ev.offset, null, "Value"); break :blk 0; }, .condition = try c.required(child, ev.offset, la[0], "Condition"), }); try c.noChildren(child); } else if (c.language == .v3 and std.mem.eql(u8, child.name, "Condition")) { const ca = try c.attributes(child, &.{"Level"}); const level = if (ca[0]) |v| try c.integer(i32, v) else blk: { _ = try c.required(child, ev.offset, null, "Level"); break :blk 0; }; try conditions.append(c.arena, .{ .location = c.location(ev.offset), .level = level, .condition = try c.conditionText(child, ev.offset), }); } else { try c.unsupportedElement(child, ev.offset); } }, .end_element => return, else => try c.ignorable(ev), } } }
fn directoryElement(c: *Compiler, e: xml.Element, offset: usize, parent: ?[]const u8, section: *Section) Stop!void { if (c.language == .v4) return c.directoryV4Element(e, offset, parent.?, section); const a = try c.attributes(e, &.{ "Id", "Name", "ShortName" }); const directory: wix.Directory = .{ .location = c.location(offset), .id = try c.required(e, offset, a[0], "Id"), .parent = parent, .name = if (a[1]) |v| try c.longName(v) else null, .short_name = if (a[2]) |v| try c.shortName(v) else null, }; try section.directories.append(c.arena, directory); try c.directoryChildren(e, directory.id, section); }
/// A WiX v4 <Directory>, whose Id may be left for zixl to make. fn directoryV4Element(c: *Compiler, e: xml.Element, offset: usize, parent: []const u8, section: *Section) Stop!void { const here = c.location(offset); const a = try c.attributes(e, &.{ "Id", "Name", "ShortName" }); try c.noAccessModifier(a[0]); const name: ?[]const u8 = if (a[1]) |v| try c.longName(v) else null; const id = if (a[0]) |v| v.value else if (name) |n| try c.generatedId("dir", &.{ parent, n }) else blk: { try c.diags.err(here, "a <Directory> needs an Id or a Name", .{}); break :blk ""; }; const directory: wix.Directory = .{ .location = here, .id = id, .parent = parent, .name = name, .short_name = if (a[2]) |v| try c.shortName(v) else null, }; try section.directories.append(c.arena, directory); try c.directoryChildren(e, directory.id, section); }
/// A WiX v4 <StandardDirectory>: what it holds goes in a standard /// directory, which needs no definition. fn standardDirectoryElement(c: *Compiler, e: xml.Element, offset: usize, section: *Section) Stop!void { const a = try c.attributes(e, &.{"Id"}); const id = try c.required(e, offset, a[0], "Id"); if (a[0] != null and !wix.isStandardDirectory(id)) { try c.diags.err(a[0].?.location, "\"{s}\" is not a standard directory", .{id}); } try c.directoryChildren(e, id, section); }
/// The directory `path` names below `base`, its parts separated by `\` /// or `/`, each made as a directory with a generated Id, as a WiX v4 /// Subdirectory attribute asks. The same path below the same base is the /// same directory, wherever it is written. fn subdirectory(c: *Compiler, base: []const u8, a: Attribute, section: *Section) Stop![]const u8 { var parent = base; var it = std.mem.tokenizeAny(u8, a.value, "\\/"); while (it.next()) |part| { if (std.mem.indexOfAny(u8, part, ":*?\"<>|") != null) { try c.diags.err(a.location, "\"{s}\" is not a valid directory name", .{part}); } const id = try c.generatedId("dir", &.{ parent, part }); try section.implicit_directories.append(c.arena, .{ .location = a.location, .id = id, .parent = parent, .name = part }); parent = id; } if (parent.ptr == base.ptr) try c.diags.err(a.location, "Subdirectory names no directory", .{}); return parent; }
/// An Id for an element that has none: `prefix` and an MD5 of what /// makes it the element it is, as the Registry table's are made. fn generatedId(c: *Compiler, comptime prefix: []const u8, parts: []const []const u8) Stop![]const u8 { var h: std.crypto.hash.Md5 = .init(.{}); for (parts, 0..) |part, i| { if (i > 0) h.update("\\"); h.update(part); } var digest: [16]u8 = undefined; h.final(&digest); return std.fmt.allocPrint(c.arena, prefix ++ "{s}", .{std.fmt.bytesToHex(digest, .upper)}); }
/// A WiX v4 <MediaTemplate>, with the defaults the WiX schema reference /// gives. fn mediaTemplateElement(c: *Compiler, e: xml.Element, offset: usize) Stop!MediaTemplate { const a = try c.attributes(e, &.{ "CabinetTemplate", "CompressionLevel", "EmbedCab", "MaximumUncompressedMediaSize" }); var t: MediaTemplate = .{ .location = c.location(offset) }; if (a[0]) |v| { if (std.mem.find(u8, v.value, "{0}") == null) try c.diags.err(v.location, "a CabinetTemplate needs {{0}} where each cabinet's number goes", .{}); t.cabinet = try c.longName(v); } if (a[1]) |v| t.compression = try c.compressionLevel(v); if (a[2]) |v| t.embed = try c.yesNo(v); if (a[3]) |v| { const megabytes = try c.integer(u32, v); if (megabytes == 0) try c.diags.err(v.location, "a cabinet must hold at least one megabyte", .{}); t.max_size = @as(u64, megabytes) * 1024 * 1024; } try c.noChildren(e); return t; }
fn compressionLevel(c: *Compiler, v: Attribute) Stop!wix.Compression { return c.oneOf(wix.Compression, v, &.{ .{ "none", .none }, .{ "low", .low }, .{ "medium", .medium }, .{ "high", .high }, .{ "mszip", .mszip }, }); }
/// A <DirectoryRef>: what it holds goes in a directory defined elsewhere. fn directoryRefElement(c: *Compiler, e: xml.Element, offset: usize, section: *Section) Stop!void { const a = try c.attributes(e, &.{"Id"}); const id = try c.required(e, offset, a[0], "Id"); try section.refs.append(c.arena, .{ .kind = .directory, .ref = .{ .location = c.location(offset), .id = id } }); try c.directoryChildren(e, id, section); }
fn directoryChildren(c: *Compiler, e: xml.Element, id: []const u8, section: *Section) Stop!void { if (e.self_closing) { _ = try c.next(); return; } while (true) { const ev = try c.next(); switch (ev.event) { .start_element => |child| { if (std.mem.eql(u8, child.name, "Directory")) { try c.directoryElement(child, ev.offset, id, section); } else if (std.mem.eql(u8, child.name, "Component")) { _ = try c.componentElement(child, ev.offset, id, section); } else if (c.language == .v4 and std.mem.eql(u8, child.name, "File")) { _ = try c.nakedFileElement(child, ev.offset, id, section); } else { try c.unsupportedElement(child, ev.offset); } }, .end_element => return, else => try c.ignorable(ev), } } }
/// A <ComponentGroup>: components defined in it, and others referred to. fn componentGroupElement(c: *Compiler, e: xml.Element, offset: usize, section: *Section) Stop!void { const a = try c.attributes(e, &.{ "Id", "Directory", "Subdirectory" }); var group: Group = .{ .location = c.location(offset), .id = try c.identifier(e, offset, a[0], "Id") }; var directory: ?[]const u8 = if (a[1]) |v| blk: { try section.refs.append(c.arena, .{ .kind = .directory, .ref = .{ .location = v.location, .id = v.value } }); break :blk v.value; } else null; if (a[2]) |v| { if (c.language == .v3) { try c.diags.err(v.location, "attribute Subdirectory of <ComponentGroup> is not supported", .{}); } else if (directory) |d| { directory = try c.subdirectory(d, v, section); } else try c.diags.err(v.location, "a Subdirectory is below a Directory, and this <ComponentGroup> has none", .{}); } if (!e.self_closing) { while (true) { const ev = try c.next(); switch (ev.event) { .start_element => |child| { if (std.mem.eql(u8, child.name, "Component")) { const id = try c.componentElement(child, ev.offset, directory, section); try group.components.append(c.arena, .{ .location = c.location(ev.offset), .id = id }); } else if (c.language == .v4 and std.mem.eql(u8, child.name, "File")) { const id = try c.nakedFileElement(child, ev.offset, directory, section); try group.components.append(c.arena, .{ .location = c.location(ev.offset), .id = id }); } else if (std.mem.eql(u8, child.name, "ComponentRef")) { try group.components.append(c.arena, try c.reference(child, ev.offset, section, .component)); try c.noChildren(child); } else if (std.mem.eql(u8, child.name, "ComponentGroupRef")) { try group.groups.append(c.arena, try c.reference(child, ev.offset, section, .component_group)); try c.noChildren(child); } else { try c.unsupportedElement(child, ev.offset); } }, .end_element => break, else => try c.ignorable(ev), } } } else _ = try c.next(); try section.groups.append(c.arena, group); }
/// Reads a <Component> in `directory` (when it is in one), returning its /// Id. fn componentElement(c: *Compiler, e: xml.Element, offset: usize, directory: ?[]const u8, section: *Section) Stop![]const u8 { const here = c.location(offset); const a = try c.attributes(e, &.{ "Id", "Guid", "DiskId", "Directory", "KeyPath", "Bitness", "Condition", "Feature", "Subdirectory" }); const v4 = c.language == .v4; if (!v4) for (a[5..]) |extra| if (extra) |v| { try c.diags.err(v.location, "this attribute of <Component> is WiX v4's, not WiX v3's", .{}); }; var component: wix.Component = .{ .location = here, // In WiX v4, a component with no Id takes its key path's. .id = if (v4) (if (a[0]) |v| v.value else "") else try c.required(e, offset, a[0], "Id"), .guid = null, .directory = directory orelse "", .files = &.{}, }; if (v4) try c.noAccessModifier(a[0]); if (a[3]) |v| { component.directory = v.value; try section.refs.append(c.arena, .{ .kind = .directory, .ref = .{ .location = v.location, .id = v.value } }); } else if (directory == null) { try c.diags.err(here, "component \"{s}\" is in no directory; give it a Directory attribute, or its group one", .{component.id}); } if (a[8]) |v| if (v4) { component.directory = try c.subdirectory(component.directory, v, section); }; if (a[1]) |v| { // An empty GUID marks a component Windows Installer does not // track, which is what the Component table's null means. if (v.value.len > 0) component.guid = try c.guid(v, true); } else if (v4) { // WiX v4 generates a GUID for a component that gives none. component.guid = "*"; } else { _ = try c.required(e, offset, null, "Guid"); } if (v4) { if (a[5]) |v| component.bitness = try c.oneOf(wix.Bitness, v, &.{ .{ "default", .default }, .{ "always32", .always32 }, .{ "always64", .always64 }, }); if (a[6]) |v| component.condition = v.value; } if (a[2]) |v| component.disk_id = try c.integer(i32, v); // KeyPath="yes" makes the component's directory its key path, so no // file is chosen for it. const directory_key_path = if (a[4]) |v| try c.yesNo(v) else false; if (directory_key_path) component.key_path = "";
var files: std.ArrayList(wix.File) = .empty; var shortcuts: std.ArrayList(wix.Shortcut) = .empty; var create_folders: std.ArrayList(wix.Reference) = .empty; var removals: std.ArrayList(wix.Removal) = .empty; var registry: std.ArrayList(wix.RegistryRow) = .empty; var environment: std.ArrayList(wix.Environment) = .empty; if (e.self_closing) { _ = try c.next(); } else { while (true) { const ev = try c.next(); switch (ev.event) { .start_element => |child| { if (std.mem.eql(u8, child.name, "File")) { const f = try c.fileElement(child, ev.offset, component.directory, false, &files, &shortcuts, section); if (f.key_path) try c.setKeyPath(&component, ev.offset, f.id, false); } else if (std.mem.eql(u8, child.name, "Shortcut")) { try c.shortcutElement(child, ev.offset, null, component.directory, &shortcuts, section); } else if (std.mem.eql(u8, child.name, "CreateFolder")) { const f = try c.attributes(child, &.{ "Directory", "Subdirectory" }); var dir = if (f[0]) |v| blk: { try section.refs.append(c.arena, .{ .kind = .directory, .ref = .{ .location = v.location, .id = v.value } }); break :blk v.value; } else component.directory; if (f[1]) |v| { if (c.language == .v4) { dir = try c.subdirectory(dir, v, section); } else try c.diags.err(v.location, "attribute Subdirectory of <CreateFolder> is not supported", .{}); } try create_folders.append(c.arena, .{ .location = c.location(ev.offset), .id = dir }); try c.noChildren(child); } else if (std.mem.eql(u8, child.name, "RemoveFolder") or std.mem.eql(u8, child.name, "RemoveFile")) { try c.removalElement(child, ev.offset, component.directory, &removals, section); } else if (std.mem.eql(u8, child.name, "RegistryKey")) { try c.registryKeyElement(child, ev.offset, .{}, &component, ®istry); } else if (std.mem.eql(u8, child.name, "RegistryValue")) { try c.registryValueElement(child, ev.offset, .{}, &component, ®istry); } else if (std.mem.eql(u8, child.name, "Environment")) { try c.environmentElement(child, ev.offset, &environment); } else { try c.unsupportedElement(child, ev.offset); } }, .end_element => break, else => try c.ignorable(ev), } } } component.files = files.items; component.registry = registry.items; component.environment = environment.items; component.shortcuts = shortcuts.items; component.create_folders = create_folders.items; component.removals = removals.items; // Without a file marked as the key path, the first file is the key // path, as the reference implementation has it; a component with no // files keeps its directory as its key path. if (component.key_path == null and files.items.len > 0) component.key_path = files.items[0].id; if (component.key_path) |k| if (k.len == 0) { component.key_path = null; }; if (component.id.len == 0) { component.id = component.key_path orelse blk: { try c.diags.err(here, "a <Component> whose key path is its directory needs an Id", .{}); break :blk ""; }; } if (v4) if (a[7]) |v| { try section.refs.append(c.arena, .{ .kind = .feature, .ref = .{ .location = v.location, .id = v.value } }); try section.memberships.append(c.arena, .{ .feature = .{ .location = v.location, .id = v.value }, .component = .{ .location = here, .id = component.id }, }); }; try section.components.append(c.arena, component); return component.id; }
fn setKeyPath(c: *Compiler, component: *wix.Component, offset: usize, id: []const u8, is_registry: bool) Stop!void { if (component.key_path) |existing| { if (existing.len == 0) { try c.diags.err(c.location(offset), "component \"{s}\" already has its directory as its key path", .{component.id}); return; } try c.diags.err(c.location(offset), "component \"{s}\" already has \"{s}\" as its key path", .{ component.id, existing }); } else { component.key_path = id; component.key_path_is_registry = is_registry; } }
/// The root and key a <RegistryValue> or <RegistryKey> inherits from the /// keys around it. const RegistryContext = struct { root: ?wix.RegistryRoot = null, key: ?[]const u8 = null, };
fn registryRoot(c: *Compiler, a: Attribute) Stop!wix.RegistryRoot { return c.oneOf(wix.RegistryRoot, a, &.{ .{ "HKLM", .hklm }, .{ "HKCU", .hkcu }, .{ "HKCR", .hkcr }, .{ "HKU", .hku }, .{ "HKMU", .hkmu }, }); }
/// `key` within `outer`, which is joined to it with a backslash. fn subkey(c: *Compiler, outer: ?[]const u8, key: ?[]const u8) Stop!?[]const u8 { const k = key orelse return outer; const o = outer orelse return k; return try std.fmt.allocPrint(c.arena, "{s}\\{s}", .{ std.mem.trimEnd(u8, o, "\\"), k }); }
/// A Registry row's Id when the WiX source gives none: `reg` and an MD5 /// of where the row writes, which is what makes it unique. fn registryId(c: *Compiler, root: wix.RegistryRoot, key: []const u8, name: ?[]const u8) Stop![]const u8 { var h: std.crypto.hash.Md5 = .init(.{}); var buf: [16]u8 = undefined; h.update(std.fmt.bufPrint(&buf, "{d}\\", .{@backingInt(root)}) catch unreachable); h.update(key); h.update("\\"); h.update(name orelse ""); var digest: [16]u8 = undefined; h.final(&digest); return std.fmt.allocPrint(c.arena, "reg{s}", .{std.fmt.bytesToHex(digest, .upper)}); }
fn registryKeyElement( c: *Compiler, e: xml.Element, offset: usize, outer: RegistryContext, component: *wix.Component, rows: *std.ArrayList(wix.RegistryRow), ) Stop!void { const here = c.location(offset); const a = try c.attributes(e, &.{ "Id", "Root", "Key", "ForceCreateOnInstall", "ForceDeleteOnUninstall", "Action" }); const ctx: RegistryContext = .{ .root = if (a[1]) |v| try c.registryRoot(v) else outer.root, .key = try c.subkey(outer.key, if (a[2]) |v| v.value else null), }; const root = ctx.root orelse blk: { try c.diags.err(here, "<RegistryKey> needs a Root, here or on a key around it", .{}); break :blk .hklm; }; const key = ctx.key orelse blk: { try c.diags.err(here, "<RegistryKey> needs a Key", .{}); break :blk ""; }; var create = if (a[3]) |v| try c.yesNo(v) else false; var delete = if (a[4]) |v| try c.yesNo(v) else false; if (a[5]) |v| { switch (try c.oneOf(enum { none, create, both }, v, &.{ .{ "none", .none }, .{ "create", .create }, .{ "createAndRemoveOnUninstall", .both }, })) { .none => {}, .create => create = true, .both => { create = true; delete = true; }, } } // The key itself, created or removed, is a row with a special name // and no value. const special: ?[]const u8 = if (create and delete) "*" else if (create) "+" else if (delete) "-" else null; if (special) |name| { try rows.append(c.arena, .{ .location = here, .id = if (a[0]) |v| v.value else try c.registryId(root, key, name), .root = root, .key = key, .name = name, .value = null, }); }
if (e.self_closing) { _ = try c.next(); return; } while (true) { const ev = try c.next(); switch (ev.event) { .start_element => |child| { if (std.mem.eql(u8, child.name, "RegistryKey")) { try c.registryKeyElement(child, ev.offset, ctx, component, rows); } else if (std.mem.eql(u8, child.name, "RegistryValue")) { try c.registryValueElement(child, ev.offset, ctx, component, rows); } else { try c.unsupportedElement(child, ev.offset); } }, .end_element => return, else => try c.ignorable(ev), } } }
fn registryValueElement( c: *Compiler, e: xml.Element, offset: usize, outer: RegistryContext, component: *wix.Component, rows: *std.ArrayList(wix.RegistryRow), ) Stop!void { const here = c.location(offset); const a = try c.attributes(e, &.{ "Id", "Root", "Key", "Name", "Value", "Type", "Action", "KeyPath" }); const root = if (a[1]) |v| try c.registryRoot(v) else outer.root orelse blk: { try c.diags.err(here, "<RegistryValue> needs a Root, here or on a key around it", .{}); break :blk .hklm; }; const key = try c.subkey(outer.key, if (a[2]) |v| v.value else null) orelse blk: { try c.diags.err(here, "<RegistryValue> needs a Key, here or on a key around it", .{}); break :blk ""; }; const Type = enum { string, integer, expandable, binary, multi_string }; const kind: Type = if (a[5]) |v| try c.oneOf(Type, v, &.{ .{ "string", .string }, .{ "integer", .integer }, .{ "expandable", .expandable }, .{ "binary", .binary }, .{ "multiString", .multi_string }, }) else .string; const Action = enum { write, append, prepend }; const action: Action = if (a[6]) |v| try c.oneOf(Action, v, &.{ .{ "write", .write }, .{ "append", .append }, .{ "prepend", .prepend }, }) else .write; if (action != .write and kind != .multi_string) { try c.diags.err(a[6].?.location, "only a multiString value can be appended to or prepended to", .{}); }
// A multi-string's strings, from its Value and each // <MultiStringValue> in it. var strings: std.ArrayList([]const u8) = .empty; if (a[4]) |v| try strings.append(c.arena, v.value); if (e.self_closing) { _ = try c.next(); } else { while (true) { const ev = try c.next(); switch (ev.event) { .start_element => |child| { if (std.mem.eql(u8, child.name, "MultiStringValue") and kind == .multi_string and c.language == .v4) { // WiX v4 gives the string as a Value. const m = try c.attributes(child, &.{"Value"}); try strings.append(c.arena, try c.required(child, ev.offset, m[0], "Value")); try c.noChildren(child); } else if (std.mem.eql(u8, child.name, "MultiStringValue") and kind == .multi_string) { _ = try c.attributes(child, &.{}); const text = c.reader.textAlloc(c.arena, .strict) catch |err| { try c.diags.err(c.location(ev.offset), "malformed XML: {s}", .{@errorName(err)}); return error.Stop; }; try c.text.append(c.arena, .{ .element = child.name, .attribute = "the text", .value = text, .location = c.location(ev.offset) }); try strings.append(c.arena, text); } else { try c.unsupportedElement(child, ev.offset); } }, .end_element => break, else => try c.ignorable(ev), } } }
const value: ?[]const u8 = switch (kind) { // A string beginning with `#` would be read as one of the other // types, so the `#` is doubled. .string => if (strings.items.len == 0) null else if (std.mem.startsWith(u8, strings.items[0], "#")) try std.fmt.allocPrint(c.arena, "#{s}", .{strings.items[0]}) else strings.items[0], .integer => blk: { const v = if (strings.items.len > 0) strings.items[0] else ""; _ = std.fmt.parseInt(i32, v, 10) catch { try c.diags.err(here, "\"{s}\" is not an integer", .{v}); }; break :blk try std.fmt.allocPrint(c.arena, "#{s}", .{v}); }, .expandable => try std.fmt.allocPrint(c.arena, "#%{s}", .{if (strings.items.len > 0) strings.items[0] else ""}), .binary => blk: { const v = if (strings.items.len > 0) strings.items[0] else ""; for (v) |ch| if (!std.ascii.isHex(ch)) { try c.diags.err(here, "\"{s}\" is not hexadecimal", .{v}); break; }; break :blk try std.fmt.allocPrint(c.arena, "#x{s}", .{v}); }, // The strings joined with `[~]`; a `[~]` at the start appends to // what is there, at the end prepends, and at both replaces it. .multi_string => blk: { const joined = try std.mem.join(c.arena, "[~]", strings.items); break :blk switch (action) { .write => try std.fmt.allocPrint(c.arena, "[~]{s}[~]", .{joined}), .append => try std.fmt.allocPrint(c.arena, "[~]{s}", .{joined}), .prepend => try std.fmt.allocPrint(c.arena, "{s}[~]", .{joined}), }; }, }; const name: ?[]const u8 = if (a[3]) |v| (if (v.value.len == 0) null else v.value) else null; const id = if (a[0]) |v| v.value else try c.registryId(root, key, name); try rows.append(c.arena, .{ .location = here, .id = id, .root = root, .key = key, .name = name, .value = value }); if (a[7]) |v| if (try c.yesNo(v)) try c.setKeyPath(component, offset, id, true); }
fn environmentElement(c: *Compiler, e: xml.Element, offset: usize, list: *std.ArrayList(wix.Environment)) Stop!void { const here = c.location(offset); const a = try c.attributes(e, &.{ "Id", "Name", "Value", "Action", "Part", "Permanent", "System", "Separator" }); const name = try c.required(e, offset, a[1], "Name"); const Action = enum { create, set, remove }; const action: Action = if (a[3]) |v| try c.oneOf(Action, v, &.{ .{ "create", .create }, .{ "set", .set }, .{ "remove", .remove }, }) else .set; const Part = enum { all, first, last }; const part: Part = if (a[4]) |v| try c.oneOf(Part, v, &.{ .{ "all", .all }, .{ "first", .first }, .{ "last", .last }, }) else .all; const permanent = if (a[5]) |v| try c.yesNo(v) else false; const system = if (a[6]) |v| try c.yesNo(v) else false; const separator = if (a[7]) |v| v.value else ";"; if (action == .create and part != .all) { try c.diags.err(here, "an environment variable that is only created, never set, cannot be added to in part", .{}); }
// The prefixes the Environment table reads: what to do on install, // `-` to undo it on uninstall, `*` for the system's environment. var prefixed: std.ArrayList(u8) = .empty; try prefixed.append(c.arena, switch (action) { .create => '+', .set => '=', .remove => '!', }); if (!permanent and action != .remove) try prefixed.append(c.arena, '-'); if (system) try prefixed.append(c.arena, '*'); try prefixed.appendSlice(c.arena, name);
const value: ?[]const u8 = if (a[2]) |v| switch (part) { .all => v.value, .first => try std.fmt.allocPrint(c.arena, "{s}{s}[~]", .{ v.value, separator }), .last => try std.fmt.allocPrint(c.arena, "[~]{s}{s}", .{ separator, v.value }), } else null; if (c.language == .v4) try c.noAccessModifier(a[0]); try list.append(c.arena, .{ .location = here, // WiX v4 leaves the Id to be made; zixl makes it from the // variable and what is done with it. .id = if (c.language == .v4 and a[0] == null) try c.generatedId("env", &.{prefixed.items}) else try c.required(e, offset, a[0], "Id"), .name = prefixed.items, .value = value, }); try c.noChildren(e); }
/// The condition a <Condition> holds as its text, which must not be /// empty. fn conditionText(c: *Compiler, e: xml.Element, offset: usize) Stop![]const u8 { if (e.self_closing) { _ = try c.next(); try c.diags.err(c.location(offset), "<Condition> holds no condition", .{}); return ""; } const text = c.reader.trimmedTextAlloc(c.arena, .strict) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => { try c.diags.err(c.location(offset), "malformed XML: {s}", .{@errorName(err)}); return error.Stop; }, }; if (text.len == 0) try c.diags.err(c.location(offset), "<Condition> holds no condition", .{}); try c.text.append(c.arena, .{ .element = e.name, .attribute = "the text", .value = text, .location = c.location(offset) }); return text; }
fn majorUpgradeElement(c: *Compiler, e: xml.Element, offset: usize) Stop!wix.MajorUpgrade { const here = c.location(offset); const a = try c.attributes(e, &.{ "AllowDowngrades", "AllowSameVersionUpgrades", "Disallow", "DisallowUpgradeErrorMessage", "DowngradeErrorMessage", "IgnoreRemoveFailure", "MigrateFeatures", "RemoveFeatures", "Schedule", "IgnoreLanguage", }); // WiX v4 finds only installed versions in the product's language, // unless told to ignore it. var m: wix.MajorUpgrade = .{ .location = here, .same_language = c.language == .v4 }; if (a[9]) |v| { if (c.language == .v3) { try c.diags.err(v.location, "attribute IgnoreLanguage of <MajorUpgrade> is not supported", .{}); } else if (try c.yesNo(v)) m.same_language = false; } if (a[0]) |v| m.allow_downgrades = try c.yesNo(v); if (a[1]) |v| m.allow_same_version_upgrades = try c.yesNo(v); if (a[2]) |v| m.disallow = try c.yesNo(v); if (a[3]) |v| m.disallow_message = v.value; if (a[4]) |v| m.downgrade_message = v.value; if (a[5]) |v| m.ignore_remove_failure = try c.yesNo(v); if (a[6]) |v| m.migrate_features = try c.yesNo(v); if (a[7]) |v| m.remove_features = v.value; if (a[8]) |v| { const S = enum { validate, initialize, finalize, execute }; m.schedule = switch (try c.oneOf(S, v, &.{ .{ "afterInstallValidate", .validate }, .{ "afterInstallInitialize", .initialize }, .{ "afterInstallFinalize", .finalize }, .{ "afterInstallExecute", .execute }, .{ "afterInstallExecuteAgain", .execute }, })) { .validate => .after_install_validate, .initialize => .after_install_initialize, .finalize => .after_install_finalize, .execute => blk: { try c.diags.err(v.location, "Schedule=\"{s}\" is not supported yet", .{v.value}); break :blk .after_install_validate; }, }; } // What WiX requires of the attributes together. if (!m.allow_downgrades and m.downgrade_message == null) { try c.diags.err(here, "<MajorUpgrade> needs a DowngradeErrorMessage, unless AllowDowngrades=\"yes\"", .{}); } if (m.allow_downgrades and m.allow_same_version_upgrades) { try c.diags.err(here, "AllowSameVersionUpgrades=\"yes\" cannot go with AllowDowngrades=\"yes\"", .{}); } if (m.disallow and m.disallow_message == null) { try c.diags.err(here, "<MajorUpgrade Disallow=\"yes\"> needs a DisallowUpgradeErrorMessage", .{}); } try c.noChildren(e); return m; }
fn iconElement(c: *Compiler, e: xml.Element, offset: usize, section: *Section) Stop!void { const a = try c.attributes(e, &.{ "Id", "SourceFile" }); const id = try c.required(e, offset, a[0], "Id"); const source = try c.required(e, offset, a[1], "SourceFile"); const resolved = try c.resolve(source); const contents = c.files.read(c.arena, resolved) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => blk: { try c.diags.err(a[1].?.location, "the icon \"{s}\" cannot be read (looked for {s})", .{ source, resolved }); break :blk ""; }, }; try section.icons.append(c.arena, .{ .location = c.location(offset), .id = id, .contents = contents }); try c.noChildren(e); }
/// A <Shortcut>, in a <File> (`file`, which it opens) or a <Component>. fn shortcutElement(c: *Compiler, e: xml.Element, offset: usize, file: ?[]const u8, component_directory: []const u8, list: *std.ArrayList(wix.Shortcut), section: *Section) Stop!void { const here = c.location(offset); const a = try c.attributes(e, &.{ "Id", "Directory", "Name", "ShortName", "Description", "Target", "Arguments", "Icon", "IconIndex", "Show", "Hotkey", "Advertise", "WorkingDirectory", "Subdirectory", "WorkingSubdirectory", }); const v4 = c.language == .v4; if (!v4) for (a[13..]) |extra| if (extra) |v| { try c.diags.err(v.location, "this attribute of <Shortcut> is WiX v4's, not WiX v3's", .{}); }; if (a[11]) |v| { if (try c.yesNo(v)) try c.diags.err(v.location, "advertised shortcuts are not supported yet", .{}); } // In WiX v4, a shortcut is in its component's directory unless it // says otherwise. var directory = if (v4 and a[1] == null) component_directory else try c.required(e, offset, a[1], "Directory"); try section.refs.append(c.arena, .{ .kind = .directory, .ref = .{ .location = here, .id = directory } }); if (v4) if (a[13]) |v| { directory = try c.subdirectory(directory, v, section); }; const target: []const u8 = if (a[5]) |v| v.value else if (file) |f| try std.fmt.allocPrint(c.arena, "[#{s}]", .{f}) else if (v4) // WiX v4: a component's shortcut opens its directory. try std.fmt.allocPrint(c.arena, "[{s}]", .{component_directory}) else blk: { try c.diags.err(here, "a <Shortcut> outside a <File> needs a Target", .{}); break :blk ""; }; const name: []const u8 = if (a[2]) |v| try c.longName(v) else try c.required(e, offset, null, "Name"); if (v4) try c.noAccessModifier(a[0]); var shortcut: wix.Shortcut = .{ .location = here, .id = if (v4 and a[0] == null) try c.generatedId("sct", &.{ directory, name }) else try c.required(e, offset, a[0], "Id"), .directory = directory, .name = name, .short_name = if (a[3]) |v| try c.shortName(v) else null, .target = target, }; if (a[4]) |v| shortcut.description = v.value; if (a[6]) |v| shortcut.arguments = v.value; if (a[7]) |v| { shortcut.icon = v.value; try section.refs.append(c.arena, .{ .kind = .icon, .ref = .{ .location = v.location, .id = v.value } }); } if (a[8]) |v| shortcut.icon_index = try c.integer(i32, v); if (a[9]) |v| shortcut.show = try c.oneOf(i32, v, &.{ .{ "normal", 1 }, .{ "maximized", 3 }, .{ "minimized", 7 } }); if (a[10]) |v| shortcut.hotkey = try c.integer(i32, v); if (a[12]) |v| { shortcut.working_directory = v.value; try section.refs.append(c.arena, .{ .kind = .directory, .ref = .{ .location = v.location, .id = v.value } }); if (v4) if (a[14]) |w| { shortcut.working_directory = try c.subdirectory(v.value, w, section); }; } else if (v4) if (a[14]) |w| { try c.diags.err(w.location, "a WorkingSubdirectory is below a WorkingDirectory, and this <Shortcut> has none", .{}); }; shortcut.properties = try c.shortcutProperties(e, shortcut.id); try list.append(c.arena, shortcut); }
/// The <ShortcutProperty> children of a <Shortcut>, as WiX v3.6 and /// later and WiX v4 have them. One with no Id gets a stable one, made /// from its shortcut and its key. fn shortcutProperties(c: *Compiler, e: xml.Element, shortcut: []const u8) Stop![]const wix.ShortcutProperty { var list: std.ArrayList(wix.ShortcutProperty) = .empty; if (e.self_closing) { _ = try c.next(); return list.items; } while (true) { const ev = try c.next(); switch (ev.event) { .start_element => |child| { if (!std.mem.eql(u8, child.name, "ShortcutProperty")) { try c.unsupportedElement(child, ev.offset); continue; } const a = try c.attributes(child, &.{ "Id", "Key", "Value" }); if (c.language == .v4) try c.noAccessModifier(a[0]); const key = try c.required(child, ev.offset, a[1], "Key"); // The table's PropVariantValue may not be null, and an // empty string is null. const value = try c.required(child, ev.offset, a[2], "Value"); if (a[2] != null and value.len == 0) try c.diags.err(a[2].?.location, "a <ShortcutProperty> needs a Value that is not empty", .{}); try list.append(c.arena, .{ .location = c.location(ev.offset), .id = if (a[0]) |v| v.value else try c.generatedId("scp", &.{ shortcut, key }), .key = key, .value = value, }); try c.noChildren(child); }, .end_element => return list.items, else => try c.ignorable(ev), } } }
/// A <RemoveFolder> or a <RemoveFile>. fn removalElement(c: *Compiler, e: xml.Element, offset: usize, directory: []const u8, list: *std.ArrayList(wix.Removal), section: *Section) Stop!void { const is_file = std.mem.eql(u8, e.name, "RemoveFile"); const a = try c.attributes(e, &.{ "Id", "Directory", "On", "Name", "ShortName", "Subdirectory" }); if (!is_file and (a[3] != null or a[4] != null)) { try c.diags.err(c.location(offset), "<RemoveFolder> removes a folder, and takes no Name", .{}); } const v4 = c.language == .v4; if (!v4) if (a[5]) |v| try c.diags.err(v.location, "attribute Subdirectory of <{s}> is not supported", .{e.name}); if (v4) try c.noAccessModifier(a[0]); var removal: wix.Removal = .{ .location = c.location(offset), .id = if (v4 and a[0] == null) "" else try c.required(e, offset, a[0], "Id"), .directory = directory, .install_mode = if (a[2]) |v| try c.oneOf(i32, v, &.{ .{ "install", 1 }, .{ "uninstall", 2 }, .{ "both", 3 } }) else blk: { _ = try c.required(e, offset, null, "On"); break :blk 2; }, }; if (a[1]) |v| { removal.directory = v.value; try section.refs.append(c.arena, .{ .kind = .directory, .ref = .{ .location = v.location, .id = v.value } }); } if (v4) if (a[5]) |v| { removal.directory = try c.subdirectory(removal.directory, v, section); }; if (is_file) { // A name may hold the wildcards * and ?, which a short name may // not, so a short name is made only for a name without them. removal.name = if (a[3]) |v| v.value else try c.required(e, offset, null, "Name"); if (a[4]) |v| removal.short_name = try c.shortName(v); } // WiX v4 leaves the Id to be made; zixl makes it from what is // removed, and when. if (removal.id.len == 0) removal.id = try c.generatedId("rmf", &.{ removal.directory, removal.name orelse "", e.name }); try list.append(c.arena, removal); try c.noChildren(e); }
/// Reads a <File>, returning its Id when it is marked as the key path. /// What a <File> said of itself beyond the file: whether it is its /// component's key path, and, for a WiX v4 file written outside any /// component, what the component made for it is to be. const FileResult = struct { id: []const u8, key_path: bool, directory: []const u8, bitness: wix.Bitness = .default, condition: ?[]const u8 = null, };
/// Reads a <File> in `directory`. A `naked` one, written outside any /// component as WiX v5 allows, may name its own directory, which is /// INSTALLFOLDER when neither it nor where it is written says. fn fileElement( c: *Compiler, e: xml.Element, offset: usize, context_directory: ?[]const u8, naked: bool, list: *std.ArrayList(wix.File), shortcuts: *std.ArrayList(wix.Shortcut), section: *Section, ) Stop!FileResult { const here = c.location(offset); const a = try c.attributes(e, &.{ "Id", "Name", "ShortName", "Source", "KeyPath", "Vital", "Directory", "Subdirectory", "Bitness", "Condition", "DefaultVersion", "DefaultLanguage" }); if (!naked) for (a[6..10]) |extra| if (extra) |v| { try c.diags.err(v.location, "only a <File> outside a <Component> may say this; a component's says it for its files", .{}); }; var result: FileResult = .{ .id = "", .key_path = false, .directory = context_directory orelse "" }; if (naked) { if (a[6]) |v| { result.directory = v.value; try section.refs.append(c.arena, .{ .kind = .directory, .ref = .{ .location = v.location, .id = v.value } }); } else if (context_directory == null) { result.directory = "INSTALLFOLDER"; try section.refs.append(c.arena, .{ .kind = .directory, .ref = .{ .location = here, .id = "INSTALLFOLDER" } }); } if (a[7]) |v| result.directory = try c.subdirectory(result.directory, v, section); if (a[8]) |v| result.bitness = try c.oneOf(wix.Bitness, v, &.{ .{ "default", .default }, .{ "always32", .always32 }, .{ "always64", .always64 }, }); if (a[9]) |v| result.condition = v.value; } const directory = result.directory; const source = try c.required(e, offset, a[3], "Source"); const resolved = try c.resolve(source); // The name defaults to the source's own, and the Id to the name. const name = if (a[1]) |v| try c.longName(v) else std.fs.path.basenamePosix(resolved); if (c.language == .v4) try c.noAccessModifier(a[0]); const id = if (a[0]) |v| v.value else if (c.language == .v4) // WiX v4 makes a stable Id for a file that has none; zixl's is // its own, from where the file is installed. try c.generatedId("fil", &.{ directory, name }) else blk: { if (!isIdentifier(name)) { try c.diags.err(here, "<File> needs an Id, since its name \"{s}\" is not one", .{name}); } break :blk name; }; // Without a Source, which is reported already, there is nothing to // read. const contents = if (a[3] == null) "" else c.files.read(c.arena, resolved) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.FileNotFound => blk: { try c.diags.err(a[3].?.location, "the source path \"{s}\" of file \"{s}\" does not exist (looked for {s})", .{ source, id, resolved }); break :blk ""; }, error.ReadFailed => blk: { try c.diags.err(a[3].?.location, "the source path \"{s}\" of file \"{s}\" cannot be read", .{ source, id }); break :blk ""; }, }; try list.append(c.arena, .{ .location = here, .id = id, .name = name, .short_name = if (a[2]) |v| try c.shortName(v) else null, .source = resolved, .contents = contents, .vital = if (a[5]) |v| try c.yesNo(v) else true, .default_version = if (a[10]) |v| try c.fileVersion(v) else null, .default_language = if (a[11]) |v| try c.fileLanguage(v) else null, }); if (e.self_closing) { _ = try c.next(); } else { while (true) { const ev = try c.next(); switch (ev.event) { .start_element => |child| { if (std.mem.eql(u8, child.name, "Shortcut")) { try c.shortcutElement(child, ev.offset, id, directory, shortcuts, section); } else { try c.unsupportedElement(child, ev.offset); } }, .end_element => break, else => try c.ignorable(ev), } } } result.id = id; result.key_path = if (a[4]) |v| try c.yesNo(v) else false; return result; }
/// A WiX v5 naked <File>: one written outside any component, which gets /// a component of its own, named for it, with a generated GUID. /// Returns the component's Id. fn nakedFileElement(c: *Compiler, e: xml.Element, offset: usize, directory: ?[]const u8, section: *Section) Stop![]const u8 { var files: std.ArrayList(wix.File) = .empty; var shortcuts: std.ArrayList(wix.Shortcut) = .empty; const f = try c.fileElement(e, offset, directory, true, &files, &shortcuts, section); try section.components.append(c.arena, .{ .location = c.location(offset), .id = f.id, .guid = "*", .directory = f.directory, .files = files.items, .shortcuts = shortcuts.items, .key_path = f.id, .bitness = f.bitness, .condition = f.condition, }); return f.id; }
fn mediaElement(c: *Compiler, e: xml.Element, offset: usize, list: *std.ArrayList(wix.Media)) Stop!void { const here = c.location(offset); const a = try c.attributes(e, &.{ "Id", "Cabinet", "EmbedCab", "CompressionLevel" }); var media: wix.Media = .{ .location = here, .disk_id = if (a[0]) |v| try c.integer(i32, v) else blk: { _ = try c.required(e, offset, null, "Id"); break :blk 1; }, // WiX v4 compresses a cabinet at medium by default. .compression = if (c.language == .v4) .medium else .mszip, }; // A cabinet's name is a file name, never a path: one kept beside the // package is written where the package is, and nowhere else. if (a[1]) |v| media.cabinet = try c.longName(v); if (a[2]) |v| media.embed = try c.yesNo(v); if (a[3]) |v| media.compression = try c.compressionLevel(v); if (media.cabinet == null) { try c.diags.err(here, "a <Media> without a Cabinet, for files left uncompressed beside the package, is not supported yet", .{}); } try list.append(c.arena, media); try c.noChildren(e); }
/// A source path made relative to the WiX source's directory, with `\` /// and `/` both taken as separators and `/` written between the parts. /// Case is left as it is. fn resolve(c: *Compiler, source: []const u8) Stop![]const u8 { const path = try c.arena.dupe(u8, source); std.mem.replaceScalar(u8, path, '\\', '/'); const absolute = path.len > 0 and path[0] == '/' or path.len > 2 and std.ascii.isAlphabetic(path[0]) and path[1] == ':' and path[2] == '/'; if (absolute) return path; const here = std.fs.path.dirnamePosix(c.sourcePath()) orelse return path; return std.fmt.allocPrint(c.arena, "{s}/{s}", .{ here, path }); }
fn sourcePath(c: *const Compiler) []const u8 { const path = c.arena.dupe(u8, c.source.path) catch return c.source.path; std.mem.replaceScalar(u8, path, '\\', '/'); return path; }
/// A long file name: anything Windows allows in one. fn longName(c: *Compiler, a: Attribute) Stop![]const u8 { if (a.value.len == 0 or std.mem.indexOfAny(u8, a.value, "\\/:*?\"<>|") != null) { try c.diags.err(a.location, "\"{s}\" is not a valid file name", .{a.value}); } return a.value; }
fn shortName(c: *Compiler, a: Attribute) Stop![]const u8 { if (!@import("shortname.zig").isValid(a.value)) { try c.diags.err(a.location, "\"{s}\" is not a valid 8.3 short name", .{a.value}); } return a.value; }
// -- GUIDs -------------------------------------------------------------
/// A GUID as Windows Installer stores it: upper case, in braces. "*", /// where `generated` allows it, is kept as it is, to be replaced once /// every input has been read. fn guid(c: *Compiler, a: Attribute, generated: bool) Stop![]const u8 { if (generated and std.mem.eql(u8, a.value, "*")) return "*"; const id = UUID.deserialize(a.value) catch { try c.diags.err(a.location, "\"{s}\" is not a GUID", .{a.value}); return a.value; }; return c.arena.dupe(u8, &id.serializeRegistry()); }
// -- attributes ------------------------------------------------------
const Attribute = struct { value: []const u8, location: Location, };
/// The decoded values of the attributes named in `known`, in that order, /// reporting any other attribute as unsupported. fn attributes(c: *Compiler, e: xml.Element, comptime known: []const []const u8) Stop![known.len]?Attribute { var out: [known.len]?Attribute = @splat(null); attrs: for (e.attributes) |a| { inline for (known, 0..) |name, i| { if (std.mem.eql(u8, a.name, name)) { out[i] = .{ .value = xml.decodeAlloc(c.arena, a.raw_value, .strict, .attribute) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => { try c.diags.err(c.location(c.offsetOf(a.raw_value)), "the value of {s} cannot be decoded: {s}", .{ a.name, @errorName(err) }); continue :attrs; }, }, .location = c.location(c.offsetOf(a.name)), }; const stored = for (not_stored) |n| { if (std.mem.eql(u8, n, name)) break false; } else true; if (stored) try c.text.append(c.arena, .{ .element = e.name, .attribute = name, .value = out[i].?.value, .location = out[i].?.location, }); continue :attrs; } } try c.unsupportedAttribute(e, a); } return out; }
fn required(c: *Compiler, e: xml.Element, offset: usize, a: ?Attribute, comptime name: []const u8) Stop![]const u8 { if (a) |v| return v.value; try c.diags.err(c.location(offset), "<{s}> needs a {s} attribute", .{ e.name, name }); return ""; }
/// A required identifier. In WiX v4 one may begin with an access /// modifier, which zixl does not support. fn identifier(c: *Compiler, e: xml.Element, offset: usize, a: ?Attribute, comptime name: []const u8) Stop![]const u8 { const value = try c.required(e, offset, a, name); if (c.language == .v4) try c.noAccessModifier(a); return value; }
fn noAccessModifier(c: *Compiler, a: ?Attribute) Stop!void { const v = a orelse return; const modifiers = [_][]const u8{ "global ", "library ", "file ", "section ", "private ", "public ", "protected ", "internal ", "virtual ", "override " }; for (modifiers) |m| if (std.mem.startsWith(u8, v.value, m)) { try c.diags.err(v.location, "the access modifier \"{s}\" is not supported", .{std.mem.trimEnd(u8, m, " ")}); return; }; }
fn integer(c: *Compiler, comptime T: type, a: Attribute) Stop!T { return std.fmt.parseInt(T, a.value, 10) catch { try c.diags.err(a.location, "\"{s}\" is not an integer", .{a.value}); return 0; }; }
/// A code page: a number, or in WiX v4 its web name too, as /// `Windows-1252` or `UTF-8`. fn codepage(c: *Compiler, a: Attribute) Stop!u16 { const by_name: ?u16 = if (c.language == .v4) blk: { if (std.ascii.eqlIgnoreCase(a.value, "utf-8")) break :blk codepage_.utf8; if (std.ascii.startsWithIgnoreCase(a.value, "windows-")) break :blk std.fmt.parseInt(u16, a.value["windows-".len..], 10) catch null; break :blk null; } else null; const cp = by_name orelse std.fmt.parseInt(u16, a.value, 10) catch { try c.diags.err(a.location, "\"{s}\" is not a code page {s}", .{ a.value, if (c.language == .v4) "number or name zixl knows" else "number" }); return 1252; }; if (!codepage_.isSupported(cp)) { try c.diags.err(a.location, "code page {d} is not supported; zixl stores strings in 0 (ASCII), 874, 1250 to 1258, or 65001", .{cp}); } else if (cp == codepage_.utf8 and c.language == .v3) { // WiX v4 makes UTF-8 its default, and says Windows Installer's // support for it is limited; WiX v3 does not. try c.diags.warn(a.location, "Windows Installer does not officially support code page 65001, UTF-8", .{}); } return cp; }
/// A version as the File table records one: up to four numbers, each /// at most 65535, separated by dots. fn fileVersion(c: *Compiler, a: Attribute) Stop![]const u8 { var parts: usize = 0; var it = std.mem.splitScalar(u8, a.value, '.'); while (it.next()) |part| : (parts += 1) { _ = std.fmt.parseInt(u16, part, 10) catch break; } else if (parts <= 4) return a.value; try c.diags.err(a.location, "\"{s}\" is not a file version: up to four numbers from 0 to 65535, separated by dots", .{a.value}); return a.value; }
/// Languages as the File table records them: decimal language IDs, /// separated by commas. fn fileLanguage(c: *Compiler, a: Attribute) Stop![]const u8 { var it = std.mem.splitScalar(u8, a.value, ','); while (it.next()) |part| { _ = std.fmt.parseInt(u16, part, 10) catch break; } else if (a.value.len <= 20) return a.value; try c.diags.err(a.location, "\"{s}\" is not a list of languages: language IDs from 0 to 65535, separated by commas, in at most 20 characters", .{a.value}); return a.value; }
fn yesNo(c: *Compiler, a: Attribute) Stop!bool { return c.oneOf(bool, a, &.{ .{ "yes", true }, .{ "no", false } }); }
fn oneOf(c: *Compiler, comptime T: type, a: Attribute, comptime choices: []const struct { []const u8, T }) Stop!T { inline for (choices) |choice| { if (std.mem.eql(u8, a.value, choice[0])) return choice[1]; } try c.diags.err(a.location, "\"{s}\" is not one of the values allowed here", .{a.value}); return choices[0][1]; }
// -- structure -------------------------------------------------------
fn noChildren(c: *Compiler, e: xml.Element) Stop!void { if (e.self_closing) { _ = try c.next(); return; } while (true) { const ev = try c.next(); switch (ev.event) { .start_element => |child| try c.unsupportedElement(child, ev.offset), .end_element => return, else => try c.ignorable(ev), } } }
/// Accepts whitespace, comments and the XML declaration between elements, /// and reports anything else. fn ignorable(c: *Compiler, ev: Located) Stop!void { switch (ev.event) { .text => |t| if (!xml.isWhitespace(t)) { try c.diags.err(c.location(ev.offset), "unexpected text", .{}); }, .comment, .declaration => {}, .cdata => try c.diags.err(c.location(ev.offset), "unexpected CDATA", .{}), .pi => |pi| try c.diags.err(c.location(ev.offset), "unexpected processing instruction <?{s}?>", .{pi.target}), .doctype => try c.diags.err(c.location(ev.offset), "a WiX source may not have a document type declaration", .{}), else => {}, } }
fn unsupportedElement(c: *Compiler, e: xml.Element, offset: usize) Stop!void { try c.diags.err(c.location(offset), "<{s}> is not supported here", .{e.name}); if (!e.self_closing) { try c.skip(); } else { _ = try c.next(); } }
fn unsupportedAttribute(c: *Compiler, e: xml.Element, a: xml.Attribute) Stop!void { try c.diags.err(c.location(c.offsetOf(a.name)), "attribute {s} of <{s}> is not supported", .{ a.name, e.name }); }
fn isIdentifier(s: []const u8) bool { if (s.len == 0 or !(std.ascii.isAlphabetic(s[0]) or s[0] == '_')) return false; for (s) |ch| if (!(std.ascii.isAlphanumeric(ch) or ch == '_' or ch == '.')) return false; return true; }};