Skip to content

Commit

Permalink
wasi: fstat{at,} support
Browse files Browse the repository at this point in the history
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 ziglang#20890
  • Loading branch information
rootbeer committed Aug 8, 2024
1 parent 8efc69e commit a630267
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 13 deletions.
41 changes: 34 additions & 7 deletions lib/std/c.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions lib/std/posix.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
71 changes: 65 additions & 6 deletions lib/std/posix/test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit a630267

Please sign in to comment.