zlob 1.5.0-dev.3

SIMD optimized glob pattern matching library faster than glob crate
Documentation
const std = @import("std");
const builtin = @import("builtin");
const walk = @import("types.zig");

pub const supported = builtin.os.tag == .windows;

const w = std.os.windows;

/// FileIdBothDirectoryInformation (class 37) record layout — not in std.
pub const FILE_ID_BOTH_DIR_INFORMATION = extern struct {
    NextEntryOffset: w.ULONG,
    FileIndex: w.ULONG,
    CreationTime: i64,
    LastAccessTime: i64,
    LastWriteTime: i64,
    ChangeTime: i64,
    EndOfFile: i64,
    AllocationSize: i64,
    FileAttributes: w.FILE.ATTRIBUTE,
    FileNameLength: w.ULONG,
    EaSize: w.ULONG,
    ShortNameLength: u8,
    ShortName: [12]w.WCHAR,
    FileId: i64,
    FileName: [1]w.WCHAR,
};

/// Difference between the Windows epoch (1601-01-01) and the Unix epoch
/// (1970-01-01) in 100ns intervals.
const windows_epoch_offset: i64 = 116444736000000000;

// Name-surrogate reparse tags — the only reparse points that behave like
// links (matches what Rust's std considers a symlink). Other tags (cloud
// placeholders, dedup, appexeclinks, ...) are materially regular files/dirs.
const IO_REPARSE_TAG_MOUNT_POINT: u32 = 0xA0000003;
const IO_REPARSE_TAG_SYMLINK: u32 = 0xA000000C;

inline fn filetimeToUnixNs(ft: i64) i64 {
    return (ft -% windows_epoch_offset) *% 100;
}

pub const ScanError = error{ ReadFailed, PermissionDenied };

pub const Scanner = if (!supported) struct {} else struct {
    handle: w.HANDLE,
    buf: []align(8) u8,
    mask: walk.MetaMask,
    /// Whether to use the Id variant of the information class (file id wanted).
    use_id: bool,
    index: usize = 0,
    end: usize = 0,
    restart: bool = true,
    /// WTF-8 conversion scratch for the current entry name (max NT component
    /// is 255 UTF-16 units -> at most 3 bytes each in WTF-8).
    name_buf: [255 * 3]u8 = undefined,

    pub fn init(handle: w.HANDLE, buf: []align(8) u8, mask: walk.MetaMask) Scanner {
        return .{
            .handle = handle,
            .buf = buf,
            .mask = mask,
            .use_id = mask.inode,
        };
    }

    fn refill(self: *Scanner) ScanError!bool {
        var iosb: w.IO_STATUS_BLOCK = undefined;
        const class: w.FILE.INFORMATION_CLASS = if (self.use_id) .IdBothDirectory else .BothDirectory;
        const rc = w.ntdll.NtQueryDirectoryFile(
            self.handle,
            null,
            null,
            null,
            &iosb,
            self.buf.ptr,
            std.math.lossyCast(w.ULONG, self.buf.len),
            class,
            .FALSE,
            null,
            .fromBool(self.restart),
        );
        self.restart = false;
        switch (rc) {
            .SUCCESS => {},
            .NO_MORE_FILES => return false,
            .ACCESS_DENIED => return error.PermissionDenied,
            else => return error.ReadFailed,
        }
        if (iosb.Information == 0) return false;
        self.index = 0;
        self.end = iosb.Information;
        return true;
    }

    /// Returns the next entry or null when the directory is exhausted.
    /// `RawEntry.name` points into the scanner's scratch buffer and is only
    /// valid until the next call.
    pub fn next(self: *Scanner) ScanError!?walk.RawEntry {
        while (true) {
            if (self.index >= self.end) {
                if (!try self.refill()) return null;
            }
            const entry = if (self.use_id)
                try self.parseOne(FILE_ID_BOTH_DIR_INFORMATION)
            else
                try self.parseOne(w.FILE_BOTH_DIR_INFORMATION);
            if (entry) |e| return e;
        }
    }

    fn parseOne(self: *Scanner, comptime T: type) ScanError!?walk.RawEntry {
        // Bounds-check the record before touching it: the kernel guarantees
        // well-formed batches, but user-mode filesystem drivers (WinFsp
        // et al.) have shipped bugs here and the cost is two compares.
        const rec_start = self.index;
        const name_off = @offsetOf(T, "FileName");
        if (rec_start + name_off > self.end) return error.ReadFailed;
        // The official docs guarantee proper alignment but faulty VM /
        // sandboxing layers have been seen to break it; match std and assume
        // only 2-byte alignment.
        const info: *align(2) T = @ptrCast(@alignCast(&self.buf[rec_start]));
        if (rec_start + name_off + info.FileNameLength > self.end) return error.ReadFailed;
        if (info.NextEntryOffset != 0) {
            self.index = rec_start + info.NextEntryOffset;
        } else {
            self.index = self.end;
        }

        const name_w = @as([*]align(2) u16, @ptrCast(&info.FileName))[0 .. info.FileNameLength / 2];
        if (name_w.len == 0) return null;
        if (std.mem.eql(u16, name_w, &[_]u16{'.'}) or
            std.mem.eql(u16, name_w, &[_]u16{ '.', '.' })) return null;

        const name_len = std.unicode.wtf16LeToWtf8(&self.name_buf, name_w);

        const attrs = info.FileAttributes;
        const kind: walk.EntryKind = if (attrs.REPARSE_POINT) blk: {
            // For directory-enumeration info classes EaSize doubles as the
            // reparse tag when REPARSE_POINT is set (documented for
            // FileBothDirectoryInformation and friends). Only name
            // surrogates are links; placeholders/dedup files keep their
            // real kind.
            const tag: u32 = info.EaSize;
            if (tag == IO_REPARSE_TAG_SYMLINK or tag == IO_REPARSE_TAG_MOUNT_POINT)
                break :blk .sym_link;
            break :blk if (attrs.DIRECTORY) .directory else .file;
        } else if (attrs.DIRECTORY)
            .directory
        else
            .file;

        var meta = walk.Metadata{};
        const mask = self.mask;
        if (mask.size and kind == .file) {
            meta.size = @intCast(@max(info.EndOfFile, 0));
            meta.valid.size = true;
        }
        if (mask.mtime) {
            meta.mtime_ns = filetimeToUnixNs(info.LastWriteTime);
            meta.valid.mtime = true;
        }
        if (mask.atime) {
            meta.atime_ns = filetimeToUnixNs(info.LastAccessTime);
            meta.valid.atime = true;
        }
        if (mask.ctime) {
            meta.ctime_ns = filetimeToUnixNs(info.ChangeTime);
            meta.valid.ctime = true;
        }
        if (mask.btime) {
            meta.btime_ns = filetimeToUnixNs(info.CreationTime);
            meta.valid.btime = true;
        }
        if (mask.inode and @hasField(T, "FileId")) {
            meta.inode = @bitCast(info.FileId);
            meta.valid.inode = true;
        }
        // nlink / mode / uid / gid have no cheap Windows equivalent — their
        // valid bits stay unset.

        return .{
            .name = self.name_buf[0..name_len],
            .kind = kind,
            .meta = meta,
        };
    }
};

