io_v2_paths.zig (5821B)
1 //! Private synchronous path operations. Observations do not pin subsequent path 2 //! operations. OS process authority, no grants, sandbox or implicit rollback. 3 const std = @import("std"); 4 const F = @import("io_v2_files.zig"); 5 const c = @cImport({ 6 @cUndef("_FORTIFY_SOURCE"); 7 @cDefine("_FORTIFY_SOURCE", "0"); 8 @cInclude("uv.h"); 9 @cInclude("sys/stat.h"); 10 @cInclude("fcntl.h"); 11 }); 12 pub const Error = F.Error || error{NotDirectory}; 13 pub const Info = union(enum) { regular: u64, directory, symlink, other }; 14 pub const Mutation = enum { create_directory, remove_directory, remove_file, rename }; 15 fn valid(path: [:0]const u8) Error!void { 16 if (path.len == 0 or std.mem.indexOfScalar(u8, path, 0) != null or !std.unicode.utf8ValidateSlice(path)) return error.InvalidArgument; 17 } 18 fn checked(status: *F.Status, code: c_int) Error!void { 19 if (code < 0) { 20 status.code = code; 21 return error.HostFailure; 22 } 23 } 24 pub fn stat(status: *F.Status, path: [:0]const u8, follow: bool) Error!Info { 25 status.* = .{}; 26 try valid(path); 27 var req: c.uv_fs_t = undefined; 28 const rc = if (follow) c.uv_fs_stat(null, &req, path.ptr, null) else c.uv_fs_lstat(null, &req, path.ptr, null); 29 defer c.uv_fs_req_cleanup(&req); 30 try checked(status, rc); 31 return switch (req.statbuf.st_mode & c.S_IFMT) { 32 c.S_IFREG => if (req.statbuf.st_size <= F.limit) .{ .regular = req.statbuf.st_size } else error.InvalidBound, 33 c.S_IFDIR => .directory, 34 c.S_IFLNK => .symlink, 35 else => .other, 36 }; 37 } 38 pub fn mutate(status: *F.Status, op: Mutation, path: [:0]const u8, destination: ?[:0]const u8) Error!void { 39 status.* = .{}; 40 try valid(path); 41 if (op == .rename) try valid(destination orelse return error.InvalidArgument); 42 var req: c.uv_fs_t = undefined; 43 const rc = switch (op) { 44 .create_directory => c.uv_fs_mkdir(null, &req, path.ptr, 0o777, null), 45 .remove_directory => c.uv_fs_rmdir(null, &req, path.ptr, null), 46 .remove_file => c.uv_fs_unlink(null, &req, path.ptr, null), 47 .rename => c.uv_fs_rename(null, &req, path.ptr, destination.?.ptr, null), 48 }; 49 defer c.uv_fs_req_cleanup(&req); 50 try checked(status, rc); 51 } 52 pub fn syncDirectory(status: *F.Status, path: [:0]const u8) Error!void { 53 status.* = .{}; 54 try valid(path); 55 var req: c.uv_fs_t = undefined; 56 // NONBLOCK plus O_DIRECTORY avoids opening a FIFO while waiting for peers. 57 const fd = c.uv_fs_open(null, &req, path.ptr, c.O_RDONLY | c.O_NONBLOCK | c.O_DIRECTORY | c.O_CLOEXEC | c.O_NOCTTY, 0, null); 58 c.uv_fs_req_cleanup(&req); 59 if (fd == c.UV_ENOTDIR) return error.NotDirectory; 60 try checked(status, fd); 61 // Close exactly once on every path; retain a primary sync/fstat error. 62 const rc = c.uv_fs_fstat(null, &req, fd, null); 63 const is_directory = rc >= 0 and req.statbuf.st_mode & c.S_IFMT == c.S_IFDIR; 64 c.uv_fs_req_cleanup(&req); 65 const synced = if (is_directory) blk: { 66 const result = c.uv_fs_fsync(null, &req, fd, null); 67 c.uv_fs_req_cleanup(&req); 68 break :blk result; 69 } else 0; 70 const closed = c.uv_fs_close(null, &req, fd, null); 71 c.uv_fs_req_cleanup(&req); 72 if (closed < 0) status.cleanup_code = closed; 73 try checked(status, rc); 74 if (!is_directory) return error.NotDirectory; 75 try checked(status, synced); 76 try checked(status, closed); 77 } 78 79 test "path observations, explicit symlink following, rename/unlink and directory durability" { 80 const t = std.testing; 81 var tmp = t.tmpDir(.{}); 82 defer tmp.cleanup(); 83 const base = try tmp.dir.realPathFileAlloc(t.io, ".", t.allocator); 84 defer t.allocator.free(base); 85 const dir = try std.fmt.allocPrintSentinel(t.allocator, "{s}/dir", .{base}, 0); 86 defer t.allocator.free(dir); 87 const source = try std.fmt.allocPrintSentinel(t.allocator, "{s}/file", .{base}, 0); 88 defer t.allocator.free(source); 89 const target = try std.fmt.allocPrintSentinel(t.allocator, "{s}/dir/file", .{base}, 0); 90 defer t.allocator.free(target); 91 const link = try std.fmt.allocPrintSentinel(t.allocator, "{s}/link", .{base}, 0); 92 defer t.allocator.free(link); 93 var status: F.Status = .{}; 94 try mutate(&status, .create_directory, dir, null); 95 try t.expect(try stat(&status, dir, true) == .directory); 96 try t.expectError(error.HostFailure, mutate(&status, .create_directory, dir, null)); 97 try t.expectEqualStrings("already-exists", F.errorCategory(status.code)); 98 var file = try F.open(&status, source, .writer); 99 defer file.close(&status) catch {}; 100 _ = try file.write(&status, "abc"); 101 var req: c.uv_fs_t = undefined; 102 const linked = c.uv_fs_symlink(null, &req, source.ptr, link.ptr, 0, null); 103 c.uv_fs_req_cleanup(&req); 104 try t.expectEqual(@as(c_int, 0), linked); 105 try t.expect(try stat(&status, link, false) == .symlink); 106 try t.expectEqual(@as(u64, 3), (try stat(&status, link, true)).regular); 107 try mutate(&status, .rename, source, target); 108 try t.expectError(error.HostFailure, stat(&status, source, true)); 109 try t.expectEqualStrings("not-found", F.errorCategory(status.code)); 110 try t.expectEqual(@as(u64, 3), (try stat(&status, target, true)).regular); 111 try syncDirectory(&status, dir); 112 try t.expectError(error.NotDirectory, syncDirectory(&status, target)); 113 try t.expectError(error.HostFailure, mutate(&status, .remove_directory, dir, null)); 114 try mutate(&status, .remove_file, target, null); 115 try mutate(&status, .remove_directory, dir, null); 116 try mutate(&status, .remove_file, link, null); 117 try t.expectError(error.InvalidArgument, mutate(&status, .rename, source, null)); 118 try t.expectError(error.InvalidArgument, stat(&status, "x\x00y", true)); 119 try t.expectError(error.InvalidArgument, syncDirectory(&status, "")); 120 }