diff --git a/Makefile b/Makefile index d4047da5..061b075d 100644 --- a/Makefile +++ b/Makefile @@ -230,7 +230,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ $(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \ $(BIN)/test_dyn_ww $(BIN)/test_selfcheck $(BIN)/test_at_test_ww \ - $(BIN)/test_fmt_run $(BIN)/test_log_run \ + $(BIN)/test_fmt_run $(BIN)/test_log_run $(BIN)/test_fnmatch_run \ $(BIN)/test_memio_run $(BIN)/test_temp_run $(BIN)/test_getopt_run \ $(BIN)/test_base32_run $(BIN)/test_base64_run \ $(BIN)/test_adler32_run $(BIN)/test_crc16_run \ @@ -419,6 +419,10 @@ $(BIN)/test_log_run: test/wcc/971_log_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_fnmatch_run: test/wcc/972_fnmatch_run.c $(BIN)/ww $(BIN)/w6c \ + $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_memio_run: test/wcc/980_memio_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/lib/fnmatch/fnmatch.ww b/lib/fnmatch/fnmatch.ww new file mode 100644 index 00000000..6b87043c --- /dev/null +++ b/lib/fnmatch/fnmatch.ww @@ -0,0 +1,607 @@ +// fnmatch — shell wildcard pattern matching. Port of Hare's +// fnmatch:: (ref/hare/fnmatch/fnmatch.ha) using ww byte-indexed +// cursors in place of Hare's strings::iterator. +// +// Surface today: +// +// fnmatch.flag — enum i32 bitmask (NONE / PATHNAME / +// NOESCAPE / PERIOD) +// fnmatch.fnmatch(pattern: str, string: str, flags: flag) bool +// +// Matching rules (Hare-spec): +// +// - '?' matches any single byte +// - '*' matches any byte sequence (including empty) +// - '['…']' bracket expression. Inside: byte ranges (`a-z`), +// POSIX character classes ([:alnum:], [:alpha:], +// …), leading '!' negates. Hare-style only: '^' +// is *not* accepted as a negation marker; it +// yields invalid. +// - '\\' escape — `` is taken literally. Disabled by +// flag.NOESCAPE, in which case '\\' is itself a +// literal. +// - any other matches itself. +// +// Flags (see [[flag]]): +// +// - PATHNAME: '/' in the string is matched only by a literal '/' +// in the pattern (segments delimited by '/' are matched +// independently with PATHNAME stripped). +// - NOESCAPE: '\\' loses its escape meaning. +// - PERIOD: a leading '.' (or — under PATHNAME — a '.' +// immediately following a '/') must be matched by a literal +// '.' in the pattern, not by '?' / '*' / '[…]'. +// +// Divergence from Hare: +// +// - ww has no UTF-8 rune iteration on str; matching is byte-wise. +// Pattern and string bytes compare directly, bracket ranges +// `[a-z]` operate over byte values (ASCII-only meaningful; +// multibyte UTF-8 only matches byte-identically, not +// codepoint-equivalently). Same precedent as +// [[strings.byteindex]]'s rune-needle arm (which matches a +// single ASCII byte). When ww grows UTF-8 iteration, this +// module graduates "in one go" (lib/CLAUDE.md) and the +// byte-only caveat retires. +// +// - Hare returns invalid up the public surface via `?` so callers +// can distinguish bad patterns from non-matches. ww collapses +// invalid → false at the public boundary, matching Hare's +// fnmatch.ha:53 (`return b is bool && b: bool;`). +// +// - Hare's algorithm uses strings::iterator (with `next` / `prev` +// / `riter` / `slice` / `iterstr`). ww has none of these +// yet; the port models a cursor as a bare `i32` byte index +// into the source `str` and threads `(pat, *i32)` through +// pat_next / match_bracket / match_ctype. The tail-match phase +// uses a fresh forward cursor at `string.len - cnt` instead of +// a reverse iterator — produces the same byte sequence. +// +// - Hare's match_ctype builds a `[name, *fn(rune) bool]` table +// and dispatches via fn-pointer call; ww uses a sequence of +// `streq` comparisons routing to the matching [[ascii.is*]] +// predicate. Equivalent, narrower lowering. +// +// Owning model: pure function — no allocation, no mutation of +// caller-owned data. Pattern and string are read-only byte sources; +// the returned `bool` is the only outward effect. +// +// if (fnmatch.fnmatch("*.c", "foo.c", fnmatch.flag.NONE)) { … }; +// if (fnmatch.fnmatch("a/*.c", "a/x.c", +// fnmatch.flag.PATHNAME)) { … }; + +use ascii; +use strings; + +// flag — bitmask altering match semantics. Stored as `enum i32` +// to match [[os.flag]] / [[temp.mode]]; the four named values are +// small unsigned bit positions where i32-vs-u32 makes no +// observable difference. Bitwise AND/OR composition lives in the +// public boundary, which casts to i32 once and back. +export type flag = enum i32 { + NONE = 0, + PATHNAME = 1, + NOESCAPE = 2, + PERIOD = 4, +}; + +// invalid — internal sentinel for a malformed pattern (bad bracket, +// unterminated escape, unknown POSIX class, …). Module-private: +// Hare exposes this to callers via `?`, ww collapses it to `false` +// at the public boundary (mirrors Hare's `fnmatch` line 53). Once +// callers need pattern-validity diagnostics, a `try`-shaped public +// entry can be added without churning the existing surface. +type invalid = !void; + +// Token tags returned by [[pat_next]]. void aliases so they can sit +// alongside `u8` (a literal pattern byte) and `invalid` in one +// tagged-union return. Same shape as [[io.eof]] / [[io.closed]]. +type star = void; +type question = void; +type bracket = void; +type endt = void; + +// pat_next — consume one pattern element starting at `*pos`. +// Returns the matching token tag for metacharacters (`*` / `?` / +// `[`), `endt` at exhaustion, the literal byte otherwise. Under +// flag.NOESCAPE, `\\` is itself a literal byte; otherwise `\\` +// returns `` and an unterminated `\\` returns `invalid`. +// Mirrors Hare's pat_next (fnmatch.ha:313). +fn pat_next(pat: str, pos: *i32, fl: flag) (u8 | star | question | bracket | endt | invalid) = { + if (*pos >= pat.len) { let e: endt; return e; }; + let c: u8 = pat[*pos]; + *pos += 1; + if (c == 42u8) { let r: star; return r; }; // '*' + if (c == 63u8) { let r: question; return r; }; // '?' + if (c == 91u8) { let r: bracket; return r; }; // '[' + if (c == 92u8) { // '\\' + if (((fl as i32) & (flag.NOESCAPE as i32)) == 0) { + if (*pos >= pat.len) { let e: invalid; return e; }; + let c2: u8 = pat[*pos]; + *pos += 1; + return c2; + }; + return 92u8; + }; + return c; +}; + +// advance_or_err — read one byte from `s` at `*pos`, advancing. +// Returns invalid on exhaustion. Mirrors Hare's advance_or_err +// (fnmatch.ha:336), narrowed to bytes. +fn advance_or_err(s: str, pos: *i32) (u8 | invalid) = { + if (*pos >= s.len) { let e: invalid; return e; }; + let c: u8 = s[*pos]; + *pos += 1; + return c; +}; + +// streq — byte-equal compare. ascii is the only character set we +// fold against; the POSIX class names are pure ASCII, so a flat +// length-checked memcmp matches Hare's behavior here. +fn streq(a: str, b: str) bool = { + if (a.len != b.len) { return false; }; + let i: i32 = 0; + for (i < a.len) { + if (a[i] != b[i]) { return false; }; + i += 1; + }; + return true; +}; + +// match_ctype — parse a "[:name:]" character class. `*pos` points +// just past the leading colon on entry; the function consumes +// through the closing "]" and returns whether `c` is a member of +// the named class. Mirrors Hare's match_ctype (fnmatch.ha:284), +// modulo: ascii.is* are dispatched by direct name compare instead +// of through a fn-pointer table. +fn match_ctype(pat: str, pos: *i32, c: u8) (bool | invalid) = { + let nameoff: i32 = *pos; + let i: i32 = 0; + let r: u8 = 0u8; + for (r != 58u8) { // ':' + let rr = advance_or_err(pat, pos); + match (rr) { + case let e: invalid => return e; + case let f: u8 => { r = f; }; + }; + if (!ascii.valid(r: rune)) { + let e: invalid; return e; + }; + i += 1; + }; + let rr = advance_or_err(pat, pos); + match (rr) { + case let e: invalid => return e; + case let f: u8 => { + if (f != 93u8) { let e: invalid; return e; }; // ']' + }; + }; + let name: str; + name.ptr = pat.ptr + (nameoff: u64); + name.len = i - 1; + let cr: rune = c: rune; + if (streq(name, "alnum")) { return ascii.isalnum(cr); }; + if (streq(name, "alpha")) { return ascii.isalpha(cr); }; + if (streq(name, "blank")) { return ascii.isblank(cr); }; + if (streq(name, "cntrl")) { return ascii.iscntrl(cr); }; + if (streq(name, "digit")) { return ascii.isdigit(cr); }; + if (streq(name, "graph")) { return ascii.isgraph(cr); }; + if (streq(name, "lower")) { return ascii.islower(cr); }; + if (streq(name, "print")) { return ascii.isprint(cr); }; + if (streq(name, "punct")) { return ascii.ispunct(cr); }; + if (streq(name, "space")) { return ascii.isspace(cr); }; + if (streq(name, "upper")) { return ascii.isupper(cr); }; + if (streq(name, "xdigit")) { return ascii.isxdigit(cr); }; + let e: invalid; return e; +}; + +// match_bracket — consume one `[…]` group at `*pos` and decide +// whether `c` is a member. Mirrors Hare's match_bracket +// (fnmatch.ha:220). +// +// On entry `*pos` is positioned just past the leading '['; on +// successful return it sits just past the closing ']'. +fn match_bracket(pat: str, pos: *i32, c: u8) (bool | invalid) = { + let oldpos: i32 = *pos; + let r0 = advance_or_err(pat, pos); + let first: u8 = 0u8; + match (r0) { + case let e: invalid => return e; + case let f: u8 => { first = f; }; + }; + if (first == 94u8) { let e: invalid; return e; }; // '^' is Hare-invalid + let inv: bool = false; + if (first == 33u8) { // '!' + inv = true; + let r1 = advance_or_err(pat, pos); + match (r1) { + case let e: invalid => return e; + case let f: u8 => { first = f; }; + }; + }; + let found: bool = (first != 91u8) && (first == c); + let havelast: bool = true; + let last: u8 = first; + if (first == 93u8) { // ']' as first member + let r2 = advance_or_err(pat, pos); + match (r2) { + case let e: invalid => return e; + case let f: u8 => { first = f; }; + }; + }; + let r: u8 = first; + let loop: bool = true; + for (loop) { + if (r == 93u8) { // ']' closes + loop = false; + } else { if (r == 45u8) { // '-' range + let er = advance_or_err(pat, pos); + let endv: u8 = 0u8; + match (er) { + case let e: invalid => return e; + case let f: u8 => { endv = f; }; + }; + if (endv == 93u8) { + // Trailing '-' matches itself. Un-eat ']' + // so the next iteration sees it. + *pos -= 1; + last = 45u8; + havelast = true; + if (c == 45u8) { found = true; }; + } else { + if (!havelast) { let e: invalid; return e; }; + if ((last: u32) <= (c: u32) && (c: u32) <= (endv: u32)) { + found = true; + }; + havelast = false; // forbid `a-f-n` + }; + } else { if (r == 91u8) { // '[' + let nx = advance_or_err(pat, pos); + let nxv: u8 = 0u8; + match (nx) { + case let e: invalid => return e; + case let f: u8 => { nxv = f; }; + }; + if (nxv == 61u8) { let e: invalid; return e; }; // '=' + if (nxv == 46u8) { let e: invalid; return e; }; // '.' + if (nxv == 58u8) { // ':' class + let t = match_ctype(pat, pos, c); + match (t) { + case let e: invalid => return e; + case let v: bool => { + if (v) { found = true; }; + }; + }; + } else { + // Not a class — un-eat the byte after '['. + *pos -= 1; + if (c == 91u8) { found = true; }; + }; + last = 91u8; + havelast = true; + } else { // literal byte + if (c == r) { found = true; }; + last = r; + havelast = true; + }; }; }; + if (loop) { + let rr = advance_or_err(pat, pos); + match (rr) { + case let e: invalid => return e; + case let f: u8 => { r = f; }; + }; + }; + }; + // Hare's degeneracy check: `[xyx]` where the bracket spans + // exactly `[X..X]` for X in `=`, `.`, `:` is invalid (would + // have been an empty collating/equivalence/class). + let cnt: i32 = *pos - oldpos; + if (havelast && first == last && cnt >= 4) { + if (first == 61u8) { let e: invalid; return e; }; + if (first == 46u8) { let e: invalid; return e; }; + if (first == 58u8) { let e: invalid; return e; }; + }; + if (inv) { return !found; }; + return found; +}; + +// fnmatch_internal — core "sea of stars" matcher. Drives the same +// three-phase algorithm as Hare (fnmatch.ha:101): +// +// 1. Exact match against pattern up to the first '*' (prefix). +// 2. Exact match of the remaining pattern after the last '*' +// against the corresponding tail of the string. +// 3. Greedy left-to-right match of each pattern segment between +// stars against the leftover middle of the string. +// +// Step (3) has no exponential corner because each star anchors a +// "match found" at increasing string positions. +fn fnmatch_internal(pattern: str, string: str, fl: flag) (bool | invalid) = { + if (((fl as i32) & (flag.PERIOD as i32)) != 0) { + if (strings.hasprefix(string, ".") && !strings.hasprefix(pattern, ".")) { + return false; + }; + }; + let pp: i32 = 0; + let sp: i32 = 0; + + // ---- prefix: match up to the first '*' ---------------------- + let sawstar: bool = false; + for (!sawstar) { + let scur: i32 = sp; + let isdone: bool = (scur >= string.len); + let curs: u8 = 0u8; + if (!isdone) { curs = string[scur]; scur += 1; }; + + let t = pat_next(pattern, &pp, fl); + let advance: bool = true; + match (t) { + case let e: invalid => return e; + case star => { sawstar = true; advance = false; }; + case endt => { return isdone; }; + case question => { + if (isdone) { return false; }; + }; + case bracket => { + if (isdone) { return false; }; + let mr = match_bracket(pattern, &pp, curs); + match (mr) { + case let e: invalid => return e; + case let b: bool => { if (!b) { return false; }; }; + }; + }; + case let r: u8 => { + if (isdone) { return false; }; + if (curs != r) { return false; }; + }; + }; + if (advance) { sp = scur; }; + }; + + // ---- find the tail (token count after the last '*') --------- + let pp_copy: i32 = pp; + let pp_last: i32 = pp; + let pp_last_cnt: i32 = 0; + let cnt: i32 = 0; + let pdone: bool = false; + for (!pdone) { + let t = pat_next(pattern, &pp, fl); + match (t) { + case let e: invalid => return e; + case endt => { pdone = true; }; + case star => { + pp_last = pp; + pp_last_cnt = cnt + 1; + cnt += 1; + }; + case bracket => { + let mr = match_bracket(pattern, &pp, 0u8); + match (mr) { + case let e: invalid => return e; + case let _b: bool => { }; + }; + cnt += 1; + }; + case question => { cnt += 1; }; + case let r: u8 => { cnt += 1; }; + }; + }; + pp = pp_last; + let tail_cnt: i32 = cnt - pp_last_cnt; + + // String has to leave room for the post-star tail; the prefix + // already consumed up to sp. + let sp_copy: i32 = sp; + let tail_pos: i32 = string.len - tail_cnt; + if (tail_pos < sp_copy) { return false; }; + + // ---- tail: match (from pp) against string[tail_pos..] ------- + let ts: i32 = tail_pos; + let tdone: bool = false; + for (!tdone) { + let t = pat_next(pattern, &pp, fl); + let havec: bool = (ts < string.len); + let ch: u8 = 0u8; + if (havec) { ch = string[ts]; }; + match (t) { + case let e: invalid => return e; + case endt => { + if (havec) { return false; }; + tdone = true; + }; + case star => { + // Unreachable — pp starts after pp_last (past the + // last star), so pat_next can't yield another star. + // Treat defensively as invalid pattern. + let e: invalid; return e; + }; + case question => { + if (!havec) { return false; }; + ts += 1; + }; + case bracket => { + if (!havec) { return false; }; + let mr = match_bracket(pattern, &pp, ch); + match (mr) { + case let e: invalid => return e; + case let b: bool => { if (!b) { return false; }; }; + }; + ts += 1; + }; + case let r: u8 => { + if (!havec) { return false; }; + if (ch != r) { return false; }; + ts += 1; + }; + }; + }; + + // ---- middle: greedy match of each star-delimited subpattern -- + let mid_pat: str; + mid_pat.ptr = pattern.ptr + (pp_copy: u64); + mid_pat.len = pp_last - pp_copy; + let mid_str: str; + mid_str.ptr = string.ptr + (sp_copy: u64); + mid_str.len = tail_pos - sp_copy; + + let mpp_copy: i32 = 0; + let msp_copy: i32 = 0; + + let outer: bool = true; + for (outer) { + let mpp: i32 = mpp_copy; + if (mpp >= mid_pat.len) { return true; }; + let msp: i32 = msp_copy; + let inner: bool = true; + let sawstarm: bool = false; + for (inner) { + let scur: i32 = msp; + let isdone: bool = (scur >= mid_str.len); + let cur: u8 = 0u8; + if (!isdone) { cur = mid_str[scur]; scur += 1; }; + + let t = pat_next(mid_pat, &mpp, fl); + let matched: bool = false; + let advance: bool = true; + match (t) { + case let e: invalid => return e; + case endt => { + // mid_pat by construction never ends with end + // here (the last token was a star); treat as + // invalid defensively. + let e: invalid; return e; + }; + case question => { matched = !isdone; }; + case bracket => { + if (isdone) { matched = false; } + else { + let mr = match_bracket(mid_pat, &mpp, cur); + match (mr) { + case let e: invalid => return e; + case let b: bool => { matched = b; }; + }; + }; + }; + case let r: u8 => { + matched = (!isdone) && (cur == r); + }; + case star => { + mpp_copy = mpp; + msp_copy = msp; + inner = false; + sawstarm = true; + advance = false; + }; + }; + if (advance) { + if (!matched) { + inner = false; + } else { + msp = scur; + }; + }; + }; + if (!sawstarm) { + // Inner failed — advance the outer string anchor by + // one byte and retry from the top of mid_pat. + if (msp_copy >= mid_str.len) { return false; }; + msp_copy += 1; + }; + }; + return false; +}; + +// fnmatch_pathname — PATHNAME variant: split pattern and string on +// '/' and match each segment independently with [[fnmatch_internal]]. +// Mirrors Hare's fnmatch_pathname (fnmatch.ha:58). +fn fnmatch_pathname(pattern: str, string: str, fl: flag) (bool | invalid) = { + let pp: i32 = 0; + let segstart_pat: i32 = 0; + let sp: i32 = 0; + let donetokens: bool = false; + let final: bool = false; + for (!final) { + segstart_pat = pp; + // Walk pattern until the next '/' (segment break) or end. + let inner: bool = true; + let kind: i32 = 0; // 0 = saw '/', 1 = saw end + for (inner) { + let t = pat_next(pattern, &pp, fl); + match (t) { + case let e: invalid => return e; + case endt => { kind = 1; inner = false; }; + case let r: u8 => { + if (r == 47u8) { kind = 0; inner = false; }; + }; + case bracket => { + let mr = match_bracket(pattern, &pp, 0u8); + match (mr) { + case let e: invalid => return e; + case let _b: bool => { }; + }; + }; + case question => { }; + case star => { }; + }; + }; + // Pull the next string token (Hare's strings::next_token + // on `/`). + if (donetokens) { return false; }; + let segstart_s: i32 = sp; + for (sp < string.len) { + if (string[sp] == 47u8) { break; }; + sp += 1; + }; + let seg: str; + seg.ptr = string.ptr + (segstart_s: u64); + seg.len = sp - segstart_s; + let hadslash: bool = (sp < string.len); + if (hadslash) { sp += 1; } + else { donetokens = true; }; + + let p_slice: str; + if (kind == 0) { + // pp is just past the '/'; slice excludes it. + p_slice.ptr = pattern.ptr + (segstart_pat: u64); + p_slice.len = (pp - 1) - segstart_pat; + } else { + // kind == 1: end of pattern; this is the final + // segment-pattern. Whole tail of pattern from + // segstart_pat is the slice. + p_slice.ptr = pattern.ptr + (segstart_pat: u64); + p_slice.len = pp - segstart_pat; + final = true; + }; + let r = fnmatch_internal(p_slice, seg, fl); + match (r) { + case let e: invalid => return e; + case let b: bool => { + if (!b) { return false; }; + }; + }; + if (final) { + // Final segment: require string also exhausted + // (Hare's `&& strings::next_token(&tok) is done`). + if (!donetokens) { return false; }; + return true; + }; + }; + return false; +}; + +// fnmatch — public entry. Selects PATHNAME-split or core matcher +// based on `flags`; invalid pattern → false (Hare's collapsing +// semantic, fnmatch.ha:53). +export fn fnmatch(pattern: str, string: str, flags: flag) bool = { + let r: (bool | invalid); + if (((flags as i32) & (flag.PATHNAME as i32)) != 0) { + r = fnmatch_pathname(pattern, string, flags); + } else { + r = fnmatch_internal(pattern, string, flags); + }; + match (r) { + case invalid => return false; + case let b: bool => return b; + }; +}; diff --git a/lib/fnmatch/fnmatchtest.ww b/lib/fnmatch/fnmatchtest.ww new file mode 100644 index 00000000..54c237a5 --- /dev/null +++ b/lib/fnmatch/fnmatchtest.ww @@ -0,0 +1,263 @@ +// fnmatchtest — exercises lib/fnmatch. Run with +// `out/bin/ww run lib/fnmatch/fnmatchtest.ww`. +// +// Table-driven via the [[check]] helper: every row is the uniform +// 4-tuple `(pattern, string, expected, flags)`. Cases follow Hare's +// ref/hare/fnmatch/+test.ha (the de-facto spec for this port); +// multibyte UTF-8 rows (わたし, ε) are skipped — our matcher is +// byte-wise and those would match byte-identically but obscure the +// ASCII semantics the test is asserting. +// +// Failure path: each @test fn bumps `signalled` to its slot index, +// `check` does `exit(signalled + 10)` on miscompare so the harness +// reports `WEXITSTATUS = 11..N` pointing at the failing scenario. +// Same convention as lib/log/logtest. + +use fnmatch; + +// Direct rt_syscall binding rather than `use os;` — os exports +// read/write/close, which collide with io.read/write/close under the +// driver's flat-scope concat. Mirrors logtest / fmttest / bufiotest. +@symbol("rt_syscall") fn syscall1ww(num: i64, a: i64) i64; +fn doexit(code: i32) void = { + syscall1ww(60i64, code: i64); +}; + +let signalled: i32 = 0; + +fn fail() void = { doexit(signalled + 10); }; + +// check — pin one row. `flags` is taken as i32 and cast to +// fnmatch.flag so the test rows can read as integer bitmasks +// without dragging enum literals into every line. +fn check(pat: str, s: str, expected: bool, flags: i32) void = { + let f: fnmatch.flag = flags as fnmatch.flag; + if (fnmatch.fnmatch(pat, s, f) != expected) { fail(); }; +}; + +// ---- basic literal / wildcard cases --------------------------------- + +@test fn basic() void = { + check("a", "a", true, 0); + check("b", "b", true, 0); + check("\0", "\0", true, 0); + check("abcde", "abcde", true, 0); + check("aaa", "bbb", false, 0); + + check("ab*cde", "abcde", true, 0); + check("g*a", "gordana", true, 0); + check("ab*cde*foo*bar", "abcdefooba", false, 0); + check("*", "foo", true, 0); + check("aa*", "aafoo", true, 0); + check("bb*", "foo", false, 0); + check("*cc", "foocc", true, 0); + check("*dd", "foo", false, 0); + check("ra**ra", "rarara", true, 0); + check("x*yy*x", "xxyyyyyxxx", true, 0); + check("*", "*", true, 0); + check("*", "", true, 0); + check("****", "a", true, 0); + check("**a**", "a", true, 0); + check("****", "", true, 0); + + check("?", "*", true, 0); + check("?", "", false, 0); + check("??", "a", false, 0); + check("??", "abc", false, 0); + check("?aa", "bbb", false, 0); + + check("**?**", "", false, 0); + check("*?*?*?*?", "abcd", true, 0); + check("*?*?*?*?", "abc", false, 0); +}; + +// ---- bracket expressions: literal / range / negation ---------------- + +@test fn brackets() void = { + check("[b]", "b", true, 0); + check("a[b]c", "abc", true, 0); + check("a[b]c", "axc", false, 0); + check("[a-c]", "b", true, 0); + check("[a-z]", "a", true, 0); + check("[c-a]", "b", false, 0); + check("x[a-c]y", "xay", true, 0); + check("x[a-c]y", "xby", true, 0); + check("x[a-c]y", "xcy", true, 0); + check("x[a-c]y", "xzy", false, 0); + check("x[a-c]y", "xy", false, 0); + check("[a-c]*[a-c]", "axxxb", true, 0); + + // Special members: '-' at start/end, ']' as first member. + check("[-]", "-", true, 0); + check("[.]", ".", true, 0); + check("[:ias]", ":", true, 0); + check("[-]", "a", false, 0); + check("[-ac]", "a", true, 0); + check("[-ac]", "-", true, 0); + check("[-ac]", "b", false, 0); + check("[ac-]", "a", true, 0); + check("[ac-]", "-", true, 0); + check("[ac-]", "b", false, 0); + + // Equivalence-class-like syntax: invalid in Hare/POSIX. + check("[.a.]", "a", false, 0); + + // Negation with '!'. + check("[!b]", "b", false, 0); + check("a[!b]c", "abc", false, 0); + check("a[!b]c", "axc", true, 0); + check("[!a-c]", "b", false, 0); + check("[!c-a]", "b", true, 0); + check("x[!a-c]y", "xay", false, 0); + check("x[!a-c]y", "xby", false, 0); + check("x[!a-c]y", "xcy", false, 0); + check("x[!a-c]y", "xzy", true, 0); + check("x[!a-c]y", "xy", false, 0); + check("[!a-c]*[!a-c]", "axxxb", false, 0); + + check("[!-]", "-", false, 0); + check("[!-]", "a", true, 0); + check("[!-ac]", "a", false, 0); + check("[!-ac]", "-", false, 0); + check("[!-ac]", "b", true, 0); +}; + +// ---- POSIX character classes: [[:alnum:]] / [[:alpha:]] / ... ------- + +@test fn ctype() void = { + check("[[:alnum:]]", "7", true, 0); + check("[[:alpha:]]", "[", false, 0); + check("[[:alpha:]]", "[[", false, 0); + check("[[alpha:]]", "a]", true, 0); + check("[[alpha:]]", ":]", true, 0); + check("[[:alpha:]]", "a", true, 0); + check("[[:blank:]]", " ", true, 0); + check("[[:alnum:]]a", "a]a", false, 0); + check("[[:alnum:][[:digit:]]", "a", true, 0); + + check("[![:alnum:]]", "a", false, 0); + check("[![:alpha:]]", "[", true, 0); + check("[![:alnum:]]a", "a]a", false, 0); + check("[![:alpha:]]a", "[a", true, 0); + check("[![:alnum:][:digit:]]", "a", false, 0); +}; + +// ---- flag.PERIOD: leading '.' must be literal in the pattern -------- + +@test fn period() void = { + let fp: i32 = 4; // flag.PERIOD + check(".", ".", true, fp); + check("*", ".", false, fp); + check("?", ".", false, fp); + check("[.]", ".", false, fp); + check(".*", ".asdf", true, fp); + check(".*", "asdf", false, fp); +}; + +// ---- flag.NOESCAPE: '\\' loses its escape meaning ------------------- + +@test fn noescape() void = { + let nesc: i32 = 2; // flag.NOESCAPE + check("\\", "\\", true, nesc); + check("\\*", "\\asdf", true, nesc); +}; + +// ---- musl-adapted cases (no flags) --------------------------------- + +@test fn musl_basic() void = { + check("*.c", "foo.c", true, 0); + check("*.c", ".c", true, 0); + check("*.a", "foo.c", false, 0); + check("*.c", ".foo.c", true, 0); + check("a\\*.c", "ax.c", false, 0); + check("a[xy].c", "ax.c", true, 0); + check("a[!y].c", "ax.c", true, 0); + check("-O[01]", "-O1", true, 0); + check("[[?*\\]", "\\", true, 0); + check("[]?*\\]", "]", true, 0); + check("[!]a-]", "b", true, 0); + check("[]-_]", "^", true, 0); + check("[!]-_]", "X", true, 0); + check("??", "-", false, 0); + + // Without PATHNAME, `[...]` and `?` happily eat '/'. + check("[*]/b", "a/b", false, 0); + check("[*]/b", "*/b", true, 0); + check("[?]/b", "a/b", false, 0); + check("[?]/b", "?/b", true, 0); + check("[[a]/b", "a/b", true, 0); + check("[[a]/b", "[/b", true, 0); + check("\\*/b", "a/b", false, 0); + check("\\*/b", "*/b", true, 0); + check("\\?/b", "a/b", false, 0); + check("\\?/b", "?/b", true, 0); + check("[/b", "[/b", false, 0); + check("\\[/b", "[/b", true, 0); + check("a[/]b", "a/b", true, 0); + + // "[![:d-d]" — bracket carrying a class-name start without + // proper close-bracket is treated as a non-class literal set. + // Hare's table asserts false in all three rows. + check("[![:d-d]", "b", false, 0); + check("[[:d-d]", "[", false, 0); + check("[![:d-d]", "[", false, 0); +}; + +// ---- flag.PATHNAME: '/' must be matched by literal '/' -------------- + +@test fn pathname() void = { + let fp: i32 = 1; // flag.PATHNAME + check("[a-z]/[a-z]", "a/b", true, fp); + check("a[/]b", "a/b", false, fp); + check("*", "a/b", false, fp); + check("*[/]b", "a/b", false, fp); + check("*[b]", "a/b", false, fp); + check("a[a/z]*.c", "a/x.c", false, fp); + check("a/*.c", "a/x.c", true, fp); + check("a*.c", "a/x.c", false, fp); + check("*/foo", "/foo", true, fp); + check("???b", "aa/b", false, fp); +}; + +// ---- flag.PERIOD + flag.PATHNAME combined --------------------------- + +@test fn combined() void = { + let pp: i32 = 1; // PATHNAME + let fp: i32 = 4; // PERIOD + let pf: i32 = pp | fp; + check("?a/b", ".a/b", false, pf); + check("a/?b", "a/.b", false, pf); + check("*a/b", ".a/b", false, pf); + check("a/*b", "a/.b", false, pf); + check("[.]a/b", ".a/b", false, pf); + check("a/[.]b", "a/.b", false, pf); + check("*/?", "a/b", true, pf); + check("?/*", "a/b", true, pf); + check(".*/?", ".a/b", true, pf); + check("*/.?", "a/.b", true, pf); + check("*/*", "a/.b", false, pf); + check("*?*/*", "a/.b", true, fp); + check("*[.]/b", "a./b", true, pf); + check("*[[:alpha:]]/*[[:alnum:]]", "a/b", true, pp); + check("a?b", "a.b", true, pf); + check("a*b", "a.b", true, pf); + check("a[.]b", "a.b", true, pf); + + check("*.c", ".foo.c", false, fp); + check("*.c", "foo.c", true, fp); + + let nesc: i32 = 2; // NOESCAPE + check("a\\*.c", "a*.c", false, nesc); +}; + +export fn main() i32 = { + signalled = 1; basic(); + signalled = 2; brackets(); + signalled = 3; ctype(); + signalled = 4; period(); + signalled = 5; noescape(); + signalled = 6; musl_basic(); + signalled = 7; pathname(); + signalled = 8; combined(); + return 0; +}; diff --git a/test/wcc/972_fnmatch_run.c b/test/wcc/972_fnmatch_run.c new file mode 100644 index 00000000..57dd2bdf --- /dev/null +++ b/test/wcc/972_fnmatch_run.c @@ -0,0 +1,52 @@ +/* + * 972_fnmatch_run — execute the lib/fnmatch @test fixture under the + * C-side `ww run` driver and assert exit 0. + * + * fnmatch is a pure leaf (depends on ascii + strings only) sitting + * alongside fmt (970) and log (971) in the stdlib-runtime band. The + * fixture (fnmatchtest.ww) carries its own `export fn main()` that + * drives the @test fns and signals which case failed via the exit + * code, so this file is a thin wrapper — no @test scanning, no + * synthetic main generation. + */ +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + + const char *src = "lib/fnmatch/fnmatchtest.ww"; + char path[1024], cmd[2048]; + snprintf(path, sizeof path, "%s/%s", cwd, src); + snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path); + int rc = runwait(cmd); + if (rc != 0) { + fprintf(stderr, "fnmatch_run FAIL: %s exited %d\n", src, rc); + return 1; + } + printf("fnmatch_run: %s ok\n", src); + return 0; +}