diff options
Diffstat (limited to '')
| -rw-r--r-- | src/main.zig | 782 |
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); + } } |