Skip to content

Commit c6995e6

Browse files
committed
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
1 parent 8549afa commit c6995e6

File tree

3 files changed

+100
-13
lines changed

3 files changed

+100
-13
lines changed

lib/std/c.zig

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1704,17 +1704,44 @@ pub const S = switch (native_os) {
17041704
.linux => linux.S,
17051705
.emscripten => emscripten.S,
17061706
.wasi => struct {
1707-
pub const IEXEC = @compileError("TODO audit this");
1707+
// Match wasi-libc's libc-bottom-half/headers/public/__mode_t.h
1708+
// NOTE: this S struct is also used by Wasi-without-libc
17081709
pub const IFBLK = 0x6000;
17091710
pub const IFCHR = 0x2000;
17101711
pub const IFDIR = 0x4000;
1711-
pub const IFIFO = 0xc000;
17121712
pub const IFLNK = 0xa000;
1713-
pub const IFMT = IFBLK | IFCHR | IFDIR | IFIFO | IFLNK | IFREG | IFSOCK;
17141713
pub const IFREG = 0x8000;
1715-
/// There's no concept of UNIX domain socket but we define this value here
1716-
/// in order to line with other OSes.
1717-
pub const IFSOCK = 0x1;
1714+
pub const IFSOCK = 0xc000;
1715+
pub const IFIFO = 0x1000;
1716+
pub const IFMT = IFBLK | IFCHR | IFDIR | IFIFO | IFLNK | IFREG | IFSOCK;
1717+
1718+
pub fn ISBLK(m: u32) bool {
1719+
return m & IFMT == IFBLK;
1720+
}
1721+
1722+
pub fn ISCHR(m: u32) bool {
1723+
return m & IFMT == IFCHR;
1724+
}
1725+
1726+
pub fn ISDIR(m: u32) bool {
1727+
return m & IFMT == IFDIR;
1728+
}
1729+
1730+
pub fn ISFIFO(m: u32) bool {
1731+
return m & IFMT == IFIFO;
1732+
}
1733+
1734+
pub fn ISLNK(m: u32) bool {
1735+
return m & IFMT == IFLNK;
1736+
}
1737+
1738+
pub fn ISREG(m: u32) bool {
1739+
return m & IFMT == IFREG;
1740+
}
1741+
1742+
pub fn ISSOCK(m: u32) bool {
1743+
return m & IFMT == IFSOCK;
1744+
}
17181745
},
17191746
.macos, .ios, .tvos, .watchos, .visionos => struct {
17201747
pub const IFMT = 0o170000;
@@ -6372,7 +6399,7 @@ pub const Stat = switch (native_os) {
63726399
dev: dev_t,
63736400
ino: ino_t,
63746401
nlink: nlink_t,
6375-
mode: mode_t,
6402+
mode: c_uint, // wasi-libc only the file-type bits (S.IFMT) no permission bits
63766403
uid: uid_t,
63776404
gid: gid_t,
63786405
__pad0: c_uint = 0,

lib/std/posix.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ else switch (native_os) {
7777
};
7878

7979
pub const Stat = std.c.Stat; // libc Stat has nice conversion code to/from Wasi stat structure
80+
pub const S = std.c.S; // libc S has all nice file-type conversions
8081

8182
pub const mode_t = void; // Wasi does not (yet) support file mode/permission bits
8283

lib/std/posix/test.zig

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ const supports_chdir = (native_os != .wasi);
2525
// Filter to skip tests on platforms that don't support absolute paths
2626
const supports_absolute_paths = (native_os != .wasi);
2727

28+
// Filter to skip tests on platforms that don't (yet) suppport fstat/fstatat
29+
const supports_fstat = (native_os != .windows);
30+
2831
test "check WASI CWD" {
2932
if (native_os == .wasi) {
3033
if (std.options.wasiCwd() != 3) {
@@ -305,6 +308,8 @@ fn testReadlink(target_path: []const u8, symlink_path: []const u8) !void {
305308
}
306309

307310
test "link with relative paths" {
311+
if (!supports_fstat) return error.SkipZigTest;
312+
308313
switch (native_os) {
309314
.wasi, .linux, .solaris, .illumos => {},
310315
else => return error.SkipZigTest,
@@ -347,6 +352,8 @@ test "link with relative paths" {
347352
}
348353

349354
test "linkat with different directories" {
355+
if (!supports_fstat) return error.SkipZigTest;
356+
350357
switch (native_os) {
351358
.wasi, .linux, .solaris, .illumos => {},
352359
else => return error.SkipZigTest,
@@ -388,9 +395,8 @@ test "linkat with different directories" {
388395
}
389396
}
390397

391-
test "fstatat" {
392-
// enable when `fstat` and `fstatat` are implemented on Windows
393-
if (native_os == .windows) return error.SkipZigTest;
398+
test "fstat(at) file" {
399+
if (!supports_fstat) return error.SkipZigTest;
394400

395401
var tmp = tmpDir(.{});
396402
defer tmp.cleanup();
@@ -402,14 +408,66 @@ test "fstatat" {
402408
// fetch file's info on the opened fd directly
403409
const file = try tmp.dir.openFile("file.txt", .{});
404410
const stat = try posix.fstat(file.handle);
405-
defer file.close();
411+
file.close();
406412

407413
// now repeat but using `fstatat` instead
408-
const flags = posix.AT.SYMLINK_NOFOLLOW;
409-
const statat = try posix.fstatat(tmp.dir.fd, "file.txt", flags);
414+
const statat = try posix.fstatat(tmp.dir.fd, "file.txt", 0);
410415
try expectEqual(stat, statat);
411416
}
412417

418+
test "fstat(at) symlink" {
419+
if (!supports_fstat) return error.SkipZigTest;
420+
421+
var tmp = testing.tmpDir(.{});
422+
defer tmp.cleanup();
423+
424+
try tmp.dir.writeFile(.{ .sub_path = "target.txt", .data = "irrelevant" });
425+
426+
const target = try tmp.dir.openFile("target.txt", .{});
427+
const statTarget = try posix.fstat(target.handle);
428+
target.close();
429+
430+
// Set up symlink
431+
try tmp.dir.symLink("target.txt", "sym.lnk", .{});
432+
433+
// Openat (+follow) + fstat() the symlink
434+
const linkFollowFd = try posix.openat(tmp.dir.fd, "sym.lnk", .{}, default_mode);
435+
defer posix.close(linkFollowFd);
436+
const statLinkFollow = try posix.fstat(linkFollowFd);
437+
438+
// fstatat (with and without follow) the symlink
439+
const statatLinkFollow = try posix.fstatat(tmp.dir.fd, "sym.lnk", 0);
440+
const statatLinkNoFollow = try posix.fstatat(tmp.dir.fd, "sym.lnk", posix.AT.SYMLINK_NOFOLLOW);
441+
442+
if (@hasField(posix.O, "PATH")) {
443+
// Can only openat() a symlink with NOFOLLOW if O.PATH is
444+
// supported. Result should exactly match result from the
445+
// no-follow fstatat() call.
446+
447+
const linkNoFollowFd = try posix.openat(tmp.dir.fd, "sym.lnk", .{ .NOFOLLOW = true, .PATH = true }, default_mode);
448+
defer posix.close(linkNoFollowFd);
449+
450+
const statLinkNoFollow = try posix.fstat(linkNoFollowFd);
451+
try testing.expectEqual(statLinkNoFollow, statatLinkNoFollow);
452+
}
453+
454+
// Link following should have followed the link
455+
try testing.expectEqual(statTarget, statLinkFollow);
456+
try testing.expectEqual(statTarget, statatLinkFollow);
457+
458+
// symlink and target are different:
459+
try testing.expect(statTarget.ino != statatLinkNoFollow.ino);
460+
try testing.expect(statTarget.mode != statatLinkNoFollow.mode);
461+
462+
// target is a regular, non-link file:
463+
try testing.expect(posix.S.ISREG(statTarget.mode));
464+
try testing.expect(!posix.S.ISLNK(statTarget.mode));
465+
466+
// symlink is a non-regular, link file:
467+
try testing.expect(!posix.S.ISREG(statatLinkNoFollow.mode));
468+
try testing.expect(posix.S.ISLNK(statatLinkNoFollow.mode));
469+
}
470+
413471
test "readlinkat" {
414472
var tmp = tmpDir(.{});
415473
defer tmp.cleanup();
@@ -1255,6 +1313,7 @@ fn expectMode(dir: posix.fd_t, file: []const u8, mode: posix.mode_t) !void {
12551313

12561314
test "fchmodat smoke test" {
12571315
if (!std.fs.has_executable_bit) return error.SkipZigTest;
1316+
if (!supports_fstat) return error.SkipZigTest; // for expectMode()
12581317

12591318
var tmp = tmpDir(.{});
12601319
defer tmp.cleanup();

0 commit comments

Comments
 (0)