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 }