diff --git a/lib/std/cache_hash.zig b/lib/std/cache_hash.zig index d160c4ebb22b..a8d1ca37e1d5 100644 --- a/lib/std/cache_hash.zig +++ b/lib/std/cache_hash.zig @@ -1,10 +1,12 @@ -const std = @import("std.zig"); +const builtin = @import("builtin"); +const std = @import("std"); const Blake3 = std.crypto.Blake3; const fs = std.fs; const base64 = std.base64; const ArrayList = std.ArrayList; const assert = std.debug.assert; const testing = std.testing; +const tmpDir = testing.tmpDir; const mem = std.mem; const fmt = std.fmt; const Allocator = std.mem.Allocator; @@ -41,6 +43,7 @@ pub const File = struct { pub const CacheHash = struct { allocator: *Allocator, blake3: Blake3, + work_dir: fs.Dir, manifest_dir: fs.Dir, manifest_file: ?fs.File, manifest_dirty: bool, @@ -48,11 +51,12 @@ pub const CacheHash = struct { b64_digest: [BASE64_DIGEST_LEN]u8, /// Be sure to call release after successful initialization. - pub fn init(allocator: *Allocator, dir: fs.Dir, manifest_dir_path: []const u8) !CacheHash { + pub fn init(allocator: *Allocator, work_dir: fs.Dir, manifest_dir_path: []const u8) !CacheHash { return CacheHash{ .allocator = allocator, .blake3 = Blake3.init(), - .manifest_dir = try dir.makeOpenPath(manifest_dir_path, .{}), + .work_dir = work_dir, + .manifest_dir = try work_dir.makeOpenPath(manifest_dir_path, .{}), .manifest_file = null, .manifest_dirty = false, .files = ArrayList(File).init(allocator), @@ -100,7 +104,13 @@ pub const CacheHash = struct { assert(self.manifest_file == null); try self.files.ensureCapacity(self.files.items.len + 1); - const resolved_path = try fs.path.resolve(self.allocator, &[_][]const u8{file_path}); + + var resolved_path: []u8 = undefined; + if (builtin.os.tag == .wasi) { + resolved_path = (try fs.path.resolveRelative(self.allocator, &[_][]const u8{file_path})) orelse unreachable; + } else { + resolved_path = try fs.path.resolveat(self.allocator, self.work_dir, &[_][]const u8{file_path}); + } const idx = self.files.items.len; self.files.addOneAssumeCapacity().* = .{ @@ -210,7 +220,7 @@ pub const CacheHash = struct { cache_hash_file.path = try mem.dupe(self.allocator, u8, file_path); } - const this_file = fs.cwd().openFile(cache_hash_file.path.?, .{ .read = true }) catch { + const this_file = self.work_dir.openFile(cache_hash_file.path.?, .{ .read = true }) catch { return error.CacheUnavailable; }; defer this_file.close(); @@ -276,7 +286,7 @@ pub const CacheHash = struct { } fn populateFileHash(self: *CacheHash, ch_file: *File) !void { - const file = try fs.cwd().openFile(ch_file.path.?, .{}); + const file = try self.work_dir.openFile(ch_file.path.?, .{}); defer file.close(); ch_file.stat = try file.stat(); @@ -322,7 +332,12 @@ pub const CacheHash = struct { pub fn addFilePostFetch(self: *CacheHash, file_path: []const u8, max_file_size: usize) ![]u8 { assert(self.manifest_file != null); - const resolved_path = try fs.path.resolve(self.allocator, &[_][]const u8{file_path}); + var resolved_path: []u8 = undefined; + if (builtin.os.tag == .wasi) { + resolved_path = (try fs.path.resolveRelative(self.allocator, &[_][]const u8{file_path})) orelse unreachable; + } else { + resolved_path = try fs.path.resolveat(self.allocator, self.work_dir, &[_][]const u8{file_path}); + } errdefer self.allocator.free(resolved_path); const new_ch_file = try self.files.addOne(); @@ -347,7 +362,12 @@ pub const CacheHash = struct { pub fn addFilePost(self: *CacheHash, file_path: []const u8) !void { assert(self.manifest_file != null); - const resolved_path = try fs.path.resolve(self.allocator, &[_][]const u8{file_path}); + var resolved_path: []u8 = undefined; + if (builtin.os.tag == .wasi) { + resolved_path = (try fs.path.resolveRelative(self.allocator, &[_][]const u8{file_path})) orelse unreachable; + } else { + resolved_path = try fs.path.resolveat(self.allocator, self.work_dir, &[_][]const u8{file_path}); + } errdefer self.allocator.free(resolved_path); const new_ch_file = try self.files.addOne(); @@ -470,16 +490,13 @@ fn isProblematicTimestamp(fs_clock: i128) bool { } test "cache file and then recall it" { - if (std.Target.current.os.tag == .wasi) { - // https://github.com/ziglang/zig/issues/5437 - return error.SkipZigTest; - } - const cwd = fs.cwd(); + var tmp = tmpDir(.{}); + defer tmp.cleanup(); const temp_file = "test.txt"; const temp_manifest_dir = "temp_manifest_dir"; - try cwd.writeFile(temp_file, "Hello, world!\n"); + try tmp.dir.writeFile(temp_file, "Hello, world!\n"); while (isProblematicTimestamp(std.time.nanoTimestamp())) { std.time.sleep(1); @@ -489,7 +506,7 @@ test "cache file and then recall it" { var digest2: [BASE64_DIGEST_LEN]u8 = undefined; { - var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir); + var ch = try CacheHash.init(testing.allocator, tmp.dir, temp_manifest_dir); defer ch.release(); ch.add(true); @@ -503,7 +520,7 @@ test "cache file and then recall it" { digest1 = ch.final(); } { - var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir); + var ch = try CacheHash.init(testing.allocator, tmp.dir, temp_manifest_dir); defer ch.release(); ch.add(true); @@ -517,8 +534,8 @@ test "cache file and then recall it" { testing.expectEqual(digest1, digest2); - try cwd.deleteTree(temp_manifest_dir); - try cwd.deleteFile(temp_file); + try tmp.dir.deleteTree(temp_manifest_dir); + try tmp.dir.deleteFile(temp_file); } test "give problematic timestamp" { @@ -534,18 +551,15 @@ test "give nonproblematic timestamp" { } test "check that changing a file makes cache fail" { - if (std.Target.current.os.tag == .wasi) { - // https://github.com/ziglang/zig/issues/5437 - return error.SkipZigTest; - } - const cwd = fs.cwd(); + var tmp = tmpDir(.{}); + defer tmp.cleanup(); const temp_file = "cache_hash_change_file_test.txt"; const temp_manifest_dir = "cache_hash_change_file_manifest_dir"; const original_temp_file_contents = "Hello, world!\n"; const updated_temp_file_contents = "Hello, world; but updated!\n"; - try cwd.writeFile(temp_file, original_temp_file_contents); + try tmp.dir.writeFile(temp_file, original_temp_file_contents); while (isProblematicTimestamp(std.time.nanoTimestamp())) { std.time.sleep(1); @@ -555,7 +569,7 @@ test "check that changing a file makes cache fail" { var digest2: [BASE64_DIGEST_LEN]u8 = undefined; { - var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir); + var ch = try CacheHash.init(testing.allocator, tmp.dir, temp_manifest_dir); defer ch.release(); ch.add("1234"); @@ -569,14 +583,14 @@ test "check that changing a file makes cache fail" { digest1 = ch.final(); } - try cwd.writeFile(temp_file, updated_temp_file_contents); + try tmp.dir.writeFile(temp_file, updated_temp_file_contents); while (isProblematicTimestamp(std.time.nanoTimestamp())) { std.time.sleep(1); } { - var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir); + var ch = try CacheHash.init(testing.allocator, tmp.dir, temp_manifest_dir); defer ch.release(); ch.add("1234"); @@ -593,24 +607,22 @@ test "check that changing a file makes cache fail" { testing.expect(!mem.eql(u8, digest1[0..], digest2[0..])); - try cwd.deleteTree(temp_manifest_dir); - try cwd.deleteFile(temp_file); + try tmp.dir.deleteTree(temp_manifest_dir); + try tmp.dir.deleteFile(temp_file); } test "no file inputs" { - if (std.Target.current.os.tag == .wasi) { - // https://github.com/ziglang/zig/issues/5437 - return error.SkipZigTest; - } - const cwd = fs.cwd(); + var tmp = tmpDir(.{}); + defer tmp.cleanup(); + const temp_manifest_dir = "no_file_inputs_manifest_dir"; - defer cwd.deleteTree(temp_manifest_dir) catch unreachable; + defer tmp.dir.deleteTree(temp_manifest_dir) catch unreachable; var digest1: [BASE64_DIGEST_LEN]u8 = undefined; var digest2: [BASE64_DIGEST_LEN]u8 = undefined; { - var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir); + var ch = try CacheHash.init(testing.allocator, tmp.dir, temp_manifest_dir); defer ch.release(); ch.add("1234"); @@ -621,7 +633,7 @@ test "no file inputs" { digest1 = ch.final(); } { - var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir); + var ch = try CacheHash.init(testing.allocator, tmp.dir, temp_manifest_dir); defer ch.release(); ch.add("1234"); @@ -633,18 +645,15 @@ test "no file inputs" { } test "CacheHashes with files added after initial hash work" { - if (std.Target.current.os.tag == .wasi) { - // https://github.com/ziglang/zig/issues/5437 - return error.SkipZigTest; - } - const cwd = fs.cwd(); + var tmp = tmpDir(.{}); + defer tmp.cleanup(); const temp_file1 = "cache_hash_post_file_test1.txt"; const temp_file2 = "cache_hash_post_file_test2.txt"; const temp_manifest_dir = "cache_hash_post_file_manifest_dir"; - try cwd.writeFile(temp_file1, "Hello, world!\n"); - try cwd.writeFile(temp_file2, "Hello world the second!\n"); + try tmp.dir.writeFile(temp_file1, "Hello, world!\n"); + try tmp.dir.writeFile(temp_file2, "Hello world the second!\n"); while (isProblematicTimestamp(std.time.nanoTimestamp())) { std.time.sleep(1); @@ -655,7 +664,7 @@ test "CacheHashes with files added after initial hash work" { var digest3: [BASE64_DIGEST_LEN]u8 = undefined; { - var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir); + var ch = try CacheHash.init(testing.allocator, tmp.dir, temp_manifest_dir); defer ch.release(); ch.add("1234"); @@ -669,7 +678,7 @@ test "CacheHashes with files added after initial hash work" { digest1 = ch.final(); } { - var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir); + var ch = try CacheHash.init(testing.allocator, tmp.dir, temp_manifest_dir); defer ch.release(); ch.add("1234"); @@ -680,14 +689,14 @@ test "CacheHashes with files added after initial hash work" { testing.expect(mem.eql(u8, &digest1, &digest2)); // Modify the file added after initial hash - try cwd.writeFile(temp_file2, "Hello world the second, updated\n"); + try tmp.dir.writeFile(temp_file2, "Hello world the second, updated\n"); while (isProblematicTimestamp(std.time.nanoTimestamp())) { std.time.sleep(1); } { - var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir); + var ch = try CacheHash.init(testing.allocator, tmp.dir, temp_manifest_dir); defer ch.release(); ch.add("1234"); @@ -703,7 +712,7 @@ test "CacheHashes with files added after initial hash work" { testing.expect(!mem.eql(u8, &digest1, &digest3)); - try cwd.deleteTree(temp_manifest_dir); - try cwd.deleteFile(temp_file1); - try cwd.deleteFile(temp_file2); + try tmp.dir.deleteTree(temp_manifest_dir); + try tmp.dir.deleteFile(temp_file1); + try tmp.dir.deleteFile(temp_file2); } diff --git a/lib/std/fs/path.zig b/lib/std/fs/path.zig index 34326f28726b..b97bc935a199 100644 --- a/lib/std/fs/path.zig +++ b/lib/std/fs/path.zig @@ -652,6 +652,74 @@ pub fn resolvePosix(allocator: *Allocator, paths: []const []const u8) ![]u8 { return allocator.shrink(result, result_index); } +pub const ResolveRelativeError = error{AbsolutePath} || Allocator.Error; + +/// This function is like a series of `cd` statements executed one after another. +/// It resolves "." and ".." relative to ".". +/// The result does not have a trailing path separator. +/// It does not dereference symlinks, and always returns a relative path. If the +/// resolution leads to ".", returns null instead. +pub fn resolveRelative(allocator: *Allocator, paths: []const []const u8) ResolveRelativeError!?[]u8 { + var max_size: usize = 0; + for (paths) |p, i| { + if (isAbsolute(p)) { + return ResolveRelativeError.AbsolutePath; + } + max_size += p.len + 1; + } + + var result_index: usize = 0; + var result = try allocator.alloc(u8, max_size); + errdefer allocator.free(result); + + for (paths) |p, i| { + var it = mem.tokenize(p, "/"); + while (it.next()) |component| { + if (mem.eql(u8, component, ".")) { + continue; + } else if (mem.eql(u8, component, "..")) { + while (true) { + if (result_index == 0) + break; + result_index -= 1; + if (result[result_index] == '/') + break; + } + } else { + if (result_index > 0) { + result[result_index] = '/'; + result_index += 1; + } + mem.copy(u8, result[result_index..], component); + result_index += component.len; + } + } + } + + if (result_index == 0) { + allocator.free(result); + return null; + } + + return allocator.shrink(result, result_index); +} + +/// Similar to `resolve`, however, resolves the paths with respect to the provided +/// `Dir` handle. +/// Unlike `resolve`, will perform one syscall to get the path of the +/// specified `dir` handle. +pub fn resolveat(allocator: *Allocator, dir: fs.Dir, paths: []const []const u8) ![]u8 { + var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; + const dir_path = try std.os.realpathat(dir.fd, "", &buffer); + + var new_paths = std.ArrayList([]const u8).init(allocator); + defer new_paths.deinit(); + try new_paths.append(dir_path); + try new_paths.appendSlice(paths); + + return resolve(allocator, new_paths.items); +} + test "resolve" { if (builtin.os.tag == .wasi) return error.SkipZigTest; @@ -744,6 +812,45 @@ fn testResolvePosix(paths: []const []const u8, expected: []const u8) !void { return testing.expect(mem.eql(u8, actual, expected)); } +test "resolveRelative" { + try testResolveRelative(&[_][]const u8{ "a/b", "c" }, "a/b/c"); + try testResolveRelative(&[_][]const u8{ "a/b", "./../" }, "a"); + try testResolveRelative(&[_][]const u8{ "a/b", "..", ".." }, null); + try testResolveRelative(&[_][]const u8{"./"}, null); + try testResolveRelative(&[_][]const u8{ "..", "../" }, null); + try testResolveRelative(&[_][]const u8{ "./a", "/" }, ResolveRelativeError.AbsolutePath); +} + +fn testResolveRelative(paths: []const []const u8, expected: var) !void { + const res = resolveRelative(testing.allocator, paths); + + if (@typeInfo(@TypeOf(expected)) == .ErrorSet) { + return testing.expectError(expected, res); + } + + const actual = try res; + defer if (actual) |a| testing.allocator.free(a); + + return switch (@typeInfo(@TypeOf(expected))) { + .Null => testing.expectEqual(actual, expected), + else => |_| testing.expect(mem.eql(u8, actual.?, expected)), + }; +} + +test "resolveat" { + if (builtin.os.tag == .wasi) return error.SkipZigTest; + + const cwd = fs.cwd(); + + const a = try resolveat(testing.allocator, cwd, &[_][]const u8{}); + defer testing.allocator.free(a); + + const b = try resolve(testing.allocator, &[_][]const u8{}); + defer testing.allocator.free(b); + + testing.expect(mem.eql(u8, a, b)); +} + /// If the path is a file in the current directory (no directory component) /// then returns null pub fn dirname(path: []const u8) ?[]const u8 { diff --git a/lib/std/os.zig b/lib/std/os.zig index 0558390b9e9a..989eff455fd6 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -3793,10 +3793,7 @@ pub fn realpathZ(pathname: [*:0]const u8, out_buffer: *[MAX_PATH_BYTES]u8) RealP }; defer close(fd); - var procfs_buf: ["/proc/self/fd/-2147483648".len:0]u8 = undefined; - const proc_path = std.fmt.bufPrint(procfs_buf[0..], "/proc/self/fd/{}\x00", .{fd}) catch unreachable; - - return readlinkZ(@ptrCast([*:0]const u8, proc_path.ptr), out_buffer); + return fdPath(fd, out_buffer); } const result_path = std.c.realpath(pathname, out_buffer) orelse switch (std.c._errno().*) { EINVAL => unreachable, @@ -3828,17 +3825,93 @@ pub fn realpathW(pathname: [*:0]const u16, out_buffer: *[MAX_PATH_BYTES]u8) Real ); defer windows.CloseHandle(h_file); - var wide_buf: [windows.PATH_MAX_WIDE]u16 = undefined; - const wide_slice = try windows.GetFinalPathNameByHandleW(h_file, &wide_buf, wide_buf.len, windows.VOLUME_NAME_DOS); + return fdPath(h_file, out_buffer); +} + +fn fdPath(fd: fd_t, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 { + switch (builtin.os.tag) { + .wasi => @compileError("fdPath is unsupported in WASI"), + .windows => { + var wide_buf: [windows.PATH_MAX_WIDE]u16 = undefined; + const wide_slice = try windows.GetFinalPathNameByHandleW(fd, &wide_buf, wide_buf.len, windows.VOLUME_NAME_DOS); + + // Windows returns \\?\ prepended to the path. + // We strip it to make this function consistent across platforms. + const prefix = [_]u16{ '\\', '\\', '?', '\\' }; + const start_index = if (mem.startsWith(u16, wide_slice, &prefix)) prefix.len else 0; + + // Trust that Windows gives us valid UTF-16LE. + const end_index = std.unicode.utf16leToUtf8(out_buffer, wide_slice[start_index..]) catch unreachable; + return out_buffer[0..end_index]; + }, + .macosx, .ios, .watchos, .tvos => { + // On macOS, we can use F_GETPATH fcntl command to query the OS for + // the path to the file descriptor. + @memset(out_buffer, 0, MAX_PATH_BYTES); + switch (errno(system.fcntl(fd, F_GETPATH, out_buffer))) { + 0 => {}, + EBADF => return error.FileNotFound, + // TODO man pages for fcntl on macOS don't really tell you what + // errno values to expect when command is F_GETPATH... + else => |err| return unexpectedErrno(err), + } + const len = mem.indexOfScalar(u8, out_buffer[0..], @as(u8, 0)) orelse MAX_PATH_BYTES; + return out_buffer[0..len]; + }, + else => { + var procfs_buf: ["/proc/self/fd/-2147483648".len:0]u8 = undefined; + const proc_path = std.fmt.bufPrint(procfs_buf[0..], "/proc/self/fd/{}\x00", .{fd}) catch unreachable; + + return readlinkZ(@ptrCast([*:0]const u8, proc_path.ptr), out_buffer); + }, + } +} + +/// Similar to `realpath`, however, returns the canonicalized absolute pathname of +/// a `pathname` relative to a file descriptor `fd`. +/// If `pathname` is an absolute path, ignores `fd` and reverts to calling +/// `realpath` on the `pathname` argument. +/// In Unix, if `fd` was obtained using `std.fs.cwd()` call, reverts to calling +/// `std.os.getcwd()` to obtain file descriptor's path. +pub fn realpathat(fd: fd_t, pathname: []const u8, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 { + if (builtin.os.tag == .wasi) { + @compileError("realpathat is unsupported in WASI"); + } + if (std.fs.path.isAbsolute(pathname)) { + return realpath(pathname, out_buffer); + } + + var buffer: [MAX_PATH_BYTES]u8 = undefined; + var fd_path: []const u8 = undefined; + if (@hasDecl(@This(), "AT_FDCWD")) { + if (fd == @field(@This(), "AT_FDCWD")) { + fd_path = getcwd(buffer[0..]) catch |err| { + return switch (err) { + GetCwdError.NameTooLong => error.NameTooLong, + GetCwdError.CurrentWorkingDirectoryUnlinked => error.FileNotFound, + else => |e| e, + }; + }; + } else { + fd_path = try fdPath(fd, &buffer); + } + } else { + fd_path = try fdPath(fd, &buffer); + } + + const total_len = fd_path.len + pathname.len + 1; // +1 to account for path separator + if (total_len >= MAX_PATH_BYTES) { + return error.NameTooLong; + } + + const sep = if (builtin.os.tag == .windows) '\\' else '/'; - // Windows returns \\?\ prepended to the path. - // We strip it to make this function consistent across platforms. - const prefix = [_]u16{ '\\', '\\', '?', '\\' }; - const start_index = if (mem.startsWith(u16, wide_slice, &prefix)) prefix.len else 0; + var unnormalized: [MAX_PATH_BYTES]u8 = undefined; + mem.copy(u8, unnormalized[0..], fd_path); + unnormalized[fd_path.len] = sep; + mem.copy(u8, unnormalized[fd_path.len + 1 ..], pathname); - // Trust that Windows gives us valid UTF-16LE. - const end_index = std.unicode.utf16leToUtf8(out_buffer, wide_slice[start_index..]) catch unreachable; - return out_buffer[0..end_index]; + return realpath(unnormalized[0..total_len], out_buffer); } /// Spurious wakeups are possible and no precision of timing is guaranteed. diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index cc3b4f574152..f34fa706b1bf 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -265,6 +265,30 @@ test "realpath" { testing.expectError(error.FileNotFound, fs.realpath("definitely_bogus_does_not_exist1234", &buf)); } +test "realpathat" { + if (builtin.os.tag == .wasi) return error.SkipZigTest; + + const cwd = fs.cwd(); + + var buf1: [fs.MAX_PATH_BYTES]u8 = undefined; + const cwd_path = try os.getcwd(&buf1); + + var buf2: [fs.MAX_PATH_BYTES]u8 = undefined; + const cwd_realpathat = try os.realpathat(cwd.fd, "", &buf2); + testing.expect(mem.eql(u8, cwd_path, cwd_realpathat)); + + // Now, open an actual Dir{"."} since on Unix `realpathat` behaves + // in a special way when `fd` equals `cwd.fd` + var dir = try cwd.openDir(".", .{}); + defer dir.close(); + + const cwd_realpathat2 = try os.realpathat(dir.fd, "", &buf2); + testing.expect(mem.eql(u8, cwd_path, cwd_realpathat2)); + + // Finally, try getting a path for something that doesn't exist + testing.expectError(error.FileNotFound, os.realpathat(dir.fd, "definitely_bogus_does_not_exist1234", &buf2)); +} + test "sigaltstack" { if (builtin.os.tag == .windows or builtin.os.tag == .wasi) return error.SkipZigTest;