zlob 1.5.0-dev.3

SIMD optimized glob pattern matching library faster than glob crate
Documentation
//! macOS bulk directory scanner built on getattrlistbulk(2).
//!
//! One syscall returns a whole batch of directory entries WITH the requested
//! metadata (size, times, ids, mode, ...) — no per-entry stat. The attribute
//! mask is built at runtime from walk.MetaMask so callers only pay for the
//! attributes they ask for.
//!
//! Attributes are returned packed, 4-byte aligned, in the canonical order
//! documented in getattrlist(2): ATTR_CMN_RETURNED_ATTRS first, then
//! ATTR_CMN_ERROR, then the remaining common attributes in ascending bit
//! order, then the dir group (for directories) or file group (for anything
//! else). We do not pass FSOPT_PACK_INVAL_ATTRS: the per-entry returned set
//! is the source of truth for which attributes are present, which also
//! handles filesystems that don't support a given attribute.

const std = @import("std");
const builtin = @import("builtin");
const walk = @import("types.zig");

pub const supported = builtin.os.tag == .macos and builtin.link_libc;

pub const AttrList = extern struct {
    bitmapcount: u16,
    reserved: u16 = 0,
    commonattr: u32 = 0,
    volattr: u32 = 0,
    dirattr: u32 = 0,
    fileattr: u32 = 0,
    forkattr: u32 = 0,
};

pub const AttributeSet = extern struct {
    commonattr: u32,
    volattr: u32,
    dirattr: u32,
    fileattr: u32,
    forkattr: u32,
};

const ATTR_BIT_MAP_COUNT: u16 = 5;

// <sys/attr.h> common attribute bits
const ATTR_CMN_NAME: u32 = 0x00000001;
const ATTR_CMN_OBJTYPE: u32 = 0x00000008;
const ATTR_CMN_CRTIME: u32 = 0x00000200;
const ATTR_CMN_MODTIME: u32 = 0x00000400;
const ATTR_CMN_CHGTIME: u32 = 0x00000800;
const ATTR_CMN_ACCTIME: u32 = 0x00001000;
const ATTR_CMN_OWNERID: u32 = 0x00008000;
const ATTR_CMN_GRPID: u32 = 0x00010000;
const ATTR_CMN_ACCESSMASK: u32 = 0x00020000;
const ATTR_CMN_FILEID: u32 = 0x02000000;
const ATTR_CMN_ERROR: u32 = 0x20000000;
const ATTR_CMN_RETURNED_ATTRS: u32 = 0x80000000;

const ATTR_DIR_LINKCOUNT: u32 = 0x00000001;

const ATTR_FILE_LINKCOUNT: u32 = 0x00000001;
const ATTR_FILE_DATALENGTH: u32 = 0x00000200;

const FSOPT_NOFOLLOW: u64 = 0x00000001;

// <sys/vnode.h> object types
const VREG: u32 = 1;
const VDIR: u32 = 2;
const VLNK: u32 = 5;

const Timespec = extern struct { sec: i64, nsec: i64 };
const AttrRef = extern struct { off: i32, len: u32 };

extern "c" fn getattrlistbulk(
    dirfd: c_int,
    alist: *const AttrList,
    attrbuf: *anyopaque,
    bufsize: usize,
    options: u64,
) c_int;

/// Raw 64-bit-inode directory read. This is the syscall wrapper `readdir`
/// itself dispatches to under 64-bit inodes (the public `getdirentries`
/// deliberately link-errors there). Calling it directly on the directory fd
/// avoids the `dup`/`fstat`/`fstatfs`/`fcntl`/`closedir` overhead that
/// `fdopendir` pays per directory — roughly four extra syscalls each.
extern "c" fn __getdirentries64(fd: c_int, buf: [*]u8, bufsize: usize, basep: *u64) isize;

/// macOS `struct dirent` (64-bit inode): ino(8) seekoff(8) reclen(2)
/// namlen(2) type(1) name[]. The name begins at byte 21 of each record.
pub const DIRENT_TYPE_OFF = 20;
pub const DIRENT_NAME_OFF = 21;

pub const DType = struct {
    pub const UNKNOWN: u8 = 0;
    pub const DIR: u8 = 4;
    pub const REG: u8 = 8;
    pub const LNK: u8 = 10;
};

