Something went wrong. Try again.
Reading and writing git repositories in Zig, without running git.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! git's glob dialect, as gitignore(5), gitglossary(7)'s `glob` pathspec//! magic and git-config(1)'s `includeIf` describe it, with the corners the//! documentation leaves out found by asking git: `git check-attr` for//! patterns matched against whole paths, `git tag -l` for patterns that//! are not, and `core.ignoreCase` and `--ignore-case` for case folding.//! Ignore files, attributes, pathspecs, ref patterns and `includeIf` all//! use it, so it depends on nothing else in the library.//!//! - `?` matches one byte, and `*` any run of bytes, empty included.//! - `[...]` matches one byte from a set: single bytes, ranges `a-z`, and//! the classes `[:alpha:]` and its eleven POSIX siblings. A `!` or `^`//! first negates it, and a `]` first, after any negation, is a member.//! A `-` first or last is a member. A range runs from the byte before//! the `-` to the byte after it, either of them escapable; one whose//! end comes before its start matches only its start. Inside a set, `[:`//! begins a class only if the next `]` follows a second `:`, and is//! otherwise two members; an unknown class name, or a set that is never//! closed, makes the whole pattern match nothing. With `pathname`, a set//! never matches `/`, negated or not.//! - `\` makes the next byte literal, inside a set as well as out, and a//! pattern ending in one matches nothing.//!//! With `pathname`, as paths are matched, no wildcard matches a `/`//! except `**`, a run of two or more `*`s that is a path component of its//! own — at the start of the pattern or after a `/`, and at its end or//! before a `/` — which matches any run of bytes. Followed by a `/`, it//! may also match nothing at all, slash included, so that `a/**/b`//! matches `a/b`. Anywhere else a run of `*`s is one `*`.//!//! With `casefold`, ASCII letters match either case, except one written//! after a `\`, which matches only a byte of its own case once the text//! has been folded to lower case: git folds the text and every unescaped//! byte of the pattern, and leaves escaped ones alone.
const std = @import("std");
/// How a pattern is matched.pub const Flags = struct { /// Wildcards other than `**` do not match `/`, as when matching paths. pathname: bool = false, /// ASCII letters match regardless of case. casefold: bool = false,};
/// Whether `text` matches the glob `pattern`.pub fn match(pattern: []const u8, text: []const u8, flags: Flags) bool { var m: Matcher = .{ .pattern = pattern, .text = text, .flags = flags }; return m.run();}
/// Whether `pattern` holds anything that is not matched literally: an/// unescaped `*`, `?`, `[` or `\`. A pattern without one matches only/// itself, as git's ref and pathspec patterns treat it.pub fn hasWildcards(pattern: []const u8) bool { for (pattern) |c| switch (c) { '*', '?', '[', '\\' => return true, else => {}, }; return false;}
/// What one step through the pattern found.const Token = union(enum) { /// A byte to match, and whether it was escaped. literal: struct { byte: u8, escaped: bool }, /// `?`. any, /// A bracket expression, which ends where `end` says. set: struct { end: usize }, /// `*`, which stops at a `/` under `pathname`. star, /// `**` as a whole component at the end of the pattern or before an /// escaped slash: anything at all. any_run, /// `**/`: nothing, or anything ending in a `/`. dirs, /// A pattern git rejects, which matches nothing. invalid,};
const Matcher = struct { pattern: []const u8, text: []const u8, flags: Flags,
/// Reads the token at `p`, and where the next one starts. fn token(m: *const Matcher, p: usize) struct { Token, usize } { const pat = m.pattern; switch (pat[p]) { '\\' => { if (p + 1 == pat.len) return .{ .invalid, p + 1 }; return .{ .{ .literal = .{ .byte = pat[p + 1], .escaped = true } }, p + 2 }; }, '?' => return .{ .any, p + 1 }, '[' => { const scan = scanSet(pat, p, 0, false) orelse return .{ .invalid, pat.len }; return .{ .{ .set = .{ .end = scan.end } }, scan.end }; }, '*' => { var q = p; while (q < pat.len and pat[q] == '*') q += 1; if (!m.flags.pathname) return .{ .star, q }; const stars = q - p; const starts_component = p == 0 or pat[p - 1] == '/'; if (stars < 2 or !starts_component) return .{ .star, q }; if (q == pat.len) return .{ .any_run, q }; if (pat[q] == '/') return .{ .dirs, q + 1 }; if (pat[q] == '\\' and q + 1 < pat.len and pat[q + 1] == '/') return .{ .any_run, q }; return .{ .star, q }; }, else => |c| return .{ .{ .literal = .{ .byte = c, .escaped = false } }, p + 1 }, } }
fn fold(m: *const Matcher, c: u8) u8 { return if (m.flags.casefold) std.ascii.toLower(c) else c; }
/// Whether the single-byte token at `p` (a literal, `?` or a set) /// matches `c`. fn matchesOne(m: *const Matcher, tok: Token, p: usize, c: u8) bool { return switch (tok) { .literal => |l| if (l.escaped) l.byte == m.fold(c) else m.fold(l.byte) == m.fold(c), .any => !(m.flags.pathname and c == '/'), .set => m.setMatches(p, c), else => unreachable, }; }
/// Whether the set from `start` (its `[`) matches `c`. fn setMatches(m: *const Matcher, start: usize, c: u8) bool { if (m.flags.pathname and c == '/') return false; const scan = scanSet(m.pattern, start, c, m.flags.casefold) orelse return false; return scan.found; }
/// Matches the whole text against the whole pattern. A `*` that can /// stretch, and the last `**` that can, are each remembered as a /// place to resume from with more of the text taken by it; resuming /// from the latest of each is enough, since anything an earlier one /// could take a later one can take instead. That keeps the work to /// pattern length times text length, whatever the pattern. fn run(m: *Matcher) bool { const pat = m.pattern; const text = m.text; var p: usize = 0; var t: usize = 0; // Where to resume after the latest `*`: the pattern after it, and // the text it would take one more byte of. var star: ?struct { p: usize, t: usize } = null; // Likewise after the latest `**` or `**/`. var run_star: ?struct { p: usize, t: usize, dirs: bool } = null; while (true) { if (p < pat.len) { const tok, const next = m.token(p); switch (tok) { .invalid => return false, .star => { star = .{ .p = next, .t = t + 1 }; p = next; continue; }, .any_run => { if (next == pat.len) return true; star = null; run_star = .{ .p = next, .t = t + 1, .dirs = false }; p = next; continue; }, .dirs => { star = null; run_star = .{ .p = next, .t = t, .dirs = true }; p = next; continue; }, else => if (t < text.len and m.matchesOne(tok, p, text[t])) { p = next; t += 1; continue; }, } } else if (t == text.len) return true;
// A mismatch: take one more byte into the latest `*`, if it // can have one, or else more into the latest `**`. if (star) |*s| { if (s.t <= text.len and !(m.flags.pathname and text[s.t - 1] == '/')) { p = s.p; t = s.t; s.t += 1; continue; } star = null; } if (run_star) |*r| { if (r.dirs) { // `**/` takes whole directories: up to and including // the next slash. const slash = std.mem.findScalarPos(u8, text, r.t, '/') orelse return false; p = r.p; t = slash + 1; r.t = slash + 1; continue; } if (r.t <= text.len) { p = r.p; t = r.t; r.t += 1; continue; } } return false; } }};
/// Reads the set whose `[` is at `start`, and says where it ends, just/// past its `]`, and whether it matches `c`; or null if nothing closes it/// or it names an unknown class, when git matches nothing.fn scanSet(pat: []const u8, start: usize, c: u8, casefold: bool) ?struct { end: usize, found: bool } { const t = if (casefold) std.ascii.toLower(c) else c; var i = start + 1; var negated = false; if (i < pat.len and (pat[i] == '!' or pat[i] == '^')) { negated = true; i += 1; } var found = false; var first = true; while (true) : (first = false) { if (i >= pat.len) return null; // A `]` first is a member. if (pat[i] == ']' and !first) return .{ .end = i + 1, .found = found != negated }; if (pat[i] == '[' and i + 1 < pat.len and pat[i + 1] == ':') { if (classSpan(pat, i)) |span| { const class = Class.fromName(span.name) orelse return null; if (class.matches(c, casefold)) found = true; i = span.end; continue; } } var lo = pat[i]; if (lo == '\\') { if (i + 1 == pat.len) return null; lo = pat[i + 1]; i += 2; // An escaped byte is compared with the folded text as it is. if (lo == t) found = true; } else { i += 1; if ((if (casefold) std.ascii.toLower(lo) else lo) == t) found = true; } if (i + 1 < pat.len and pat[i] == '-' and pat[i + 1] != ']') { var hi = pat[i + 1]; i += 2; if (hi == '\\') { if (i == pat.len) return null; hi = pat[i]; i += 1; } if (lo <= t and t <= hi) found = true; if (casefold and std.ascii.isLower(t)) { const u = std.ascii.toUpper(t); if (lo <= u and u <= hi) found = true; } } }}
/// The class named by `[:name:]` at `i`, and where it ends, if it is one:/// a `[:` whose next `]` follows a colon other than its own.fn classSpan(pat: []const u8, i: usize) ?struct { name: []const u8, end: usize } { const close = std.mem.findScalarPos(u8, pat, i + 2, ']') orelse return null; if (close == i + 2 or pat[close - 1] != ':') return null; return .{ .name = pat[i + 2 .. close - 1], .end = close + 1 };}
/// The POSIX character classes, over ASCII only.const Class = enum { alnum, alpha, blank, cntrl, digit, graph, lower, print, punct, space, upper, xdigit,
fn fromName(name: []const u8) ?Class { return std.meta.stringToEnum(Class, name); }
fn matches(class: Class, c: u8, casefold: bool) bool { return switch (class) { .alnum => std.ascii.isAlphanumeric(c), .alpha => std.ascii.isAlphabetic(c), .blank => c == ' ' or c == '\t', .cntrl => std.ascii.isControl(c), .digit => std.ascii.isDigit(c), .graph => c > ' ' and c < 0x7f, .lower => std.ascii.isLower(c) or (casefold and std.ascii.isUpper(c)), .print => c >= ' ' and c < 0x7f, .punct => std.ascii.isPrint(c) and !std.ascii.isAlphanumeric(c) and c != ' ', // Not vertical tab or form feed, which git leaves out. .space => c == ' ' or c == '\t' or c == '\n' or c == '\r', .upper => std.ascii.isUpper(c) or (casefold and std.ascii.isLower(c)), .xdigit => std.ascii.isHex(c), }; }};
const testing = std.testing;const fixture = @import("testing/fixture.zig");const fuzz_input = @import("testing/fuzz.zig");
/// The meaning of a pattern, followed the slow way: every way of/// splitting the text among the wildcards is tried. The tests hold the/// fast matcher to it.fn reference(m: *const Matcher, p: usize, t: usize) bool { const pat = m.pattern; const text = m.text; if (p == pat.len) return t == text.len; const tok, const next = m.token(p); switch (tok) { .invalid => return false, .star => { var k = t; while (true) : (k += 1) { if (reference(m, next, k)) return true; if (k == text.len or (m.flags.pathname and text[k] == '/')) return false; } }, .any_run => { for (t..text.len + 1) |k| if (reference(m, next, k)) return true; return false; }, .dirs => { if (reference(m, next, t)) return true; for (t..text.len) |k| if (text[k] == '/' and reference(m, next, k + 1)) return true; return false; }, else => return t < text.len and m.matchesOne(tok, p, text[t]) and reference(m, next, t + 1), }}
fn referenceMatch(pattern: []const u8, text: []const u8, flags: Flags) bool { const m: Matcher = .{ .pattern = pattern, .text = text, .flags = flags }; return reference(&m, 0, 0);}
const all_flags = [_]Flags{ .{}, .{ .pathname = true }, .{ .casefold = true }, .{ .pathname = true, .casefold = true } };
fn expectMatch(want: bool, pattern: []const u8, text: []const u8, flags: Flags) !void { errdefer std.debug.print("pattern \"{f}\" text \"{f}\" {any}\n", .{ std.zig.fmtString(pattern), std.zig.fmtString(text), flags }); try testing.expectEqual(want, match(pattern, text, flags)); try testing.expectEqual(want, referenceMatch(pattern, text, flags));}
test "wildcards match as git's do" { const path: Flags = .{ .pathname = true }; try expectMatch(true, "*", ".x", path); try expectMatch(true, "a/**/b", "a/b", path); try expectMatch(true, "a/**/b", "a/x/y/b", path); try expectMatch(true, "a/**/b", "a//b", path); try expectMatch(false, "a**b", "ax/b", path); try expectMatch(true, "a**b", "ax/b", .{}); try expectMatch(false, "a/**b", "a/x/b", path); try expectMatch(true, "**", "a/b", path); try expectMatch(true, "a/***/b", "a/x/y/b", path); try expectMatch(true, "abc/**", "abc/x/y", path); try expectMatch(false, "abc/**", "abc", path); try expectMatch(true, "a\\/**", "a/b/c", path); try expectMatch(true, "**\\/b", "x/y/b", path); try expectMatch(false, "a/**\\/b", "a/b", path); try expectMatch(true, "a/**\\/b", "a/x/b", path); try expectMatch(false, "a?b", "a/b", path); try expectMatch(true, "a?b", "a/b", .{}); try expectMatch(false, "a[/]b", "a/b", path); try expectMatch(false, "a[!x]b", "a/b", path); try expectMatch(true, "a[/]b", "a/b", .{});}
test "bracket expressions are read as git reads them" { try expectMatch(false, "a[", "a[", .{}); try expectMatch(false, "a[]", "a[]", .{}); try expectMatch(true, "a[]]", "a]", .{}); try expectMatch(true, "a[!]]b", "axb", .{}); try expectMatch(false, "a\\", "a\\", .{}); try expectMatch(true, "a[b-]", "a-", .{}); try expectMatch(true, "a[\\]]", "a]", .{}); try expectMatch(false, "a[[:foo:]]", "a[", .{}); try expectMatch(true, "a[z-a]", "az", .{}); try expectMatch(false, "a[z-a]", "am", .{}); try expectMatch(true, "a[[:]", "a:", .{}); try expectMatch(true, "a[[:alpha]", "al", .{}); try expectMatch(false, "a[[:alpha]", "ab", .{}); try expectMatch(false, "a[a-[:digit:]]", "a5", .{}); try expectMatch(true, "a[a-[:digit:]]", "a:]", .{}); try expectMatch(true, "[a-c-e]", "-", .{}); try expectMatch(false, "[a-c-e]", "d", .{}); try expectMatch(true, "x[[:a]b:]", "x[b:]", .{}); try expectMatch(false, "x[[:ALPHA:]]", "xa", .{}); try expectMatch(true, "[[:xdigit:][:upper:]]", "G", .{}); try expectMatch(false, "[[:space:]]", "\x0b", .{}); try expectMatch(false, "[[:space:]]", "\x0c", .{}); try expectMatch(true, "[[:space:]]", "\r", .{}); try expectMatch(true, "[]-a]", "^", .{}); try expectMatch(true, "x[\\]-a]", "x^", .{});}
test "case folding leaves escaped bytes alone" { const fold: Flags = .{ .pathname = true, .casefold = true }; try expectMatch(true, "abc", "aBc", fold); try expectMatch(true, "[A-Z]", "a", fold); try expectMatch(true, "[[:upper:]]", "a", fold); try expectMatch(false, "[!a]", "A", fold); try expectMatch(true, "[Z-a]", "z", fold); try expectMatch(false, "[Z-a]", "B", fold); try expectMatch(false, "\\A", "a", fold); try expectMatch(true, "\\a", "A", fold); try expectMatch(false, "[\\A]", "A", fold); try expectMatch(true, "[\\A]", "A", .{});}
test "a pattern of many stars takes no longer than its length times the text's" { const pattern = "*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*b"; const text: [4000]u8 = @splat('a'); for (all_flags) |flags| try testing.expect(!match(pattern, &text, flags)); const dirs: [90]u8 = @bitCast(@as([30][3]u8, @splat("**/".*))); const deep: [2000]u8 = @bitCast(@as([1000][2]u8, @splat("a/".*))); try testing.expect(!match(&dirs ++ "b", &deep, .{ .pathname = true }));}
/// Patterns for the comparisons with git: every one of up to three bytes/// from an alphabet of the bytes that mean something, and those the/// experiments that shaped this file used.fn differentialPatterns(gpa: std.mem.Allocator) !std.ArrayList([]u8) { var list: std.ArrayList([]u8) = .empty; errdefer { for (list.items) |p| gpa.free(p); list.deinit(gpa); } const alphabet = "a/*?[]!-\\"; for (1..4) |len| { var digits: [3]usize = @splat(0); outer: while (true) { var buf: [3]u8 = undefined; for (0..len) |k| buf[k] = alphabet[digits[k]]; try list.append(gpa, try gpa.dupe(u8, buf[0..len])); var k: usize = 0; while (k < len) : (k += 1) { digits[k] += 1; if (digits[k] < alphabet.len) continue :outer; digits[k] = 0; } break; } } for (handwritten_patterns) |p| try list.append(gpa, try gpa.dupe(u8, p)); return list;}
const handwritten_patterns = [_][]const u8{ "abc/**", "**/x", "a/**/b", "a**b", "**a", "a**", "a/**b", "a**/b", "**", "***", "a/***/b", "a[/]b", "a[!x]b", "a?b", "a[", "a[]", "a[]]", "a[!]]b", "a[b-]", "a[\\]]", "a[[:alpha:]]", "a[[:foo:]]", "a[z-a]", "[a-]x", "a[-z]", "a[^b]", "a[[]", "a[[:]", "a[x[:digit:]]", "a[[:alpha:]", "a[[:alpha]", "a[[:alpha:]x]", "a[[:alpha:]-z]", "a[a-[:digit:]]", "a[!", "a[\\", "a[a\\-z]", "a[\\a-\\z]", "a\\*", "a\\[b]", "a[:alpha:]", "[[:space:]]", "[[:xdigit:][:upper:]]", "[[:punct:]]", "[[:print:]]", "[[:graph:]]", "[[:blank:]]", "[[:cntrl:]]", "[[:lower:]]", "[[:upper:]]", "[[:alnum:]]", "[[:digit:]]", "*/*", "**/**", "**/b/**", "a/**/**/b", "*a/**", "\\\\", "[\\\\]", "a[^]]", "[]-a]", "[!]-a]", "a[--0]", "[[:alpha:]-]", "[a-c-e]", "**/a/**/b/**", "*.c", "[[:", "[[:]]", "[[::]]", "[[=a=]]", "[[.a.]]", "a?", "[\\A]", "\\a", "\\A", "[a-\\]]", "x[[:a]b:]", "[[:alpha:][:a]", "x[a-]]", "[a-z-]", "x[[:al]", "x[[:alpha:]]", "x[[:ALPHA:]]", "x[[:alpha:]y", "[a-]]", "x[\\]-a]", "x[!/]", "x*/y", "a/***b", "a\\/**", "**\\/b", "a/**\\/b", "a/\\**/b", "a/*\\*/b", "[Z-a]", "[^-z]", "[A-Z]", "[a-z]", "A*", "*B", "a/*/b", "*/b/*", "a/**/*", "**/*b", "?/?", "*?*", "[!a-b]*", "a*b*c", "*a*b", "a/b", "A/B", "**/A", "a*/**/b", "\\**", "*\\*", "\\?", "[?*]", "[*]/[?]", "a[!a/]",};
/// Paths for the comparisons with git: no slash first or last, no empty/// component, since `git check-attr` reads those as something else.const differential_paths = [_][]const u8{ "a", "b", "A", "B", "ab", "aB", "Ab", "aa", "abc", "ABC", "a/b", "a/B", "A/b", "a/a", "a/x/b", "a/x/y/b", "a/b/c", "x/a", "x/y/a", "ax/b", "a-", "-", "-a", "a-b", "a]", "]", "a[", "[", "a!", "!", "a*", "*", "a?", "?", "a\\", "\\", "a\\b", "a\\/b", "*/?", "a^", "^", ":", "a:", "a:]", "a[b:]", "x[b:]", "_", "z", "Z", "m", "5", " ", "\t", "\n", "\r", "\x0c", "\x0b", "\x01", "\x7f", "a.c", "x.c", "d/x.c", "abc/x", "abc/x/y", "xa/b", "b/c", "x/b/c", "a/b/x", "a/x/b/y", "x/a/y/b/z", "a\xc3\xa9", "\xc3\xa9", "a/xb", "axb", "abcabc", "a/b/b", "aa/bb", "*/b/*", "[]", "a[]",};
/// Writes `s` C-quoted, as a `.gitattributes` pattern may be.fn writeQuoted(w: *std.Io.Writer, s: []const u8) !void { try w.writeByte('"'); for (s) |c| { if (c == '"' or c == '\\') { try w.writeAll(&.{ '\\', c }); } else if (c < 0x20 or c >= 0x7f) { try w.print("\\{o:0>3}", .{c}); } else try w.writeByte(c); } try w.writeByte('"');}
test "patterns match whole paths as git check-attr matches them" { const gpa = testing.allocator; var repo = try fixture.Repo.init(gpa, testing.io); defer repo.deinit(); var patterns = try differentialPatterns(gpa); defer { for (patterns.items) |p| gpa.free(p); patterns.deinit(gpa); }
// One attribute per pattern, each anchored by a leading slash so that // it is matched against the whole path, as `pathname` matches. git // reads a pattern ending in a slash as one only for directories, so // those are left out. var attributes: std.Io.Writer.Allocating = .init(gpa); defer attributes.deinit(); var kept: std.ArrayList(usize) = .empty; defer kept.deinit(gpa); for (patterns.items, 0..) |p, i| { if (p[p.len - 1] == '/') continue; // An escaped slash at the end is the same to git. if (std.mem.endsWith(u8, p, "\\/")) continue; const anchored = try std.fmt.allocPrint(gpa, "/{s}", .{p}); defer gpa.free(anchored); try writeQuoted(&attributes.writer, anchored); try attributes.writer.print(" p{d}\n", .{i}); try kept.append(gpa, i); } try repo.tmp.dir.writeFile(repo.io, .{ .sub_path = ".gitattributes", .data = attributes.written() });
var input: std.ArrayList(u8) = .empty; defer input.deinit(gpa); for (differential_paths) |path| { try input.appendSlice(gpa, path); try input.append(gpa, 0); }
for ([_]bool{ false, true }) |casefold| { const args: []const []const u8 = if (casefold) &.{ "-c", "core.ignoreCase=true", "check-attr", "--stdin", "-z", "-a" } else &.{ "check-attr", "--stdin", "-z", "-a" }; const out = try repo.outputWithInput(args, input.items); defer gpa.free(out); // Which (path, pattern) pairs git says match. var matched: std.AutoHashMapUnmanaged(struct { usize, usize }, void) = .empty; defer matched.deinit(gpa); var fields = std.mem.splitScalar(u8, out, 0); while (fields.next()) |path| { if (path.len == 0) break; const attr = fields.next().?; _ = fields.next().?; const path_index = for (differential_paths, 0..) |p, k| { if (std.mem.eql(u8, p, path)) break k; } else return error.UnexpectedPath; const pattern_index = try std.fmt.parseInt(usize, attr[1..], 10); try matched.put(gpa, .{ path_index, pattern_index }, {}); } var compared: usize = 0; for (kept.items) |i| { for (differential_paths, 0..) |path, k| { const want = matched.contains(.{ k, i }); try expectMatch(want, patterns.items[i], path, .{ .pathname = true, .casefold = casefold }); compared += 1; } } try testing.expect(compared > 50_000); }}
test "patterns match names without the pathname rule as git tag -l matches them" { const gpa = testing.allocator; var repo = try fixture.Repo.init(gpa, testing.io); defer repo.deinit(); try repo.commit("first"); const names = [_][]const u8{ "a", "ab", "b", "A", "Ab", "a-b", "a.b", "c/d", "c/de", "e/f/g", "e/fg", "-a", "a]b", "!a", "a!b", "xay", "aXa/b", }; var updates: std.ArrayList(u8) = .empty; defer updates.deinit(gpa); for (names) |n| try updates.print(gpa, "create refs/tags/{s} HEAD\n", .{n}); const ignored = try repo.outputWithInput(&.{ "update-ref", "--stdin" }, updates.items); gpa.free(ignored);
const patterns = handwritten_patterns ++ [_][]const u8{ "a*", "*b", "a?b", "c*", "[a-c]*", "*/*", "e**g", "?", "*", "[!a]*", "a[!/]b", "*X*" }; for (patterns) |pattern| { for ([_]bool{ false, true }) |casefold| { const args: []const []const u8 = if (casefold) &.{ "tag", "-l", "-i", pattern } else &.{ "tag", "-l", pattern }; const out = try repo.output(args); defer gpa.free(out); for (names) |n| { var lines = std.mem.splitScalar(u8, out, '\n'); const want = while (lines.next()) |l| { if (std.mem.eql(u8, l, n)) break true; } else false; try expectMatch(want, pattern, n, .{ .casefold = casefold }); } } }}
fn fuzzOne(_: void, smith: *testing.Smith) anyerror!void { var buffer: [256]u8 = undefined; const input = fuzz_input.bytes(smith, &buffer); // The pattern, a NUL, and the text. const nul = std.mem.findScalar(u8, input, 0) orelse input.len; const pattern = input[0..nul]; const text = if (nul == input.len) "" else input[nul + 1 ..]; for (all_flags) |flags| { const got = match(pattern, text, flags); // The slow way is exponential, so it is asked only of what it can // answer quickly. if (pattern.len <= 24 and text.len <= 24) try testing.expectEqual(referenceMatch(pattern, text, flags), got); }}
test "fuzz: wildmatch" { try testing.fuzz({}, fuzzOne, .{ .corpus = &.{ @embedFile("fuzz/wildmatch-dirs"), @embedFile("fuzz/wildmatch-set"), @embedFile("fuzz/wildmatch-stars"), } });}