// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! Where zixl gets the files it reads: WiX sources, included files, and the //! files named by source paths. //! //! zixl never opens a file itself. The command line supplies `Disk`, and //! tests supply `Memory`, so that each decides where files come from and the //! library does not have to know. The command line wraps its `Disk` in a //! `Recording`, which is how the `build.zig` step learns what a package was //! made from. const std = @import("std"); const Allocator = std.mem.Allocator; const Io = std.Io; const FileSource = @This(); ptr: *anyopaque, vtable: *const VTable, pub const Error = error{ FileNotFound, /// Anything else that stopped the file being read. ReadFailed, } || Allocator.Error; pub const VTable = struct { /// The whole contents of the file at `path`, allocated with `gpa`. read: *const fn (ptr: *anyopaque, gpa: Allocator, path: []const u8) Error![]u8, }; pub fn read(fs: FileSource, gpa: Allocator, path: []const u8) Error![]u8 { return fs.vtable.read(fs.ptr, gpa, path); } /// Files held in memory, keyed by path exactly as it is asked for. pub const Memory = struct { files: []const File, pub const File = struct { path: []const u8, contents: []const u8, }; pub fn fileSource(m: *Memory) FileSource { return .{ .ptr = m, .vtable = &.{ .read = readMemory } }; } fn readMemory(ptr: *anyopaque, gpa: Allocator, path: []const u8) Error![]u8 { const m: *Memory = @ptrCast(@alignCast(ptr)); for (m.files) |f| { if (std.mem.eql(u8, f.path, path)) return gpa.dupe(u8, f.contents); } return error.FileNotFound; } }; /// Files on disk, with relative paths taken from the current directory. pub const Disk = struct { io: Io, pub fn fileSource(d: *Disk) FileSource { return .{ .ptr = d, .vtable = &.{ .read = readDisk } }; } fn readDisk(ptr: *anyopaque, gpa: Allocator, path: []const u8) Error![]u8 { const d: *Disk = @ptrCast(@alignCast(ptr)); return Io.Dir.cwd().readFileAlloc(d.io, path, gpa, .unlimited) catch |e| switch (e) { error.FileNotFound => error.FileNotFound, error.OutOfMemory => error.OutOfMemory, else => error.ReadFailed, }; } }; /// Another file source, noting the path of every file it hands over, so that /// whatever builds a package can say afterwards what the package was made /// from. Files asked for and not found are not noted. pub const Recording = struct { inner: FileSource, gpa: Allocator, paths: std.ArrayList([]const u8) = .empty, pub fn fileSource(r: *Recording) FileSource { return .{ .ptr = r, .vtable = &.{ .read = readRecording } }; } pub fn deinit(r: *Recording) void { for (r.paths.items) |p| r.gpa.free(p); r.paths.deinit(r.gpa); } fn readRecording(ptr: *anyopaque, gpa: Allocator, path: []const u8) Error![]u8 { const r: *Recording = @ptrCast(@alignCast(ptr)); const contents = try r.inner.read(gpa, path); errdefer gpa.free(contents); for (r.paths.items) |p| { if (std.mem.eql(u8, p, path)) return contents; } const copy = try r.gpa.dupe(u8, path); errdefer r.gpa.free(copy); try r.paths.append(r.gpa, copy); return contents; } }; test "memory" { var m: Memory = .{ .files = &.{.{ .path = "a.wxs", .contents = "" }} }; const fs = m.fileSource(); const got = try fs.read(std.testing.allocator, "a.wxs"); defer std.testing.allocator.free(got); try std.testing.expectEqualStrings("", got); try std.testing.expectError(error.FileNotFound, fs.read(std.testing.allocator, "b.wxs")); } test "recording" { const gpa = std.testing.allocator; var m: Memory = .{ .files = &.{ .{ .path = "a.wxs", .contents = "" }, .{ .path = "b.wxi", .contents = "" }, } }; var r: Recording = .{ .inner = m.fileSource(), .gpa = gpa }; defer r.deinit(); const fs = r.fileSource(); for ([_][]const u8{ "a.wxs", "b.wxi", "a.wxs" }) |path| gpa.free(try fs.read(gpa, path)); try std.testing.expectError(error.FileNotFound, fs.read(gpa, "c.wxs")); try std.testing.expectEqual(2, r.paths.items.len); try std.testing.expectEqualStrings("a.wxs", r.paths.items[0]); try std.testing.expectEqualStrings("b.wxi", r.paths.items[1]); }