tiny.sandbox.layer
Defined in tiny.sandbox.
API (3)
Actions
Public operations.
Types and contracts
Public types and contracts.
Source
Source: lib/sandbox/src/layer.zig
zig
const std = @import("std");const builtin = @import("builtin");const sys = @import("sys");const change = @import("change.zig");const scan = @import("scan.zig");const Allocator = std.mem.Allocator;const fs_io = sys.fs.debugIo();const root_dir_name = "root";pub const Options = struct { scratch: sys.fs.Dir, source: ?sys.fs.Dir = null, prefix: []const u8 = "sandbox", max_file_bytes: usize = 16 * 1024 * 1024,};pub const Layer = struct { allocator: Allocator, scratch: sys.fs.Dir, root: sys.fs.Dir, path: []u8, cleanup: bool = true, skipped: std.ArrayList([]u8) = .empty, pub fn deinit(self: *Layer) void { self.root.close(fs_io); if (self.cleanup) self.scratch.deleteTree(fs_io, self.path) catch {}; for (self.skipped.items) |item| self.allocator.free(item); self.skipped.deinit(self.allocator); self.allocator.free(self.path); self.* = undefined; } pub fn readFileAlloc(self: *const Layer, allocator: Allocator, path: []const u8, limit: usize) ![]u8 { return try self.root.readFileAlloc(fs_io, path, allocator, .limited(limit)); }};pub fn create(allocator: Allocator, options: Options) !Layer { var layer = try createEmpty(allocator, options.scratch, options.prefix); errdefer layer.deinit(); if (options.source) |source| try copyTree(allocator, source, layer.root, options.max_file_bytes, &layer.skipped); return layer;}fn createEmpty(allocator: Allocator, scratch: sys.fs.Dir, prefix: []const u8) !Layer { var attempt: usize = 0; while (attempt < 128) : (attempt += 1) { const name = try uniqueNameAlloc(allocator, prefix, attempt); errdefer allocator.free(name); scratch.createDir(fs_io, name, .default_dir) catch |err| switch (err) { error.PathAlreadyExists => { allocator.free(name); continue; }, else => return err, }; errdefer scratch.deleteTree(fs_io, name) catch {}; var container = try scratch.openDir(fs_io, name, .{}); defer container.close(fs_io); try container.createDir(fs_io, root_dir_name, .default_dir); const root = try container.openDir(fs_io, root_dir_name, .{ .iterate = true }); return .{ .allocator = allocator, .scratch = scratch, .root = root, .path = name, }; } return error.UniquePathExhausted;}fn uniqueNameAlloc(allocator: Allocator, prefix: []const u8, attempt: usize) Allocator.Error![]u8 { const pid = sys.process.currentProcessId() catch 0; const now = sys.time.nanoTimestamp(); return try std.fmt.allocPrint(allocator, "{s}-{x}-{x}-{d}", .{ prefix, pid, now, attempt });}fn copyTree(allocator: Allocator, source: sys.fs.Dir, dest: sys.fs.Dir, max_file_bytes: usize, skipped: *std.ArrayList([]u8)) !void { var dir = try source.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 dest.createDirPath(fs_io, entry.path), .file => switch (try copyFile(source, dest, entry.path, max_file_bytes)) { .copied => {}, .skipped => try skipped.append(allocator, try allocator.dupe(u8, entry.path)), }, .sym_link => try copyLink(source, dest, entry.path), else => return error.UnsupportedSourceEntry, } }}const CopyOutcome = enum { copied, skipped };fn copyFile(source: sys.fs.Dir, dest: sys.fs.Dir, path: []const u8, max_file_bytes: usize) !CopyOutcome { var input = try source.openFile(fs_io, path, .{}); defer input.close(fs_io); const source_stat = try input.stat(fs_io); if (source_stat.size > max_file_bytes) return .skipped; if (std.fs.path.dirname(path)) |parent| { if (parent.len != 0) try dest.createDirPath(fs_io, parent); } var output = try dest.createFile(fs_io, path, .{ .truncate = true, .permissions = source_stat.permissions, }); defer output.close(fs_io); var reader_buffer: [16 * 1024]u8 = undefined; var reader = input.reader(fs_io, &reader_buffer); var writer_buffer: [16 * 1024]u8 = undefined; var writer = output.writer(fs_io, &writer_buffer); var chunk: [16 * 1024]u8 = undefined; while (true) { const count = reader.interface.readSliceShort(&chunk) catch |err| switch (err) { error.ReadFailed => return reader.err orelse error.ReadFailed, }; if (count == 0) break; try writer.interface.writeAll(chunk[0..count]); } try writer.interface.flush(); try output.setPermissions(fs_io, source_stat.permissions); return .copied;}fn copyLink(source: sys.fs.Dir, dest: sys.fs.Dir, path: []const u8) !void { if (std.fs.path.dirname(path)) |parent| { if (parent.len != 0) try dest.createDirPath(fs_io, parent); } var target_buffer: [std.fs.max_path_bytes]u8 = undefined; const target_len = try source.readLink(fs_io, path, &target_buffer); try dest.symLink(fs_io, target_buffer[0..target_len], path, .{ .is_directory = linkPointsToDirectory(source, path) });}fn linkPointsToDirectory(source: sys.fs.Dir, path: []const u8) bool { const stat = source.statFile(fs_io, path, .{}) catch return false; return stat.kind == .directory;}fn createFifo(dir: sys.fs.Dir, path: [:0]const u8) !void { if (builtin.os.tag != .linux) return error.SkipZigTest; return sys.fs.createNamedPipe(dir, path, 0o600) catch |err| switch (err) { error.AccessDenied, error.UnsupportedPlatform => error.SkipZigTest, error.CreateFailed => error.CreateFifoFailed, };}fn scratchEntryCount(dir: sys.fs.Dir, allocator: Allocator) !usize { var opened = try dir.openDir(fs_io, ".", .{ .iterate = true }); defer opened.close(fs_io); var walker = try opened.walk(allocator); defer walker.deinit(); var count: usize = 0; while (try walker.next(fs_io)) |_| count += 1; return count;}test "layer copies source tree and cleans up" { var source = std.testing.tmpDir(.{}); defer source.cleanup(); var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); try source.dir.createDirPath(fs_io, "dir"); try source.dir.writeFile(fs_io, .{ .sub_path = "dir/a.txt", .data = "alpha" }); var value = try create(std.testing.allocator, .{ .scratch = scratch.dir, .source = source.dir, .prefix = "layer-test", }); const path = try std.testing.allocator.dupe(u8, value.path); defer std.testing.allocator.free(path); const content = try value.readFileAlloc(std.testing.allocator, "dir/a.txt", 1024); defer std.testing.allocator.free(content); try std.testing.expectEqualStrings("alpha", content); value.deinit(); try std.testing.expectError(error.FileNotFound, scratch.dir.statFile(fs_io, path, .{}));}test "layer preserves source symlinks" { var source = std.testing.tmpDir(.{}); defer source.cleanup(); var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); try source.dir.writeFile(fs_io, .{ .sub_path = "target.txt", .data = "target" }); source.dir.symLink(fs_io, "target.txt", "link", .{}) catch |err| switch (err) { error.AccessDenied, error.PermissionDenied => return error.SkipZigTest, else => return err, }; var value = try create(std.testing.allocator, .{ .scratch = scratch.dir, .source = source.dir, .prefix = "layer-link", }); defer value.deinit(); var target_buffer: [std.fs.max_path_bytes]u8 = undefined; const target_len = try value.root.readLink(fs_io, "link", &target_buffer); try std.testing.expectEqualStrings("target.txt", target_buffer[0..target_len]); var snapshot = try scan.capture(value.root, std.testing.allocator); defer snapshot.deinit(); try std.testing.expectEqual(change.Kind.sym_link, snapshot.find("link").?.kind);}test "layer preserves source file permissions" { if (comptime !sys.fs.FilePermissions.has_executable_bit) return error.SkipZigTest; var source = std.testing.tmpDir(.{}); defer source.cleanup(); var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); try source.dir.writeFile(fs_io, .{ .sub_path = "tool.sh", .data = "#!/bin/sh\n" }); try source.dir.setFilePermissions(fs_io, "tool.sh", .executable_file, .{}); var value = try create(std.testing.allocator, .{ .scratch = scratch.dir, .source = source.dir, .prefix = "layer-mode", }); defer value.deinit(); const stat = try value.root.statFile(fs_io, "tool.sh", .{}); try std.testing.expect((stat.permissions.toMode() & 0o111) != 0);}test "layer rejects unsupported source entries and cleans up" { var source = std.testing.tmpDir(.{}); defer source.cleanup(); var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); try createFifo(source.dir, "pipe"); try std.testing.expectError(error.UnsupportedSourceEntry, create(std.testing.allocator, .{ .scratch = scratch.dir, .source = source.dir, .prefix = "layer-fifo", })); try std.testing.expectEqual(@as(usize, 0), try scratchEntryCount(scratch.dir, std.testing.allocator));}test "layer skips oversized source files and records them" { var source = std.testing.tmpDir(.{}); defer source.cleanup(); var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); try source.dir.writeFile(fs_io, .{ .sub_path = "huge.txt", .data = "abcdef" }); try source.dir.writeFile(fs_io, .{ .sub_path = "small.txt", .data = "ok" }); var value = try create(std.testing.allocator, .{ .scratch = scratch.dir, .source = source.dir, .prefix = "layer-size", .max_file_bytes = 2, }); defer value.deinit(); try std.testing.expectEqual(@as(usize, 1), value.skipped.items.len); try std.testing.expectEqualStrings("huge.txt", value.skipped.items[0]); try std.testing.expectError(error.FileNotFound, value.root.statFile(fs_io, "huge.txt", .{})); const small = try value.readFileAlloc(std.testing.allocator, "small.txt", 1024); defer std.testing.allocator.free(small); try std.testing.expectEqualStrings("ok", small);}Source: lib/sandbox/src/root.zig:36
zig
pub const layer = @import("layer.zig");Audit
| Definitions | 3 |
|---|---|
| Public names | 3 |
| Members | 4 |
| Version | 26.7.0 |
| Revision | daab053ee433 |