about summary refs log tree commit diff
path: root/src/main.zig
diff options
context:
space:
mode:
authorRobert Günzler <r@gnzler.io>2026-09-18 04:00:18 +0200
committerRobert Günzler <r@gnzler.io>2026-09-22 22:37:51 +0200
commit5bc242742e1225c7c3b87a31c2c2a3998ac18685 (patch)
treebaae923037b49ba31bb0a15ece2eec4bea13ab25 /src/main.zig
parentc677e8873e0eae790a1b8b07844a10efa9f63a1e (diff)
downloadsite-5bc242742e1225c7c3b87a31c2c2a3998ac18685.tar.gz
v2
Signed-off-by: Robert Günzler <r@gnzler.io>
Diffstat (limited to '')
-rw-r--r--src/main.zig782
1 files changed, 689 insertions, 93 deletions
diff --git a/src/main.zig b/src/main.zig
index 354eed9..2134d45 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,127 +1,723 @@
-// TODO: just let compiler infer error set through recursive calls
-// depends on: https://github.com/ziglang/zig/issues/2971
-
-// This code is heavily insprired by Devine's implementation in C:
-// https://github.com/hundredrabbits/100r.co/blob/main/src/main.c and his
-// personal site https://github.com/XXIIVV/Oscean
-
-const Context = @import("Context.zig");
-const os = @import("os.zig");
-
 const std = @import("std");
+const Io = std.Io;
+const Allocator = std.mem.Allocator;
+const linux = std.os.linux;
+
+const Digester = std.crypto.hash.Md5;
+pub const Digest = [16]u8;
 
 const log = std.log.scoped(.rbc);
 
-// tune std.log options
+const BUFFER_SIZE = 1024;
+
+// tune log options
 pub const std_options = std.Options{
     .log_level = if (@import("builtin").mode == .Debug) .debug else .info,
 };
 
+pub fn main(init: std.process.Init) !void {
+    try Cli.run(init);
+}
+
 const Cli = struct {
-    const Defaults = struct {
-        const ROOT: []const u8 = "src/inc";
-        const SITE: []const u8 = "site";
+    fn usage() void {
+        std.debug.print("usage: rbc SOURCE TARGET-DIR\n\n", .{});
+        std.debug.print("Writes SOURCE into TARGET-DIR, resolving references to other files in ROOT (= parent dir of SOURCE).\n", .{});
+        std.debug.print("\n", .{});
+    }
+
+    fn run(init: std.process.Init) !void {
+        const io = init.io;
+        const gpa = init.gpa;
+
+        const args = try init.minimal.args.toSlice(gpa);
+        defer gpa.free(args);
+
+        // parse options
+        for (args) |arg| {
+            if (std.mem.eql(u8, arg, "help")) return Cli.usage();
+            if (std.mem.eql(u8, arg, "--help")) return Cli.usage();
+            if (std.mem.eql(u8, arg, "-h")) return Cli.usage();
+        }
+
+        if (args.len < 3) return Cli.usage();
+
+        // TODO: flag handling
+
+        const source_path = args[1];
+        const target_path = args[2];
+
+        log.info("target={s} source={s} ", .{ target_path, source_path });
+
+        // TODO: allow setting custom root path via flags
+        const root_path = std.fs.path.dirname(source_path) orelse return error.NoDirname;
+        const root_dir = try Io.Dir.cwd().openDir(io, root_path, .{});
+        defer root_dir.close(io);
+
+        var source = try File.init(io, root_dir, std.fs.path.basename(source_path), null);
+        defer source.close(io);
+
+        // open/create destination dir
+        const target_dir = try Io.Dir.cwd().createDirPathOpen(io, target_path, .{});
+        defer target_dir.close(io);
+
+        const b = try Builder.init(gpa, root_dir, source, target_dir);
+        defer b.deinit(gpa);
+        try b.build(io, gpa);
+    }
+};
+
+const File = struct {
+    path: []const u8,
+    fd: Io.File,
+    mmap: Io.File.MemoryMap,
+    mtime: Io.Timestamp,
+
+    fn init(io: Io, dir: Io.Dir, path: []const u8, options: ?Io.Dir.OpenFileOptions) !File {
+        const fd = try dir.openFile(io, path, options orelse .{
+            .lock = .exclusive,
+            .mode = .read_write,
+        });
+        var mmap = try fd.createMemoryMap(io, .{
+            .len = try fd.length(io),
+            // .protection = .{ .read = true, .write = false },
+        });
+        try mmap.read(io);
+        const stat = try fd.stat(io);
+        return .{
+            .path = path,
+            .fd = fd,
+            .mmap = mmap,
+            .mtime = stat.mtime,
+        };
+    }
+
+    fn digest(self: File) !Digest {
+        var buffer: [Digester.digest_length]u8 = undefined;
+        var d = Digester.init(.{});
+        d.update(self.mmap.memory);
+        var buf2: [BUFFER_SIZE]u8 = undefined;
+        d.update(try std.fmt.bufPrint(&buf2, "{d}", .{self.mtime}));
+        d.final(&buffer);
+        return buffer;
+    }
+
+    fn close(self: *File, io: Io) void {
+        self.mmap.destroy(io);
+        self.fd.close(io);
+    }
+};
+
+const Builder = struct {
+    root_dir: Io.Dir,
+    source: File,
+    target_dir: Io.Dir,
+    options: Options,
+
+    // track files which affect build output (paths relative to root_dir)
+    parts: std.ArrayList([]const u8),
+    // track component digests which should affect digest check
+    dgsts: std.ArrayList(Digest),
+
+    pub const Options = struct {
+        // TODO: this is a bit of a hack no?
+        offset_from_siteroot: ?[]const u8,
     };
 
-    fn run(init: std.process.Init, ctx: *Context) !void {
-        var argv = init.minimal.args.iterate();
+    const DELIM_START: []const u8 = "{[";
+    const DELIM_END: []const u8 = "]}";
+
+    fn init(gpa: Allocator, root_dir: Io.Dir, source: File, target_dir: Io.Dir) !*Builder {
+        const b = try gpa.create(Builder);
+        b.* = .{
+            .root_dir = root_dir,
+            .source = source,
+            .target_dir = target_dir,
+            .options = .{
+                .offset_from_siteroot = "/site",
+            },
+            .parts = .empty,
+            .dgsts = .empty,
+        };
+        return b;
+    }
+
+    fn deinit(self: *Builder, gpa: Allocator) void {
+        for (self.parts.items) |p| gpa.free(p);
+        self.parts.deinit(gpa);
+        self.dgsts.deinit(gpa);
+        gpa.destroy(self);
+    }
+
+    fn build(self: *Builder, io: Io, gpa: Allocator) !void {
+        log.debug("{s}: building", .{self.source.path});
+
+        // track source
+        try self.parts.append(gpa, try gpa.dupe(u8, self.source.path));
+        try self.dgsts.append(gpa, try self.source.digest());
+
+        const source_stem = std.fs.path.stem(self.source.path);
 
-        _ = argv.skip(); // skip command name
+        // handle meta pages before even creating target_path
+        if (std.mem.startsWith(u8, source_stem, "meta.tilde")) {
+            return try self.metaTilde(io, gpa);
+        }
+
+        // target file
+        const target_path = try std.fmt.allocPrint(gpa, "{s}.html", .{source_stem});
+        defer gpa.free(target_path);
+
+        // atomic?
+        const target_fd = try self.target_dir.createFile(io, target_path, .{
+            .lock = .exclusive,
+        });
+        defer target_fd.close(io);
 
-        if (argv.inner.remaining.len == 0) {
-            return Cli.helpCmd();
+        // check if we need to do any work
+        if (try targetExists(io, gpa, target_fd, self.root_dir)) {
+            defer log.debug("{s}: done", .{self.source.path});
+            return; // skip any further processing
         }
 
-        while (argv.next()) |arg| {
-            if (std.mem.eql(u8, arg, "help")) {
-                return Cli.helpCmd();
-            } else if (std.mem.eql(u8, arg, "gen")) {
-                try Cli.genCmd(init, &argv, ctx);
-            } else if (std.mem.eql(u8, arg, "md")) {
-                try Cli.mdCmd(ctx, &argv);
-            } else if (std.mem.eql(u8, arg, "watch")) {
-                try Cli.watchCmd(init, &argv);
-            } else if (std.mem.eql(u8, arg, "serve")) {
-                try Cli.serveCmd(init, &argv);
-            } else {
-                return Cli.helpCmd();
+        log.debug("{s}: writing", .{self.source.path});
+
+        var writer_buf: [BUFFER_SIZE]u8 = undefined;
+        var writer = target_fd.writer(io, &writer_buf);
+
+        // determine if this is a fragment and exit if not
+        if (try isFullPage(self.source)) {
+            // only copy source to target
+            try writer.interface.writeAll(self.source.mmap.memory);
+        } else {
+            // load baseof fragment
+            var baseof = try File.init(io, self.root_dir, "meta.baseof.htm", null);
+            defer baseof.close(io);
+
+            try self.inject(io, gpa, baseof, &writer.interface);
+        }
+
+        try self.finalize(gpa, target_fd);
+        defer log.debug("{s}: done", .{self.source.path});
+    }
+
+    // finalize computes the digest and attaches it to the target file
+    fn finalize(self: *Builder, gpa: Allocator, target_fd: Io.File) !void {
+        log.debug("{s}: finalizing", .{self.source.path});
+
+        const composite_dgst = compositeDigest(self.dgsts.items);
+        try setXattrSourceDigest(target_fd, composite_dgst);
+        try setXattrSourceComponents(gpa, target_fd, self.parts.items);
+    }
+
+    // inject interprets source and writes to w
+    fn inject(self: *Builder, io: Io, gpa: Allocator, source: File, w: *Io.Writer) !void {
+        var iter = std.mem.splitScalar(u8, source.mmap.memory, '\n');
+        while (iter.next()) |line| {
+            var offset: usize = 0;
+            parseLn: while (true) {
+                if (std.mem.findPos(u8, line, offset, DELIM_START)) |start| {
+                    if (std.mem.findPos(u8, line, offset + start + 1, DELIM_END)) |end| {
+                        // write out beginning of chunk
+                        try w.writeAll(line[offset..start]);
+
+                        // do templating
+                        const ref_name = line[(offset + start + DELIM_START.len)..end];
+                        try self.template(io, gpa, ref_name, w);
+
+                        // advance offset
+                        offset += end + DELIM_END.len;
+                    } else break :parseLn;
+                } else break :parseLn;
             }
+
+            // write out remaining chunk
+            try w.writeAll(line[offset..]);
+            try w.writeByte('\n');
         }
+        try w.flush();
+
+        // track injected file
+        try self.parts.append(gpa, try gpa.dupe(u8, source.path));
+        try self.dgsts.append(gpa, try source.digest());
     }
 
-    fn helpCmd() void {
-        std.debug.print("usage: rbc COMMAND\n\n", .{});
-        std.debug.print(" rbc gen ROOT SITE [force=true]\n", .{});
-        std.debug.print(" rbc watch\n", .{});
-        std.debug.print(" rbc serve\n", .{});
-        std.debug.print("\n", .{});
+    fn template(self: *Builder, io: Io, gpa: Allocator, ref_name: []const u8, w: *Io.Writer) anyerror!void {
+        log.debug("templating ref={s}", .{ref_name});
+
+        // special case for when we're injecting the source into the baseof
+        if (std.mem.eql(u8, ref_name, "builtin.fragment")) {
+            try w.writeByte('\n');
+            try self.inject(io, gpa, self.source, w);
+            try w.writeByte('\n');
+            return;
+        }
+        // builtin variables
+        if (std.mem.eql(u8, ref_name, "builtin.title")) {
+            const title = try formatName(gpa, std.fs.path.stem(self.source.path));
+            defer gpa.free(title);
+            try w.writeAll(title);
+            return;
+        }
+        if (std.mem.eql(u8, ref_name, "builtin.path")) {
+            try w.writeAll(self.source.path);
+            return;
+        }
+        // TODO: other builtins?
+
+        switch (ref_name[0]) {
+            '/' => try self.templatePortal(io, gpa, ref_name[1..], w),
+            '$' => return try self.templateLink(gpa, ref_name[1..], w),
+            '+' => return try self.templateName(gpa, ref_name[1..], w),
+            else => try self.templateRef(io, gpa, ref_name, w),
+        }
     }
 
-    fn genCmd(init: std.process.Init, argv: *std.process.Args.Iterator, ctx: *Context) !void {
-        const root = argv.next() orelse Cli.Defaults.ROOT;
-        const site = argv.next() orelse Cli.Defaults.SITE;
-        const force = blk: {
-            const forcearg = argv.next();
-            if (forcearg != null and std.mem.eql(u8, forcearg.?, "force=true")) {
-                break :blk true;
-            } else {
-                break :blk false;
+    fn templatePortal(self: *Builder, io: Io, gpa: Allocator, ref_name: []const u8, w: *Io.Writer) !void {
+        log.debug("portaling ref={s}", .{ref_name});
+
+        const ref_path = try std.fmt.allocPrint(gpa, "{s}.htm", .{ref_name});
+        defer gpa.free(ref_path);
+
+        var ref = try File.init(io, self.root_dir, ref_path, null);
+        defer ref.close(io);
+
+        try w.writeByte('\n');
+        try self.inject(io, gpa, ref, w);
+        try w.writeByte('\n');
+    }
+
+    fn templateRef(self: *Builder, io: Io, gpa: Allocator, ref_str: []const u8, w: *Io.Writer) !void {
+        const fragment = std.mem.indexOf(u8, ref_str, "#");
+        const ref = if (fragment) |i| ref_str[0..i] else ref_str;
+
+        log.debug("referencing ref={s}", .{ref});
+
+        const ref_path = try std.fmt.allocPrint(gpa, "{s}.html", .{ref});
+        defer gpa.free(ref_path);
+
+        // check ref'ed page can exist
+        {
+            const ref_htm = try std.fmt.allocPrint(gpa, "{s}.htm", .{ref});
+            defer gpa.free(ref_htm);
+
+            const possible_paths = [_][]const u8{ ref_path, ref_htm };
+            var found = false;
+            for (possible_paths) |p|
+                found = found or (blk: {
+                    self.root_dir.access(io, p, .{}) catch break :blk false;
+                    break :blk true;
+                });
+            if (!found) {
+                log.err("{s} not found", .{ref});
+                return error.FileNotFound;
             }
-        };
-        log.info("root = {s}", .{root});
-        log.info("site = {s}", .{site});
-        try ctx.index(try std.Io.Dir.cwd().openDir(init.io, root, .{ .iterate = true }));
-        try ctx.render(try std.Io.Dir.cwd().createDirPathOpen(init.io, site, .{}), force);
+        }
+
+        const ref_name = try formatName(gpa, ref);
+        defer gpa.free(ref_name);
+        const ref_link = try formatLink(gpa, ref, self.options.offset_from_siteroot);
+        defer gpa.free(ref_link);
+
+        try w.print(
+            \\<a href="{s}{s}">{s}</a>
+        , .{
+            ref_link,
+            if (fragment) |i| ref_str[i..] else "",
+            ref_name,
+        });
     }
 
-    fn mdCmd(init: std.process.Init, argv: *std.process.Args.Iterator, ctx: *Context) !void {
-        const root = argv.next() orelse Cli.Defaults.ROOT;
-        log.info("root = {s}", .{root});
+    fn templateLink(self: *Builder, gpa: Allocator, ref_name: []const u8, w: *std.Io.Writer) !void {
+        const link = try formatLink(gpa, ref_name, self.options.offset_from_siteroot);
+        defer gpa.free(link);
+        try w.writeAll(link);
+    }
 
-        try os.shell(init.io,
-            \\pandoc --from markdown+wikilinks_title_after_pipe+autolink_bare_uris --to html5 --lua-filter preproc.lua --strip-comments --output=-
-        , .{ .environ_map = &env_map, .cwd = .{ .path = root } });
+    fn templateName(_: *Builder, gpa: Allocator, ref_name: []const u8, w: *std.Io.Writer) !void {
+        const name = try formatName(gpa, ref_name);
+        defer gpa.free(name);
+        try w.writeAll(name);
     }
 
-    fn watchCmd(init: std.process.Init, argv: *std.process.Args.Iterator) !void {
-        const root = argv.next() orelse Cli.Defaults.ROOT;
-        const site = argv.next() orelse Cli.Defaults.SITE;
+    // meta_tilde implements
+    fn metaTilde(self: *Builder, io: Io, gpa: Allocator) !void {
+        const source_stem = std.fs.path.stem(self.source.path)[11..]; // trim "meta.tilde."
+        log.debug("meta.tilde ~{s}", .{source_stem});
+
+        const content = std.mem.trim(u8, self.source.mmap.memory, "\n\t\r");
+        const target_name = try std.fmt.allocPrint(gpa, "~{s}", .{source_stem});
+        defer gpa.free(target_name);
 
-        const exePath = blk: {
-            var buf: [std.os.linux.PATH_MAX]u8 = undefined;
-            const n = try std.process.executablePath(init.io, &buf);
-            break :blk buf[0..n];
+        const target_fd = try self.target_dir.createFile(io, target_name, .{ .exclusive = true });
+        if (try targetExists(io, gpa, target_fd, self.root_dir)) return;
+
+        var buffer: [BUFFER_SIZE]u8 = undefined;
+        var writer = target_fd.writer(io, &buffer);
+
+        try writer.interface.print(
+            \\<!DOCTYPE html>
+            \\<html lang="en">
+            \\<head>
+            \\<meta charset="utf-8">
+            \\<meta http-equiv="refresh" content="0; url={[LINK]s}">
+            \\<meta name="fediverse:creator" content="@robertgzr@toot.berlin">
+            \\<link rel="stylesheet" type="text/css" href="links/main.css">
+            \\</head>
+            \\<body>
+            \\<noscript>Continue to <a href="{[LINK]s}">{[LINK]s}</a>.</noscript>
+            \\<script>
+            \\'use strict'
+            \\window.location.replace("{[LINK]s}")
+            \\</script>
+            \\</body>
+            \\</html>
+            \\
+        , .{
+            .LINK = content,
+        });
+        try writer.interface.flush();
+
+        try self.finalize(gpa, target_fd);
+        log.debug("{s}: done", .{self.source.path});
+    }
+};
+
+// helpers
+
+fn formatName(gpa: Allocator, name: []const u8) ![]u8 {
+    return try std.mem.replaceOwned(u8, gpa, name, "_", " ");
+}
+
+fn formatLink(gpa: Allocator, name: []const u8, offset: ?[]const u8) ![]u8 {
+    var link = try std.fmt.allocPrint(gpa, "{s}.html", .{name});
+    if (offset) |_| {
+        const link2 = try std.fs.path.join(gpa, &.{ offset.?, link });
+        gpa.free(link);
+        link = link2;
+    }
+    return link;
+}
+
+fn targetExists(io: Io, gpa: Allocator, target_fd: Io.File, root_dir: Io.Dir) !bool {
+    const target_parts = getXattrSourceComponents(gpa, target_fd) catch |err|
+        switch (err) {
+            error.XattrNotFound => return false,
+            else => return err,
+        };
+    defer gpa.free(target_parts);
+    defer for (target_parts) |p| gpa.free(p);
+
+    const target_dgst = getXattrSourceDigest(gpa, target_fd) catch |err|
+        switch (err) {
+            error.XattrNotFound => return false,
+            else => return err,
         };
-        const command = try std.fmt.allocPrint(init.gpa,
-            \\find "{s}" -path '*.htm' -type f -print | entr -nc -dd -- "{s}" gen "{s}" "{s}"
-        , .{ root, exePath, root, site });
-        try os.shell(init.io, command, .{});
-    }
-
-    fn serveCmd(init: std.process.Init, argv: *std.process.Args.Iterator) !void {
-        const root = argv.next() orelse ".";
-        log.info("root = {s}", .{root});
-
-        var env_map = try init.minimal.environ.createMap(init.gpa);
-        defer env_map.deinit();
-        try env_map.put("CADDY_HTTP_PORT", "1414");
-        try env_map.put("CADDY_HOST", "localhost");
-
-        if ((try std.Io.Dir.cwd().openDir(init.io, root, .{})).access(init.io, "Caddyfile", .{})) {
-            try os.shell(init.io,
-                \\caddy fmt --overwrite && caddy run --watch
-            , .{ .environ_map = &env_map, .cwd = .{ .path = root } });
-        } else |_| {
-            try os.shell(init.io,
-                \\caddy file-server --listen=$CADDY_HOST:$CADDY_HTTP_PORT
-            , .{ .environ_map = &env_map, .cwd = .{ .path = root } });
+
+    // create digests for parts
+    var actual_dgsts: std.ArrayList(Digest) = .empty;
+    defer actual_dgsts.deinit(gpa);
+    for (target_parts) |p| {
+        var f = try File.init(io, root_dir, p, .{ .mode = .read_write });
+        defer f.close(io);
+        try actual_dgsts.append(gpa, try f.digest());
+    }
+    const actual_dgst = compositeDigest(actual_dgsts.items);
+    return std.mem.eql(u8, &actual_dgst, &target_dgst);
+}
+
+fn isFullPage(source: File) !bool {
+    // exit early if extension is .html vs .htm
+    const ext = std.fs.path.extension(source.path);
+    if (std.mem.eql(u8, ext, ".html")) return true;
+
+    // peek if file starts with <html>
+    const buf = source.mmap.memory[0..6];
+    return std.mem.eql(u8, buf, "<html>");
+}
+
+fn compositeDigest(parts: []Digest) Digest {
+    std.sort.block(Digest, parts, {}, comptime struct {
+        pub fn inner(_: void, a: Digest, b: Digest) bool {
+            return std.mem.lessThan(u8, &a, &b);
         }
+    }.inner);
+
+    var buffer: [Digester.digest_length]u8 = undefined;
+    var d = Digester.init(.{});
+    for (parts) |dgst| d.update(&dgst);
+    d.final(&buffer);
+    return buffer;
+}
+
+// xattr code
+// TODO: fallback to cache dir when xattr isnt supported
+
+fn setXattr(fd: Io.File, key: [:0]const u8, val: []const u8) !void {
+    const rc = linux.fsetxattr(fd.handle, key.ptr, val.ptr, val.len, 0);
+    switch (linux.errno(rc)) {
+        .SUCCESS => {},
+        .OPNOTSUPP => return error.XattrNotSupported,
+        .NODATA => return error.XattrNotFound,
+        else => return error.XattrError,
     }
-};
+}
 
-pub fn main(init: std.process.Init) !void {
-    var ctx = try Context.init(init.io, init.gpa);
-    defer ctx.deinit();
+fn getXattr(gpa: Allocator, fd: Io.File, key: [:0]const u8) ![]u8 {
+    var buffer: [BUFFER_SIZE]u8 = undefined;
+    const rc = linux.fgetxattr(fd.handle, key.ptr, &buffer, buffer.len);
+    switch (linux.errno(rc)) {
+        .SUCCESS => {},
+        .OPNOTSUPP => return error.XattrNotSupported,
+        .NODATA => return error.XattrNotFound,
+        else => {
+            log.err("errno: {d}", .{linux.errno(rc)});
+            return error.XattrError;
+        },
+    }
+    return try gpa.dupe(u8, buffer[0..@intCast(rc)]);
+}
+
+const XATTR_SOURCE_DIGEST = "user.rbc-source-digest";
+const XATTR_SOURCE_COMPONENTS = "user.rbc-source-components";
+
+fn setXattrSourceDigest(fd: Io.File, dgst: Digest) !void {
+    return try setXattr(fd, XATTR_SOURCE_DIGEST, &dgst);
+}
+
+fn getXattrSourceDigest(gpa: Allocator, fd: Io.File) !Digest {
+    const val = try getXattr(gpa, fd, XATTR_SOURCE_DIGEST);
+    defer gpa.free(val);
+    return val[0..16].*;
+}
+
+fn setXattrSourceComponents(gpa: Allocator, fd: Io.File, parts: []const []const u8) !void {
+    const val = try std.mem.join(gpa, ";", parts);
+    defer gpa.free(val);
+    return try setXattr(fd, XATTR_SOURCE_COMPONENTS, val);
+}
+
+fn getXattrSourceComponents(gpa: Allocator, fd: Io.File) ![][]const u8 {
+    const val = try getXattr(gpa, fd, XATTR_SOURCE_COMPONENTS);
+    defer gpa.free(val);
+
+    var parts: std.ArrayList([]const u8) = .empty;
+    errdefer parts.deinit(gpa);
+
+    var iter = std.mem.splitScalar(u8, val, ';');
+    while (iter.next()) |p| try parts.append(gpa, try gpa.dupe(u8, p));
+    return try parts.toOwnedSlice(gpa);
+}
+
+// tests
+
+pub const Testing = if (@import("builtin").is_test)
+    struct {
+        pub fn shell(
+            io: std.Io,
+            command: []const u8,
+            opts: struct {
+                cwd: std.process.Child.Cwd = .inherit,
+                environ_map: ?*const std.process.Environ.Map = null,
+                progress_node: std.Progress.Node = std.Progress.Node.none,
+            },
+        ) !void {
+            log.debug("Running Shell <<< {s}", .{command});
+
+            var p = try std.process.spawn(io, .{
+                .argv = &.{ "sh", "-c", command },
+                .cwd = opts.cwd,
+                .stdin = .ignore,
+                .stderr = .inherit,
+                .stdout = .inherit,
+                .environ_map = opts.environ_map,
+                .progress_node = opts.progress_node,
+            });
+
+            const result = try p.wait(io);
+            if (result.exited != 0) return error.NonZeroExitCode;
+        }
+
+        pub fn parse_hash(comptime str: []const u8) !Digest {
+            var buffer: [BUFFER_SIZE]u8 = undefined;
+            var out = try std.fmt.hexToBytes(&buffer, str);
+            return out[0..Digester.digest_length].*;
+        }
+    };
 
-    try Cli.run(init, &ctx);
+test "xattr" {
+    std.testing.log_level = .debug;
+
+    const io = std.testing.io;
+    const gpa = std.testing.allocator;
+
+    var tmp = std.testing.tmpDir(.{});
+    defer tmp.cleanup();
+
+    const fd = try tmp.dir.createFile(io, &tmp.sub_path, .{});
+
+    // test generic funcs
+    {
+        try setXattr(fd, "user.foo", "bar");
+        const val = try getXattr(gpa, fd, "user.foo");
+        defer gpa.free(val);
+        try std.testing.expectEqualStrings("bar", val);
+    }
+
+    // test digest func
+    {
+        const dgst = try Testing.parse_hash("accd9547a71463f530ae12a8dd0d7696");
+        try setXattrSourceDigest(fd, dgst);
+        const val = try getXattrSourceDigest(gpa, fd);
+        try std.testing.expectEqualStrings(&dgst, &val);
+    }
+
+    // test paths func
+    {
+        const parts = &[_][]const u8{ "foo.htm", "bar.htm" };
+        try setXattrSourceComponents(gpa, fd, parts);
+        const val = try getXattrSourceComponents(gpa, fd);
+        defer gpa.free(val);
+        defer for (val) |v| gpa.free(v);
+        try std.testing.expectEqual(parts.len, val.len);
+        for (parts, 0..) |p, i| try std.testing.expectEqualStrings(p, val[i]);
+    }
+}
+
+test "targetExists" {
+    std.testing.log_level = .debug;
+
+    const io = std.testing.io;
+    const gpa = std.testing.allocator;
+
+    var tmp = std.testing.tmpDir(.{});
+    defer tmp.cleanup();
+
+    var dgsts: std.ArrayList(Digest) = .empty;
+    defer dgsts.deinit(gpa);
+
+    const source_fd = try tmp.dir.createFile(io, "source.htm", .{});
+    try source_fd.writePositionalAll(io, "hello world", 0);
+    const source = try File.init(io, tmp.dir, "source.htm", null);
+    try dgsts.append(gpa, try source.digest());
+
+    const ref_fd = try tmp.dir.createFile(io, "ref.htm", .{});
+    try ref_fd.writePositionalAll(io, "foobar", 0);
+    const ref = try File.init(io, tmp.dir, "ref.htm", null);
+    try dgsts.append(gpa, try ref.digest());
+
+    const dgst = compositeDigest(dgsts.items);
+
+    const target_fd = try tmp.dir.createFile(io, "target.htm", .{});
+    try setXattrSourceDigest(target_fd, dgst);
+    try setXattrSourceComponents(gpa, target_fd, &.{
+        source.path,
+        ref.path,
+    });
+
+    const exists = try targetExists(io, gpa, target_fd, tmp.dir);
+    try std.testing.expectEqual(true, exists);
+}
+
+test "e2e" {
+    std.testing.log_level = .debug;
+
+    const io = std.testing.io;
+    const gpa = std.testing.allocator;
+
+    const Tc = struct {
+        const Self = @This();
+
+        source: File,
+        root_dir: Io.Dir,
+
+        expect_path: []const u8,
+        expect_dgst: Digest,
+
+        fn init(path: []const u8, expect_path: []const u8, expect_dgst: Digest) !Self {
+            const root_dir = try std.Io.Dir.cwd().openDir(io, std.fs.path.dirname(path).?, .{});
+            return .{
+                .root_dir = root_dir,
+                .source = try File.init(io, root_dir, std.fs.path.basename(path), null),
+                .expect_path = expect_path,
+                .expect_dgst = expect_dgst,
+            };
+        }
+
+        fn run(tc: *Self, dest: Io.Dir) !void {
+            std.debug.print("=== {s} =============================\n", .{tc.source.path});
+
+            defer tc.root_dir.close(io);
+            defer tc.source.close(io);
+
+            const b = try Builder.init(gpa, tc.root_dir, tc.source, dest);
+            defer b.deinit(gpa);
+            try b.build(
+                io,
+                gpa,
+            );
+
+            const fd = try dest.openFile(io, tc.expect_path, .{ .mode = .read_write, .lock = .exclusive });
+            defer fd.close(io);
+            const xattr_dgst = try getXattrSourceDigest(gpa, fd);
+
+            log.debug("{x} == {x}", .{ tc.expect_dgst, xattr_dgst });
+            if (!std.mem.eql(u8, &tc.expect_dgst, &xattr_dgst)) {
+                std.debug.print("====== expected this digest: =========\n", .{});
+                std.debug.print("{x}\n\n", .{tc.expect_dgst});
+                std.debug.print("======== instead found this: =========\n", .{});
+                std.debug.print("{x}\n\n", .{xattr_dgst});
+                std.debug.print("======================================\n", .{});
+                return error.TestExpectedEqual;
+            }
+
+            try Testing.shell(io, "tree", .{ .cwd = .{ .dir = dest } });
+            const cmd = try std.fmt.allocPrint(gpa, "cat {s}", .{tc.expect_path});
+            defer gpa.free(cmd);
+            try Testing.shell(io, cmd, .{ .cwd = .{ .dir = dest } });
+        }
+    };
+
+    var tmp = std.testing.tmpDir(.{ .iterate = true });
+    defer tmp.cleanup();
+
+    {
+        var tc = try Tc.init(
+            "./testdata/only-copy.html",
+            "only-copy.html",
+            try Testing.parse_hash("eae5ff7df7c71f8bbeda89302f9fe721"),
+        );
+        try tc.run(tmp.dir);
+    }
+    {
+        var tc = try Tc.init(
+            "./testdata/only-copy.html",
+            "only-copy.html",
+            try Testing.parse_hash("eae5ff7df7c71f8bbeda89302f9fe721"),
+        );
+        try tc.run(tmp.dir);
+    }
+    {
+        var tc = try Tc.init(
+            "./testdata/fragment.htm",
+            "fragment.html",
+            try Testing.parse_hash("a5c099fac15ea9ea9928712fa6229702"),
+        );
+        try tc.run(tmp.dir);
+    }
+    {
+        var tc = try Tc.init(
+            "./testdata/page.htm",
+            "page.html",
+            try Testing.parse_hash("5d810c74eb9ba4d03fa49ab920b85e45"),
+        );
+        try tc.run(tmp.dir);
+    }
+    {
+        var tc = try Tc.init(
+            "./testdata/meta.tilde.foo.htm",
+            "~foo",
+            try Testing.parse_hash("f5bdd49b1a3d691113033ee606e0aa1d"),
+        );
+        try tc.run(tmp.dir);
+    }
 }