From 536fd4b85137e026cc68a7ac4d2a2320bcf461bb Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Fri, 19 Jun 2020 09:05:34 +0200 Subject: [PATCH 1/5] Add realpathat fn `std.os.realpathat` is similar to `std.os.realpath`, however, it accepts a tuple `(fd_t, []const u8)` of args and thus works out the realpath of a relative path wrt to some opened file descriptor. If the input pathname argument turns out to be an absolute path, this function reverts to calling `realpath` on that pathname completely ignoring the input file descriptor. This behaviour is standard in POSIX and IMHO a good rule of thumb to follow. If the input file descriptor was obtained using `std.fs.cwd()` call, this function reverts to `std.os.getcwd()` to obtain the file descriptor's path. --- lib/std/os.zig | 99 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 86 insertions(+), 13 deletions(-) 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. From 5635a407d8d8edd2a3a699bfeaf9f65dccf8701f Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Fri, 19 Jun 2020 09:12:31 +0200 Subject: [PATCH 2/5] Add unit test --- lib/std/os/test.zig | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index cc3b4f574152..62494303d510 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -265,6 +265,29 @@ 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)); + const cwd_realpathat2 = try os.realpathat(cwd.fd, ".", &buf2); // shouldn't change a thing + testing.expect(mem.eql(u8, cwd_path, cwd_realpathat2)); + + // Now, open an actual Dir{"."} since on Unix `realpathat` behaves + // in a special way when `fd` equals `cwd.fd` + const dir = try cwd.openDir(".", .{}); + const cwd_realpathat3 = try os.realpathat(dir.fd, "", &buf2); + testing.expect(mem.eql(u8, cwd_path, cwd_realpathat3)); + const cwd_realpathat4 = try os.realpathat(dir.fd, ".", &buf2); // shouldn't change a thing + testing.expect(mem.eql(u8, cwd_path, cwd_realpathat4)); +} + test "sigaltstack" { if (builtin.os.tag == .windows or builtin.os.tag == .wasi) return error.SkipZigTest; From 93b1a8f1989f3501b0a4e11dd7b727a582e02c04 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Fri, 19 Jun 2020 17:01:51 +0200 Subject: [PATCH 3/5] Add std.fs.path.resolveat fn and use it in CacheHash This commit adds `std.fs.path.resolveat` functions which is similar to `std.fs.path.resolve`, however, differs in the sense that, in addition to a slice of paths, it also accepts a `Dir` handle with respect to which the paths should be resolved. It also issues one syscall in order to workout the path to the `Dir` handle. With `std.fs.path.resolveat`, it is now possible to store `work_dir` handle directly in `CacheHash` struct and workout paths to cached files wrt to it rather than wrt `cwd`. --- lib/std/cache_hash.zig | 17 ++++++++++------- lib/std/fs/path.zig | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/lib/std/cache_hash.zig b/lib/std/cache_hash.zig index d160c4ebb22b..1daa31eb90c6 100644 --- a/lib/std/cache_hash.zig +++ b/lib/std/cache_hash.zig @@ -41,6 +41,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 +49,13 @@ 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, .{}), + // TODO should claim ownership of `work_dir`? + .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 +103,7 @@ 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}); + const 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 +213,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 +279,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 +325,7 @@ 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}); + const 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 +350,7 @@ 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}); + const 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(); diff --git a/lib/std/fs/path.zig b/lib/std/fs/path.zig index 34326f28726b..adbe75c39515 100644 --- a/lib/std/fs/path.zig +++ b/lib/std/fs/path.zig @@ -652,6 +652,22 @@ pub fn resolvePosix(allocator: *Allocator, paths: []const []const u8) ![]u8 { return allocator.shrink(result, result_index); } +/// Similar to `resolve`, however, resolves the paths with respect to the provided +/// `Dir` handle. +/// This function, 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; From 043da82b2dcc98737b718f2a709e1dcf813dd3a2 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Fri, 19 Jun 2020 19:20:50 +0200 Subject: [PATCH 4/5] Add std.fs.path.resolveRelative helper fn `std.fs.path.resolveRelative` function is like `std.fs.path.resolve` however it resolves relative paths **only**, and they are always resolved to wrt ".". If the final path is resolved to "." or beyond, `null` is returned instead to signify this. Also, if an absolute path is passed as an argument, `ResolveRelativeError.AbsolutePath` is thrown. This function is particularly useful for Capsicum-like targets such as WASI. This commit also tweaks `std.cache_hash.CacheHash` to use either `std.fs.path.resolveat` or `std.fs.path.resolveRelative` (the latter used when targeting WASI). This way, we essentially got rid of any mention of CWD, and hence, it is now possible to use `CacheHash` struct in WASI. As a result, all tests in the said module have been enabled for WASI. --- lib/std/cache_hash.zig | 102 ++++++++++++++++++++++------------------- lib/std/fs/path.zig | 95 +++++++++++++++++++++++++++++++++++++- 2 files changed, 147 insertions(+), 50 deletions(-) diff --git a/lib/std/cache_hash.zig b/lib/std/cache_hash.zig index 1daa31eb90c6..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; @@ -53,7 +55,6 @@ pub const CacheHash = struct { return CacheHash{ .allocator = allocator, .blake3 = Blake3.init(), - // TODO should claim ownership of `work_dir`? .work_dir = work_dir, .manifest_dir = try work_dir.makeOpenPath(manifest_dir_path, .{}), .manifest_file = null, @@ -103,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.resolveat(self.allocator, self.work_dir, &[_][]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().* = .{ @@ -325,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.resolveat(self.allocator, self.work_dir, &[_][]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(); @@ -350,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.resolveat(self.allocator, self.work_dir, &[_][]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(); @@ -473,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); @@ -492,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); @@ -506,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); @@ -520,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" { @@ -537,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); @@ -558,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"); @@ -572,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"); @@ -596,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"); @@ -624,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"); @@ -636,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); @@ -658,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"); @@ -672,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"); @@ -683,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"); @@ -706,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 adbe75c39515..b97bc935a199 100644 --- a/lib/std/fs/path.zig +++ b/lib/std/fs/path.zig @@ -652,10 +652,62 @@ 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. -/// This function, unlike `resolve`, will perform one syscall to get the path of -/// the specified `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); @@ -760,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 { From fd5fc00fb91a4ad856fd130a65d38fc022310326 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Sat, 20 Jun 2020 00:05:04 +0200 Subject: [PATCH 5/5] Tweak realpathat tests --- lib/std/os/test.zig | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index 62494303d510..f34fa706b1bf 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -276,16 +276,17 @@ test "realpathat" { 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)); - const cwd_realpathat2 = try os.realpathat(cwd.fd, ".", &buf2); // shouldn't change a thing - testing.expect(mem.eql(u8, cwd_path, cwd_realpathat2)); // Now, open an actual Dir{"."} since on Unix `realpathat` behaves // in a special way when `fd` equals `cwd.fd` - const dir = try cwd.openDir(".", .{}); - const cwd_realpathat3 = try os.realpathat(dir.fd, "", &buf2); - testing.expect(mem.eql(u8, cwd_path, cwd_realpathat3)); - const cwd_realpathat4 = try os.realpathat(dir.fd, ".", &buf2); // shouldn't change a thing - testing.expect(mem.eql(u8, cwd_path, cwd_realpathat4)); + 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" {