/// One getdirentries64 batch. `name`s point into `buf` and are valid until
/// the next `refill`.
pub const DirEntries = if (!supported) struct {} else struct {
    fd: c_int,
    buf: []align(8) u8,
    len: usize = 0,
    pos: usize = 0,
    basep: u64 = 0,

    pub const Entry = struct { name: []const u8, kind: walk.EntryKind };

    pub fn init(fd: c_int, buf: []align(8) u8) DirEntries {
        return .{ .fd = fd, .buf = buf };
    }

    pub const Error = error{ReadFailed};

    fn refill(self: *DirEntries) Error!bool {
        while (true) {
            const r = __getdirentries64(self.fd, self.buf.ptr, self.buf.len, &self.basep);
            if (r < 0) {
                if (std.c._errno().* == @intFromEnum(std.c.E.INTR)) continue;
                return error.ReadFailed;
            }
            if (r == 0) return false;
            self.len = @intCast(r);
            self.pos = 0;
            return true;
        }
    }

    pub fn next(self: *DirEntries) Error!?Entry {
        while (true) {
            if (self.pos >= self.len) {
                if (!try self.refill()) return null;
            }
            const rec = self.pos;
            if (rec + DIRENT_NAME_OFF > self.len) return error.ReadFailed;
            const reclen = readUnaligned(u16, self.buf, rec + 16);
            const namlen = readUnaligned(u16, self.buf, rec + 18);
            const d_type = self.buf[rec + DIRENT_TYPE_OFF];
            // A zero reclen would spin forever; only a malformed fs yields one.
            if (reclen < DIRENT_NAME_OFF or rec + reclen > self.len) return error.ReadFailed;
            self.pos = rec + reclen;

            const name_start = rec + DIRENT_NAME_OFF;
            if (name_start + namlen > self.len) return error.ReadFailed;
            const name = self.buf[name_start .. name_start + namlen];
            if (name.len == 0) continue;
            if (name[0] == '.' and (name.len == 1 or (name.len == 2 and name[1] == '.'))) continue;

            const kind: walk.EntryKind = switch (d_type) {
                DType.REG => .file,
                DType.DIR => .directory,
                DType.LNK => .sym_link,
                else => .unknown,
            };
            return .{ .name = name, .kind = kind };
        }
    }
};

inline fn readUnaligned(comptime T: type, buf: []const u8, pos: usize) T {
    var v: T = undefined;
    @memcpy(@as([*]u8, @ptrCast(&v))[0..@sizeOf(T)], buf[pos..][0..@sizeOf(T)]);
    return v;
}

inline fn tsToNs(ts: Timespec) i64 {
    return ts.sec *% std.time.ns_per_s +% ts.nsec;
}

pub const ScanError = error{
    /// Filesystem does not support getattrlistbulk — caller must fall back
    /// to readdir+fstatat.
    Unsupported,
    PermissionDenied,
    ReadFailed,
};

