lib/sandbox/src/scan.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const sys = @import("sys");
3 const change = @import("change.zig");
4
5 const Allocator = std.mem.Allocator;
6 const fs_io = sys.fs.debugIo();
7
8 pub const Snapshot = struct {
9 allocator: Allocator,
10 entries: std.ArrayList(change.Entry) = .empty,
11
12 pub fn init(allocator: Allocator) Snapshot {
13 return .{ .allocator = allocator };
14 }
15
16 pub fn deinit(self: *Snapshot) void {
17 for (self.entries.items) |*entry| entry.deinit(self.allocator);
18 self.entries.deinit(self.allocator);
19 self.* = undefined;
20 }
21
22 pub fn append(self: *Snapshot, entry: change.Entry) Allocator.Error!void {
23 try self.entries.append(self.allocator, try entry.clone(self.allocator));
24 }
25
26 pub fn find(self: *const Snapshot, path: []const u8) ?change.Entry {
27 for (self.entries.items) |entry| {
28 if (std.mem.eql(u8, entry.path, path)) return entry;
29 }
30 return null;
31 }
32
33 pub fn sort(self: *Snapshot) void {
34 std.mem.sort(change.Entry, self.entries.items, {}, lessThan);
35 }
36 };
37
38 pub fn capture(root: sys.fs.Dir, allocator: Allocator) !Snapshot {
39 var snapshot = Snapshot.init(allocator);
40 errdefer snapshot.deinit();
41
42 var dir = try root.openDir(fs_io, ".", .{ .iterate = true });
43 defer dir.close(fs_io);
44 var walker = try dir.walk(allocator);
45 defer walker.deinit();
46 while (try walker.next(fs_io)) |entry| {
47 switch (entry.kind) {
48 .directory => try snapshot.append(.{
49 .path = @constCast(entry.path),
50 .kind = .directory,
51 }),
52 .file => try appendFile(root, allocator, &snapshot, entry.path),
53 .sym_link => try appendLink(root, &snapshot, entry.path),
54 else => try snapshot.append(.{
55 .path = @constCast(entry.path),
56 .kind = .other,
57 }),
58 }
59 }
60 snapshot.sort();
61 return snapshot;
62 }
63
64 pub fn capturePaths(root: sys.fs.Dir, allocator: Allocator) !Snapshot {
65 var snapshot = Snapshot.init(allocator);
66 errdefer snapshot.deinit();
67
68 var dir = try root.openDir(fs_io, ".", .{ .iterate = true });
69 defer dir.close(fs_io);
70 var walker = try dir.walk(allocator);
71 defer walker.deinit();
72 while (try walker.next(fs_io)) |entry| {
73 switch (entry.kind) {
74 .sym_link => try appendLink(root, &snapshot, entry.path),
75 .directory => try snapshot.append(.{ .path = @constCast(entry.path), .kind = .directory }),
76 .file => try snapshot.append(.{ .path = @constCast(entry.path), .kind = .file }),
77 else => try snapshot.append(.{ .path = @constCast(entry.path), .kind = .other }),
78 }
79 }
80 snapshot.sort();
81 return snapshot;
82 }
83
84 pub fn diff(allocator: Allocator, before: *const Snapshot, after: *const Snapshot) Allocator.Error!change.Set {
85 var set = change.Set.init(allocator);
86 errdefer set.deinit();
87
88 var before_index: usize = 0;
89 var after_index: usize = 0;
90 while (before_index < before.entries.items.len or after_index < after.entries.items.len) {
91 if (before_index >= before.entries.items.len) {
92 try set.append(.put, after.entries.items[after_index]);
93 after_index += 1;
94 continue;
95 }
96 if (after_index >= after.entries.items.len) {
97 try set.append(.delete, before.entries.items[before_index]);
98 before_index += 1;
99 continue;
100 }
101 const before_entry = before.entries.items[before_index];
102 const after_entry = after.entries.items[after_index];
103 const order = std.mem.order(u8, before_entry.path, after_entry.path);
104 switch (order) {
105 .lt => {
106 try set.append(.delete, before_entry);
107 before_index += 1;
108 },
109 .gt => {
110 try set.append(.put, after_entry);
111 after_index += 1;
112 },
113 .eq => {
114 if (!change.sameEntry(before_entry, after_entry)) try set.append(.put, after_entry);
115 before_index += 1;
116 after_index += 1;
117 },
118 }
119 }
120 return set;
121 }
122
123 pub fn hashAlloc(allocator: Allocator, content: []const u8) Allocator.Error![]u8 {
124 var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined;
125 std.crypto.hash.sha2.Sha256.hash(content, &digest, .{});
126 return try digestAlloc(allocator, &digest);
127 }
128
129 const FileDigest = struct {
130 hash: []u8,
131 bytes: u64,
132 };
133
134 fn digestAlloc(allocator: Allocator, digest: *const [std.crypto.hash.sha2.Sha256.digest_length]u8) Allocator.Error![]u8 {
135 const prefix = "sha256:";
136 const hex = std.fmt.bytesToHex(digest, .lower);
137 const value = try allocator.alloc(u8, prefix.len + hex.len);
138 @memcpy(value[0..prefix.len], prefix);
139 @memcpy(value[prefix.len..], hex[0..]);
140 return value;
141 }
142
143 fn appendFile(root: sys.fs.Dir, allocator: Allocator, snapshot: *Snapshot, path: []const u8) !void {
144 const digest = try hashFileAlloc(root, allocator, path);
145 defer allocator.free(digest.hash);
146 try snapshot.append(.{
147 .path = @constCast(path),
148 .kind = .file,
149 .hash = digest.hash,
150 .bytes = digest.bytes,
151 });
152 }
153
154 fn hashFileAlloc(root: sys.fs.Dir, allocator: Allocator, path: []const u8) !FileDigest {
155 var file = try root.openFile(fs_io, path, .{});
156 defer file.close(fs_io);
157
158 var reader_buffer: [16 * 1024]u8 = undefined;
159 var reader = file.reader(fs_io, &reader_buffer);
160 var chunk: [16 * 1024]u8 = undefined;
161 var hasher = std.crypto.hash.sha2.Sha256.init(.{});
162 var total: u64 = 0;
163 while (true) {
164 const count = reader.interface.readSliceShort(&chunk) catch |err| switch (err) {
165 error.ReadFailed => return reader.err orelse error.ReadFailed,
166 };
167 if (count == 0) break;
168 hasher.update(chunk[0..count]);
169 total += @intCast(count);
170 }
171
172 var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined;
173 hasher.final(&digest);
174 return .{
175 .hash = try digestAlloc(allocator, &digest),
176 .bytes = total,
177 };
178 }
179
180 fn appendLink(root: sys.fs.Dir, snapshot: *Snapshot, path: []const u8) !void {
181 var target_buffer: [std.fs.max_path_bytes]u8 = undefined;
182 const target_len = try root.readLink(fs_io, path, &target_buffer);
183 try snapshot.append(.{
184 .path = @constCast(path),
185 .kind = .sym_link,
186 .target = target_buffer[0..target_len],
187 });
188 }
189
190 fn lessThan(_: void, left: change.Entry, right: change.Entry) bool {
191 return std.mem.lessThan(u8, left.path, right.path);
192 }
193
194 test "scan captures files directories and links" {
195 var tmp = std.testing.tmpDir(.{});
196 defer tmp.cleanup();
197
198 try tmp.dir.createDirPath(fs_io, "dir");
199 try tmp.dir.writeFile(fs_io, .{ .sub_path = "dir/a.txt", .data = "alpha" });
200 tmp.dir.symLink(fs_io, "dir/a.txt", "link", .{}) catch |err| switch (err) {
201 error.AccessDenied, error.PermissionDenied => return error.SkipZigTest,
202 else => return err,
203 };
204
205 var snapshot = try capture(tmp.dir, std.testing.allocator);
206 defer snapshot.deinit();
207 try std.testing.expect(snapshot.find("dir") != null);
208 try std.testing.expectEqual(change.Kind.file, snapshot.find("dir/a.txt").?.kind);
209 try std.testing.expectEqual(change.Kind.sym_link, snapshot.find("link").?.kind);
210 try std.testing.expectEqualStrings("dir/a.txt", snapshot.find("link").?.target);
211 }
212
213 test "scan diff records puts deletes and content changes" {
214 var before = Snapshot.init(std.testing.allocator);
215 defer before.deinit();
216 var after = Snapshot.init(std.testing.allocator);
217 defer after.deinit();
218
219 try before.append(.{ .path = @constCast("gone.txt"), .kind = .file, .hash = @constCast("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), .bytes = 1 });
220 try before.append(.{ .path = @constCast("same.txt"), .kind = .file, .hash = @constCast("sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), .bytes = 1 });
221 try before.append(.{ .path = @constCast("update.txt"), .kind = .file, .hash = @constCast("sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), .bytes = 1 });
222 try after.append(.{ .path = @constCast("new.txt"), .kind = .file, .hash = @constCast("sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"), .bytes = 1 });
223 try after.append(.{ .path = @constCast("same.txt"), .kind = .file, .hash = @constCast("sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), .bytes = 1 });
224 try after.append(.{ .path = @constCast("update.txt"), .kind = .file, .hash = @constCast("sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"), .bytes = 2 });
225 before.sort();
226 after.sort();
227
228 var changes = try diff(std.testing.allocator, &before, &after);
229 defer changes.deinit();
230 try std.testing.expectEqual(@as(usize, 3), changes.len());
231 try std.testing.expectEqual(change.Operation.delete, changes.find("gone.txt").?.operation);
232 try std.testing.expectEqual(change.Operation.put, changes.find("new.txt").?.operation);
233 try std.testing.expectEqual(@as(u64, 2), changes.find("update.txt").?.entry.bytes);
234 }