Why std.Io.File.stat does not identify symbolic link here?

Hello

Am i missing something stupidly evident in the Zig version here ?

viny@nyx ~> zig version
0.17.0-dev.2131+d08989840
viny@nyx ~> ls --color=auto -l /home/viny/.gitconfig
lrwxrwxrwx 1 viny viny 27 27 avril  2025 /home/viny/.gitconfig -> dotfiles/home/dot-gitconfig
viny@nyx ~> ./zlink /home/viny/.gitconfig
/home/viny/.gitconfig is not a symbolic link
viny@nyx ~> ./clink /home/viny/.gitconfig
/home/viny/.gitconfig is a symbolic link

With zlink.zig

const std = @import("std");

pub fn main(init: std.process.Init) !void {
    const arena = init.arena.allocator();
    const io = init.io;

    var it = try init.minimal.args.iterateAllocator(arena);
    const program_name = it.next() orelse "zlink";

    const arg = it.next() orelse {
        std.debug.print("Usage: {s} <path>\n", .{program_name});
        std.process.exit(1);
    };

    const file = try std.Io.Dir.cwd().openFile(io, arg, .{ .mode = .read_only });
    const stat = try file.stat(io);
    if (stat.kind == .sym_link) {
        std.debug.print("{s} is a symbolic link\n", .{arg});
    } else {
        std.debug.print("{s} is not a symbolic link\n", .{arg});
    }
}

and clink.c

#include <stdio.h>
#include <sys/stat.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <path>\n", argv[0]);
        return 1;
    }

    struct stat st;

    if (lstat(argv[1], &st) != 0) {
        perror("lstat");
        return 1;
    }

    if (S_ISLNK(st.st_mode)) {
        printf("%s is a symbolic link\n", argv[1]);
    } else {
        printf("%s is not a symbolic link\n", argv[1]);
    }

    return 0;
}

I’m not at the computer right now so I can’t check, but I’m pretty sure it’s because you’re opening the symlink in the Zig version, causing the OS to dereference it. Thereby, you’re stating the target file instead of the link. (Setting .follow_symlinks = false in the open options might help.)

Good catch, but unfortunately now it crash :frowning:

viny@nyx ~> ./zlink /home/viny/.gitconfig
error: SymLinkLoop
/home/viny/Lang/zig/zig-x86_64-linux-0.17.0-dev.2264+230c63650/lib/std/Io/Threaded.zig:4982:34: 0x119eec7 in dirOpenFilePosix (std.zig)
                        .LOOP => return error.SymLinkLoop,
                                 ^
/home/viny/Lang/zig/zig-x86_64-linux-0.17.0-dev.2264+230c63650/lib/std/Io/Dir.zig:581:5: 0x10374a0 in openFile (std.zig)
    return io.vtable.dirOpenFile(io.userdata, dir, sub_path, options);
    ^
/home/viny/Code/kondo/zlink.zig:15:18: 0x11e665e in main (zlink.zig)
    const file = try std.Io.Dir.cwd().openFile(io, arg, .{ .mode = .read_only, .follow_symlinks = false });

I’m not sure if this is the answer, but in the documentation of OpenFileOptions There is this:

path_only: bool = false

Indicates intent for only some operations to be performed on this opened file:

  • close
  • stat On Linux and FreeBSD, this corresponds to std.posix.O.PATH.

I’d try setting this option to true as well and see what happens.

You want const stat = try std.Io.Dir.cwd().statFile(io, arg, .{ .follow_symlinks = false });. In your zig version you were opening a file (following symlinks to even get a handle on one), and then checking the stats. So the equivalent in the C version would be to use fstat, not lstat.

Thank you @xash it works !

const std = @import("std");

pub fn main(init: std.process.Init) !void {
    const arena = init.arena.allocator();
    const io = init.io;

    var it = try init.minimal.args.iterateAllocator(arena);
    const program_name = it.next() orelse "zlink";

    const arg = it.next() orelse {
        std.debug.print("Usage: {s} <path>\n", .{program_name});
        std.process.exit(1);
    };

    const stat = try std.Io.Dir.cwd().statFile(io, arg, .{ .follow_symlinks = false });
    if (stat.kind == .sym_link) {
        std.debug.print("{s} is a symbolic link\n", .{arg});
    } else {
        std.debug.print("{s} is not a symbolic link\n", .{arg});
    }
}