zlob 1.6.2

SIMD optimized glob pattern matching library faster than glob crate
Documentation
const std = @import("std");
const builtin = @import("builtin");
const mem = std.mem;
const linux = std.os.linux;
const types = @import("types.zig");
const worker = @import("worker.zig");
const GitIgnore = @import("../gitignore.zig").GitIgnore;

const IgnoreNode = worker.IgnoreNode;
const MAX_DIR_CHAIN = 64;

pub const __internal_test_api = if (builtin.is_test) struct {
    pub const max_dir_chain = MAX_DIR_CHAIN;

    pub fn isIgnoredResolved(rules: *const IgnoreRules, normalized: []const u8, basename: []const u8, is_dir: bool) bool {
        return rules.isIgnoredResolved(normalized, basename, is_dir);
    }

    pub fn isIgnoredPathSlow(rules: *const IgnoreRules, normalized: []const u8, basename: []const u8, is_dir: bool) bool {
        return rules.isIgnoredPathSlow(normalized, basename, is_dir);
    }
} else struct {};

pub const IgnoreRules = struct {
    allocator: std.mem.Allocator,
    by_dir: std.StringHashMapUnmanaged(*IgnoreNode) = .empty,
    extra: ?*IgnoreNode = null,
    walk_root: []const u8 = &.{},

    /// Returns `true` if the path needs to be ignored or not accessible
    pub fn isIgnoredPath(self: *const IgnoreRules, path: []const u8) bool {
        var normalized_buffer: [types.MAX_PATH]u8 = undefined;
        const path_slash: []const u8 = if (mem.indexOfScalar(u8, path, '\\')) |_| blk: {
            if (path.len > normalized_buffer.len) return true; // pathologically long -> ignored
            @memcpy(normalized_buffer[0..path.len], path);
            for (normalized_buffer[0..path.len]) |*b| {
                if (b.* == '\\') b.* = '/';
            }
            break :blk normalized_buffer[0..path.len];
        } else path;

        // Strip a leading "./" for user-friendliness (relative form).
        const trimmed = if (path_slash.len >= 2 and path_slash[0] == '.' and path_slash[1] == '/')
            path_slash[2..]
        else
            path_slash;

        // Split into (root-relative form, absolute form for lstat)
        // Absolute inputs start with '/' on Unix; on Windows we detect both
        // `/foo/bar` (still absolute here since our slash-normalize ran) and
        // `C:/foo/bar` drive-prefixed forms.
        var abs_buf: [types.MAX_PATH:0]u8 = undefined;
        var relative: []const u8 = undefined;
        var abs_z: [:0]const u8 = undefined;

        if (std.fs.path.isAbsolute(trimmed)) {
            const stripped = stripRootPrefix(self.walk_root, trimmed) orelse return true;
            relative = stripped;
            if (trimmed.len >= abs_buf.len) return true;
            @memcpy(abs_buf[0..trimmed.len], trimmed);
            abs_buf[trimmed.len] = 0;
            abs_z = abs_buf[0..trimmed.len :0];
        } else {
            // Root-relative query: join walk_root + '/' + rel for lstat.
            relative = trimmed;
            const wr = self.walk_root;
            const need_sep = wr.len > 0 and wr[wr.len - 1] != '/';
            const joined_len = wr.len + @intFromBool(need_sep) + relative.len;
            if (joined_len >= abs_buf.len) return true;
            @memcpy(abs_buf[0..wr.len], wr);
            var off_out: usize = wr.len;
            if (need_sep) {
                abs_buf[off_out] = '/';
                off_out += 1;
            }
            @memcpy(abs_buf[off_out..][0..relative.len], relative);
            off_out += relative.len;
            abs_buf[off_out] = 0;
            abs_z = abs_buf[0..off_out :0];
        }

        // ignore if the path is unreachable
        const is_dir = lstatIsDir(abs_z) orelse return true;

        const basename = if (mem.lastIndexOfScalar(u8, relative, '/')) |p| relative[p + 1 ..] else relative;
        return self.isIgnoredResolved(relative, basename, is_dir);
    }

    fn isIgnoredResolved(
        self: *const IgnoreRules,
        normalized: []const u8,
        basename: []const u8,
        is_dir: bool,
    ) bool {
        // Innermost first: extra rules, nested directories, then the root file.
        var ignore_nodes: [MAX_DIR_CHAIN + 1]GitIgnore.ScopedRules = undefined;
        var count: usize = 0;

        if (self.extra) |node| {
            ignore_nodes[count] = .{ .gi = &node.gi, .offset = 0 };
            count += 1;
        }

        var end: usize = normalized.len;
        while (mem.lastIndexOfScalar(u8, normalized[0..end], '/')) |slash| {
            end = slash;
            if (self.by_dir.get(normalized[0..slash])) |node| {
                // if deeper than we can hold as a stack resolution go to the slow method
                if (count == ignore_nodes.len) return self.isIgnoredPathSlow(normalized, basename, is_dir);
                ignore_nodes[count] = .{ .gi = &node.gi, .offset = node.relative_offset };
                count += 1;
            }
        }

        // The loop does not visit the empty root prefix, so add it separately.
        if (self.by_dir.get("")) |node| {
            if (count == ignore_nodes.len) return self.isIgnoredPathSlow(normalized, basename, is_dir);
            ignore_nodes[count] = .{ .gi = &node.gi, .offset = node.relative_offset };
            count += 1;
        }

        const stack = GitIgnore.Stack{ .docs = ignore_nodes[0..count] };
        return stack.verdictWithAncestors(normalized, basename, is_dir) orelse false;
    }

    /// Slow sequential fallback if the path exceeds 64 segments (which is very rare)
    fn isIgnoredPathSlow(
        self: *const IgnoreRules,
        norm: []const u8,
        basename: []const u8,
        is_dir: bool,
    ) bool {
        var offset: usize = 0;
        while (mem.indexOfScalarPos(u8, norm, offset, '/')) |slash| {
            const ancestor = norm[0..slash];
            if (self.slowVerdict(ancestor, GitIgnore.basenameOf(ancestor), true) == true) return true;
            offset = slash + 1;
        }
        return self.slowVerdict(norm, basename, is_dir) orelse false;
    }

    fn slowVerdict(self: *const IgnoreRules, path: []const u8, basename: []const u8, is_dir: bool) ?bool {
        if (self.extra) |node| {
            if (node.gi.checkInode(path, basename, is_dir)) |answer| return answer;
        }
        var end: usize = path.len;
        while (mem.lastIndexOfScalar(u8, path[0..end], '/')) |slash| {
            end = slash;
            if (self.by_dir.get(path[0..slash])) |node| {
                if (node.gi.checkInode(path[node.relative_offset..], basename, is_dir)) |answer| return answer;
            }
        }
        if (self.by_dir.get("")) |node| {
            if (node.gi.checkInode(path, basename, is_dir)) |answer| return answer;
        }
        return null;
    }

    pub fn put(self: *IgnoreRules, dir_rel: []const u8, node: *IgnoreNode) !void {
        const gop = try self.by_dir.getOrPut(self.allocator, dir_rel);
        if (gop.found_existing) {
            // A directory yields at most one node; replacing would leak a ref.
            return;
        }
        errdefer _ = self.by_dir.remove(dir_rel);
        gop.key_ptr.* = try self.allocator.dupe(u8, dir_rel);
        node.retain();
        gop.value_ptr.* = node;
    }

    pub fn setExtra(self: *IgnoreRules, node: ?*IgnoreNode) void {
        if (self.extra) |old| old.release(self.allocator);
        self.extra = node;
        if (node) |n| n.retain();
    }

    pub fn setWalkRoot(self: *IgnoreRules, root: []const u8) !void {
        if (self.walk_root.len > 0) self.allocator.free(self.walk_root);
        self.walk_root = if (root.len == 0) &.{} else try self.allocator.dupe(u8, root);
    }

    pub fn deinit(self: *IgnoreRules) void {
        if (self.walk_root.len > 0) self.allocator.free(self.walk_root);
        self.walk_root = &.{};
        if (self.extra) |x| x.release(self.allocator);
        self.extra = null;
        var it = self.by_dir.iterator();
        while (it.next()) |e| {
            self.allocator.free(e.key_ptr.*);
            e.value_ptr.*.release(self.allocator);
        }
        self.by_dir.deinit(self.allocator);
    }
};

