io_driver.zig (69578B)
1 const std = @import("std"); 2 const Arena = @import("arena.zig").Arena; 3 const codecs = @import("codecs.zig"); 4 const source_text = @import("source_text.zig"); 5 const indexed = @import("indexed.zig"); 6 const reduce = @import("reduce.zig"); 7 const tree = @import("tree.zig"); 8 9 const c = @cImport({ 10 // Import declarations, not glibc's fortified C inline wrappers. With the 11 // pinned Zig/glibc, optimized translate-c diagnoses the variadic open/openat 12 // wrapper bodies even though this driver calls uv_fs_open, not libc open. 13 // This is local to header translation: libuv and C callers retain their 14 // normal build hardening, and Zig ReleaseSafe retains runtime checks. 15 @cUndef("_FORTIFY_SOURCE"); 16 @cDefine("_FORTIFY_SOURCE", "0"); 17 @cInclude("uv.h"); 18 }); 19 20 // --------------------------------------------------------------------------- 21 // Action tag constants (must match lib/io.tri and IODriver.hs) 22 // --------------------------------------------------------------------------- 23 24 pub const ActionTag = enum(u8) { 25 pure = 0, 26 bind = 1, 27 putStr = 10, 28 getLine = 11, 29 readFile = 20, 30 readSourceFile = 37, 31 encodeSourceText = 38, 32 readIndexedTree = 50, 33 hashIndexedTree = 51, 34 writeIndexedTreeIfAbsent = 52, 35 writeFile = 21, 36 writeBytes = 22, 37 renameFile = 24, 38 createDirectory = 25, 39 fileExists = 27, 40 sha256Hex = 28, 41 writeBytesIfAbsent = 32, 42 atomicWriteBytes = 33, 43 ask = 30, 44 local = 31, 45 get = 40, 46 put = 41, 47 fork = 60, 48 await = 61, 49 yield = 62, 50 sleep = 63, 51 }; 52 53 const FileWrite = struct { path: u32, contents: u32 }; 54 55 pub const Action = union(ActionTag) { 56 pure: u32, 57 bind: struct { left: u32, k: u32 }, 58 putStr: u32, 59 getLine, 60 readFile: u32, 61 readSourceFile: u32, 62 encodeSourceText: u32, 63 readIndexedTree: struct { path: u32, digest: u32 }, 64 hashIndexedTree: u32, 65 writeIndexedTreeIfAbsent: struct { path: u32, digest: u32, tree: u32 }, 66 writeFile: FileWrite, 67 writeBytes: FileWrite, 68 renameFile: struct { old: u32, new: u32 }, 69 createDirectory: u32, 70 fileExists: u32, 71 sha256Hex: u32, 72 writeBytesIfAbsent: FileWrite, 73 atomicWriteBytes: FileWrite, 74 ask, 75 local: struct { f: u32, action: u32 }, 76 get, 77 put: u32, 78 fork: u32, 79 await: u32, 80 yield, 81 sleep: u32, 82 }; 83 84 // --------------------------------------------------------------------------- 85 // Error codes (must match IODriver.hs) 86 // --------------------------------------------------------------------------- 87 88 const ERR_DOES_NOT_EXIST: u64 = 1; 89 const ERR_PERMISSION: u64 = 2; 90 const ERR_ALREADY_EXISTS: u64 = 3; 91 const ERR_IO_OTHER: u64 = 4; 92 const ERR_POLICY_DENY: u64 = 20; 93 const ERR_INVALID_ACTION: u64 = 40; 94 const ERR_INVALID_STRING: u64 = 41; 95 96 // --------------------------------------------------------------------------- 97 // Permissions 98 // --------------------------------------------------------------------------- 99 100 pub const IOPerms = struct { 101 allow_read_all: bool = false, 102 allow_write_all: bool = false, 103 }; 104 105 // --------------------------------------------------------------------------- 106 // IO sentinel detection 107 // --------------------------------------------------------------------------- 108 109 pub fn isIOSentinel(arena: *Arena, root: u32) !?u32 { 110 const node = arena.get(try reduce.reduce(root, arena, 1_000_000)).*; 111 if (node != .fork) return null; 112 113 const sentinel = node.fork.left; 114 const rest = node.fork.right; 115 116 const sentinel_str = try codecs.toString(arena, sentinel); 117 defer { 118 if (sentinel_str) |s| { 119 arena.allocator.free(s); 120 } 121 } 122 if (sentinel_str == null) return null; 123 if (!std.mem.eql(u8, sentinel_str.?, "tricuIO")) return null; 124 125 const rest_node = arena.get(try reduce.reduce(rest, arena, 1_000_000)).*; 126 if (rest_node != .fork) return null; 127 128 const version_num = try codecs.toNumber(arena, rest_node.fork.left); 129 if (version_num == null or version_num.? != 1) return null; 130 131 return rest_node.fork.right; 132 } 133 134 // --------------------------------------------------------------------------- 135 // Action decoding 136 // --------------------------------------------------------------------------- 137 138 pub fn decodeAction(arena: *Arena, root: u32) !?Action { 139 const node = arena.get(root).*; 140 if (node != .fork) return null; 141 142 const tag_num = try codecs.toNumber(arena, node.fork.left); 143 if (tag_num == null) return null; 144 145 const tag: ActionTag = switch (tag_num.?) { 146 0 => .pure, 147 1 => .bind, 148 10 => .putStr, 149 11 => .getLine, 150 20 => .readFile, 151 37 => .readSourceFile, 152 38 => .encodeSourceText, 153 50 => .readIndexedTree, 154 51 => .hashIndexedTree, 155 52 => .writeIndexedTreeIfAbsent, 156 21 => .writeFile, 157 22 => .writeBytes, 158 24 => .renameFile, 159 25 => .createDirectory, 160 27 => .fileExists, 161 28 => .sha256Hex, 162 32 => .writeBytesIfAbsent, 163 33 => .atomicWriteBytes, 164 30 => .ask, 165 31 => .local, 166 40 => .get, 167 41 => .put, 168 60 => .fork, 169 61 => .await, 170 62 => .yield, 171 63 => .sleep, 172 else => return null, 173 }; 174 175 const payload = node.fork.right; 176 177 return switch (tag) { 178 .pure => Action{ .pure = payload }, 179 .bind => blk: { 180 const payload_node = arena.get(try reduce.reduce(payload, arena, 1_000_000)); 181 if (payload_node.* != .fork) return null; 182 break :blk Action{ .bind = .{ .left = payload_node.fork.left, .k = payload_node.fork.right } }; 183 }, 184 .putStr => Action{ .putStr = payload }, 185 .getLine => Action.getLine, 186 .readFile => Action{ .readFile = payload }, 187 .readSourceFile => Action{ .readSourceFile = payload }, 188 .encodeSourceText => Action{ .encodeSourceText = payload }, 189 .hashIndexedTree => Action{ .hashIndexedTree = payload }, 190 .readIndexedTree, .writeIndexedTreeIfAbsent => blk: { 191 const pair = arena.get(try reduce.reduce(payload, arena, 1_000_000)).*; 192 if (pair != .fork) return null; 193 if (tag == .readIndexedTree) break :blk .{ .readIndexedTree = .{ .path = pair.fork.left, .digest = pair.fork.right } }; 194 const rest = arena.get(try reduce.reduce(pair.fork.right, arena, 1_000_000)).*; 195 if (rest != .fork) return null; 196 break :blk .{ .writeIndexedTreeIfAbsent = .{ .path = pair.fork.left, .digest = rest.fork.left, .tree = rest.fork.right } }; 197 }, 198 .writeFile, .writeBytes, .writeBytesIfAbsent, .atomicWriteBytes, .renameFile => blk: { 199 const pair = arena.get(try reduce.reduce(payload, arena, 1_000_000)); 200 if (pair.* != .fork) return null; 201 break :blk switch (tag) { 202 .writeFile => Action{ .writeFile = .{ .path = pair.fork.left, .contents = pair.fork.right } }, 203 .writeBytes => Action{ .writeBytes = .{ .path = pair.fork.left, .contents = pair.fork.right } }, 204 .writeBytesIfAbsent => Action{ .writeBytesIfAbsent = .{ .path = pair.fork.left, .contents = pair.fork.right } }, 205 .atomicWriteBytes => Action{ .atomicWriteBytes = .{ .path = pair.fork.left, .contents = pair.fork.right } }, 206 .renameFile => Action{ .renameFile = .{ .old = pair.fork.left, .new = pair.fork.right } }, 207 else => unreachable, 208 }; 209 }, 210 .createDirectory => Action{ .createDirectory = payload }, 211 .fileExists => Action{ .fileExists = payload }, 212 .sha256Hex => Action{ .sha256Hex = payload }, 213 .ask => Action.ask, 214 .local => blk: { 215 const payload_node = arena.get(try reduce.reduce(payload, arena, 1_000_000)); 216 if (payload_node.* != .fork) return null; 217 break :blk Action{ .local = .{ .f = payload_node.fork.left, .action = payload_node.fork.right } }; 218 }, 219 .get => Action.get, 220 .put => Action{ .put = payload }, 221 .fork => Action{ .fork = payload }, 222 .await => Action{ .await = payload }, 223 .yield => Action.yield, 224 .sleep => Action{ .sleep = payload }, 225 }; 226 } 227 228 // --------------------------------------------------------------------------- 229 // Response tree constructors 230 // --------------------------------------------------------------------------- 231 232 pub fn makePure(arena: *Arena, val: u32) !u32 { 233 const tag = try codecs.ofNumber(arena, 0); 234 return try arena.alloc(.{ .fork = .{ .left = tag, .right = val } }); 235 } 236 237 pub fn makeOkResult(arena: *Arena, val: u32) !u32 { 238 const ok_tag = try arena.alloc(.{ .stem = .{ .child = try arena.alloc(.leaf) } }); 239 const val_pair = try arena.alloc(.{ .fork = .{ .left = val, .right = try arena.alloc(.leaf) } }); 240 return try arena.alloc(.{ .fork = .{ .left = ok_tag, .right = val_pair } }); 241 } 242 243 pub fn makeErrString(arena: *Arena, message: []const u8) !u32 { 244 return makeErrValue(arena, try codecs.ofString(arena, message)); 245 } 246 247 fn makeErrValue(arena: *Arena, text: u32) !u32 { 248 const value = try arena.alloc(.{ .fork = .{ .left = text, .right = try arena.alloc(.leaf) } }); 249 return try arena.alloc(.{ .fork = .{ .left = try arena.alloc(.leaf), .right = value } }); 250 } 251 252 fn sourceReadError(arena: *Arena, path: []const u8, reason: []const u8) !u32 { 253 const message = try std.fmt.allocPrint(arena.allocator, "{s}: {s}", .{ path, reason }); 254 defer arena.allocator.free(message); 255 return makeErrValue(arena, try source_text.fromUtf8(arena, message)); 256 } 257 258 fn sourceReadResult(arena: *Arena, path: []const u8, bytes: []const u8) !u32 { 259 const scalars = source_text.fromUtf8(arena, bytes) catch |err| switch (err) { 260 error.InvalidUtf8 => return sourceReadError(arena, path, "invalid UTF-8 source"), 261 else => return err, 262 }; 263 if (std.mem.startsWith(u8, bytes, "\xef\xbb\xbf")) 264 return sourceReadError(arena, path, "UTF-8 source BOM is not permitted"); 265 const raw = try codecs.ofBytes(arena, bytes); 266 const pair = try arena.alloc(.{ .fork = .{ .left = raw, .right = scalars } }); 267 return makeOkResult(arena, pair); 268 } 269 270 pub fn makeErrResult(arena: *Arena, code: u64) !u32 { 271 // The portable IO protocol carries text errors, matching the Haskell 272 // driver; numeric values remain internal filesystem status codes. 273 return makeErrString(arena, fsErrorMessage(code)); 274 } 275 276 // A null callback makes libuv filesystem operations synchronous. Each 277 // request is cleaned up; temp files live in the destination directory so 278 // both link (no replace) and rename (replace) are atomic on that filesystem. 279 const FsWriteResult = union(enum) { written: bool, failure: u64 }; 280 281 const FsReadResult = union(enum) { bytes: []u8, failure: u64 }; 282 283 fn readFs(loop: *c.uv_loop_t, gpa: std.mem.Allocator, path: [:0]const u8) !FsReadResult { 284 var req: c.uv_fs_t = undefined; 285 const fd = c.uv_fs_open(loop, &req, path.ptr, c.O_RDONLY, 0, null); 286 c.uv_fs_req_cleanup(&req); 287 if (fd < 0) return .{ .failure = mapUvErr(fd) }; 288 defer { 289 _ = c.uv_fs_close(loop, &req, fd, null); 290 c.uv_fs_req_cleanup(&req); 291 } 292 var bytes: std.ArrayList(u8) = .empty; 293 defer bytes.deinit(gpa); 294 var buffer: [4096]u8 = undefined; 295 var uv_buffer = c.uv_buf_init(&buffer, buffer.len); 296 while (true) { 297 const count = c.uv_fs_read(loop, &req, fd, &uv_buffer, 1, -1, null); 298 c.uv_fs_req_cleanup(&req); 299 if (count < 0) return .{ .failure = mapUvErr(count) }; 300 if (count == 0) return .{ .bytes = try bytes.toOwnedSlice(gpa) }; 301 try bytes.appendSlice(gpa, buffer[0..@intCast(count)]); 302 } 303 } 304 305 fn uvFileError(rc: c_int) u64 { 306 return mapUvErr(rc); 307 } 308 309 fn syncFsPath(loop: *c.uv_loop_t, path: [*:0]const u8, directory: bool) ?u64 { 310 var open_req: c.uv_fs_t = undefined; 311 const fd = c.uv_fs_open(loop, &open_req, path, c.O_RDONLY | (if (directory) @as(c_int, c.O_DIRECTORY) else 0), 0, null); 312 c.uv_fs_req_cleanup(&open_req); 313 if (fd < 0) return uvFileError(fd); 314 var sync_req: c.uv_fs_t = undefined; 315 const synced = c.uv_fs_fsync(loop, &sync_req, @intCast(fd), null); 316 c.uv_fs_req_cleanup(&sync_req); 317 var close_req: c.uv_fs_t = undefined; 318 const closed = c.uv_fs_close(loop, &close_req, @intCast(fd), null); 319 c.uv_fs_req_cleanup(&close_req); 320 if (synced < 0) return uvFileError(synced); 321 if (closed < 0) return uvFileError(closed); 322 return null; 323 } 324 325 fn syncFsParent(loop: *c.uv_loop_t, gpa: std.mem.Allocator, path: []const u8) !?u64 { 326 const parent = std.fs.path.dirname(path) orelse "."; 327 const parent_z = try gpa.dupeZ(u8, parent); 328 defer gpa.free(parent_z); 329 return syncFsPath(loop, parent_z, true); 330 } 331 332 fn writeAtomicFs(loop: *c.uv_loop_t, gpa: std.mem.Allocator, path: [:0]const u8, contents: []const u8, exclusive: bool) !FsWriteResult { 333 const template = try std.fmt.allocPrintSentinel(gpa, "{s}.tmp.XXXXXX", .{path}, 0); 334 defer gpa.free(template); 335 var req: c.uv_fs_t = undefined; 336 const fd = c.uv_fs_mkstemp(loop, &req, template.ptr, null); 337 if (fd < 0) { 338 c.uv_fs_req_cleanup(&req); 339 return .{ .failure = uvFileError(fd) }; 340 } 341 const tmp = gpa.dupeZ(u8, std.mem.span(req.path)) catch |err| { 342 var close_req: c.uv_fs_t = undefined; 343 _ = c.uv_fs_close(loop, &close_req, fd, null); 344 c.uv_fs_req_cleanup(&close_req); 345 var unlink_req: c.uv_fs_t = undefined; 346 _ = c.uv_fs_unlink(loop, &unlink_req, req.path, null); 347 c.uv_fs_req_cleanup(&unlink_req); 348 c.uv_fs_req_cleanup(&req); 349 return err; 350 }; 351 c.uv_fs_req_cleanup(&req); 352 defer gpa.free(tmp); 353 defer { 354 var unlink_req: c.uv_fs_t = undefined; 355 _ = c.uv_fs_unlink(loop, &unlink_req, tmp.ptr, null); 356 c.uv_fs_req_cleanup(&unlink_req); 357 } 358 var offset: usize = 0; 359 var failure: ?u64 = null; 360 while (offset < contents.len) { 361 const remain = contents[offset..]; 362 var buf = c.uv_buf_init(@ptrCast(@constCast(remain.ptr)), @intCast(@min(remain.len, std.math.maxInt(c_uint)))); 363 var write_req: c.uv_fs_t = undefined; 364 const n = c.uv_fs_write(loop, &write_req, fd, &buf, 1, @intCast(offset), null); 365 c.uv_fs_req_cleanup(&write_req); 366 if (n <= 0) { 367 failure = if (n == 0) ERR_IO_OTHER else uvFileError(n); 368 break; 369 } 370 offset += @intCast(n); 371 } 372 var close_req: c.uv_fs_t = undefined; 373 const closed = c.uv_fs_close(loop, &close_req, fd, null); 374 c.uv_fs_req_cleanup(&close_req); 375 if (failure) |err| return .{ .failure = err }; 376 if (closed < 0) return .{ .failure = uvFileError(closed) }; 377 if (syncFsPath(loop, tmp.ptr, false)) |err| return .{ .failure = err }; 378 var publish_req: c.uv_fs_t = undefined; 379 const published = if (exclusive) 380 c.uv_fs_link(loop, &publish_req, tmp.ptr, path.ptr, null) 381 else 382 c.uv_fs_rename(loop, &publish_req, tmp.ptr, path.ptr, null); 383 c.uv_fs_req_cleanup(&publish_req); 384 if (published == c.UV_EEXIST and exclusive) { 385 if (syncFsPath(loop, path.ptr, false)) |err| return .{ .failure = err }; 386 if (try syncFsParent(loop, gpa, path)) |err| return .{ .failure = err }; 387 return .{ .written = false }; 388 } 389 if (published < 0) return .{ .failure = uvFileError(published) }; 390 if (try syncFsParent(loop, gpa, path)) |err| return .{ .failure = err }; 391 return .{ .written = true }; 392 } 393 394 fn fsErrorMessage(err: u64) []const u8 { 395 return switch (err) { 396 ERR_DOES_NOT_EXIST => "does not exist", 397 ERR_PERMISSION, ERR_POLICY_DENY => "permission denied", 398 ERR_ALREADY_EXISTS => "already exists", 399 ERR_INVALID_STRING => "invalid string", 400 ERR_INVALID_ACTION => "invalid action", 401 else => "io error", 402 }; 403 } 404 405 // --------------------------------------------------------------------------- 406 // Frame stack and runtime 407 // --------------------------------------------------------------------------- 408 409 const Frame = union(enum) { 410 bind: u32, // continuation k 411 local: u32, // old env 412 }; 413 414 const Runtime = struct { 415 env: u32, 416 state: u32, 417 }; 418 419 // --------------------------------------------------------------------------- 420 // Task 421 // --------------------------------------------------------------------------- 422 423 const Task = struct { 424 id: u64, 425 parent: ?*Task, 426 frames: std.ArrayList(Frame), 427 runtime: Runtime, 428 current: u32, 429 status: enum { runnable, blocked, completed }, 430 result: ?u32, 431 waiting_for: ?u64, 432 433 fn init(gpa: std.mem.Allocator, id: u64, parent: ?*Task, env: u32, state: u32, current: u32) !*Task { 434 const task = try gpa.create(Task); 435 task.* = .{ 436 .id = id, 437 .parent = parent, 438 .frames = std.ArrayList(Frame).empty, 439 .runtime = .{ .env = env, .state = state }, 440 .current = current, 441 .status = .runnable, 442 .result = null, 443 .waiting_for = null, 444 }; 445 return task; 446 } 447 448 fn deinit(self: *Task, gpa: std.mem.Allocator) void { 449 self.frames.deinit(gpa); 450 gpa.destroy(self); 451 } 452 453 // finishValue processes a value through the frame stack. 454 // Returns true if the task has completed (no more frames). 455 fn finishValue(self: *Task, arena: *Arena, value: u32) !bool { 456 if (self.frames.pop()) |frame| { 457 switch (frame) { 458 .bind => |k| { 459 self.current = try arena.alloc(.{ .app = .{ .func = k, .arg = value } }); 460 return false; 461 }, 462 .local => |old_env| { 463 self.runtime.env = old_env; 464 self.current = try makePure(arena, value); 465 return false; 466 }, 467 } 468 } else { 469 self.current = value; 470 return true; 471 } 472 } 473 }; 474 475 // --------------------------------------------------------------------------- 476 // Scheduler 477 // --------------------------------------------------------------------------- 478 479 const Scheduler = struct { 480 gpa: std.mem.Allocator, 481 loop: *c.uv_loop_t, 482 arena: *Arena, 483 tasks: std.ArrayList(*Task), 484 runnable: std.ArrayList(*Task), 485 next_id: u64, 486 perms: IOPerms, 487 488 fn init(gpa: std.mem.Allocator, loop: *c.uv_loop_t, arena: *Arena, perms: IOPerms) !Scheduler { 489 const sched = Scheduler{ 490 .gpa = gpa, 491 .loop = loop, 492 .arena = arena, 493 .tasks = std.ArrayList(*Task).empty, 494 .runnable = std.ArrayList(*Task).empty, 495 .next_id = 1, 496 .perms = perms, 497 }; 498 return sched; 499 } 500 501 fn deinit(self: *Scheduler) void { 502 for (self.tasks.items) |task| { 503 task.deinit(self.gpa); 504 } 505 self.tasks.deinit(self.gpa); 506 self.runnable.deinit(self.gpa); 507 } 508 509 fn createTask(self: *Scheduler, parent: ?*Task, env: u32, state: u32, current: u32) !*Task { 510 const id = self.next_id; 511 self.next_id += 1; 512 const task = try Task.init(self.gpa, id, parent, env, state, current); 513 errdefer task.deinit(self.gpa); 514 try self.tasks.append(self.gpa, task); 515 return task; 516 } 517 518 fn run(self: *Scheduler) !void { 519 while (true) { 520 if (self.runnable.items.len > 0) { 521 const task = self.runnable.orderedRemove(0); 522 try self.stepTask(task); 523 } else if (self.hasPendingHandles()) { 524 _ = c.uv_run(self.loop, c.UV_RUN_ONCE); 525 } else { 526 break; 527 } 528 } 529 } 530 531 fn hasPendingHandles(self: *Scheduler) bool { 532 return c.uv_loop_alive(self.loop) != 0; 533 } 534 535 fn completeTask(self: *Scheduler, task: *Task) !void { 536 task.status = .completed; 537 task.result = task.current; 538 // Unblock any tasks waiting for this one 539 for (self.tasks.items) |t| { 540 if (t.status == .blocked and t.waiting_for == task.id) { 541 t.status = .runnable; 542 t.waiting_for = null; 543 t.current = try makePure(self.arena, task.result.?); 544 try self.runnable.append(self.gpa, t); 545 } 546 } 547 } 548 549 fn completeIO(self: *Scheduler, task: *Task, result: u32) !void { 550 if (try task.finishValue(self.arena, result)) { 551 try self.completeTask(task); 552 } else { 553 try self.runnable.append(self.gpa, task); 554 } 555 } 556 557 fn fsReply(self: *Scheduler, task: *Task, err: u64) !void { 558 try self.completeIO(task, try makeErrString(self.arena, fsErrorMessage(err))); 559 } 560 561 // Like the native CAS operations, source reads use synchronous libuv calls. 562 // This avoids entering callback/reducer code while local trees are unrooted; 563 // allocation and filesystem failures propagate normally to the host. 564 // A source read occupies one scheduler turn (it is not a streaming action). 565 fn readSource(self: *Scheduler, task: *Task, path_tree: u32) !void { 566 const path = source_text.toUtf8(self.arena, path_tree) catch |err| switch (err) { 567 error.InvalidScalarList => return self.fsReply(task, ERR_INVALID_STRING), 568 else => return err, 569 }; 570 defer self.arena.allocator.free(path); 571 if (!self.perms.allow_read_all) return self.fsReply(task, ERR_POLICY_DENY); 572 if (std.mem.indexOfScalar(u8, path, 0) != null) return self.fsReply(task, ERR_INVALID_STRING); 573 const path_z = try self.gpa.dupeZ(u8, path); 574 defer self.gpa.free(path_z); 575 const read = try readFs(self.loop, self.gpa, path_z); 576 switch (read) { 577 .failure => |code| try self.fsReply(task, code), 578 .bytes => |bytes| { 579 defer self.gpa.free(bytes); 580 try self.completeIO(task, try sourceReadResult(self.arena, path, bytes)); 581 }, 582 } 583 } 584 585 // IO protocol adapters: codec and filesystem helpers know nothing about 586 // compiler manifests, cache aliases, path selection or publication order. 587 fn indexedIO(self: *Scheduler, task: *Task, path_tree: u32, expected_tree: u32, value: ?u32) !void { 588 const path = source_text.toUtf8(self.arena, path_tree) catch |err| switch (err) { 589 error.InvalidScalarList => return self.fsReply(task, ERR_INVALID_STRING), 590 else => return err, 591 }; 592 defer self.arena.allocator.free(path); 593 if (std.mem.indexOfScalar(u8, path, 0) != null) return self.fsReply(task, ERR_INVALID_STRING); 594 const expected = source_text.toUtf8(self.arena, expected_tree) catch |err| switch (err) { 595 error.InvalidScalarList => return self.completeIO(task, try makeErrString(self.arena, "invalid CAS digest")), 596 else => return err, 597 }; 598 defer self.arena.allocator.free(expected); 599 if (expected.len != 64) return self.completeIO(task, try makeErrString(self.arena, "invalid CAS digest")); 600 for (expected) |ch| if (!(ch >= '0' and ch <= '9' or ch >= 'a' and ch <= 'f')) 601 return self.completeIO(task, try makeErrString(self.arena, "invalid CAS digest")); 602 if (!self.perms.allow_read_all or (value != null and !self.perms.allow_write_all)) return self.fsReply(task, ERR_POLICY_DENY); 603 const path_z = try self.gpa.dupeZ(u8, path); 604 defer self.gpa.free(path_z); 605 var encoded: ?[]u8 = null; 606 defer if (encoded) |bytes| self.arena.allocator.free(bytes); 607 var fresh = false; 608 if (value) |tree_value| { 609 encoded = try indexed.encode(self.arena, tree_value); 610 if (!std.mem.eql(u8, &indexed.digest(encoded.?), expected)) 611 return self.completeIO(task, try makeErrString(self.arena, "CAS digest mismatch")); 612 switch (try writeAtomicFs(self.loop, self.gpa, path_z, encoded.?, true)) { 613 .failure => |code| return self.fsReply(task, code), 614 .written => |written| fresh = written, 615 } 616 } 617 // Authenticate before decode; for publication compare the actual winner 618 // byte-for-byte, including after an EEXIST race. Never overwrite it. 619 switch (try readFs(self.loop, self.gpa, path_z)) { 620 .failure => |code| return self.fsReply(task, code), 621 .bytes => |bytes| { 622 defer self.gpa.free(bytes); 623 const result = if (encoded) |ours| blk: { 624 if (!std.mem.eql(u8, bytes, ours)) return self.completeIO(task, try makeErrString(self.arena, "CAS object exists with different bytes")); 625 break :blk if (fresh) try self.arena.alloc(.{ .stem = .{ .child = try self.arena.alloc(.leaf) } }) else try self.arena.alloc(.leaf); 626 } else blk: { 627 if (!std.mem.eql(u8, &indexed.digest(bytes), expected)) return self.completeIO(task, try makeErrString(self.arena, "CAS digest mismatch")); 628 break :blk indexed.load(self.arena, bytes, "root", true) catch |err| switch (err) { 629 error.OutOfMemory => return err, 630 else => return self.completeIO(task, try makeErrString(self.arena, "invalid indexed tree")), 631 }; 632 }; 633 try self.completeIO(task, try makeOkResult(self.arena, result)); 634 }, 635 } 636 } 637 638 fn stepTask(self: *Scheduler, task: *Task) !void { 639 // Snapshot every scheduler-owned node for this turn. A turn may force 640 // repeatedly; frames popped during it remain rooted until it completes. 641 // Callbacks only allocate/link trees and never enter the reducer. 642 const base = self.arena.roots.items.len; 643 defer self.arena.roots.shrinkRetainingCapacity(base); 644 for (self.tasks.items) |t| { 645 try self.arena.roots.appendSlice(self.gpa, &.{ t.current, t.runtime.env, t.runtime.state }); 646 if (t.result) |result| try self.arena.roots.append(self.gpa, result); 647 for (t.frames.items) |frame| switch (frame) { 648 .bind => |k| try self.arena.roots.append(self.gpa, k), 649 .local => |env| try self.arena.roots.append(self.gpa, env), 650 }; 651 } 652 const reduced = try reduce.reduce(task.current, self.arena, std.math.maxInt(u64)); 653 654 const decoded = try decodeAction(self.arena, reduced); 655 if (decoded == null) { 656 // Not a recognized action ā if no frames, it's the final result. 657 // Otherwise treat as invalid. 658 if (task.frames.items.len == 0) { 659 task.current = reduced; 660 try self.completeTask(task); 661 return; 662 } 663 const invalid = self.arena.get(reduced).*; 664 const tag = if (invalid == .fork) try codecs.toNumber(self.arena, invalid.fork.left) else null; 665 var diagnostic: [96]u8 = undefined; 666 const message = if (tag) |n| try std.fmt.bufPrint(&diagnostic, "invalid or unsupported IO action tag {d}", .{n}) else "invalid action"; 667 const err = try makeErrString(self.arena, message); 668 if (try task.finishValue(self.arena, err)) { 669 try self.completeTask(task); 670 } else { 671 try self.runnable.append(self.gpa, task); 672 } 673 return; 674 } 675 676 const action = decoded.?; 677 switch (action) { 678 .pure => |val| { 679 if (try task.finishValue(self.arena, val)) { 680 try self.completeTask(task); 681 } else { 682 try self.runnable.append(self.gpa, task); 683 } 684 }, 685 686 .bind => |b| { 687 try task.frames.append(self.gpa, .{ .bind = b.k }); 688 task.current = b.left; 689 try self.runnable.append(self.gpa, task); 690 }, 691 692 .putStr => |str_tree| { 693 const str = try codecs.toString(self.arena, str_tree) orelse { 694 const err = try makeErrResult(self.arena, ERR_INVALID_STRING); 695 if (try task.finishValue(self.arena, err)) { 696 try self.completeTask(task); 697 } else { 698 try self.runnable.append(self.gpa, task); 699 } 700 return; 701 }; 702 defer self.gpa.free(str); 703 var written: usize = 0; 704 while (written < str.len) { 705 const n = std.c.write(1, str[written..].ptr, str.len - written); 706 if (n <= 0) return error.WriteFailed; 707 written += @intCast(n); 708 } 709 const leaf = try self.arena.alloc(.leaf); 710 if (try task.finishValue(self.arena, leaf)) { 711 try self.completeTask(task); 712 } else { 713 try self.runnable.append(self.gpa, task); 714 } 715 }, 716 717 .getLine => { 718 var buf: [4096]u8 = undefined; 719 var len: usize = 0; 720 while (len < buf.len) { 721 const n = std.c.read(0, buf[len..].ptr, 1); 722 if (n <= 0) break; 723 if (buf[len] == '\n') break; 724 len += 1; 725 } 726 const line = buf[0..len]; 727 const str_tree = try codecs.ofString(self.arena, line); 728 if (try task.finishValue(self.arena, str_tree)) { 729 try self.completeTask(task); 730 } else { 731 try self.runnable.append(self.gpa, task); 732 } 733 }, 734 735 .readSourceFile => |path_tree| try self.readSource(task, path_tree), 736 .readIndexedTree => |read| try self.indexedIO(task, read.path, read.digest, null), 737 .writeIndexedTreeIfAbsent => |write| try self.indexedIO(task, write.path, write.digest, write.tree), 738 .hashIndexedTree => |value| { 739 const bytes = try indexed.encode(self.arena, value); 740 defer self.arena.allocator.free(bytes); 741 const hex = indexed.digest(bytes); 742 try self.completeIO(task, try makeOkResult(self.arena, try codecs.ofString(self.arena, &hex))); 743 }, 744 745 .encodeSourceText => |text| { 746 const bytes = source_text.toUtf8(self.arena, text) catch |err| switch (err) { 747 error.InvalidScalarList => { 748 try self.completeIO(task, try makeErrString(self.arena, "invalid source scalar list")); 749 return; 750 }, 751 else => return err, 752 }; 753 defer self.arena.allocator.free(bytes); 754 try self.completeIO(task, try makeOkResult(self.arena, try codecs.ofBytes(self.arena, bytes))); 755 }, 756 757 .readFile => |path_tree| { 758 const path = try codecs.toString(self.arena, path_tree) orelse { 759 const err = try makeErrResult(self.arena, ERR_INVALID_STRING); 760 if (try task.finishValue(self.arena, err)) { 761 try self.completeTask(task); 762 } else { 763 try self.runnable.append(self.gpa, task); 764 } 765 return; 766 }; 767 768 if (!self.perms.allow_read_all) { 769 self.arena.allocator.free(path); 770 const err = try makeErrResult(self.arena, ERR_POLICY_DENY); 771 if (try task.finishValue(self.arena, err)) { 772 try self.completeTask(task); 773 } else { 774 try self.runnable.append(self.gpa, task); 775 } 776 return; 777 } 778 779 const ctx = try self.gpa.create(FileReadCtx); 780 ctx.* = .{ 781 .scheduler = self, 782 .task = task, 783 .arena = self.arena, 784 .gpa = self.gpa, 785 .fd = -1, 786 .buf = std.ArrayList(u8).empty, 787 .path = try self.gpa.dupeZ(u8, path), 788 .req = undefined, 789 .read_buf = null, 790 }; 791 self.arena.allocator.free(path); 792 ctx.req.data = ctx; 793 _ = c.uv_fs_open(self.loop, &ctx.req, ctx.path.ptr, c.O_RDONLY, 0, file_open_cb); 794 }, 795 796 .writeFile => |wf| { 797 const path = try codecs.toString(self.arena, wf.path) orelse { 798 const err = try makeErrResult(self.arena, ERR_INVALID_STRING); 799 if (try task.finishValue(self.arena, err)) { 800 try self.completeTask(task); 801 } else { 802 try self.runnable.append(self.gpa, task); 803 } 804 return; 805 }; 806 807 const contents = try codecs.toString(self.arena, wf.contents) orelse { 808 self.arena.allocator.free(path); 809 const err = try makeErrResult(self.arena, ERR_INVALID_STRING); 810 if (try task.finishValue(self.arena, err)) { 811 try self.completeTask(task); 812 } else { 813 try self.runnable.append(self.gpa, task); 814 } 815 return; 816 }; 817 818 if (!self.perms.allow_write_all) { 819 self.arena.allocator.free(path); 820 self.arena.allocator.free(contents); 821 const err = try makeErrResult(self.arena, ERR_POLICY_DENY); 822 if (try task.finishValue(self.arena, err)) { 823 try self.completeTask(task); 824 } else { 825 try self.runnable.append(self.gpa, task); 826 } 827 return; 828 } 829 830 const ctx = try self.gpa.create(FileWriteCtx); 831 ctx.* = .{ 832 .scheduler = self, 833 .task = task, 834 .arena = self.arena, 835 .gpa = self.gpa, 836 .fd = -1, 837 .path = try self.gpa.dupeZ(u8, path), 838 .contents = contents, 839 .offset = 0, 840 .req = undefined, 841 }; 842 self.arena.allocator.free(path); 843 ctx.req.data = ctx; 844 const flags = c.O_WRONLY | c.O_CREAT | c.O_TRUNC; 845 _ = c.uv_fs_open(self.loop, &ctx.req, ctx.path.ptr, flags, 0o644, file_write_open_cb); 846 }, 847 848 .writeBytes, .writeBytesIfAbsent, .atomicWriteBytes => |write| { 849 const path = try codecs.toString(self.arena, write.path) orelse { 850 try self.fsReply(task, ERR_INVALID_STRING); 851 return; 852 }; 853 defer self.arena.allocator.free(path); 854 const contents = try codecs.toBytes(self.arena, write.contents) orelse { 855 try self.completeIO(task, try makeErrString(self.arena, "invalid bytes")); 856 return; 857 }; 858 defer self.arena.allocator.free(contents); 859 if (!self.perms.allow_write_all) { 860 try self.fsReply(task, ERR_POLICY_DENY); 861 return; 862 } 863 const path_z = try self.gpa.dupeZ(u8, path); 864 defer self.gpa.free(path_z); 865 const exclusive = action == .writeBytesIfAbsent; 866 const published = try writeAtomicFs(self.loop, self.gpa, path_z, contents, exclusive); 867 switch (published) { 868 .failure => |code| try self.fsReply(task, code), 869 .written => |fresh| { 870 const value = if (action == .writeBytes) try self.arena.alloc(.leaf) else if (fresh) try self.arena.alloc(.{ .stem = .{ .child = try self.arena.alloc(.leaf) } }) else try self.arena.alloc(.leaf); 871 try self.completeIO(task, try makeOkResult(self.arena, value)); 872 }, 873 } 874 }, 875 876 .createDirectory, .fileExists => |path_tree| { 877 const path = try codecs.toString(self.arena, path_tree) orelse { 878 try self.fsReply(task, ERR_INVALID_STRING); 879 return; 880 }; 881 defer self.arena.allocator.free(path); 882 const is_create = action == .createDirectory; 883 if (is_create and !self.perms.allow_write_all or 884 !is_create and !self.perms.allow_read_all) 885 { 886 try self.fsReply(task, ERR_POLICY_DENY); 887 return; 888 } 889 const path_z = try self.gpa.dupeZ(u8, path); 890 defer self.gpa.free(path_z); 891 var req: c.uv_fs_t = undefined; 892 const rc = if (is_create) c.uv_fs_mkdir(self.loop, &req, path_z.ptr, 0o755, null) else c.uv_fs_stat(self.loop, &req, path_z.ptr, null); 893 c.uv_fs_req_cleanup(&req); 894 if (rc < 0 and (is_create and rc != c.UV_EEXIST or 895 !is_create and rc != c.UV_ENOENT)) 896 { 897 try self.fsReply(task, uvFileError(rc)); 898 return; 899 } 900 if (is_create and rc == c.UV_EEXIST) { 901 var stat_req: c.uv_fs_t = undefined; 902 const stat = c.uv_fs_stat(self.loop, &stat_req, path_z.ptr, null); 903 const is_dir = stat >= 0 and stat_req.statbuf.st_mode & c.S_IFMT == c.S_IFDIR; 904 c.uv_fs_req_cleanup(&stat_req); 905 if (!is_dir) { 906 try self.fsReply(task, ERR_ALREADY_EXISTS); 907 return; 908 } 909 } 910 if (is_create) { 911 if (syncFsPath(self.loop, path_z.ptr, true)) |err| { 912 try self.fsReply(task, err); 913 return; 914 } 915 if (try syncFsParent(self.loop, self.gpa, path_z)) |err| { 916 try self.fsReply(task, err); 917 return; 918 } 919 } 920 const value = if (is_create) try self.arena.alloc(.leaf) else if (rc == 0) try self.arena.alloc(.{ .stem = .{ .child = try self.arena.alloc(.leaf) } }) else try self.arena.alloc(.leaf); 921 try self.completeIO(task, try makeOkResult(self.arena, value)); 922 }, 923 924 .renameFile => |paths| { 925 const old = try codecs.toString(self.arena, paths.old) orelse { 926 try self.fsReply(task, ERR_INVALID_STRING); 927 return; 928 }; 929 defer self.arena.allocator.free(old); 930 const new = try codecs.toString(self.arena, paths.new) orelse { 931 try self.fsReply(task, ERR_INVALID_STRING); 932 return; 933 }; 934 defer self.arena.allocator.free(new); 935 if (!self.perms.allow_write_all) { 936 try self.fsReply(task, ERR_POLICY_DENY); 937 return; 938 } 939 const old_z = try self.gpa.dupeZ(u8, old); 940 defer self.gpa.free(old_z); 941 const new_z = try self.gpa.dupeZ(u8, new); 942 defer self.gpa.free(new_z); 943 var req: c.uv_fs_t = undefined; 944 const rc = c.uv_fs_rename(self.loop, &req, old_z.ptr, new_z.ptr, null); 945 c.uv_fs_req_cleanup(&req); 946 if (rc < 0) return self.fsReply(task, uvFileError(rc)); 947 try self.completeIO(task, try makeOkResult(self.arena, try self.arena.alloc(.leaf))); 948 }, 949 950 .sha256Hex => |bytes_tree| { 951 const bytes = try codecs.toBytes(self.arena, bytes_tree) orelse { 952 try self.completeIO(task, try makeErrString(self.arena, "invalid bytes")); 953 return; 954 }; 955 defer self.arena.allocator.free(bytes); 956 var digest: [32]u8 = undefined; 957 std.crypto.hash.sha2.Sha256.hash(bytes, &digest, .{}); 958 const hex = std.fmt.bytesToHex(digest, .lower); 959 try self.completeIO(task, try makeOkResult(self.arena, try codecs.ofString(self.arena, &hex))); 960 }, 961 962 .ask => { 963 if (try task.finishValue(self.arena, task.runtime.env)) { 964 try self.completeTask(task); 965 } else { 966 try self.runnable.append(self.gpa, task); 967 } 968 }, 969 970 .local => |loc| { 971 const new_env = try reduce.reduce(try self.arena.alloc(.{ .app = .{ .func = loc.f, .arg = task.runtime.env } }), self.arena, std.math.maxInt(u64)); 972 try task.frames.append(self.gpa, .{ .local = task.runtime.env }); 973 task.runtime.env = new_env; 974 task.current = loc.action; 975 try self.runnable.append(self.gpa, task); 976 }, 977 978 .get => { 979 if (try task.finishValue(self.arena, task.runtime.state)) { 980 try self.completeTask(task); 981 } else { 982 try self.runnable.append(self.gpa, task); 983 } 984 }, 985 986 .put => |new_state| { 987 task.runtime.state = new_state; 988 const leaf = try self.arena.alloc(.leaf); 989 if (try task.finishValue(self.arena, leaf)) { 990 try self.completeTask(task); 991 } else { 992 try self.runnable.append(self.gpa, task); 993 } 994 }, 995 996 .fork => |child_action| { 997 const child = try self.createTask(task, task.runtime.env, task.runtime.state, child_action); 998 try self.runnable.append(self.gpa, child); 999 const handle = try codecs.ofNumber(self.arena, child.id); 1000 if (try task.finishValue(self.arena, handle)) { 1001 try self.completeTask(task); 1002 } else { 1003 try self.runnable.append(self.gpa, task); 1004 } 1005 }, 1006 1007 .await => |handle_tree| { 1008 const handle = try codecs.toNumber(self.arena, handle_tree) orelse { 1009 const err = try makeErrResult(self.arena, ERR_INVALID_ACTION); 1010 if (try task.finishValue(self.arena, err)) { 1011 try self.completeTask(task); 1012 } else { 1013 try self.runnable.append(self.gpa, task); 1014 } 1015 return; 1016 }; 1017 var found: ?*Task = null; 1018 for (self.tasks.items) |t| { 1019 if (t.id == handle) { 1020 found = t; 1021 break; 1022 } 1023 } 1024 if (found == null) { 1025 const err = try makeErrResult(self.arena, ERR_INVALID_ACTION); 1026 if (try task.finishValue(self.arena, err)) { 1027 try self.completeTask(task); 1028 } else { 1029 try self.runnable.append(self.gpa, task); 1030 } 1031 return; 1032 } 1033 if (found.?.status == .completed) { 1034 const result = found.?.result.?; 1035 if (try task.finishValue(self.arena, result)) { 1036 try self.completeTask(task); 1037 } else { 1038 try self.runnable.append(self.gpa, task); 1039 } 1040 } else { 1041 task.status = .blocked; 1042 task.waiting_for = handle; 1043 // Task remains out of runnable until child completes 1044 } 1045 }, 1046 1047 .yield => { 1048 const leaf = try self.arena.alloc(.leaf); 1049 if (try task.finishValue(self.arena, leaf)) { 1050 try self.completeTask(task); 1051 } else { 1052 try self.runnable.append(self.gpa, task); 1053 } 1054 }, 1055 1056 .sleep => |ms_tree| { 1057 const ms = try codecs.toNumber(self.arena, ms_tree) orelse 0; 1058 const ctx = try self.gpa.create(SleepCtx); 1059 ctx.* = .{ 1060 .scheduler = self, 1061 .task = task, 1062 .arena = self.arena, 1063 .timer = undefined, 1064 }; 1065 ctx.timer.data = ctx; 1066 _ = c.uv_timer_init(self.loop, &ctx.timer); 1067 _ = c.uv_timer_start(&ctx.timer, sleep_cb, @intCast(ms), 0); 1068 }, 1069 } 1070 } 1071 }; 1072 1073 // --------------------------------------------------------------------------- 1074 // Async file read 1075 // --------------------------------------------------------------------------- 1076 1077 const FileReadCtx = struct { 1078 scheduler: *Scheduler, 1079 task: *Task, 1080 arena: *Arena, 1081 gpa: std.mem.Allocator, 1082 fd: c_int, 1083 buf: std.ArrayList(u8), 1084 path: [:0]const u8, 1085 req: c.uv_fs_t, 1086 read_buf: ?[]u8, 1087 }; 1088 1089 fn mapUvErr(uv_err: c_int) u64 { 1090 return switch (uv_err) { 1091 c.UV_ENOENT => ERR_DOES_NOT_EXIST, 1092 c.UV_EACCES => ERR_PERMISSION, 1093 c.UV_EEXIST => ERR_ALREADY_EXISTS, 1094 else => ERR_IO_OTHER, 1095 }; 1096 } 1097 1098 fn file_open_cb(req: [*c]c.uv_fs_t) callconv(.c) void { 1099 const ctx = @as(*FileReadCtx, @ptrCast(@alignCast(req.*.data))); 1100 const result = req.*.result; 1101 c.uv_fs_req_cleanup(req); 1102 if (result < 0) { 1103 const err = makeErrResult(ctx.arena, mapUvErr(@intCast(result))) catch { 1104 ctx.gpa.destroy(ctx); 1105 return; 1106 }; 1107 if (ctx.task.finishValue(ctx.arena, err) catch false) { 1108 ctx.scheduler.completeTask(ctx.task) catch {}; 1109 } else { 1110 ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {}; 1111 } 1112 ctx.buf.deinit(ctx.gpa); 1113 ctx.gpa.free(ctx.path); 1114 ctx.gpa.destroy(ctx); 1115 return; 1116 } 1117 ctx.fd = @intCast(result); 1118 const read_buf = ctx.gpa.alloc(u8, 4096) catch unreachable; 1119 ctx.read_buf = read_buf; 1120 var uv_buf = c.uv_buf_init(@ptrCast(read_buf.ptr), @intCast(read_buf.len)); 1121 _ = c.uv_fs_read(ctx.scheduler.loop, req, ctx.fd, &uv_buf, 1, -1, file_read_cb); 1122 } 1123 1124 fn file_read_cb(req: [*c]c.uv_fs_t) callconv(.c) void { 1125 const ctx = @as(*FileReadCtx, @ptrCast(@alignCast(req.*.data))); 1126 const nread = req.*.result; 1127 c.uv_fs_req_cleanup(req); 1128 if (nread < 0) { 1129 _ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, null); 1130 const err = makeErrResult(ctx.arena, mapUvErr(@intCast(nread))) catch { 1131 ctx.gpa.destroy(ctx); 1132 return; 1133 }; 1134 if (ctx.task.finishValue(ctx.arena, err) catch false) { 1135 ctx.scheduler.completeTask(ctx.task) catch {}; 1136 } else { 1137 ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {}; 1138 } 1139 if (ctx.read_buf) |b| ctx.gpa.free(b); 1140 ctx.buf.deinit(ctx.gpa); 1141 ctx.gpa.free(ctx.path); 1142 ctx.gpa.destroy(ctx); 1143 return; 1144 } 1145 if (nread == 0) { 1146 // EOF 1147 _ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, null); 1148 const bytes_tree = codecs.ofBytes(ctx.arena, ctx.buf.items) catch { 1149 ctx.gpa.destroy(ctx); 1150 return; 1151 }; 1152 const ok = makeOkResult(ctx.arena, bytes_tree) catch { 1153 ctx.gpa.destroy(ctx); 1154 return; 1155 }; 1156 if (ctx.task.finishValue(ctx.arena, ok) catch false) { 1157 ctx.scheduler.completeTask(ctx.task) catch {}; 1158 } else { 1159 ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {}; 1160 } 1161 if (ctx.read_buf) |b| ctx.gpa.free(b); 1162 ctx.buf.deinit(ctx.gpa); 1163 ctx.gpa.free(ctx.path); 1164 ctx.gpa.destroy(ctx); 1165 return; 1166 } 1167 const data = ctx.read_buf.?[0..@intCast(nread)]; 1168 ctx.buf.appendSlice(ctx.gpa, data) catch unreachable; 1169 ctx.gpa.free(ctx.read_buf.?); 1170 const read_buf = ctx.gpa.alloc(u8, 4096) catch unreachable; 1171 ctx.read_buf = read_buf; 1172 var uv_buf = c.uv_buf_init(@ptrCast(read_buf.ptr), @intCast(read_buf.len)); 1173 _ = c.uv_fs_read(ctx.scheduler.loop, req, ctx.fd, &uv_buf, 1, -1, file_read_cb); 1174 } 1175 1176 // --------------------------------------------------------------------------- 1177 // Async file write 1178 // --------------------------------------------------------------------------- 1179 1180 const FileWriteCtx = struct { 1181 scheduler: *Scheduler, 1182 task: *Task, 1183 arena: *Arena, 1184 gpa: std.mem.Allocator, 1185 fd: c_int, 1186 path: [:0]const u8, 1187 contents: []const u8, 1188 offset: usize, 1189 req: c.uv_fs_t, 1190 }; 1191 1192 fn file_write_open_cb(req: [*c]c.uv_fs_t) callconv(.c) void { 1193 const ctx = @as(*FileWriteCtx, @ptrCast(@alignCast(req.*.data))); 1194 const result = req.*.result; 1195 c.uv_fs_req_cleanup(req); 1196 if (result < 0) { 1197 const err = makeErrResult(ctx.arena, mapUvErr(@intCast(result))) catch { 1198 ctx.gpa.destroy(ctx); 1199 return; 1200 }; 1201 if (ctx.task.finishValue(ctx.arena, err) catch false) { 1202 ctx.scheduler.completeTask(ctx.task) catch {}; 1203 } else { 1204 ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {}; 1205 } 1206 ctx.gpa.free(ctx.path); 1207 ctx.gpa.free(ctx.contents); 1208 ctx.gpa.destroy(ctx); 1209 return; 1210 } 1211 ctx.fd = @intCast(result); 1212 if (ctx.contents.len == 0) { 1213 _ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, file_write_close_cb); 1214 return; 1215 } 1216 file_write_next(ctx, req); 1217 } 1218 1219 fn file_write_next(ctx: *FileWriteCtx, req: [*c]c.uv_fs_t) void { 1220 const remaining = ctx.contents[ctx.offset..]; 1221 var uv_buf = c.uv_buf_init(@ptrCast(@constCast(remaining.ptr)), @intCast(@min(remaining.len, std.math.maxInt(c_uint)))); 1222 _ = c.uv_fs_write(ctx.scheduler.loop, req, ctx.fd, &uv_buf, 1, @intCast(ctx.offset), file_write_cb); 1223 } 1224 1225 fn file_write_cb(req: [*c]c.uv_fs_t) callconv(.c) void { 1226 const ctx = @as(*FileWriteCtx, @ptrCast(@alignCast(req.*.data))); 1227 const nwrite = req.*.result; 1228 c.uv_fs_req_cleanup(req); 1229 if (nwrite <= 0) { 1230 _ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, null); 1231 const err = makeErrResult(ctx.arena, if (nwrite == 0) ERR_IO_OTHER else mapUvErr(@intCast(nwrite))) catch { 1232 ctx.gpa.destroy(ctx); 1233 return; 1234 }; 1235 if (ctx.task.finishValue(ctx.arena, err) catch false) { 1236 ctx.scheduler.completeTask(ctx.task) catch {}; 1237 } else { 1238 ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {}; 1239 } 1240 ctx.gpa.free(ctx.path); 1241 ctx.gpa.free(ctx.contents); 1242 ctx.gpa.destroy(ctx); 1243 return; 1244 } 1245 ctx.offset += @intCast(nwrite); 1246 if (ctx.offset < ctx.contents.len) { 1247 file_write_next(ctx, req); 1248 return; 1249 } 1250 _ = c.uv_fs_close(ctx.scheduler.loop, req, ctx.fd, file_write_close_cb); 1251 } 1252 1253 fn file_write_close_cb(req: [*c]c.uv_fs_t) callconv(.c) void { 1254 const ctx = @as(*FileWriteCtx, @ptrCast(@alignCast(req.*.data))); 1255 c.uv_fs_req_cleanup(req); 1256 const leaf = ctx.arena.alloc(.leaf) catch { 1257 ctx.gpa.destroy(ctx); 1258 return; 1259 }; 1260 const ok = makeOkResult(ctx.arena, leaf) catch { 1261 ctx.gpa.destroy(ctx); 1262 return; 1263 }; 1264 if (ctx.task.finishValue(ctx.arena, ok) catch false) { 1265 ctx.scheduler.completeTask(ctx.task) catch {}; 1266 } else { 1267 ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {}; 1268 } 1269 ctx.gpa.free(ctx.path); 1270 ctx.gpa.free(ctx.contents); 1271 ctx.gpa.destroy(ctx); 1272 } 1273 1274 // --------------------------------------------------------------------------- 1275 // Async sleep 1276 // --------------------------------------------------------------------------- 1277 1278 const SleepCtx = struct { 1279 scheduler: *Scheduler, 1280 task: *Task, 1281 arena: *Arena, 1282 timer: c.uv_timer_t, 1283 }; 1284 1285 fn sleep_cb(handle: [*c]c.uv_timer_t) callconv(.c) void { 1286 const ctx = @as(*SleepCtx, @ptrCast(@alignCast(handle.*.data))); 1287 defer ctx.scheduler.gpa.destroy(ctx); 1288 const leaf = ctx.arena.alloc(.leaf) catch { 1289 ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {}; 1290 return; 1291 }; 1292 if (ctx.task.finishValue(ctx.arena, leaf) catch false) { 1293 ctx.scheduler.completeTask(ctx.task) catch {}; 1294 } else { 1295 ctx.scheduler.runnable.append(ctx.scheduler.gpa, ctx.task) catch {}; 1296 } 1297 } 1298 1299 // --------------------------------------------------------------------------- 1300 // Public entry point 1301 // --------------------------------------------------------------------------- 1302 1303 pub fn runIO(gpa: std.mem.Allocator, arena: *Arena, program: u32, perms: IOPerms) !u32 { 1304 const base = arena.roots.items.len; 1305 defer arena.roots.shrinkRetainingCapacity(base); 1306 try arena.roots.append(gpa, program); 1307 const action_tree = try isIOSentinel(arena, program) orelse { 1308 return error.InvalidIOSentinel; 1309 }; 1310 1311 var loop: c.uv_loop_t = undefined; 1312 const rc = c.uv_loop_init(&loop); 1313 if (rc != 0) return error.LoopInitFailed; 1314 defer _ = c.uv_loop_close(&loop); 1315 1316 var scheduler = try Scheduler.init(gpa, &loop, arena, perms); 1317 defer scheduler.deinit(); 1318 1319 const main_task = try scheduler.createTask(null, try arena.alloc(.leaf), try arena.alloc(.leaf), action_tree); 1320 try scheduler.runnable.append(gpa, main_task); 1321 1322 try scheduler.run(); 1323 1324 // Return the main task's result 1325 return main_task.result orelse program; 1326 } 1327 1328 test "scheduler safepoint roots include blocked tasks, frames, environment, state and results" { 1329 const testing = std.testing; 1330 var arena = Arena.init(testing.allocator); 1331 defer arena.deinit(); 1332 var loop: c.uv_loop_t = undefined; 1333 try testing.expectEqual(@as(c_int, 0), c.uv_loop_init(&loop)); 1334 defer _ = c.uv_loop_close(&loop); 1335 var scheduler = try Scheduler.init(testing.allocator, &loop, &arena, .{}); 1336 defer scheduler.deinit(); 1337 const leaf = try arena.alloc(.leaf); 1338 const external = try codecs.ofNumber(&arena, 99); 1339 try arena.roots.append(arena.allocator, external); 1340 const value = try codecs.ofNumber(&arena, 42); 1341 const action = try makePure(&arena, value); 1342 const task = try scheduler.createTask(null, leaf, leaf, action); 1343 const blocked = try scheduler.createTask(null, try codecs.ofNumber(&arena, 17), try codecs.ofNumber(&arena, 23), leaf); 1344 blocked.status = .blocked; 1345 blocked.result = try codecs.ofNumber(&arena, 31); 1346 const saved_env = try codecs.ofNumber(&arena, 47); 1347 const continuation = try codecs.ofNumber(&arena, 53); 1348 try blocked.frames.append(arena.allocator, .{ .local = saved_env }); 1349 try blocked.frames.append(arena.allocator, .{ .bind = continuation }); 1350 for (0..1000) |_| _ = try arena.alloc(.leaf); 1351 const before_collection = arena.len(); 1352 arena.gc_enabled = true; 1353 try arena.setGcPolicy(1, 1); 1354 try scheduler.stepTask(task); 1355 try testing.expect(arena.collections > 0); 1356 try testing.expect(before_collection - arena.len() + arena.free.items.len >= 1000); 1357 try testing.expectEqual(@as(usize, 1), arena.roots.items.len); 1358 // Observations below are outside a scheduler turn; explicitly hold its 1359 // roots by stopping automatic collection, not by accidentally relying on 1360 // an allocation threshold. 1361 arena.gc_enabled = false; 1362 const ids = [_]u32{ external, task.result.?, blocked.runtime.env, blocked.runtime.state, blocked.result.?, saved_env, continuation }; 1363 const expected = [_]u64{ 99, 42, 17, 23, 31, 47, 53 }; 1364 for (ids, expected) |id, n| try testing.expectEqual(n, (try codecs.toNumber(&arena, id)).?); 1365 } 1366 1367 test "IO sentinel forces delayed outer and version/action pair" { 1368 const testing = std.testing; 1369 var arena = Arena.init(testing.allocator); 1370 defer arena.deinit(); 1371 const leaf = try arena.alloc(.leaf); 1372 const value = try codecs.ofNumber(&arena, 42); 1373 const action = try makePure(&arena, value); 1374 const version = try codecs.ofNumber(&arena, 1); 1375 const pair = try arena.alloc(.{ .fork = .{ .left = version, .right = action } }); 1376 const k_pair = try arena.alloc(.{ .fork = .{ .left = leaf, .right = pair } }); 1377 const delayed_pair = try arena.alloc(.{ .app = .{ .func = k_pair, .arg = leaf } }); 1378 const marker = try codecs.ofString(&arena, "tricuIO"); 1379 const sentinel = try arena.alloc(.{ .fork = .{ .left = marker, .right = delayed_pair } }); 1380 const k_sentinel = try arena.alloc(.{ .fork = .{ .left = leaf, .right = sentinel } }); 1381 const program = try arena.alloc(.{ .app = .{ .func = k_sentinel, .arg = leaf } }); 1382 arena.gc_enabled = true; 1383 try arena.setGcPolicy(1, 1); 1384 const result = try runIO(arena.allocator, &arena, program, .{}); 1385 arena.gc_enabled = false; // result is held locally, outside a registered scope 1386 try testing.expectEqual(@as(u64, 42), (try codecs.toNumber(&arena, result)).?); 1387 try testing.expectEqual(@as(usize, 0), arena.roots.items.len); 1388 } 1389 1390 fn testAction(arena: *Arena, tag: u64, payload: u32, perms: IOPerms) !codecs.UnwrapResult { 1391 const action = try arena.alloc(.{ .fork = .{ .left = try codecs.ofNumber(arena, tag), .right = payload } }); 1392 const rest = try arena.alloc(.{ .fork = .{ .left = try codecs.ofNumber(arena, 1), .right = action } }); 1393 const program = try arena.alloc(.{ .fork = .{ .left = try codecs.ofString(arena, "tricuIO"), .right = rest } }); 1394 arena.gc_enabled = true; 1395 try arena.setGcPolicy(1, 1); 1396 const result = try runIO(arena.allocator, arena, program, perms); 1397 arena.gc_enabled = false; // observations are local, outside the scheduler 1398 return (try codecs.unwrapResult(arena, result)).?; 1399 } 1400 1401 fn expectErrorText(arena: *Arena, reply: codecs.UnwrapResult, text: []const u8) !void { 1402 try std.testing.expect(!reply.ok); 1403 const message = try source_text.toUtf8(arena, reply.value); 1404 defer arena.allocator.free(message); 1405 try std.testing.expect(std.mem.indexOf(u8, message, text) != null); 1406 } 1407 1408 test "encodeSourceText is strict, lazy, permission-free and distinct from byte codecs" { 1409 var arena = Arena.init(std.testing.allocator); 1410 defer arena.deinit(); 1411 const expected = "\u{feff}aĆ©š\x00\r\ne\u{301}"; 1412 const list = try source_text.fromUtf8(&arena, expected); 1413 const leaf = try arena.alloc(.leaf); 1414 const constant = try arena.alloc(.{ .fork = .{ .left = leaf, .right = list } }); 1415 const delayed = try arena.alloc(.{ .app = .{ .func = constant, .arg = leaf } }); 1416 const result = try testAction(&arena, 38, delayed, .{}); 1417 try std.testing.expect(result.ok); 1418 const bytes = (try codecs.toBytes(&arena, result.value)).?; 1419 defer arena.allocator.free(bytes); 1420 try std.testing.expectEqualStrings(expected, bytes); 1421 for ([_]u64{ 0xd800, 0x110000 }) |n| { 1422 const bad = try codecs.ofList(&arena, &.{try codecs.ofNumber(&arena, n)}); 1423 try expectErrorText(&arena, try testAction(&arena, 38, bad, .{}), "invalid source scalar list"); 1424 } 1425 // Noncanonical zero has the right numeric value, but is not a scalar tree. 1426 const zero = try arena.alloc(.leaf); 1427 const redundant = try arena.alloc(.{ .fork = .{ .left = zero, .right = zero } }); 1428 const bad = try codecs.ofList(&arena, &.{redundant}); 1429 try expectErrorText(&arena, try testAction(&arena, 38, bad, .{}), "invalid source scalar list"); 1430 } 1431 1432 test "readSourceFile preserves bytes and scalars, rejects malformed input, and enforces read permission" { 1433 var tmp = std.testing.tmpDir(.{}); 1434 defer tmp.cleanup(); 1435 const directory = try tmp.dir.realPathFileAlloc(std.testing.io, ".", std.testing.allocator); 1436 defer std.testing.allocator.free(directory); 1437 const path = try std.fmt.allocPrint(std.testing.allocator, "{s}/Ć©š.tri", .{directory}); 1438 defer std.testing.allocator.free(path); 1439 var arena = Arena.init(std.testing.allocator); 1440 defer arena.deinit(); 1441 var bytes: std.ArrayList(u8) = .empty; 1442 defer bytes.deinit(std.testing.allocator); 1443 try bytes.appendNTimes(std.testing.allocator, 'a', 4095); 1444 try bytes.appendSlice(std.testing.allocator, "š\r\nĆ©\x00\u{feff}e\u{301}"); 1445 for ([_][]const u8{ bytes.items, "" }) |contents| { 1446 try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "Ć©š.tri", .data = contents }); 1447 const reply = try testAction(&arena, 37, try source_text.fromUtf8(&arena, path), .{ .allow_read_all = true }); 1448 try std.testing.expect(reply.ok); 1449 const pair = arena.get(reply.value).fork; 1450 const raw = (try codecs.toBytes(&arena, pair.left)).?; 1451 defer arena.allocator.free(raw); 1452 const text = try source_text.toUtf8(&arena, pair.right); 1453 defer arena.allocator.free(text); 1454 try std.testing.expectEqualStrings(contents, raw); 1455 try std.testing.expectEqualStrings(contents, text); 1456 } 1457 try expectErrorText(&arena, try testAction(&arena, 37, try source_text.fromUtf8(&arena, path), .{}), "permission denied"); 1458 for ([_][]const u8{ "\x80", "\xc0\xaf", "\xe2\x82", "\xed\xa0\x80", "\xf4\x90\x80\x80", "\xef\xbb\xbfok" }) |contents| { 1459 try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "Ć©š.tri", .data = contents }); 1460 const reply = try testAction(&arena, 37, try source_text.fromUtf8(&arena, path), .{ .allow_read_all = true }); 1461 try expectErrorText(&arena, reply, path); 1462 try expectErrorText(&arena, reply, if (std.mem.startsWith(u8, contents, "\xef\xbb\xbf")) "BOM is not permitted" else "invalid UTF-8 source"); 1463 } 1464 // An embedded NUL must not turn a different path into the existing file. 1465 const nul_path = try std.fmt.allocPrint(std.testing.allocator, "{s}\x00ignored", .{path}); 1466 defer std.testing.allocator.free(nul_path); 1467 try expectErrorText(&arena, try testAction(&arena, 37, try source_text.fromUtf8(&arena, nul_path), .{ .allow_read_all = true }), "invalid string"); 1468 try tmp.dir.deleteFile(std.testing.io, "Ć©š.tri"); 1469 try expectErrorText(&arena, try testAction(&arena, 37, try source_text.fromUtf8(&arena, path), .{ .allow_read_all = true }), "does not exist"); 1470 // Ordinary readFile remains a byte transport: malformed UTF-8 and BOMs 1471 // are not interpreted. Use its existing byte-oriented path convention. 1472 const binary = "\xff\x00\xef\xbb\xbf\r\n"; 1473 try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "Ć©š.tri", .data = binary }); 1474 const reply = try testAction(&arena, 20, try codecs.ofString(&arena, path), .{ .allow_read_all = true }); 1475 try std.testing.expect(reply.ok); 1476 const raw = (try codecs.toBytes(&arena, reply.value)).?; 1477 defer arena.allocator.free(raw); 1478 try std.testing.expectEqualStrings(binary, raw); 1479 } 1480 1481 fn sourceIOAllocationProbe(allocator: std.mem.Allocator, path: []const u8) !void { 1482 var arena = Arena.init(allocator); 1483 defer arena.deinit(); 1484 const reply = try testAction(&arena, 37, try source_text.fromUtf8(&arena, path), .{ .allow_read_all = true }); 1485 try std.testing.expect(reply.ok); 1486 const scalars = arena.get(reply.value).fork.right; 1487 const encoded = try testAction(&arena, 38, scalars, .{}); 1488 try std.testing.expect(encoded.ok); 1489 const raw = (try codecs.toBytes(&arena, encoded.value)).?; 1490 defer allocator.free(raw); 1491 try std.testing.expectEqualStrings("aĆ©š", raw); 1492 } 1493 1494 test "source IO unwinds allocation failures without leaking tasks, buffers or requests" { 1495 var tmp = std.testing.tmpDir(.{}); 1496 defer tmp.cleanup(); 1497 try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "source.tri", .data = "aĆ©š" }); 1498 const path = try tmp.dir.realPathFileAlloc(std.testing.io, "source.tri", std.testing.allocator); 1499 defer std.testing.allocator.free(path); 1500 try std.testing.checkAllAllocationFailures(std.testing.allocator, sourceIOAllocationProbe, .{path}); 1501 } 1502 1503 fn testIndexedAction(arena: *Arena, tag: u64, path: []const u8, hash: []const u8, value: u32, perms: IOPerms) !codecs.UnwrapResult { 1504 const path_tree = try source_text.fromUtf8(arena, path); 1505 const digest_tree = try codecs.ofString(arena, hash); 1506 const rest = if (tag == 52) try arena.alloc(.{ .fork = .{ .left = digest_tree, .right = value } }) else digest_tree; 1507 const args = try arena.alloc(.{ .fork = .{ .left = path_tree, .right = rest } }); 1508 return testAction(arena, tag, args, perms); 1509 } 1510 1511 test "indexed IO authenticates, enforces permissions before demand, and never replaces a different winner" { 1512 var tmp = std.testing.tmpDir(.{}); 1513 defer tmp.cleanup(); 1514 const directory = try tmp.dir.realPathFileAlloc(std.testing.io, ".", std.testing.allocator); 1515 defer std.testing.allocator.free(directory); 1516 const path = try std.fmt.allocPrint(std.testing.allocator, "{s}/Ć©.arboricx", .{directory}); 1517 defer std.testing.allocator.free(path); 1518 var arena = Arena.init(std.testing.allocator); 1519 defer arena.deinit(); 1520 const leaf = try arena.alloc(.leaf); 1521 // Test helpers turn on collection; retain the tree across independent IO calls. 1522 try arena.roots.append(arena.allocator, leaf); 1523 const bytes = try indexed.encode(&arena, leaf); 1524 defer arena.allocator.free(bytes); 1525 const hash = indexed.digest(bytes); 1526 const perms = IOPerms{ .allow_read_all = true, .allow_write_all = true }; 1527 const hashed = try testAction(&arena, 51, leaf, .{}); 1528 try std.testing.expect(hashed.ok); 1529 const actual = (try codecs.toString(&arena, hashed.value)).?; 1530 defer arena.allocator.free(actual); 1531 try std.testing.expectEqualStrings(&hash, actual); 1532 for ([_]bool{ true, false }) |fresh| { 1533 const result = try testIndexedAction(&arena, 52, path, &hash, leaf, perms); 1534 try std.testing.expect(result.ok); 1535 try std.testing.expectEqual(fresh, (try codecs.toBool(&arena, result.value)).?); 1536 } 1537 const read = try testIndexedAction(&arena, 50, path, &hash, leaf, .{ .allow_read_all = true }); 1538 try std.testing.expect(read.ok and arena.get(read.value).* == .leaf); 1539 // A denied write must not try to normalize a divergent supplied tree. 1540 const poison = try arena.alloc(.leaf); 1541 arena.get(poison).* = .{ .app = .{ .func = poison, .arg = leaf } }; 1542 try arena.roots.append(arena.allocator, poison); 1543 for ([_]IOPerms{ .{}, .{ .allow_read_all = true }, .{ .allow_write_all = true } }) |denied| { 1544 try expectErrorText(&arena, try testIndexedAction(&arena, 52, path, &hash, poison, denied), "permission denied"); 1545 } 1546 try expectErrorText(&arena, try testIndexedAction(&arena, 50, path, &hash, leaf, .{}), "permission denied"); 1547 try expectErrorText(&arena, try testIndexedAction(&arena, 50, path, "ABC", leaf, perms), "invalid CAS digest"); 1548 const wrong = "0" ** 64; 1549 try expectErrorText(&arena, try testIndexedAction(&arena, 50, path, wrong, leaf, perms), "CAS digest mismatch"); 1550 try expectErrorText(&arena, try testIndexedAction(&arena, 52, path, wrong, leaf, perms), "CAS digest mismatch"); 1551 try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "Ć©.arboricx", .data = "corrupt" }); 1552 try expectErrorText(&arena, try testIndexedAction(&arena, 52, path, &hash, leaf, perms), "different bytes"); 1553 const still_corrupt = try readFsForTest(&arena, path); 1554 defer arena.allocator.free(still_corrupt); 1555 try std.testing.expectEqualStrings("corrupt", still_corrupt); 1556 const corrupt_hash = indexed.digest("corrupt"); 1557 try expectErrorText(&arena, try testIndexedAction(&arena, 50, path, &corrupt_hash, leaf, perms), "invalid indexed tree"); 1558 } 1559 1560 fn readFsForTest(arena: *Arena, path: []const u8) ![]u8 { 1561 var loop: c.uv_loop_t = undefined; 1562 try std.testing.expectEqual(@as(c_int, 0), c.uv_loop_init(&loop)); 1563 defer _ = c.uv_loop_close(&loop); 1564 const z = try arena.allocator.dupeZ(u8, path); 1565 defer arena.allocator.free(z); 1566 return switch (try readFs(&loop, arena.allocator, z)) { 1567 .bytes => |b| b, 1568 .failure => error.TestUnexpectedResult, 1569 }; 1570 } 1571 1572 fn indexedIOAllocationProbe(allocator: std.mem.Allocator, path: []const u8) !void { 1573 var arena = Arena.init(allocator); 1574 defer arena.deinit(); 1575 const leaf = try arena.alloc(.leaf); 1576 try arena.roots.append(allocator, leaf); 1577 const hash = "6f93378feaa8bf5ab7cefc071175a654c871bfd53f165d7d96795fb2b38483f1"; 1578 const perms = IOPerms{ .allow_read_all = true, .allow_write_all = true }; 1579 const written = try testIndexedAction(&arena, 52, path, hash, leaf, perms); 1580 try std.testing.expect(written.ok); 1581 const read = try testIndexedAction(&arena, 50, path, hash, leaf, perms); 1582 try std.testing.expect(read.ok and arena.get(read.value).* == .leaf); 1583 } 1584 1585 test "indexed publication and reads unwind every allocator failure" { 1586 var tmp = std.testing.tmpDir(.{}); 1587 defer tmp.cleanup(); 1588 const directory = try tmp.dir.realPathFileAlloc(std.testing.io, ".", std.testing.allocator); 1589 defer std.testing.allocator.free(directory); 1590 const path = try std.fmt.allocPrint(std.testing.allocator, "{s}/object", .{directory}); 1591 defer std.testing.allocator.free(path); 1592 try std.testing.checkAllAllocationFailures(std.testing.allocator, indexedIOAllocationProbe, .{path}); 1593 }