zlob 1.3.3

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;

// 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");
    }
}