/// Opens a child directory by (single-component) name relative to a parent
/// directory handle. Never follows reparse points.
pub fn openChildDir(parent: w.HANDLE, name: []const u8) !w.HANDLE {
    if (!supported) unreachable;
    var name_w: [255]u16 = undefined;
    const name_w_len = std.unicode.wtf8ToWtf16Le(&name_w, name) catch return error.NameTooLong;

    var handle: w.HANDLE = undefined;
    var iosb: w.IO_STATUS_BLOCK = undefined;
    const rc = w.ntdll.NtCreateFile(
        &handle,
        .{
            .SPECIFIC = .{ .FILE_DIRECTORY = .{
                .LIST = true,
                .READ_ATTRIBUTES = true,
                .TRAVERSE = true,
            } },
            .STANDARD = .{ .SYNCHRONIZE = true },
        },
        &.{
            .RootDirectory = parent,
            .ObjectName = @constCast(&w.UNICODE_STRING.init(name_w[0..name_w_len])),
        },
        &iosb,
        null,
        .{ .NORMAL = true },
        .VALID_FLAGS,
        .OPEN,
        .{
            .DIRECTORY_FILE = true,
            .IO = .SYNCHRONOUS_NONALERT,
            .OPEN_FOR_BACKUP_INTENT = true,
            .OPEN_REPARSE_POINT = true,
        },
        null,
        0,
    );
    return switch (rc) {
        .SUCCESS => handle,
        .OBJECT_NAME_NOT_FOUND, .OBJECT_PATH_NOT_FOUND => error.FileNotFound,
        .OBJECT_NAME_INVALID => error.BadPathName,
        .NOT_A_DIRECTORY => error.NotDir,
        .ACCESS_DENIED, .SHARING_VIOLATION => error.AccessDenied,
        else => error.Unexpected,
    };
}

/// Reads a (small) file by name relative to an open directory handle.
/// Returns null when it can't be opened/read — callers treat it as absent.
pub fn readSmallFileAt(
    gpa: std.mem.Allocator,
    dir: w.HANDLE,
    name: []const u8,
    max_size: usize,
) ?[]u8 {
    if (!supported) unreachable;
    var name_w: [255]u16 = undefined;
    const name_w_len = std.unicode.wtf8ToWtf16Le(&name_w, name) catch return null;

    var handle: w.HANDLE = undefined;
    var iosb: w.IO_STATUS_BLOCK = undefined;
    const rc = w.ntdll.NtCreateFile(
        &handle,
        .{
            .STANDARD = .{ .SYNCHRONIZE = true },
            .GENERIC = .{ .READ = true },
        },
        &.{
            .RootDirectory = dir,
            .ObjectName = @constCast(&w.UNICODE_STRING.init(name_w[0..name_w_len])),
        },
        &iosb,
        null,
        .{ .NORMAL = true },
        .VALID_FLAGS,
        .OPEN,
        .{
            .IO = .SYNCHRONOUS_NONALERT,
            .NON_DIRECTORY_FILE = true,
        },
        null,
        0,
    );
    if (rc != .SUCCESS) return null;
    defer w.CloseHandle(handle);

    var list: std.ArrayList(u8) = .empty;
    defer list.deinit(gpa);
    list.ensureTotalCapacity(gpa, 8192) catch return null;
    while (true) {
        const spare = list.unusedCapacitySlice();
        if (spare.len == 0) {
            if (list.items.len >= max_size) break;
            list.ensureUnusedCapacity(gpa, list.items.len) catch return null;
            continue;
        }
        var read_iosb: w.IO_STATUS_BLOCK = undefined;
        const read_rc = w.ntdll.NtReadFile(
            handle,
            null,
            null,
            null,
            &read_iosb,
            spare.ptr,
            std.math.lossyCast(w.ULONG, spare.len),
            null,
            null,
        );
        switch (read_rc) {
            .SUCCESS => {
                if (read_iosb.Information == 0) break;
                list.items.len += read_iosb.Information;
            },
            .END_OF_FILE => break,
            else => return null,
        }
    }
    return list.toOwnedSlice(gpa) catch null;
}