io_v2_file_host.zig (51825B)
1 //! Synchronous native compiler/file profile, shared by the CLI and private 2 //! integration probes. Explicit capabilities; no standard streams or broad OS parity. 3 const std = @import("std"); 4 const Protocol = @import("io_v2_protocol.zig"); 5 const Core = @import("io_v2_core.zig"); 6 const F = @import("io_v2_files.zig"); 7 const Paths = @import("io_v2_paths.zig"); 8 const UTF8 = @import("io_v2_utf8.zig"); 9 const Stage = @import("io_v2_stage.zig"); 10 const Digest = @import("io_v2_digest.zig").Digest; 11 const Snapshot = @import("io_v2_snapshot.zig"); 12 const Indexed = @import("indexed.zig"); 13 const C = @import("codecs.zig"); 14 const Text = @import("source_text.zig"); 15 pub const Host = struct { 16 // Fixed-size host diagnostics remain inspectable even when a reducer/allocator 17 // failure prevents producing a tree Result. No allocation or dangling node IDs. 18 algorithm_detail: [512]u8 = undefined, 19 algorithm_detail_len: usize = 0, 20 utf8_detail: [96]u8 = undefined, 21 utf8_detail_len: usize = 0, 22 snapshot_cleanup_failed: bool = false, 23 stage_operation: bool = false, 24 stage_status: Stage.Status = .{}, 25 published: usize = 0, 26 opened: usize = 0, 27 closed: usize = 0, 28 close_errors: [16]F.Status = undefined, 29 close_error_count: usize = 0, 30 close_errors_omitted: usize = 0, 31 fn recordCloseError(self: *Host, status: F.Status) void { 32 if (self.close_error_count < self.close_errors.len) { 33 self.close_errors[self.close_error_count] = status; 34 self.close_error_count += 1; 35 } else self.close_errors_omitted += 1; 36 } 37 pub const Backend = union(enum) { placeholder, file: *F.File, digest: *Digest, stage: *Stage.Stage }; 38 pub const standard = [3]Backend{ .placeholder, .placeholder, .placeholder }; 39 fn fileBackend(file: *F.File) Backend { 40 return .{ .file = file }; 41 } 42 pub fn close(self: *Host, registry: anytype, backend: Backend) !void { 43 var status: F.Status = .{}; 44 return self.closeWithStatus(registry, backend, &status); 45 } 46 fn closeWithStatus(self: *Host, registry: anytype, backend: Backend, status: *F.Status) !void { 47 defer self.closed += 1; 48 switch (backend) { 49 .placeholder => unreachable, 50 .digest => |digest| registry.allocator.destroy(digest), 51 .file => |file| { 52 defer registry.allocator.destroy(file); 53 file.close(status) catch |err| { 54 self.recordCloseError(status.*); 55 return err; 56 }; 57 }, 58 .stage => |stage| { 59 defer registry.allocator.destroy(stage); 60 defer stage.deinit(); 61 var released: Stage.Status = .{}; 62 stage.release(&released) catch |err| { 63 status.* = .{ .code = released.code, .cleanup_code = released.secondary }; 64 self.recordCloseError(status.*); 65 return err; 66 }; 67 }, 68 } 69 } 70 const Op = enum { open_reader, open_writer, open_appender, open_update, read, write, close, seek, position, size, resize, sync, stat, lstat, create_directory, remove_directory, remove_file, rename_path, sync_directory, open_digest, update_digest, finish_digest, open_stage, seal_stage, publish_stage, encode_utf8, decode_utf8, hash_indexed, write_indexed, verify_indexed, read_indexed, capabilities, inspect }; 71 const entries = .{ 72 .{ "runtime", "capabilities", Op.capabilities }, .{ "runtime", "inspect", Op.inspect }, 73 .{ "indexed-tree", "read", Op.read_indexed }, .{ "indexed-tree", "hash", Op.hash_indexed }, 74 .{ "indexed-tree", "write", Op.write_indexed }, .{ "indexed-tree", "verify", Op.verify_indexed }, 75 .{ "utf8", "encode", Op.encode_utf8 }, .{ "utf8", "decode", Op.decode_utf8 }, 76 .{ "stage", "open", Op.open_stage }, .{ "stage", "seal", Op.seal_stage }, 77 .{ "stage", "publish", Op.publish_stage }, .{ "digest", "open", Op.open_digest }, 78 .{ "digest", "update", Op.update_digest }, .{ "digest", "finish", Op.finish_digest }, 79 .{ "path", "stat", Op.stat }, .{ "path", "lstat", Op.lstat }, 80 .{ "path", "remove-file", Op.remove_file }, .{ "path", "rename", Op.rename_path }, 81 .{ "directory", "create", Op.create_directory }, .{ "directory", "remove", Op.remove_directory }, 82 .{ "directory", "sync", Op.sync_directory }, .{ "file", "open-reader", Op.open_reader }, 83 .{ "file", "open-writer", Op.open_writer }, .{ "file", "open-appender", Op.open_appender }, 84 .{ "file", "open-update", Op.open_update }, .{ "stream", "read", Op.read }, 85 .{ "stream", "write", Op.write }, .{ "resource", "close", Op.close }, 86 .{ "file", "seek", Op.seek }, .{ "file", "position", Op.position }, 87 .{ "file", "size", Op.size }, .{ "file", "resize", Op.resize }, 88 .{ "file", "sync", Op.sync }, 89 }; 90 fn resolve(group: []const u8, operation: []const u8) ?Op { 91 inline for (entries) |entry| if (std.mem.eql(u8, group, entry[0]) and std.mem.eql(u8, operation, entry[1])) return entry[2]; 92 return null; 93 } 94 pub fn request(self: *Host, engine: anytype, scope: u64, req: anytype) !u32 { 95 if (req.revision != 1) return Core.unsupported(engine, req); 96 const op = resolve(req.group.slice(), req.operation.slice()) orelse return Core.unsupported(engine, req); 97 var name_buffer: [129]u8 = undefined; 98 const name = try std.fmt.bufPrint(&name_buffer, "{s}/{s}", .{ req.group.slice(), req.operation.slice() }); 99 var status: F.Status = .{}; 100 self.stage_operation = false; 101 self.snapshot_cleanup_failed = false; 102 self.stage_status = .{}; 103 const value = self.dispatch(engine, scope, op, req.arguments, &status) catch |err| { 104 if (err == error.InvalidAlgorithm) return engine.result(false, try engine.list(&.{ 105 try engine.text("unsupported"), try engine.text(name), 106 try Text.fromUtf8(engine.arena, self.algorithm_detail[0..self.algorithm_detail_len]), try engine.text("none"), 107 })); 108 if (self.stage_operation) status.code = self.stage_status.code; 109 const category = switch (err) { 110 error.InvalidArgument, error.EmptySnapshotDirectory => "invalid-argument", 111 error.InvalidData, error.SnapshotTruncated, error.SnapshotInvalid => "invalid-data", 112 error.SnapshotBound => "invalid-bound", 113 error.SnapshotDigestMismatch => "integrity-failed", 114 error.SnapshotCleanup => "cleanup-failed", 115 error.InvalidState => "invalid-state", 116 error.InvalidDestination => "invalid-destination", 117 error.InvalidBound, error.EncodedLimit => "invalid-bound", 118 error.DigestMismatch, error.FrameMismatch => "integrity-failed", 119 error.UnsupportedFileType, error.UnsupportedMode => "unsupported", 120 error.WrongAccess => "wrong-kind", 121 error.NotDirectory => "not-directory", 122 error.Closed, error.ClosedHandle => "closed-handle", 123 error.StaleHandle => "stale-handle", 124 error.OutOfScope => "out-of-scope", 125 error.Busy => "busy", 126 error.ClosedScope => "closed-scope", 127 error.BorrowedResource => "borrowed-resource", 128 error.InternalResource => "internal-resource", 129 error.HostFailure => F.errorCategory(status.code), 130 else => return err, 131 }; 132 const detail = if (err == error.EmptySnapshotDirectory) "empty snapshot directory" else if (err == error.SnapshotBound) "invalid snapshot size limit" else if (err == error.SnapshotTruncated) "incomplete indexed frame" else if (err == error.SnapshotInvalid) "invalid indexed frame" else if (err == error.SnapshotDigestMismatch) "CAS digest mismatch" else if (err == error.SnapshotCleanup) "snapshot release failed" else if (err == error.EncodedLimit) "encoded tree exceeds limit" else if (err == error.DigestMismatch) "CAS digest mismatch" else if (err == error.FrameMismatch) "indexed frame differs from canonical tree" else if (err == error.InvalidData) self.utf8_detail[0..self.utf8_detail_len] else if (err == error.HostFailure) F.errorText(status.code) else if (err == error.UnsupportedFileType) "path-based files support regular files only" else if (err == error.UnsupportedMode) "operation unavailable in this file mode" else if (err == error.NotDirectory) "path is not a directory" else if (err == error.InvalidDestination) "destination must name a file outside the private staging namespace" else if (err == error.InvalidState) (if (self.stage_operation) "stage is not in the required phase" else "digest already finished") else ""; 133 // Mutating opens can create/truncate before a later failure. Never 134 // claim absence of effects; this is not transactional rollback. 135 const progress = if (err == error.SnapshotTruncated or err == error.SnapshotInvalid or err == error.SnapshotDigestMismatch) "unknown" else if (err == error.FrameMismatch) "unknown" else if (self.stage_operation) switch (self.stage_status.progress) { 136 .none => "none", 137 .not_published => "not-published", 138 .unknown => "unknown", 139 .published => "published", 140 } else if (err == error.HostFailure and op != .open_reader and op != .size and op != .position and op != .stat and op != .lstat) "unknown" else "none"; 141 var detail_buffer: [1024]u8 = undefined; 142 const reported = if (self.snapshot_cleanup_failed) try std.fmt.bufPrint(&detail_buffer, "{s}; cleanup: snapshot release failed", .{detail}) else detail; 143 return engine.result(false, try engine.list(&.{ try engine.text(category), try engine.text(name), try engine.text(reported), try engine.text(progress) })); 144 }; 145 return engine.result(true, value); 146 } 147 fn decodeText(engine: anytype, input: u32) ![]u8 { 148 var output: std.ArrayList(u8) = .empty; 149 errdefer output.deinit(engine.arena.allocator); 150 var rest = input; 151 const reader = engine.reader(); 152 while (try reader.node(rest) != .leaf) { 153 const cell = try reader.pair(rest) orelse return error.InvalidArgument; 154 const scalar = try reader.nat(cell.first, 21) orelse return error.InvalidArgument; 155 if (scalar > 0x10ffff or (scalar >= 0xd800 and scalar <= 0xdfff)) return error.InvalidArgument; 156 var encoded: [4]u8 = undefined; 157 const count = try std.unicode.utf8Encode(@intCast(scalar), &encoded); 158 try output.appendSlice(engine.arena.allocator, encoded[0..count]); 159 rest = cell.second; 160 } 161 return output.toOwnedSlice(engine.arena.allocator); 162 } 163 fn decodeChunk(engine: anytype, input: u32, bytes: *[4096]u8) !usize { 164 var count: usize = 0; 165 var rest = input; 166 const reader = engine.reader(); 167 while (try reader.node(rest) != .leaf) { 168 // Match the protocol decoder (invalid argument), not the lower-level 169 // byte-IO bound check. Observe excess spine, never excess head/tail. 170 if (count == bytes.len) return error.InvalidArgument; 171 const cell = try reader.pair(rest) orelse return error.InvalidArgument; 172 bytes[count] = @intCast(try reader.nat(cell.first, 8) orelse return error.InvalidArgument); 173 count += 1; 174 rest = cell.second; 175 } 176 return count; 177 } 178 fn readIndexed(self: *Host, engine: anytype, scope: u64, arguments: u32, status: *F.Status) !u32 { 179 const r = engine.reader(); 180 var fields: [5]u32 = undefined; 181 var rest = arguments; 182 for (&fields) |*field| { 183 const cell = try r.pair(rest) orelse return error.InvalidArgument; 184 field.* = cell.first; 185 rest = cell.second; 186 } 187 if (try r.node(rest) != .leaf) return error.InvalidArgument; 188 const token = try engine.token(fields[0], "io.v2.resource") orelse return error.InvalidArgument; 189 const directory = try decodeText(engine, fields[1]); 190 defer engine.arena.allocator.free(directory); 191 const path = try engine.arena.allocator.dupeZ(u8, directory); 192 defer engine.arena.allocator.free(path); 193 const limit = try r.nat(fields[2], 63) orelse return error.InvalidArgument; 194 const size = try r.nat(fields[3], 63) orelse return error.InvalidArgument; 195 var digest: [64]u8 = undefined; 196 rest = fields[4]; 197 for (&digest) |*byte| { 198 const cell = try r.pair(rest) orelse return error.InvalidArgument; 199 const scalar = try r.nat(cell.first, 7) orelse return error.InvalidArgument; 200 if (!((scalar >= '0' and scalar <= '9') or (scalar >= 'a' and scalar <= 'f'))) return error.InvalidArgument; 201 byte.* = @intCast(scalar); 202 rest = cell.second; 203 } 204 if (try r.node(rest) != .leaf) return error.InvalidArgument; 205 if (size > limit) return error.SnapshotBound; 206 if (directory.len == 0) return error.EmptySnapshotDirectory; 207 const registry = engine.scheduler.registry; 208 const backend = (try registry.lookup(scope, token)).backend; 209 if (backend != .file or (backend.file.mode != .reader and backend.file.mode != .update)) return error.WrongAccess; 210 const lease = try registry.beginLease(scope, token, .exclusive, false); 211 defer registry.endLease(lease.token) catch unreachable; 212 const temporary = blk: { 213 const admission = try registry.beginAcquire(scope); 214 errdefer registry.abortAcquire(admission) catch unreachable; 215 const file = try registry.allocator.create(F.File); 216 errdefer registry.allocator.destroy(file); 217 file.* = Snapshot.create(registry.allocator, status, path) catch |err| { 218 if (status.cleanup_code != 0) self.recordCloseError(.{ .code = status.cleanup_code }); 219 return err; 220 }; 221 errdefer { 222 var cleanup: F.Status = .{}; 223 file.close(&cleanup) catch self.recordCloseError(cleanup); 224 } 225 break :blk try registry.publish(admission, .{ .file = file }, .{ .internal = true }); 226 }; 227 self.opened += 1; 228 const file = (try registry.lookup(scope, temporary)).backend.file; 229 const result = blk: { 230 Snapshot.authenticate(file, backend.file, status, size, &digest) catch |err| break :blk @as(anyerror!u32, err); 231 var snapshot = Snapshot.Snapshot{ .file = file, .status = status, .length = size }; 232 break :blk Indexed.loadSnapshot(engine.arena, &snapshot) catch |err| switch (err) { 233 error.InvalidData, error.ExportNotFound => error.SnapshotInvalid, 234 else => err, 235 }; 236 }; 237 // All host work is synchronous. Retire the internal descriptor even on 238 // ordinary Result failure or OOM; cleanup must not replace a primary fault. 239 const owned = (try registry.claimClose(scope, temporary, true)).?; 240 defer registry.finishClose(temporary) catch unreachable; 241 var cleanup: F.Status = .{}; 242 self.closeWithStatus(registry, owned.backend, &cleanup) catch { 243 if (result) |_| return error.SnapshotCleanup else |primary| { 244 self.snapshot_cleanup_failed = true; 245 return primary; 246 } 247 }; 248 return result; 249 } 250 fn indexed(self: *Host, engine: anytype, scope: u64, op: Op, arguments: u32, status: *F.Status) !u32 { 251 const r = engine.reader(); 252 const first = try r.pair(arguments) orelse return error.InvalidArgument; 253 if (op == .hash_indexed) { 254 const limit = try r.nat(first.first, 63) orelse return error.InvalidArgument; 255 var plan = try Indexed.prepare(engine.arena, first.second); 256 defer plan.deinit(); 257 if (plan.encodedLength() > limit) return error.EncodedLimit; 258 const hash = try plan.hash(); 259 return engine.list(&.{ try C.ofNumber(engine.arena, plan.encodedLength()), try engine.text(&hash) }); 260 } 261 const token = try engine.token(first.first, "io.v2.resource") orelse return error.InvalidArgument; 262 const second = try r.pair(first.second) orelse return error.InvalidArgument; 263 const limit = try r.nat(second.first, 63) orelse return error.InvalidArgument; 264 const third = try r.pair(second.second) orelse return error.InvalidArgument; 265 const tree_field = if (op == .write_indexed) try r.pair(third.second) orelse return error.InvalidArgument else third; 266 if (try r.node(tree_field.second) != .leaf) return error.InvalidArgument; 267 var expected: [64]u8 = undefined; 268 if (op == .write_indexed) { 269 var rest = third.first; 270 for (&expected) |*byte| { 271 const cell = try r.pair(rest) orelse return error.InvalidArgument; 272 const scalar = try r.nat(cell.first, 7) orelse return error.InvalidArgument; 273 if (!((scalar >= '0' and scalar <= '9') or (scalar >= 'a' and scalar <= 'f'))) return error.InvalidArgument; 274 byte.* = @intCast(scalar); 275 rest = cell.second; 276 } 277 if (try r.node(rest) != .leaf) return error.InvalidArgument; 278 } 279 const registry = engine.scheduler.registry; 280 const backend = (try registry.lookup(scope, token)).backend; 281 if (op == .verify_indexed) { 282 if (backend != .file or (backend.file.mode != .reader and backend.file.mode != .update)) return error.WrongAccess; 283 } else switch (backend) { 284 .file => |file| if (file.mode == .reader) return error.WrongAccess, 285 .stage => {}, 286 else => return error.WrongAccess, 287 } 288 const lease = try registry.beginLease(scope, token, .exclusive, false); 289 defer registry.endLease(lease.token) catch unreachable; 290 var plan = try Indexed.prepare(engine.arena, tree_field.first); 291 defer plan.deinit(); 292 if (plan.encodedLength() > limit) return error.EncodedLimit; 293 const hash = try plan.hash(); 294 if (op == .write_indexed and !std.mem.eql(u8, &hash, &expected)) return error.DigestMismatch; 295 var sink = IndexedSink{ .host = self, .backend = backend, .status = status, .verify = op == .verify_indexed }; 296 try plan.emit(&sink); 297 return engine.list(&.{ try C.ofNumber(engine.arena, plan.encodedLength()), try engine.text(&hash) }); 298 } 299 const IndexedSink = struct { 300 host: *Host, 301 backend: Backend, 302 status: *F.Status, 303 verify: bool, 304 pub fn writeAll(self: *@This(), bytes: []const u8) !void { 305 var offset: usize = 0; 306 while (offset < bytes.len) { 307 const count = if (self.verify) blk: { 308 var actual: [4096]u8 = undefined; 309 const n = try self.backend.file.read(self.status, actual[0 .. bytes.len - offset]); 310 if (n == 0 or !std.mem.eql(u8, actual[0..n], bytes[offset..][0..n])) return error.FrameMismatch; 311 break :blk n; 312 } else switch (self.backend) { 313 .file => |file| try file.write(self.status, bytes[offset..]), 314 .stage => |stage| blk: { 315 self.host.stage_operation = true; 316 break :blk try stage.write(&self.host.stage_status, bytes[offset..]); 317 }, 318 else => unreachable, 319 }; 320 std.debug.assert(count > 0 and count <= bytes.len - offset); 321 offset += count; 322 } 323 } 324 }; 325 const core_names = [_][]const u8{ "pure", "bind", "fail", "scope", "current-scope", "spawn", "join", "discard", "cancel", "cancelled" }; 326 fn strings(engine: anytype, values: []const []const u8) !u32 { 327 var ids: std.ArrayList(u32) = .empty; 328 defer ids.deinit(engine.arena.allocator); 329 for (values) |value| try ids.append(engine.arena.allocator, try engine.text(value)); 330 return engine.list(ids.items); 331 } 332 fn row(engine: anytype, key: []const u8, value: u32) !u32 { 333 return engine.list(&.{ try engine.text(key), value }); 334 } 335 fn capabilities(engine: anytype) !u32 { 336 var groups: std.ArrayList(u32) = .empty; 337 defer groups.deinit(engine.arena.allocator); 338 inline for (.{ "runtime", "resource", "stream", "file", "path", "directory", "stage", "digest", "indexed-tree", "utf8" }) |group| { 339 var operations: std.ArrayList(u32) = .empty; 340 defer operations.deinit(engine.arena.allocator); 341 inline for (entries) |entry| { 342 if (comptime std.mem.eql(u8, group, entry[0])) try operations.append(engine.arena.allocator, try engine.text(entry[1])); 343 } 344 try groups.append(engine.arena.allocator, try engine.list(&.{ try engine.text(group), try C.ofNumber(engine.arena, 1), try engine.list(operations.items) })); 345 } 346 const limits = try engine.list(&.{ 347 try row(engine, "chunk-bytes", try C.ofNumber(engine.arena, 4096)), 348 try row(engine, "transfer-handles", try C.ofNumber(engine.arena, 256)), 349 try row(engine, "file-offset-max", try C.ofNumber(engine.arena, F.limit)), 350 try row(engine, "indexed-sections", try C.ofNumber(engine.arena, 64)), 351 try row(engine, "indexed-manifest-bytes", try C.ofNumber(engine.arena, 65536)), 352 try row(engine, "utf8-encode-scalars", try C.ofNumber(engine.arena, 1024)), 353 }); 354 return engine.list(&.{ 355 try row(engine, "schema", try C.ofNumber(engine.arena, 1)), 356 try row(engine, "io-version", try C.ofNumber(engine.arena, 2)), 357 try row(engine, "backend", try engine.text("zig-native")), 358 try row(engine, "authority", try engine.text("os-process")), 359 try row(engine, "core", try strings(engine, &core_names)), 360 try row(engine, "groups", try engine.list(groups.items)), 361 try row(engine, "limits", limits), 362 try row(engine, "digest-algorithms", try strings(engine, &.{"sha256"})), 363 try row(engine, "restrictions", try strings(engine, &.{ 364 "action-boundary-cancellation", "no-hard-deadlines", "synchronous-host-calls", 365 "path-files-regular-only", "standard-streams-unsupported", "directory-cursors-unsupported", 366 "file-single-cursor-lease", "append-no-position-or-resize", "stage-same-filesystem", 367 "stage-no-copy-fallback", "stage-private-namespace", "publish-new-requires-hard-links", 368 "no-crash-recovery", "indexed-authenticated-snapshot", "indexed-caller-frame-and-disk-limit", 369 "indexed-dag-resident", "indexed-writer-preflight", "indexed-budget-not-heap-quota", 370 "sync-host-filesystem-guarantees", "utf8-strict-scalars", 371 })), 372 }); 373 } 374 fn dispatch(self: *Host, engine: anytype, scope: u64, op: Op, arguments: u32, status: *F.Status) !u32 { 375 const r = engine.reader(); 376 const registry = engine.scheduler.registry; 377 if (op == .capabilities) { 378 if (try r.node(arguments) != .leaf) return error.InvalidArgument; 379 return capabilities(engine); 380 } 381 if (op == .inspect) { 382 const action = Protocol.decode(engine.arena, arguments, r.fuel) catch |err| switch (err) { 383 error.InvalidComputationShape, error.UnknownComputationConstructor, error.InvalidFacilityHeader => return error.InvalidArgument, 384 else => return err, 385 }; 386 return switch (action) { 387 .core => |value| engine.list(&.{ try engine.text("core"), try engine.text(core_names[@intFromEnum(value.code)]) }), 388 .request => |value| engine.list(&.{ try engine.text("facility"), try engine.text(value.group.slice()), try C.ofNumber(engine.arena, value.revision), try engine.text(value.operation.slice()), try engine.text(if (value.revision == 1 and resolve(value.group.slice(), value.operation.slice()) != null) "available" else "unsupported") }), 389 }; 390 } 391 if (op == .read_indexed) return self.readIndexed(engine, scope, arguments, status); 392 if (op == .hash_indexed or op == .write_indexed or op == .verify_indexed) return self.indexed(engine, scope, op, arguments, status); 393 if (op == .encode_utf8) { 394 var bytes: [4096]u8 = undefined; 395 var scalars: usize = 0; 396 var used: usize = 0; 397 var rest = arguments; 398 while (try r.node(rest) != .leaf) { 399 if (scalars == 1024) return error.InvalidArgument; 400 const cell = try r.pair(rest) orelse return error.InvalidArgument; 401 const scalar = try r.nat(cell.first, 21) orelse return error.InvalidArgument; 402 if (scalar > 0x10ffff or (scalar >= 0xd800 and scalar <= 0xdfff)) return error.InvalidArgument; 403 used += try std.unicode.utf8Encode(@intCast(scalar), bytes[used..][0..4]); 404 scalars += 1; 405 rest = cell.second; 406 } 407 return C.ofBytes(engine.arena, bytes[0..used]); 408 } 409 if (op == .decode_utf8) { 410 const final = try r.pair(arguments) orelse return error.InvalidArgument; 411 const input = try r.pair(final.second) orelse return error.InvalidArgument; 412 if (try r.node(input.second) != .leaf) return error.InvalidArgument; 413 const ended = try r.nat(final.first, 1) orelse return error.InvalidArgument; 414 var bytes: [4096]u8 = undefined; 415 const count = try decodeChunk(engine, input.first, &bytes); 416 return switch (UTF8.scan(bytes[0..count], ended == 1)) { 417 .too_large => unreachable, // argument decoder already bounds it 418 .fault => |fault| blk: { 419 const why: []const u8 = switch (fault.kind) { 420 .leading => "invalid UTF-8 leading byte", 421 .sequence => "invalid UTF-8 sequence", 422 .incomplete => "incomplete UTF-8 sequence", 423 }; 424 const detail = try std.fmt.bufPrint(&self.utf8_detail, "{s} at byte {d}", .{ why, fault.offset }); 425 self.utf8_detail_len = detail.len; 426 break :blk error.InvalidData; 427 }, 428 .prefix => |prefix| try engine.list(&.{ try Text.fromUtf8(engine.arena, bytes[0..prefix]), try C.ofBytes(engine.arena, bytes[prefix..count]) }), 429 }; 430 } 431 switch (op) { 432 .stat, .lstat, .create_directory, .remove_directory, .remove_file, .rename_path, .sync_directory => { 433 const paths = if (op == .rename_path) try r.pair(arguments) orelse return error.InvalidArgument else null; 434 const raw = try decodeText(engine, if (paths) |p| p.first else arguments); 435 defer engine.arena.allocator.free(raw); 436 const path = try engine.arena.allocator.dupeZ(u8, raw); 437 defer engine.arena.allocator.free(path); 438 if (op == .stat or op == .lstat) { 439 const info = try Paths.stat(status, path, op == .stat); 440 return switch (info) { 441 .regular => |size| try engine.list(&.{ try engine.text("file"), try C.ofNumber(engine.arena, size) }), 442 .directory => try engine.list(&.{try engine.text("directory")}), 443 .symlink => try engine.list(&.{try engine.text("symlink")}), 444 .other => try engine.list(&.{try engine.text("other")}), 445 }; 446 } 447 if (op == .sync_directory) { 448 Paths.syncDirectory(status, path) catch |err| { 449 if (status.cleanup_code != 0) self.recordCloseError(.{ .code = status.cleanup_code }); 450 return err; 451 }; 452 return engine.leaf; 453 } 454 if (op == .rename_path) { 455 const other_raw = try decodeText(engine, paths.?.second); 456 defer engine.arena.allocator.free(other_raw); 457 const other = try engine.arena.allocator.dupeZ(u8, other_raw); 458 defer engine.arena.allocator.free(other); 459 try Paths.mutate(status, .rename, path, other); 460 } else { 461 try Paths.mutate(status, switch (op) { 462 .create_directory => .create_directory, 463 .remove_directory => .remove_directory, 464 .remove_file => .remove_file, 465 else => unreachable, 466 }, path, null); 467 } 468 return engine.leaf; 469 }, 470 else => {}, 471 } 472 switch (op) { 473 .open_reader, .open_writer, .open_appender, .open_update, .open_digest, .open_stage => { 474 const pair = try r.pair(arguments) orelse return error.InvalidArgument; 475 const owner = try engine.token(pair.first, "io.v2.scope") orelse return error.InvalidArgument; 476 if (owner.run != registry.identity or !registry.within(scope, owner.key)) return error.InvalidArgument; 477 const text = try decodeText(engine, pair.second); 478 defer engine.arena.allocator.free(text); 479 const path = try engine.arena.allocator.dupeZ(u8, text); 480 defer engine.arena.allocator.free(path); 481 if (op == .open_digest) { 482 if (!std.mem.eql(u8, text, "sha256")) { 483 const view = try std.unicode.Utf8View.init(text); 484 var it = view.iterator(); 485 var scalars: usize = 0; 486 while (scalars < 128 and it.nextCodepoint() != null) : (scalars += 1) {} 487 self.algorithm_detail_len = it.i; 488 @memcpy(self.algorithm_detail[0..it.i], text[0..it.i]); 489 return error.InvalidAlgorithm; 490 } 491 // End acquisition rollback BEFORE encoding the source token. 492 // Once published, only scope teardown owns this state, even 493 // when reply allocation fails immediately afterwards. 494 const token = acquired: { 495 const admission = try registry.beginAcquire(owner.key); 496 errdefer registry.abortAcquire(admission) catch unreachable; 497 const digest = try registry.allocator.create(Digest); 498 errdefer registry.allocator.destroy(digest); 499 digest.* = .{}; 500 break :acquired try registry.publish(admission, .{ .digest = digest }, .{}); 501 }; 502 self.opened += 1; 503 return engine.marker("io.v2.resource", token.key); 504 } 505 if (op == .open_stage) { 506 self.stage_operation = true; 507 const token = acquired: { 508 const admission = try registry.beginAcquire(owner.key); 509 errdefer registry.abortAcquire(admission) catch unreachable; 510 const stage = try registry.allocator.create(Stage.Stage); 511 errdefer registry.allocator.destroy(stage); 512 stage.* = try Stage.Stage.init(registry.allocator, &self.stage_status, path); 513 errdefer { 514 var released: Stage.Status = .{}; 515 stage.release(&released) catch self.recordCloseError(.{ .code = released.code, .cleanup_code = released.secondary }); 516 stage.deinit(); 517 } 518 break :acquired try registry.publish(admission, .{ .stage = stage }, .{}); 519 }; 520 self.opened += 1; 521 return engine.marker("io.v2.resource", token.key); 522 } 523 const mode: F.Mode = switch (op) { 524 .open_reader => .reader, 525 .open_writer => .writer, 526 .open_appender => .appender, 527 .open_update => .update, 528 else => unreachable, 529 }; 530 const token = F.acquireWrapped(registry, owner.key, status, path, mode, fileBackend) catch |err| { 531 if (status.cleanup_code != 0) self.recordCloseError(.{ .code = status.cleanup_code }); 532 return err; 533 }; 534 self.opened += 1; 535 return engine.marker("io.v2.resource", token.key); 536 }, 537 else => {}, 538 } 539 const paired = op == .read or op == .write or op == .update_digest or op == .seek or op == .resize or op == .publish_stage; 540 const pair = if (paired) try r.pair(arguments) orelse return error.InvalidArgument else null; 541 const token = try engine.token(if (pair) |p| p.first else arguments, "io.v2.resource") orelse return error.InvalidArgument; 542 var number: u64 = 0; 543 var bytes: [4096]u8 = undefined; 544 var count: usize = 0; 545 switch (op) { 546 .read, .seek, .resize => { 547 number = try r.nat(pair.?.second, if (op == .read) 16 else 63) orelse return error.InvalidArgument; 548 if (op == .read and (number == 0 or number > 4096)) return error.InvalidBound; 549 }, 550 .write, .update_digest => { 551 count = try decodeChunk(engine, pair.?.second, &bytes); 552 if (count == 0) return error.InvalidBound; 553 }, 554 else => {}, 555 } 556 var destination: ?[:0]u8 = null; 557 defer if (destination) |path| engine.arena.allocator.free(path); 558 if (op == .publish_stage) { 559 const fields = try r.pair(pair.?.second) orelse return error.InvalidArgument; 560 number = try r.nat(fields.first, 1) orelse return error.InvalidArgument; 561 const raw = try decodeText(engine, fields.second); 562 defer engine.arena.allocator.free(raw); 563 destination = try engine.arena.allocator.dupeZ(u8, raw); 564 } 565 if (op == .close) { 566 const resource = try registry.claimClose(scope, token, false) orelse return error.Busy; 567 defer registry.finishClose(token) catch unreachable; 568 try self.closeWithStatus(registry, resource.backend, status); 569 return engine.leaf; 570 } 571 const backend = (try registry.lookup(scope, token)).backend; 572 if (op == .seal_stage or op == .publish_stage or (op == .write and backend == .stage)) { 573 if (backend != .stage) return error.WrongAccess; 574 const lease = try registry.beginLease(scope, token, .exclusive, false); 575 defer registry.endLease(lease.token) catch unreachable; 576 self.stage_operation = true; 577 if (op == .write) return C.ofNumber(engine.arena, try backend.stage.write(&self.stage_status, bytes[0..count])); 578 if (op == .seal_stage) { 579 try backend.stage.seal(&self.stage_status); 580 return engine.leaf; 581 } 582 const published = try backend.stage.publish(&self.stage_status, number == 1, destination.?); 583 if (published == .published) self.published += 1; 584 return engine.text(if (published == .published) "published" else "exists"); 585 } 586 if (op == .update_digest or op == .finish_digest) { 587 if (backend != .digest) return error.WrongAccess; 588 const lease = try registry.beginLease(scope, token, .exclusive, false); 589 defer registry.endLease(lease.token) catch unreachable; 590 if (op == .update_digest) { 591 try backend.digest.update(bytes[0..count]); 592 return engine.leaf; 593 } 594 const hex = try backend.digest.finish(); 595 return engine.text(&hex); 596 } 597 if (backend != .file) return error.WrongAccess; 598 const file = backend.file; 599 const lease = try registry.beginLease(scope, token, .exclusive, false); 600 defer registry.endLease(lease.token) catch unreachable; 601 return switch (op) { 602 .read => blk: { 603 const n = try file.read(status, bytes[0..@intCast(number)]); 604 const label = try engine.text(if (n == 0) "eof" else "data"); 605 break :blk try engine.arena.alloc(.{ .fork = .{ .left = label, .right = if (n == 0) engine.leaf else try C.ofBytes(engine.arena, bytes[0..n]) } }); 606 }, 607 .write => try C.ofNumber(engine.arena, try file.write(status, bytes[0..count])), 608 .seek => blk: { 609 try file.seek(number); 610 break :blk engine.leaf; 611 }, 612 .resize => blk: { 613 try file.resize(status, number); 614 break :blk engine.leaf; 615 }, 616 .position => try C.ofNumber(engine.arena, try file.position()), 617 .size => try C.ofNumber(engine.arena, try file.size(status)), 618 .sync => blk: { 619 try file.sync(status); 620 break :blk engine.leaf; 621 }, 622 else => unreachable, 623 }; 624 } 625 }; 626 627 fn testProgram(a: *@import("arena.zig").Arena, path: []const u8, terminal: ?u64) !u32 { 628 return testOpenProgram(a, path, terminal, "file", "open-reader"); 629 } 630 fn testOpenProgram(a: *@import("arena.zig").Arena, path: []const u8, terminal: ?u64, group: []const u8, operation: []const u8) !u32 { 631 const leaf = try a.alloc(.leaf); 632 const scope = try C.ofList(a, &.{ try C.ofString(a, "io.v2.scope"), try C.ofNumber(a, 1), leaf }); 633 const args = try a.alloc(.{ .fork = .{ .left = scope, .right = try Text.fromUtf8(a, path) } }); 634 const named = try a.alloc(.{ .fork = .{ .left = try C.ofString(a, group), .right = try a.alloc(.{ .fork = .{ .left = try C.ofNumber(a, 1), .right = try a.alloc(.{ .fork = .{ .left = try C.ofString(a, operation), .right = args } }) } }) } }); 635 const request = try a.alloc(.{ .fork = .{ .left = try C.ofNumber(a, 10), .right = named } }); 636 if (terminal) |tag| { 637 const next = try a.alloc(.{ .fork = .{ .left = try C.ofNumber(a, tag), .right = leaf } }); 638 const constant = try a.alloc(.{ .fork = .{ .left = leaf, .right = next } }); 639 return a.alloc(.{ .fork = .{ .left = try C.ofNumber(a, 1), .right = try a.alloc(.{ .fork = .{ .left = request, .right = constant } }) } }); 640 } 641 return request; 642 } 643 fn failureProbe(allocator: std.mem.Allocator, path: []const u8) !void { 644 var a = @import("arena.zig").Arena.init(allocator); 645 defer a.deinit(); 646 const program = try testProgram(&a, path, null); 647 a.gc_enabled = true; 648 try a.setGcPolicy(1, 1); 649 var host: Host = .{}; 650 defer std.debug.assert(host.opened == host.closed); 651 _ = try Core.runWithHost(Host, &host, &a, program, 1, 10000); 652 try std.testing.expectEqual(@as(usize, 1), host.opened); 653 } 654 test "physical resources settle on success, fail, cancellation, protocol fault and allocation failure" { 655 const testing = std.testing; 656 var tmp = testing.tmpDir(.{}); 657 defer tmp.cleanup(); 658 try tmp.dir.writeFile(testing.io, .{ .sub_path = "file", .data = "abc" }); 659 const path = try tmp.dir.realPathFileAlloc(testing.io, "file", testing.allocator); 660 defer testing.allocator.free(path); 661 for ([_]u64{ 0, 2, 9, 11 }) |tag| { 662 var a = @import("arena.zig").Arena.init(testing.allocator); 663 defer a.deinit(); 664 const program = try testProgram(&a, path, tag); 665 var host: Host = .{}; 666 const outcome = try Core.runWithHost(Host, &host, &a, program, 1, 10000); 667 try testing.expectEqual(@as(usize, 1), host.opened); 668 try testing.expectEqual(host.opened, host.closed); 669 try testing.expectEqual(@as(usize, 0), host.close_error_count); 670 const expected: @TypeOf(outcome.kind) = if (tag == 0) .completed else if (tag == 9) .cancelled else .failed; 671 try testing.expectEqual(expected, outcome.kind); 672 } 673 try testing.checkAllAllocationFailures(testing.allocator, failureProbe, .{path}); 674 } 675 676 const CloseFailureHost = struct { 677 pub const Backend = Host.Backend; 678 pub const standard = Host.standard; 679 inner: Host = .{}, 680 pub fn request(self: *@This(), engine: anytype, scope: u64, req: anytype) !u32 { 681 return self.inner.request(engine, scope, req); 682 } 683 pub fn close(self: *@This(), registry: anytype, backend: Backend) !void { 684 // Simulate a close that has released its descriptor but reports failure. 685 // No external FD is closed/reused, and every real descriptor is released. 686 var original = backend.file.*; 687 var status: F.Status = .{}; 688 try original.close(&status); 689 backend.file.fd = -1; 690 try self.inner.close(registry, backend); 691 } 692 }; 693 test "physical close failures defeat success but retain primary failure and cancellation" { 694 const testing = std.testing; 695 var tmp = testing.tmpDir(.{}); 696 defer tmp.cleanup(); 697 try tmp.dir.writeFile(testing.io, .{ .sub_path = "file", .data = "abc" }); 698 const path = try tmp.dir.realPathFileAlloc(testing.io, "file", testing.allocator); 699 defer testing.allocator.free(path); 700 for ([_]u64{ 0, 2, 9 }) |tag| { 701 var a = @import("arena.zig").Arena.init(testing.allocator); 702 defer a.deinit(); 703 const program = try testProgram(&a, path, tag); 704 var host: CloseFailureHost = .{}; 705 const outcome = try Core.runWithHost(CloseFailureHost, &host, &a, program, 1, 10000); 706 try testing.expectEqual(@as(usize, 1), host.inner.closed); 707 try testing.expectEqual(@as(usize, 1), host.inner.close_error_count); 708 const expected: @TypeOf(outcome.kind) = if (tag == 9) .cancelled else .failed; 709 try testing.expectEqual(expected, outcome.kind); 710 if (tag != 0) { 711 try testing.expect(a.get(outcome.value).* == .leaf); 712 try testing.expectEqual(@as(usize, 1), outcome.cleanup.len); 713 } 714 } 715 } 716 717 fn digestAllocationProbe(allocator: std.mem.Allocator) !void { 718 var a = @import("arena.zig").Arena.init(allocator); 719 defer a.deinit(); 720 const program = try testOpenProgram(&a, "sha256", null, "digest", "open"); 721 a.gc_enabled = true; 722 try a.setGcPolicy(1, 1); 723 var host: Host = .{}; 724 defer std.debug.assert(host.opened == host.closed); 725 _ = try Core.runWithHost(Host, &host, &a, program, 1, 10000); 726 try std.testing.expectEqual(@as(usize, 1), host.opened); 727 } 728 test "digest acquisition and reply allocation failures retire every admitted state" { 729 try std.testing.checkAllAllocationFailures(std.testing.allocator, digestAllocationProbe, .{}); 730 } 731 732 fn stageAllocationProbe(allocator: std.mem.Allocator, directory: []const u8, dir: std.Io.Dir) !void { 733 var a = @import("arena.zig").Arena.init(allocator); 734 defer a.deinit(); 735 const program = try testOpenProgram(&a, directory, null, "stage", "open"); 736 a.gc_enabled = true; 737 try a.setGcPolicy(1, 1); 738 var host: Host = .{}; 739 defer std.debug.assert(host.opened == host.closed); 740 const outcome = Core.runWithHost(Host, &host, &a, program, 1, 10000); 741 var iterator = dir.iterate(); 742 try std.testing.expect((try iterator.next(std.testing.io)) == null); 743 _ = try outcome; 744 try std.testing.expectEqual(@as(usize, 1), host.opened); 745 } 746 test "stage acquisition and reply allocation failures remove every private name" { 747 const testing = std.testing; 748 var tmp = testing.tmpDir(.{ .iterate = true }); 749 defer tmp.cleanup(); 750 const directory = try tmp.dir.realPathFileAlloc(testing.io, ".", testing.allocator); 751 defer testing.allocator.free(directory); 752 try testing.checkAllAllocationFailures(testing.allocator, stageAllocationProbe, .{ directory, tmp.dir }); 753 } 754 755 fn testRequest(a: *@import("arena.zig").Arena, group: []const u8, operation: []const u8, args: u32) !u32 { 756 const payload = try a.alloc(.{ .fork = .{ .left = try C.ofString(a, group), .right = try a.alloc(.{ .fork = .{ .left = try C.ofNumber(a, 1), .right = try a.alloc(.{ .fork = .{ .left = try C.ofString(a, operation), .right = args } }) } }) } }); 757 return a.alloc(.{ .fork = .{ .left = try C.ofNumber(a, 10), .right = payload } }); 758 } 759 fn then(a: *@import("arena.zig").Arena, left: u32, right: u32) !u32 { 760 const constant = try a.alloc(.{ .fork = .{ .left = try a.alloc(.leaf), .right = right } }); 761 return a.alloc(.{ .fork = .{ .left = try C.ofNumber(a, 1), .right = try a.alloc(.{ .fork = .{ .left = left, .right = constant } }) } }); 762 } 763 fn publicationAllocationProbe(allocator: std.mem.Allocator, directory: []const u8, destination: []const u8, dir: std.Io.Dir) !void { 764 var a = @import("arena.zig").Arena.init(allocator); 765 defer a.deinit(); 766 const opened = try testOpenProgram(&a, directory, null, "stage", "open"); 767 const token = try C.ofList(&a, &.{ try C.ofString(&a, "io.v2.resource"), try C.ofNumber(&a, 1), try C.ofNumber(&a, 5) }); 768 const sealed = try testRequest(&a, "stage", "seal", token); 769 const args = try a.alloc(.{ .fork = .{ .left = token, .right = try a.alloc(.{ .fork = .{ .left = try C.ofNumber(&a, 1), .right = try Text.fromUtf8(&a, destination) } }) } }); 770 const publish = try testRequest(&a, "stage", "publish", args); 771 const program = try then(&a, opened, try then(&a, sealed, publish)); 772 a.gc_enabled = true; 773 try a.setGcPolicy(1, 1); 774 var host: Host = .{}; 775 const result = Core.runWithHost(Host, &host, &a, program, 1, 10000); 776 try std.testing.expectEqual(host.opened, host.closed); 777 var iterator = dir.iterate(); 778 var visible: usize = 0; 779 while (try iterator.next(std.testing.io)) |entry| { 780 try std.testing.expectEqualStrings("out", entry.name); // never a private name 781 visible += 1; 782 } 783 try std.testing.expectEqual(host.published, visible); // OOM cannot roll back visibility 784 if (visible != 0) try dir.deleteFile(std.testing.io, "out"); 785 _ = try result; 786 try std.testing.expectEqual(@as(usize, 1), host.published); 787 } 788 test "allocation failures before and after publication clean private names without rollback" { 789 const testing = std.testing; 790 var tmp = testing.tmpDir(.{ .iterate = true }); 791 defer tmp.cleanup(); 792 const directory = try tmp.dir.realPathFileAlloc(testing.io, ".", testing.allocator); 793 defer testing.allocator.free(directory); 794 const destination = try std.fmt.allocPrint(testing.allocator, "{s}/out", .{directory}); 795 defer testing.allocator.free(destination); 796 try testing.checkAllAllocationFailures(testing.allocator, publicationAllocationProbe, .{ directory, destination, tmp.dir }); 797 } 798 799 fn indexedWriteAllocationProbe(allocator: std.mem.Allocator, path: []const u8, dir: std.Io.Dir) !void { 800 defer dir.deleteFile(std.testing.io, "out") catch {}; 801 var a = @import("arena.zig").Arena.init(allocator); 802 defer a.deinit(); 803 const opened = try testOpenProgram(&a, path, null, "file", "open-writer"); 804 const token = try C.ofList(&a, &.{ try C.ofString(&a, "io.v2.resource"), try C.ofNumber(&a, 1), try C.ofNumber(&a, 5) }); 805 const args = try C.ofList(&a, &.{ token, try C.ofNumber(&a, 431), try C.ofString(&a, "6f93378feaa8bf5ab7cefc071175a654c871bfd53f165d7d96795fb2b38483f1"), try a.alloc(.leaf) }); 806 const program = try then(&a, opened, try testRequest(&a, "indexed-tree", "write", args)); 807 a.gc_enabled = true; 808 try a.setGcPolicy(1, 1); 809 var host: Host = .{}; 810 const outcome = Core.runWithHost(Host, &host, &a, program, 1, 10000); 811 try std.testing.expectEqual(host.opened, host.closed); 812 if (host.opened != 0) { 813 const bytes = try dir.readFileAlloc(std.testing.io, "out", std.testing.allocator, .limited(1024)); 814 defer std.testing.allocator.free(bytes); 815 // Preflight never writes; a failure encoding the reply cannot undo the 816 // completed write. There are no allocations inside native emission. 817 try std.testing.expect(bytes.len == 0 or bytes.len == 431); 818 if (bytes.len != 0) try std.testing.expectEqualStrings("6f93378feaa8bf5ab7cefc071175a654c871bfd53f165d7d96795fb2b38483f1", &Indexed.digest(bytes)); 819 if (outcome) |_| try std.testing.expectEqual(@as(usize, 431), bytes.len) else |_| {} 820 } 821 _ = try outcome; 822 try std.testing.expectEqual(@as(usize, 1), host.opened); 823 } 824 test "indexed writer preflight and reply OOM settle leases without rolling back writes" { 825 var tmp = std.testing.tmpDir(.{}); 826 defer tmp.cleanup(); 827 const directory = try tmp.dir.realPathFileAlloc(std.testing.io, ".", std.testing.allocator); 828 defer std.testing.allocator.free(directory); 829 const path = try std.fmt.allocPrint(std.testing.allocator, "{s}/out", .{directory}); 830 defer std.testing.allocator.free(path); 831 try std.testing.checkAllAllocationFailures(std.testing.allocator, indexedWriteAllocationProbe, .{ path, tmp.dir }); 832 } 833 834 fn snapshotAllocationProbe(allocator: std.mem.Allocator, path: []const u8, directory: []const u8, dir: std.Io.Dir) !void { 835 var a = @import("arena.zig").Arena.init(allocator); 836 defer a.deinit(); 837 const opened = try testOpenProgram(&a, path, null, "file", "open-reader"); 838 const token = try C.ofList(&a, &.{ try C.ofString(&a, "io.v2.resource"), try C.ofNumber(&a, 1), try C.ofNumber(&a, 5) }); 839 const args = try C.ofList(&a, &.{ token, try Text.fromUtf8(&a, directory), try C.ofNumber(&a, 431), try C.ofNumber(&a, 431), try C.ofString(&a, "6f93378feaa8bf5ab7cefc071175a654c871bfd53f165d7d96795fb2b38483f1") }); 840 const program = try then(&a, opened, try testRequest(&a, "indexed-tree", "read", args)); 841 a.gc_enabled = true; 842 try a.setGcPolicy(1, 1); 843 var host: Host = .{}; 844 const outcome = Core.runWithHost(Host, &host, &a, program, 1, 10000); 845 try std.testing.expectEqual(host.opened, host.closed); 846 var iterator = dir.iterate(); 847 while (try iterator.next(std.testing.io)) |entry| try std.testing.expectEqualStrings("in", entry.name); 848 _ = try outcome; 849 try std.testing.expectEqual(@as(usize, 2), host.opened); 850 } 851 test "snapshot acquisition decode and reply OOM close every file without private names" { 852 var tmp = std.testing.tmpDir(.{ .iterate = true }); 853 defer tmp.cleanup(); 854 const directory = try tmp.dir.realPathFileAlloc(std.testing.io, ".", std.testing.allocator); 855 defer std.testing.allocator.free(directory); 856 const path = try std.fmt.allocPrint(std.testing.allocator, "{s}/in", .{directory}); 857 defer std.testing.allocator.free(path); 858 var arena = @import("arena.zig").Arena.init(std.testing.allocator); 859 defer arena.deinit(); 860 const bytes = try Indexed.encode(&arena, try arena.alloc(.leaf)); 861 defer std.testing.allocator.free(bytes); 862 try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "in", .data = bytes }); 863 try std.testing.checkAllAllocationFailures(std.testing.allocator, snapshotAllocationProbe, .{ path, directory, tmp.dir }); 864 }