From a6302678a4bddb47705de17e534c0413d74813e8 Mon Sep 17 00:00:00 2001 From: Pat Tullmann Date: Mon, 5 Aug 2024 23:51:20 -0700 Subject: [PATCH] wasi: fstat{at,} support Clean up the std.c.S flags mapped from wasi-libc, and share them with the no-libc wasi POSIX code. Fix the wasi-libc Stat.mode field. Add a test for fstat{,at} with symlinks (for everyone, not just wasi) Fixes #20890 --- lib/std/c.zig | 41 +++++++++++++++++++----- lib/std/posix.zig | 1 + lib/std/posix/test.zig | 71 ++++++++++++++++++++++++++++++++++++++---- 3 files changed, 100 insertions(+), 13 deletions(-) diff --git a/lib/std/c.zig b/lib/std/c.zig index 824c71a46ef2..270af4f48e35 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -1704,17 +1704,44 @@ pub const S = switch (native_os) { .linux => linux.S, .emscripten => emscripten.S, .wasi => struct { - pub const IEXEC = @compileError("TODO audit this"); + // Match wasi-libc's libc-bottom-half/headers/public/__mode_t.h + // NOTE: this S struct is also used by Wasi-without-libc pub const IFBLK = 0x6000; pub const IFCHR = 0x2000; pub const IFDIR = 0x4000; - pub const IFIFO = 0xc000; pub const IFLNK = 0xa000; - pub const IFMT = IFBLK | IFCHR | IFDIR | IFIFO | IFLNK | IFREG | IFSOCK; pub const IFREG = 0x8000; - /// There's no concept of UNIX domain socket but we define this value here - /// in order to line with other OSes. - pub const IFSOCK = 0x1; + pub const IFSOCK = 0xc000; + pub const IFIFO = 0x1000; + pub const IFMT = IFBLK | IFCHR | IFDIR | IFIFO | IFLNK | IFREG | IFSOCK; + + pub fn ISBLK(m: u32) bool { + return m & IFMT == IFBLK; + } + + pub fn ISCHR(m: u32) bool { + return m & IFMT == IFCHR; + } + + pub fn ISDIR(m: u32) bool { + return m & IFMT == IFDIR; + } + + pub fn ISFIFO(m: u32) bool { + return m & IFMT == IFIFO; + } + + pub fn ISLNK(m: u32) bool { + return m & IFMT == IFLNK; + } + + pub fn ISREG(m: u32) bool { + return m & IFMT == IFREG; + } + + pub fn ISSOCK(m: u32) bool { + return m & IFMT == IFSOCK; + } }, .macos, .ios, .tvos, .watchos, .visionos => struct { pub const IFMT = 0o170000; @@ -6372,7 +6399,7 @@ pub const Stat = switch (native_os) { dev: dev_t, ino: ino_t, nlink: nlink_t, - mode: mode_t, + mode: c_uint, // wasi-libc only the file-type bits (S.IFMT) no permission bits uid: uid_t, gid: gid_t, __pad0: c_uint = 0, diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 8460c5d99098..f139ae0c36f8 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -77,6 +77,7 @@ else switch (native_os) { }; pub const Stat = std.c.Stat; // libc Stat has nice conversion code to/from Wasi stat structure + pub const S = std.c.S; // libc S has all nice file-type conversions pub const mode_t = void; // Wasi does not (yet) support file mode/permission bits diff --git a/lib/std/posix/test.zig b/lib/std/posix/test.zig index 3a2da267a6dc..c59631671a44 100644 --- a/lib/std/posix/test.zig +++ b/lib/std/posix/test.zig @@ -25,6 +25,9 @@ const supports_chdir = (native_os != .wasi); // Filter to skip tests on platforms that don't support absolute paths const supports_absolute_paths = (native_os != .wasi); +// Filter to skip tests on platforms that don't (yet) suppport fstat/fstatat +const supports_fstat = (native_os != .windows); + test "check WASI CWD" { if (native_os == .wasi) { if (std.options.wasiCwd() != 3) { @@ -297,6 +300,8 @@ fn testReadlink(target_path: []const u8, symlink_path: []const u8) !void { } test "link with relative paths" { + if (!supports_fstat) return error.SkipZigTest; + switch (native_os) { .wasi, .linux, .solaris, .illumos => {}, else => return error.SkipZigTest, @@ -339,6 +344,8 @@ test "link with relative paths" { } test "linkat with different directories" { + if (!supports_fstat) return error.SkipZigTest; + switch (native_os) { .wasi, .linux, .solaris, .illumos => {}, else => return error.SkipZigTest, @@ -380,9 +387,8 @@ test "linkat with different directories" { } } -test "fstatat" { - // enable when `fstat` and `fstatat` are implemented on Windows - if (native_os == .windows) return error.SkipZigTest; +test "fstat(at) file" { + if (!supports_fstat) return error.SkipZigTest; var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -394,14 +400,66 @@ test "fstatat" { // fetch file's info on the opened fd directly const file = try tmp.dir.openFile("file.txt", .{}); const stat = try posix.fstat(file.handle); - defer file.close(); + file.close(); // now repeat but using `fstatat` instead - const flags = posix.AT.SYMLINK_NOFOLLOW; - const statat = try posix.fstatat(tmp.dir.fd, "file.txt", flags); + const statat = try posix.fstatat(tmp.dir.fd, "file.txt", 0); try expectEqual(stat, statat); } +test "fstat(at) symlink" { + if (!supports_fstat) return error.SkipZigTest; + + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + + try tmp.dir.writeFile(.{ .sub_path = "target.txt", .data = "irrelevant" }); + + const target = try tmp.dir.openFile("target.txt", .{}); + const statTarget = try posix.fstat(target.handle); + target.close(); + + // Set up symlink + try tmp.dir.symLink("target.txt", "sym.lnk", .{}); + + // Openat (+follow) + fstat() the symlink + const linkFollowFd = try posix.openat(tmp.dir.fd, "sym.lnk", .{}, default_mode); + defer posix.close(linkFollowFd); + const statLinkFollow = try posix.fstat(linkFollowFd); + + // fstatat (with and without follow) the symlink + const statatLinkFollow = try posix.fstatat(tmp.dir.fd, "sym.lnk", 0); + const statatLinkNoFollow = try posix.fstatat(tmp.dir.fd, "sym.lnk", posix.AT.SYMLINK_NOFOLLOW); + + if (@hasField(posix.O, "PATH")) { + // Can only openat() a symlink with NOFOLLOW if O.PATH is + // supported. Result should exactly match result from the + // no-follow fstatat() call. + + const linkNoFollowFd = try posix.openat(tmp.dir.fd, "sym.lnk", .{ .NOFOLLOW = true, .PATH = true }, default_mode); + defer posix.close(linkNoFollowFd); + + const statLinkNoFollow = try posix.fstat(linkNoFollowFd); + try testing.expectEqual(statLinkNoFollow, statatLinkNoFollow); + } + + // Link following should have followed the link + try testing.expectEqual(statTarget, statLinkFollow); + try testing.expectEqual(statTarget, statatLinkFollow); + + // symlink and target are different: + try testing.expect(statTarget.ino != statatLinkNoFollow.ino); + try testing.expect(statTarget.mode != statatLinkNoFollow.mode); + + // target is a regular, non-link file: + try testing.expect(posix.S.ISREG(statTarget.mode)); + try testing.expect(!posix.S.ISLNK(statTarget.mode)); + + // symlink is a non-regular, link file: + try testing.expect(!posix.S.ISREG(statatLinkNoFollow.mode)); + try testing.expect(posix.S.ISLNK(statatLinkNoFollow.mode)); +} + test "readlinkat" { var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -1247,6 +1305,7 @@ fn expectMode(dir: posix.fd_t, file: []const u8, mode: posix.mode_t) !void { test "fchmodat smoke test" { if (!std.fs.has_executable_bit) return error.SkipZigTest; + if (!supports_fstat) return error.SkipZigTest; // for expectMode() var tmp = tmpDir(.{}); defer tmp.cleanup();