From c677e8873e0eae790a1b8b07844a10efa9f63a1e Mon Sep 17 00:00:00 2001 From: Robert Günzler Date: Wed, 24 Jun 2026 11:57:02 +0900 Subject: wip design for v2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Robert Günzler --- src/Content.zig | 210 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 src/Content.zig (limited to 'src/Content.zig') diff --git a/src/Content.zig b/src/Content.zig new file mode 100644 index 0000000..120d8b5 --- /dev/null +++ b/src/Content.zig @@ -0,0 +1,210 @@ +const std = @import("std"); +const os = @import("os.zig"); + +const Content = @This(); +const log = std.log.scoped(.Content); + +pub const Digest = [64]u8; + +io: std.Io, +arena: *std.heap.ArenaAllocator, +root: std.Io.Dir, + +pub fn init(io: std.Io, allocator: std.mem.Allocator, root_dir: std.Io.Dir) !Content { + const arena = try allocator.create(std.heap.ArenaAllocator); + errdefer allocator.destroy(arena); + arena.* = std.heap.ArenaAllocator.init(allocator); + + return .{ + .io = io, + .arena = arena, + .root = root_dir, + }; +} + +pub fn deinit(self: *Content) void { + self.root.close(self.io); + const allocator = self.arena.child_allocator; + self.arena.deinit(); + allocator.destroy(self.arena); +} + +pub fn digest(self: *Content, dir: std.Io.Dir, sub_path: []const u8) !Digest { + // generate sha256 digest + // TODO: use zig-based hashing? + const command = blk: { + var buf: [std.os.linux.PATH_MAX + 10]u8 = undefined; + @memcpy(buf[0..10], "sha256sum "); + @memcpy(buf[10 .. 10 + sub_path.len], sub_path); + break :blk buf[0 .. 10 + sub_path.len]; + }; + var buf: [std.os.linux.PATH_MAX + 64]u8 = undefined; + var w = std.Io.Writer.fixed(&buf); + try os.captureStdout(self.io, command, .{ .cwd = .{ .dir = dir } }, &w); + + // take only the digest + return buf[0..64].*; +} + +pub fn get(self: *Content, dgst: Digest) !?Digest { + const found = blk: { + self.root.access(self.io, &dgst, .{}) catch |err| switch (err) { + error.FileNotFound => break :blk false, + else => return err, + }; + break :blk true; + }; + if (found) { + return dgst; + } else { + return null; + } +} + +pub fn add(self: *Content, dir: std.Io.Dir, sub_path: []const u8) !Digest { + const dgst = try self.digest(dir, sub_path); + + return (try self.get(dgst)) orelse { + // create digest directory + try self.root.createDirPath(self.io, &dgst); + + log.debug("Added {s}", .{dgst}); + return dgst; + }; +} + +pub fn del(self: *Content, dgst: Digest) !void { + try self.root.deleteTree(self.io, &dgst); +} + +const Enumerator = struct { + content: *Content, + walker: std.Io.Dir.SelectiveWalker, + + pub fn deinit(self: *Enumerator) void { + self.walker.deinit(); + } + + pub fn next(self: *Enumerator) !?Digest { + if (try self.walker.next(self.content.io)) |e| { + switch (e.kind) { + // TODO: we should probably check that this is actually a digest + .directory => return e.basename[0..64].*, + else => return null, + } + } else { + return null; + } + } +}; + +pub fn enumerate(self: *Content, allocator: std.mem.Allocator) !Enumerator { + return .{ + .walker = try self.root.walkSelectively(allocator), + .content = self, + }; +} + +pub fn propSet(self: *Content, dgst: Digest, key: []const u8, val: ?[]const u8) !void { + const digest_dir = try self.root.openDir(self.io, &dgst, .{}); + defer digest_dir.close(self.io); + + if (val == null) { + return try digest_dir.deleteFile(self.io, key); + } + + return try digest_dir.writeFile(self.io, .{ + .sub_path = key, + .data = val.?, + }); +} + +pub fn propGet(self: *Content, dgst: Digest, key: []const u8, allocator: std.mem.Allocator) !?[]const u8 { + const digest_dir = try self.root.openDir(self.io, &dgst, .{}); + defer digest_dir.close(self.io); + + if (digest_dir.readFileAlloc(self.io, key, allocator, .unlimited)) |val| { + return val; + } else |err| switch (err) { + error.FileNotFound => return null, + else => return err, + } +} + +// fn checkLink(self: *Content, allocator: std.mem.Allocator, dgst: Digest) ![]const u8 { +// const link_target = try self.propGet(dgst, "link", allocator); +// // check target exists +// try std.Io.Dir.cwd().access(self.io, link_target, .{ .read = true }); +// return link_target; +// } + +test "e2e" { + std.testing.log_level = .debug; + + const DEMO_DIR: []const u8 = "/home/robert/tmp/rbc-demo/"; + const CONTENT_DIR: []const u8 = DEMO_DIR ++ ".content"; + + try std.Io.Dir.cwd().deleteTree(std.testing.io, CONTENT_DIR); + + const demo_dir = try std.Io.Dir.cwd().openDir(std.testing.io, DEMO_DIR, .{ .iterate = true }); + const content_dir = try std.Io.Dir.cwd().createDirPathOpen(std.testing.io, CONTENT_DIR, .{ .open_options = .{ .iterate = true } }); + + var cnt = try Content.init(std.testing.io, std.testing.allocator, content_dir); + defer cnt.deinit(); + + { + var walker = try demo_dir.walkSelectively(std.testing.allocator); + defer walker.deinit(); + + while (try walker.next(std.testing.io)) |e| { + switch (e.kind) { + .file => { + log.info("+ {s}", .{e.path}); + const dgst = try cnt.add(demo_dir, e.path); + try content_dir.access(std.testing.io, &dgst, .{}); + const rel = try std.fs.path.relativePosix(std.testing.allocator, DEMO_DIR, CONTENT_DIR, e.path); + defer std.testing.allocator.free(rel); + try cnt.propSet(dgst, "link", rel); + }, + else => {}, + } + } + } + + var etor = try cnt.enumerate(std.testing.allocator); + defer etor.deinit(); + + { + const dgst = try etor.next(); + try std.testing.expect(dgst != null); + + try cnt.propSet(dgst.?, "foo", "bar"); + const val1 = try cnt.propGet(dgst.?, "foo", std.testing.allocator); + try std.testing.expect(val1 != null); + defer std.testing.allocator.free(val1); + try std.testing.expect(std.mem.eql(u8, "bar", val1.?)); + + try cnt.propSet(dgst.?, "foo", null); + const val2 = try cnt.propGet(dgst.?, "foo", std.testing.allocator); + try std.testing.expect(val2 == null); + } + + { + const dgst = try etor.next(); + try std.testing.expect(dgst != null); + + try cnt.del(dgst.?); + const err = content_dir.access(std.testing.io, &dgst.?, .{}); + try std.testing.expectError(error.FileNotFound, err); + } + + while (try etor.next()) |dgst| { + log.info("? {s}", .{dgst}); + try content_dir.access(std.testing.io, &dgst, .{}); + const link = try cnt.propGet(dgst, "link", std.testing.allocator); + try std.testing.expect(link != null); + defer std.testing.allocator.free(link.?); + try content_dir.access(std.testing.io, link.?, .{}); + log.info("l {s}", .{link.?}); + } +} -- cgit 1.4.1