tiny.sandbox.scan
Defined in tiny.sandbox.
API (5)
Actions
Public operations.
Types and contracts
Public types and contracts.
Source
Source: lib/sandbox/src/root.zig:43
zig
pub const scan = @import("scan.zig");Source: lib/sandbox/src/scan.zig
zig
const std = @import("std");const sys = @import("sys");const change = @import("change.zig");const Allocator = std.mem.Allocator;const fs_io = sys.fs.debugIo();pub const Snapshot = struct { allocator: Allocator, entries: std.ArrayList(change.Entry) = .empty, pub fn init(allocator: Allocator) Snapshot { return .{ .allocator = allocator }; } pub fn deinit(self: *Snapshot) void { for (self.entries.items) |*entry| entry.deinit(self.allocator); self.entries.deinit(self.allocator); self.* = undefined; } pub fn append(self: *Snapshot, entry: change.Entry) Allocator.Error!void { try self.entries.append(self.allocator, try entry.clone(self.allocator)); } pub fn find(self: *const Snapshot, path: []const u8) ?change.Entry { for (self.entries.items) |entry| { if (std.mem.eql(u8, entry.path, path)) return entry; } return null; } pub fn sort(self: *Snapshot) void { std.mem.sort(change.Entry, self.entries.items, {}, lessThan); }};pub fn capture(root: sys.fs.Dir, allocator: Allocator) !Snapshot { var snapshot = Snapshot.init(allocator); errdefer snapshot.deinit(); var dir = try root.openDir(fs_io, ".", .{ .iterate = true }); defer dir.close(fs_io); var walker = try dir.walk(allocator); defer walker.deinit(); while (try walker.next(fs_io)) |entry| { switch (entry.kind) { .directory => try snapshot.append(.{ .path = @constCast(entry.path), .kind = .directory, }), .file => try appendFile(root, allocator, &snapshot, entry.path), .sym_link => try appendLink(root, &snapshot, entry.path), else => try snapshot.append(.{ .path = @constCast(entry.path), .kind = .other, }), } } snapshot.sort(); return snapshot;}pub fn capturePaths(root: sys.fs.Dir, allocator: Allocator) !Snapshot { var snapshot = Snapshot.init(allocator); errdefer snapshot.deinit(); var dir = try root.openDir(fs_io, ".", .{ .iterate = true }); defer dir.close(fs_io); var walker = try dir.walk(allocator); defer walker.deinit(); while (try walker.next(fs_io)) |entry| { switch (entry.kind) { .sym_link => try appendLink(root, &snapshot, entry.path), .directory => try snapshot.append(.{ .path = @constCast(entry.path), .kind = .directory }), .file => try snapshot.append(.{ .path = @constCast(entry.path), .kind = .file }), else => try snapshot.append(.{ .path = @constCast(entry.path), .kind = .other }), } } snapshot.sort(); return snapshot;}pub fn diff(allocator: Allocator, before: *const Snapshot, after: *const Snapshot) Allocator.Error!change.Set { var set = change.Set.init(allocator); errdefer set.deinit(); var before_index: usize = 0; var after_index: usize = 0; while (before_index < before.entries.items.len or after_index < after.entries.items.len) { if (before_index >= before.entries.items.len) { try set.append(.put, after.entries.items[after_index]); after_index += 1; continue; } if (after_index >= after.entries.items.len) { try set.append(.delete, before.entries.items[before_index]); before_index += 1; continue; } const before_entry = before.entries.items[before_index]; const after_entry = after.entries.items[after_index]; const order = std.mem.order(u8, before_entry.path, after_entry.path); switch (order) { .lt => { try set.append(.delete, before_entry); before_index += 1; }, .gt => { try set.append(.put, after_entry); after_index += 1; }, .eq => { if (!change.sameEntry(before_entry, after_entry)) try set.append(.put, after_entry); before_index += 1; after_index += 1; }, } } return set;}pub fn hashAlloc(allocator: Allocator, content: []const u8) Allocator.Error![]u8 { var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined; std.crypto.hash.sha2.Sha256.hash(content, &digest, .{}); return try digestAlloc(allocator, &digest);}const FileDigest = struct { hash: []u8, bytes: u64,};fn digestAlloc(allocator: Allocator, digest: *const [std.crypto.hash.sha2.Sha256.digest_length]u8) Allocator.Error![]u8 { const prefix = "sha256:"; const hex = std.fmt.bytesToHex(digest, .lower); const value = try allocator.alloc(u8, prefix.len + hex.len); @memcpy(value[0..prefix.len], prefix); @memcpy(value[prefix.len..], hex[0..]); return value;}fn appendFile(root: sys.fs.Dir, allocator: Allocator, snapshot: *Snapshot, path: []const u8) !void { const digest = try hashFileAlloc(root, allocator, path); defer allocator.free(digest.hash); try snapshot.append(.{ .path = @constCast(path), .kind = .file, .hash = digest.hash, .bytes = digest.bytes, });}fn hashFileAlloc(root: sys.fs.Dir, allocator: Allocator, path: []const u8) !FileDigest { var file = try root.openFile(fs_io, path, .{}); defer file.close(fs_io); var reader_buffer: [16 * 1024]u8 = undefined; var reader = file.reader(fs_io, &reader_buffer); var chunk: [16 * 1024]u8 = undefined; var hasher = std.crypto.hash.sha2.Sha256.init(.{}); var total: u64 = 0; while (true) { const count = reader.interface.readSliceShort(&chunk) catch |err| switch (err) { error.ReadFailed => return reader.err orelse error.ReadFailed, }; if (count == 0) break; hasher.update(chunk[0..count]); total += @intCast(count); } var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined; hasher.final(&digest); return .{ .hash = try digestAlloc(allocator, &digest), .bytes = total, };}fn appendLink(root: sys.fs.Dir, snapshot: *Snapshot, path: []const u8) !void { var target_buffer: [std.fs.max_path_bytes]u8 = undefined; const target_len = try root.readLink(fs_io, path, &target_buffer); try snapshot.append(.{ .path = @constCast(path), .kind = .sym_link, .target = target_buffer[0..target_len], });}fn lessThan(_: void, left: change.Entry, right: change.Entry) bool { return std.mem.lessThan(u8, left.path, right.path);}test "scan captures files directories and links" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(fs_io, "dir"); try tmp.dir.writeFile(fs_io, .{ .sub_path = "dir/a.txt", .data = "alpha" }); tmp.dir.symLink(fs_io, "dir/a.txt", "link", .{}) catch |err| switch (err) { error.AccessDenied, error.PermissionDenied => return error.SkipZigTest, else => return err, }; var snapshot = try capture(tmp.dir, std.testing.allocator); defer snapshot.deinit(); try std.testing.expect(snapshot.find("dir") != null); try std.testing.expectEqual(change.Kind.file, snapshot.find("dir/a.txt").?.kind); try std.testing.expectEqual(change.Kind.sym_link, snapshot.find("link").?.kind); try std.testing.expectEqualStrings("dir/a.txt", snapshot.find("link").?.target);}test "scan diff records puts deletes and content changes" { var before = Snapshot.init(std.testing.allocator); defer before.deinit(); var after = Snapshot.init(std.testing.allocator); defer after.deinit(); try before.append(.{ .path = @constCast("gone.txt"), .kind = .file, .hash = @constCast("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), .bytes = 1 }); try before.append(.{ .path = @constCast("same.txt"), .kind = .file, .hash = @constCast("sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), .bytes = 1 }); try before.append(.{ .path = @constCast("update.txt"), .kind = .file, .hash = @constCast("sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), .bytes = 1 }); try after.append(.{ .path = @constCast("new.txt"), .kind = .file, .hash = @constCast("sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"), .bytes = 1 }); try after.append(.{ .path = @constCast("same.txt"), .kind = .file, .hash = @constCast("sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), .bytes = 1 }); try after.append(.{ .path = @constCast("update.txt"), .kind = .file, .hash = @constCast("sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"), .bytes = 2 }); before.sort(); after.sort(); var changes = try diff(std.testing.allocator, &before, &after); defer changes.deinit(); try std.testing.expectEqual(@as(usize, 3), changes.len()); try std.testing.expectEqual(change.Operation.delete, changes.find("gone.txt").?.operation); try std.testing.expectEqual(change.Operation.put, changes.find("new.txt").?.operation); try std.testing.expectEqual(@as(u64, 2), changes.find("update.txt").?.entry.bytes);}Audit
| Definitions | 5 |
|---|---|
| Public names | 5 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |