zlob 1.5.0-dev.1

SIMD optimized glob pattern matching library faster than glob crate
Documentation
const std = @import("std");
const mem = std.mem;
const worker = @import("worker.zig");
const IgnoreNode = worker.IgnoreNode;

pub const IgnoreRules = struct {
    allocator: std.mem.Allocator,
    by_dir: std.StringHashMapUnmanaged(*IgnoreNode) = .empty,

    /// Retruns `true` if the path has to be ignored, no syscalls
    pub fn isIgnored(self: *const IgnoreRules, path: []const u8, is_dir: bool) bool {
        const norm = if (path.len > 2 and path[0] == '.' and path[1] == '/') path[2..] else path;
        const basename = if (mem.lastIndexOfScalar(u8, norm, '/')) |p| norm[p + 1 ..] else norm;

        // Visit ancestor directories deepest-first: for "a/b/c.txt" that is
        // "a/b", then "a", then "" (root). The first directory with a node
        // resolves the whole chain (the node links to its ancestors).
        var dir: []const u8 = if (mem.lastIndexOfScalar(u8, norm, '/')) |p| norm[0..p] else "";
        while (true) {
            if (self.by_dir.get(dir)) |node| {
                return chainIgnored(node, norm, basename, is_dir);
            }
            if (dir.len == 0) return false;
            dir = if (mem.lastIndexOfScalar(u8, dir, '/')) |p| dir[0..p] else "";
        }
    }

    /// Like `isIgnored` but determines directory-ness by stat'ing `path` on
    /// disk (lstat: symlinks are not followed)
    pub fn isIgnoredUntrusted(self: *const IgnoreRules, path: []const u8) bool {
        const clean = if (path.len > 0 and path[path.len - 1] == '/') path[0 .. path.len - 1] else path;
        return self.isIgnored(clean, statIsDir(clean));
    }

    /// Checks a verified existing path wether it needs to be gitignored
    pub fn isIgnoredPath(self: *const IgnoreRules, path: []const u8) bool {
        if (path.len > 0 and path[path.len - 1] == '/') {
            return self.isIgnored(path[0 .. path.len - 1], true);
        }
        return self.isIgnored(path, false);
    }

    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 deinit(self: *IgnoreRules) void {
        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);
    }
};

/// lstat `path` and report whether it is a directory. False on any error or on
/// platforms without a C lstat (the std_fs fallback never reaches here from a
/// retained walk, but keep it total).
fn statIsDir(path: []const u8) bool {
    if (!@hasDecl(std.c, "lstat")) return false;
    if (path.len == 0 or path.len >= 4096) return false;

    var buf: [4096:0]u8 = undefined;
    @memcpy(buf[0..path.len], path);
    buf[path.len] = 0;
    var st: std.c.Stat = undefined;
    if (std.c.lstat(buf[0..path.len :0], &st) != 0) return false;
    return (@as(u32, @intCast(st.mode)) & 0o170000) == 0o040000;
}

/// Deepest-first resolution across the node's ancestor chain (same rule the
/// walker uses): the first file with a decisive verdict wins.
fn chainIgnored(start: *IgnoreNode, rel: []const u8, basename: []const u8, is_dir: bool) bool {
    var node: ?*IgnoreNode = start;
    while (node) |n| : (node = n.parent) {
        if (n.gi.checkWithBasename(rel[n.rel_off..], basename, is_dir)) |verdict| {
            return verdict;
        }
    }
    return false;
}