zlob 1.4.1

SIMD optimized glob pattern matching library faster than glob crate
Documentation
const std = @import("std");
const builtin = @import("builtin");
const mem = std.mem;
const Allocator = std.mem.Allocator;
const zlob_flags = @import("zlob_flags");

pub const ZlobFlags = zlob_flags.ZlobFlags;

/// Find the first index `i` such that `s[i] == '/' and s[i+1] == '/'`.
/// SIMD-accelerated lane-paired compare for inputs >= vec_len + 1.
inline fn indexOfDoubleSlash(s: []const u8) ?usize {
    if (s.len < 2) return null;

    const vec_len = std.simd.suggestVectorLength(u8) orelse 16;

    const Vec = @Vector(vec_len, u8);
    const MaskInt = std.meta.Int(.unsigned, vec_len);
    const slash_vec: Vec = @splat('/');

    if (s.len >= vec_len + 1) {
        var i: usize = 0;
        while (i + vec_len + 1 <= s.len) : (i += vec_len) {
            const cur: Vec = s[i..][0..vec_len].*;
            const nxt: Vec = s[i + 1 ..][0..vec_len].*;
            const cur_mask = @as(MaskInt, @bitCast(cur == slash_vec));
            const nxt_mask = @as(MaskInt, @bitCast(nxt == slash_vec));
            const mask = cur_mask & nxt_mask;
            if (mask != 0) return i + @ctz(mask);
        }

        var j = i;
        while (j + 1 < s.len) : (j += 1) {
            if (s[j] == '/' and s[j + 1] == '/') return j;
        }
        return null;
    }

    var j: usize = 0;
    while (j + 1 < s.len) : (j += 1) {
        if (s[j] == '/' and s[j + 1] == '/') return j;
    }
    return null;
}

const is_windows = builtin.os.tag == .windows;

/// Collapse runs of `/` in `src` into a single `/` (output <= `src.len`, so
/// `dst.len >= src.len` suffices; in-place `dst.ptr == src.ptr` is safe).
/// On Windows `\\` is also treated as a separator and rewritten to `/`
/// (POSIX comptime-elides that branch). Fast path: no `//` run (and no `\\`
/// on Windows) → plain `@memcpy` / no-op.
pub fn normalizeSlashes(src: []const u8, dst: []u8) []const u8 {
    std.debug.assert(dst.len >= src.len);

    const first_dup = indexOfDoubleSlash(src);
    const first_back: ?usize = if (is_windows)
        std.mem.indexOfScalar(u8, src, '\\')
    else
        null;

    if (first_dup == null and first_back == null) {
        if (dst.ptr != src.ptr) @memcpy(dst[0..src.len], src);
        return dst[0..src.len];
    }

    // Slow path: copy the clean prefix, then process the rest with
    // combined `//` collapse and (Windows-only) `\\` → `/` rewrite.
    const start: usize = blk: {
        const d = first_dup orelse src.len;
        const b = first_back orelse src.len;
        break :blk @min(d, b);
    };

    if (dst.ptr != src.ptr and start > 0) {
        @memcpy(dst[0..start], src[0..start]);
    }

    // Prime prev_slash from the byte before `start` so a `\\` immediately
    // after a `/` (Windows) is treated as a continuation of the slash run.
    var w: usize = start;
    var prev_slash = if (start > 0) src[start - 1] == '/' else false;
    for (src[start..]) |c| {
        const ch: u8 = if (is_windows and c == '\\') '/' else c;
        const is_slash = ch == '/';
        const skip = is_slash and prev_slash;
        dst[w] = ch;
        w += @intFromBool(!skip);
        prev_slash = is_slash;
    }
    return dst[0..w];
}

/// Rewrite `\\` → `/` in `src` on Windows, returning either `src` itself
/// (if the input is already `\\`-free) or a freshly-rewritten slice in
/// `scratch`. POSIX always returns `src` (comptime no-op).
pub fn normalizePathSeparators(src: []const u8, scratch: []u8) []const u8 {
    if (comptime !is_windows) return src;
    if (std.mem.indexOfScalar(u8, src, '\\') == null) return src;
    if (src.len > scratch.len) return src;
    @memcpy(scratch[0..src.len], src);
    for (scratch[0..src.len]) |*c| {
        if (c.* == '\\') c.* = '/';
    }
    return scratch[0..src.len];
}

// pwd.h is only available on POSIX systems where libc headers are present.
// Excluded on: Windows (no pwd.h), Android (Zig can't provide bionic headers),
// and Apple mobile platforms (iOS/tvOS/watchOS/visionOS — Zig doesn't ship their SDK headers).
// Note: Zig force-enables link_libc for Darwin-based targets, so we must check os.tag directly.
const has_pwd = switch (builtin.os.tag) {
    .windows, .ios, .tvos, .watchos, .visionos => false,
    else => builtin.link_libc,
};
const pwd = if (has_pwd) @cImport({
    @cInclude("pwd.h");
}) else struct {
    // Stub for platforms without pwd.h (Windows, Android, Apple mobile, no-libc builds)
    pub const passwd = opaque {};
    pub fn getpwnam(_: anytype) ?*passwd {
        return null;
    }
};

/// Build a path from directory and name components into the provided buffer.
pub fn buildPathInBuffer(buf: []u8, dir: []const u8, name: []const u8) []const u8 {
    var len: usize = 0;

    if (dir.len > 0 and !mem.eql(u8, dir, ".")) {
        @memcpy(buf[0..dir.len], dir);
        len = dir.len;
        buf[len] = '/';
        len += 1;
    }

    @memcpy(buf[len..][0..name.len], name);
    len += name.len;

    return buf[0..len];
}

