zlob 1.6.2

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

const mem = std.mem;
const fnmatch_mod = glob.fnmatch;
const splitPathComponentsNormalized = compiled_pattern.splitPathComponentsNormalized;

/// Match pre-split pattern segments against a path that still needs splitting.
pub fn matchGlobSimplePresplit(pat_segments: [][]const u8, path: []const u8) bool {
    var path_segments_buf: [compiled_pattern.MAX_PATH_COMPONENTS][]const u8 = undefined;
    if (splitPathComponentsNormalized(path, &path_segments_buf)) |path_segments| {
        return matchSegmentsSimple(pat_segments, path_segments, 0, 0);
    }

    // Deep paths exceed the fixed stack buffer. Match directly against the raw
    // path instead of turning a valid pattern into a false negative or
    // allocating on this hot path.
    return matchSegmentsStreaming(pat_segments, path, 0, 0);
}

/// Match pre-split pattern segments against pre-split path segments.
pub fn matchGlobSimplePresplitWithPath(
    pat_segments: [][]const u8,
    path_segments: [][]const u8,
) bool {
    return matchSegmentsSimple(pat_segments, path_segments, 0, 0);
}

/// True when a pattern segment acts as a recursive `**`
/// (git matcher treats double stars as anything that is more than 2 stars while glibc not)
inline fn isDoubleStarSegment(seg: []const u8) bool {
    return seg.len >= 2 and mem.allEqual(u8, seg, '*');
}

const PathSegment = struct {
    text: []const u8,
    next_offset: usize,
};

inline fn nextPathSegment(path: []const u8, initial_offset: usize) ?PathSegment {
    var start = initial_offset;
    while (start < path.len and glob.isPathSep(path[start])) : (start += 1) {}
    if (start == path.len) return null;

    var end = start;
    while (end < path.len and !glob.isPathSep(path[end])) : (end += 1) {}
    return .{ .text = path[start..end], .next_offset = end };
}

/// Unbounded fallback for paths that do not fit the fixed pre-split buffer.
/// Pattern segments are already compiled; only the path is consumed lazily.
fn matchSegmentsStreaming(
    pattern_segments: []const []const u8,
    path: []const u8,
    initial_pat_idx: usize,
    initial_path_offset: usize,
) bool {
    var pat_idx = initial_pat_idx;
    var path_offset = initial_path_offset;

    while (true) {
        if (pat_idx >= pattern_segments.len) {
            return nextPathSegment(path, path_offset) == null;
        }

        const current_pattern = pattern_segments[pat_idx];
        if (isDoubleStarSegment(current_pattern)) {
            if (pat_idx + 1 >= pattern_segments.len) {
                return nextPathSegment(path, path_offset) != null;
            }

            if (matchSegmentsStreaming(pattern_segments, path, pat_idx + 1, path_offset)) {
                return true;
            }

            var scan_offset = path_offset;
            while (nextPathSegment(path, scan_offset)) |segment| {
                scan_offset = segment.next_offset;
                if (matchSegmentsStreaming(pattern_segments, path, pat_idx + 1, scan_offset)) {
                    return true;
                }
            }
            return false;
        }

        const segment = nextPathSegment(path, path_offset) orelse return false;
        if (!fnmatch_mod.fnmatch(current_pattern, segment.text, .{})) return false;

        pat_idx += 1;
        path_offset = segment.next_offset;
    }
}

/// Core recursive segment matching for `**` patterns (no allocation, no PERIOD).
fn matchSegmentsSimple(
    pattern_segments: []const []const u8,
    path_segments: []const []const u8,
    initial_pat_idx: usize,
    initial_path_idx: usize,
) bool {
    var pat_idx = initial_pat_idx;
    var path_idx = initial_path_idx;

    while (true) {
        if (pat_idx >= pattern_segments.len) {
            return path_idx >= path_segments.len;
        }

        const current_pattern = pattern_segments[pat_idx];

        if (isDoubleStarSegment(current_pattern)) {
            if (pat_idx + 1 >= pattern_segments.len) {
                // A trailing `**` means "everything below", so it has to consume
                // at least one segment: git matches `a/**` against `a/f.txt` but
                // not against `a` itself.
                return path_idx < path_segments.len;
            }

            if (matchSegmentsSimple(pattern_segments, path_segments, pat_idx + 1, path_idx)) {
                return true;
            }

            var skip: usize = 1;
            while (path_idx + skip <= path_segments.len) : (skip += 1) {
                if (matchSegmentsSimple(pattern_segments, path_segments, pat_idx + 1, path_idx + skip)) {
                    return true;
                }
            }
            return false;
        }

        if (path_idx >= path_segments.len) {
            return false;
        }

        if (!fnmatch_mod.fnmatch(current_pattern, path_segments[path_idx], .{})) {
            return false;
        }

        pat_idx += 1;
        path_idx += 1;
    }
}