const std = @import("std");const builtin = @import("builtin");const darwin_process_spawn = @import("darwin_process_spawn.zig"); pub const RawEnviron = [*:null]const ?[*:0]const u8; // Process globals are installed before threads start and remain read-only.var real_io: ?std.Io = null; // Fallback used by non-test code only when setIo was not called. The real// application installs `real_io` from main before spawning threads.var fallback_threaded: std.Io.Threaded = .init_single_threaded; var global_environ: ?*const std.process.Environ.Map = null;var global_environ_block: ?std.process.Environ.Block = null;var global_raw_environ: ?RawEnviron = null; pub fn setIo(zio: std.Io) void { real_io = process_io_for(builtin.os.tag, zio);} fn process_io_for(comptime os_tag: std.Target.Os.Tag, zio: std.Io) std.Io { return if (os_tag == .macos) darwin_process_spawn.wrap(zio) else zio;} pub fn getIo() std.Io { if (real_io) |zio| return zio; if (comptime builtin.is_test) return process_io_for(builtin.os.tag, std.testing.io); return process_io_for(builtin.os.tag, fallback_threaded.io());} /// Opens an absolute directory path without following any path component./// The caller owns the returned directory handle.pub fn openDirAbsoluteNoFollow(path: []const u8, options: std.Io.Dir.OpenOptions) !std.Io.Dir { if (!std.fs.path.isAbsolute(path)) return error.InvalidPath; var components = std.fs.path.componentIterator(path); const root = components.root() orelse return error.InvalidPath; var component = components.next() orelse { var root_options = options; root_options.follow_symlinks = false; return std.Io.Dir.openDirAbsolute(getIo(), root, root_options); }; var dir = try std.Io.Dir.openDirAbsolute(getIo(), root, .{ .follow_symlinks = false }); errdefer dir.close(getIo()); while (components.next()) |next_component| { if (std.mem.eql(u8, component.name, ".") or std.mem.eql(u8, component.name, "..")) { return error.InvalidPath; } const next_dir = try dir.openDir(getIo(), component.name, .{ .follow_symlinks = false }); dir.close(getIo()); dir = next_dir; component = next_component; } if (std.mem.eql(u8, component.name, ".") or std.mem.eql(u8, component.name, "..")) { return error.InvalidPath; } var final_options = options; final_options.follow_symlinks = false; const result = try dir.openDir(getIo(), component.name, final_options); dir.close(getIo()); return result;} test "Darwin process I/O replaces only processSpawn with stable storage" { const original = std.testing.io; const selected = process_io_for(.macos, original); const selected_again = process_io_for(.macos, original); try std.testing.expect(selected.userdata == original.userdata); try std.testing.expect(selected.vtable == selected_again.vtable); inline for (@typeInfo(std.Io.VTable).@"struct".fields) |field| { if (comptime std.mem.eql(u8, field.name, "processSpawn")) { try std.testing.expect(@field(selected.vtable, field.name) != @field(original.vtable, field.name)); } else { try std.testing.expectEqual(@field(original.vtable, field.name), @field(selected.vtable, field.name)); } }} test "getIo applies Darwin process selection to the test fallback" { if (builtin.os.tag != .macos) return error.SkipZigTest; const previous = real_io; real_io = null; defer real_io = previous; const selected = getIo(); try std.testing.expect(selected.userdata == std.testing.io.userdata); try std.testing.expect(selected.vtable.processSpawn != std.testing.io.vtable.processSpawn);} test "getIo Darwin test fallback runs a child through the selected backend" { if (builtin.os.tag != .macos) return error.SkipZigTest; const previous = real_io; real_io = null; defer real_io = previous; const alloc = std.testing.allocator; const io = getIo(); const result = try std.process.run(alloc, io, .{ .argv = &.{ "/usr/bin/printf", "get-io-spawn-ok" }, }); defer alloc.free(result.stdout); defer alloc.free(result.stderr); switch (result.term) { .exited => |code| try std.testing.expectEqual(@as(u8, 0), code), else => return error.UnexpectedTermination, } try std.testing.expectEqualStrings("get-io-spawn-ok", result.stdout); try std.testing.expectEqualStrings("", result.stderr);} test "non-Darwin process I/O keeps the original vtable" { const original = std.testing.io; const selected = process_io_for(.linux, original); try std.testing.expect(selected.userdata == original.userdata); try std.testing.expect(selected.vtable == original.vtable);} test "openDirAbsoluteNoFollow rejects unsafe path components" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(getIo(), "real/child"); try writeTempFile(tmp.dir, "plain-file", "not a directory"); tmp.dir.symLink(std.testing.io, "real", "linked", .{ .is_directory = true }) catch |err| { if (err == error.AccessDenied or err == error.FileSystem) return error.SkipZigTest; return err; }; const root = try dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(root); const linked_child = try std.fs.path.join(alloc, &.{ root, "linked/child" }); defer alloc.free(linked_child); const missing = try std.fs.path.join(alloc, &.{ root, "missing" }); defer alloc.free(missing); const wrong_kind = try std.fs.path.join(alloc, &.{ root, "plain-file" }); defer alloc.free(wrong_kind); if (openDirAbsoluteNoFollow(linked_child, .{})) |dir| { dir.close(getIo()); return error.TestExpectedError; } else |err| switch (err) { error.NotDir, error.SymLinkLoop => {}, else => return err, } try std.testing.expectError(error.FileNotFound, openDirAbsoluteNoFollow(missing, .{})); try std.testing.expectError(error.NotDir, openDirAbsoluteNoFollow(wrong_kind, .{}));} /// Opens an existing regular file without following the final symlink and/// without waiting on a special file that races the initial metadata check./// The caller owns the returned file.pub fn openExistingRegularFile( dir: std.Io.Dir, sub_path: []const u8, mode: std.Io.Dir.OpenFileOptions.Mode,) !std.Io.File { const initial = try dir.statFile(getIo(), sub_path, .{ .follow_symlinks = false, }); if (initial.kind != .file or initial.nlink != 1) { return error.DurablePathUnsafe; } if (comptime builtin.os.tag == .windows or builtin.os.tag == .wasi) { var file = try dir.openFile(getIo(), sub_path, .{ .mode = mode, .allow_directory = false, .follow_symlinks = false, }); errdefer file.close(getIo()); const stat = try file.stat(getIo()); try verifyOpenedRegularFile(stat, mode); return file; } var flags: std.posix.O = .{ .ACCMODE = switch (mode) { .read_only => .RDONLY, .write_only => .WRONLY, .read_write => .RDWR, }, .NOFOLLOW = true, .NONBLOCK = true, }; if (@hasField(std.posix.O, "CLOEXEC")) flags.CLOEXEC = true; if (@hasField(std.posix.O, "LARGEFILE")) flags.LARGEFILE = true; if (@hasField(std.posix.O, "NOCTTY")) flags.NOCTTY = true; const fd = std.posix.openat(dir.handle, sub_path, flags, 0) catch |err| switch (err) { error.NoDevice, error.SymLinkLoop, error.NotDir => return error.DurablePathUnsafe, else => return err, }; var file = std.Io.File{ .handle = fd, .flags = .{ .nonblocking = true }, }; errdefer file.close(getIo()); const stat = try file.stat(getIo()); try verifyOpenedRegularFile(stat, mode); try makeFileBlocking(&file); return file;} pub fn verifyOpenedRegularFile( stat: std.Io.File.Stat, mode: std.Io.Dir.OpenFileOptions.Mode,) !void { if (stat.kind != .file or stat.nlink > 1) { return error.DurablePathUnsafe; } if (mode != .read_only and stat.nlink != 1) { return error.DurablePathUnsafe; }} fn makeFileBlocking(file: *std.Io.File) !void { const current = while (true) { const rc = std.posix.system.fcntl( file.handle, std.posix.F.GETFL, @as(usize, 0), ); switch (std.posix.errno(rc)) { .SUCCESS => break @as(usize, @intCast(rc)), .INTR => continue, else => return error.FileControlFailed, } }; const nonblock = @as(usize, 1) << @bitOffsetOf(std.posix.O, "NONBLOCK"); while (true) { const rc = std.posix.system.fcntl( file.handle, std.posix.F.SETFL, current & ~nonblock, ); switch (std.posix.errno(rc)) { .SUCCESS => break, .INTR => continue, else => return error.FileControlFailed, } } file.flags.nonblocking = false;} test "read-only regular files remain valid when atomic replacement unlinks the descriptor" { if (comptime builtin.os.tag == .windows or builtin.os.tag == .wasi) { return error.SkipZigTest; } const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.writeFile(getIo(), .{ .sub_path = "target", .data = "old" }); var file = try openExistingRegularFile(tmp.dir, "target", .read_only); defer file.close(getIo()); try tmp.dir.writeFile(getIo(), .{ .sub_path = "replacement", .data = "new" }); try tmp.dir.rename("replacement", tmp.dir, "target", getIo()); const stat = try file.stat(getIo()); try std.testing.expectEqual(@as(u64, 0), stat.nlink); try verifyOpenedRegularFile(stat, .read_only); try std.testing.expectError( error.DurablePathUnsafe, verifyOpenedRegularFile(stat, .read_write), ); const bytes = try readFileToEnd(alloc, &file, 16); defer alloc.free(bytes); try std.testing.expectEqualStrings("old", bytes);} pub fn setEnvironMap(m: *const std.process.Environ.Map) void { global_environ = m; global_environ_block = null; global_raw_environ = null;} pub fn setEnvironBlock(block: std.process.Environ.Block) void { global_environ = null; global_environ_block = block; global_raw_environ = null;} pub fn setRawEnviron(raw: RawEnviron) void { global_environ = null; global_environ_block = null; global_raw_environ = raw;} pub fn getenv(key: []const u8) ?[]const u8 { if (global_environ) |m| return m.get(key); if (global_environ_block) |block| return getenvFromBlock(block, key); if (global_raw_environ) |raw| return getenvFromLibc(key) orelse getenvFromRaw(raw, key); return null;} pub fn e2eFailIfDurableMutationAttempted() void { const enabled = getenv("FX_E2E_FAIL_ON_DURABLE_MUTATION") orelse return; if (!std.mem.eql(u8, enabled, "1")) return; std.process.exit(86);} pub fn environMap() ?*const std.process.Environ.Map { return global_environ;} fn getenvFromBlock(block: std.process.Environ.Block, key: []const u8) ?[]const u8 { if (comptime builtin.os.tag == .windows or builtin.os.tag == .freestanding or builtin.os.tag == .other) { return null; } if (comptime (builtin.os.tag == .wasi or builtin.os.tag == .emscripten) and !builtin.link_libc) { return null; } const view = block.view(); for (view.slice) |entry_z| { const entry = std.mem.sliceTo(entry_z, 0); if (entry.len <= key.len or entry[key.len] != '=') continue; if (std.mem.eql(u8, entry[0..key.len], key)) return entry[key.len + 1 ..]; } return null;} fn getenvFromRaw(raw: RawEnviron, key: []const u8) ?[]const u8 { @setRuntimeSafety(false); var i: usize = 0; while (raw[i]) |entry_z| : (i += 1) { const entry = std.mem.sliceTo(entry_z, 0); if (entry.len <= key.len or entry[key.len] != '=') continue; if (std.mem.eql(u8, entry[0..key.len], key)) return entry[key.len + 1 ..]; } return null;} fn getenvFromLibc(key: []const u8) ?[]const u8 { if (comptime !builtin.link_libc) return null; if (key.len >= 128) return null; var key_buf: [128]u8 = undefined; @memcpy(key_buf[0..key.len], key); key_buf[key.len] = 0; const value_z = std.c.getenv(key_buf[0..key.len :0].ptr) orelse return null; return std.mem.sliceTo(value_z, 0);} pub fn readFileToEnd(alloc: std.mem.Allocator, file: *std.Io.File, max_bytes: usize) ![]u8 { const zio = getIo(); var read_buf: [8192]u8 = undefined; var r = file.reader(zio, &read_buf); return r.interface.allocRemaining(alloc, std.Io.Limit.limited(max_bytes));} pub fn readFileToEndZ(alloc: std.mem.Allocator, file: *std.Io.File, max_bytes: usize) ![:0]u8 { const data = try readFileToEnd(alloc, file, max_bytes); errdefer alloc.free(data); const result = try alloc.realloc(data, data.len + 1); result[data.len] = 0; return result[0..data.len :0];} pub fn milliTimestamp() i64 { const ts = std.Io.Timestamp.now(getIo(), .real); return @intCast(@divFloor(ts.nanoseconds, 1_000_000));} pub fn nanoTimestamp() i128 { const ts = std.Io.Timestamp.now(getIo(), .real); return @intCast(ts.nanoseconds);} pub fn writeFileAtomic(alloc: std.mem.Allocator, path: []const u8, text: []const u8) !void { e2eFailIfDurableMutationAttempted(); const maybe_existing_permissions = existingFilePermissions(path); if (maybe_existing_permissions) |existing_permissions| { if (existing_permissions.toMode() & 0o222 == 0) return error.AccessDenied; } const permissions = maybe_existing_permissions orelse .default_file; const temp_path = try std.fmt.allocPrint(alloc, "{s}.tmp.{d}", .{ path, nanoTimestamp() }); defer alloc.free(temp_path); var cleanup_temp = true; defer if (cleanup_temp) std.Io.Dir.deleteFileAbsolute(getIo(), temp_path) catch {}; { var file = try std.Io.Dir.createFileAbsolute(getIo(), temp_path, .{ .truncate = true, .permissions = permissions }); defer file.close(getIo()); try file.writeStreamingAll(getIo(), text); try file.sync(getIo()); } try std.Io.Dir.renameAbsolute(temp_path, path, getIo()); cleanup_temp = false;} const private_dir_permissions = std.Io.File.Permissions.fromMode(0o700);const private_file_permissions = std.Io.File.Permissions.fromMode(0o600); pub const VerifiedDir = struct { dir: std.Io.Dir, pub fn close(self: *VerifiedDir) void { self.dir.close(getIo()); self.* = undefined; }}; fn defaultSyncFile(_: ?*anyopaque, file: std.Io.File) anyerror!void { try file.sync(getIo());} fn defaultSyncDir(_: ?*anyopaque, dir: std.Io.Dir) anyerror!void { try syncVerifiedDir(dir);} pub const DurableOps = struct { ctx: ?*anyopaque = null, sync_file: *const fn (?*anyopaque, std.Io.File) anyerror!void = defaultSyncFile, sync_dir: *const fn (?*anyopaque, std.Io.Dir) anyerror!void = defaultSyncDir,}; fn defaultTryLock(_: ?*anyopaque, file: std.Io.File) anyerror!bool { return file.tryLock(getIo(), .exclusive);} fn defaultLockNow(_: ?*anyopaque) i64 { return milliTimestamp();} fn defaultLockSleep(_: ?*anyopaque, millis: u64) void { sleep(millis * std.time.ns_per_ms);} pub const LockOps = struct { ctx: ?*anyopaque = null, try_lock: *const fn (?*anyopaque, std.Io.File) anyerror!bool = defaultTryLock, now_ms: *const fn (?*anyopaque) i64 = defaultLockNow, sleep_ms: *const fn (?*anyopaque, u64) void = defaultLockSleep,}; pub const TimedAdvisoryLock = struct { file: std.Io.File, pub fn release(self: *TimedAdvisoryLock) void { self.file.unlock(getIo()); self.file.close(getIo()); self.* = undefined; }}; fn validateRelativeLeaf(name: []const u8) !void { if (name.len == 0 or std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) { return error.DurablePathUnsafe; } if (std.mem.indexOfAny(u8, name, "/\\") != null) return error.DurablePathUnsafe;} fn verifyPrivateRegularFile(file: std.Io.File) !void { const stat = try file.stat(getIo()); if (stat.kind != .file or stat.nlink != 1) return error.DurablePathUnsafe; if (stat.permissions.toMode() & 0o777 != 0o600) return error.PrivateStatePermissionsUnsupported;} fn verifyPrivateDirectory(dir: std.Io.Dir) !void { const stat = try dir.stat(getIo()); if (stat.kind != .directory) return error.DurablePathUnsafe; if (stat.permissions.toMode() & 0o777 != 0o700) return error.PrivateStatePermissionsUnsupported;} /// The handle must come from an `openDir` that requested iteration. Linux returns an/// `O_PATH` descriptor otherwise, and `fsync` rejects those with `EBADF`.pub fn syncVerifiedDir(dir: std.Io.Dir) !void { if (comptime builtin.os.tag == .windows) return error.OperationUnsupported; while (true) { const rc = std.c.fsync(dir.handle); if (rc == 0) return; switch (std.c.errno(rc)) { .INTR => continue, .INVAL, .OPNOTSUPP => return error.OperationUnsupported, .NOSPC => return error.NoSpaceLeft, .DQUOT => return error.DiskQuota, .ROFS => return error.ReadOnlyFileSystem, else => return error.DirectorySyncFailed, } }} fn openOrCreateVerifiedPrivateChild(parent: std.Io.Dir, name: []const u8) !VerifiedDir { e2eFailIfDurableMutationAttempted(); try validateRelativeLeaf(name); const zio = getIo(); var created = false; var dir = parent.openDir(zio, name, .{ .iterate = true,Lines 1–500 of 1298. Load more
const std = @import("std"); const builtin = @import("builtin"); const darwin_process_spawn = @import("darwin_process_spawn.zig"); pub const RawEnviron = [*:null]const ?[*:0]const u8; // Process globals are installed before threads start and remain read-only. var real_io: ?std.Io = null; // Fallback used by non-test code only when setIo was not called. The real // application installs `real_io` from main before spawning threads. var fallback_threaded: std.Io.Threaded = .init_single_threaded; var global_environ: ?*const std.process.Environ.Map = null; var global_environ_block: ?std.process.Environ.Block = null; var global_raw_environ: ?RawEnviron = null; pub fn setIo(zio: std.Io) void { real_io = process_io_for(builtin.os.tag, zio); } fn process_io_for(comptime os_tag: std.Target.Os.Tag, zio: std.Io) std.Io { return if (os_tag == .macos) darwin_process_spawn.wrap(zio) else zio; } pub fn getIo() std.Io { if (real_io) |zio| return zio; if (comptime builtin.is_test) return process_io_for(builtin.os.tag, std.testing.io); return process_io_for(builtin.os.tag, fallback_threaded.io()); } /// Opens an absolute directory path without following any path component. /// The caller owns the returned directory handle. pub fn openDirAbsoluteNoFollow(path: []const u8, options: std.Io.Dir.OpenOptions) !std.Io.Dir { if (!std.fs.path.isAbsolute(path)) return error.InvalidPath; var components = std.fs.path.componentIterator(path); const root = components.root() orelse return error.InvalidPath; var component = components.next() orelse { var root_options = options; root_options.follow_symlinks = false; return std.Io.Dir.openDirAbsolute(getIo(), root, root_options); }; var dir = try std.Io.Dir.openDirAbsolute(getIo(), root, .{ .follow_symlinks = false }); errdefer dir.close(getIo()); while (components.next()) |next_component| { if (std.mem.eql(u8, component.name, ".") or std.mem.eql(u8, component.name, "..")) { return error.InvalidPath; } const next_dir = try dir.openDir(getIo(), component.name, .{ .follow_symlinks = false }); dir.close(getIo()); dir = next_dir; component = next_component; } if (std.mem.eql(u8, component.name, ".") or std.mem.eql(u8, component.name, "..")) { return error.InvalidPath; } var final_options = options; final_options.follow_symlinks = false; const result = try dir.openDir(getIo(), component.name, final_options); dir.close(getIo()); return result; } test "Darwin process I/O replaces only processSpawn with stable storage" { const original = std.testing.io; const selected = process_io_for(.macos, original); const selected_again = process_io_for(.macos, original); try std.testing.expect(selected.userdata == original.userdata); try std.testing.expect(selected.vtable == selected_again.vtable); inline for (@typeInfo(std.Io.VTable).@"struct".fields) |field| { if (comptime std.mem.eql(u8, field.name, "processSpawn")) { try std.testing.expect(@field(selected.vtable, field.name) != @field(original.vtable, field.name)); } else { try std.testing.expectEqual(@field(original.vtable, field.name), @field(selected.vtable, field.name)); } } } test "getIo applies Darwin process selection to the test fallback" { if (builtin.os.tag != .macos) return error.SkipZigTest; const previous = real_io; real_io = null; defer real_io = previous; const selected = getIo(); try std.testing.expect(selected.userdata == std.testing.io.userdata); try std.testing.expect(selected.vtable.processSpawn != std.testing.io.vtable.processSpawn); } test "getIo Darwin test fallback runs a child through the selected backend" { if (builtin.os.tag != .macos) return error.SkipZigTest; const previous = real_io; real_io = null; defer real_io = previous; const alloc = std.testing.allocator; const io = getIo(); const result = try std.process.run(alloc, io, .{ .argv = &.{ "/usr/bin/printf", "get-io-spawn-ok" }, }); defer alloc.free(result.stdout); defer alloc.free(result.stderr); switch (result.term) { .exited => |code| try std.testing.expectEqual(@as(u8, 0), code), else => return error.UnexpectedTermination, } try std.testing.expectEqualStrings("get-io-spawn-ok", result.stdout); try std.testing.expectEqualStrings("", result.stderr); } test "non-Darwin process I/O keeps the original vtable" { const original = std.testing.io; const selected = process_io_for(.linux, original); try std.testing.expect(selected.userdata == original.userdata); try std.testing.expect(selected.vtable == original.vtable); } test "openDirAbsoluteNoFollow rejects unsafe path components" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(getIo(), "real/child"); try writeTempFile(tmp.dir, "plain-file", "not a directory"); tmp.dir.symLink(std.testing.io, "real", "linked", .{ .is_directory = true }) catch |err| { if (err == error.AccessDenied or err == error.FileSystem) return error.SkipZigTest; return err; }; const root = try dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(root); const linked_child = try std.fs.path.join(alloc, &.{ root, "linked/child" }); defer alloc.free(linked_child); const missing = try std.fs.path.join(alloc, &.{ root, "missing" }); defer alloc.free(missing); const wrong_kind = try std.fs.path.join(alloc, &.{ root, "plain-file" }); defer alloc.free(wrong_kind); if (openDirAbsoluteNoFollow(linked_child, .{})) |dir| { dir.close(getIo()); return error.TestExpectedError; } else |err| switch (err) { error.NotDir, error.SymLinkLoop => {}, else => return err, } try std.testing.expectError(error.FileNotFound, openDirAbsoluteNoFollow(missing, .{})); try std.testing.expectError(error.NotDir, openDirAbsoluteNoFollow(wrong_kind, .{})); } /// Opens an existing regular file without following the final symlink and /// without waiting on a special file that races the initial metadata check. /// The caller owns the returned file. pub fn openExistingRegularFile( dir: std.Io.Dir, sub_path: []const u8, mode: std.Io.Dir.OpenFileOptions.Mode, ) !std.Io.File { const initial = try dir.statFile(getIo(), sub_path, .{ .follow_symlinks = false, }); if (initial.kind != .file or initial.nlink != 1) { return error.DurablePathUnsafe; } if (comptime builtin.os.tag == .windows or builtin.os.tag == .wasi) { var file = try dir.openFile(getIo(), sub_path, .{ .mode = mode, .allow_directory = false, .follow_symlinks = false, }); errdefer file.close(getIo()); const stat = try file.stat(getIo()); try verifyOpenedRegularFile(stat, mode); return file; } var flags: std.posix.O = .{ .ACCMODE = switch (mode) { .read_only => .RDONLY, .write_only => .WRONLY, .read_write => .RDWR, }, .NOFOLLOW = true, .NONBLOCK = true, }; if (@hasField(std.posix.O, "CLOEXEC")) flags.CLOEXEC = true; if (@hasField(std.posix.O, "LARGEFILE")) flags.LARGEFILE = true; if (@hasField(std.posix.O, "NOCTTY")) flags.NOCTTY = true; const fd = std.posix.openat(dir.handle, sub_path, flags, 0) catch |err| switch (err) { error.NoDevice, error.SymLinkLoop, error.NotDir => return error.DurablePathUnsafe, else => return err, }; var file = std.Io.File{ .handle = fd, .flags = .{ .nonblocking = true }, }; errdefer file.close(getIo()); const stat = try file.stat(getIo()); try verifyOpenedRegularFile(stat, mode); try makeFileBlocking(&file); return file; } pub fn verifyOpenedRegularFile( stat: std.Io.File.Stat, mode: std.Io.Dir.OpenFileOptions.Mode, ) !void { if (stat.kind != .file or stat.nlink > 1) { return error.DurablePathUnsafe; } if (mode != .read_only and stat.nlink != 1) { return error.DurablePathUnsafe; } } fn makeFileBlocking(file: *std.Io.File) !void { const current = while (true) { const rc = std.posix.system.fcntl( file.handle, std.posix.F.GETFL, @as(usize, 0), ); switch (std.posix.errno(rc)) { .SUCCESS => break @as(usize, @intCast(rc)), .INTR => continue, else => return error.FileControlFailed, } }; const nonblock = @as(usize, 1) << @bitOffsetOf(std.posix.O, "NONBLOCK"); while (true) { const rc = std.posix.system.fcntl( file.handle, std.posix.F.SETFL, current & ~nonblock, ); switch (std.posix.errno(rc)) { .SUCCESS => break, .INTR => continue, else => return error.FileControlFailed, } } file.flags.nonblocking = false; } test "read-only regular files remain valid when atomic replacement unlinks the descriptor" { if (comptime builtin.os.tag == .windows or builtin.os.tag == .wasi) { return error.SkipZigTest; } const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.writeFile(getIo(), .{ .sub_path = "target", .data = "old" }); var file = try openExistingRegularFile(tmp.dir, "target", .read_only); defer file.close(getIo()); try tmp.dir.writeFile(getIo(), .{ .sub_path = "replacement", .data = "new" }); try tmp.dir.rename("replacement", tmp.dir, "target", getIo()); const stat = try file.stat(getIo()); try std.testing.expectEqual(@as(u64, 0), stat.nlink); try verifyOpenedRegularFile(stat, .read_only); try std.testing.expectError( error.DurablePathUnsafe, verifyOpenedRegularFile(stat, .read_write), ); const bytes = try readFileToEnd(alloc, &file, 16); defer alloc.free(bytes); try std.testing.expectEqualStrings("old", bytes); } pub fn setEnvironMap(m: *const std.process.Environ.Map) void { global_environ = m; global_environ_block = null; global_raw_environ = null; } pub fn setEnvironBlock(block: std.process.Environ.Block) void { global_environ = null; global_environ_block = block; global_raw_environ = null; } pub fn setRawEnviron(raw: RawEnviron) void { global_environ = null; global_environ_block = null; global_raw_environ = raw; } pub fn getenv(key: []const u8) ?[]const u8 { if (global_environ) |m| return m.get(key); if (global_environ_block) |block| return getenvFromBlock(block, key); if (global_raw_environ) |raw| return getenvFromLibc(key) orelse getenvFromRaw(raw, key); return null; } pub fn e2eFailIfDurableMutationAttempted() void { const enabled = getenv("FX_E2E_FAIL_ON_DURABLE_MUTATION") orelse return; if (!std.mem.eql(u8, enabled, "1")) return; std.process.exit(86); } pub fn environMap() ?*const std.process.Environ.Map { return global_environ; } fn getenvFromBlock(block: std.process.Environ.Block, key: []const u8) ?[]const u8 { if (comptime builtin.os.tag == .windows or builtin.os.tag == .freestanding or builtin.os.tag == .other) { return null; } if (comptime (builtin.os.tag == .wasi or builtin.os.tag == .emscripten) and !builtin.link_libc) { return null; } const view = block.view(); for (view.slice) |entry_z| { const entry = std.mem.sliceTo(entry_z, 0); if (entry.len <= key.len or entry[key.len] != '=') continue; if (std.mem.eql(u8, entry[0..key.len], key)) return entry[key.len + 1 ..]; } return null; } fn getenvFromRaw(raw: RawEnviron, key: []const u8) ?[]const u8 { @setRuntimeSafety(false); var i: usize = 0; while (raw[i]) |entry_z| : (i += 1) { const entry = std.mem.sliceTo(entry_z, 0); if (entry.len <= key.len or entry[key.len] != '=') continue; if (std.mem.eql(u8, entry[0..key.len], key)) return entry[key.len + 1 ..]; } return null; } fn getenvFromLibc(key: []const u8) ?[]const u8 { if (comptime !builtin.link_libc) return null; if (key.len >= 128) return null; var key_buf: [128]u8 = undefined; @memcpy(key_buf[0..key.len], key); key_buf[key.len] = 0; const value_z = std.c.getenv(key_buf[0..key.len :0].ptr) orelse return null; return std.mem.sliceTo(value_z, 0); } pub fn readFileToEnd(alloc: std.mem.Allocator, file: *std.Io.File, max_bytes: usize) ![]u8 { const zio = getIo(); var read_buf: [8192]u8 = undefined; var r = file.reader(zio, &read_buf); return r.interface.allocRemaining(alloc, std.Io.Limit.limited(max_bytes)); } pub fn readFileToEndZ(alloc: std.mem.Allocator, file: *std.Io.File, max_bytes: usize) ![:0]u8 { const data = try readFileToEnd(alloc, file, max_bytes); errdefer alloc.free(data); const result = try alloc.realloc(data, data.len + 1); result[data.len] = 0; return result[0..data.len :0]; } pub fn milliTimestamp() i64 { const ts = std.Io.Timestamp.now(getIo(), .real); return @intCast(@divFloor(ts.nanoseconds, 1_000_000)); } pub fn nanoTimestamp() i128 { const ts = std.Io.Timestamp.now(getIo(), .real); return @intCast(ts.nanoseconds); } pub fn writeFileAtomic(alloc: std.mem.Allocator, path: []const u8, text: []const u8) !void { e2eFailIfDurableMutationAttempted(); const maybe_existing_permissions = existingFilePermissions(path); if (maybe_existing_permissions) |existing_permissions| { if (existing_permissions.toMode() & 0o222 == 0) return error.AccessDenied; } const permissions = maybe_existing_permissions orelse .default_file; const temp_path = try std.fmt.allocPrint(alloc, "{s}.tmp.{d}", .{ path, nanoTimestamp() }); defer alloc.free(temp_path); var cleanup_temp = true; defer if (cleanup_temp) std.Io.Dir.deleteFileAbsolute(getIo(), temp_path) catch {}; { var file = try std.Io.Dir.createFileAbsolute(getIo(), temp_path, .{ .truncate = true, .permissions = permissions }); defer file.close(getIo()); try file.writeStreamingAll(getIo(), text); try file.sync(getIo()); } try std.Io.Dir.renameAbsolute(temp_path, path, getIo()); cleanup_temp = false; } const private_dir_permissions = std.Io.File.Permissions.fromMode(0o700); const private_file_permissions = std.Io.File.Permissions.fromMode(0o600); pub const VerifiedDir = struct { dir: std.Io.Dir, pub fn close(self: *VerifiedDir) void { self.dir.close(getIo()); self.* = undefined; } }; fn defaultSyncFile(_: ?*anyopaque, file: std.Io.File) anyerror!void { try file.sync(getIo()); } fn defaultSyncDir(_: ?*anyopaque, dir: std.Io.Dir) anyerror!void { try syncVerifiedDir(dir); } pub const DurableOps = struct { ctx: ?*anyopaque = null, sync_file: *const fn (?*anyopaque, std.Io.File) anyerror!void = defaultSyncFile, sync_dir: *const fn (?*anyopaque, std.Io.Dir) anyerror!void = defaultSyncDir, }; fn defaultTryLock(_: ?*anyopaque, file: std.Io.File) anyerror!bool { return file.tryLock(getIo(), .exclusive); } fn defaultLockNow(_: ?*anyopaque) i64 { return milliTimestamp(); } fn defaultLockSleep(_: ?*anyopaque, millis: u64) void { sleep(millis * std.time.ns_per_ms); } pub const LockOps = struct { ctx: ?*anyopaque = null, try_lock: *const fn (?*anyopaque, std.Io.File) anyerror!bool = defaultTryLock, now_ms: *const fn (?*anyopaque) i64 = defaultLockNow, sleep_ms: *const fn (?*anyopaque, u64) void = defaultLockSleep, }; pub const TimedAdvisoryLock = struct { file: std.Io.File, pub fn release(self: *TimedAdvisoryLock) void { self.file.unlock(getIo()); self.file.close(getIo()); self.* = undefined; } }; fn validateRelativeLeaf(name: []const u8) !void { if (name.len == 0 or std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) { return error.DurablePathUnsafe; } if (std.mem.indexOfAny(u8, name, "/\\") != null) return error.DurablePathUnsafe; } fn verifyPrivateRegularFile(file: std.Io.File) !void { const stat = try file.stat(getIo()); if (stat.kind != .file or stat.nlink != 1) return error.DurablePathUnsafe; if (stat.permissions.toMode() & 0o777 != 0o600) return error.PrivateStatePermissionsUnsupported; } fn verifyPrivateDirectory(dir: std.Io.Dir) !void { const stat = try dir.stat(getIo()); if (stat.kind != .directory) return error.DurablePathUnsafe; if (stat.permissions.toMode() & 0o777 != 0o700) return error.PrivateStatePermissionsUnsupported; } /// The handle must come from an `openDir` that requested iteration. Linux returns an /// `O_PATH` descriptor otherwise, and `fsync` rejects those with `EBADF`. pub fn syncVerifiedDir(dir: std.Io.Dir) !void { if (comptime builtin.os.tag == .windows) return error.OperationUnsupported; while (true) { const rc = std.c.fsync(dir.handle); if (rc == 0) return; switch (std.c.errno(rc)) { .INTR => continue, .INVAL, .OPNOTSUPP => return error.OperationUnsupported, .NOSPC => return error.NoSpaceLeft, .DQUOT => return error.DiskQuota, .ROFS => return error.ReadOnlyFileSystem, else => return error.DirectorySyncFailed, } } } fn openOrCreateVerifiedPrivateChild(parent: std.Io.Dir, name: []const u8) !VerifiedDir { e2eFailIfDurableMutationAttempted(); try validateRelativeLeaf(name); const zio = getIo(); var created = false; var dir = parent.openDir(zio, name, .{ .iterate = true,