pub const PathBuildResult = struct {
    ptr: [*c]u8,
    len: usize,
    buf: []u8,
};

/// Optimized path builder that pre-allocates space for trailing slash if ZLOB_MARK is set.
pub inline fn buildFullPathWithMark(
    allocator: Allocator,
    dirname: []const u8,
    name: []const u8,
    use_dirname: bool,
    is_dir: bool,
    flags: ZlobFlags,
) !PathBuildResult {
    const base_len = if (use_dirname)
        dirname.len + 1 + name.len
    else
        name.len;

    const needs_mark = flags.mark and is_dir;
    const alloc_len = if (needs_mark) base_len + 1 else base_len;

    const path_buf_slice = try allocator.allocSentinel(u8, alloc_len, 0);

    if (use_dirname) {
        @memcpy(path_buf_slice[0..dirname.len], dirname);
        path_buf_slice[dirname.len] = '/';
        @memcpy(path_buf_slice[dirname.len + 1 ..][0..name.len], name);
    } else {
        @memcpy(path_buf_slice[0..name.len], name);
    }

    const final_len = if (needs_mark) blk: {
        path_buf_slice[base_len] = '/';
        path_buf_slice[base_len + 1] = 0;
        break :blk base_len + 1;
    } else base_len;

    return .{
        .ptr = @ptrCast(path_buf_slice.ptr),
        .len = final_len,
        .buf = path_buf_slice,
    };
}

/// Expand tilde (~) in patterns to home directory.
/// Returns null if ZLOB_TILDE_CHECK is set and expansion fails.
pub fn expandTilde(allocator: Allocator, pattern: [:0]const u8, flags: ZlobFlags) !?[:0]const u8 {
    if (pattern.len == 0 or pattern[0] != '~') {
        return pattern;
    }

    var username_end: usize = 1;
    while (username_end < pattern.len and pattern[username_end] != '/' and pattern[username_end] != '\\') : (username_end += 1) {}

    // Handle ~ (current user's home) vs ~username (other user's home)
    if (username_end == 1) {
        // Simple ~ expansion - get current user's home directory
        const home_dir = getHomeDirectory(allocator);
        if (home_dir) |home| {
            defer if (builtin.os.tag == .windows) allocator.free(home);
            const rest = pattern[username_end..];
            const new_pattern = try allocator.allocSentinel(u8, home.len + rest.len, 0);
            @memcpy(new_pattern[0..home.len], home);
            @memcpy(new_pattern[home.len..][0..rest.len], rest);
            return new_pattern;
        } else {
            if (flags.tilde_check) {
                return null;
            }
            return pattern;
        }
    } else {
        // ~username expansion - only supported on POSIX systems with libc (pwd.h)
        if (!has_pwd) {
            // No pwd.h support (Windows, Android, no-libc builds)
            if (flags.tilde_check) {
                return null;
            }
            return pattern;
        }

        const username = pattern[1..username_end];

        var username_z: [256]u8 = undefined;
        if (username.len >= 256) {
            if (flags.tilde_check) {
                return null;
            }
            return pattern;
        }
        @memcpy(username_z[0..username.len], username);
        username_z[username.len] = 0;

        const pw_entry = pwd.getpwnam(&username_z);
        if (pw_entry == null) {
            if (flags.tilde_check) {
                return null;
            }
            return pattern;
        }

        const home_cstr = pw_entry.*.pw_dir;
        if (home_cstr == null) {
            if (flags.tilde_check) {
                return null;
            }
            return pattern;
        }

        const home = mem.sliceTo(home_cstr, 0);
        const rest = pattern[username_end..];
        const new_pattern = try allocator.allocSentinel(u8, home.len + rest.len, 0);
        @memcpy(new_pattern[0..home.len], home);
        @memcpy(new_pattern[home.len..][0..rest.len], rest);
        return new_pattern;
    }
}

/// Look up an environment variable via libc's `getenv`, returning a borrowed
/// slice. Returns null if libc is not available, the variable is unset, or
/// the name cannot be null-terminated.
fn getEnvVarBorrowed(allocator: Allocator, name: []const u8) ?[]const u8 {
    if (!builtin.link_libc) return null;
    const name_z = allocator.dupeZ(u8, name) catch return null;
    defer allocator.free(name_z);
    const ptr = std.c.getenv(name_z.ptr) orelse return null;
    return mem.sliceTo(ptr, 0);
}

/// Get the current user's home directory.
/// On Windows, returns an allocated string that must be freed.
/// On POSIX, returns a borrowed slice from the environment.
fn getHomeDirectory(allocator: Allocator) ?[]const u8 {
    if (builtin.os.tag == .windows) {
        // On Windows, try USERPROFILE first, then HOMEDRIVE+HOMEPATH
        if (getEnvVarBorrowed(allocator, "USERPROFILE")) |user_profile| {
            return allocator.dupe(u8, user_profile) catch null;
        }

        const home_drive = getEnvVarBorrowed(allocator, "HOMEDRIVE") orelse return null;
        const home_path = getEnvVarBorrowed(allocator, "HOMEPATH") orelse return null;

        // Concatenate HOMEDRIVE and HOMEPATH
        const combined = allocator.alloc(u8, home_drive.len + home_path.len) catch return null;
        @memcpy(combined[0..home_drive.len], home_drive);
        @memcpy(combined[home_drive.len..], home_path);
        return combined;
    } else {
        return getEnvVarBorrowed(allocator, "HOME");
    }
}