fn stripRootPrefix(root: []const u8, path: []const u8) ?[]const u8 {
    if (root.len == 0) return path;
    // Normalize any trailing slash on the root for comparison stability.
    const clean_root = if (root[root.len - 1] == '/') root[0 .. root.len - 1] else root;
    if (path.len < clean_root.len) return null;
    if (!mem.eql(u8, path[0..clean_root.len], clean_root)) return null;
    if (path.len == clean_root.len) return &.{}; // exact root
    if (path[clean_root.len] != '/') return null; // "root_foo" ≠ "root/..."

    return path[clean_root.len + 1 ..];
}

fn lstatIsDir(path: [:0]const u8) ?bool {
    if (builtin.os.tag == .linux) {
        var stx: linux.Statx = undefined;
        const rc = linux.statx(
            types.AT_FDCWD,
            path.ptr,
            linux.AT.SYMLINK_NOFOLLOW,
            .{ .TYPE = true },
            &stx,
        );
        if (linux.errno(rc) != .SUCCESS) return null;
        return (@as(u32, stx.mode) & 0o170000) == 0o040000;
    }
    // Windows (and any other no-libc target): use the portable std.Io API
    // instead of std.c.fstatat, which would force a libc dependency.
    if (builtin.os.tag == .windows or !@hasDecl(std.c, "fstatat")) {
        const io = std.Io.Threaded.global_single_threaded.io();
        const st = std.Io.Dir.cwd().statFile(io, path, .{ .follow_symlinks = false }) catch return null;
        return st.kind == .directory;
    }
    var st: std.c.Stat = undefined;
    if (std.c.fstatat(types.AT_FDCWD, path.ptr, &st, std.c.AT.SYMLINK_NOFOLLOW) != 0) return null;
    return (@as(u32, @intCast(st.mode)) & 0o170000) == 0o040000;
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------