/// Streaming scanner over one directory fd. Names returned by `next()` point
/// into `buf` and are invalidated by the next kernel refill — callers must
/// copy them out before draining the batch (the walker copies into its
/// per-directory scratch arena).
pub const BulkScanner = if (!supported) struct {} else struct {
    fd: std.c.fd_t,
    buf: []align(8) u8,
    alist: AttrList,
    mask: walk.MetaMask,
    /// Entries remaining in the current batch.
    remaining: u32 = 0,
    pos: usize = 0,
    /// True once any batch was successfully fetched. EINVAL on the *first*
    /// call means "unsupported filesystem"; later it would be a real error.
    primed: bool = false,

    pub fn init(fd: std.c.fd_t, buf: []align(8) u8, mask: walk.MetaMask) BulkScanner {
        // TODO unify the mask so we do not need to recreate it & make the mask
        var common: u32 = ATTR_CMN_RETURNED_ATTRS | ATTR_CMN_ERROR | ATTR_CMN_NAME | ATTR_CMN_OBJTYPE;
        if (mask.btime) common |= ATTR_CMN_CRTIME;
        if (mask.mtime) common |= ATTR_CMN_MODTIME;
        if (mask.ctime) common |= ATTR_CMN_CHGTIME;
        if (mask.atime) common |= ATTR_CMN_ACCTIME;
        if (mask.uid) common |= ATTR_CMN_OWNERID;
        if (mask.gid) common |= ATTR_CMN_GRPID;
        if (mask.mode) common |= ATTR_CMN_ACCESSMASK;
        if (mask.inode) common |= ATTR_CMN_FILEID;

        var dir: u32 = 0;
        if (mask.nlink) dir |= ATTR_DIR_LINKCOUNT;

        var file: u32 = 0;
        if (mask.nlink) file |= ATTR_FILE_LINKCOUNT;
        if (mask.size) file |= ATTR_FILE_DATALENGTH;

        return .{
            .fd = fd,
            .buf = buf,
            .mask = mask,
            .alist = .{
                .bitmapcount = ATTR_BIT_MAP_COUNT,
                .commonattr = common,
                .dirattr = dir,
                .fileattr = file,
            },
        };
    }

    fn refill(self: *BulkScanner) ScanError!bool {
        while (true) {
            const n = getattrlistbulk(self.fd, &self.alist, self.buf.ptr, self.buf.len, FSOPT_NOFOLLOW);
            if (n < 0) {
                const e = std.c._errno().*;
                switch (@as(std.c.E, @enumFromInt(e))) {
                    .INTR => continue,
                    .INVAL, .OPNOTSUPP => return if (self.primed) error.ReadFailed else error.Unsupported,
                    .ACCES, .PERM => return error.PermissionDenied,
                    else => return error.ReadFailed,
                }
            }
            if (n == 0) return false;
            self.primed = true;
            self.remaining = @intCast(n);
            self.pos = 0;
            return true;
        }
    }

    /// Returns the next entry, or null when the directory is exhausted.
    /// "." and ".." are never returned by getattrlistbulk.
    pub fn next(self: *BulkScanner) ScanError!?walk.RawEntry {
        outer: while (true) {
            if (self.remaining == 0) {
                if (!try self.refill()) return null;
            }

            const rec_start = self.pos;
            if (rec_start + 4 > self.buf.len) return error.ReadFailed;
            const reclen = readUnaligned(u32, self.buf, rec_start);
            if (reclen < 4 or rec_start + reclen > self.buf.len) return error.ReadFailed;
            self.pos = rec_start + reclen;
            self.remaining -= 1;

            var p = rec_start + 4;
            const ret = readUnaligned(AttributeSet, self.buf, p);
            p += @sizeOf(AttributeSet);

            if (ret.commonattr & ATTR_CMN_ERROR != 0) {
                const entry_err = readUnaligned(u32, self.buf, p);
                p += 4;
                // Entry-level error (e.g. dataless file fault): skip it.
                if (entry_err != 0) continue :outer;
            }

            var entry = walk.RawEntry{ .name = &.{}, .kind = .unknown, .meta = .{} };

            if (ret.commonattr & ATTR_CMN_NAME != 0) {
                const ref = readUnaligned(AttrRef, self.buf, p);
                const name_start: usize = if (ref.off >= 0)
                    p + @as(usize, @intCast(ref.off))
                else blk: {
                    const back: usize = @intCast(-ref.off);
                    if (back > p) return error.ReadFailed;
                    break :blk p - back;
                };
                // ref.len includes the trailing NUL.
                if (ref.len == 0 or name_start + ref.len > self.buf.len) return error.ReadFailed;
                entry.name = self.buf[name_start .. name_start + ref.len - 1];
                p += @sizeOf(AttrRef);
            }

            if (ret.commonattr & ATTR_CMN_OBJTYPE != 0) {
                entry.kind = switch (readUnaligned(u32, self.buf, p)) {
                    VREG => .file,
                    VDIR => .directory,
                    VLNK => .sym_link,
                    else => .unknown,
                };
                p += 4;
            }

            if (ret.commonattr & ATTR_CMN_CRTIME != 0) {
                entry.meta.btime_ns = tsToNs(readUnaligned(Timespec, self.buf, p));
                entry.meta.valid.btime = true;
                p += @sizeOf(Timespec);
            }
            if (ret.commonattr & ATTR_CMN_MODTIME != 0) {
                entry.meta.mtime_ns = tsToNs(readUnaligned(Timespec, self.buf, p));
                entry.meta.valid.mtime = true;
                p += @sizeOf(Timespec);
            }
            if (ret.commonattr & ATTR_CMN_CHGTIME != 0) {
                entry.meta.ctime_ns = tsToNs(readUnaligned(Timespec, self.buf, p));
                entry.meta.valid.ctime = true;
                p += @sizeOf(Timespec);
            }
            if (ret.commonattr & ATTR_CMN_ACCTIME != 0) {
                entry.meta.atime_ns = tsToNs(readUnaligned(Timespec, self.buf, p));
                entry.meta.valid.atime = true;
                p += @sizeOf(Timespec);
            }
            if (ret.commonattr & ATTR_CMN_OWNERID != 0) {
                entry.meta.uid = readUnaligned(u32, self.buf, p);
                entry.meta.valid.uid = true;
                p += 4;
            }
            if (ret.commonattr & ATTR_CMN_GRPID != 0) {
                entry.meta.gid = readUnaligned(u32, self.buf, p);
                entry.meta.valid.gid = true;
                p += 4;
            }
            if (ret.commonattr & ATTR_CMN_ACCESSMASK != 0) {
                entry.meta.mode = readUnaligned(u32, self.buf, p) & 0o7777;
                entry.meta.valid.mode = true;
                p += 4;
            }
            if (ret.commonattr & ATTR_CMN_FILEID != 0) {
                entry.meta.inode = readUnaligned(u64, self.buf, p);
                entry.meta.valid.inode = true;
                p += 8;
            }

            if (entry.kind == .directory) {
                if (ret.dirattr & ATTR_DIR_LINKCOUNT != 0) {
                    entry.meta.nlink = readUnaligned(u32, self.buf, p);
                    entry.meta.valid.nlink = true;
                    p += 4;
                }
            } else {
                if (ret.fileattr & ATTR_FILE_LINKCOUNT != 0) {
                    entry.meta.nlink = readUnaligned(u32, self.buf, p);
                    entry.meta.valid.nlink = true;
                    p += 4;
                }
                if (ret.fileattr & ATTR_FILE_DATALENGTH != 0) {
                    entry.meta.size = readUnaligned(u64, self.buf, p);
                    entry.meta.valid.size = true;
                    p += 8;
                }
            }

            if (entry.name.len == 0) continue :outer;
            return entry;
        }
    }
};