Skip to content

Commit a505934

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 acf74ce commit a505934

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) {
@@ -297,6 +300,8 @@ fn testReadlink(target_path: []const u8, symlink_path: []const u8) !void {
297300
}
298301

299302
test "link with relative paths" {
303+
if (!supports_fstat) return error.SkipZigTest;
304+
300305
switch (native_os) {
301306
.wasi, .linux, .solaris, .illumos => {},
302307
else => return error.SkipZigTest,
@@ -339,6 +344,8 @@ test "link with relative paths" {
339344
}
340345

341346
test "linkat with different directories" {
347+
if (!supports_fstat) return error.SkipZigTest;
348+
342349
switch (native_os) {
343350
.wasi, .linux, .solaris, .illumos => {},
344351
else => return error.SkipZigTest,
@@ -380,9 +387,8 @@ test "linkat with different directories" {
380387
}
381388
}
382389

383-
test "fstatat" {
384-
// enable when `fstat` and `fstatat` are implemented on Windows
385-
if (native_os == .windows) return error.SkipZigTest;
390+
test "fstat(at) file" {
391+
if (!supports_fstat) return error.SkipZigTest;
386392

387393
var tmp = tmpDir(.{});
388394
defer tmp.cleanup();
@@ -394,14 +400,66 @@ test "fstatat" {
394400
// fetch file's info on the opened fd directly
395401
const file = try tmp.dir.openFile("file.txt", .{});
396402
const stat = try posix.fstat(file.handle);
397-
defer file.close();
403+
file.close();
398404

399405
// now repeat but using `fstatat` instead
400-
const flags = posix.AT.SYMLINK_NOFOLLOW;
401-
const statat = try posix.fstatat(tmp.dir.fd, "file.txt", flags);
406+
const statat = try posix.fstatat(tmp.dir.fd, "file.txt", 0);
402407
try expectEqual(stat, statat);
403408
}
404409

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

12481306
test "fchmodat smoke test" {
12491307
if (!std.fs.has_executable_bit) return error.SkipZigTest;
1308+
if (!supports_fstat) return error.SkipZigTest; // for expectMode()
12501309

12511310
var tmp = tmpDir(.{});
12521311
defer tmp.cleanup();

0 commit comments

Comments
 (0)