zsh/ported/pattern.rs
1//! Pattern matching — port of `Src/pattern.c`.
2//!
3//! This is the bytecode-based port. The C source compiles patterns
4//! into a flat `char *patout` buffer of packed opcodes; the matcher
5//! is an interpreter that walks the buffer using pointer arithmetic.
6//!
7//! Rust port preserves the bytecode architecture using `Vec<u8>`:
8//! * Each opcode is 1 byte (matches C `Upat::c`).
9//! * `next_off` is a 4-byte little-endian `u32` offset to the next
10//! opcode in sequence (0 = end). C uses native-endian `long`; we
11//! pin to LE for portable on-disk bytecode caches.
12//! * Payloads (strings, ranges, branch operands) are encoded inline
13//! after the `next_off` slot.
14//!
15//! Top-level declaration order mirrors `Src/pattern.c`:
16//! 1. Opcode constants (P_*) — pattern.c:97-127
17//! 2. Macro-style accessors (P_OP / P_NEXT / etc.) — pattern.c:175-210
18//! 3. Flag-bit constants (P_SIMPLE / P_HSTART / P_PURESTR) — pattern.c:216
19//! 4. `struct patprog` — zsh.h:1601
20//! 5. PAT_* flag constants — zsh.h:1623
21//! 6. ZPC_* indexes — zsh.h:1644
22//! 7. File-static globals — pattern.c file-scope
23//! 8. Bytecode write helpers (`patadd`, `patnode`, `patinsert`,
24//! `pattail`, `patoptail`, `patcompcharsset`, `patcompstart`)
25//! 9. Compiler entry points (`patcompile`, `patcompswitch`,
26//! `patcompbranch`, `patcomppiece`, `patcompnot`)
27//! 10. Glob-flag parser (`patgetglobflags`)
28//! 11. Range helpers (`range_type`, `pattern_range_to_string`)
29//! 12. Char-decode helpers (`charref`, `charnext`, `charrefinc`,
30//! `charsub`, `metacharinc`, `clear_shiftstate`)
31//! 13. Matcher entry points (`pattry`, `pattrylen`, `pattryrefs`)
32//! 14. `patmatch` interpreter — pattern.c:2694
33//! 15. `patmatchrange` — pattern.c:3865. The Cpattern-byte-stream
34//! walker, relocated here from `zle/compmatch.rs` (the Rule C
35//! move this section used to say was pending). The earlier
36//! Rust-only entries `patmatchrange(&[char], char, igncase)` and
37//! `patmatchindex(&[char], idx)` were deleted as Rule B /
38//! semantic deviations; see the deletion sites.
39//! 16. String pre-processing (`patmungestring`, `patallocstr`,
40//! `pattrystart`)
41//! 17. Module-loader / disable mgmt (`startpatternscope`,
42//! `endpatternscope`, `savepatterndisables`,
43//! `restorepatterndisables`, `clearpatterndisables`,
44//! `freepatprog`, `pat_enables`)
45//! 18. (Section removed — formerly held Rust-only convenience entries
46//! `patmatch(pat, text)`, `patmatchlen(prog, string)`, `patrepeat(
47//! prog, s, max)`, `mb_patmatchrange`, `mb_patmatchindex`. All
48//! deleted: `patmatch` got renamed to the C-faithful bytecode-
49//! walker name; the others had zero Rust callers + Rule S1
50//! signature deviations. `haswilds` is a real C name — port lives
51//! in section 4 helpers.)
52//!
53//! See `docs/PORT.md` Rules A/B/C/D/E.
54
55#![allow(non_upper_case_globals)]
56#![allow(non_camel_case_types)]
57
58use crate::ported::params::{paramtab, paramtab_hashed_storage};
59use crate::ported::utils::ztrsub;
60use crate::ported::zle::zle_h::{COMP_LIST_COMPLETE, COMP_LIST_EXPAND};
61pub use crate::ported::zsh_h::{
62 patstralloc, Patstralloc, GF_BACKREF, GF_IGNCASE, GF_LCMATCHUC, GF_MATCHREF, GF_MULTIBYTE,
63 PAT_ANY, PAT_FILE, PAT_FILET, PAT_HAS_EXCLUDP, PAT_HEAPDUP, PAT_LCMATCHUC, PAT_NOANCH,
64 PAT_NOGLD, PAT_NOTEND, PAT_NOTSTART, PAT_PURES, PAT_SCAN, PAT_STATIC, PAT_ZDUP, PP_ALNUM,
65 PP_ALPHA, PP_ASCII, PP_BLANK, PP_CNTRL, PP_DIGIT, PP_GRAPH, PP_IDENT, PP_IFS, PP_IFSSPACE,
66 PP_INCOMPLETE, PP_INVALID, PP_LOWER, PP_PRINT, PP_PUNCT, PP_RANGE, PP_SPACE, PP_UPPER, PP_WORD,
67 PP_XDIGIT, ZMB_INCOMPLETE, ZMB_INVALID, ZPC_SEG_COUNT,
68};
69use crate::utils::zerrnam;
70use crate::zsh_h::{
71 isset, patprog, Bang, Bar, Hat, Inang, Inbrack, Inpar, Marker, Meta, Nularg, Outbrack, Pound,
72 Quest, Star, BASHAUTOLIST, CASEGLOB, CASEPATHS, EXTENDEDGLOB, KSHGLOB, MULTIBYTE,
73 NUMERICGLOBSORT, PM_HASHED, PM_TYPE, RCQUOTES, SHGLOB, SORTIT_IGNORING_BACKSLASHES,
74 SORTIT_NUMERICALLY, ZPC_BAR, ZPC_BNULLKEEP, ZPC_COUNT, ZPC_HASH, ZPC_HAT, ZPC_INANG,
75 ZPC_INBRACK, ZPC_INPAR, ZPC_KSH_AT, ZPC_KSH_BANG, ZPC_KSH_BANG2, ZPC_KSH_PLUS, ZPC_KSH_QUEST,
76 ZPC_KSH_STAR, ZPC_NULL, ZPC_OUTPAR, ZPC_QUEST, ZPC_SLASH, ZPC_STAR, ZPC_TILDE,
77};
78use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering};
79use std::sync::Mutex;
80
81// =====================================================================
82// 6. ZPC_* enum from zsh.h:1644 — indexes into the active-pattern-
83// characters table that compile-time and runtime both consult.
84// =====================================================================
85
86/// Maximum captures, from `pattern.c:94 NSUBEXP`.
87pub const NSUBEXP: usize = 9;
88
89// =====================================================================
90// 1. P_* opcode constants — pattern.c:97-127
91//
92// Numbered identically to C so a buffer compiled by this port matches
93// the C source's bytecode-cache format byte-for-byte (modulo native
94// endianness; we pin LE — see file header).
95// =====================================================================
96/// `P_END` constant.
97pub const P_END: u8 = 0x00; // c:97 End of program.
98/// `P_EXCSYNC` constant.
99pub const P_EXCSYNC: u8 = 0x01; // c:98 Test if following exclude already failed
100/// `P_EXCEND` constant.
101pub const P_EXCEND: u8 = 0x02; // c:99 Test if exclude matched orig branch
102/// `P_BACK` constant.
103pub const P_BACK: u8 = 0x03; // c:100 Match "", "next" ptr points backward.
104/// `P_EXACTLY` constant.
105pub const P_EXACTLY: u8 = 0x04; // c:101 lstr — match this string.
106/// `P_NOTHING` constant.
107pub const P_NOTHING: u8 = 0x05; // c:102 Match empty string.
108/// `P_ONEHASH` constant.
109pub const P_ONEHASH: u8 = 0x06; // c:103 node — match 0 or more of preceding simple.
110/// `P_TWOHASH` constant.
111pub const P_TWOHASH: u8 = 0x07; // c:104 node — match 1 or more of preceding simple.
112/// `P_GFLAGS` constant.
113pub const P_GFLAGS: u8 = 0x08; // c:105 long — match nothing and set globbing flags.
114/// `P_ISSTART` constant.
115pub const P_ISSTART: u8 = 0x09; // c:106 Match start of string.
116/// `P_ISEND` constant.
117pub const P_ISEND: u8 = 0x0a; // c:107 Match end of string.
118/// `P_COUNTSTART` constant.
119pub const P_COUNTSTART: u8 = 0x0b; // c:108 Initialise P_COUNT.
120/// `P_COUNT` constant.
121pub const P_COUNT: u8 = 0x0c; // c:109 3*long uc* node — match a number of repetitions.
122/// `P_BRANCH` constant.
123pub const P_BRANCH: u8 = 0x20; // c:112 node — match this alternative, or the next.
124/// `P_WBRANCH` constant.
125pub const P_WBRANCH: u8 = 0x21; // c:113 uc* node — P_BRANCH, but match at least 1 char.
126/// `P_EXCLUDE` constant.
127pub const P_EXCLUDE: u8 = 0x30; // c:114 uc* node — exclude this from previous branch.
128/// `P_EXCLUDP` constant.
129pub const P_EXCLUDP: u8 = 0x31; // c:115 uc* node — exclude, using full file path so far.
130/// `P_ANY` constant.
131pub const P_ANY: u8 = 0x40; // c:117 Match any one character.
132/// `P_ANYOF` constant.
133pub const P_ANYOF: u8 = 0x41; // c:118 str — match any character in this string.
134/// `P_ANYBUT` constant.
135pub const P_ANYBUT: u8 = 0x42; // c:119 str — match any character not in this string.
136/// `P_STAR` constant.
137pub const P_STAR: u8 = 0x43; // c:120 Match any set of characters.
138/// `P_NUMRNG` constant.
139pub const P_NUMRNG: u8 = 0x44; // c:121 zr,zr — match a numeric range.
140/// `P_NUMFROM` constant.
141pub const P_NUMFROM: u8 = 0x45; // c:122 zr — match a number >= X.
142/// `P_NUMTO` constant.
143pub const P_NUMTO: u8 = 0x46; // c:123 zr — match a number <= X.
144/// `P_NUMANY` constant.
145pub const P_NUMANY: u8 = 0x47; // c:124 Match any set of decimal digits.
146/// `P_OPEN` constant.
147pub const P_OPEN: u8 = 0x80; // c:126 Mark this point in input as start of n.
148/// `P_CLOSE` constant.
149pub const P_CLOSE: u8 = 0x90; // c:127 Analogous to OPEN.
150
151/// Port of `P_ISBRANCH()` from `Src/pattern.c:200`.
152/// C macro `#define P_ISBRANCH(p) ((p)->l & 0x20)`.
153#[inline]
154pub fn P_ISBRANCH(op: u8) -> bool {
155 (op & 0x20) != 0
156}
157
158/// Port of `P_ISEXCLUDE()` from `Src/pattern.c:201`.
159/// C macro `#define P_ISEXCLUDE(p) (((p)->l & 0x30) == 0x30)`.
160#[inline]
161pub fn P_ISEXCLUDE(op: u8) -> bool {
162 (op & 0x30) == 0x30
163}
164
165/// Port of `P_NOTDOT()` from `Src/pattern.c:202`.
166/// C macro `#define P_NOTDOT(p) ((p)->l & 0x40)`.
167#[inline]
168pub fn P_NOTDOT(op: u8) -> bool {
169 (op & 0x40) != 0
170}
171
172// =====================================================================
173// 3. Flag-bit constants returned via flagp out-params during compile.
174// pattern.c:216-218
175// =====================================================================
176/// `P_SIMPLE` constant.
177pub const P_SIMPLE: i32 = 0x01; // c:216 Simple enough to be # / ## operand.
178/// `P_HSTART` constant.
179pub const P_HSTART: i32 = 0x02; // c:217 Starts with # or ##'d pattern.
180/// `P_PURESTR` constant.
181pub const P_PURESTR: i32 = 0x04; // c:218 Can be matched with a strcmp.
182
183// =====================================================================
184// 5. PAT_* flag constants — re-exports of zsh.h:1623-1640 already in
185// zsh_h.rs. Re-published here so callers in pattern's API don't need
186// the longer path. C source has these as `#define` in zsh.h, not
187// pattern.c, so the canonical home is zsh_h.rs; we just alias.
188// =====================================================================
189
190// C: `static int patnpar;` — number of active parens (1-indexed at
191// compile time; the *struct* patnpar is the actual count).
192pub static patnpar: AtomicI32 = AtomicI32::new(0); // c:271
193
194// GF_* glob-flag bits live in `zsh.h:1763-1773`, ported to
195// `src/ported/zsh_h.rs:2287-2291` per Rule C. Re-export so pattern's
196// matcher arms can read them without the longer path.
197
198// =====================================================================
199// 7. File-static globals — direct mirror of pattern.c file-scope
200// statics. Each C `static` ports to a Rust `static` of matching name
201// (Rule A) and `Mutex<>` / `Atomic*` for thread-safe access. None
202// are aggregated (Rule D).
203// =====================================================================
204
205// C: `static char *patout, *patcode;` + `static long patsize;` +
206// `static int patalloc;` — pattern.c:267-272 (in macro region).
207//
208// patout: the bytecode buffer.
209// patcode: write cursor (offset into patout).
210// patsize: current logical size.
211// patalloc: allocated capacity.
212//
213// In Rust, `Vec<u8>` already tracks both len and capacity, so we
214// hold just the buffer; patcode/patsize/patalloc are derived.
215pub static patout: Mutex<Vec<u8>> = Mutex::new(Vec::new()); // c:267
216
217// C: `static int patflags;` — current PAT_* flag set during compile.
218pub static patflags: AtomicI32 = AtomicI32::new(0); // c:272
219
220// C: `static int patglobflags;` — current globbing flags during compile.
221pub static patglobflags: AtomicI32 = AtomicI32::new(0); // c:273
222
223/// Port of file-static `int patinlen` from `pattrystate` struct
224/// at `Src/pattern.c:1877` (accessed via `#define patinlen
225/// (pattrystate.patinlen)` at line 1894). Length in metafied bytes
226/// of the last successful pattry match; computed at the end of
227/// `pattry`/`pattrylen`/`pattryrefs` (`patinput - patinstart`,
228/// pattern.c:2508). Read by `patmatchlen()` and by the
229/// `${var//pat/repl}` paramsubst pipeline that needs to know how
230/// much of the input was consumed.
231pub static patinlen: AtomicI32 = AtomicI32::new(0); // c:1877
232
233/// Port of file-static `int globdots` from the `pattrystate` struct at
234/// `Src/pattern.c:1890` ("Glob initial dots?"), reached through
235/// `#define globdots (pattrystate.globdots)` at `Src/pattern.c:1903`.
236/// Set once per top-level match from the program's flags —
237/// `Src/pattern.c:2494` `globdots = !(patflags & PAT_NOGLD);` — and
238/// forced on for the duration of an exclusion operand
239/// (`Src/pattern.c:3116` `globdots = 1; /* OK to match . first */`,
240/// restored at `Src/pattern.c:3192`).
241///
242/// The matcher reads it in exactly two places, both guarded by
243/// `P_NOTDOT` — the "opcode has bit 0x40" test (`Src/pattern.c:202`)
244/// that marks the wildcard opcodes `P_ANY`/`P_ANYOF`/`P_ANYBUT`/
245/// `P_STAR`/`P_NUM*` (`Src/pattern.c:116-124`): `Src/pattern.c:2731`
246/// (top of the node walk) and `Src/pattern.c:3322` (the closure
247/// operand). That is the whole of zsh's "a wildcard does not match a
248/// leading dot" rule; a LITERAL dot in the pattern is unaffected
249/// because `P_EXACTLY` does not carry bit 0x40.
250pub static globdots: AtomicI32 = AtomicI32::new(1); // c:1890
251
252// =====================================================================
253// 12. Char-decode helpers — pattern.c:327, :336, :1909-1997
254// =====================================================================
255
256/// Port of `clear_shiftstate()` from `Src/pattern.c:327`. C uses
257/// `mbstate_t`; Rust `char` is already a code point, so no shift
258/// state to clear.
259pub fn clear_shiftstate() {} // c:327
260
261/// Port of `metacharinc()` from `Src/pattern.c:336`.
262/// C: `static wchar_t metacharinc(char **x)`.
263/// Advances `*x` past one metafied / multibyte char and returns the
264/// decoded codepoint. The C body branches on:
265/// - `GF_MULTIBYTE` clear OR high-bit-clear: single-byte path
266/// with `itok(*x)` zsh-token translation and `Meta` xor-32
267/// - else: `mbrtowc` over the metafied bytes with state machine
268///
269/// **Rust port:** UTF-32-native delegation to `&str.chars()`.
270/// Delegates to Rust's native UTF-8 iterator — both C branches
271/// collapse because Rust's `char` is already the wide-char form,
272/// and the stored slice is the post-Meta-decode form (zshrs's
273/// source bytes are already UTF-8, not Meta-encoded). The `itok` →
274/// `ztokens[]` zsh-token table mapping doesn't apply because the
275/// pattern compiler stores raw chars; translation happens at
276/// compile time, not at scan time.
277///
278/// Rust signature differs from C `metacharinc(char **x)`: C mutates
279/// `*x` to advance the pointer; Rust returns the new byte position
280/// since `&str` length is immutable. Callers update their cursor
281/// from the return value.
282pub fn metacharinc(s: &str, pos: usize) -> usize {
283 // c:336
284 // c:343-360 single-byte short-circuit + c:363-380 mbrtowc loop:
285 // both collapse to Rust's `chars().next()` which decodes one
286 // valid UTF-8 codepoint from the slice, regardless of byte width.
287 s[pos..]
288 .chars()
289 .next()
290 .map(|c| pos + c.len_utf8())
291 .unwrap_or(pos)
292}
293
294// =====================================================================
295// 8. Bytecode write helpers — pattern.c:412-1856
296// =====================================================================
297
298/// Port of the anonymous `enum { PA_NOALIGN = 1, PA_UNMETA = 2 };`
299/// from `Src/pattern.c:405-408`. Flags passed as the `paflags` arg
300/// to `patadd`.
301pub const PA_NOALIGN: i32 = 1; // c:406
302/// `PA_UNMETA` constant.
303pub const PA_UNMETA: i32 = 2; // c:407
304
305/// Port of `patadd()` from `Src/pattern.c:412`.
306/// C: `static void patadd(char *add, int ch, long n,
307/// int paflags)` from `Src/pattern.c:410-450`.
308///
309/// Append `n` bytes from `add` (or a single `ch` byte when `add ==
310/// None`) to `patout`, growing the backing storage if needed and
311/// padding to the C `union upat` alignment unless `PA_NOALIGN` is
312/// set. With `PA_UNMETA`, walk the input as zsh-metafied bytes
313/// (Meta + (b^32)) and untokenize (`itok` → `ztokens[]`).
314///
315/// **The previous Rust impl had three concrete defects:**
316/// 1. Declared `-> i64` returning the starting offset. C is
317/// `static void`; callers use side-effects on `patcode`,
318/// not a return value.
319/// 2. When `add == None`, the C body writes `ch` **once**
320/// (`*patcode++ = ch;`). The Rust port looped `0..n` pushing
321/// `ch` n times, so a single-byte literal grew to n copies.
322/// 3. Skipped the C alignment-round-up to `sizeof(union upat)`
323/// (8 bytes on every supported arch — c:417-418), so any
324/// caller reading `patout` as a `[upat]` would mis-align.
325///
326/// zshrs's `patcode` pointer collapses into `patout.len()`; the
327/// realloc dance in C (c:419-425) is implicit via `Vec::extend`.
328/// `patsize` updates are the post-write `patout.len()`.
329fn patadd(add: Option<&[u8]>, ch: u8, n: i64, paflags: i32) {
330 use crate::ported::lex::ztokens as ztokens_str;
331 use crate::ported::zsh_h::Pound;
332 use crate::ported::ztype_h::itok;
333 let ztokens = ztokens_str.as_bytes();
334 // c:412
335 // c:415 — `long newpatsize = patsize + n;`
336 let mut buf = patout.lock().unwrap();
337 let patsize = buf.len() as i64;
338 let mut newpatsize = patsize + n;
339 // c:416-418 — round up to upat-union alignment (sizeof(union upat) =
340 // 8 on every supported arch) unless PA_NOALIGN.
341 if (paflags & PA_NOALIGN) == 0 {
342 // c:416
343 let upat = 8i64; // c:417 sizeof(union upat)
344 newpatsize = (newpatsize + upat - 1) & !(upat - 1); // c:417-418
345 }
346 // c:419-425 — realloc; Rust Vec auto-grows so just resize_to.
347 if newpatsize > buf.len() as i64 {
348 buf.resize(newpatsize as usize, 0);
349 }
350 // c:426 — `patsize = newpatsize;` — patsize is the buffer's
351 // logical write head; zshrs tracks it via buf.len() AFTER the
352 // write below.
353 let mut patcode_pos = patsize as usize; // c:Src/pattern.c — `patcode` pointer.
354
355 if let Some(add_bytes) = add {
356 // c:427 — `if (add) {`
357 if (paflags & PA_UNMETA) != 0 {
358 // c:428-442 — PA_UNMETA: walk add_bytes, unmetafy + untokenize
359 // as we go. Meta chars aren't counted in n. itok bytes route
360 // through ztokens[*add - Pound].
361 let mut idx = 0usize;
362 let mut remaining = n;
363 while remaining > 0 && idx < add_bytes.len() {
364 let b = add_bytes[idx];
365 if itok(b) {
366 // c:434-435 — `if (itok(*add)) *patcode++ =
367 // ztokens[*add++ - Pound];`
368 if idx < add_bytes.len() {
369 let tok_idx = b.wrapping_sub(Pound as u8) as usize;
370 if tok_idx < ztokens.len() {
371 // c:435
372 if patcode_pos < buf.len() {
373 buf[patcode_pos] = ztokens[tok_idx];
374 }
375 patcode_pos += 1;
376 }
377 idx += 1;
378 }
379 } else if b == Meta {
380 // c:436-438 — `else if (*add == Meta) { add++;
381 // *patcode++ = *add++ ^ 32; }`
382 idx += 1;
383 if idx < add_bytes.len() {
384 if patcode_pos < buf.len() {
385 buf[patcode_pos] = add_bytes[idx] ^ 32;
386 }
387 patcode_pos += 1;
388 idx += 1;
389 }
390 } else {
391 // c:439-440 — `else { *patcode++ = *add++; }`
392 if patcode_pos < buf.len() {
393 buf[patcode_pos] = b;
394 }
395 patcode_pos += 1;
396 idx += 1;
397 }
398 remaining -= 1;
399 }
400 } else {
401 // c:444-445 — `while (n--) *patcode++ = *add++;` — plain copy.
402 let n_actual = (n as usize).min(add_bytes.len());
403 for &b in &add_bytes[..n_actual] {
404 if patcode_pos < buf.len() {
405 buf[patcode_pos] = b;
406 }
407 patcode_pos += 1;
408 }
409 }
410 } else {
411 // c:447-448 — `else *patcode++ = ch;` — single-byte write.
412 if patcode_pos < buf.len() {
413 buf[patcode_pos] = ch;
414 }
415 patcode_pos += 1;
416 }
417 // c:449 — `patcode = patout + patsize;` — restore the post-write
418 // head so subsequent patadd calls append at the next slot. zshrs
419 // tracks via buf.len(); trim back to newpatsize so the alignment
420 // padding stays visible to consumers as zeroes.
421 let _ = patcode_pos;
422 // (No explicit truncate — buf.len() == newpatsize already.)
423}
424
425/// Port of `patcompcharsset()` from `Src/pattern.c:464`.
426///
427/// Initializes the `zpc_special` table for the active globbing
428/// regime. The C source resets every ZPC_* slot to its literal
429/// character, then masks off characters via `Marker` (0xa2 — the
430/// canonical "invalid" sentinel) for three option-driven cases:
431/// 1. `!isset(EXTENDEDGLOB)` → Tilde/Hat/Hash disabled.
432/// 2. `!isset(KSHGLOB)` → KSH_QUEST/STAR/PLUS/BANG/BANG2/AT disabled.
433/// 3. `isset(SHGLOB)` → Inpar/Inang disabled.
434///
435pub fn patcompcharsset() {
436 // c:464
437 let mut sp = zpc_special.lock().unwrap();
438 *sp = [0u8; ZPC_COUNT as usize];
439 // c:469 — `memcpy(zpc_special, zpc_chars, ZPC_COUNT)`. The default
440 // char for every ZPC_* slot. Direct positional assignment here
441 // since zshrs doesn't carry the `zpc_chars` const array yet.
442 //
443 // NOTE on token bytes: C's `zpc_chars[]` (pattern.c:248) uses
444 // the TOKENIZED high-bit bytes (`Bar`, `Tilde`, etc.); the C
445 // shell lexer tokenizes raw ASCII to these only inside grouped
446 // alternation. zshrs's parser doesn't yet run that pre-tokenize
447 // pass — every byte reaches patcompile as raw ASCII. Switching
448 // these slots to the high-bit form would break ~80 library
449 // tests that rely on raw `|` triggering alternation. The
450 // narrower fix for bug #12 lives in vm_helper::glob_match_static
451 // where we conservatively escape pattern bytes the param-strip
452 // path could not have intended as metacharacters.
453 sp[ZPC_SLASH as usize] = b'/';
454 sp[ZPC_NULL as usize] = 0;
455 sp[ZPC_BAR as usize] = b'|';
456 sp[ZPC_OUTPAR as usize] = b')';
457 sp[ZPC_TILDE as usize] = b'~';
458 sp[ZPC_INPAR as usize] = b'(';
459 sp[ZPC_QUEST as usize] = b'?';
460 sp[ZPC_STAR as usize] = b'*';
461 sp[ZPC_INBRACK as usize] = b'[';
462 sp[ZPC_INANG as usize] = b'<';
463 sp[ZPC_HAT as usize] = b'^';
464 sp[ZPC_HASH as usize] = b'#';
465 sp[ZPC_BNULLKEEP as usize] = 0;
466 // c:478-490 — KSH_GLOB slots (omitted from previous Rust port).
467 // Each defaults to the literal ksh-glob trigger char. The
468 // option-mask pass below disables them when KSHGLOB is off.
469 sp[ZPC_KSH_QUEST as usize] = b'?';
470 sp[ZPC_KSH_STAR as usize] = b'*';
471 sp[ZPC_KSH_PLUS as usize] = b'+';
472 sp[ZPC_KSH_BANG as usize] = b'!';
473 sp[ZPC_KSH_BANG2 as usize] = b'!';
474 sp[ZPC_KSH_AT as usize] = b'@';
475
476 let marker_byte = Marker as u32 as u8;
477
478 // c:471-478 — `for (...; i < ZPC_COUNT; ...) if (*disp) *spp = Marker;`
479 // Apply user disables from `disable -p` BEFORE the option-driven
480 // masks (so EXTENDEDGLOB / KSHGLOB / SHGLOB layer over the per-
481 // pattern disables).
482 {
483 let disp = zpc_disables.lock().unwrap();
484 for i in 0..(ZPC_COUNT as usize) {
485 if disp[i] != 0 {
486 sp[i] = marker_byte; // c:476
487 }
488 }
489 }
490
491 // c:480-483 — `if (!isset(EXTENDEDGLOB))` mask Tilde/Hat/Hash.
492 if !isset(EXTENDEDGLOB) {
493 sp[ZPC_TILDE as usize] = marker_byte;
494 sp[ZPC_HAT as usize] = marker_byte;
495 sp[ZPC_HASH as usize] = marker_byte;
496 }
497
498 // c:485-491 — `if (!isset(KSHGLOB))` mask the six KSH_* slots.
499 if !isset(KSHGLOB) {
500 sp[ZPC_KSH_QUEST as usize] = marker_byte;
501 sp[ZPC_KSH_STAR as usize] = marker_byte;
502 sp[ZPC_KSH_PLUS as usize] = marker_byte;
503 sp[ZPC_KSH_BANG as usize] = marker_byte;
504 sp[ZPC_KSH_BANG2 as usize] = marker_byte;
505 sp[ZPC_KSH_AT as usize] = marker_byte;
506 }
507
508 // c:499-505 — `if (isset(SHGLOB))` mask Inpar/Inang (case/numeric
509 // ranges not valid under sh-emulation).
510 if isset(SHGLOB) {
511 sp[ZPC_INPAR as usize] = marker_byte;
512 sp[ZPC_INANG as usize] = marker_byte;
513 }
514}
515
516/// Port of `patcompstart()` from `Src/pattern.c:517`.
517///
518/// Resets per-compile globals. Called at the start of `patcompile`.
519///
520/// **C body (c:517-526)** — strict order matters:
521/// 1. `patcompcharsset()` — must run FIRST so the zpc_special
522/// table reflects the current option state before parsing.
523/// 2. `patglobflags = isset(CASEGLOB) || isset(CASEPATHS) ? 0 :
524/// GF_IGNCASE;` — case-insensitivity is the default UNLESS
525/// one of the case-sensitive options is set.
526/// 3. `if (isset(MULTIBYTE)) patglobflags |= GF_MULTIBYTE;` —
527/// multibyte handling is option-gated, NOT unconditional.
528///
529/// The previous Rust port had three divergences: (a) called
530/// patcompcharsset LAST instead of FIRST, (b) unconditionally set
531/// GF_MULTIBYTE even when `setopt nomultibyte`, (c) NEVER set
532/// GF_IGNCASE — `setopt nocaseglob` had zero effect on pattern
533/// case-folding.
534pub fn patcompstart() {
535 // c:517
536 // c:519 — `patcompcharsset()` FIRST.
537 patcompcharsset();
538 patout.lock().unwrap().clear();
539 patnpar.store(1, Ordering::Relaxed);
540 patflags.store(0, Ordering::Relaxed);
541 // c:520-523 — CASE option dispatch.
542 let mut flags: i32 = if isset(CASEGLOB) || isset(CASEPATHS) {
543 0 // c:521 case-sensitive
544 } else {
545 GF_IGNCASE // c:523 default = ignore case
546 };
547 // c:524-525 — MULTIBYTE option respect.
548 if isset(MULTIBYTE) {
549 flags |= GF_MULTIBYTE; // c:525
550 }
551 patglobflags.store(flags, Ordering::Relaxed);
552 errsfound.store(0, Ordering::Relaxed);
553 forceerrs.store(-1, Ordering::Relaxed);
554 patparse_off.store(0, Ordering::Relaxed);
555}
556
557// =====================================================================
558// 9. Compiler entry points — pattern.c:540
559// =====================================================================
560
561/// Port of `patcompile(char *exp, int inflags, char **endexp)` from `Src/pattern.c:540`.
562///
563/// C signature: `Patprog patcompile(char *exp, int inflags, char **endexp)`.
564/// Compiles pattern `exp` under flags `inflags`, returns a `Patprog`
565/// on success or `NULL` on failure. `endexp` (if non-NULL) is set to
566/// the input cursor at end of parse — used by `bin_zregexparse` to
567/// detect partial-parse cases.
568pub fn patcompile(exp: &str, inflags: i32, mut endexp: Option<&mut String>) -> Option<Patprog> {
569 // Global compiled-pattern cache (crate::pat_cache, a zshrs-only opt —
570 // see that module for the safety/key/threading rationale). Skipped when
571 // `endexp` is requested: that out-param is a compile side effect the
572 // cache can't reproduce. The check runs BEFORE the compile mutex so a
573 // hit never serialises on the compiler.
574 let cacheable = endexp.is_none();
575 if cacheable {
576 if let Some(hit) = crate::pat_cache::get(exp, inflags) {
577 return Some(hit);
578 }
579 }
580 // Capture the ORIGINAL pattern text for the cache-store key: `exp` is
581 // shadowed below by the decoded form, and the option state that feeds
582 // the fingerprint is unchanged between here and the store (patcompstart
583 // only reads options), so get/put keys match.
584 let exp_orig: String = if cacheable {
585 exp.to_string()
586 } else {
587 String::new()
588 };
589 // Hold the compile mutex for the entire body — `patcompstart`
590 // resets every file-scope static (`Src/pattern.c:267-281`) and the
591 // emit/parse helpers mutate them in sequence. C is single-threaded
592 // so the statics are race-free there; Rust must serialise.
593 let _compile_guard = PATCOMPILE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
594 // c:1610 `patstartch` — the leading plain character of the pattern.
595 // C only needs the NOGLD leading-dot rule to know whether a pattern
596 // explicitly begins with `.` (so `.*` matches dot files while `*`
597 // does not). Capture it before `exp` is shadowed by the normalizer
598 // below. A leading glob token (Star/Quest/Inbrack/…) is not plain, so
599 // only a literal `.` is recorded; everything else stays 0.
600 let patstartch_lead: u8 = if exp.starts_with('.') { b'.' } else { 0 };
601 // c:796 — `patcompstart()` has exactly ONE caller in C: `parsepat`
602 // (Src/glob.c:796), which runs it ONCE for a whole file glob before
603 // `parsecomplist` compiles the individual path components. C's
604 // `patcompile` never re-enters it. zshrs calls it here instead, which is
605 // harmless for the char-set setup but NOT for the globflags seed: a
606 // pattern-level `(#i)` that `parsepat` folded into `patglobflags`
607 // (Src/glob.c:801-807) was wiped on entry to every component after the
608 // first, so `(#i)SUB/DEEP.TXT` matched case-insensitively in `SUB` and
609 // case-SENSITIVELY in `DEEP.TXT` — i.e. not at all. Snapshot the incoming
610 // value and restore it for a FILE compile, which is exactly what C's
611 // `patcompile` does by leaving `patglobflags` alone unless the pattern is
612 // non-FILE (c:568-576).
613 let incoming_globflags = patglobflags.load(Ordering::Relaxed);
614 patcompstart();
615 if (inflags & PAT_FILE as i32) != 0 {
616 patglobflags.store(incoming_globflags, Ordering::Relaxed); // c:568 — no reset for a file glob
617 } else {
618 // c:568-570 — `if (!(patflags & PAT_FILE)) { patcompcharsset();
619 // zpc_special[ZPC_SLASH] = Marker; … }`. A non-file pattern has no
620 // path components, so '/' is an ordinary character. C masks the slot
621 // rather than testing PAT_FILE at each use site, which is what lets
622 // `patcompbranch` (c:950) break on '/' unconditionally while
623 // `[[ x = ((#s)|/)a ]]` and `${arr:#*/*}` keep '/' literal.
624 // `patcompstart` → `patcompcharsset` re-seeds the slot to '/' on every
625 // compile (pattern.rs:434), so this mask does not leak between calls.
626 zpc_special.lock().unwrap()[ZPC_SLASH as usize] = Marker as u32 as u8; // c:570
627 }
628 // c:525 — `patcompstart` seeds `patglobflags` with the option-derived
629 // default (GF_IGNCASE when CASEGLOB/CASEPATHS are off, GF_MULTIBYTE when
630 // MULTIBYTE is on). The `patglobflags.store(0)` reset below (clean slate
631 // for the `(#…)` hoist loop) would otherwise drop that seed before the
632 // prog's globflags are built — capture the case bits now so `pattry`
633 // honors `setopt nocaseglob`.
634 let mut seeded_globflags = patglobflags.load(Ordering::Relaxed);
635 // c:568-576 — `patcompile` RESETS patglobflags to `GF_MULTIBYTE`/0 for a
636 // NON-FILE pattern, DROPPING the nocaseglob-derived GF_IGNCASE that
637 // patcompstart seeded. `setopt nocaseglob` makes only FILENAME GLOBBING
638 // (PAT_FILE) case-insensitive; `[[ ]]`, `${arr:#pat}`, `case`, and `${p//pat}`
639 // stay case-SENSITIVE. Without this drop, `setopt nocaseglob; [[ ABC == abc ]]`
640 // wrongly matched and `${(ABC):#abc}` wrongly filtered. GF_MULTIBYTE stays.
641 if (inflags & PAT_FILE as i32) == 0 {
642 seeded_globflags &= !GF_IGNCASE;
643 }
644 // === C-contract input decode (zpc_chars, Src/pattern.c:248) =====
645 // C's patcompile consumes the LEXER'S tokenized encoding: glob
646 // metachars arrive as token bytes (`Pound`..`Bang`, zsh.h:159-183
647 // — zpc_chars holds `Star`/`Quest`/`Inbrack`/... so a raw `*`
648 // NEVER dispatches as ZPC_STAR), quoted/escaped chars arrive as
649 // `Bnull`/`Bnullkeep` + payload (zsh.h:195-200), and `Nularg` is
650 // stripped (remnulargs). Callers pair `tokenize()` with
651 // `patcompile()` exactly like C (zutil.c:734, etc.).
652 //
653 // The Rust parser below uses a raw-ASCII internal encoding with
654 // `\X` as its literal form; transpose the C contract onto it:
655 // token char (U+e184..=U+e19E) -> ztokens[c - Pound] (meta)
656 // Bnull/Bnullkeep + X (U+e19F/U+e1A0) -> \X (literal)
657 // Nularg (U+e1A1) -> dropped
658 // Meta (U+0083) + X -> \X (literal)
659 // raw `\` + X -> \X passthrough — raw
660 // backslash is the parser's established Bnull-equivalent
661 // (see the `\X` arm below); C's lexer encodes the same
662 // quoting info as Bnull+X before zshtokenize ever runs.
663 // raw ASCII glob metachar -> \X (literal)
664 // `/`, `+`, `!`, `@` stay verbatim — C's zpc_chars keeps those
665 // slots RAW (path split + ksh-glob triggers fire on raw bytes).
666 // c:Src/pattern.c:751 — C sets `*endexp = patparse`, a POINTER INTO
667 // the caller's own tokenized buffer, so the remainder keeps its exact
668 // original bytes. zshrs normalizes the input below, so record for each
669 // NORMALIZED char the index of the ORIGINAL char it came from; the
670 // endexp sites then slice the ORIGINAL string instead of re-tokenizing
671 // the normalized suffix. That round-trip was lossy: parsecomplist
672 // strips the leading `(` of `(sub/)#end` before calling patcompile, so
673 // the normalizer saw the group's `Outpar` as UNBALANCED and demoted it
674 // to a literal `\)`, and re-tokenizing produced `Bnull )` where C still
675 // has `Outpar`. The `(dir/)#` branch's c:752 test
676 // `instr[1] == Outpar` then failed and the closure never formed.
677 let exp_tokenized: Vec<char> = exp.chars().collect();
678 let mut norm_to_orig: Vec<usize> = Vec::with_capacity(exp.len());
679 let exp: String = {
680 let ztokens: Vec<char> = crate::ported::glob::ZTOKENS.chars().collect();
681 let chars: Vec<char> = exp.chars().collect();
682 let mut out = String::with_capacity(exp.len());
683 // Track open Inpar/Outpar groups so an UNBALANCED Outpar token
684 // (a `)` with no group to close) transposes to a literal instead
685 // of the raw `)` metachar. C's tokenizer never emits an Outpar
686 // for an unbalanced `)`, and patcompile then treats it as an
687 // ordinary string char (Src/pattern.c:1294-1298: "allow ')' as an
688 // ordinary string character if there are no parentheses to
689 // close"). Without this, `${arr:#*)--*}` (e.g. _rm's option
690 // filter) broke at the stray `)` and collapsed to `*`. Note the
691 // asymmetry: an unbalanced `(` is a "bad pattern" in zsh, not a
692 // literal, so only Outpar is demoted here.
693 let mut open_paren: i32 = 0;
694 // Inside a `[...]` bracket class, `(`/`)` (Inpar/Outpar tokens) are
695 // LITERAL members, not group delimiters — so they must not move
696 // `open_paren`. The lexer tokenizes a `)` inside `[^)]` to Outpar
697 // (0x8a) all the same, so without this guard the bracket-interior
698 // Outpar wrongly decremented open_paren, and the real group-close
699 // then read `open_paren==0` and got demoted to a literal — collapsing
700 // `(|[^)]#…)` (every `_gnu_generic`/`_arguments --help` option-dedup
701 // pattern) into a "bad pattern". Track Inbrack(0x91)/Outbrack(0x92).
702 let mut in_bracket = false;
703 let mut i = 0;
704 // Emit one normalized char and record which ORIGINAL char it came
705 // from, so `endexp` can be sliced out of the untouched tokenized
706 // input the way C's `*endexp = patparse` pointer is. Indexed by
707 // BYTE offset into `out`, because both endexp sites slice the
708 // normalized string by byte offset. Defined after `i` so
709 // macro_rules definition-site hygiene resolves it to the cursor.
710 macro_rules! opush {
711 ($c:expr) => {{
712 let before = out.len();
713 out.push($c);
714 for _ in before..out.len() {
715 norm_to_orig.push(i);
716 }
717 }};
718 }
719 while i < chars.len() {
720 let c = chars[i];
721 let cu = c as u32;
722 if cu == 0xe191 {
723 in_bracket = true; // Inbrack — fall through to the generic `[` emit.
724 } else if cu == 0xe192 {
725 in_bracket = false; // Outbrack — fall through to the generic `]` emit.
726 }
727 if cu == 0xe188 && !in_bracket {
728 // Inpar — opens a group.
729 open_paren += 1;
730 opush!('(');
731 i += 1;
732 continue;
733 }
734 if cu == 0xe18a && !in_bracket {
735 // Outpar — closes an open group, else literal `)`.
736 if open_paren > 0 {
737 open_paren -= 1;
738 opush!(')');
739 } else {
740 opush!('\\');
741 opush!(')');
742 }
743 i += 1;
744 continue;
745 }
746 if cu == 0x83 {
747 // Meta + payload — a metafied RAW byte
748 // (vm_helper::meta_encode_byte, c:Src/utils.c:7289-
749 // 7294). C's pattern matcher compares metafied bytes
750 // on BOTH sides, so the compiled pattern must match
751 // the pair AS STORED in the subject string: emit both
752 // chars as literals (`\Meta \payload`). The previous
753 // `\payload` form dropped the Meta char and never
754 // matched a metafied subject — `[[ $'\xff' ==
755 // $'\xff' ]]` failed. Bug #127.
756 opush!('\\');
757 opush!(c);
758 if i + 1 < chars.len() {
759 opush!('\\');
760 opush!(chars[i + 1]);
761 i += 2;
762 } else {
763 i += 1;
764 }
765 continue;
766 }
767 if cu == 0xe19f || cu == 0xe1a0 {
768 // Bnull / Bnullkeep — payload is a literal.
769 if i + 1 < chars.len() {
770 opush!('\\');
771 opush!(chars[i + 1]);
772 i += 2;
773 } else {
774 i += 1;
775 }
776 continue;
777 }
778 if cu == 0xe1a1 {
779 // Nularg — stripped like C's remnulargs.
780 i += 1;
781 continue;
782 }
783 if (0xe184..=0xe19e).contains(&cu) {
784 // Token -> the raw metachar the parser dispatches on.
785 opush!(ztokens[(cu - 0xe184) as usize]);
786 i += 1;
787 continue;
788 }
789 if c == '\\' {
790 // Raw `\X` — Bnull-equivalent quoting marker in the
791 // zshrs pipeline; pass both through to the parser's
792 // `\X` literal arm. Trailing lone `\` stays itself.
793 //
794 // C draws two provenances here that this arm cannot see
795 // apart (docs/BUGS.md #1090):
796 // source quote — `[[ "man ls" = man\ * ]]`, `${v//\%/%%}`
797 // pattern DATA — `p='a\ b'; ${arr[(I)$p]}`
798 // c:Src/glob.c:3633-3643 `zshtokenize` honors the escape only
799 // before a `ztokens` metacharacter and leaves BOTH bytes as
800 // literals before anything else — so in C a raw backslash in
801 // the tokenized input is DATA and a quote is `Bnull`. Every
802 // source path in zshrs hands this arm a raw backslash for the
803 // quote (the cond/case pattern builder in
804 // `extensions::compile_zsh`, `${…//…}`'s builder in
805 // `ported::subst`), so the raw form has to keep meaning
806 // "quote" here; enforcing the C rule at this site alone
807 // regressed 8 real-world corpus tests across zinit / p10k /
808 // zpwr / fsh.
809 //
810 // The provenance is therefore settled UPSTREAM instead: a
811 // pattern built from a VALUE spells a data backslash `\\`
812 // (this arm's literal-backslash form) before it gets here,
813 // via `crate::pattern_data_escape::escape_data_backslashes`.
814 // Two families of caller do that:
815 // * `ported::subst`'s paramsubst, for the search-subscript
816 // patterns (`${a[(I)…]}` / `(i)` / `(r)` / `(R)` / `(K)`)
817 // — the whole set that reaches `patcompile` through
818 // `zshtokenize` alone (c:Src/params.c:1727).
819 // * the cond/case pattern builder
820 // (`extensions::compile_zsh::emit_glob_subst_pattern`),
821 // for a `${~spec}` / `$~spec` segment and for
822 // `setopt globsubst` — the `strcatsub` `shtokenize` C
823 // runs at c:Src/subst.c:822/830. The escape is emitted
824 // ONLY on that pattern path, so a NON-pattern use of
825 // `${~arr[i]}` (which nothing untokenizes back) still
826 // prints its single backslash.
827 // Covered by `tests/parity/cond_parity.rs`'s
828 // `backslash_provenance_in_patterns`.
829 opush!('\\');
830 if i + 1 < chars.len() {
831 opush!(chars[i + 1]);
832 i += 2;
833 } else {
834 i += 1;
835 }
836 continue;
837 }
838 if matches!(c, '*' | '?' | '[' | '(' | ')' | '|' | '~' | '^' | '#' | '<') {
839 // Untokenized ASCII metachar — literal per zpc_chars.
840 opush!('\\');
841 opush!(c);
842 i += 1;
843 continue;
844 }
845 opush!(c);
846 i += 1;
847 }
848 out
849 };
850 // c:751 — resolve a byte offset in the NORMALIZED string back to the
851 // corresponding suffix of the ORIGINAL tokenized input, which is what
852 // C's `*endexp = patparse` yields for free (it is a pointer into the
853 // caller's own buffer). An offset past the last recorded char means
854 // the whole pattern was consumed, so the remainder is empty.
855 let orig_remainder = |orig: &[char], map: &[usize], norm_byte_off: usize| -> String {
856 let start = map.get(norm_byte_off).copied().unwrap_or(orig.len());
857 orig[start.min(orig.len())..].iter().collect()
858 };
859 let exp = exp.as_str();
860 *patstart.lock().unwrap() = exp.to_string();
861 *patparse.lock().unwrap() = exp.to_string();
862 patflags.store(
863 inflags & !(PAT_PURES | PAT_HAS_EXCLUDP) as i32,
864 Ordering::Relaxed,
865 ); // c:566
866 // c:568-576 — `patcompile` re-seeds the RUNNING `patglobflags`
867 // (`if (isset(MULTIBYTE)) patglobflags = GF_MULTIBYTE; else
868 // patglobflags = 0;`) for a non-file pattern, and leaves
869 // `patcompstart`'s equally option-gated seed alone for a file glob.
870 // Every P_GFLAGS node emitted below records that running value
871 // verbatim (c:993 `up.l = patglobflags`), so storing a bare 0 here
872 // made a mid-pattern `(#i)` / `(#b)` node clear GF_MULTIBYTE for the
873 // remainder of the match (c:2942 assigns the payload ABSOLUTELY).
874 //
875 // GF_MULTIBYTE is forced ON rather than taken from `seeded_globflags`:
876 // the option gate is correct C (c:572-575) but zshrs's capture and
877 // substitution layers index the subject as UTF-8, and `unsetopt
878 // multibyte` makes zsh match in raw BYTES (c:1946), which those layers
879 // cannot yet represent — see the `charinc` note below.
880 patglobflags.store(seeded_globflags | GF_MULTIBYTE, Ordering::Relaxed);
881
882 // c:583-590 — leading `(#...)` flag specifiers are hoisted into
883 // patprog.globflags so the matcher applies them globally for the
884 // whole match; mid-pattern `(#...)` forms emit real P_GFLAGS nodes
885 // (see the patnode(P_GFLAGS) sites below).
886 // c:525 — `patglobflags = isset(MULTIBYTE) ? GF_MULTIBYTE : 0`.
887 // (#U) inside the spec clears the bit per c:1116.
888 //
889 // c:953-957 — C gates the `(#...)` recognition on
890 // `*patparse == zpc_special[ZPC_INPAR] &&`
891 // `patparse[1] == zpc_special[ZPC_HASH]`.
892 // When EXTENDEDGLOB is off, patcompcharsset (c:480-483) masks
893 // ZPC_HASH to the Marker byte (c:476). The second-byte equality
894 // therefore fails and the parser falls through to "treat `(` as
895 // a literal group" (c:1020+). Previously this Rust hoist loop
896 // compared the raw byte `b'#'`, allowing `(#s)` / `(#e)` /
897 // `(#i)` to fire even without EXTENDEDGLOB (parity bugs #18/#19
898 // vs real zsh).
899 // c:525 — the compiled prog's globflags accumulate from `patglobflags`,
900 // which `patcompstart` seeded with GF_IGNCASE when CASEGLOB/CASEPATHS are
901 // off (`setopt nocaseglob`). Carry those case bits through so `pattry`
902 // matches case-insensitively; `matchpat` masks the loss by pre-folding
903 // both sides, but the glob scanner now drives `pattry` directly.
904 //
905 // c:572-575 — `if (isset(MULTIBYTE)) patglobflags = GF_MULTIBYTE; else
906 // patglobflags = 0;`. The bit is OPTION-GATED in C, but it is kept
907 // unconditional here (see the `patglobflags.store` note above): zshrs's
908 // capture / substitution layers slice the subject as UTF-8, and the
909 // nomultibyte unit is a raw BYTE. An explicit in-pattern `(#U)` still
910 // clears it below (c:1116), which the matcher now honours.
911 //
912 // The `0xff` is the `(#aN)` APPROXIMATION BUDGET (c:1054-1066), which C
913 // carries in the low byte of the same word. Masking it off here dropped a
914 // budget that `parsepat` (c:801-807) had set from a PATTERN-LEVEL `(#aN)`
915 // in front of a multi-component path: c:568 leaves `patglobflags` alone for
916 // a PAT_FILE compile precisely so the budget reaches every component, and
917 // the restore at pattern.rs:615 put it back only for this mask to discard
918 // it. `(#i)` survived (its bit is in the mask) while `(#a1)` did not, so
919 // `(#a1)/abs/path/ofo` matched nothing where zsh 5.9.2 answers
920 // `/abs/path/{fo,foo,oof}`. A relative `(#a1)ofo` was unaffected — the flag
921 // sits inside the sole component and this very hoist loop reads it there.
922 let mut hoisted_globflags: i32 =
923 GF_MULTIBYTE | (seeded_globflags & (GF_IGNCASE | GF_LCMATCHUC | 0xff));
924 // c:953-954 gates BOTH bytes: `*patparse == zpc_special[ZPC_INPAR]`
925 // as well as `patparse[1] == zpc_special[ZPC_HASH]`. SHGLOB
926 // (c:500-510) and `disable -p '('` mask the INPAR slot to Marker,
927 // and then `(#i)abc` is ordinary text, not a flag spec.
928 let (inpar_char_pre, hash_char_pre) = {
929 let sp = zpc_special.lock().unwrap();
930 (sp[ZPC_INPAR as usize], sp[ZPC_HASH as usize]) // c:953, c:954
931 };
932 while hash_char_pre == b'#' && inpar_char_pre == b'(' {
933 let off = patparse_off.load(Ordering::Relaxed);
934 let p = patparse.lock().unwrap();
935 if off + 1 >= p.len() || &p.as_bytes()[off..off + 2] != b"(#" {
936 break;
937 }
938 let rest = p[off..].to_string();
939 drop(p);
940 match patgetglobflags(&rest) {
941 Some((_bits, assert, _consumed)) if assert != 0 => {
942 // c:Src/pattern.c:1103-1109 — `(#s)`/`(#e)` set `*assertp`
943 // (P_ISSTART/P_ISEND), a POSITIONAL zero-width anchor, not a
944 // whole-match glob flag. Do NOT hoist/consume it here: leave
945 // it (and everything after) for the main compile loop
946 // (pattern.rs:1245) to emit as a positional node. Hoisting
947 // dropped the anchor, so a leading `(#e)PAT` matched as if
948 // the `(#e)` were absent — `[[ o == (#e)o ]]` wrongly matched
949 // and `${s//(#e)r/END}` wrongly replaced. `(#s)` was masked
950 // in `[[ ]]` only because that context is start-anchored.
951 break;
952 }
953 Some((bits, _assert, consumed)) => {
954 // The fold / backref / matchref / multibyte flags are ABSOLUTE
955 // state, not a set-only delta. patgetglobflags's returned `bits`
956 // starts at 0 and can only carry flags turned ON, so `(#I)`
957 // clearing LCMATCHUC (c: `patglobflags &= ~(GF_LCMATCHUC |
958 // GF_IGNCASE)`) comes back as 0 — an `|=` accumulate then leaves
959 // an earlier `(#l)`/`(#i)`'s fold bit set, so `(#l)(#I)foo` kept
960 // folding where zsh, after `(#I)`, matches case-sensitively.
961 //
962 // patgetglobflags ALSO updates the global patglobflags with the
963 // sets AND the clears, so it holds the authoritative running
964 // value. REPLACE the toggle-able bits from it rather than
965 // OR-accumulating the lossy return.
966 // GF_MULTIBYTE is deliberately NOT in the mask: the init above
967 // force-sets it (this port always matches UTF-8) while the
968 // global patglobflags may not carry it, so replacing it from the
969 // global would wrongly clear it and break multibyte matching.
970 // The original `|=` could not clear it either, so leaving it out
971 // preserves that (rare `(#U)` single-byte requests are no more
972 // broken than before). The fold/backref/matchref bits, which DO
973 // need clear semantics for `(#I)`/`(#B)`/`(#M)`, come from the
974 // authoritative global.
975 let gmask = GF_IGNCASE | GF_LCMATCHUC | GF_BACKREF | GF_MATCHREF;
976 let cur = patglobflags.load(Ordering::Relaxed);
977 hoisted_globflags = (hoisted_globflags & !gmask) | (cur & gmask);
978 // `(#aN)` substitution budget — low 8 bits of
979 // `patglobflags` per c:1066. Only carry it when this
980 // flag-spec actually had `(#a)` digits (non-zero err
981 // count); other specs may set high bits and we don't
982 // want their bits-AND to clear our budget byte.
983 // Per-spec OR-leak is C-faithful (a later `(#a3)` after
984 // `(#a1)` raises the budget); the matcher reads the
985 // final cumulative byte.
986 let errs_byte = bits & 0xff;
987 if errs_byte != 0 {
988 hoisted_globflags = (hoisted_globflags & !0xff) | errs_byte;
989 // c:1066
990 }
991 if (bits & GF_MULTIBYTE) == 0 && rest.contains('U') {
992 hoisted_globflags &= !GF_MULTIBYTE;
993 }
994 patparse_off.fetch_add(consumed, Ordering::Relaxed);
995 }
996 None => break,
997 }
998 }
999
1000 // c:584-610 — PAT_PURES fast path. A literal with no glob tokens
1001 // compiles to a "pure string" rather than bytecode. For FILE globs
1002 // (PAT_FILE) the scan STOPS at '/', so each path component becomes a
1003 // pure section and `parsecomplist` can split the path on '/' (it
1004 // reads endexp = the '/' remainder). Gated on PAT_FILE so non-file
1005 // callers (matchpat/[[ ]]/:#) keep normal compilation unchanged; the
1006 // matcher consumes PURES sections via literal extraction (the
1007 // scanner), never `pattry`, so no PAT_PURES match path is needed.
1008 let pf_pre = patflags.load(Ordering::Relaxed);
1009 // c:Src/pattern.c:644-704 — for a FILE glob a literal path component is
1010 // emitted as a PAT_PURES section even under GF_IGNCASE (`setopt
1011 // nocaseglob`): C compiles it, then converts the single P_EXACTLY back
1012 // to PURES (c:664) while flagging the prog GF_IGNCASE (c:645). The net
1013 // effect is that intermediate literal directory components descend by
1014 // EXACT name (the scanner's PURES path stats the real path, glob.rs:436
1015 // — case-insensitivity applies to the FINAL wildcard component, not to
1016 // directory descent), so path splitting on `/` keeps working. zshrs's
1017 // port only had the first PURES gate (c:586), which excludes GF_IGNCASE,
1018 // so under nocaseglob every literal component (e.g. `tmp` in
1019 // `/tmp/.../*.zsh`) fell to the general compiler and matched nothing —
1020 // the whole path collapsed and `${(N)...}(DN)` globs returned empty.
1021 // That is what made zinit's plugin-discovery globs empty on a shell with
1022 // `caseglob` off → the `.zinit-load-plugin:source:110: no such file or
1023 // directory: ./` startup flood. Take the PURES fast path here for file
1024 // globs when the only extra glob flag is GF_IGNCASE; GF_IGNCASE stays on
1025 // `hoisted_globflags` so the final wildcard component still matches
1026 // case-insensitively.
1027 //
1028 // CORRECTION (see the `(#i)` note below): the exemption above transcribed
1029 // C's `__CYGWIN__` arm. c:586's fast-path gate is
1030 // `!(patglobflags & ~GF_MULTIBYTE)` on every other platform — the
1031 // `|| (!(patglobflags & ~GF_IGNCASE) && (patflags & PAT_FILE))` alternative
1032 // is inside `#ifdef __CYGWIN__` (c:587-594) precisely because only there is
1033 // the FILESYSTEM itself case-insensitive, making a stat of the pattern's
1034 // own spelling equivalent to a scan. Everywhere else C reaches PAT_PURES
1035 // for a literal component only via the c:650-664 conversion, and c:1392-1417
1036 // withholds it whenever `patglobflags & (0xFF|GF_LCMATCHUC|GF_IGNCASE)`:
1037 // "It's much simpler to turn off pure string mode for any case-insensitive
1038 // or approximate matching" (c:1382-1385). The single exception is a `.` or
1039 // `..` file component, which stays pure (c:1414-1416).
1040 //
1041 // This matters beyond flag bookkeeping: the PURES path STATS the pattern's
1042 // spelling, so on a case-insensitive filesystem `(#i)ALPHA.TXT` "found" the
1043 // file and emitted `ALPHA.TXT` — the pattern echoed back — where zsh scans
1044 // the directory and emits the real on-disk `alpha.txt`.
1045 let case_or_approx = hoisted_globflags & (0xff | GF_LCMATCHUC | GF_IGNCASE); // c:1392, c:1409
1046 if (pf_pre & PAT_FILE as i32) != 0 && (pf_pre & PAT_ANY as i32) == 0 {
1047 let off = patparse_off.load(Ordering::Relaxed);
1048 let p = patparse.lock().unwrap();
1049 let s = &p[off..];
1050 // c:606 — scan for a pure literal segment. By this point patparse
1051 // has been NORMALIZED to raw metachars (Star -> '*') with literal
1052 // metachars backslash-escaped (`\*`), so scan that form: stop at
1053 // '/' (segment boundary) or any UNescaped glob metachar (=> not
1054 // pure). `\X` is an escaped literal — skip both chars.
1055 let mut cut = s.len();
1056 let mut at_token = false;
1057 // The component's literal spelling. c:Src/glob.c:3633-3643
1058 // `zshtokenize` turns `\` before a `ztokens` metacharacter into
1059 // `Bnull`, a token, so C compiles that component and c:650-664
1060 // hands the scanner the UNQUOTED string (`A\(B\)` -> `A(B)`). Before
1061 // any other character the backslash is data and stays. Copying the
1062 // escapes verbatim stat'ed `A\(B\)`, so a quoted directory component
1063 // never matched (Y01completion #16).
1064 let mut literal_s = String::with_capacity(s.len());
1065 let mut it = s.char_indices();
1066 while let Some((i, c)) = it.next() {
1067 if c == '/' {
1068 cut = i;
1069 break;
1070 }
1071 if c == '\\' {
1072 match it.next() {
1073 Some((_, n)) if crate::ported::lex::ztokens.contains(n) => literal_s.push(n),
1074 Some((_, n)) => {
1075 literal_s.push('\\');
1076 literal_s.push(n);
1077 }
1078 None => literal_s.push('\\'),
1079 }
1080 continue;
1081 }
1082 if matches!(c, '*' | '?' | '[' | '(' | '|' | '~' | '^' | '#' | '<') {
1083 cut = i;
1084 at_token = true;
1085 break;
1086 }
1087 literal_s.push(c);
1088 }
1089 // c:1414-1416 — `.` and `..` stay pure even under case-insensitive or
1090 // approximate matching, so a `../x` path component still descends by
1091 // name instead of being scanned for.
1092 let dot_or_dotdot = matches!(&s[..cut], "." | ".."); // c:1414-1415
1093 // c:947 `*flagp = P_PURESTR;` — only a compiled P_EXACTLY clears it
1094 // (c:1409-1417), so an EMPTY component (the tail of `*/`) stays pure
1095 // under nocaseglob too. Withholding it made the tail a scan for the
1096 // empty name, and `setopt nocaseglob; print */` matched nothing.
1097 let empty_component = cut == 0;
1098 // c:610 — pure iff we stopped at end or '/', not at a glob meta.
1099 if !at_token && (case_or_approx == 0 || dot_or_dotdot || empty_component) {
1100 let literal = literal_s.into_bytes();
1101 drop(p);
1102 let mlen = literal.len() as i64;
1103 if let Some(end) = endexp.as_deref_mut() {
1104 // c:751 `*endexp = patparse` — a pointer INTO the caller's
1105 // tokenized buffer. Map the normalized cut back to the
1106 // original char index and slice the untouched tokenized
1107 // input, so the remainder keeps its exact original tokens.
1108 // `cut` indexes into `s`, which starts at `off` — the offset
1109 // patparse_off reached after the leading `(#...)` flag block
1110 // was hoisted above. `orig_remainder` maps an ABSOLUTE
1111 // normalized offset (the general path at the bottom of this
1112 // function passes `consumed_off`, already absolute), so a
1113 // bare `cut` under-reports the consumed length by exactly the
1114 // width of the hoisted flags: `(#i)alpha.txt` compiled the
1115 // literal correctly but reported `.txt` as unconsumed, and
1116 // `parsecomplist` (Src/glob.c:773) then saw a remainder that
1117 // was neither `/` nor empty and failed the whole pattern with
1118 // "bad pattern". Only wildcard-free components hit this path,
1119 // which is why `(#i)a*` always worked and `(#i)alpha.txt` did
1120 // not.
1121 *end = orig_remainder(&exp_tokenized, &norm_to_orig, off + cut);
1122 }
1123 let prog: Patprog = Box::new((
1124 patprog {
1125 startoff: 0,
1126 size: mlen,
1127 mustoff: 0,
1128 patmlen: mlen, // c:623 — pure-string length
1129 globflags: hoisted_globflags,
1130 globend: patglobflags.load(Ordering::Relaxed),
1131 flags: pf_pre | PAT_PURES as i32, // c:625
1132 patnpar: 0,
1133 patstartch: patstartch_lead, // c:1610
1134 },
1135 literal,
1136 ));
1137 if cacheable {
1138 crate::pat_cache::put(&exp_orig, inflags, &prog);
1139 }
1140 return Some(prog);
1141 }
1142 }
1143
1144 let mut flagp: i32 = 0;
1145 let root = patcompswitch(0, &mut flagp);
1146 if root < 0 {
1147 return None; // c:646 compile error
1148 }
1149 // Emit the terminal P_END and chain every branch's operand to it.
1150 let end_off = patnode(P_END);
1151 chain_branches_to(root as usize, end_off);
1152
1153 let code = patout.lock().unwrap().clone();
1154 let consumed_off = patparse_off.load(Ordering::Relaxed);
1155 if let Some(end) = endexp.as_deref_mut() {
1156 // c:751 `*endexp = patparse` — a pointer INTO the caller's
1157 // tokenized buffer. Map the normalized consume-point back to the
1158 // original char index and slice the untouched tokenized input.
1159 *end = orig_remainder(&exp_tokenized, &norm_to_orig, consumed_off);
1160 }
1161
1162 let prog: Patprog = Box::new((
1163 patprog {
1164 startoff: 0,
1165 size: code.len() as i64,
1166 mustoff: 0,
1167 patmlen: 0,
1168 globflags: hoisted_globflags,
1169 globend: patglobflags.load(Ordering::Relaxed),
1170 // c:637 `p->flags = patflags;` — patflags ONLY. A prior
1171 // Rust port OR'd `hoisted_globflags` into here, contaminating
1172 // `prog.flags & 0xff` checks (which read PAT_STATIC / PAT_PURES
1173 // bits in the low byte) with whatever ended up in the
1174 // `(#aN)` budget byte. The two flag-sets stay strictly
1175 // separated per C source.
1176 flags: patflags.load(Ordering::Relaxed),
1177
1178 patnpar: patnpar.load(Ordering::Relaxed) - 1,
1179 patstartch: patstartch_lead, // c:1610
1180 },
1181 code,
1182 ));
1183 if cacheable {
1184 crate::pat_cache::put(&exp_orig, inflags, &prog);
1185 }
1186 Some(prog)
1187}
1188
1189/// Port of `patcompswitch(int paren, int *flagp)` from `Src/pattern.c:765`.
1190///
1191/// C: `static long patcompswitch(int paren, int *flagp)`. Parses an
1192/// alternation (`a|b|c`), emitting a chain of P_BRANCH nodes. Returns
1193/// offset of the first branch, or -1 on error.
1194pub fn patcompswitch(paren: i32, flagp: &mut i32) -> i64 {
1195 // c:765
1196 // Emit the first P_BRANCH header. Its operand is the content
1197 // emitted by the immediately-following patcompbranch call (lives
1198 // inline at starter+I_BODY). Does NOT emit a terminator — caller
1199 // (patcompile for top-level, patcomppiece for sub-pattern)
1200 // chains branches to the appropriate follow-on opcode.
1201 // c:773 — `long savglobflags = (long)patglobflags;`
1202 let savglobflags = patglobflags.load(Ordering::Relaxed);
1203 // c:772 — `int flags, gfchanged = 0;`
1204 let mut gfchanged: i32 = 0;
1205 let starter = patnode(P_BRANCH);
1206 let mut branch_flags: i32 = 0;
1207 let first_branch = patcompbranch(&mut branch_flags, paren);
1208 if first_branch < 0 {
1209 return -1;
1210 }
1211 // c:786-787 — `if (patglobflags != (int)savglobflags) gfchanged++;`
1212 if patglobflags.load(Ordering::Relaxed) != savglobflags {
1213 gfchanged += 1;
1214 }
1215 *flagp |= branch_flags & P_HSTART;
1216
1217 let mut last_branch = starter;
1218 // c:769 — `Upat excsync = NULL;` — set on first `~` exclusion;
1219 // reused so consecutive `~clause` chain to the same sync node.
1220 let mut excsync: usize = 0;
1221
1222 // Snapshot zpc_special for this compile pass — locked once to
1223 // avoid re-locking inside the per-iteration parse-byte check.
1224 let (sp_bar, sp_tilde, sp_special_set) = {
1225 let sp = zpc_special.lock().unwrap();
1226 let bar = sp[ZPC_BAR as usize];
1227 let tilde = sp[ZPC_TILDE as usize];
1228 // c:803 — `memchr(zpc_special, patparse[1], ZPC_SEG_COUNT)` —
1229 // is the lookahead byte one of the segment-special bytes? If
1230 // so, `~X` is NOT an exclusion (`~|`, `~)`, etc.).
1231 let mut set = [false; 256];
1232 for i in 0..(ZPC_SEG_COUNT as usize) {
1233 set[sp[i] as usize] = true;
1234 }
1235 (bar, tilde, set)
1236 };
1237
1238 // Alternation + exclusion loop:
1239 // `|` → next alternative (P_BRANCH)
1240 // `~` → exclusion (P_EXCLUDE / P_EXCLUDP) — when followed by
1241 // `/` (top-level path component split) OR a non-special
1242 // char (ordinary content). `~~`, `~|`, `~)` stay literal.
1243 loop {
1244 let off = patparse_off.load(Ordering::Relaxed);
1245 let parse = patparse.lock().unwrap();
1246 if off >= parse.len() {
1247 break;
1248 }
1249 let bytes = parse.as_bytes();
1250 let c = bytes[off];
1251 // c:799-803 — accept `|` always; accept `~` only when the
1252 // lookahead char is `/` or NOT a segment-special byte.
1253 let is_bar = c == sp_bar;
1254 let is_tilde_exclude = c == sp_tilde && off + 1 < bytes.len() && {
1255 let la = bytes[off + 1];
1256 la == b'/' || !sp_special_set[la as usize]
1257 };
1258 if !is_bar && !is_tilde_exclude {
1259 break;
1260 }
1261 drop(parse);
1262 patparse_off.fetch_add(1, Ordering::Relaxed); // c:803 `*patparse++`
1263 let br: usize;
1264 // c:805 — `long gfnode = 0, newbr;`
1265 let mut gfnode: usize = 0;
1266 // c:839-841 — C tracks this by bumping `tilde` to 2; a bool is the
1267 // same signal in Rust, where `tilde` is the `is_tilde_exclude` bool.
1268 let mut tilde_masked_slash = false;
1269 if is_tilde_exclude {
1270 // c:808-836 — `if (tilde)` arm. Emit the EXCSYNC sync node
1271 // (if first `~` in this switch) then the EXCLUDE / EXCLUDP
1272 // node with an 8-byte NULL syncptr payload. Note we DON'T
1273 // reset patglobflags's low byte (`(#aN)` budget) here —
1274 // the Rust port doesn't yet propagate per-pattern `(#a)`
1275 // through nested patmatch frames, so dropping the budget
1276 // mid-compile is a no-op.
1277 if excsync == 0 {
1278 excsync = patnode(P_EXCSYNC); // c:813
1279 patoptail(last_branch, excsync); // c:814
1280 }
1281 // c:820-824 — "By default, approximations are turned off in
1282 // exclusions: we need to do this here as otherwise the code
1283 // compiling the exclusion doesn't know if the flags have
1284 // really changed if the error count gets restored."
1285 // `patglobflags &= ~0xff;`
1286 // That last clause is exactly why this matters now that
1287 // patcompbranch honours c:995-997's "No effect" skip: without
1288 // the clear, `(#ia1)README~(#a1)READ_ME` sees the exclusion's
1289 // own `(#a1)` as a no-op and emits no P_GFLAGS, so the
1290 // exclusion never re-arms its error budget.
1291 patglobflags.fetch_and(!0xff, Ordering::Relaxed);
1292 // c:816-825 — `if (!(patflags & PAT_FILET) || paren)` →
1293 // P_EXCLUDE; else P_EXCLUDP for top-level file globs.
1294 let pf = patflags.load(Ordering::Relaxed);
1295 let use_excludp = (pf & (PAT_FILET as i32)) != 0 && paren == 0;
1296 if use_excludp {
1297 br = patnode(P_EXCLUDP); // c:823
1298 patflags.fetch_or(PAT_HAS_EXCLUDP as i32, Ordering::Relaxed); // c:824
1299 } else {
1300 br = patnode(P_EXCLUDE); // c:818
1301 }
1302 // c:826-827 — `up.p = NULL; patadd((char *)&up, 0, sizeof(up), 0);`
1303 // 8-byte syncptr slot, NULL-initialised. Sized for a
1304 // 64-bit pointer to match C `union upat`.
1305 {
1306 let mut buf = patout.lock().unwrap();
1307 buf.extend_from_slice(&[0u8; 8]);
1308 }
1309 /* / is not treated as special if we are at top level */ // c:839
1310 // c:840-842 — `if (!paren && zpc_special[ZPC_SLASH] == '/') {
1311 // tilde++; zpc_special[ZPC_SLASH] = Marker; }`. This is what
1312 // keeps a top-level exclusion like `*~(bench/x)y` legal, and it
1313 // is the reason C can afford the unconditional '/' break in
1314 // patcompbranch (c:950).
1315 if paren == 0 {
1316 let mut sp = zpc_special.lock().unwrap();
1317 if sp[ZPC_SLASH as usize] == b'/' {
1318 tilde_masked_slash = true; // c:841 — `tilde++` (tilde becomes 2)
1319 sp[ZPC_SLASH as usize] = Marker as u32 as u8; // c:842
1320 }
1321 }
1322 } else {
1323 // c:843 — `excsync = 0; br = patnode(P_BRANCH);`
1324 excsync = 0;
1325 br = patnode(P_BRANCH);
1326 // c:845-847 — "The position of the following statements
1327 // means globflags set in the main branch carry over to the
1328 // exclusion."
1329 if paren == 0 {
1330 // c:849 — `patglobflags = 0;`
1331 //
1332 // c:850-870 — "If at top level, we need to reinitialize
1333 // flags to zero, since (#i)foo|bar only applies to foo
1334 // and we stuck the #i into the global flags."
1335 //
1336 // !!! RUST-ONLY DETAIL !!! C tests
1337 // `((Patprog)patout)->globflags`, the header field it
1338 // wrote at c:977 when the flag spec sat at patstart.
1339 // zshrs computes the header value in a separate
1340 // patcompile hoist scan (pattern.rs:844+), so the live
1341 // equivalent here is "were any flags in effect on the
1342 // way into this branch?".
1343 let before_reset = patglobflags.load(Ordering::Relaxed);
1344 patglobflags.store(0, Ordering::Relaxed); // c:849
1345 if before_reset != 0 {
1346 // c:861-864 — `gfnode = patnode(P_GFLAGS); up.l =
1347 // patglobflags;` (which c:849 just set to 0).
1348 gfnode = patnode(P_GFLAGS);
1349 let mut buf = patout.lock().unwrap();
1350 buf.extend_from_slice(&0i32.to_le_bytes());
1351 }
1352 } else {
1353 // c:872 — `patglobflags = (int)savglobflags;`
1354 patglobflags.store(savglobflags, Ordering::Relaxed);
1355 }
1356 }
1357 // Chain previous branch's `next` directly to this new branch
1358 // (alternative chain, not operand chain).
1359 set_next(last_branch, br);
1360 let mut bf: i32 = 0;
1361 let inner = patcompbranch(&mut bf, paren);
1362 // c:880-883 — `if (tilde == 2) { /* restore special treatment of / */
1363 // zpc_special[ZPC_SLASH] = '/'; }`. C does this BEFORE the
1364 // `if (!newbr) return 0` bail at c:884-885, so the restore must not
1365 // sit behind the error return either.
1366 if tilde_masked_slash {
1367 zpc_special.lock().unwrap()[ZPC_SLASH as usize] = b'/';
1368 }
1369 if inner < 0 {
1370 return -1;
1371 }
1372 // c:884-885 — `if (gfnode) pattail(gfnode, newbr);`. In the Rust
1373 // encoding a branch's operand lives inline at `br + I_BODY`, so
1374 // the P_GFLAGS emitted above IS the operand; chain it to the
1375 // branch body that patcompbranch just emitted.
1376 if gfnode != 0 {
1377 set_next(gfnode, inner as usize);
1378 }
1379 // c:888-889 — `if (!tilde && patglobflags != (int)savglobflags)
1380 // gfchanged++;`
1381 if !is_tilde_exclude && patglobflags.load(Ordering::Relaxed) != savglobflags {
1382 gfchanged += 1;
1383 }
1384 // c:Src/pattern.c:891-892 — `if (excsync) patoptail(br,
1385 // patnode(P_EXCEND));`. Terminate an exclusion branch's operand
1386 // with P_EXCEND so the matcher knows where the excluded pattern
1387 // ends; without it `A~B` ran B past its own end into the
1388 // following pattern and never excluded (`STATES__*~*local*`
1389 // failed to drop STATES__local_bar).
1390 if excsync != 0 {
1391 let excend = patnode(P_EXCEND);
1392 patoptail(br, excend);
1393 }
1394 *flagp |= bf & P_HSTART;
1395 last_branch = br;
1396 }
1397
1398 // c:913-917 — "check for proper termination":
1399 // if ((paren && *patparse++ != Outpar) ||
1400 // (!paren && *patparse &&
1401 // !((patflags & PAT_FILE) && *patparse == '/')))
1402 // return 0;
1403 // The `paren` half lives at the two call sites that OPEN a group —
1404 // patcomppiece's `b'('` arm and patcompnot's paren arm both test for
1405 // the closing `)` and consume it — so only the `!paren` half belongs
1406 // here: input left over after the top-level switch is a BAD PATTERN.
1407 //
1408 // What reaches this point. `patcompbranch` (c:950) stops on any of the
1409 // first ZPC_SEG_COUNT slots — SLASH, NULL, BAR, OUTPAR. BAR is consumed
1410 // by the alternation loop above, NULL is end-of-input, and SLASH is
1411 // Marker'd for every non-PAT_FILE compile (patcompile, c:570), which is
1412 // why C spells the PAT_FILE escape hatch explicitly. That leaves `)` as
1413 // the byte this test actually rejects, and it can only survive to here
1414 // when `(` was NOT a grouping character — `isset(SHGLOB)` (c:500-510) or
1415 // `disable -p '('` — so the `(` opened no group for it to close:
1416 // setopt shglob; [[ "-A" = -([AMO]*|[0CRSWnsw]) ]]
1417 // zsh: bad pattern: -([AMO]*|[0CRSWnsw])
1418 // which is `_arguments`:14 under the option state compsys runs it in.
1419 //
1420 // A leftover `)` is NOT the same thing as the c:1294-1298 rule that lets
1421 // patcomppiece swallow `)` as an ordinary character: that one fires only
1422 // when there is no group to close AND the scan is already inside a
1423 // literal run, so `-(a|b)` compiles to `-(a` | `b)` while `-(a|b*)` —
1424 // the `*` ends the literal run and hands `)` back to patcompbranch —
1425 // is rejected. zshrs reproduces the same split because its input
1426 // normalizer (see patcompile) demotes an UNBALANCED `)` to a literal
1427 // before the compile starts, leaving exactly the balanced-but-ungrouped
1428 // case for this test.
1429 if paren == 0 {
1430 let off = patparse_off.load(Ordering::Relaxed);
1431 let parse = patparse.lock().unwrap();
1432 let bytes = parse.as_bytes();
1433 if off < bytes.len() {
1434 // c:915-916 — `!((patflags & PAT_FILE) && *patparse == '/')`:
1435 // a file glob legitimately stops at the component separator,
1436 // and its caller (parsecomplist) resumes from there.
1437 let is_file_slash =
1438 (patflags.load(Ordering::Relaxed) & PAT_FILE as i32) != 0 && bytes[off] == b'/';
1439 if !is_file_slash {
1440 return -1; // c:917 `return 0`
1441 }
1442 }
1443 }
1444 let _ = first_branch;
1445 // c:919-929 — C emits the closing P_GFLAGS restore right here, using
1446 // this local `gfchanged`. The Rust port emits it from patcomppiece's
1447 // `(` arm (which owns the P_CLOSE node), so hand the flag across the
1448 // call boundary. See PATSWITCH_GFCHANGED's warning block.
1449 PATSWITCH_GFCHANGED.store(gfchanged, Ordering::Relaxed);
1450 starter as i64
1451}
1452
1453// !!! WARNING: RUST-ONLY HELPER !!!
1454// C keeps `gfchanged` as a local of `patcompswitch` and emits the
1455// group-closing P_GFLAGS restore inside the same function
1456// (Src/pattern.c:919-929), immediately after the `ender` node it also
1457// creates there. The Rust port splits that work: patcomppiece's `b'('`
1458// arm owns P_OPEN/P_CLOSE and therefore owns the restore emission, so
1459// the counter has to cross the call boundary. patcompswitch stores it
1460// just before returning; the `(` arm reads it immediately after its own
1461// patcompswitch call returns, so nested groups each observe their own
1462// value.
1463static PATSWITCH_GFCHANGED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
1464
1465/// Port of `patcompbranch(int *flagp, int paren)` from `Src/pattern.c:942`.
1466///
1467/// C: `static long patcompbranch(int *flagp, int paren)`. Parses a
1468/// single branch — a sequence of pieces. Returns offset of the first
1469/// node in the branch, or -1 on error.
1470pub fn patcompbranch(flagp: &mut i32, paren: i32) -> i64 {
1471 // c:942
1472 let mut chain_start: i64 = -1;
1473 let mut last_tail: usize = 0;
1474 // Track preceding piece for POSTFIX `(#cN,M)` wrap. C does
1475 // `patinsert(P_COUNTSTART, starter, ...)` to embed COUNTSTART
1476 // BEFORE the just-compiled piece (c:pattern.c:1686). We snapshot
1477 // the piece bytes, truncate, emit P_COUNT, then re-append.
1478 let mut last_piece_off: i64 = -1; // start offset of preceding piece
1479 let mut prev_chain_tail: i64 = -1; // tail of chain BEFORE preceding piece (-1 if piece was first)
1480 // Flags the preceding patcomppiece reported. P_HSTART marks a piece that
1481 // already had a closure applied (`a#`, `a##`, or an earlier `(#cN,M)`) —
1482 // patcomppiece sets `*flagp = P_HSTART` on every such arm, mirroring C
1483 // (c:1626/1629/1636/1640/1643). Used below to reject a stacked count.
1484 let mut last_piece_flags: i32 = 0;
1485 *flagp = P_PURESTR;
1486
1487 // c:951-952 — snapshot the segment-special set so we can do
1488 // `memchr(zpc_special, byte, ZPC_SEG_COUNT)`-equivalent lookups.
1489 // Only the first ZPC_SEG_COUNT slots (SLASH, NULL, BAR, OUTPAR,
1490 // TILDE) matter here.
1491 let (sp_tilde, sp_seg_set, sp_inpar, sp_hash, sp_slash) = {
1492 let sp = zpc_special.lock().unwrap();
1493 let tilde = sp[ZPC_TILDE as usize];
1494 let mut set = [false; 256];
1495 for i in 0..(ZPC_SEG_COUNT as usize) {
1496 set[sp[i] as usize] = true;
1497 }
1498 (
1499 tilde,
1500 set,
1501 sp[ZPC_INPAR as usize],
1502 sp[ZPC_HASH as usize], // c:1609
1503 sp[ZPC_SLASH as usize], // c:949 — the slot holds '/' only while special
1504 )
1505 };
1506
1507 loop {
1508 let off = patparse_off.load(Ordering::Relaxed);
1509 // Snapshot the parse buffer into an owned slice for branch
1510 // decisions; release the lock so subsequent emit helpers
1511 // (which acquire patout's lock) can't contend.
1512 let snapshot: Vec<u8> = {
1513 let parse = patparse.lock().unwrap();
1514 parse.as_bytes().to_vec()
1515 };
1516 if off >= snapshot.len() {
1517 break;
1518 }
1519 let c = snapshot[off];
1520 // Branch terminators: |, ), end of pattern.
1521 if c == b'|' || c == b')' {
1522 break;
1523 }
1524 // c:949-952 — '/' is ZPC_SLASH (slot 0, within ZPC_SEG_COUNT) and
1525 // terminates the segment at EVERY paren depth: C's loop condition is
1526 // `!memchr(zpc_special, *patparse, ZPC_SEG_COUNT)` with no `paren`
1527 // test anywhere. C keeps '/' literal by MASKING THE SLOT instead —
1528 // for a non-FILE pattern (c:568-570) and around a top-level `~`
1529 // exclusion (c:840-842, restored c:880-883). A `/` inside a group
1530 // therefore leaves the group unterminated and c:913-914
1531 // (`if (paren && *patparse++ != Outpar) return 0`) rejects the whole
1532 // pattern, which is why `*(-/):t:directories` is a "bad pattern" in
1533 // zsh. Gating on `paren == 0 && PAT_FILE` instead compiled it happily
1534 // and answered "no matches found": `_files` then never took the
1535 // errflag unwind that stops it reaching its remaining `-g` patterns,
1536 // so `CC <TAB>` listed every directory where zsh inserts one match.
1537 if c == b'/' && sp_slash == b'/' {
1538 break;
1539 }
1540 // c:950-952 — `~` is an additional terminator when active as
1541 // the exclusion operator. The C condition includes all five
1542 // segment-specials (SLASH, NULL, BAR, OUTPAR, TILDE), but
1543 // `|` and `)` are already covered above and SLASH inside
1544 // `(...)` alternation is intentionally left literal here to
1545 // preserve the existing `zsh_corpus_hash_s_e_anchors_match_
1546 // bare_test` parity pin.
1547 if c == sp_tilde && c != 0 {
1548 let la = snapshot.get(off + 1).copied().unwrap_or(0);
1549 // Literal-tilde exception: `~` followed by a segment-
1550 // special OTHER than `/` keeps the `~` as literal.
1551 let literal_tilde_exception = la != b'/' && sp_seg_set[la as usize];
1552 if !literal_tilde_exception {
1553 break;
1554 }
1555 }
1556 let bytes = snapshot.as_slice();
1557 // Mid-pattern `(#cN,M)` counted-repetition specifier — emit
1558 // P_COUNT with bounds + inline operand following. Detected
1559 // BEFORE the generic patgetglobflags path because `c` is not
1560 // a flag char in that fn.
1561 //
1562 // c:1608-1610 — the `(` and `#` are compared against
1563 // `zpc_special[ZPC_INPAR]` / `zpc_special[ZPC_HASH]`, NOT against
1564 // literal bytes. That indirection is the whole EXTENDED_GLOB gate:
1565 // patcompcharsset() rewrites `zpc_special[ZPC_HASH]` to Marker
1566 // (c:482) when EXTENDED_GLOB is off, so a literal `#` can never
1567 // equal it and `(#c...)` stops being a counted closure — it falls
1568 // through and parses as an ordinary group. Testing `b'#'` directly
1569 // applied the closure with EXTENDED_GLOB unset, so
1570 // `[[ aaa = a(#c2,3) ]]` matched (zsh: no match).
1571 if off + 2 < bytes.len()
1572 && bytes[off] == sp_inpar
1573 && bytes[off + 1] == sp_hash
1574 && bytes[off + 2] == b'c'
1575 {
1576 let mut j = off + 3;
1577 let mut min: i64 = 0;
1578 let min_start = j;
1579 while j < bytes.len() && bytes[j].is_ascii_digit() {
1580 min = min * 10 + (bytes[j] - b'0') as i64;
1581 j += 1;
1582 }
1583 let mut max: i64 = i64::MAX;
1584 // Three valid shapes (per zsh extended-glob (#cN,M)):
1585 // (#cN) — exact count N (min=N, max=N)
1586 // (#cN,) — min N, no max (min=N, max=∞)
1587 // (#cN,M) — N..M range (min=N, max=M)
1588 // (#c,M) — max M, no min (min=0, max=M) ← bug #489 missing case
1589 // (#c,) — equivalent to no count (min=0, max=∞)
1590 // The original `if j > min_start` gate skipped the `)`
1591 // check for the no-min shapes, leaving them unparsed.
1592 let has_min_digits = j > min_start;
1593 let has_comma = j < bytes.len() && bytes[j] == b',';
1594 if has_min_digits || has_comma {
1595 if has_comma {
1596 j += 1; // skip ,
1597 let max_start = j;
1598 let mut mx: i64 = 0;
1599 while j < bytes.len() && bytes[j].is_ascii_digit() {
1600 mx = mx * 10 + (bytes[j] - b'0') as i64;
1601 j += 1;
1602 }
1603 if j > max_start {
1604 max = mx;
1605 }
1606 } else {
1607 // (#cN) — exact count N.
1608 max = min;
1609 }
1610 if j < bytes.len() && bytes[j] == b')' {
1611 j += 1;
1612 patparse_off.store(j, Ordering::Relaxed);
1613 // POSTFIX semantics — c:pattern.c:1686.
1614 // C does `patinsert(P_COUNTSTART, starter, ...)`
1615 // then `pattail(opnd, patnode(P_BACK))` to loop the
1616 // operand back. zshrs uses a simpler encoding where
1617 // P_COUNT carries [min, max] then operand bytes
1618 // inline; matcher iterates outside. So we relocate
1619 // the preceding piece into the operand slot.
1620 // c:1431-1436 — `case Star:` sets `kshchar = -1`, whose
1621 // only purpose is (per C's own comment) "a sign that we
1622 // can't have #'s". c:1620-1622 then rejects the pattern:
1623 //
1624 // /* too much at once doesn't currently work */
1625 // if (kshchar && (hash || count))
1626 // return 0;
1627 //
1628 // So a count may not follow `*`, nor stack on a piece
1629 // that already carries a closure (`a#(#c2,3)`). zshrs
1630 // attaches the count to the preceding piece rather than
1631 // tracking kshchar, so the same rule is expressed by
1632 // looking at that piece's opcode. Without this, zshrs
1633 // silently accepted `*(#c2,3)` / `a#(#c2,3)`, which zsh
1634 // rejects as a bad pattern.
1635 if last_piece_off >= 0 {
1636 let prev_op = {
1637 let buf = patout.lock().unwrap();
1638 buf.get(last_piece_off as usize + I_OP)
1639 .copied()
1640 .unwrap_or(0)
1641 };
1642 // `*` is C's kshchar = -1 (its head node is P_STAR);
1643 // P_HSTART marks a piece that already closured, which
1644 // is what makes `a#(#c2,3)` / `a##(#c2,3)` bad too.
1645 if prev_op == P_STAR || (last_piece_flags & P_HSTART) != 0 {
1646 return -1; // c:1622 `return 0;`
1647 }
1648 }
1649 if last_piece_off >= 0 {
1650 let piece_start = last_piece_off as usize;
1651 // Snapshot preceding piece (everything emitted
1652 // by the prior patcomppiece call).
1653 let piece_bytes: Vec<u8> = {
1654 let buf = patout.lock().unwrap();
1655 buf[piece_start..].to_vec()
1656 };
1657 // Truncate to remove the piece.
1658 {
1659 let mut buf = patout.lock().unwrap();
1660 buf.truncate(piece_start);
1661 }
1662 // Cut chain at prev_chain_tail (was pointing
1663 // into the piece via set_next or as
1664 // chain_start).
1665 if prev_chain_tail >= 0 {
1666 set_next(prev_chain_tail as usize, 0);
1667 } else {
1668 chain_start = -1;
1669 }
1670 // Emit P_COUNT at current end.
1671 let count_off = patnode(P_COUNT);
1672 let operand_new_start: usize;
1673 {
1674 let mut buf = patout.lock().unwrap();
1675 buf.extend_from_slice(&min.to_le_bytes());
1676 buf.extend_from_slice(&max.to_le_bytes());
1677 operand_new_start = buf.len();
1678 }
1679 // Relocate piece bytes: rewrite internal next
1680 // links (absolute offsets >= piece_start) by
1681 // delta = operand_new_start - piece_start.
1682 let delta: i64 = operand_new_start as i64 - piece_start as i64;
1683 let mut relocated = piece_bytes.clone();
1684 let mut i = 0;
1685 while i + I_BODY <= relocated.len() {
1686 let op = relocated[i + I_OP];
1687 if op == 0 {
1688 i += 1;
1689 continue;
1690 }
1691 let nxt = u32::from_le_bytes(
1692 relocated[i + I_NEXT..i + I_NEXT + 4].try_into().unwrap(),
1693 );
1694 if nxt != 0 {
1695 let new_nxt = (nxt as i64 + delta) as u32;
1696 relocated[i + I_NEXT..i + I_NEXT + 4]
1697 .copy_from_slice(&new_nxt.to_le_bytes());
1698 }
1699 let next_i = advance_past_instr(&relocated, i);
1700 if next_i == 0 || next_i <= i {
1701 break;
1702 }
1703 i = next_i;
1704 }
1705 {
1706 let mut buf = patout.lock().unwrap();
1707 buf.extend_from_slice(&relocated);
1708 }
1709 // Hook P_COUNT into chain.
1710 if chain_start < 0 {
1711 chain_start = count_off as i64;
1712 } else {
1713 set_next(prev_chain_tail as usize, count_off);
1714 }
1715 last_tail = count_off;
1716 // Consumed — clear tracking. The resulting piece IS a
1717 // closure (C: `*flagp = P_HSTART`, c:1636), so a second
1718 // count stacked on it must be rejected.
1719 last_piece_off = -1;
1720 prev_chain_tail = -1;
1721 last_piece_flags = P_HSTART;
1722 continue;
1723 }
1724 // No preceding piece — `(#cN,M)` is a POSTFIX
1725 // modifier in real zsh and requires a piece to
1726 // attach to. Without one C `Src/pattern.c:1606+`
1727 // rejects the pattern. Bug #521: zshrs previously
1728 // took a legacy PREFIX path (compile next piece as
1729 // operand) which silently matched empty for `0,0`
1730 // and similar degenerate ranges.
1731 //
1732 // Report nothing here: C's patcomppiece signals a bad
1733 // pattern by `return 0` alone (c:1600, c:1622) and the
1734 // CALLER prints the diagnostic with the pattern it was
1735 // given — `matchpat` zerrs "bad pattern: %s" with the
1736 // full pattern (glob.c:2522), and `[[ ]]` zwarnnams it
1737 // from cond.c:314. Emitting a message from inside the
1738 // compiler printed only the `(#cN,M)` fragment, losing
1739 // the rest of the pattern the user actually wrote.
1740 return -1; // c:1622 `return 0;`
1741 }
1742 }
1743 // Malformed `(#c...)` — fall through to generic flag handler.
1744 }
1745 // c:953-984 — mid-pattern `(#...)` glob-flag specifier. Emits
1746 // P_GFLAGS in C to switch GF_IGNCASE / GF_LCMATCHUC /
1747 // GF_MULTIBYTE / etc. mid-match. C body:
1748 // `if ((*patparse == zpc_special[ZPC_INPAR] &&`
1749 // ` patparse[1] == zpc_special[ZPC_HASH]) || ...)`
1750 // ` if (!patgetglobflags(&patparse, &assert, &ignore))`
1751 // ` return 0;`
1752 // Both bytes must equal their zpc_special slots, which
1753 // patcompcharsset (c:480-483) masks to Marker when
1754 // EXTENDEDGLOB is off (c:476). Previously this Rust arm
1755 // compared the raw byte `b'#'`, allowing `(#s)` / `(#e)` /
1756 // `(#i)` to fire even without EXTENDEDGLOB. Parity bugs
1757 // #18/#19 vs real zsh.
1758 let hash_char = zpc_special.lock().unwrap()[ZPC_HASH as usize]; // c:957
1759 // c:953-954 compares the FIRST byte against
1760 // `zpc_special[ZPC_INPAR]` too, which patcompcharsset masks to
1761 // Marker under SHGLOB (c:500-510) or `disable -p '('`. With `(`
1762 // disabled, `(#i)abc` is the LITERAL text `(#i)abc`, so the
1763 // flag spec must not fire.
1764 // c:955-956 — the ksh form `@(#...)`:
1765 // `(*patparse == zpc_special[ZPC_KSH_AT] &&
1766 // patparse[1] == Inpar && patparse[2] == zpc_special[ZPC_HASH])`
1767 // is equally a glob-flag specifier. c:961 then skips 3 bytes
1768 // instead of 2. Misc/globtests.ksh:94 writes `(#i)FOO@(#I)X@(#i)X`
1769 // and zshrs reported "bad pattern" without this arm.
1770 let sp_ksh_at = zpc_special.lock().unwrap()[ZPC_KSH_AT as usize];
1771 let at_form = hash_char == b'#'
1772 && sp_ksh_at != 0
1773 && off + 2 < bytes.len()
1774 && bytes[off] == sp_ksh_at
1775 && bytes[off + 1] == sp_inpar
1776 && bytes[off + 2] == b'#';
1777 if at_form
1778 || (hash_char == b'#'
1779 && off + 1 < bytes.len()
1780 && bytes[off] == sp_inpar
1781 && bytes[off + 1] == b'#')
1782 {
1783 // c:961 — `patparse += (*patparse == '@') ? 3 : 2;` — the
1784 // Rust patgetglobflags() consumes the leading `(#` itself, so
1785 // hand it the string starting at the `(`.
1786 let skip = usize::from(at_form);
1787 let rest = std::str::from_utf8(&bytes[off + skip..])
1788 .unwrap_or("")
1789 .to_string();
1790 // c:959 — `int oldglobflags = patglobflags, ignore;`
1791 let oldglobflags = patglobflags.load(Ordering::Relaxed);
1792 if let Some((_bits, assertp, consumed)) = patgetglobflags(&rest) {
1793 patparse_off.fetch_add(consumed + skip, Ordering::Relaxed);
1794 // c:995-997 — `} else { /* No effect. */ continue; }`.
1795 // A specifier that leaves patglobflags unchanged emits
1796 // nothing. Matters after c:872's per-branch reset:
1797 // `((#I)foo|(#i)rod)` re-enters branch 2 already
1798 // case-insensitive, so its `(#i)` is a no-op.
1799 if assertp == 0 && patglobflags.load(Ordering::Relaxed) == oldglobflags {
1800 continue;
1801 }
1802 // Emit P_GFLAGS for flag-bit changes if any. Include the
1803 // low byte (the `(#aN)` approximation budget) so a
1804 // mid-pattern `(#a0)` / `(#a2)` actually changes the
1805 // error allowance for the following segment — c:Src/
1806 // pattern.c:2941 `patglobflags = P_OPERAND(scan)->l`
1807 // sets the WHOLE value. Without the 0xff, `(#a1)cat(#a0)
1808 // dog` kept the outer budget and `dog` wrongly tolerated
1809 // an error.
1810 // c:989-994 — the payload is the RUNNING `patglobflags`
1811 // (`up.l = patglobflags`), not the delta this specifier
1812 // contributed. `patgetglobflags` only ever reports bits it
1813 // turned ON, so building the payload from it dropped
1814 // GF_MULTIBYTE — and since c:2942 assigns the payload
1815 // ABSOLUTELY (`patglobflags = P_OPERAND(scan)->l`), a
1816 // mid-pattern `(#b)` demoted the rest of the match to
1817 // single-byte stepping: `[[ $'αβγδε' = *(#b)(?) ]]` then
1818 // matched only the last BYTE of `ε`.
1819 let flag_bits = patglobflags.load(Ordering::Relaxed)
1820 & (GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE | 0xff);
1821 if flag_bits != 0 || (flag_bits == 0 && assertp == 0) {
1822 let gf_off = patnode(P_GFLAGS);
1823 let mut buf = patout.lock().unwrap();
1824 buf.extend_from_slice(&flag_bits.to_le_bytes());
1825 drop(buf);
1826 if chain_start < 0 {
1827 chain_start = gf_off as i64;
1828 } else {
1829 set_next(last_tail, gf_off);
1830 }
1831 last_tail = gf_off;
1832 }
1833 // Emit P_ISSTART / P_ISEND when assertp set.
1834 if assertp != 0 {
1835 let as_off = patnode(assertp as u8);
1836 if chain_start < 0 {
1837 chain_start = as_off as i64;
1838 } else {
1839 set_next(last_tail, as_off);
1840 }
1841 last_tail = as_off;
1842 }
1843 continue;
1844 }
1845 // patgetglobflags failed — treat the `(` as a literal group.
1846 }
1847
1848 // c:1011-1014 — `^pat` standalone negation. When `^` is the
1849 // active EXTENDEDGLOB special and appears at piece position
1850 // (not inside a `[...]` bracket class), consume it and call
1851 // patcompnot(0, ...) to emit the EXCLUDE structure.
1852 let sp_hat = zpc_special.lock().unwrap()[ZPC_HAT as usize];
1853 if c == sp_hat && sp_hat != 0 {
1854 patparse_off.fetch_add(1, Ordering::Relaxed); // c:1012 patparse++
1855 let mut not_flags: i32 = 0;
1856 let starter = patcompnot(0, &mut not_flags); // c:1013 patcompnot(0, ...)
1857 if starter < 0 {
1858 return -1;
1859 }
1860 *flagp |= not_flags & P_HSTART;
1861 if chain_start < 0 {
1862 chain_start = starter;
1863 } else {
1864 set_next(last_tail, starter as usize);
1865 }
1866 // patcompnot (c:1759-1784) ends by emitting the shared trailing
1867 // P_NOTHING (the `excend`/`excl` convergence point), so the
1868 // chain tail is the last node emitted: the current emit position
1869 // is the start of the next node.
1870 let cur_emit = patout.lock().unwrap().len();
1871 last_tail = cur_emit.saturating_sub(I_BODY);
1872 continue;
1873 }
1874 drop(snapshot); // hint: explicit release
1875 let mut piece_flags: i32 = 0;
1876 let mut piece_tail: usize = 0;
1877 // Snapshot the chain tail BEFORE patcomppiece — used by a
1878 // following POSTFIX (#cN,M) to detach this piece from the chain.
1879 let prev_tail_before_piece: i64 = if chain_start < 0 {
1880 -1
1881 } else {
1882 last_tail as i64
1883 };
1884 // !!! WARNING: RUST-ONLY HELPER !!!
1885 //
1886 // C has no such guard here because it cannot need one: c:1336's
1887 // `DPUTS(patparse == str0, "BUG: matched nothing in patcomppiece.")`
1888 // is a debug-only assertion resting on the invariant that every
1889 // `patcomppiece` arm advances `patparse`. When a Rust arm broke that
1890 // invariant (the multibyte `é#` backtrack, pattern.rs:2960) the loop
1891 // below became an unkillable CPU spin that ate the whole shell.
1892 // Turning the same invariant into a hard bail keeps a future
1893 // regression a "bad pattern" error instead of a hang.
1894 let off_before_piece = patparse_off.load(Ordering::Relaxed);
1895 let piece = patcomppiece(&mut piece_flags, paren, &mut piece_tail);
1896 if piece < 0 {
1897 return -1;
1898 }
1899 if patparse_off.load(Ordering::Relaxed) <= off_before_piece {
1900 // c:1336 — the DPUTS condition, made fatal.
1901 return -1;
1902 }
1903 if chain_start < 0 {
1904 chain_start = piece;
1905 } else {
1906 // Chain previous piece's tail → this piece's head directly.
1907 set_next(last_tail, piece as usize);
1908 }
1909 last_tail = piece_tail;
1910 last_piece_off = piece;
1911 prev_chain_tail = prev_tail_before_piece;
1912 last_piece_flags = piece_flags;
1913 *flagp &= piece_flags;
1914 }
1915
1916 if chain_start < 0 {
1917 chain_start = patnode(P_NOTHING) as i64;
1918 }
1919 chain_start
1920}
1921
1922// =====================================================================
1923// 10. Glob-flag parser — pattern.c:1037
1924// =====================================================================
1925
1926/// Port of `patgetglobflags()` from `Src/pattern.c:1037`.
1927/// C: `patgetglobflags(char **strp, long *assertp, int *ignore)`.
1928///
1929/// C signature: `int patgetglobflags(char **strp, long *assertp,
1930/// int *ignore)`. C reads/writes the file-static `patglobflags`
1931/// directly via `|=` / `&=` at each switch arm (c:1064, 1070, 1075,
1932/// 1080, 1085, 1090, 1095, 1100, 1112, 1116). The hoist loop in
1933/// patcompile at c:582 / c:636 snapshots `patglobflags` into the
1934/// compiled `Patprog`'s `globflags` and `globend` fields, so the
1935/// per-arm writes are the canonical store — the return value is
1936/// only the success/fail signal (1 / 0).
1937///
1938/// The Rust port stores `patglobflags` in an AtomicI32 and mirrors
1939/// every C write through `fetch_or` / `fetch_and` ops so the same
1940/// snapshot at c:636 works. The return tuple `(flag_bits, assertp,
1941/// consumed)` is kept as an adapter for the patcompile hoist loop
1942/// (pattern.rs:589) which used it to accumulate `hoisted_globflags`
1943/// — those callers see the same bits via the atomic now, but
1944/// changing the return shape is a wider refactor.
1945///
1946/// Returns `Some((flag_bits, assertp_val, consumed_bytes))` on
1947/// success (C returns 1), or `None` on parse failure (C returns 0).
1948pub fn patgetglobflags(s: &str) -> Option<(i32, i64, usize)> {
1949 // c:1037
1950 let bytes = s.as_bytes();
1951 if !s.starts_with("(#") {
1952 return None;
1953 }
1954 let mut i = 2;
1955 // `bits` tracks what this call set so the caller can OR into
1956 // `hoisted_globflags` (the patcompile-side accumulator). The
1957 // canonical store is `patglobflags` (updated below per C arm).
1958 let mut bits: i32 = 0;
1959 let mut assertp: i64 = 0;
1960
1961 while i < bytes.len() && bytes[i] != b')' {
1962 match bytes[i] {
1963 // c:1073-1076 `'i'`: `patglobflags = (patglobflags &
1964 // ~GF_LCMATCHUC) | GF_IGNCASE`.
1965 b'i' => {
1966 patglobflags.fetch_and(!GF_LCMATCHUC, Ordering::Relaxed);
1967 patglobflags.fetch_or(GF_IGNCASE, Ordering::Relaxed);
1968 bits |= GF_IGNCASE;
1969 bits &= !GF_LCMATCHUC;
1970 i += 1;
1971 } // c:1075
1972 // c:1078-1081 — `'I'`: `patglobflags &= ~(GF_LCMATCHUC|GF_IGNCASE)`.
1973 b'I' => {
1974 patglobflags.fetch_and(!(GF_LCMATCHUC | GF_IGNCASE), Ordering::Relaxed);
1975 bits &= !(GF_LCMATCHUC | GF_IGNCASE);
1976 i += 1;
1977 } // c:1080
1978 // c:1068-1071 — `'l'`: `patglobflags = (patglobflags &
1979 // ~GF_IGNCASE) | GF_LCMATCHUC`.
1980 b'l' => {
1981 patglobflags.fetch_and(!GF_IGNCASE, Ordering::Relaxed);
1982 patglobflags.fetch_or(GF_LCMATCHUC, Ordering::Relaxed);
1983 bits |= GF_LCMATCHUC;
1984 bits &= !GF_IGNCASE;
1985 i += 1;
1986 } // c:1070
1987 // c:1083-1086 — `'b'`: `patglobflags |= GF_BACKREF`.
1988 b'b' => {
1989 patglobflags.fetch_or(GF_BACKREF, Ordering::Relaxed);
1990 bits |= GF_BACKREF;
1991 i += 1;
1992 } // c:1085
1993 // c:1088-1091 — `'B'`: `patglobflags &= ~GF_BACKREF`.
1994 b'B' => {
1995 patglobflags.fetch_and(!GF_BACKREF, Ordering::Relaxed);
1996 bits &= !GF_BACKREF;
1997 i += 1;
1998 } // c:1090
1999 // c:1093-1096 — `'m'`: `patglobflags |= GF_MATCHREF`.
2000 b'm' => {
2001 patglobflags.fetch_or(GF_MATCHREF, Ordering::Relaxed);
2002 bits |= GF_MATCHREF;
2003 i += 1;
2004 } // c:1095
2005 // c:1098-1101 — `'M'`: `patglobflags &= ~GF_MATCHREF`.
2006 b'M' => {
2007 patglobflags.fetch_and(!GF_MATCHREF, Ordering::Relaxed);
2008 bits &= !GF_MATCHREF;
2009 i += 1;
2010 } // c:1100
2011 // c:1103-1105 — `'s'`: sets `*assertp = P_ISSTART`,
2012 // doesn't touch patglobflags.
2013 b's' => {
2014 assertp = P_ISSTART as i64;
2015 i += 1;
2016 } // c:1104
2017 // c:1107-1109 — `'e'`: sets `*assertp = P_ISEND`.
2018 b'e' => {
2019 assertp = P_ISEND as i64;
2020 i += 1;
2021 } // c:1108
2022 // c:1111-1113 — `'u'`: `patglobflags |= GF_MULTIBYTE`.
2023 b'u' => {
2024 patglobflags.fetch_or(GF_MULTIBYTE, Ordering::Relaxed);
2025 bits |= GF_MULTIBYTE;
2026 i += 1;
2027 } // c:1112
2028 // c:1115-1117 — `'U'`: `patglobflags &= ~GF_MULTIBYTE`.
2029 b'U' => {
2030 patglobflags.fetch_and(!GF_MULTIBYTE, Ordering::Relaxed);
2031 bits &= !GF_MULTIBYTE;
2032 i += 1;
2033 } // c:1116
2034 // c:1054-1066 — `'a'`: approximate-match error count.
2035 // `ret = zstrtol(++ptr, &nptr, 10);` then
2036 // `patglobflags = (patglobflags & ~0xff) | (ret & 0xff)`.
2037 b'a' => {
2038 i += 1;
2039 let digit_start = i;
2040 let mut errs: i32 = 0;
2041 while i < bytes.len() && bytes[i].is_ascii_digit() {
2042 errs = errs * 10 + (bytes[i] - b'0') as i32;
2043 i += 1;
2044 }
2045 if i == digit_start {
2046 // c:1062 `ptr == nptr` — no digits consumed.
2047 return None;
2048 }
2049 if errs < 0 || errs > 254 {
2050 return None; // c:1062
2051 }
2052 // c:1064 — `patglobflags = (patglobflags & ~0xff) | (ret & 0xff)`.
2053 let mask: i32 = !0xff;
2054 let cur = patglobflags.load(Ordering::Relaxed);
2055 patglobflags.store((cur & mask) | (errs & 0xff), Ordering::Relaxed);
2056 bits = (bits & !0xff) | (errs & 0xff);
2057 }
2058 // c:1046-1050 — `'q'`: glob qualifier, ignored in pattern
2059 // code. Skip to the closing `)` without consuming bits.
2060 b'q' => {
2061 while i < bytes.len() && bytes[i] != b')' {
2062 i += 1;
2063 }
2064 }
2065 // c:1119-1120 — `default: return 0`.
2066 _ => return None,
2067 }
2068 }
2069 // c:1124-1125 — `if (*ptr != Outpar) return 0;`.
2070 if i >= bytes.len() {
2071 return None;
2072 }
2073 i += 1; // c:1129 — `*strp = ptr + 1;` advance past `)`.
2074 Some((bits, assertp, i))
2075}
2076
2077// =====================================================================
2078// 11. Range helpers — pattern.c:1148, :1179
2079// =====================================================================
2080
2081/// Port of `range_type()` from `Src/pattern.c:1148`.
2082/// C: `range_type(char *start, int len)`. Looks up the
2083/// integer code for a POSIX character class name (e.g. "alpha" → 1).
2084/// Returns None for unknown names.
2085///
2086/// C signature takes a `(char *start, int len)` non-NUL-terminated
2087/// substring; Rust's `&str` carries the length implicitly, so the
2088/// two-arg C shape collapses to a single arg. Param name `start`
2089/// matches C verbatim (Rule E).
2090pub fn range_type(start: &str) -> Option<usize> {
2091 // c:1148
2092 POSIX_CLASS_NAMES
2093 .iter()
2094 .position(|n| *n == start)
2095 .map(|i| i + 1)
2096}
2097
2098/// Port of `pattern_range_to_string()` from `Src/pattern.c:1179`.
2099/// C: `int pattern_range_to_string(char *rangestr, char *outstr)`
2100/// from `Src/pattern.c:1179`. Walks a Meta-encoded range bytestring,
2101/// re-emitting the human-readable form: literal chars as-is,
2102/// PP_RANGE pairs as `c1-c2`, POSIX classes as `[:name:]`.
2103/// Returns the output length; `outstr` is the destination buffer
2104/// (NULL = measure-only). The Rust port operates on `&str` (UTF-8
2105/// native) — `Meta`-byte decode collapses since zshrs's pattern
2106/// compiler stores raw chars, so the function effectively walks
2107/// the chars and emits POSIX class names from the `[i]` table at
2108/// `range_type`'s reverse lookup.
2109///
2110/// Rust signature: drops C's `outstr` out-param. C measures-then-fills
2111/// via two passes when `outstr` is non-NULL; Rust returns the String
2112/// directly. Callers needing the C "measure-only" mode read `.len()`
2113/// on the result.
2114pub fn pattern_range_to_string(rangestr: &str) -> String {
2115 // c:1179
2116 let mut out = String::with_capacity(rangestr.len()); // c:1181 int len = 0
2117 let mut chars = rangestr.chars().peekable();
2118 while let Some(c) = chars.next() {
2119 // c:1184-1247 — swtype dispatch via Meta+PP_*. zshrs stores
2120 // POSIX-class markers as a sentinel byte followed by the
2121 // class name. The Meta encoding doesn't apply (UTF-8 native),
2122 // so we just pass chars through, recognizing PP_* class tags
2123 // when they appear as `[:name:]` literal syntax.
2124 if c == '[' && chars.peek() == Some(&':') {
2125 // c:1242 — `[:alpha:]` and similar.
2126 let mut name = String::new();
2127 chars.next(); // consume ':'
2128 while let Some(&cc) = chars.peek() {
2129 if cc == ':' {
2130 chars.next();
2131 if chars.peek() == Some(&']') {
2132 chars.next();
2133 break;
2134 }
2135 name.push(':');
2136 } else {
2137 name.push(cc);
2138 chars.next();
2139 }
2140 }
2141 out.push_str(&format!("[:{}:]", name)); // c:1244
2142 } else {
2143 // c:1185-1213 — single-char or range pair.
2144 // Check for `c1-c2` PP_RANGE form.
2145 out.push(c); // c:1210/1216
2146 if chars.peek() == Some(&'-') {
2147 chars.next();
2148 if let Some(c2) = chars.next() {
2149 out.push('-'); // c:1219
2150 out.push(c2); // c:1220
2151 }
2152 }
2153 }
2154 }
2155 out // c:1261 return len
2156}
2157
2158/// Port of `patcomppiece()` from `Src/pattern.c:1261`.
2159/// C: `patcomppiece(int *flagp, int paren)`.
2160///
2161/// C: `static long patcomppiece(int *flagp, int paren)`. Parses a
2162/// single atom + optional quantifier. Returns offset of compiled node.
2163/// Out-param `tail_out` receives the byte offset of the LAST opcode
2164/// in the compiled piece — the node whose `.next` should be chained
2165/// to whatever follows in the sequence. For simple atoms (P_EXACTLY,
2166/// P_ANY, etc.) the tail equals the head; for compound pieces
2167/// `(...)` / quantified atoms it points to the trailing P_CLOSE_N or
2168/// quantifier-injected node.
2169///
2170/// Rust signature adds `tail_out: &mut usize` for the LAST-opcode-
2171/// offset that C threads through pointer arithmetic on the
2172/// caller-side `Upat` cursor. Without the out-param, every Rust
2173/// caller would need to re-walk the just-emitted bytecode to find
2174/// the tail; the explicit out-param avoids the re-scan. Rule B
2175/// deviation sanctioned by zshrs's byte-offset bytecode (vs C's
2176/// pointer-arithmetic) substrate.
2177pub fn patcomppiece(flagp: &mut i32, paren: i32, tail_out: &mut usize) -> i64 {
2178 // c:1261
2179 // c:1509-1512 — `case Inpar:` carries two DPUTS asserting that a
2180 // BARE `(` (no kshchar prefix) never reaches the group compiler
2181 // when `zpc_special[ZPC_INPAR] == Marker`. patcompcharsset sets
2182 // that Marker for `isset(SHGLOB)` (c:500-510) and for a user
2183 // `disable -p '('`. So `(` opens a group only when its slot still
2184 // holds the literal byte, or when a ksh trigger (`@(`, `*(`, …)
2185 // put us here deliberately (c:1419 consumes the trigger and then
2186 // falls into `case Inpar`).
2187 let inpar_active = { zpc_special.lock().unwrap()[ZPC_INPAR as usize] == b'(' };
2188 // c:1312-1313 — the literal run ends on `memchr(zpc_special, *patparse,
2189 // ZPC_NO_KSH_GLOB)`, and c:1430-1444 DPUTS that `case Quest` / `case Star`
2190 // / `case Inbrack` are never reached with the slot at Marker. So after
2191 // `disable -p '?'` / `'*'` / `'['` (c:471-476 masks the slot) the byte is
2192 // an ordinary string character, like `(` and `<` above and below.
2193 let (quest_active, star_active, inbrack_active) = {
2194 let sp = zpc_special.lock().unwrap();
2195 (
2196 sp[ZPC_QUEST as usize] == b'?',
2197 sp[ZPC_STAR as usize] == b'*',
2198 sp[ZPC_INBRACK as usize] == b'[',
2199 )
2200 };
2201 let off = patparse_off.load(Ordering::Relaxed);
2202 let parse = patparse.lock().unwrap();
2203 if off >= parse.len() {
2204 return patnode(P_NOTHING) as i64;
2205 }
2206 let bytes = parse.as_bytes();
2207 let c = bytes[off];
2208
2209 // c:1278 — `kshchar = '\0';`. KSH-glob trigger detection: when the
2210 // current byte matches one of the six ZPC_KSH_* slots AND the next
2211 // byte is `(`, record which kshchar so the post-atom dispatch can
2212 // emit the right quantifier shape (c:1615-1746).
2213 //
2214 // c:1279 — `if (*patparse && patparse[1] == Inpar) { ... }`. Note
2215 // the C code uses literal `Inpar` (the `(` token) for the lookahead
2216 // — NOT `zpc_special[ZPC_INPAR]` — so SHGLOB disabling `(` as a
2217 // pattern char doesn't suppress ksh-glob detection here.
2218 let mut kshchar: u8 = 0; // c:1278
2219 if off + 1 < parse.len() && bytes[off + 1] == b'(' {
2220 // c:1279
2221 let sp = zpc_special.lock().unwrap();
2222 if c == sp[ZPC_KSH_PLUS as usize] {
2223 kshchar = b'+';
2224 }
2225 // c:1280-1281
2226 else if c == sp[ZPC_KSH_BANG as usize] {
2227 kshchar = b'!';
2228 }
2229 // c:1282-1283
2230 else if c == sp[ZPC_KSH_BANG2 as usize] {
2231 kshchar = b'!';
2232 }
2233 // c:1284-1285
2234 else if c == sp[ZPC_KSH_AT as usize] {
2235 kshchar = b'@';
2236 }
2237 // c:1286-1287
2238 else if c == sp[ZPC_KSH_STAR as usize] {
2239 kshchar = b'*';
2240 }
2241 // c:1288-1289
2242 else if c == sp[ZPC_KSH_QUEST as usize] {
2243 kshchar = b'?';
2244 } // c:1290-1291
2245 }
2246 drop(parse);
2247
2248 // c:1419-1420 — `if (kshchar) patparse++;`. Skip the trigger byte
2249 // so the atom dispatch consumes the leading `(` as a group.
2250 let dispatch_c = if kshchar != 0 {
2251 patparse_off.fetch_add(1, Ordering::Relaxed);
2252 b'(' // c:1419 — fall through to Inpar case
2253 } else {
2254 c
2255 };
2256
2257 // Atom dispatch. Each arm sets `*tail_out` to the offset of the
2258 // last opcode emitted by this piece (for simple atoms, tail = head).
2259 let atom = match dispatch_c {
2260 b'?' if quest_active => {
2261 // c:1430
2262 patparse_off.fetch_add(1, Ordering::Relaxed);
2263 *flagp |= P_SIMPLE;
2264 *flagp &= !P_PURESTR;
2265 let h = patnode(P_ANY);
2266 *tail_out = h;
2267 h as i64
2268 }
2269 b'*' if star_active => {
2270 // c:1436
2271 patparse_off.fetch_add(1, Ordering::Relaxed);
2272 *flagp &= !P_PURESTR;
2273 let h = patnode(P_STAR);
2274 *tail_out = h;
2275 h as i64
2276 }
2277 b'[' if inbrack_active => {
2278 // c:Src/pattern.c:1438 `case Inbrack` + c:1497-1498 —
2279 // `if (*patparse != Outbrack) return 0;`: an ACTIVE `[`
2280 // (token-derived; the patcompile entry normalization maps
2281 // the Inbrack token to raw `[` and every literal/escaped
2282 // bracket to `\[`, which the `\X` arm consumed before
2283 // reaching here) with no closing `]` is a BAD PATTERN.
2284 // Callers zerr "bad pattern: %s" exactly like zsh:
2285 // `print [a-` / `[[ x == [a- ]]` / `case x in [a-)`.
2286 //
2287 // History: a 2026-06-03 partial fix for Bug #564 made
2288 // this case fall back to a literal `[` because the cond
2289 // path then stripped `\` before patcompile. The escape
2290 // now survives into the `\X` literal arm (all three #564
2291 // probes pass), so the leniency only masked genuine bad
2292 // patterns.
2293 let probe_off = off + 1;
2294 let parse_check = patparse.lock().unwrap();
2295 let check_bytes = parse_check.as_bytes();
2296 let has_close = check_bytes[probe_off..].contains(&b']');
2297 drop(parse_check);
2298 if !has_close {
2299 return -1; // c:1498 `return 0;` — bad pattern
2300 }
2301 patparse_off.fetch_add(1, Ordering::Relaxed);
2302 *flagp |= P_SIMPLE;
2303 *flagp &= !P_PURESTR;
2304 // Inline bracket-expression parse (C patcomppiece bracket case).
2305 let mut chars: Vec<u8> = Vec::new();
2306 // Multibyte augmentation: `chars` is a flat ASCII byte set, so a
2307 // multibyte input char (Cyrillic/Greek/CJK/accented) can never
2308 // match it. C's `mb_patmatchrange` handles wide chars via
2309 // `iswalpha`/wide ranges. To match without disturbing the
2310 // (heavily-tested, byte-for-byte) ASCII path, collect the class
2311 // predicates and the non-ASCII literals/ranges SEPARATELY and
2312 // consult them only when the input char is multibyte (P_ANYOF).
2313 let mut mb_classmask: u32 = 0;
2314 let mut mb_chars: Vec<u32> = Vec::new();
2315 let mut mb_ranges: Vec<(u32, u32)> = Vec::new();
2316 let mut negate = false;
2317 let bracket_start = patparse_off.load(Ordering::Relaxed);
2318 let parse_b = patparse.lock().unwrap();
2319 let bb = parse_b.as_bytes();
2320 let mut i_b = bracket_start;
2321 if i_b < bb.len() && (bb[i_b] == b'^' || bb[i_b] == b'!') {
2322 negate = true;
2323 i_b += 1;
2324 }
2325 // c:Src/pattern.c:1456-1459 — `[]...]` exception: when `]` is
2326 // the FIRST char inside the class AND another `]` follows
2327 // later (so the close is not ambiguous), the first `]` is a
2328 // class member. Mirrors `[]x]` matching `]` or `x` in C.
2329 if i_b < bb.len() && bb[i_b] == b']' && bb[i_b + 1..].contains(&b']') {
2330 chars.push(b']');
2331 i_b += 1;
2332 }
2333 // A METAFIED raw byte (Meta U+0083 + byte^32, how zshrs holds a
2334 // byte that is not valid UTF-8) is ONE member, the character
2335 // C's range walk decodes from it: the byte itself without
2336 // GF_MULTIBYTE, else `mbrtowc` over the raw run, or
2337 // WCHAR_INVALID (c:384 metacharinc, compared at c:3634
2338 // `metacharinc(&range) == ch`). The input decode above spells
2339 // each pair `\<Meta>\<payload>`, which the `\X` arm below took
2340 // as two one-byte members — the first UTF-8 byte of each char —
2341 // so `[\xcc]` never matched `\xcc`. Rebuild the run of pairs
2342 // (a payload is always a 2-byte char, so a pair is 4 bytes
2343 // there and 6 here) and decode it with charrefinc.
2344 let esc_pair = |at: usize| -> bool {
2345 bb.get(at..at + 4) == Some(&[b'\\', 0xc2, 0x83, b'\\'][..])
2346 && bb.get(at + 4).is_some_and(|&x| x >= 0xc2)
2347 };
2348 let member = |at: usize, z: &mut i32| -> Option<(u32, usize)> {
2349 let mut run = String::new();
2350 let mut q = at;
2351 while esc_pair(q) {
2352 let payload = std::str::from_utf8(bb.get(q + 4..q + 6)?).ok()?;
2353 run.push('\u{83}');
2354 run.push_str(payload);
2355 q += 6;
2356 }
2357 let mut used = 0;
2358 let wc = charrefinc(&run, &mut used, patglobflags.load(Ordering::Relaxed), z)?;
2359 Some((wc, at + used / 4 * 6))
2360 };
2361 while i_b < bb.len() && bb[i_b] != b']' {
2362 if esc_pair(i_b) {
2363 let mut z = 0;
2364 if let Some((lo, mut p)) = member(i_b, &mut z) {
2365 let mut hi = lo;
2366 if bb.get(p) == Some(&b'-') && esc_pair(p + 1) {
2367 if let Some((h, q)) = member(p + 1, &mut z) {
2368 hi = h;
2369 p = q;
2370 }
2371 }
2372 if (patglobflags.load(Ordering::Relaxed) & GF_MULTIBYTE) == 0 {
2373 // c:1974-1975 — one raw byte per character.
2374 for r in lo..=hi.min(0xff) {
2375 chars.push(r as u8);
2376 }
2377 } else if lo == hi {
2378 mb_chars.push(lo);
2379 } else {
2380 mb_ranges.push((lo, hi));
2381 }
2382 i_b = p;
2383 continue;
2384 }
2385 }
2386 // c:Src/pattern.c — `\X` inside a class is handled in C
2387 // by shtokenize converting it to `Bnullkeep X` upstream;
2388 // by the time C's bracket walker runs, the `]` after `\`
2389 // doesn't appear as raw `]` so the close-`]` scan
2390 // doesn't trip on it. The Rust port may receive raw
2391 // `\X` (callers that bypass shtokenize), so handle the
2392 // backslash-escape inline: consume `\X` as a literal
2393 // class-member byte. Bug surface:
2394 // `${q//(#m)[\][()|\\*?#<>~^]/...}` — escaped `]` /
2395 // `\\` / etc. inside the class are class members, not
2396 // metacharacters.
2397 if i_b + 1 < bb.len() && bb[i_b] == b'\\' {
2398 chars.push(bb[i_b + 1]);
2399 i_b += 2;
2400 continue;
2401 }
2402 if i_b + 1 < bb.len() && bb[i_b] == b'[' && bb[i_b + 1] == b':' {
2403 let class_start = i_b + 2;
2404 let mut j_b = class_start;
2405 while j_b + 1 < bb.len() && !(bb[j_b] == b':' && bb[j_b + 1] == b']') {
2406 j_b += 1;
2407 }
2408 if j_b + 1 < bb.len() {
2409 let class_name = std::str::from_utf8(&bb[class_start..j_b]).unwrap_or("");
2410 // Inline POSIX class expansion.
2411 match class_name {
2412 "alpha" => {
2413 for c in b'a'..=b'z' {
2414 chars.push(c);
2415 }
2416 for c in b'A'..=b'Z' {
2417 chars.push(c);
2418 }
2419 }
2420 "upper" => {
2421 for c in b'A'..=b'Z' {
2422 chars.push(c);
2423 }
2424 }
2425 "lower" => {
2426 for c in b'a'..=b'z' {
2427 chars.push(c);
2428 }
2429 }
2430 "digit" => {
2431 for c in b'0'..=b'9' {
2432 chars.push(c);
2433 }
2434 }
2435 "xdigit" => {
2436 for c in b'0'..=b'9' {
2437 chars.push(c);
2438 }
2439 for c in b'a'..=b'f' {
2440 chars.push(c);
2441 }
2442 for c in b'A'..=b'F' {
2443 chars.push(c);
2444 }
2445 }
2446 "alnum" => {
2447 for c in b'a'..=b'z' {
2448 chars.push(c);
2449 }
2450 for c in b'A'..=b'Z' {
2451 chars.push(c);
2452 }
2453 for c in b'0'..=b'9' {
2454 chars.push(c);
2455 }
2456 }
2457 "space" => {
2458 for b in b" \t\n\r\x0b\x0c".iter() {
2459 chars.push(*b);
2460 }
2461 }
2462 "blank" => {
2463 chars.push(b' ');
2464 chars.push(b'\t');
2465 }
2466 "punct" => {
2467 for b in b"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".iter() {
2468 chars.push(*b);
2469 }
2470 }
2471 "cntrl" => {
2472 for c in 0u8..=31 {
2473 chars.push(c);
2474 }
2475 chars.push(127);
2476 }
2477 "print" => {
2478 for c in 32u8..=126 {
2479 chars.push(c);
2480 }
2481 }
2482 "graph" => {
2483 for c in 33u8..=126 {
2484 chars.push(c);
2485 }
2486 }
2487 // c:Src/pattern.c:3894 — `PP_ASCII: return !(c & ~0x7f)`.
2488 // A FIXED, locale-independent set, so unlike the
2489 // PP_IDENT/PP_IFS/PP_WORD family below it belongs in
2490 // the compile-time `chars` set rather than a
2491 // match-time classmask bit.
2492 //
2493 // It had NO arm here and NO bit in the classmask
2494 // match below, so it fell to both `_` catch-alls and
2495 // contributed nothing: `[[ a == [[:ascii:]] ]]` was
2496 // FALSE where zsh is true, and the negation inverted.
2497 // `[[:ascii:]0-9]` still matched `0-9`, which is what
2498 // made the class look present. Every other POSIX
2499 // class was already correct.
2500 //
2501 // A byte >= 0x80 leads a UTF-8 sequence and is
2502 // handled by the multibyte path below, which
2503 // correctly reports no match for this class — so the
2504 // ASCII-only member set is complete, not partial.
2505 "ascii" => {
2506 for c in 0u8..=127 {
2507 chars.push(c);
2508 }
2509 }
2510 // c:Src/pattern.c:3693-3700 + Src/ztype.h
2511 // IIDENT — `[[:IDENT:]]` is zsh's
2512 // identifier-character class (alnum + `_`,
2513 // the chars valid in a parameter name).
2514 // Static for the ASCII range; the
2515 // multibyte wcsitype(IIDENT) path isn't
2516 // expanded here. p10k/zsh-z validate names
2517 // with `[[:IDENT:]]##`.
2518 // c:3702-3719 — PP_IDENT / PP_IFS /
2519 // PP_IFSSPACE / PP_WORD are evaluated at MATCH
2520 // time in C (`wcsitype(ch, IIDENT|ISEP|IWORD)`,
2521 // `iwsep`), reading the live `$IFS` / `$WORDCHARS`
2522 // through the ztype table. Expanding them into a
2523 // static `chars` set at COMPILE time froze the
2524 // first observed value into the cached Patprog, so
2525 // `IFS=' ' [[ $'\n' = [[:IFS:]] ]]` reused the set
2526 // built under the previous `$IFS`. Emit a
2527 // classmask bit instead; anyof_membership resolves
2528 // it per candidate character.
2529 // c:3737-3744 — PP_INCOMPLETE / PP_INVALID test the
2530 // multibyte DECODE STATE of the candidate
2531 // character (`zmb_ind == ZMB_INCOMPLETE` /
2532 // `ZMB_INVALID`), never a fixed character set, so
2533 // like the four live classes above they carry no
2534 // compile-time members.
2535 "IDENT" | "IFS" | "IFSSPACE" | "WORD" | "INCOMPLETE" | "INVALID" => {}
2536 _ => {}
2537 }
2538 // Record the class predicate for multibyte input.
2539 // Bits mirror the Unicode-aware `iswXXX` checks C's
2540 // mb_patmatchrange applies (pattern.rs:3364). digit/
2541 // xdigit stay ASCII-only (already in `chars`) — zsh's
2542 // iswdigit is ASCII in the common locale.
2543 mb_classmask |= match class_name {
2544 "alpha" => 1 << 0,
2545 "alnum" => 1 << 1,
2546 "upper" => 1 << 2,
2547 "lower" => 1 << 3,
2548 "space" => 1 << 4,
2549 "IFS" => 1 << 11,
2550 "IFSSPACE" => 1 << 12,
2551 "blank" => 1 << 5,
2552 "punct" => 1 << 6,
2553 "cntrl" => 1 << 7,
2554 "print" => 1 << 8,
2555 "graph" => 1 << 9,
2556 // c:3702-3719 — live PP_ classes, resolved at
2557 // match time (see the arm above).
2558 "IDENT" => 1 << 10,
2559 "WORD" => 1 << 13,
2560 // c:3737-3744 — decode-state classes.
2561 "INCOMPLETE" => 1 << 14,
2562 "INVALID" => 1 << 15,
2563 _ => 0,
2564 };
2565 i_b = j_b + 2;
2566 continue;
2567 }
2568 }
2569 // Multibyte member: a byte >= 0x80 leads a UTF-8 sequence
2570 // that cannot live in the ASCII `chars` set. Decode it as a
2571 // wide literal, or a wide range `<ch>-<ch>`, for the P_ANYOF
2572 // multibyte path.
2573 if bb[i_b] >= 0x80 {
2574 if let Some(lo) = std::str::from_utf8(&bb[i_b..])
2575 .ok()
2576 .and_then(|s| s.chars().next())
2577 {
2578 let lolen = lo.len_utf8();
2579 if i_b + lolen < bb.len()
2580 && bb[i_b + lolen] == b'-'
2581 && i_b + lolen + 1 < bb.len()
2582 && bb[i_b + lolen + 1] != b']'
2583 {
2584 if let Some(hi) = std::str::from_utf8(&bb[i_b + lolen + 1..])
2585 .ok()
2586 .and_then(|s| s.chars().next())
2587 {
2588 mb_ranges.push((lo as u32, hi as u32));
2589 i_b += lolen + 1 + hi.len_utf8();
2590 continue;
2591 }
2592 }
2593 mb_chars.push(lo as u32);
2594 i_b += lolen;
2595 continue;
2596 }
2597 }
2598 if i_b + 2 < bb.len() && bb[i_b + 1] == b'-' && bb[i_b + 2] != b']' {
2599 let lo = bb[i_b];
2600 let hi = bb[i_b + 2];
2601 // ASCII-low, multibyte-high range (`[a-я]`): record as a
2602 // wide range so the high end matches; the ASCII portion
2603 // of the range still expands into `chars` below.
2604 if hi >= 0x80 {
2605 if let Some(hich) = std::str::from_utf8(&bb[i_b + 2..])
2606 .ok()
2607 .and_then(|s| s.chars().next())
2608 {
2609 mb_ranges.push((lo as u32, hich as u32));
2610 for c in lo..=0x7f {
2611 chars.push(c);
2612 }
2613 i_b += 2 + hich.len_utf8();
2614 continue;
2615 }
2616 }
2617 for c in lo..=hi {
2618 chars.push(c);
2619 }
2620 i_b += 3;
2621 } else {
2622 chars.push(bb[i_b]);
2623 i_b += 1;
2624 }
2625 }
2626 drop(parse_b);
2627 if let Some(p_lock) = patparse.lock().ok() {
2628 if i_b < p_lock.len() && p_lock.as_bytes()[i_b] == b']' {
2629 i_b += 1;
2630 }
2631 }
2632 patparse_off.store(i_b, Ordering::Relaxed);
2633 let opcode = if negate { P_ANYBUT } else { P_ANYOF };
2634 let off2 = patnode(opcode);
2635 let mut buf = patout.lock().unwrap();
2636 // Body layout: `[len:u32]` (total following bytes) then
2637 // `[chars_len:u32][chars][classmask:u32][n_mbchars:u32]
2638 // [mbchars:u32*n][n_mbranges:u32][(lo,hi):u32*2]`. `len` counts
2639 // the WHOLE body so `advance_past_instr` / node traversal (which
2640 // do `4 + len`) skip it unchanged — only the P_ANYOF/P_ANYBUT
2641 // matcher parses the sub-structure. Pure-ASCII brackets carry a
2642 // zeroed 12-byte tail (classmask 0, no mbchars/mbranges).
2643 let mut body_ext: Vec<u8> = Vec::new();
2644 body_ext.extend_from_slice(&(chars.len() as u32).to_le_bytes());
2645 body_ext.extend_from_slice(&chars);
2646 body_ext.extend_from_slice(&mb_classmask.to_le_bytes());
2647 body_ext.extend_from_slice(&(mb_chars.len() as u32).to_le_bytes());
2648 for c in &mb_chars {
2649 body_ext.extend_from_slice(&(*c as u32).to_le_bytes());
2650 }
2651 body_ext.extend_from_slice(&(mb_ranges.len() as u32).to_le_bytes());
2652 for (lo, hi) in &mb_ranges {
2653 body_ext.extend_from_slice(&(*lo as u32).to_le_bytes());
2654 body_ext.extend_from_slice(&(*hi as u32).to_le_bytes());
2655 }
2656 let len = body_ext.len() as u32;
2657 buf.extend_from_slice(&len.to_le_bytes());
2658 buf.extend_from_slice(&body_ext);
2659 *tail_out = off2;
2660 off2 as i64
2661 }
2662 // c:1509-1512 — guard mirrors the two DPUTS: a disabled `(`
2663 // (SHGLOB, or `disable -p '('`) is an ORDINARY character unless
2664 // a ksh trigger routed us here.
2665 b'(' if kshchar != 0 || inpar_active => {
2666 patparse_off.fetch_add(1, Ordering::Relaxed);
2667 *flagp &= !P_PURESTR;
2668 // c:1508-1525 — `if (kshchar == '!') patcompnot(1, ...) else
2669 // patcompswitch(1, ...)`. The `!(pat)` form needs a
2670 // negation-aware compile that builds an EXCLUDE subtree;
2671 // every other case (including `@(pat)`, the bare `(pat)`
2672 // group, and the `+/*/?` ksh forms) takes the alternation
2673 // switch path.
2674 if kshchar == b'!' {
2675 // c:1522-1523
2676 let mut flags2: i32 = 0;
2677 let starter_off = patcompnot(1, &mut flags2);
2678 if starter_off < 0 {
2679 return -1;
2680 }
2681 *flagp |= flags2 & P_HSTART; // c:1526
2682 // c:1019 — the caller chains the FOLLOWING piece with
2683 // `pattail(chain, latest)`, which WALKS to the end of this
2684 // piece's next-chain. patcompnot's chain is
2685 // `P_BRANCH -> P_EXCLUDE -> P_NOTHING` (c:1773, c:1779-1781),
2686 // so the tail is the trailing P_NOTHING, NOT the P_BRANCH.
2687 // Reporting the P_BRANCH made patcompbranch's `set_next`
2688 // OVERWRITE the branch's link to the P_EXCLUDE, silently
2689 // deleting the negation whenever anything followed it:
2690 // `[[ foob = !(foo)b* ]]` matched, as did
2691 // `[[ mad.moo.cow = !(*.*).!(*.*) ]]`. (The `^pat` arm in
2692 // patcompbranch already compensates by hand.)
2693 let mut t = starter_off as usize;
2694 loop {
2695 let n = {
2696 let buf = patout.lock().unwrap();
2697 if t + I_NEXT + 4 > buf.len() {
2698 0
2699 } else {
2700 u32::from_le_bytes(buf[t + I_NEXT..t + I_NEXT + 4].try_into().unwrap())
2701 as usize
2702 }
2703 };
2704 if n == 0 || n == t {
2705 break;
2706 }
2707 t = n;
2708 }
2709 *tail_out = t;
2710 // c:1419 already consumed `!`; the b'(' branch consumed
2711 // `(`; patcompnot drained through to `)`. Verify here.
2712 return starter_off;
2713 }
2714 // c:775-783 — `if (paren && (patglobflags & GF_BACKREF) &&
2715 // patnpar <= NSUBEXP) { parno = patnpar++; starter =
2716 // patnode(P_OPEN + parno); } else starter = 0;`. A group is
2717 // NUMBERED (capture slot allocated) only when GF_BACKREF is
2718 // live in patglobflags at the point the `(` is compiled —
2719 // wherever the `(#b)` appeared (leading, after a literal
2720 // prefix, mid-pattern). Beyond NSUBEXP, C "just use[s]
2721 // P_OPEN on its own" (c:777-780): parno stays 0 and the
2722 // match arms record nothing (c:2957 `if (no && ...)`).
2723 // The previous Rust arm numbered EVERY paren and returned
2724 // -1 past NSUBEXP — both divergent.
2725 let gf_now = patglobflags.load(Ordering::Relaxed);
2726 let n = if (gf_now & GF_BACKREF) != 0
2727 && patnpar.load(Ordering::Relaxed) <= NSUBEXP as i32
2728 {
2729 patnpar.fetch_add(1, Ordering::Relaxed)
2730 } else {
2731 0 // plain (uncaptured) group — P_OPEN+0 / P_CLOSE+0
2732 };
2733 let opcode = P_OPEN + n as u8;
2734 let open_off = patnode(opcode);
2735 // c:770 — `savglobflags = patglobflags`. Snapshot the glob
2736 // flags entering the group so a flag set INSIDE it (e.g.
2737 // `((#i)foo)`) can be restored on the way out.
2738 let savglobflags = patglobflags.load(Ordering::Relaxed);
2739 let mut inner_flags: i32 = 0;
2740 let inner = patcompswitch(1, &mut inner_flags);
2741 if inner < 0 {
2742 return -1;
2743 }
2744 // Expect closing ')'.
2745 let cur_off = patparse_off.load(Ordering::Relaxed);
2746 let p = patparse.lock().unwrap();
2747 if cur_off >= p.len() || p.as_bytes()[cur_off] != b')' {
2748 return -1;
2749 }
2750 drop(p);
2751 patparse_off.fetch_add(1, Ordering::Relaxed);
2752 let close_off = patnode(P_CLOSE + n as u8);
2753 // P_OPEN_N.next → first BRANCH of inner alternation.
2754 set_next(open_off, inner as usize);
2755 // Mirror C pattern.c:903 `pattail(starter, ender)`:
2756 // walk the BRANCH alt-chain from P_OPEN and patch the
2757 // last BRANCH's `.next` to P_CLOSE_N. Without this, the
2758 // outer `pattail` walk would descend into the inner
2759 // alt-chain (P_OPEN.next → inner BRANCH) and corrupt
2760 // its terminator.
2761 pattail(open_off, close_off);
2762 // Each branch's operand chain ends at P_CLOSE_N.
2763 chain_branches_to(inner as usize, close_off);
2764 *flagp &= !P_PURESTR;
2765 // c:920-929 — `if (paren && gfchanged) { pattail(ender,
2766 // patnode(P_GFLAGS)); patglobflags = savglobflags; }`. When
2767 // a glob flag that the matcher honors mid-pattern
2768 // (IGNCASE / LCMATCHUC / MULTIBYTE) changed inside the
2769 // group, append a P_GFLAGS node after P_CLOSE that restores
2770 // the entering value. Without it `[[ FOOXx = ((#i)foox)X ]]`
2771 // wrongly matched: the `(#i)` set GF_IGNCASE for the rest of
2772 // the match, so the trailing case-sensitive `X` folded too.
2773 //
2774 // c:921-925 — "gfchanged detects a change in any branch
2775 // (except exclusions which are separate), since we need to
2776 // emit this even if a later branch happened to put the flags
2777 // back." Comparing only the END state missed
2778 // `((#I)foo|(#i)rod)grud`: branch 1's P_GFLAGS(0) leaks into
2779 // the trailing `grud` because branch 2 restored the entering
2780 // value, so no restore node was emitted.
2781 let relevant = GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE;
2782 if PATSWITCH_GFCHANGED.load(Ordering::Relaxed) != 0 {
2783 let gf_off = patnode(P_GFLAGS);
2784 {
2785 let mut buf = patout.lock().unwrap();
2786 buf.extend_from_slice(&(savglobflags & relevant).to_le_bytes());
2787 }
2788 set_next(close_off, gf_off);
2789 patglobflags.store(savglobflags, Ordering::Relaxed); // c:927
2790 *tail_out = gf_off;
2791 return open_off as i64;
2792 }
2793 *tail_out = close_off;
2794 open_off as i64
2795 }
2796 b'\\' => {
2797 // c:Src/pattern.c — raw `\X` reaches this arm when the
2798 // input bypassed shtokenize (Src/glob.c:3565), which is
2799 // the path taken by paramsubst callers passing singsub-
2800 // computed pattern strings. C's faithful path: shtokenize
2801 // converts `\X` (special X) to `Bnullkeep X` (case at
2802 // pattern.c:1584), and a `\X` with non-special X stays
2803 // raw — both bytes survive to pattern.c's literal-run
2804 // default arm. Trailing lone `\` also survives shtokenize
2805 // (bslash flag stays 1 to end-of-string, no replacement
2806 // fires) and reaches the default-arm literal emission.
2807 //
2808 // Two arms mirror that:
2809 // - `\X` (off2 in range): emit X as literal. Equivalent
2810 // to Bnullkeep X arm in C's pattern.c — the next byte
2811 // is the user-literal payload.
2812 // - trailing lone `\`: emit `\` as literal. Equivalent
2813 // to C's pattern.c default arm receiving a raw `\`
2814 // byte that survived shtokenize.
2815 patparse_off.fetch_add(1, Ordering::Relaxed);
2816 let p = patparse.lock().unwrap();
2817 let off2 = patparse_off.load(Ordering::Relaxed);
2818 if off2 >= p.len() {
2819 drop(p);
2820 *flagp |= P_SIMPLE;
2821 let lit_off = patnode(P_EXACTLY);
2822 let mut buf_lit = patout.lock().unwrap();
2823 buf_lit.extend_from_slice(&1u32.to_le_bytes());
2824 buf_lit.push(b'\\');
2825 *tail_out = lit_off;
2826 return lit_off as i64;
2827 }
2828 let escaped = p.as_bytes()[off2];
2829 drop(p);
2830 patparse_off.fetch_add(1, Ordering::Relaxed);
2831 *flagp |= P_SIMPLE;
2832 let lit_off = patnode(P_EXACTLY);
2833 let mut buf_lit = patout.lock().unwrap();
2834 buf_lit.extend_from_slice(&1u32.to_le_bytes());
2835 buf_lit.push(escaped);
2836 *tail_out = lit_off;
2837 lit_off as i64
2838 }
2839 b'<' => {
2840 // Numeric range: <a-b> / <a-> / <-b> / <-> .
2841 // Port of pattern.c:1528-1570 (Inang case).
2842 //
2843 // c:Src/pattern.c:1528 — C's `case Inang:` only fires for
2844 // the Inang TOKEN (0x99) emitted by the lexer at
2845 // Src/lex.c:1202 when `isnumglob()` returns true (i.e.
2846 // the `<…>` body parses as a numeric range). Raw `<` (0x3C)
2847 // in source — quoted `"<"`, escaped `\<`, or any `<` in a
2848 // non-numglob position — never reaches the pattern compiler
2849 // as a metachar; it falls through to the literal-run arm.
2850 //
2851 // zshrs's pattern compiler is fed by callers that mix lexer
2852 // tokens with raw ASCII (singsub output, runtime-computed
2853 // pattern strings that never went through `Src/lex.c:1201`'s
2854 // isnumglob check). The faithful port: do the same
2855 // `[digit]*-[digit]*>` walk inline that `isnumglob` does at
2856 // lex.c:580-610, with full state-save + rewind on failure so
2857 // a non-numeric `<` (e.g. `<[:digit:]]#>` from zconvey's
2858 // color-marker pattern, or `<no-data>` from zinit.zsh:2507)
2859 // falls back to the literal-run arm.
2860 //
2861 // The numglob shape mirrors C's isnumglob exactly: digits
2862 // then `-` then digits then `>`. Empty leading / trailing
2863 // digit runs are OK (gives the `<-N>` / `<N->` / `<->` forms).
2864 // c:Src/pattern.c:499-506 — `if (isset(SHGLOB))
2865 // zpc_special[ZPC_INPAR] = zpc_special[ZPC_INANG] = Marker;`
2866 // ("Grouping and numeric ranges are not valid. We do allow
2867 // alternation, however; it's needed for case."). patcompcharsset
2868 // (pattern.rs:487) already applies that mask, but this arm never
2869 // consulted the table: it ran its inline isnumglob walk
2870 // unconditionally, so `<->` stayed an ACTIVE numeric range under
2871 // `emulate sh` / `emulate ksh`. C asserts the case is unreachable
2872 // there — `DPUTS(isset(SHGLOB), "Treating <..> as numeric range
2873 // with SHGLOB")` (c:1532). A disabled slot also covers user
2874 // `disable -p '<'`. Fall through to the same literal-`<` emit the
2875 // non-numeric rewind path uses. Bug #1053-B.
2876 let inang_disabled = { zpc_special.lock().unwrap()[ZPC_INANG as usize] != b'<' };
2877 if inang_disabled {
2878 let h = patnode(P_EXACTLY);
2879 let mut buf = patout.lock().unwrap();
2880 let len: u32 = 1;
2881 buf.extend_from_slice(&len.to_le_bytes());
2882 buf.push(b'<');
2883 drop(buf);
2884 patparse_off.fetch_add(1, Ordering::Relaxed);
2885 *flagp |= P_SIMPLE;
2886 *tail_out = h;
2887 return h as i64;
2888 }
2889 let entry_off = patparse_off.load(Ordering::Relaxed);
2890 patparse_off.fetch_add(1, Ordering::Relaxed); // consume `<`
2891 let parse_n = patparse.lock().unwrap();
2892 let nb = parse_n.as_bytes();
2893 let mut j = patparse_off.load(Ordering::Relaxed);
2894 let mut len_flag: u8 = 0; // bit 0 = lo present, bit 1 = hi present
2895 let mut from: i64 = 0;
2896 let lo_start = j;
2897 while j < nb.len() && nb[j].is_ascii_digit() {
2898 from = from * 10 + (nb[j] - b'0') as i64;
2899 j += 1;
2900 }
2901 if j > lo_start {
2902 len_flag |= 1;
2903 } // c:1538 — `len |= 1`
2904 // Mandatory dash. C's isnumglob bails (ret=0) here too —
2905 // walks until non-digit, then if non-digit isn't `-` (or `>`
2906 // after seeing `-`), returns "not numglob". Rewind the `<`
2907 // consumption and fall through to the literal-run arm.
2908 if j >= nb.len() || nb[j] != b'-' {
2909 drop(parse_n);
2910 patparse_off.store(entry_off, Ordering::Relaxed);
2911 let h = patnode(P_EXACTLY);
2912 let mut buf = patout.lock().unwrap();
2913 let len: u32 = 1;
2914 buf.extend_from_slice(&len.to_le_bytes());
2915 buf.push(b'<');
2916 patparse_off.fetch_add(1, Ordering::Relaxed);
2917 *flagp |= P_SIMPLE;
2918 *tail_out = h;
2919 return h as i64;
2920 }
2921 j += 1; // c:1543 patparse++
2922 let mut to: i64 = 0;
2923 let hi_start = j;
2924 while j < nb.len() && nb[j].is_ascii_digit() {
2925 to = to * 10 + (nb[j] - b'0') as i64;
2926 j += 1;
2927 }
2928 if j > hi_start {
2929 len_flag |= 2;
2930 } // c:1548 — `len |= 2`
2931 // Expect closing '>'. Mirror the dash-failure rewind here
2932 // too: a `<N-` without `>` is non-numglob per C's isnumglob.
2933 if j >= nb.len() || nb[j] != b'>' {
2934 drop(parse_n);
2935 patparse_off.store(entry_off, Ordering::Relaxed);
2936 let h = patnode(P_EXACTLY);
2937 let mut buf = patout.lock().unwrap();
2938 let len: u32 = 1;
2939 buf.extend_from_slice(&len.to_le_bytes());
2940 buf.push(b'<');
2941 patparse_off.fetch_add(1, Ordering::Relaxed);
2942 *flagp |= P_SIMPLE;
2943 *tail_out = h;
2944 return h as i64;
2945 }
2946 j += 1;
2947 drop(parse_n);
2948 patparse_off.store(j, Ordering::Relaxed);
2949 *flagp &= !P_PURESTR;
2950
2951 let off2 = match len_flag {
2952 // c:1552-1567
2953 3 => {
2954 // c:1554 P_NUMRNG
2955 let off2 = patnode(P_NUMRNG);
2956 let mut buf = patout.lock().unwrap();
2957 buf.extend_from_slice(&from.to_le_bytes());
2958 buf.extend_from_slice(&to.to_le_bytes());
2959 off2
2960 }
2961 2 => {
2962 // c:1559 P_NUMTO
2963 let off2 = patnode(P_NUMTO);
2964 let mut buf = patout.lock().unwrap();
2965 buf.extend_from_slice(&to.to_le_bytes());
2966 off2
2967 }
2968 1 => {
2969 // c:1563 P_NUMFROM
2970 let off2 = patnode(P_NUMFROM);
2971 let mut buf = patout.lock().unwrap();
2972 buf.extend_from_slice(&from.to_le_bytes());
2973 off2
2974 }
2975 _ => patnode(P_NUMANY), // c:1568
2976 };
2977 *tail_out = off2;
2978 off2 as i64
2979 }
2980 _ => {
2981 // Accumulate a literal run.
2982 let mut buf: Vec<u8> = Vec::new();
2983 let mut local_off = off;
2984 let p = patparse.lock().unwrap();
2985 // c:1312-1317 — break on kshchar trigger (any of the six
2986 // ZPC_KSH_* slots whose literal byte is followed by `(`).
2987 // Snapshot the zpc_special slots that participate; when
2988 // KSHGLOB is off they're Marker (0xa2) and won't match
2989 // ordinary bytes.
2990 let (ksh_plus, ksh_bang, ksh_bang2, ksh_at, ksh_star, ksh_quest) = {
2991 let sp = zpc_special.lock().unwrap();
2992 (
2993 sp[ZPC_KSH_PLUS as usize],
2994 sp[ZPC_KSH_BANG as usize],
2995 sp[ZPC_KSH_BANG2 as usize],
2996 sp[ZPC_KSH_AT as usize],
2997 sp[ZPC_KSH_STAR as usize],
2998 sp[ZPC_KSH_QUEST as usize],
2999 )
3000 };
3001 // c:1314-1317 — `~` is a literal-run terminator IFF it's
3002 // active as the exclusion operator (EXTENDEDGLOB on) AND
3003 // the lookahead is `/` or a non-segment-special byte.
3004 // Without EXTENDEDGLOB the byte is Marker (0xa2) so the
3005 // equality check fails naturally. Limited to TILDE only —
3006 // the other ZPC_SEG_COUNT slots (SLASH, NULL, BAR, OUTPAR)
3007 // are already covered by the explicit stop list above
3008 // (`|` `)` handled there; `/` was deliberately NOT a
3009 // literal-run break in this Rust port, see
3010 // `zsh_corpus_hash_s_e_anchors_match_bare_test` which
3011 // expects `/` to stay literal inside `(...)` alternation).
3012 let (sp_tilde_lit, sp_seg_lit_set, sp_hat_lit, sp_hash_lit, sp_slash_lit) = {
3013 let sp = zpc_special.lock().unwrap();
3014 let mut set = [false; 256];
3015 for i in 0..(ZPC_SEG_COUNT as usize) {
3016 set[sp[i] as usize] = true;
3017 }
3018 (
3019 sp[ZPC_TILDE as usize],
3020 set,
3021 sp[ZPC_HAT as usize],
3022 sp[ZPC_HASH as usize],
3023 sp[ZPC_SLASH as usize],
3024 )
3025 };
3026 while local_off < p.len() {
3027 let b = p.as_bytes()[local_off];
3028 // c:Src/pattern.c:480-483 — `if (!isset(EXTENDEDGLOB))`
3029 // masks ZPC_HAT and ZPC_HASH to Marker so the
3030 // patcompiece dispatch treats `^` / `#` as literals
3031 // (no negation / no zero-or-more closure).
3032 // The literal-run break list hardcoded `b'^'` /
3033 // `b'#'` though, which still terminated the literal
3034 // accumulator and forced patcompiece's empty-buf
3035 // -1 return → "bad pattern: ^.*" for the unset
3036 // EXTENDEDGLOB case. Gate the break on the actual
3037 // zpc_special slot so an EXTENDEDGLOB-off run treats
3038 // `^` and `#` as ordinary chars. Bug #421.
3039 // c:1294-1319 — the literal-run loop is WRAPPED in
3040 // if (zpc_special[ZPC_INPAR] != Marker ||
3041 // *patparse != Outpar || paren) { ...break... }
3042 // "If '(' is disabled as a pattern char, allow ')' as
3043 // an ordinary string character if there are no
3044 // parentheses to close. Don't allow it otherwise, it
3045 // changes the syntax." So with `(` disabled (SHGLOB /
3046 // `disable -p '('`) at top level, a `)` stays in the
3047 // run; inside a group (`paren`) it still closes it.
3048 // `(` itself is likewise only a run terminator while
3049 // its slot is live (c:1312-1313 memchr over
3050 // zpc_special, which holds Marker when disabled).
3051 let stop_here = match b {
3052 b'(' => inpar_active,
3053 b')' => inpar_active || paren != 0,
3054 // c:1312-1313 — a disabled `?` / `*` / `[` is not in
3055 // zpc_special, so it stays in the literal run.
3056 b'?' => quest_active,
3057 b'*' => star_active,
3058 b'[' => inbrack_active,
3059 b'|' | b'\\' | b'<' => true,
3060 b'^' => sp_hat_lit == b'^',
3061 b'#' => sp_hash_lit == b'#',
3062 // c:950 / c:1312-1317 — `/` is ZPC_SLASH, a segment
3063 // terminator, and C keeps it one by MASKING the slot
3064 // (`zpc_special[ZPC_SLASH] = Marker`) where it must stay
3065 // literal — for a non-FILE pattern (c:570) and inside
3066 // parens (c:840-842) — rather than by dropping the rule.
3067 // Mirror that with the same gate `patcompbranch` already
3068 // uses above, so `(...)` alternation and `[[ ]]`/`:#`
3069 // patterns are untouched. Without this a literal file
3070 // component swallowed the rest of the path: the general
3071 // compiler is only reached for such a component when the
3072 // PURES fast path is off, i.e. under `(#i)`/`(#a1)`, so
3073 // `(#i)sub/deep.txt` compiled `sub/deep.txt` as ONE
3074 // component and matched nothing — even with exact case.
3075 // c:949-952 — see patcompbranch: liveness of the segment
3076 // break comes from the MASKED SLOT, never from `paren`.
3077 b'/' => sp_slash_lit == b'/',
3078 _ => false,
3079 };
3080 if stop_here {
3081 break;
3082 }
3083 if b == sp_tilde_lit && sp_tilde_lit != 0 {
3084 let la = p.as_bytes().get(local_off + 1).copied().unwrap_or(0);
3085 // Literal-tilde exception: `~` followed by a
3086 // segment-special OTHER than `/` stays in the run.
3087 let literal_tilde_exception = la != b'/' && sp_seg_lit_set[la as usize];
3088 if !literal_tilde_exception {
3089 break;
3090 }
3091 }
3092 // c:1278-1292 — ksh-glob trigger lookahead. If the next
3093 // byte is `(` AND this byte matches one of the six
3094 // ZPC_KSH_* slots, stop the literal run BEFORE this byte
3095 // so the next patcomppiece call consumes it as a ksh
3096 // quantifier prefix (e.g. `pre+(x)post` accumulates
3097 // `pre` then breaks at `+` since `+(` follows).
3098 if local_off + 1 < p.len() && p.as_bytes()[local_off + 1] == b'(' {
3099 if b == ksh_plus
3100 || b == ksh_bang
3101 || b == ksh_bang2
3102 || b == ksh_at
3103 || b == ksh_star
3104 || b == ksh_quest
3105 {
3106 break;
3107 }
3108 }
3109 buf.push(b);
3110 local_off += 1;
3111 }
3112 drop(p);
3113 if buf.is_empty() {
3114 return -1;
3115 }
3116 // c:Src/pattern.c:1340-1351 — multi-char run with TRAILING `#`
3117 // (or `(#cN,M)`) must backtrack ONE char so the trailing
3118 // quantifier applies to the LAST char only. Without this,
3119 // `fo#` compiles as `(fo)#` instead of `f` + `o#`, breaking
3120 // `(fo#)#` against "ffo" (the test pin we are fixing).
3121 //
3122 // C body:
3123 // if ((*patparse == zpc_special[ZPC_HASH] || ...) && morelen)
3124 // patparse = patprev;
3125 //
3126 // morelen = "more than one char in the literal run". The
3127 // backtrack pops the LAST byte from buf and rewinds patparse
3128 // by 1 so the next patcomppiece call sees that byte as its
3129 // own atom.
3130 // c:1341 — "If we have more than one character, a following hash
3131 // or (#c...) only applies to the last, so backtrack one character."
3132 // C tests THREE shapes, not just the bare `#` (c:1342-1350):
3133 //
3134 // if ((*patparse == zpc_special[ZPC_HASH] ||
3135 // (*patparse == zpc_special[ZPC_INPAR] &&
3136 // patparse[1] == zpc_special[ZPC_HASH] &&
3137 // patparse[2] == 'c') ||
3138 // (*patparse == zpc_special[ZPC_KSH_AT] &&
3139 // patparse[1] == Inpar &&
3140 // patparse[2] == zpc_special[ZPC_HASH] &&
3141 // patparse[3] == 'c')) && morelen)
3142 // patparse = patprev;
3143 //
3144 // Only the first arm was ported, so `FPATH+(#c0,1)=` compiled the
3145 // count over the whole literal run `FPATH+` instead of over the
3146 // trailing `+` alone, and `FPATH=` never matched.
3147 let has_trailing_quantifier = {
3148 let sp = zpc_special.lock().unwrap();
3149 let sp_hash = sp[ZPC_HASH as usize];
3150 let sp_inpar = sp[ZPC_INPAR as usize];
3151 let sp_ksh_at = sp[ZPC_KSH_AT as usize];
3152 drop(sp);
3153 let p = patparse.lock().unwrap();
3154 let b = p.as_bytes();
3155 let at = |i: usize| -> u8 { b.get(local_off + i).copied().unwrap_or(0) };
3156 // c:1342 — bare `#` / `##` closure.
3157 let hash_at = sp_hash == b'#' && at(0) == b'#';
3158 // c:1343-1345 — `(#c...)` count spec.
3159 let count_at =
3160 sp_hash == b'#' && at(0) == sp_inpar && at(1) == b'#' && at(2) == b'c';
3161 // c:1346-1349 — ksh `@(#c...)` form. `Inpar` is the tokenized
3162 // `(`; this port keeps pattern bytes raw (see patcompcharsset
3163 // at pattern.rs:443), so the literal `(` stands in for it.
3164 let ksh_count_at = sp_hash == b'#'
3165 && at(0) == sp_ksh_at
3166 && at(1) == b'('
3167 && at(2) == b'#'
3168 && at(3) == b'c';
3169 drop(p);
3170 hash_at || count_at || ksh_count_at
3171 };
3172 // c:1322 — `patprev = patparse; METACHARINC(patparse);`, so
3173 // `patprev` is the START of the LAST character of the run, which
3174 // may be multibyte. `last_char_start` is that boundary: walk back
3175 // over UTF-8 continuation bytes, exactly what METACHARINC's
3176 // stride implies.
3177 let last_char_start = {
3178 let mut cut = buf.len() - 1;
3179 while cut > 0 && (buf[cut] & 0xC0) == 0x80 {
3180 cut -= 1;
3181 }
3182 cut
3183 };
3184 // c:1338 — `morelen = (patprev > str0);`. This is "the run holds
3185 // MORE THAN ONE CHARACTER", not more than one BYTE. The previous
3186 // port tested `buf.len() > 1`, so a run consisting of a SINGLE
3187 // multibyte character followed by `#` (`é#` under EXTENDEDGLOB)
3188 // took the backtrack branch, popped the whole 2-byte character,
3189 // rewound `local_off` to where the piece started, and returned a
3190 // zero-length P_EXACTLY having consumed NOTHING — so
3191 // `patcompbranch`'s piece loop (pattern.rs:1406) span forever
3192 // emitting empty nodes. C cannot reach that state: c:1336's
3193 // `DPUTS(patparse == str0, "BUG: matched nothing in
3194 // patcomppiece.")` records that a piece always consumes, and
3195 // `morelen` being character-based is what guarantees it — the
3196 // backtrack can only ever give back a character that was NOT the
3197 // first one in the run.
3198 let morelen = last_char_start > 0; // c:1338
3199 // c:1340-1351 — "If we have more than one character, a following
3200 // hash or (#c...) only applies to the last, so backtrack one
3201 // character." `if (… ) && morelen) patparse = patprev;`
3202 if morelen && has_trailing_quantifier {
3203 let popped = buf.len() - last_char_start;
3204 buf.truncate(last_char_start);
3205 local_off -= popped;
3206 }
3207 patparse_off.store(local_off, Ordering::Relaxed);
3208 // c:1352-1357 — "If len is 1, we can't have an active # following,
3209 // so doesn't matter that we don't make X in `XX#' simple."
3210 // if (!morelen) flags |= P_SIMPLE;
3211 // Only a ONE-character run can be the operand of a following
3212 // closure; a multi-character run stays non-simple (and pure
3213 // string). This port used to set P_SIMPLE unconditionally.
3214 if !morelen {
3215 *flagp |= P_SIMPLE; // c:1357
3216 }
3217 // c:1372-1376 —
3218 // if ((patglobflags & GF_MULTIBYTE) && slen > 1)
3219 // /* for multibyte single characters, treat x# as (x)# */
3220 // flags &= ~P_SIMPLE;
3221 // `slen` is the run's UNMETAFIED byte length (c:1371), which for
3222 // this port is simply `buf.len()`. The P_SIMPLE closure path
3223 // (c:1718-1720 `patinsert(op, starter, NULL, 0)`, matched by
3224 // `patrepeat` at c:4121-4129, which asserts `P_LS_LEN(p) == 1`)
3225 // assumes a ONE-BYTE operand, so a single multibyte character
3226 // has to go through the general `(x)#` branch construction
3227 // instead.
3228 if (patglobflags.load(Ordering::Relaxed) & GF_MULTIBYTE) != 0 && buf.len() > 1 {
3229 *flagp &= !P_SIMPLE; // c:1375
3230 }
3231 let lit_off = patnode(P_EXACTLY);
3232 let mut buf_lit = patout.lock().unwrap();
3233 let len = buf.len() as u32;
3234 buf_lit.extend_from_slice(&len.to_le_bytes());
3235 buf_lit.extend_from_slice(&buf);
3236 *tail_out = lit_off;
3237 lit_off as i64
3238 }
3239 };
3240
3241 if atom < 0 {
3242 return atom;
3243 }
3244
3245 // c:1606-1644 — quantifier dispatch. Three sources of quantifier:
3246 // (a) trailing `#` / `##` (zsh-extended hash repetition)
3247 // (b) trailing `(#cN,M)` count-spec (handled in patcompbranch, not here)
3248 // (c) kshchar form `+/*/?` already at the atom's leading byte.
3249 //
3250 // Rule c:1615 — if no hash AND no count AND kshchar is one of
3251 // (none, '@', '!'), no quantifier; return atom as-is. `@` is the
3252 // identity quantifier (match group exactly once); `!` already had
3253 // its negation compiled by patcompnot inside the atom.
3254 let q_off = patparse_off.load(Ordering::Relaxed);
3255 let parse2 = patparse.lock().unwrap();
3256 // c:1611-1620 — `if (*patparse == zpc_special[ZPC_HASH])` quantifier
3257 // dispatch. The C source reads `zpc_special[ZPC_HASH]`, which
3258 // patcompcharsset (c:480-483) sets to `'#'` only when
3259 // EXTENDEDGLOB is on, else to the Marker byte (c:476). A literal
3260 // '#' in the source pattern therefore does NOT trigger the
3261 // quantifier path when EXTENDEDGLOB is off — the comparison
3262 // `*patparse == Marker` fails. Previously this Rust check used
3263 // a bare `b'#'` comparison instead of consulting zpc_special,
3264 // making `(x)#` and `[a-z]##[0-9]##` match even with
3265 // extendedglob OFF (parity bugs #5/#6 vs real zsh).
3266 let hash_char = zpc_special.lock().unwrap()[ZPC_HASH as usize]; // c:1612
3267 let has_hash = hash_char == b'#' && q_off < parse2.len() && parse2.as_bytes()[q_off] == b'#'; // c:1611-1614
3268 drop(parse2);
3269 if !has_hash && (kshchar == 0 || kshchar == b'@' || kshchar == b'!') {
3270 return atom; // c:1616-1617
3271 }
3272
3273 // c:1626-1627 — `if (kshchar && (hash || count)) return 0;`
3274 // /* too much at once doesn't currently work */
3275 //
3276 // `case Star:` (c:1440) sets `kshchar = -1` — per C's own comment there,
3277 // "kshchar is used as a sign that we can't have #'s" — so a closure may
3278 // not follow a `*`. This port's `kshchar` is a u8 trigger byte and cannot
3279 // hold C's sentinel, so the atom's head opcode expresses it instead: a
3280 // Star atom is a P_STAR node (c:1441).
3281 //
3282 // The `count` half of this same C condition WAS ported, in patcompbranch
3283 // (pattern.rs:1587-1600, which rejects `*(#c2,3)`); only the `hash` half
3284 // was missing, so the two arms of one C test disagreed. Measured, `zsh -f`
3285 // 5.9 vs this port: `*#`, `*##` and `a*#b` are `bad pattern` in zsh and
3286 // were all accepted here, while `*(#c2,3)` was already rejected by both.
3287 let atom_is_star = {
3288 let buf = patout.lock().unwrap();
3289 buf.get(atom as usize + I_OP).copied().unwrap_or(0) == P_STAR // c:1440-1441
3290 };
3291 if (kshchar != 0 || atom_is_star) && has_hash {
3292 return -1; // c:1627 `return 0;`
3293 }
3294
3295 // c:1710 / c:1723 — `flags`, the ATOM's own flag word. C keeps it in a
3296 // local for the whole of patcomppiece and only publishes it through
3297 // `*flagp` at c:1621 (the no-quantifier return); the dispatch below
3298 // OVERWRITES `*flagp` with P_HSTART (c:1631-1648) before either
3299 // optimisation reads its operand's flags. This port merged the two into
3300 // `*flagp`, so `*flagp & P_SIMPLE` was necessarily 0 by the time the
3301 // c:1710 and c:1723 tests ran and BOTH simple paths were dead: every
3302 // `x#` compiled to the c:1730 P_WBRANCH form instead.
3303 //
3304 // That is what kept the leading-dot rule off closures. C's `?#` becomes
3305 // a bare P_STAR (c:1721) and `[a]#` a P_ONEHASH whose operand is the
3306 // P_ANYOF (c:1725) — both P_NOTDOT-visible, at c:2731 and c:3322
3307 // respectively. A P_WBRANCH (0x21) is neither. Measured with `.hidden`
3308 // present and GLOB_DOTS off, `zsh -f` 5.9 lists nothing for `?#.hidden`
3309 // and `[a]#.hidden`; this port listed `.hidden` for both.
3310 let flags = *flagp; // c:1710 `flags`
3311
3312 // c:1624-1644 — pick the operator + post-atom flags.
3313 let op: u8;
3314 let mut consume_hashes = 0;
3315 if kshchar == b'*' {
3316 // c:1624-1626
3317 op = P_ONEHASH;
3318 *flagp = P_HSTART;
3319 } else if kshchar == b'+' {
3320 // c:1627-1629
3321 op = P_TWOHASH;
3322 *flagp = P_HSTART;
3323 } else if kshchar == b'?' {
3324 // c:1630-1632
3325 op = 0; // sentinel — `?` desugars to (x|) via the BRANCH path below
3326 *flagp = 0;
3327 } else {
3328 // c:1637-1644 — `#` / `##`.
3329 let parse_h = patparse.lock().unwrap();
3330 let two = q_off + 1 < parse_h.len() && parse_h.as_bytes()[q_off + 1] == b'#';
3331 drop(parse_h);
3332 if two {
3333 op = P_TWOHASH;
3334 consume_hashes = 2;
3335 } else {
3336 op = P_ONEHASH;
3337 consume_hashes = 1;
3338 }
3339 *flagp = P_HSTART;
3340 }
3341 if consume_hashes > 0 {
3342 patparse_off.fetch_add(consume_hashes, Ordering::Relaxed);
3343 }
3344
3345 // c:1705-1746 — quantifier emission.
3346 //
3347 // Read the atom's opcode so c:1705 (`?#` → `*`) optimization can
3348 // apply. The atom byte sits at `atom + I_OP` in patout.
3349 let atom_op = {
3350 let buf = patout.lock().unwrap();
3351 if (atom as usize) + I_OP < buf.len() {
3352 buf[atom as usize + I_OP]
3353 } else {
3354 0
3355 }
3356 };
3357
3358 if ((flags & P_SIMPLE) != 0) && (op == P_ONEHASH || op == P_TWOHASH) && atom_op == P_ANY {
3359 // c:1705-1717 — `?#` becomes `*`; `?##` becomes `?*`. The atom
3360 // is P_ANY at offset `atom`; rewrite or pad as needed.
3361 let mut buf = patout.lock().unwrap();
3362 if op == P_TWOHASH {
3363 // c:1717-1719 — `/* ?## becomes ?* */`; the C keeps the atom
3364 // as P_ANY (`uptr->l = (uptr->l & ~0xff) | P_ANY;` is a no-op
3365 // on an already-P_ANY node) and then runs
3366 // `pattail(starter, patnode(P_STAR));`. The `pattail` is the
3367 // load-bearing half: it writes the new P_STAR into the P_ANY's
3368 // next slot, so the two nodes form the `?*` chain. Emitting the
3369 // P_STAR without chaining it leaves it unreachable and makes
3370 // `?##` match exactly one character.
3371 drop(buf);
3372 let star = patnode(P_STAR);
3373 pattail(atom as usize, star); // c:1719
3374 // The piece's chain-tail is now the P_STAR, not the P_ANY:
3375 // whatever patcompbranch splices in next belongs after the
3376 // star (C reaches the same node because `pattail` walks the
3377 // chain from the returned `starter`).
3378 *tail_out = star;
3379 } else {
3380 // c:1715-1716 — `?#` → `*`: just rewrite atom's opcode.
3381 buf[atom as usize + I_OP] = P_STAR;
3382 drop(buf);
3383 *tail_out = atom as usize;
3384 }
3385 *flagp &= !P_PURESTR;
3386 } else if (flags & P_SIMPLE) != 0
3387 && op != 0
3388 && (patglobflags.load(Ordering::Relaxed) & 0xff) == 0
3389 {
3390 // c:1718-1720 — simple operand, no approximate-match counter:
3391 // emit `patinsert(op, starter, NULL, 0)`. The matcher walks
3392 // the inserted P_ONEHASH/P_TWOHASH operand at scan+I_BODY.
3393 patinsert(op, atom as usize, None, 0);
3394 *flagp &= !P_PURESTR;
3395 *tail_out = atom as usize;
3396 } else if op == P_ONEHASH {
3397 // c:1721-1729 — emit x# as (x&|): P_WBRANCH with NULL payload
3398 // ahead of atom; loop via P_BACK; null alternative branch.
3399 // patinsert with `sz=size_of::<union upat>()=8` (we use 8 to
3400 // mirror the C `union upat` slot, then zero-pad it).
3401 let payload = [0u8; 8];
3402 patinsert(P_WBRANCH, atom as usize, Some(&payload), 8);
3403 // Either x — atom is now at atom+I_BODY+8.
3404 let back = patnode(P_BACK);
3405 patoptail(atom as usize, back); // c:1726 — loop back
3406 patoptail(atom as usize, atom as usize); // c:1727
3407 let alt = patnode(P_BRANCH);
3408 pattail(atom as usize, alt); // c:1728 — or
3409 let null_node = patnode(P_NOTHING);
3410 pattail(atom as usize, null_node); // c:1729 — null
3411 *flagp &= !P_PURESTR;
3412 // The piece's chain-tail is the trailing P_NOTHING node — its
3413 // `.next` slot is empty and is the correct splice point for
3414 // whatever piece patcompbranch chains in next.
3415 *tail_out = null_node;
3416 } else if op == P_TWOHASH {
3417 // c:1730-1738 — emit x## as x(&|): P_WBRANCH after atom; loop
3418 // back; null branch.
3419 let wbranch = patnode(P_WBRANCH); // c:1732 — either
3420 let payload = [0u8; 8]; // c:1733-1734 patadd((char *)&up, ..., sizeof(up), 0)
3421 {
3422 let mut buf = patout.lock().unwrap();
3423 buf.extend_from_slice(&payload);
3424 }
3425 pattail(atom as usize, wbranch); // c:1735
3426 let back = patnode(P_BACK);
3427 pattail(back, atom as usize); // c:1736 — loop back
3428 let alt = patnode(P_BRANCH);
3429 pattail(wbranch, alt); // c:1737 — or
3430 let null_node = patnode(P_NOTHING);
3431 pattail(atom as usize, null_node); // c:1738 — null
3432 *flagp &= !P_PURESTR;
3433 // Same as ONEHASH path — tail at the trailing P_NOTHING.
3434 *tail_out = null_node;
3435 } else if kshchar == b'?' {
3436 // c:1739-1746 — emit ?(x) as (x|).
3437 patinsert(P_BRANCH, atom as usize, None, 0); // c:1741 — either x
3438 let alt = patnode(P_BRANCH); // c:1742 — or
3439 pattail(atom as usize, alt);
3440 let null_node = patnode(P_NOTHING); // c:1743 — null
3441 pattail(atom as usize, null_node); // c:1744
3442 patoptail(atom as usize, null_node); // c:1745
3443 *flagp &= !P_PURESTR;
3444 // Same as ONEHASH/TWOHASH — tail at the trailing P_NOTHING.
3445 *tail_out = null_node;
3446 }
3447
3448 // c:1747-1748 — a stray `#` immediately after = compile error.
3449 {
3450 let p = patparse.lock().unwrap();
3451 let q2 = patparse_off.load(Ordering::Relaxed);
3452 if q2 < p.len() && p.as_bytes()[q2] == b'#' {
3453 // Only flag as error when EXTENDEDGLOB-style # is enabled.
3454 let sp = zpc_special.lock().unwrap();
3455 if sp[ZPC_HASH as usize] == b'#' {
3456 return -1;
3457 }
3458 }
3459 }
3460
3461 atom
3462}
3463
3464/// Port of `patcompnot()` from `Src/pattern.c:1760`.
3465/// C: `patcompnot(int paren, int *flagsp)`.
3466///
3467/// C: `static long patcompnot(int paren, int *flagsp)`. Implements
3468/// the `^pat` (paren=0) or `!(pat)` (paren=1) extended/ksh-glob
3469/// negation by emitting `P_BRANCH → P_STAR → P_EXCSYNC` followed by
3470/// `P_EXCLUDE [payload]` and the asserted pattern terminated by
3471/// `P_EXCEND`, all joined at a trailing `P_NOTHING`.
3472///
3473/// NOTE: matcher support for `P_EXCSYNC`/`P_EXCEND`/`P_EXCLUDE` is
3474/// only partial in the Rust port — compile is faithful per
3475/// pattern.c:1760-1784 but match-time negation semantics may diverge
3476/// from C for complex backtracking edge cases. Tests covering
3477/// `!(foo)` / `!(foo|bar)` exercise the basic working subset.
3478pub fn patcompnot(paren: i32, flagsp: &mut i32) -> i64 {
3479 // c:1760
3480 // c:1767 — `*flagsp = P_HSTART;`. Negation always starts with `*`
3481 // semantically so the caller knows the piece can match at the
3482 // start of input.
3483 *flagsp = P_HSTART;
3484
3485 // c:1769 — `starter = patnode(P_BRANCH);`
3486 let starter = patnode(P_BRANCH);
3487 // c:1770 — `br = patnode(P_STAR);`
3488 let br = patnode(P_STAR);
3489 // c:1771 — `excsync = patnode(P_EXCSYNC);`
3490 let excsync = patnode(P_EXCSYNC);
3491 // c:1772 — `pattail(br, excsync);`
3492 pattail(br, excsync);
3493 // c:1773 — `pattail(starter, excl = patnode(P_EXCLUDE));`
3494 let excl = patnode(P_EXCLUDE);
3495 pattail(starter, excl);
3496 // c:1774-1775 — `up.p = NULL; patadd((char *)&up, 0, sizeof(up), 0);`
3497 // The EXCLUDE node carries an 8-byte syncptr payload, NULL-init.
3498 {
3499 let mut buf = patout.lock().unwrap();
3500 buf.extend_from_slice(&[0u8; 8]);
3501 }
3502 // c:1776 — `br = (paren ? patcompswitch(1, &dummy) : patcompbranch(&dummy, 0));`
3503 let mut dummy: i32 = 0;
3504 let inner = if paren != 0 {
3505 // c:1776 `patcompswitch(1, ...)` — in C the switch itself opens
3506 // the group: a P_OPEN when `(#b)` is live (c:775-783) and the
3507 // closing `ender` every branch is hooked to (c:897-909). The Rust
3508 // patcompswitch leaves both to its caller (see the `b'('` arm of
3509 // patcomppiece), so emit them here the same way. Without the
3510 // ender each branch's operand chain ended in next == 0, which
3511 // patmatch reads as "restart the program": a negated group whose
3512 // last piece was `*` re-matched the WHOLE pattern from the split
3513 // point, so `[[ xmc = x!(m*) ]]` and globtests.ksh's
3514 // `[[ mad.moo.cow = !(*.*).!(*.*) ]]` matched.
3515 let savglobflags = patglobflags.load(Ordering::Relaxed); // c:770
3516 let parno = if (savglobflags & GF_BACKREF) != 0
3517 && patnpar.load(Ordering::Relaxed) <= NSUBEXP as i32
3518 {
3519 patnpar.fetch_add(1, Ordering::Relaxed) // c:782 `parno = patnpar++;`
3520 } else {
3521 0
3522 };
3523 let open_off = if parno != 0 {
3524 Some(patnode(P_OPEN + parno as u8)) // c:783
3525 } else {
3526 None
3527 };
3528 let r = patcompswitch(1, &mut dummy);
3529 if r < 0 {
3530 return -1;
3531 }
3532 // c:913 — `if ((paren && *patparse++ != Outpar) ...) return 0;`
3533 {
3534 let cur = patparse_off.load(Ordering::Relaxed);
3535 let p = patparse.lock().unwrap();
3536 if cur >= p.len() || p.as_bytes()[cur] != b')' {
3537 return -1;
3538 }
3539 drop(p);
3540 patparse_off.fetch_add(1, Ordering::Relaxed);
3541 }
3542 // c:789-792 — `if (starter) pattail(starter, br); else starter = br;`
3543 let starter_sw = match open_off {
3544 Some(o) => {
3545 set_next(o, r as usize);
3546 o
3547 }
3548 None => r as usize,
3549 };
3550 // c:902 — `ender = patnode(paren ? parno ? P_CLOSE+parno : P_NOTHING : P_END);`
3551 let ender = patnode(if parno != 0 {
3552 P_CLOSE + parno as u8
3553 } else {
3554 P_NOTHING
3555 });
3556 // c:903 — `pattail(starter, ender);`
3557 pattail(starter_sw, ender);
3558 // c:909-911 — hook the tail of every branch to the closing node.
3559 chain_branches_to(r as usize, ender);
3560 // c:919-929 — `if (paren && gfchanged) { pattail(ender,
3561 // patnode(P_GFLAGS)); patglobflags = savglobflags; ... }`, with the
3562 // same honored-bits mask the `b'('` arm stores.
3563 if PATSWITCH_GFCHANGED.load(Ordering::Relaxed) != 0 {
3564 let relevant = GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE;
3565 let gf_off = patnode(P_GFLAGS);
3566 {
3567 let mut buf = patout.lock().unwrap();
3568 buf.extend_from_slice(&(savglobflags & relevant).to_le_bytes());
3569 }
3570 pattail(ender, gf_off);
3571 patglobflags.store(savglobflags, Ordering::Relaxed); // c:927
3572 }
3573 starter_sw as i64
3574 } else {
3575 patcompbranch(&mut dummy, 0)
3576 };
3577 if inner < 0 {
3578 return -1; // c:1777 `return 0;` (Rust uses -1 for failure)
3579 }
3580 // c:1778 — `pattail(br, patnode(P_EXCEND));`
3581 let excend = patnode(P_EXCEND);
3582 pattail(inner as usize, excend);
3583 // c:1779 — `n = patnode(P_NOTHING);`
3584 let n = patnode(P_NOTHING);
3585 // c:1780 — `pattail(excsync, n);`
3586 pattail(excsync, n);
3587 // c:1781 — `pattail(excl, n);`
3588 pattail(excl, n);
3589 // c:1783
3590 let _ = br; // suppress unused-var (kept for c-name parity)
3591 starter as i64
3592}
3593
3594/// Port of `patnode()` from `Src/pattern.c:1790`.
3595/// C: `patnode(long op)`.
3596///
3597/// C: `static long patnode(long op)` — writes a 1-byte opcode plus a
3598/// 4-byte zeroed next-offset. Returns the offset of the opcode byte.
3599fn patnode(op: u8) -> usize {
3600 // c:1790
3601 let mut buf = patout.lock().unwrap();
3602 let off = buf.len();
3603 buf.push(op); // I_OP
3604 buf.extend_from_slice(&[0, 0, 0, 0]); // I_NEXT zeroed
3605 off
3606}
3607
3608/// Port of `patinsert()` from `Src/pattern.c:1807`.
3609/// C: `patinsert(long op, int opnd, char *xtra, int sz)`.
3610///
3611/// C: `static void patinsert(long op, int opnd, char *xtra, int sz)`.
3612/// Inserts an opcode (+ next slot) at position `opnd`, shifting bytes
3613/// after it down by `5 + sz`, then writes `xtra` payload of `sz` bytes.
3614fn patinsert(op: u8, opnd: usize, xtra: Option<&[u8]>, sz: usize) {
3615 // c:1807
3616 let mut buf = patout.lock().unwrap();
3617 let header_sz = 1 + 4; // op + next
3618 let total = header_sz + sz;
3619 // Insert `total` zeroed bytes at opnd, then overwrite.
3620 let mut inserted = vec![0u8; total];
3621 inserted[0] = op;
3622 if let Some(x) = xtra {
3623 let copy_n = x.len().min(sz);
3624 inserted[header_sz..header_sz + copy_n].copy_from_slice(&x[..copy_n]);
3625 }
3626 buf.splice(opnd..opnd, inserted);
3627 // Patch up next_off chains pointing past opnd by adding `total`.
3628 fixup_offsets_after_insert(&mut buf, opnd, total as u32);
3629}
3630
3631/// Port of `pattail()` from `Src/pattern.c:1834`.
3632/// C: `pattail(long p, long val)`.
3633///
3634/// C: `static void pattail(long p, long val)` — patches the next-offset
3635/// field of the opcode at offset `p` to point to `val`. Walks any
3636/// existing chain to the end before patching.
3637fn pattail(p: usize, val: usize) {
3638 // c:1834
3639 let mut buf = patout.lock().unwrap();
3640 let mut cur = p;
3641 loop {
3642 if cur + I_BODY > buf.len() {
3643 return;
3644 }
3645 let next_bytes: [u8; 4] = buf[cur + I_NEXT..cur + I_NEXT + 4].try_into().unwrap();
3646 let next = u32::from_le_bytes(next_bytes) as usize;
3647 if next == 0 {
3648 break;
3649 }
3650 cur = next;
3651 }
3652 let val_bytes = (val as u32).to_le_bytes();
3653 if cur + I_NEXT + 4 <= buf.len() {
3654 buf[cur + I_NEXT..cur + I_NEXT + 4].copy_from_slice(&val_bytes);
3655 }
3656}
3657
3658/// Port of `patoptail()` from `Src/pattern.c:1856`.
3659/// C: `patoptail(long p, long val)`.
3660///
3661/// C: `static void patoptail(long p, long val)` — like pattail but
3662/// only patches branches (P_BRANCH/P_WBRANCH).
3663///
3664/// C: c:1862-1865 — for P_BRANCH the operand sits at `P_OPERAND(p)` =
3665/// `p+1` (Upat slot, i.e. byte offset `p + I_BODY` in Rust); for
3666/// P_WBRANCH the operand sits at `P_OPERAND(p) + 1` (skipping the
3667/// 8-byte syncptr payload Upat slot), i.e. byte offset
3668/// `p + I_BODY + 8` in Rust.
3669fn patoptail(p: usize, val: usize) {
3670 // c:1856
3671 let buf = patout.lock().unwrap();
3672 if p + I_OP >= buf.len() {
3673 return;
3674 }
3675 let op = buf[p + I_OP];
3676 drop(buf);
3677 if P_ISBRANCH(op) {
3678 // c:1862-1865 — operand offset depends on op kind.
3679 if op == P_BRANCH {
3680 pattail(p + I_BODY, val);
3681 } else {
3682 // P_WBRANCH / P_EXCLUDE / P_EXCLUDP — operand sits after
3683 // the 8-byte syncptr payload.
3684 pattail(p + I_BODY + 8, val);
3685 }
3686 }
3687}
3688
3689// =====================================================================
3690// 13/14. Matcher — pattern.c:2223-3579
3691// =====================================================================
3692
3693/// State accumulated during a single `patmatch` walk. C uses
3694/// per-thread globals (`patbeginp[]` / `patendp[]` for captures);
3695/// the Rust port encapsulates them in this struct passed by `&mut`.
3696/// Rule D: this struct represents matcher-internal scratch state
3697/// (analogous to `struct rpat pattrystate` at pattern.c:248), not a
3698/// bag-of-globals from unrelated subsystems.
3699///
3700/// **C counterpart**: `struct rpat` at `pattern.c:248`.
3701#[derive(Clone)]
3702#[allow(non_camel_case_types)]
3703pub struct rpat {
3704 /// Per-P_WBRANCH visit bitmap, keyed by WBRANCH-opcode offset.
3705 /// Mirrors C's `Upat ptrp` (`pattern.c:3217`): every WBRANCH carries
3706 /// an 8-byte payload sized as `union upat` — initialised to NULL,
3707 /// then lazily filled by `patmatch` with a buffer the size of the
3708 /// input string. Each byte tracks "have we already tried this
3709 /// WBRANCH at this input position with at most this many errors?".
3710 /// On revisit at the same position with the same-or-fewer errors,
3711 /// C returns 0 to bound the recursion (`pattern.c:3245-3248`). The
3712 /// Rust port keeps the bitmap on rpat (per-pattry call) instead of
3713 /// inside the bytecode payload so the bytecode stays read-only —
3714 /// the key is the WBRANCH offset; the value is a Vec<u8> the size
3715 /// of the input. Without it, `(fo#)#` against ANY input that
3716 /// requires the closure to consume at least one char per iteration
3717 /// (like "ffo") burns through PATMATCH_MAX_DEPTH and aborts.
3718 ///
3719 /// SHARED, not copied, across `rpat` clones. C hangs the buffer off
3720 /// the bytecode operand (`ptrp->p`, c:3226-3233) and frees it once on
3721 /// unwind (c:3253-3257), so every backtracking branch of one `pattry`
3722 /// sees the SAME marks — that persistence is the whole point of the
3723 /// guard: a branch already tried at this input position with no fewer
3724 /// errors must not be re-explored.
3725 ///
3726 /// Owning it by value made `#[derive(Clone)]` deep-copy a
3727 /// `|input|+1`-byte Vec per WBRANCH at all 12 `state.clone()` sites,
3728 /// one of which (c:6379 here) runs per branch BEFORE the skip check.
3729 /// Profiling `i<TAB>` put 58% of non-idle CPU in the resulting
3730 /// `_platform_memmove` + `madvise`. It also silently WEAKENED the
3731 /// guard: marks made inside a failed branch were thrown away with the
3732 /// clone, so the same branch could be re-explored anyway.
3733 pub wbranch_visits: std::rc::Rc<std::cell::RefCell<std::collections::HashMap<usize, Vec<u8>>>>,
3734 pub patbeginp: [usize; NSUBEXP], // c:241 capture starts (byte offsets)
3735 pub patendp: [usize; NSUBEXP], // c:242 capture ends
3736 /// `parsfound` from `Src/pattern.c` (per c:2957/c:2989 references).
3737 /// Two-stripe bitmap: bits `0..NSUBEXP` track per-group P_OPEN
3738 /// first-write (`patbeginp[i]` committed); bits `NSUBEXP..2*NSUBEXP`
3739 /// track per-group P_CLOSE first-write (`patendp[i]` committed).
3740 /// Bit `n-1` (open) = `1 << (n-1)`; bit `n-1+NSUBEXP` (close) =
3741 /// `1 << (n-1+NSUBEXP)`. Width u32 to fit `2*NSUBEXP = 18` bits.
3742 /// The prior u16 width with a single-stripe (close-only) bit
3743 /// allowed P_OPEN to re-overwrite `patbeginp[i]` on every
3744 /// backtrack iteration — turning the SECOND capture's start
3745 /// offset into the FIRST iteration's, e.g. `(*)-(*)` against
3746 /// `hello-world` returned `match[2] = "hello-world"` instead of
3747 /// the right `"world"`.
3748 pub captures_set: u32, // c:Src/pattern.c parsfound
3749 /// Port of file-static `int errsfound` from `Src/pattern.c:2046`.
3750 /// Cumulative edit-count for approximate-match `(#aN)`. Reset to 0
3751 /// at the top of each pattry, incremented on P_EXACTLY mismatches
3752 /// when `glob_flags & 0xff > 0` (the substitution-budget byte).
3753 pub errsfound: i32, // c:2046
3754}
3755
3756/// Port of `charref()` from `Src/pattern.c:1909`.
3757/// C: `wchar_t charref(char *x, char *y, int *zmb_ind)` from
3758/// `Src/pattern.c:1909`. Decode the char at `pos` without
3759/// advancing. UTF-32-native delegation: `s.chars().next()` returns
3760/// the decoded codepoint; delegates to Rust's native UTF-8 string
3761/// iterator instead of the C Meta-decode + zshtoken-translate +
3762/// mbrtowc state machine, which all collapse because Rust's `&str`
3763/// is already UTF-8.
3764///
3765/// Rust signature differs from C `charref(char *x, char *y, int *zmb_ind)`:
3766/// the C `y` end-of-string pointer is captured by `&str` length; the
3767/// `zmb_ind` out-param (multibyte-completion index) is dropped — the
3768/// Rust UTF-8 decode is one-shot, never partial.
3769pub fn charref(s: &str, pos: usize) -> Option<char> {
3770 // c:1909
3771 s[pos..].chars().next()
3772}
3773
3774/// Port of `charnext()` from `Src/pattern.c:1936`.
3775/// C: `char *charnext(char *x, char *y)`.
3776/// C returns the pointer past the current character; the Rust port
3777/// returns that position as a byte offset. Delegates to `metacharinc`
3778/// — same advance-by-one-codepoint logic.
3779pub fn charnext(x: &str, y: usize) -> usize {
3780 // c:1936
3781 metacharinc(x, y)
3782}
3783
3784/// Port of `charrefinc()` from `Src/pattern.c:1969`.
3785/// C: `wchar_t charrefinc(char **x, char *y, int *z)` — decode the
3786/// character at `*x` and advance past it. A natively stored `char` is
3787/// already one decoded character (the byte-at-a-time view of a native
3788/// multibyte character without GF_MULTIBYTE is stepped by the callers).
3789///
3790/// Rust signature differs from C: C mutates `*x` via the
3791/// `char **` to advance; Rust mutates `pos` directly. The `y`
3792/// end-of-string sentinel is captured by `&str` length. C reads the
3793/// file-static `patglobflags`; the Rust matcher threads its flags as a
3794/// value, so they are the `gflags` argument. Returns the C `wchar_t`
3795/// as a `u32` because `WCHAR_INVALID` (c:241-242, `0xDC00 + byte`) is a
3796/// surrogate no Rust `char` can hold.
3797///
3798/// zshrs keeps a byte that is not valid UTF-8 METAFIED inside the `&str`
3799/// (Meta `U+0083` + `byte ^ 32`, the encoding `script_bytes` and the
3800/// `$'\xNN'` decoders produce), where C's matcher sees the raw byte. One
3801/// such pair is one raw byte, so it is decoded here the way C decodes
3802/// that byte: on its own without GF_MULTIBYTE (c:1974-1975), otherwise
3803/// through `mbrtowc` together with any pairs that follow (c:1977), and
3804/// as `WCHAR_INVALID` with `*z = 1` when that fails (c:1979-1984).
3805pub fn charrefinc(s: &str, pos: &mut usize, gflags: i32, z: &mut i32) -> Option<u32> {
3806 // c:1969
3807 let rest = s.get(*pos..)?;
3808 let mut it = rest.chars();
3809 let c = it.next()?;
3810 let meta_byte = |n: Option<char>| n.filter(|&n| (n as u32) >= 0x80).map(|n| (n as u32 as u8) ^ 32);
3811 let Some(first) = (c == '\u{83}').then(|| meta_byte(it.clone().next())).flatten() else {
3812 // A natively stored character is valid UTF-8 already.
3813 *pos += c.len_utf8();
3814 return Some(c as u32);
3815 };
3816 let pair_len = c.len_utf8() + it.clone().next().map_or(0, |n| n.len_utf8());
3817 // c:1974-1975 — `if (!(patglobflags & GF_MULTIBYTE) || …) return
3818 // (wchar_t) (unsigned char) *(*x)++;`
3819 if (gflags & GF_MULTIBYTE) == 0 {
3820 *pos += pair_len;
3821 return Some(first as u32);
3822 }
3823 // c:1977 — `ret = mbrtowc(&wc, *x, y-*x, &shiftstate);`, over the raw
3824 // bytes the following Meta pairs stand for.
3825 let mut raw: Vec<u8> = Vec::new();
3826 let mut lens: Vec<usize> = Vec::new();
3827 let mut scan = it.clone();
3828 scan.next();
3829 raw.push(first);
3830 lens.push(pair_len);
3831 loop {
3832 let mut wc: libc::wchar_t = 0;
3833 let mut mbs = crate::ported::utils::MBSTATE_ZERO;
3834 let ret = unsafe {
3835 crate::ported::utils::mbrtowc(
3836 &mut wc,
3837 raw.as_ptr() as *const libc::c_char,
3838 raw.len(),
3839 &mut mbs as *mut crate::ported::utils::MbStateBuf as *mut libc::c_void,
3840 )
3841 };
3842 if ret == crate::ported::utils::MB_INCOMPLETE && raw.len() < crate::ported::zsh_h::MB_CUR_MAX {
3843 let mut look = scan.clone();
3844 if look.next() == Some('\u{83}') {
3845 if let Some(b) = meta_byte(look.clone().next()) {
3846 let n = look.next().unwrap();
3847 raw.push(b);
3848 lens.push('\u{83}'.len_utf8() + n.len_utf8());
3849 scan = look;
3850 continue;
3851 }
3852 }
3853 }
3854 if ret == crate::ported::utils::MB_INVALID
3855 || ret == crate::ported::utils::MB_INCOMPLETE
3856 {
3857 // c:1979-1984 — "Error. Treat as single byte, but flag."
3858 *z = 1;
3859 *pos += lens[0];
3860 return Some(0xDC00 + first as u32); // c:1984 WCHAR_INVALID (c:241-242)
3861 }
3862 // c:1988-1990 — `*x += ret ? ret : 1; return wc;`
3863 *pos += lens[..ret.max(1).min(lens.len())].iter().sum::<usize>();
3864 return Some(wc as u32);
3865 }
3866}
3867
3868/// Port of `charsub()` from `Src/pattern.c:1997`.
3869/// C: `ptrdiff_t charsub(char *x, char *y)`.
3870///
3871/// Returns the number of characters between the start `x` and the
3872/// position `y` — i.e. the character-distance of byte offset `y` from
3873/// the string start. Non-multibyte: the byte distance `y` (each byte
3874/// is one character). Multibyte: the codepoint count of `x[0..y]`.
3875/// `patmatch` uses this (via the C `CHARSUB` macro) to report match
3876/// lengths and backreference positions in characters.
3877///
3878/// Replaces the prior fake that stepped back one char and returned a
3879/// byte offset — unrelated to the C character-count semantics.
3880pub fn charsub(x: &str, y: usize) -> usize {
3881 // c:1997
3882 let y = y.min(x.len());
3883 if !isset(MULTIBYTE) {
3884 // c:2003-2004 — `if (!isset(MULTIBYTE)) return y - x;`
3885 return y;
3886 }
3887 // c:2011-2026 — count characters in `x[0..y]`. C bounds each
3888 // `mbrtowc` by `y-x` (c:2012), so an END OFFSET THAT CUTS a
3889 // multibyte character in half yields MB_INCOMPLETE and c:2014-2018
3890 // `return res + (y - x)` counts every leftover byte as one
3891 // character. That is reachable: with GF_MULTIBYTE clear (`(#U)`,
3892 // c:1116) the matcher steps raw BYTES (c:1946), so `patendp[i]` can
3893 // land mid-character. A plain `x[..y].chars().count()` PANICS there.
3894 let mut res = 0usize;
3895 let mut i = 0usize;
3896 while i < y {
3897 // `i` is always on a character boundary — it only ever advances
3898 // by a whole character below.
3899 match x[i..].chars().next() {
3900 Some(c) if i + c.len_utf8() <= y => {
3901 res += 1; // c:2024
3902 i += c.len_utf8(); // c:2025
3903 }
3904 _ => return res + (y - i), // c:2018
3905 }
3906 }
3907 res // c:2028
3908}
3909
3910// =====================================================================
3911// 16. String pre-processing — pattern.c:2063, :2080, :2132
3912// =====================================================================
3913
3914/// Port of `pattrystart()` from `Src/pattern.c:2063`. C resets per-
3915/// match state globals; Rust state is per-call so no-op.
3916pub fn pattrystart() {} // c:2063
3917
3918/// Port of `patmungestring()` from `Src/pattern.c:2080`.
3919/// C: `void patmungestring(char **string, int *stringlen, int *unmetalenin)`
3920/// from `Src/pattern.c:2080`.
3921///
3922/// Skips a leading `Nularg` (the empty-tokenised-string sentinel)
3923/// and computes `*stringlen` from `strlen` when caller passed `-1`.
3924/// Mutates all three in/out args; mirrors C's `char **` /
3925/// `int *` semantics via Rust mutable references.
3926pub fn patmungestring(string: &mut &str, stringlen: &mut i32, unmetalenin: &mut i32) {
3927 // c:2080
3928 // c:2082-2091 — `if (*stringlen > 0 && **string == Nularg)` — skip
3929 // the leading Nularg sentinel and adjust lengths.
3930 let bytes = string.as_bytes();
3931 if *stringlen > 0 && !bytes.is_empty() && bytes[0] as char == Nularg {
3932 // c:2085 — `(*string)++;` — advance past Nularg.
3933 *string = &string[1..]; // c:2085
3934 // c:2090 — `if (*unmetalenin > 0) (*unmetalenin)--;`
3935 if *unmetalenin > 0 {
3936 // c:2089
3937 *unmetalenin -= 1; // c:2090
3938 }
3939 // c:2092 — `if (*stringlen > 0) (*stringlen)--;`
3940 if *stringlen > 0 {
3941 // c:2091
3942 *stringlen -= 1; // c:2092
3943 }
3944 }
3945
3946 // c:2096-2097 — `if (*stringlen < 0) *stringlen = strlen(*string);`
3947 if *stringlen < 0 {
3948 // c:2096
3949 *stringlen = string.len() as i32; // c:2097
3950 }
3951}
3952
3953/// Port of `pattry()` from `Src/pattern.c:2223`.
3954/// C: `pattry(Patprog prog, char *string)`.
3955///
3956/// C signature: `int pattry(Patprog prog, char *string)`. Returns
3957/// non-zero on match, 0 on no-match.
3958pub fn pattry(prog: &Patprog, string: &str) -> bool {
3959 // c:2223
3960 // c:2225 — `return pattrylen(prog, string, len, -1, NULL, 0);`
3961 pattrylen(prog, string, string.len() as i32, -1, None, 0) // c:2225
3962}
3963
3964/// Port of `pattrylen()` from `Src/pattern.c:2236`.
3965/// C: `int pattrylen(Patprog prog, char *string, int len,
3966/// int unmetalen, Patstralloc patstralloc, int offset)` from
3967/// `Src/pattern.c:2236`.
3968///
3969/// ```c
3970/// int
3971/// pattrylen(Patprog prog, char *string, int len, int unmetalen,
3972/// Patstralloc patstralloc, int offset)
3973/// {
3974/// return pattryrefs(prog, string, len, unmetalen, patstralloc, offset,
3975/// NULL, NULL, NULL);
3976/// }
3977/// ```
3978pub fn pattrylen(
3979 prog: &Patprog,
3980 string: &str,
3981 len: i32, // c:2236
3982 unmetalen: i32,
3983 patstralloc: Option<&Patstralloc>,
3984 offset: i32,
3985) -> bool {
3986 // c:2238
3987 pattryrefs(
3988 prog,
3989 string,
3990 len,
3991 unmetalen,
3992 patstralloc,
3993 offset,
3994 None,
3995 None,
3996 None, // c:2239
3997 )
3998}
3999
4000/// Port of `pattryrefs()` from `Src/pattern.c:2294`.
4001/// C: `int pattryrefs(Patprog prog, char *string, int stringlen,
4002/// int unmetalenin, Patstralloc patstralloc, int patoffset,
4003/// int *nump, int *begp, int *endp)` from `Src/pattern.c:2294`.
4004/// Runs `prog` against `string[0..stringlen]` (or whole string when
4005/// stringlen=-1) at `patoffset`, returning capture-group ranges.
4006///
4007/// C signature preserved per Rule S1. `Patstralloc` (the metafied-
4008/// string-allocator carrier from zsh.h:1613) is threaded through as
4009/// `Option<&Patstralloc>` matching C's `NULL`-passable pointer; the
4010/// matcher body currently ignores its contents (the substrate that
4011/// uses pre-allocated metafied buffers isn't wired into the Rust
4012/// matcher yet), but the param shape matches C so call sites trace
4013/// 1:1 to upstream.
4014#[allow(clippy::too_many_arguments)]
4015pub fn pattryrefs(
4016 // c:2294
4017 prog: &Patprog,
4018 string: &str,
4019 stringlen: i32,
4020 _unmetalenin: i32,
4021 _patstralloc: Option<&Patstralloc>,
4022 patoffset: i32,
4023 nump: Option<&mut i32>,
4024 begp: Option<&mut Vec<i32>>,
4025 endp: Option<&mut Vec<i32>>,
4026) -> bool {
4027 let trial: &str = if stringlen < 0 || (stringlen as usize) >= string.len() {
4028 string
4029 } else {
4030 &string[..stringlen as usize]
4031 };
4032 // Substring fast path for the ubiquitous `*literal*` shape
4033 // (optionally `(#i)`): program is [P_GFLAGS] P_STAR P_EXACTLY
4034 // P_STAR P_END with no captures requested. history-search-multi-
4035 // word's `${history[(R)(#i)*pat*]}` runs this shape over EVERY
4036 // history entry — 566k backtracking patmatch walks took CPU-
4037 // minutes per ^R (the reported freeze); a memmem-style scan is
4038 // what the shape actually needs. C reads this spirit via its
4039 // `mustoff` must-match prefilter (Src/pattern.c:2460-2483) but
4040 // declines under globflags; here the exact-bytes variant covers
4041 // any content and the `(#i)` variant is taken only when pattern
4042 // AND subject are pure ASCII (byte-fold == the matcher's char
4043 // fold there); anything else falls through to the full matcher.
4044 if nump.is_none()
4045 && begp.is_none()
4046 && endp.is_none()
4047 && patoffset == 0
4048 && (prog.0.flags & (PAT_NOTSTART | PAT_NOTEND) as i32) == 0
4049 // c:2399-2406 — a successful match is REJECTED for a file pattern
4050 // (PAT_NOGLD) whose subject starts with '.', unless the pattern itself
4051 // starts with a literal dot. That rejection lives below, AFTER
4052 // patmatch; returning from the fast path skipped it entirely, so
4053 // `*.*` matched `.hidden` (the literal `.` was found at offset 0 by
4054 // the substring scan) where zsh lists no dot files at all. Hand any
4055 // dot-leading subject under a file pattern to the full matcher, which
4056 // applies the rule. Costs nothing where the fast path was aimed:
4057 // history search (`${history[(R)(#i)*pat*]}`) is not a file glob and
4058 // never sets PAT_NOGLD, and non-dot files still take the fast path.
4059 && !((prog.0.flags & PAT_NOGLD as i32) != 0 && trial.starts_with('.'))
4060 {
4061 // Shape probe, inline (build-gate: no new fn in ported/ without
4062 // a C counterpart): Some((literal, igncase)) iff the program is
4063 // exactly `[P_GFLAGS] P_STAR P_EXACTLY(lit) P_STAR P_END` with
4064 // glob flags ⊆ IGNCASE|MULTIBYTE (no approx budget, no backrefs).
4065 let shape: Option<(String, bool)> = (|| {
4066 let buf: &[u8] = &prog.1;
4067 let mut off = prog.0.startoff as usize;
4068 let mut gflags = prog.0.globflags;
4069 let op_at = |o: usize| -> Option<u8> {
4070 if o + I_BODY <= buf.len() {
4071 Some(buf[o + I_OP])
4072 } else {
4073 None
4074 }
4075 };
4076 // A sole leading BRANCH (next == 0: no alternative) wraps
4077 // the whole program — step inside it.
4078 if op_at(off)? == P_BRANCH {
4079 let next = u32::from_le_bytes(buf[off + I_NEXT..off + I_NEXT + 4].try_into().ok()?);
4080 if next != 0 {
4081 return None; // real alternation — full matcher
4082 }
4083 off = advance_past_instr(buf, off);
4084 }
4085 if op_at(off)? == P_GFLAGS {
4086 let body = off + I_BODY;
4087 if body + 4 > buf.len() {
4088 return None;
4089 }
4090 gflags |= i32::from_le_bytes(buf[body..body + 4].try_into().ok()?);
4091 off = advance_past_instr(buf, off);
4092 }
4093 if (gflags & !(GF_IGNCASE | GF_MULTIBYTE)) != 0 {
4094 return None;
4095 }
4096 if op_at(off)? != P_STAR {
4097 return None;
4098 }
4099 off = advance_past_instr(buf, off);
4100 // One or more contiguous EXACTLY chunks form the literal
4101 // (the compiler splits long/space-bearing literals).
4102 if op_at(off)? != P_EXACTLY {
4103 return None;
4104 }
4105 let mut lit = String::new();
4106 while op_at(off)? == P_EXACTLY {
4107 let body = off + I_BODY;
4108 if body + 4 > buf.len() {
4109 return None;
4110 }
4111 let len = u32::from_le_bytes(buf[body..body + 4].try_into().ok()?) as usize;
4112 if body + 4 + len > buf.len() {
4113 return None;
4114 }
4115 lit.push_str(std::str::from_utf8(&buf[body + 4..body + 4 + len]).ok()?);
4116 off = advance_past_instr(buf, off);
4117 }
4118 if op_at(off)? != P_STAR {
4119 return None;
4120 }
4121 off = advance_past_instr(buf, off);
4122 if off + I_OP >= buf.len() || buf[off + I_OP] != P_END {
4123 return None;
4124 }
4125 Some((lit, (gflags & GF_IGNCASE) != 0))
4126 })();
4127 // c:3316-3317 — the trailing `*` walks `patinput` to `patinend`
4128 // and `if (P_OP(next) == P_END) return 1;`, so a hit consumed the
4129 // whole trial: c:2508 `patinlen = patinput - patinstart` is its
4130 // length. The fast path returned without recording it, leaving
4131 // `patmatchlen()` stale for the PAT_NOANCH callers (igetmatch
4132 // `##`, the `(S)` scans): `x=aXb; ${(S)x##*X*}` stripped nothing.
4133 let hit = |ok: bool| -> bool {
4134 if ok {
4135 patinlen.store(trial.len() as i32, Ordering::Relaxed); // c:2508
4136 }
4137 ok
4138 };
4139 if let Some((lit, igncase)) = shape.as_ref().map(|(l, i)| (l.as_str(), *i)) {
4140 if !igncase {
4141 return hit(trial.contains(lit));
4142 }
4143 if lit.is_ascii() && trial.is_ascii() {
4144 let lb = lit.as_bytes();
4145 let tb = trial.as_bytes();
4146 if lb.is_empty() {
4147 return hit(true);
4148 }
4149 if lb.len() <= tb.len() {
4150 let l0 = lb[0].to_ascii_lowercase();
4151 'scan: for s in 0..=(tb.len() - lb.len()) {
4152 if tb[s].to_ascii_lowercase() != l0 {
4153 continue;
4154 }
4155 for k in 1..lb.len() {
4156 if tb[s + k].to_ascii_lowercase() != lb[k].to_ascii_lowercase() {
4157 continue 'scan;
4158 }
4159 }
4160 return hit(true);
4161 }
4162 }
4163 return false;
4164 }
4165 // Non-ASCII under (#i): general matcher below.
4166 }
4167 }
4168 let mut state = rpat::new();
4169 // c:Src/pattern.c:2334 — `patflags = prog->flags;` C copies the
4170 // prog's flags into the file-static `patflags` so subsequent
4171 // patmatch calls can read PAT_NOTSTART / PAT_NOTEND inside the
4172 // P_ISSTART / P_ISEND arms (c:3393, 3397). Rust mirrors this via
4173 // the `patflags` AtomicI32 at pattern.rs:217.
4174 //
4175 // Without this propagation, `${str:#(#s)PAT}` / `${str:#PAT(#e)}`
4176 // anchor checks fired at slice-local offset 0 / slice-local end
4177 // regardless of whether the caller meant the slice was an
4178 // interior chunk of a larger string (PAT_NOTSTART/PAT_NOTEND
4179 // bits exist on the prog precisely so callers can signal this).
4180 patflags.store(prog.0.flags, Ordering::Relaxed);
4181 // c:2494 — `globdots = !(patflags & PAT_NOGLD);`. The leading-dot rule
4182 // is enforced INSIDE the walker (c:2731 / c:3322), on wildcard opcodes
4183 // only, so it has to be published before patmatch runs.
4184 globdots.store(
4185 i32::from((prog.0.flags & PAT_NOGLD as i32) == 0),
4186 Ordering::Relaxed,
4187 );
4188 // Pass the prog's GLOB flags (GF_* + `(#aN)` budget byte) to the
4189 // matcher. C threads `patglobflags` as a per-thread file-static;
4190 // the Rust port carries it through a fn param. The PAT_* flags
4191 // (PAT_STATIC etc.) stay on `prog.0.flags` for the outer
4192 // anchor/PURES checks at lines below.
4193 // c:Src/pattern.c — `if (prog->flags & PAT_ANY) { ret = 1; } else { ... }`.
4194 // Optimisation for a single "*" (the `**` complist node compiles its
4195 // section as `patcompile(NULL, …|PAT_ANY, …)`): it always matches the
4196 // whole string without running the bytecode. The PAT_NOGLD leading-dot
4197 // rejection below still applies (the "except for no_glob_dots" caveat).
4198 // Without this the empty `PAT_ANY` program was matched literally and
4199 // only matched the empty string, so `**` descended into nothing.
4200 let (mut ok, matched_end) = if (prog.0.flags & PAT_ANY as i32) != 0 {
4201 (true, trial.len())
4202 } else {
4203 match patmatch(&prog.1, 0, trial, 0, &mut state, prog.0.globflags) {
4204 Some(end_pos) => {
4205 // c:2438 — `if (matched && !(prog->flags & (PAT_NOANCH|PAT_NOTEND))) ...`
4206 let no_anchor = (prog.0.flags & (PAT_NOANCH | PAT_NOTEND) as i32) != 0;
4207 (no_anchor || end_pos == trial.len(), end_pos)
4208 }
4209 None => (false, 0),
4210 }
4211 };
4212 // c:2404-2410 — `if (ret) { if ((prog->flags & PAT_NOGLD) &&
4213 // *patinstart == '.') ret = 0; else ... }`.
4214 //
4215 // This rejection sits INSIDE C's `if (prog->flags & (PAT_PURES|PAT_ANY))`
4216 // fast path (c:2347), where no bytecode runs and the walker's own
4217 // P_NOTDOT gates (c:2731 / c:3322) never get a chance to fire. It is NOT
4218 // a blanket rule over every match: for a compiled program C rejects a
4219 // leading dot only when a WILDCARD opcode is what would consume it.
4220 //
4221 // The port used to apply it to every match and compensate with
4222 // `prog.patstartch != b'.'`. `patstartch` is recorded only for a program
4223 // whose FIRST node is a bare `P_EXACTLY` with no glob flags (c:709-711),
4224 // so every other way of writing a literal leading dot lost its dot files:
4225 // measured against `zsh -f` 5.9 in a directory holding `.hidden`,
4226 // `(.)hidden`, `(.|x)hidden` and `(#i).HIDDEN` each listed `.hidden` in
4227 // zsh and NOTHING in this port.
4228 if ok && (prog.0.flags & (PAT_PURES | PAT_ANY) as i32) != 0 && trial.starts_with('.') && (prog.0.flags & PAT_NOGLD as i32) != 0
4229 {
4230 ok = false; // c:2410
4231 }
4232 if ok {
4233 // c:2508 — `patinlen = patinput - patinstart;` — record the
4234 // byte-length of the successful match so `patmatchlen()` can
4235 // return it after the strings/state are torn down. `end_pos`
4236 // is the in-trial byte offset where patmatch stopped, which
4237 // is `patinput - patinstart` in C terms.
4238 patinlen.store(matched_end as i32, Ordering::Relaxed);
4239 }
4240 if ok {
4241 let n = (prog.0.patnpar as usize).min(NSUBEXP);
4242 let have_nump = nump.is_some();
4243 // c:2536 / c:2587 — `metafy(patinstart, *sp - patinstart, META_DUP)`:
4244 // C hands back the RAW matched bytes, re-metafied. With GF_MULTIBYTE
4245 // clear (`(#U)`, c:1116) the match unit is a single byte (c:1946), so
4246 // `lo`/`hi` can cut a multibyte character in half and a plain
4247 // `trial[lo..hi]` slice PANICS. Rebuild such a span the way zshrs
4248 // stores `$'\xNN'` elsewhere — Meta (0x83) followed by `byte ^ 32` —
4249 // which is exactly what C's `metafy` produces.
4250 let metafy_span = |lo: usize, hi: usize| -> String {
4251 if let Some(s) = trial.get(lo..hi) {
4252 return s.to_string();
4253 }
4254 let mut out = String::new();
4255 for &b in &trial.as_bytes()[lo..hi] {
4256 if b >= crate::ported::zsh_h::Meta {
4257 out.push('\u{83}'); // c:4862 Meta
4258 out.push(char::from(b ^ 32)); // c:4863 `*t++ = *p ^ 32`
4259 } else {
4260 out.push(char::from(b));
4261 }
4262 }
4263 out
4264 };
4265 if let Some(np) = nump {
4266 *np = n as i32;
4267 }
4268 // c:2526-2542 — GF_MATCHREF (`(#m)`): on success, write the
4269 // matched substring to $MATCH and its 1-based (KSHARRAYS-
4270 // aware) char span to $MBEGIN/$MEND. C does this INSIDE
4271 // pattryrefs; the previous Rust port left it to a bridge
4272 // wrapper that sniffed the pattern TEXT for "(#m)" — wrong
4273 // mechanism (missed hoisted/nested flag positions) and
4274 // wrong layer.
4275 // c:2526 — `if ((patglobflags & GF_MATCHREF) && !(patflags & PAT_FILE))`.
4276 // The source is the GLOBAL patglobflags (c:273), not `prog->globflags`.
4277 // They are not the same thing: c:Src/zsh.h:1606 documents the struct
4278 // field as "globbing flags to set at START", so it keeps whatever the
4279 // pattern opened with, while the global is what patgetglobflags leaves
4280 // after walking every flag — including a later `(#M)` turning
4281 // GF_MATCHREF back off (c:1099-1100).
4282 //
4283 // Reading the struct field made `(#M)` inert: for all three of
4284 // `(#m)a*`, `(#m)(#M)a*` and `(#M)(#m)a*` it is 0x1800, whereas the
4285 // global correctly ends at 0x800 / 0x0 / 0x800. So `(#m)(#M)a*` still
4286 // wrote $MATCH where zsh leaves it unset.
4287 let cur_globflags = patglobflags.load(Ordering::Relaxed);
4288 if (cur_globflags & GF_MATCHREF) != 0 && (prog.0.flags & PAT_FILE as i32) == 0 {
4289 let hi = matched_end.min(trial.len());
4290 let mstr: String = metafy_span(0, hi); // c:2536 metafy(patinstart..patinput)
4291 let mlen = mstr.chars().count() as i64; // c:2534 CHARSUB
4292 let base: i64 = if crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHARRAYS) {
4293 0
4294 } else {
4295 1
4296 };
4297 crate::ported::params::setsparam("MATCH", &mstr); // c:2537
4298 crate::ported::params::setiparam("MBEGIN", patoffset as i64 + base); // c:2538
4299 crate::ported::params::setiparam("MEND", mlen + patoffset as i64 + base - 1);
4300 // c:2539-2541
4301 }
4302 // c:2425+ — emit captured offsets to begp/endp out-arrays.
4303 // Check the CLOSE bit (high stripe at NSUBEXP+i) since the
4304 // group is only fully captured after its P_CLOSE fired; the
4305 // matching open-bit `1 << i` would be set after P_OPEN even
4306 // when the rest of the match later fails to reach P_CLOSE.
4307 //
4308 // c:Src/pattern.c:2556-2558 — `*begp++ = CHARSUB(patinstart, *sp) + patoffset;`.
4309 // `patoffset` is the start position of `string` within the
4310 // ORIGINAL string the caller was matching against (used for
4311 // paramsubst's sliding-window scan via `${var/PAT/REPL}`). Add
4312 // it to every reported beg/end so MBEGIN/MEND / `$match` see
4313 // positions relative to the original string, not the local
4314 // trial slice.
4315 // c:2556-2566 — `*begp++ = …` writes into the caller's FIXED-SIZE array
4316 // (C: `int begpos[MAX_POS]`), filling the first `n` slots and leaving
4317 // the array's length unchanged. Write IN PLACE (extend only if the
4318 // caller's Vec is shorter than `n`) rather than clear()+push — clearing
4319 // shrank a MAX_POS-length array down to `n`, so the caller's
4320 // [n..MAX_POS] reset loop (complist.rs getcol, c:631) indexed past the
4321 // end and panicked (`index 4 on len 4`) on patterns with < MAX_POS
4322 // backrefs.
4323 if let Some(bv) = begp {
4324 for i in 0..n {
4325 let close_bit = 1u32 << (i + NSUBEXP);
4326 let val = if (state.captures_set & close_bit) != 0 {
4327 // c:2556-2558 — `CHARSUB(patinstart, *sp)` counts
4328 // CHARACTERS, not bytes (c:1997 charsub honours the
4329 // MULTIBYTE option). `patbeginp[i]` is a byte offset into
4330 // `trial`, so convert before adding the (already
4331 // character-based, c:2285-2286) `patoffset`.
4332 charsub(trial, state.patbeginp[i]) as i32 + patoffset
4333 } else {
4334 -1 // c:2563-2566 — unset group (unmatched alternation branch)
4335 };
4336 if i < bv.len() {
4337 bv[i] = val;
4338 } else {
4339 bv.push(val);
4340 }
4341 }
4342 }
4343 if let Some(ev) = endp {
4344 for i in 0..n {
4345 let close_bit = 1u32 << (i + NSUBEXP);
4346 let val = if (state.captures_set & close_bit) != 0 {
4347 // c:2562-2564 — `*endp++ = CHARSUB(patinstart, *ep) +
4348 // patoffset - 1;`. The reported end is the index of the LAST
4349 // matched character, not one past it; `patendp[i]` is the
4350 // exclusive end, hence the `- 1`.
4351 //
4352 // Load-bearing for the only consumer of these arrays,
4353 // complist's putmatchcol (c:Src/Zle/complist.c:889).
4354 // `doiscol`'s finished-empty test at complist.c:646 is
4355 // `endpos[i] < begpos[i]`, which an empty capture can only
4356 // satisfy when the end is INCLUSIVE (b, b-1). With the
4357 // exclusive value (b, b) an empty `(#b)` group consumed a
4358 // colour AND painted a character, and every non-empty group
4359 // stayed coloured one character too long.
4360 //
4361 // `CHARSUB` counts characters (c:1997), so the byte offset
4362 // in `patendp[i]` converts through `charsub` first.
4363 charsub(trial, state.patendp[i]) as i32 + patoffset - 1
4364 } else {
4365 -1 // c:2565
4366 };
4367 if i < ev.len() {
4368 ev[i] = val;
4369 } else {
4370 ev.push(val);
4371 }
4372 }
4373 }
4374 // c:2570-2621 — `else if (prog->patnpar && !(patflags &
4375 // PAT_FILE))`: the caller passed NO capture arrays, so
4376 // pattryrefs itself publishes the `(#b)` groups as the
4377 // $match / $mbegin / $mend arrays. This arm was missing
4378 // from the port — every plain pattry of a (#b) pattern
4379 // silently dropped its captures (the bridge compensated
4380 // with a wrapper; now C-faithful at the source).
4381 if !have_nump && n > 0 && (prog.0.flags & PAT_FILE as i32) == 0 {
4382 let base: i32 = if crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHARRAYS) {
4383 0
4384 } else {
4385 1
4386 };
4387 let mut match_arr: Vec<String> = Vec::with_capacity(n);
4388 let mut begin_arr: Vec<String> = Vec::with_capacity(n);
4389 let mut end_arr: Vec<String> = Vec::with_capacity(n);
4390 for i in 0..n {
4391 let close_bit = 1u32 << (i + NSUBEXP);
4392 if (state.captures_set & close_bit) != 0 {
4393 let b = state.patbeginp[i];
4394 let e = state.patendp[i];
4395 let lo = b.min(trial.len());
4396 let hi = e.min(trial.len()).max(lo);
4397 match_arr.push(metafy_span(lo, hi)); // c:2587 metafy(*sp..*ep)
4398 // c:2596-2599 — `CHARSUB(patinstart, *sp) + patoffset +
4399 // !isset(KSHARRAYS)`. CHARSUB (c:1997) counts CHARACTERS;
4400 // `patbeginp`/`patendp` hold BYTE offsets into `trial`, so
4401 // convert through charsub before adding the (already
4402 // character-based, c:2285-2286) patoffset.
4403 begin_arr.push((charsub(trial, b) as i32 + patoffset + base).to_string()); // c:2596-2599
4404 // c:2601-2604 — mend = last matched char index
4405 // (inclusive): end + offset + base - 1.
4406 end_arr.push((charsub(trial, e) as i32 + patoffset + base - 1).to_string());
4407 } else {
4408 // c:2607-2613 — unmatched branch / hashed paren.
4409 match_arr.push(String::new());
4410 begin_arr.push("-1".to_string());
4411 end_arr.push("-1".to_string());
4412 }
4413 }
4414 crate::ported::params::setaparam("match", match_arr); // c:2619
4415 crate::ported::params::setaparam("mbegin", begin_arr); // c:2620
4416 crate::ported::params::setaparam("mend", end_arr); // c:2621
4417 }
4418 }
4419 ok
4420}
4421
4422// =====================================================================
4423// 15. Range matching — pattern.c:3856, :4004, :3610, :3767
4424// =====================================================================
4425
4426/// Port of `patmatchlen()` from `Src/pattern.c:2649`.
4427/// C: `int patmatchlen(void)`.
4428///
4429/// ```c
4430/// /**/
4431/// int
4432/// patmatchlen(void)
4433/// {
4434/// return patinlen;
4435/// }
4436/// ```
4437///
4438/// Returns the length in metafied bytes of the last successful
4439/// `pattry` / `pattryrefs` match. `patinlen` is set at
4440/// `pattern.c:2508` (`patinlen = patinput - patinstart`); the
4441/// Rust port sets the equivalent `patinlen` AtomicI32 at the end
4442/// of `pattryrefs` (see `pattern.rs::pattryrefs`).
4443pub fn patmatchlen() -> i32 {
4444 // c:2649-2652
4445 patinlen.load(Ordering::Relaxed) // c:2651 — `return patinlen;`
4446}
4447/// Port of `patmatchindex()` from `Src/pattern.c:4004`.
4448/// C: `int patmatchindex(char *range, int ind, int *chr, int *mtp)`
4449/// from `Src/pattern.c:4004` (MULTIBYTE_SUPPORT-disabled single-byte
4450/// variant). Walks a NULL-terminated, METAFIED, PP_*-encoded byte
4451/// stream `range` and returns the character (or POSIX-class id)
4452/// at index `ind`. Output:
4453/// - `Some((Some(ch), mtp))` — literal character or PP_RANGE hit;
4454/// `chr = ch`, `mtp = 0`.
4455/// - `Some((None, mtp))` — POSIX class match (PP_ALPHA etc);
4456/// `chr = -1` in C, `None` in Rust; `mtp = class id`.
4457/// - `None` — `ind` exceeds the descriptor's length.
4458///
4459/// Encoding the byte stream uses:
4460/// * literal byte `< 0x83` = match char as itself
4461/// * `Meta(0x83)` + (byte ^ 0x20) = ordinary metafied character
4462/// * `Meta + PP_*` (0x84..) = POSIX class marker
4463/// * `Meta + PP_RANGE` = next two metafied chars are `lo, hi`
4464pub fn patmatchindex(range: &[u8], mut ind: i32) -> Option<(Option<u8>, i32)> {
4465 // c:4004-4081
4466 let mut chr: Option<u8> = None; // c:4014 — `*chr = -1`
4467 let mut mtp: i32 = 0; // c:4015 — `*mtp = 0`
4468
4469 // C `UNMETA(range)` + `METACHARINC(range)` macro pair from
4470 // Src/zsh.h:1796-1797 — decode-and-advance one metafied
4471 // character. Returned as `(decoded_byte, byte_advance)`.
4472 let unmeta = |bytes: &[u8], i: usize| -> (u8, usize) {
4473 if i < bytes.len() && bytes[i] == Meta && i + 1 < bytes.len() {
4474 (bytes[i + 1] ^ 0x20, 2)
4475 } else if i < bytes.len() {
4476 (bytes[i], 1)
4477 } else {
4478 (0, 0)
4479 }
4480 };
4481
4482 let mut i = 0usize;
4483 while i < range.len() {
4484 // c:4017 — `for (; *range; range++)`
4485 let b = range[i];
4486 if crate::ported::utils::imeta_byte(b) {
4487 // c:4018 — `if (imeta((unsigned char) *range))`
4488 // c:4019 — `int swtype = (unsigned char) *range - (unsigned char) Meta;`
4489 let swtype = (b as i32) - (Meta as i32);
4490 match swtype {
4491 0 => {
4492 // c:4021-4028 — `case 0: ordinary metafied character`
4493 // c:4023 — `rchr = (unsigned char) *++range ^ 32;`
4494 i += 1;
4495 if i >= range.len() {
4496 break;
4497 }
4498 let rchr = range[i] ^ 0x20;
4499 if ind == 0 {
4500 // c:4024-4026 — `if (!ind) { *chr = rchr; return 1; }`
4501 chr = Some(rchr);
4502 return Some((chr, mtp));
4503 }
4504 // c:4027 — falls through to `if (!ind--) break;`
4505 }
4506 t if (PP_ALPHA..=PP_INVALID).contains(&t) => {
4507 // c:4030-4051 — POSIX class markers PP_ALPHA..PP_INVALID
4508 if ind == 0 {
4509 // c:4052-4054 — `if (!ind) { *mtp = swtype; return 1; }`
4510 mtp = swtype;
4511 return Some((None, mtp));
4512 }
4513 }
4514 t if t == PP_RANGE => {
4515 // c:4057-4069 — PP_RANGE: next two metafied chars
4516 // `range++; r1 = UNMETA(range); METACHARINC(range);
4517 // r2 = UNMETA(range); if (*range == Meta) range++;`
4518 i += 1;
4519 if i >= range.len() {
4520 break;
4521 }
4522 let (r1, adv1) = unmeta(range, i);
4523 i += adv1;
4524 if i >= range.len() {
4525 break;
4526 }
4527 let (r2, adv2) = unmeta(range, i);
4528 i += adv2;
4529 let rdiff = r2 as i32 - r1 as i32;
4530 if rdiff >= ind {
4531 // c:4063-4067 — `if (rdiff >= ind) { *chr = r1 + ind; return 1; }`
4532 chr = Some((r1 as i32 + ind) as u8);
4533 return Some((chr, mtp));
4534 }
4535 // c:4068 — `ind -= rdiff;` (extra decrement happens via
4536 // the `ind--` below, accounting for the C comment
4537 // "note the extra decrement to ind below").
4538 ind -= rdiff;
4539 // Skip the trailing `if (!ind--) break;` decrement
4540 // for this branch since C's loop already does it.
4541 }
4542 _ => {
4543 // c:4070-4076 — PP_UNKWN / default → DPUTS bug warn
4544 // (unreachable in well-formed compiled output; bail).
4545 }
4546 }
4547 } else {
4548 // c:4079-4082 — literal char path
4549 if ind == 0 {
4550 // c:4080-4082 — `if (!ind) { *chr = (unsigned char) *range; return 1; }`
4551 chr = Some(b);
4552 return Some((chr, mtp));
4553 }
4554 }
4555 // c:4087 — `if (!ind--) break;` — pre-decrement-check.
4556 if ind == 0 {
4557 break;
4558 }
4559 ind -= 1;
4560 i += 1; // c:4017 — `range++`
4561 }
4562 // c:4091 — `/* No corresponding index. */ return 0;`
4563 None
4564}
4565
4566/// Port of `patmatchrange(char *range, int ch, int *indptr, int *mtp)`
4567/// from `Src/pattern.c:3865-3995` (reached through the
4568/// `PATMATCHRANGE` macro). Walks an encoded character-range
4569/// descriptor in `str` (Cpattern.str byte sequence) and tests
4570/// whether `c` falls inside. Encoding written by
4571/// `complete.rs::parse_class` (c:Src/Zle/complete.c:523/539):
4572/// 0x80 + PP_RANGE (=0x95): next 2 bytes are lo,hi range
4573/// 0x80 + PP_* (POSIX class id): single-byte class marker
4574/// plain byte: literal char (0x00-0x7F)
4575/// The port's marker base is 0x80 where C uses `Meta` (0x83); every
4576/// decoder in the port agrees on 0x80, so the offset is internal.
4577///
4578/// `indptr` is the position of `c` among the class MEMBERS seen so far:
4579/// c:3970 adds `ch - r1` on a hit, c:3974 adds `r2 - r1` when stepping
4580/// over a non-matching range, and c:3992-3993 adds a further 1 at the
4581/// end of EVERY non-returning iteration — so a skipped range advances
4582/// by its full `r2 - r1 + 1` member count and a skipped literal or
4583/// POSIX-class marker advances by exactly 1. That is the same counting
4584/// `pattern_match_equivalence`'s PATMATCHINDEX walk (c:1320,
4585/// pattern.c:4013-4088) performs in reverse, so producer and consumer
4586/// must agree element-for-element. `pattern_match1` turns that
4587/// into the equivalence-class index (`ind + 1`, c:1283) that
4588/// `pattern_match` compares between the line and word side
4589/// (c:1573 `if (ind != wind) return 0;`), and that
4590/// `pattern_match_equivalence` feeds back through PATMATCHINDEX.
4591/// The port previously incremented by 1 per element, so every char of
4592/// a `{a-z}`-style class collapsed to index 0: `m:{a-z\-}={A-Z\_}`
4593/// then matched `a` against EVERY uppercase name (`cd /a<TAB>`
4594/// offered /Applications /Library /System /Users /Volumes, and the
4595/// bogus ambiguity suppressed the insertion entirely).
4596pub fn patmatchrange(
4597 s: Option<&[u8]>,
4598 c: u32,
4599 mut indp: Option<&mut u32>,
4600 mtp: Option<&mut i32>,
4601) -> bool {
4602 let Some(bytes) = s else {
4603 return false;
4604 };
4605 // c:3869 — `if (indptr) *indptr = 0;`
4606 if let Some(out) = indp.as_deref_mut() {
4607 *out = 0;
4608 }
4609
4610 let mut i = 0usize;
4611 let mut mtp_dest: Option<&mut i32> = mtp;
4612 // c:3876 — `for (; *range; range++)`
4613 while i < bytes.len() {
4614 let b = bytes[i];
4615 if b >= 0x80 {
4616 // c:3877-3878 — `imeta(*range)`; swtype = marker - base.
4617 let swtype = (b as i32) - 0x80;
4618 // c:3879-3880 — `if (mtp) *mtp = swtype;` runs for EVERY
4619 // meta element, whether or not it goes on to match.
4620 if let Some(out) = mtp_dest.as_deref_mut() {
4621 *out = swtype;
4622 }
4623 if swtype == 0 {
4624 // c:3882-3885 — metafied literal: next byte ^ 32.
4625 if i + 1 >= bytes.len() {
4626 break;
4627 }
4628 if (bytes[i + 1] ^ 32) as u32 == c {
4629 return true;
4630 }
4631 i += 2;
4632 // c:3992-3993 — fall through to the shared `(*indptr)++`.
4633 if let Some(out) = indp.as_deref_mut() {
4634 *out += 1;
4635 }
4636 continue;
4637 }
4638 if swtype == PP_RANGE {
4639 // c:3961-3975
4640 if i + 2 >= bytes.len() {
4641 break;
4642 }
4643 let r1 = bytes[i + 1] as u32;
4644 let r2 = bytes[i + 2] as u32;
4645 if r1 <= c && c <= r2 {
4646 // c:3968-3971
4647 if let Some(out) = indp.as_deref_mut() {
4648 *out += c - r1;
4649 }
4650 return true;
4651 }
4652 // c:3973-3974 — `if (indptr && r1 < r2) *indptr += r2 - r1;`
4653 if r1 < r2 {
4654 if let Some(out) = indp.as_deref_mut() {
4655 *out += r2 - r1;
4656 }
4657 }
4658 i += 3;
4659 // c:3992-3993 — plus the per-iteration increment, so a skipped
4660 // range advances the index by its full member count
4661 // (r2 - r1 + 1). Omitting it made the producer disagree with
4662 // pattern_match_equivalence's PATMATCHINDEX consumer, which
4663 // does count every element.
4664 if let Some(out) = indp.as_deref_mut() {
4665 *out += 1;
4666 }
4667 continue;
4668 }
4669 // c:3886-3960 — POSIX classes. Single-byte locale
4670 // (the C build only reaches this file without
4671 // MULTIBYTE_SUPPORT), so `ch` outside 0-255 never matches.
4672 let hit = c < 256 && {
4673 let cb = c as u8;
4674 match swtype {
4675 PP_ALPHA => cb.is_ascii_alphabetic(), // c:3886
4676 PP_ALNUM => cb.is_ascii_alphanumeric(), // c:3890
4677 PP_ASCII => (c & !0x7f) == 0, // c:3894
4678 PP_BLANK => cb == b' ' || cb == b'\t', // c:3898
4679 PP_CNTRL => cb.is_ascii_control(), // c:3907
4680 PP_DIGIT => cb.is_ascii_digit(), // c:3911
4681 PP_GRAPH => cb.is_ascii_graphic(), // c:3915
4682 PP_LOWER => cb.is_ascii_lowercase(), // c:3919
4683 // c:3923 ZISPRINT — C isprint(): graphic or space.
4684 PP_PRINT => cb.is_ascii_graphic() || cb == b' ',
4685 PP_PUNCT => cb.is_ascii_punctuation(), // c:3927
4686 // c:3931 isspace(): " \t\n\v\f\r"
4687 PP_SPACE => matches!(cb, b' ' | 0x09..=0x0d),
4688 PP_UPPER => cb.is_ascii_uppercase(), // c:3935
4689 PP_XDIGIT => cb.is_ascii_hexdigit(), // c:3939
4690 PP_IDENT => crate::ported::ztype_h::iident(cb), // c:3943
4691 PP_IFS => crate::ported::ztype_h::isep(cb), // c:3947
4692 PP_IFSSPACE => crate::ported::ztype_h::iwsep(cb), // c:3951
4693 PP_WORD => crate::ported::ztype_h::iword(cb), // c:3955
4694 // c:3959-3961 PP_INCOMPLETE / PP_INVALID are never
4695 // true without MULTIBYTE_SUPPORT; PP_UNKWN / unknown
4696 // markers fall through as no-match.
4697 _ => false,
4698 }
4699 };
4700 if hit {
4701 return true;
4702 }
4703 i += 1;
4704 // c:3992-3993 — a skipped POSIX-class marker is one class member.
4705 if let Some(out) = indp.as_deref_mut() {
4706 *out += 1;
4707 }
4708 } else if b as u32 == c {
4709 // c:3987-3990 — plain literal match sets `*mtp = 0`.
4710 if let Some(out) = mtp_dest.as_deref_mut() {
4711 *out = 0;
4712 }
4713 return true;
4714 } else {
4715 i += 1;
4716 // c:3992-3993 — and so is a skipped literal.
4717 if let Some(out) = indp.as_deref_mut() {
4718 *out += 1;
4719 }
4720 }
4721 }
4722 false
4723}
4724
4725/// Port of `mb_patmatchrange()` from `Src/pattern.c:3610`.
4726/// C: `int mb_patmatchrange(char *range, wchar_t ch,
4727/// int zmb_ind, wint_t *indptr, int *mtp)` from `Src/pattern.c:3610`.
4728/// Multibyte variant of `patmatchrange`: walks the metafied,
4729/// PP_*-encoded byte stream `range` and tests whether wide char `ch`
4730/// is in it. `indptr` (when Some) accumulates the running index for
4731/// `mb_patmatchindex`-side lookup; `mtp` (when Some) records which
4732/// PP_* class fired.
4733///
4734/// `zmb_ind` is the `ZMB_*` multibyte-completion state from the
4735/// caller (typically the pattry input pos): ZMB_INCOMPLETE /
4736/// ZMB_INVALID feed the PP_INCOMPLETE / PP_INVALID branches.
4737///
4738/// The Rust port delegates `iswalpha` / `iswdigit` / `wcsitype` to
4739/// `is_alphabetic()` / `is_numeric()` / etc. on the decoded `char`,
4740/// matching the C `iswXXX(wchar_t)` semantics.
4741pub fn mb_patmatchrange(
4742 range: &[u8],
4743 ch: char,
4744 zmb_ind: i32,
4745 mut indptr: Option<&mut u32>,
4746 mut mtp: Option<&mut i32>,
4747) -> bool {
4748 // c:3610-3766
4749 if let Some(p) = indptr.as_deref_mut() {
4750 *p = 0; // c:3615-3616 — `if (indptr) *indptr = 0;`
4751 }
4752 // C `UNMETA(s)` + `METACHARINC(s)` macro pair (Src/zsh.h).
4753 let unmeta = |bytes: &[u8], i: usize| -> (u8, usize) {
4754 if i < bytes.len() && bytes[i] == Meta && i + 1 < bytes.len() {
4755 (bytes[i + 1] ^ 0x20, 2)
4756 } else if i < bytes.len() {
4757 (bytes[i], 1)
4758 } else {
4759 (0, 0)
4760 }
4761 };
4762 let mut i = 0usize;
4763 while i < range.len() {
4764 // c:3623 — `while (*range)`
4765 let b = range[i];
4766 if crate::ported::utils::imeta_byte(b) {
4767 // c:3624 — `if (imeta((unsigned char) *range))`
4768 // c:3625 — `swtype = (unsigned char) *range++ - (unsigned char) Meta;`
4769 let swtype = (b as i32) - (Meta as i32);
4770 i += 1;
4771 if let Some(p) = mtp.as_deref_mut() {
4772 *p = swtype; // c:3626-3627 — `if (mtp) *mtp = swtype;`
4773 }
4774 let class_hit = match swtype {
4775 0 => {
4776 // c:3629-3634 — `case 0: ordinary metafied character`
4777 i -= 1; // c:3631 — `range--;`
4778 let (decoded, adv) = unmeta(range, i);
4779 i += adv;
4780 decoded == (ch as u32) as u8 && (ch as u32) < 256
4781 }
4782 t if t == PP_ALPHA => ch.is_alphabetic(),
4783 t if t == PP_ALNUM => ch.is_alphanumeric(),
4784 t if t == PP_ASCII => (ch as u32) < 128,
4785 t if t == PP_BLANK => ch == ' ' || ch == '\t',
4786 t if t == PP_CNTRL => ch.is_control(),
4787 t if t == PP_DIGIT => ch.is_ascii_digit(),
4788 t if t == PP_GRAPH => !ch.is_whitespace() && !ch.is_control(),
4789 t if t == PP_LOWER => ch.is_lowercase(),
4790 t if t == PP_PRINT => !ch.is_control(),
4791 t if t == PP_PUNCT => ch.is_ascii_punctuation(),
4792 t if t == PP_SPACE => ch.is_whitespace(),
4793 t if t == PP_UPPER => ch.is_uppercase(),
4794 t if t == PP_XDIGIT => ch.is_ascii_hexdigit(),
4795 t if t == PP_IDENT => {
4796 // c:3704 — `PP_IDENT: if (wcsitype(ch, IIDENT)) return 1;`
4797 // IIDENT = alphanumeric, underscore, or "$" depending on
4798 // option state. Conservative: ASCII identifier chars.
4799 ch.is_alphanumeric() || ch == '_'
4800 }
4801 t if t == PP_IFS => {
4802 // c:3708 — `wcsitype(ch, ISEP)`. ISEP = space, tab,
4803 // newline + user IFS additions. Default IFS in zsh
4804 // is ` \t\n`.
4805 ch == ' ' || ch == '\t' || ch == '\n' || ch == '\0'
4806 }
4807 t if t == PP_IFSSPACE => {
4808 // c:3712-3713 — ASCII-only IFS-space.
4809 (ch as u32) < 128 && (ch == ' ' || ch == '\t' || ch == '\n')
4810 }
4811 t if t == PP_WORD => {
4812 // c:3716 — `wcsitype(ch, IWORD)`. IWORD = WORDCHARS-derived;
4813 // conservative ASCII alphanumeric + `_`.
4814 ch.is_alphanumeric() || ch == '_'
4815 }
4816 t if t == PP_RANGE => {
4817 // c:3719-3735 — PP_RANGE: two metafied wide chars are
4818 // `r1` and `r2`; matches if `r1 <= ch <= r2`.
4819 let (r1_byte, adv1) = unmeta(range, i);
4820 i += adv1;
4821 let (r2_byte, adv2) = unmeta(range, i);
4822 i += adv2;
4823 let r1 = r1_byte as u32;
4824 let r2 = r2_byte as u32;
4825 let chu = ch as u32;
4826 if r1 <= chu && chu <= r2 {
4827 if let Some(p) = indptr.as_deref_mut() {
4828 *p += chu - r1; // c:3725-3726
4829 }
4830 return true; // c:3727
4831 }
4832 if let Some(p) = indptr.as_deref_mut() {
4833 if r1 < r2 {
4834 *p += r2 - r1; // c:3733-3734
4835 }
4836 }
4837 false
4838 }
4839 t if t == PP_INCOMPLETE => {
4840 // c:3740 — `if (zmb_ind == ZMB_INCOMPLETE) return 1;`
4841 zmb_ind == ZMB_INCOMPLETE
4842 }
4843 t if t == PP_INVALID => {
4844 // c:3744 — `if (zmb_ind == ZMB_INVALID) return 1;`
4845 zmb_ind == ZMB_INVALID
4846 }
4847 _ => {
4848 // c:3747-3753 — PP_UNKWN / default → DPUTS bug warn
4849 false
4850 }
4851 };
4852 if class_hit {
4853 return true;
4854 }
4855 } else {
4856 // c:3757-3762 — literal-char path
4857 let (decoded, adv) = unmeta(range, i);
4858 i += adv;
4859 if decoded == (ch as u32) as u8 && (ch as u32) < 256 {
4860 if let Some(p) = mtp.as_deref_mut() {
4861 *p = 0; // c:3760
4862 }
4863 return true; // c:3761
4864 }
4865 }
4866 if let Some(p) = indptr.as_deref_mut() {
4867 *p += 1; // c:3764-3765 — `if (indptr) (*indptr)++;`
4868 }
4869 }
4870 false // c:3766 — `return 0;`
4871}
4872
4873/// Port of `mb_patmatchindex()` from `Src/pattern.c:3767`.
4874/// C: `int mb_patmatchindex(char *range, wint_t ind,
4875/// wint_t *chr, int *mtp)` from `Src/pattern.c:3767`. The reverse
4876/// of `mb_patmatchrange`: given a metafied byte range and an index
4877/// `ind` into it, return the character (or PP_* class) at that
4878/// position.
4879///
4880/// Returns:
4881/// - `Some((Some(ch), 0))` — literal or PP_RANGE hit (chr = ch).
4882/// - `Some((None, mtp))` — POSIX class match; chr = WEOF / None.
4883/// - `None` — index out of range.
4884pub fn mb_patmatchindex(range: &[u8], mut ind: u32) -> Option<(Option<char>, i32)> {
4885 // c:3767-3849
4886 let mut chr: Option<char> = None; // c:3776 — `*chr = WEOF`
4887 let mut mtp: i32 = 0; // c:3777 — `*mtp = 0`
4888
4889 // C `UNMETA(s)` + `METACHARINC(s)` macro pair (Src/zsh.h).
4890 let unmeta = |bytes: &[u8], i: usize| -> (u8, usize) {
4891 if i < bytes.len() && bytes[i] == Meta && i + 1 < bytes.len() {
4892 (bytes[i + 1] ^ 0x20, 2)
4893 } else if i < bytes.len() {
4894 (bytes[i], 1)
4895 } else {
4896 (0, 0)
4897 }
4898 };
4899
4900 let mut i = 0usize;
4901 while i < range.len() {
4902 // c:3779 — `while (*range)`
4903 let b = range[i];
4904 if crate::ported::utils::imeta_byte(b) {
4905 // c:3780 — `if (imeta((unsigned char) *range))`
4906 let swtype = (b as i32) - (Meta as i32);
4907 i += 1;
4908 match swtype {
4909 0 => {
4910 // c:3782-3789 — `case 0: ordinary metafied char`
4911 i -= 1;
4912 let (decoded, adv) = unmeta(range, i);
4913 i += adv;
4914 let rchr = decoded as char;
4915 if ind == 0 {
4916 chr = Some(rchr);
4917 return Some((chr, mtp));
4918 }
4919 }
4920 t if (PP_ALPHA..=PP_INVALID).contains(&t) => {
4921 // c:3791-3812 — POSIX class markers
4922 if ind == 0 {
4923 mtp = swtype;
4924 return Some((None, mtp));
4925 }
4926 }
4927 t if t == PP_RANGE => {
4928 // c:3814-3829 — PP_RANGE: two metafied wide chars
4929 let (r1_byte, adv1) = unmeta(range, i);
4930 i += adv1;
4931 let (r2_byte, adv2) = unmeta(range, i);
4932 i += adv2;
4933 let r1 = r1_byte as u32;
4934 let r2 = r2_byte as u32;
4935 let rdiff = r2.saturating_sub(r1);
4936 if rdiff >= ind {
4937 chr = char::from_u32(r1 + ind);
4938 return Some((chr, mtp));
4939 }
4940 ind = ind.saturating_sub(rdiff);
4941 }
4942 _ => {
4943 // c:3831-3837 — PP_UNKWN / default → DPUTS
4944 }
4945 }
4946 } else {
4947 // c:3840-3843 — literal byte path
4948 if ind == 0 {
4949 chr = Some(b as char);
4950 return Some((chr, mtp));
4951 }
4952 }
4953 // c:3846 — `if (!ind--) break;`
4954 if ind == 0 {
4955 break;
4956 }
4957 ind -= 1;
4958 i += 1;
4959 }
4960 None // c:3849 — `return 0`
4961}
4962
4963/// Port of `freepatprog()` from `Src/pattern.c:4161`.
4964/// C: `freepatprog(Patprog prog)`. Frees a Patprog.
4965/// Rust's `Drop` on `Box<patprog>` handles this; the explicit fn
4966/// exists for C parity (Rule A).
4967#[allow(unused_variables)]
4968pub fn freepatprog(prog: Patprog) {} // c:4161
4969
4970/// Port of `pat_enables()` from `Src/pattern.c:4171`.
4971/// C: `int pat_enables(const char *cmd, char **patp, int enable)`
4972/// from `Src/pattern.c:4171`. Implements `enable -p`/`disable -p`: with
4973/// an empty `patp`, prints the currently enabled (or disabled, if
4974/// `!enable`) tokens by walking `zpc_strings[]`/`zpc_disables[]` in
4975/// lockstep. Otherwise toggles each named token's `zpc_disables[i]`
4976/// slot, emitting `invalid pattern: NAME` for misses.
4977pub fn pat_enables(cmd: &str, patp: &[&str], enable: bool) -> i32 {
4978 // c:4171
4979 let mut ret: i32 = 0; // c:4173
4980 if patp.is_empty() {
4981 // c:4177 !*patp
4982 let strings = ZPC_STRINGS; // c:4179 zpc_strings
4983 let disp = zpc_disables.lock().unwrap(); // c:4179 zpc_disables
4984 let mut done = false; // c:4178
4985 let mut out: String = String::new();
4986 for i in 0..(ZPC_COUNT as usize) {
4987 // c:4180
4988 let sp = match strings[i] {
4989 // c:4182 !*stringp
4990 Some(s) => s,
4991 None => continue,
4992 };
4993 let is_disabled = disp[i] != 0;
4994 if enable == is_disabled {
4995 // c:4184 enable?*disp:!*disp
4996 continue;
4997 }
4998 if done {
4999 // c:4186
5000 out.push(' '); // c:4187
5001 }
5002 out.push_str(&format!("'{}'", sp)); // c:4188
5003 done = true; // c:4189
5004 }
5005 if done {
5006 // c:4191
5007 println!("{}", out); // c:4187-4192
5008 }
5009 return 0; // c:4193
5010 }
5011 for p in patp {
5012 // c:4196
5013 let strings = ZPC_STRINGS;
5014 let mut disp = zpc_disables.lock().unwrap();
5015 let mut matched = false;
5016 for i in 0..(ZPC_COUNT as usize) {
5017 // c:4197
5018 if let Some(s) = strings[i] {
5019 if s == *p {
5020 // c:4200 !strcmp
5021 disp[i] = if enable { 0u8 } else { 1u8 }; // c:4201 *disp = !enable
5022 matched = true;
5023 break; // c:4202
5024 }
5025 }
5026 }
5027 if !matched {
5028 // c:4205
5029 zerrnam(cmd, &format!("invalid pattern: {}", p)); // c:4206
5030 ret = 1; // c:4207
5031 }
5032 }
5033 ret // c:4211
5034}
5035
5036/// Port of `mod_export const char *zpc_strings[ZPC_COUNT]` from
5037/// `Src/pattern.c:258`. Static token-name table indexed by ZPC_*;
5038/// NULL entries (ZPC_NULL, ZPC_BNULLKEEP, ZPC_INPAR_PIPE,
5039/// ZPC_KSHCHAR) have no user-visible name.
5040pub const ZPC_STRINGS: [Option<&'static str>; ZPC_COUNT as usize] = [
5041 // c:258
5042 None,
5043 None,
5044 Some("|"),
5045 None,
5046 Some("~"),
5047 Some("("),
5048 Some("?"),
5049 Some("*"),
5050 Some("["),
5051 Some("<"),
5052 Some("^"),
5053 Some("#"),
5054 None,
5055 Some("?("),
5056 Some("*("),
5057 Some("+("),
5058 Some("!("),
5059 Some("\\!("),
5060 Some("@("),
5061];
5062
5063/// Port of `savepatterndisables()` from `Src/pattern.c:4220`.
5064/// C: `unsigned int savepatterndisables(void)` from
5065/// `Src/pattern.c:4220`.
5066///
5067/// C body (c:4220-4233):
5068/// ```c
5069/// unsigned int disables, bit;
5070/// char *disp;
5071/// disables = 0;
5072/// for (bit = 1, disp = zpc_disables;
5073/// disp < zpc_disables + ZPC_COUNT;
5074/// bit <<= 1, disp++) {
5075/// if (*disp) disables |= bit;
5076/// }
5077/// return disables;
5078/// ```
5079///
5080/// Encodes the current `zpc_disables\[ZPC_COUNT\]` byte-array as a u32
5081/// bitmask (one bit per slot, low bit = `zpc_disables\[0\]`).
5082/// The previous Rust port returned a `Vec<String>` clone of
5083/// `patterndisables` (a completely different data structure — names
5084/// list, not the per-token byte array). `restorepatterndisables(u32)`
5085/// at c:4258 reads this bitmask back into `zpc_disables`, so the
5086/// returned shape MUST be the u32 bitmask.
5087pub fn savepatterndisables() -> u32 {
5088 // c:4220
5089 let disp = zpc_disables.lock().unwrap(); // c:4225 disp = zpc_disables
5090 let mut disables: u32 = 0; // c:4224
5091 let mut bit: u32 = 1; // c:4226 bit = 1
5092 for i in 0..(ZPC_COUNT as usize) {
5093 // c:4226-4228
5094 if disp[i] != 0 {
5095 // c:4230
5096 disables |= bit; // c:4231
5097 }
5098 bit <<= 1; // c:4226 bit <<= 1
5099 }
5100 disables // c:4232
5101}
5102
5103/// Port of `startpatternscope()` from `Src/pattern.c:4241`.
5104/// C: `void startpatternscope(void)`.
5105/// Pushes a frame onto `PATSCOPE_STACK` (`zpc_disables_stack` in C)
5106/// carrying the current `zpc_disables[]` state as a `savepatterndisables()`
5107/// u32 bitmap. Called at function entry; `endpatternscope` pops it.
5108///
5109/// ```c
5110/// void startpatternscope(void) {
5111/// Zpc_disables_save newdis = zalloc(sizeof(*newdis));
5112/// newdis->next = zpc_disables_stack;
5113/// newdis->disables = savepatterndisables(); // c:4247
5114/// zpc_disables_stack = newdis;
5115/// }
5116/// ```
5117pub fn startpatternscope() {
5118 // c:4241
5119 let saved = savepatterndisables(); // c:4247
5120 PATSCOPE_STACK.with(|s| s.borrow_mut().push(saved));
5121}
5122
5123/// Port of `restorepatterndisables()` from `Src/pattern.c:4258`.
5124/// C: `void restorepatterndisables(unsigned int disables)` from
5125/// `Src/pattern.c:4258`. Walks the 12-slot `zpc_disables[]` array,
5126/// setting each slot's byte from the bitmask: `disables & (1<<i)`
5127/// → slot `i` gets 1, else 0.
5128/// ```c
5129/// void
5130/// restorepatterndisables(unsigned int disables)
5131/// {
5132/// char *disp;
5133/// unsigned int bit;
5134/// for (bit = 1, disp = zpc_disables;
5135/// disp < zpc_disables + ZPC_COUNT;
5136/// bit <<= 1, disp++) {
5137/// if (disables & bit) *disp = 1;
5138/// else *disp = 0;
5139/// }
5140/// }
5141/// ```
5142pub fn restorepatterndisables(disables: u32) {
5143 // c:4258
5144 let mut disp = zpc_disables.lock().unwrap(); // c:4263
5145 let mut bit: u32 = 1;
5146 for i in 0..(ZPC_COUNT as usize) {
5147 // c:4263-4265
5148 if (disables & bit) != 0 {
5149 // c:4266
5150 disp[i] = 1; // c:4267
5151 } else {
5152 disp[i] = 0; // c:4269
5153 }
5154 bit <<= 1;
5155 }
5156}
5157
5158/// Port of `endpatternscope()` from `Src/pattern.c:4279`.
5159/// C: `void endpatternscope(void)`.
5160/// Pops the saved bitmap from `PATSCOPE_STACK` (`zpc_disables_stack`
5161/// in C); restores `zpc_disables[]` from the bitmap ONLY when
5162/// `isset(LOCALPATTERNS)` per C c:4286. Called at function exit.
5163///
5164/// ```c
5165/// void endpatternscope(void) {
5166/// Zpc_disables_save olddis = zpc_disables_stack;
5167/// zpc_disables_stack = olddis->next;
5168/// if (isset(LOCALPATTERNS))
5169/// restorepatterndisables(olddis->disables); // c:4287
5170/// zfree(olddis, sizeof(*olddis));
5171/// }
5172/// ```
5173pub fn endpatternscope() {
5174 // c:4279
5175 if let Some(prev) = PATSCOPE_STACK.with(|s| s.borrow_mut().pop()) {
5176 if isset(crate::ported::zsh_h::LOCALPATTERNS) {
5177 // c:4286-4287
5178 restorepatterndisables(prev);
5179 }
5180 }
5181}
5182
5183/// Port of `clearpatterndisables()` from `Src/pattern.c:4296`.
5184/// C: `void clearpatterndisables(void)`.
5185/// C body: `memset(zpc_disables, 0, ZPC_COUNT)` — zero every slot.
5186pub fn clearpatterndisables() {
5187 // c:4296
5188 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize]; // c:4298
5189}
5190
5191/// Port of `haswilds()` from `Src/pattern.c:4306`.
5192/// C: `haswilds(char *str)`.
5193///
5194/// Check whether `str` is eligible for filename generation.
5195///
5196/// C scans a TOKENIZED string: every input reaching it has been
5197/// through the lexer or `tokenize()`/`shtokenize()` (glob.c:3548/
5198/// 3563), so glob metachars are token codes and escaped/quoted
5199/// chars are Bnull'd literals. Callers holding un-tokenized strings
5200/// must `tokenize()` a copy first — exactly what C does for
5201/// runtime-built strings (compcore.c:2231 tokenizes fignore entries
5202/// immediately before its c:2235 haswilds call). The switch matches
5203/// ONLY token codes, never literal ASCII metachars.
5204///
5205/// The walk is over chars: zshrs strings hold token chars as
5206/// codepoints (Star = U+e187) and the input layer is char-domain
5207/// (input.rs `shingetline`/`ingetc`), so the char is the unit that
5208/// the metafied byte is in C. Scanning bytes here false-positived
5209/// on UTF-8 continuation bytes of plain text (`↔` = E2 86 94
5210/// carries 0x86/0x94 = Hat/Inang as u8) — bug #627.
5211///
5212/// The C source's `%?foo` job-ref special case (c:4317-4318), which
5213/// mutates `str[1]` in place to demote the `?`, becomes a "skip
5214/// position 1" scan adjustment here since `&str` is immutable.
5215pub fn haswilds(str: &str) -> bool {
5216 // c:4306
5217 // c:4325-4372 — every `return 1` needs one of Inpar / Bar / Star /
5218 // Inbrack / Inang / Quest / Pound / Hat, and all eight are TOKEN
5219 // chars (crate::token_char), i.e. non-ASCII. An all-ASCII string
5220 // therefore always falls through to c:4374 `return 0`. Answer it
5221 // with libcore's precompiled word-at-a-time `<[u8]>::is_ascii`
5222 // instead of building a `Vec<char>` to walk: `haswilds` runs once
5223 // per candidate word, and a `man <TAB>` offers 60605 of them.
5224 if str.is_ascii() {
5225 return false; // c:4374
5226 }
5227 let chars: Vec<char> = str.chars().collect();
5228 let len = chars.len();
5229 if len == 0 {
5230 return false;
5231 }
5232
5233 // c:4309 — `[' and `]' are legal even if bad patterns are usually not.
5234 if len == 1 && (chars[0] == Inbrack || chars[0] == Outbrack) {
5235 return false; // c:4311
5236 }
5237
5238 // c:4313-4315 — If % is immediately followed by ?, then that ? is
5239 // not treated as a wildcard. This is so you don't have
5240 // to escape job references such as %?foo.
5241 let skip_pos_1 = len >= 2 && chars[0] == '%' && chars[1] == Quest; // c:4316-4317
5242
5243 // c:4319-4321 — Note that at this point zpc_special has not been set up.
5244 let disp = zpc_disables.lock().unwrap(); // c:read zpc_disables[]
5245
5246 for i in 0..len {
5247 // c:4323 for (; *str; str++)
5248 if skip_pos_1 && i == 1 {
5249 continue; // c:4317 `str[1] = '?'` demote
5250 }
5251 let c = chars[i]; // c:4324 switch (*str)
5252 let prev: char = if i > 0 { chars[i - 1] } else { '\0' }; // c: str[-1]
5253
5254 if c == Inpar {
5255 // c:4325-4335
5256 if (!isset(SHGLOB) && disp[ZPC_INPAR as usize] == 0)
5257 || (i > 0
5258 && isset(KSHGLOB)
5259 && ((prev == Quest && disp[ZPC_KSH_QUEST as usize] == 0)
5260 || (prev == Star && disp[ZPC_KSH_STAR as usize] == 0)
5261 || (prev == '+' && disp[ZPC_KSH_PLUS as usize] == 0)
5262 || (prev == Bang && disp[ZPC_KSH_BANG as usize] == 0)
5263 || (prev == '!' && disp[ZPC_KSH_BANG2 as usize] == 0)
5264 || (prev == '@' && disp[ZPC_KSH_AT as usize] == 0)))
5265 {
5266 return true; // c:4335
5267 }
5268 } else if c == Bar {
5269 if disp[ZPC_BAR as usize] == 0 {
5270 return true; // c:4340
5271 }
5272 } else if c == Star {
5273 if disp[ZPC_STAR as usize] == 0 {
5274 return true; // c:4345
5275 }
5276 } else if c == Inbrack {
5277 if disp[ZPC_INBRACK as usize] == 0 {
5278 return true; // c:4350
5279 }
5280 } else if c == Inang {
5281 if disp[ZPC_INANG as usize] == 0 {
5282 return true; // c:4355
5283 }
5284 } else if c == Quest {
5285 if disp[ZPC_QUEST as usize] == 0 {
5286 return true; // c:4360
5287 }
5288 } else if c == Pound {
5289 if isset(EXTENDEDGLOB) && disp[ZPC_HASH as usize] == 0 {
5290 return true; // c:4365
5291 }
5292 } else if c == Hat {
5293 if isset(EXTENDEDGLOB) && disp[ZPC_HAT as usize] == 0 {
5294 return true; // c:4370
5295 }
5296 }
5297 }
5298 false // c:4374
5299}
5300
5301// =====================================================================
5302// 4. struct patprog — zsh.h:1601
5303// =====================================================================
5304
5305/// `typedef struct patprog *Patprog;` from `zsh.h:542`.
5306#[allow(non_camel_case_types)]
5307/// `typedef struct patprog *Patprog;` from `zsh.h:542`.
5308///
5309/// C zsh allocates the `struct patprog` header + bytecode as one
5310/// contiguous `malloc` block, accessing bytecode via
5311/// `(char *)prog + prog->startoff`. Rust has no flexible array
5312/// members, so this typedef pairs the C-exact `patprog` header
5313/// (zsh_h.rs:768) with an owned bytecode `Vec<u8>` — header at
5314/// `.0`, bytecode at `.1`. `startoff`/`size` index into `.1`.
5315pub type Patprog = Box<(patprog, Vec<u8>)>;
5316// =====================================================================
5317// Bytecode field offsets within each instruction.
5318//
5319// Instruction layout in `patout`:
5320// +0 u8 opcode
5321// +1..+5 u32 LE next_off (offset of next instr in chain, 0 = end)
5322// +5.. u8... opcode-specific payload
5323//
5324// C uses a different layout (Upat union = 4-byte or 8-byte slots);
5325// the Rust port pins the layout to byte offsets for portability.
5326// =====================================================================
5327
5328const I_OP: usize = 0; // opcode byte
5329
5330impl rpat {
5331 pub fn new() -> Self {
5332 Self {
5333 wbranch_visits: std::rc::Rc::new(std::cell::RefCell::new(
5334 std::collections::HashMap::new(),
5335 )),
5336 patbeginp: [usize::MAX; NSUBEXP],
5337 patendp: [0; NSUBEXP],
5338 captures_set: 0,
5339 errsfound: 0, // c:2066 — errsfound = 0 at pattry init
5340 }
5341 }
5342}
5343const I_NEXT: usize = 1; // u32 next-offset starts here
5344const I_BODY: usize = 5; // payload starts here
5345
5346/// Serialises every entry into `patcompile`. The C source at
5347/// `Src/pattern.c:267-281` declares `patout`, `patparse`, `patstart`,
5348/// `patnpar`, `patflags`, `patglobflags`, `errsfound`, `forceerrs`,
5349/// `zpc_special`, `patstrcache` as file-scope statics that the compile
5350/// mutates in sequence; zsh-the-program is single-threaded so the C
5351/// source is safe under that invariant. zshrs callers (zutil's
5352/// `style_table::get` via `crate::ported::pattern::patmatch`, params.rs,
5353/// subst.rs, options.rs) can invoke `patcompile` from concurrent test
5354/// threads, so the lock restores the single-writer invariant. Held
5355/// only for the compile phase; the matcher (`pattry`/`patmatch`)
5356/// operates on the returned `Patprog.code` and touches no globals.
5357static PATCOMPILE_LOCK: Mutex<()> = Mutex::new(());
5358
5359// C: `static char *patparse, *patstart;` — pattern parsing cursors
5360// into the *input* pattern string. patstart points to start of
5361// pattern; patparse moves forward as we consume tokens.
5362pub static patparse: Mutex<String> = Mutex::new(String::new()); // c:269
5363pub static patstart: Mutex<String> = Mutex::new(String::new()); // c:269
5364
5365/// Position within `patparse` we're currently looking at. C source
5366/// uses a `char *` cursor; Rust uses byte offset into the String.
5367pub static patparse_off: AtomicUsize = AtomicUsize::new(0);
5368
5369// C: `static int errsfound;` — approximate-match error count.
5370pub static errsfound: AtomicI32 = AtomicI32::new(0); // c:274
5371
5372// C: `static int forceerrs;` — required error count for approximate match.
5373pub static forceerrs: AtomicI32 = AtomicI32::new(-1); // c:275
5374
5375// C: `static long patglobflags_orig;` — saved at branch entry.
5376pub static patglobflags_orig: AtomicI32 = AtomicI32::new(0); // c:276
5377
5378// C: `static const char *zpc_special;` — table of currently-special
5379// characters during compile (indexed by ZPC_*).
5380//
5381// pattern.c uses `static char zpc_special[ZPC_COUNT];` and resets it
5382// in patcompcharsset(). Rust mirrors as a Mutex-wrapped byte array.
5383pub static zpc_special: Mutex<[u8; ZPC_COUNT as usize]> = Mutex::new([0u8; ZPC_COUNT as usize]); // c:278
5384
5385// C: `static char *patstrcache;` — caches the unmetafied trial string.
5386// Rust port has no Meta encoding so the cache is unnecessary; we leave
5387// the static declared for parity (Rule A — name exists in C).
5388pub static patstrcache: Mutex<String> = Mutex::new(String::new()); // c:281
5389
5390/// `Marker` constant — alias for `crate::ported::zsh_h::Marker`
5391/// (`Src/zsh.h:224` `#define Marker ((char) 0xa2)`).
5392///
5393/// The previous Rust port had this as `pub const Marker: u8 = 0x80`
5394/// which is WRONG — `\200` (0x80) is NOT the canonical Marker byte.
5395/// C's Marker is 0xa2 per `Src/zsh.h:224`. No in-tree callers used
5396
5397/// Port of `static const char *colon_stuffs[]` from `Src/pattern.c:1134-1138`.
5398/// 19 entries matching C's table in declaration order, so `range_type(name)`
5399/// returns the same `(index + PP_FIRST)` value C does:
5400/// alpha=1, alnum=2, ascii=3, blank=4, cntrl=5, digit=6, graph=7, lower=8,
5401/// print=9, punct=10, space=11, upper=12, xdigit=13, IDENT=14, IFS=15,
5402/// IFSSPACE=16, WORD=17, INCOMPLETE=18, INVALID=19. Prior Rust port had
5403/// only 12 lowercase entries and was MISSING `ascii` between `alnum` and
5404/// `blank`, so `range_type("digit")` returned 5 instead of C's PP_DIGIT=6
5405/// and every `[:class:]` byte marker emitted by `complete.rs:733`
5406/// (`0x80 + ch`) was off-by-one for classes after `alnum`. Real port bug.
5407const POSIX_CLASS_NAMES: &[&str] = &[
5408 "alpha",
5409 "alnum",
5410 "ascii",
5411 "blank",
5412 "cntrl",
5413 "digit",
5414 "graph",
5415 "lower",
5416 "print",
5417 "punct",
5418 "space",
5419 "upper",
5420 "xdigit",
5421 "IDENT",
5422 "IFS",
5423 "IFSSPACE",
5424 "WORD",
5425 "INCOMPLETE",
5426 "INVALID",
5427];
5428
5429/// Port of file-static `zpc_disables_stack` from `Src/pattern.c:4244`.
5430/// Per-evaluator function-scope disable save-stack (bucket-1: each
5431/// worker thread parses/executes its own function calls, so each must
5432/// have its own scope stack). Reason for `thread_local!` over `Mutex`:
5433/// in zsh C this is a per-process file-static; in zshrs each worker
5434/// thread is its own evaluator — TLS preserves the per-evaluator
5435/// semantic without serializing across workers.
5436///
5437/// Element type: u32 bitmap matching `savepatterndisables` return
5438/// shape (c:4220 `unsigned int disables`). Prior Rust port used
5439/// `Vec<Vec<String>>` and `startpatternscope` cloned a `Vec<String>`
5440/// of names instead of the bitmap — a real port bug that made
5441/// `setopt LOCALPATTERNS` function entry/exit silently fail to save/
5442/// restore the disable state.
5443thread_local! {
5444 static PATSCOPE_STACK: std::cell::RefCell<Vec<u32>> =
5445 const { std::cell::RefCell::new(Vec::new()) };
5446}
5447
5448// =====================================================================
5449// 17. Module-loader / disable mgmt — pattern.c:4161-4296
5450// =====================================================================
5451
5452// `pub static patterndisables: Mutex<Vec<String>>` deleted — was a
5453// dead tombstone with no callers outside the buggy
5454// `startpatternscope` / `endpatternscope` / `clearpatterndisables`
5455// fns (which cloned/cleared it instead of operating on the real
5456// `zpc_disables` byte array). The canonical zsh `zpc_disables[ZPC_COUNT]`
5457// (Src/pattern.c:268) is the disable state; the names list does not
5458// exist as a separate data structure in C.
5459
5460/// Port of `char zpc_disables[ZPC_COUNT]` from `Src/pattern.c:268`.
5461/// Per-token disable byte — when `zpc_disables[i]` is non-zero, the
5462/// pattern token at index `i` (ZPC_*) is treated as a literal,
5463/// not its meta-meaning.
5464pub static zpc_disables: Mutex<[u8; ZPC_COUNT as usize]> = Mutex::new([0u8; ZPC_COUNT as usize]); // c:268
5465
5466/// !!! WARNING: RUST-ONLY HELPER !!!
5467///
5468/// There is NO `fixup_offsets_after_insert()` in `Src/pattern.c`.
5469/// It stands in for C's ABSENCE of a fixup step: C's `patinsert()`
5470/// (`Src/pattern.c:1807`) links nodes by RELATIVE `P_NEXT` deltas
5471/// (`Src/pattern.c:198`) held in `union upat` slots, so shifting the
5472/// tail of `patout` down leaves every link valid and C only has to
5473/// `memmove` (`Src/pattern.c:1816-1820`). zshrs's bytecode stores
5474/// ABSOLUTE byte offsets in the 4-byte `I_NEXT` slot, which the
5475/// memmove invalidates — so every next-offset past `opnd` has to be
5476/// re-based by `delta`. Called only from `patinsert()`; both writer
5477/// and reader live in pattern.rs.
5478///
5479/// Walks the buffer linearly opcode-by-opcode reading I_NEXT slots.
5480/// Conservatively adjusts every nonzero next that lands past opnd.
5481fn fixup_offsets_after_insert(buf: &mut [u8], opnd: usize, delta: u32) {
5482 let mut i = 0;
5483 while i + I_BODY <= buf.len() {
5484 let op = buf[i + I_OP];
5485 if op == 0 {
5486 i += 1;
5487 continue;
5488 } // sentinel byte, skip
5489 let next_bytes = &buf[i + I_NEXT..i + I_NEXT + 4];
5490 let cur = u32::from_le_bytes(next_bytes.try_into().unwrap());
5491 if cur != 0 {
5492 let abs = cur as usize;
5493 if abs >= opnd && abs <= buf.len() {
5494 let new = cur + delta;
5495 buf[i + I_NEXT..i + I_NEXT + 4].copy_from_slice(&new.to_le_bytes());
5496 }
5497 }
5498 i = advance_past_instr(buf, i);
5499 if i == 0 {
5500 break;
5501 }
5502 }
5503}
5504
5505/// !!! WARNING: RUST-ONLY HELPER !!!
5506///
5507/// There is NO `advance_past_instr()` in `Src/pattern.c`. C never
5508/// needs one: its bytecode is an array of fixed-size `union upat`
5509/// slots (`union upat`, `Src/pattern.c:84-89`), so "the next slot"
5510/// is just
5511/// `p + 1` pointer arithmetic and the payload width is implicit in
5512/// the slot count each emitter wrote. zshrs emits a packed BYTE
5513/// stream instead, so stepping over an instruction requires decoding
5514/// its payload width from the opcode. This function is that decode
5515/// table. Called only from `fixup_offsets_after_insert()`; both
5516/// writer and reader live in pattern.rs.
5517///
5518/// Encodes the per-opcode payload size table — must stay in sync
5519/// with patnode/patinsert calls in the compiler.
5520fn advance_past_instr(buf: &[u8], pos: usize) -> usize {
5521 if pos + I_BODY > buf.len() {
5522 return 0;
5523 }
5524 let op = buf[pos + I_OP];
5525 let body_start = pos + I_BODY;
5526 match op {
5527 P_END | P_NOTHING | P_BACK | P_EXCSYNC | P_EXCEND | P_ISSTART | P_ISEND | P_COUNTSTART
5528 | P_ANY | P_STAR | P_NUMANY => body_start,
5529 P_GFLAGS => body_start + 4, // i32 flag-bits payload
5530 P_EXACTLY => {
5531 // payload: u32 len + len bytes
5532 if body_start + 4 > buf.len() {
5533 return 0;
5534 }
5535 let len =
5536 u32::from_le_bytes(buf[body_start..body_start + 4].try_into().unwrap()) as usize;
5537 body_start + 4 + len
5538 }
5539 P_ANYOF | P_ANYBUT => {
5540 if body_start + 4 > buf.len() {
5541 return 0;
5542 }
5543 let len =
5544 u32::from_le_bytes(buf[body_start..body_start + 4].try_into().unwrap()) as usize;
5545 body_start + 4 + len
5546 }
5547 P_ONEHASH | P_TWOHASH | P_BRANCH => body_start,
5548 // c:113 P_WBRANCH and c:114-115 P_EXCLUDE/P_EXCLUDP carry an
5549 // 8-byte syncptr payload (one Upat slot in C) right after
5550 // their header, before the chained operand.
5551 P_WBRANCH | P_EXCLUDE | P_EXCLUDP => body_start + 8,
5552 P_OPEN..=0x88 | P_CLOSE..=0x98 => body_start,
5553 P_NUMRNG => body_start + 16, // two i64
5554 P_NUMFROM | P_NUMTO => body_start + 8,
5555 P_COUNT => body_start + 16, // min i64 + max i64; operand inline follows
5556 _ => body_start,
5557 }
5558}
5559
5560/// !!! WARNING: RUST-ONLY HELPER !!!
5561///
5562/// There is NO `set_next()` in `Src/pattern.c`. C writes a node's
5563/// next-link inline as one assignment against the `union upat`
5564/// pointer — `scan->l |= offset << 8;` (`Src/pattern.c:1849`), the
5565/// write half of the `P_NEXT` macro at `Src/pattern.c:198`, as used
5566/// in `pattail()` (`Src/pattern.c:1846-1849`). zshrs stores that link
5567/// as a 4-byte little-endian absolute offset inside a `Vec<u8>`
5568/// behind a mutex, so the same one-liner needs a lock, a bounds
5569/// check and a `to_le_bytes` — factored here rather than repeated at
5570/// every emit site. Both writer and reader live in pattern.rs.
5571fn set_next(pos: usize, val: usize) {
5572 let mut buf = patout.lock().unwrap();
5573 if pos + I_NEXT + 4 <= buf.len() {
5574 buf[pos + I_NEXT..pos + I_NEXT + 4].copy_from_slice(&(val as u32).to_le_bytes());
5575 }
5576}
5577
5578/// !!! WARNING: RUST-ONLY HELPER !!!
5579///
5580/// There is NO `chain_branches_to()` in `Src/pattern.c`. C performs
5581/// this walk inline at the end of `patcompswitch()`
5582/// (`Src/pattern.c:909-911`): `for (ptr = (Upat)patout + starter; ptr;
5583/// ptr = PATNEXT(ptr)) if (!P_ISEXCLUDE(ptr)) patoptail(ptr -
5584/// (Upat)patout, ender);` — `union upat` pointer arithmetic reaches
5585/// each alternative and its operand. In zshrs the operand is at
5586/// `br + I_BODY` and the branch link is a
5587/// 4-byte absolute offset read under the `patout` mutex, so the loop
5588/// is factored out to keep `patcompswitch()` readable. Both writer
5589/// and reader live in pattern.rs.
5590///
5591/// Walks every branch's operand chain and patches each branch's
5592/// last-operand-node `.next` to `target`. Used to chain a fully-
5593/// compiled alternation switch to whatever opcode follows (P_END for
5594/// the outermost compile, P_CLOSE_N for a sub-group).
5595fn chain_branches_to(starter: usize, target: usize) {
5596 let mut cur = starter;
5597 loop {
5598 // Operand starts at cur + I_BODY (the byte right after this
5599 // branch's header). Walk operand's next-chain to its end
5600 // and set its .next = target.
5601 pattail(cur + I_BODY, target);
5602 // Move to next alternative.
5603 let buf = patout.lock().unwrap();
5604 if cur + I_NEXT + 4 > buf.len() {
5605 break;
5606 }
5607 let nb: [u8; 4] = buf[cur + I_NEXT..cur + I_NEXT + 4].try_into().unwrap();
5608 let n = u32::from_le_bytes(nb) as usize;
5609 drop(buf);
5610 if n == 0 {
5611 break;
5612 }
5613 cur = n;
5614 }
5615}
5616
5617/// !!! WARNING: RUST-ONLY HELPER !!!
5618///
5619/// There is NO `approx_match_exactly()` in `Src/pattern.c`. It is the
5620/// `case P_EXACTLY:` arm of `patmatch()` (c:2742-2784) together with the
5621/// shared approximation block it falls into on a mismatch (c:3472-3576),
5622/// for a program carrying an `(#aN)` budget. C keeps both inline and
5623/// communicates the half-matched literal through the file-statics
5624/// `exactpos`/`exactend`, retrying the SAME node with `patmatch(scan)`;
5625/// the Rust matcher threads its state through `&mut rpat`, so the retry
5626/// is a recursive call with the literal offset `p` standing in for
5627/// `exactpos`. Nothing outside the P_EXACTLY arm calls it.
5628///
5629/// Order is C's, first success wins: characters that match are consumed
5630/// with no alternative tried (c:2755-2778); at the first mismatch, one
5631/// error buys, in turn, (1) omitting an input character, (2) swapping
5632/// two characters, (3) omitting a character from both, (4) omitting a
5633/// pattern character (c:3477-3575). A fully matched literal hands off to
5634/// `next` without backtracking into the literal. The unit is a
5635/// CHARACTER under GF_MULTIBYTE (CHARREFINC / CHARINC, c:2770-2771), a
5636/// byte otherwise. Returns the end offset of the whole match.
5637fn approx_match_exactly(
5638 code: &[u8],
5639 next: usize,
5640 string: &str,
5641 s_off: usize,
5642 str_bytes: &[u8],
5643 state: &mut rpat,
5644 glob_flags: i32,
5645 max_errs: i32,
5646) -> Option<usize> {
5647 // CHARREFINC (c:1969) — the character at `off` and its byte length.
5648 let char_at = |b: &[u8], off: usize| -> (u32, usize) {
5649 if (glob_flags & GF_MULTIBYTE) != 0 {
5650 let want = match b[off] {
5651 0x00..=0x7f => 1,
5652 0xc0..=0xdf => 2,
5653 0xe0..=0xef => 3,
5654 0xf0..=0xf7 => 4,
5655 _ => 1,
5656 };
5657 if let Some(c) = b
5658 .get(off..off + want)
5659 .and_then(|s| std::str::from_utf8(s).ok())
5660 .and_then(|s| s.chars().next())
5661 {
5662 return (c as u32, want);
5663 }
5664 }
5665 (b[off] as u32, 1)
5666 };
5667 // c:Src/pattern.c CHARMATCH — case folding under (#i) / (#l).
5668 let charmatch = |chin: u32, chpa: u32| -> bool {
5669 let upper = |c: u32| -> u32 {
5670 char::from_u32(c)
5671 .filter(|ch| ch.is_lowercase())
5672 .and_then(|ch| ch.to_uppercase().next())
5673 .map_or(c, |u| u as u32)
5674 };
5675 let is_lower = |c: u32| char::from_u32(c).is_some_and(|ch| ch.is_lowercase());
5676 chin == chpa
5677 || if (glob_flags & GF_IGNCASE) != 0 {
5678 upper(chin) == upper(chpa)
5679 } else if (glob_flags & GF_LCMATCHUC) != 0 {
5680 is_lower(chpa) && upper(chpa) == chin
5681 } else {
5682 false
5683 }
5684 };
5685 fn walk(
5686 code: &[u8],
5687 next: usize,
5688 string: &str,
5689 str_bytes: &[u8],
5690 mut s: usize,
5691 mut p: usize,
5692 state: &mut rpat,
5693 glob_flags: i32,
5694 max_errs: i32,
5695 char_at: &dyn Fn(&[u8], usize) -> (u32, usize),
5696 charmatch: &dyn Fn(u32, u32) -> bool,
5697 ) -> Option<usize> {
5698 let input = string.as_bytes();
5699 loop {
5700 if p >= str_bytes.len() {
5701 // c:2779-2784 — the literal is used up: on to `next`.
5702 return if next == 0 {
5703 Some(s)
5704 } else {
5705 patmatch(code, next, string, s, state, glob_flags)
5706 };
5707 }
5708 let (chpa, plen) = char_at(str_bytes, p);
5709 if s < input.len() {
5710 let (chin, ilen) = char_at(input, s);
5711 if charmatch(chin, chpa) {
5712 // c:2770-2777 — matched: consume, no alternative.
5713 s += ilen;
5714 p += plen;
5715 continue;
5716 }
5717 }
5718 // c:3473-3474 — a mismatch spends an error, if any are left.
5719 let fe = forceerrs.load(Ordering::Relaxed);
5720 if !(state.errsfound < max_errs && (fe == -1 || state.errsfound < fe)) {
5721 return None; // c:3578-3579
5722 }
5723 state.errsfound += 1; // c:3497 `saverrsfound = ++errsfound`
5724 let saved = state.clone();
5725 if s < input.len() {
5726 let (chin0, ilen0) = char_at(input, s);
5727 // c:3503-3514 — (1) omit a character from the input.
5728 if let Some(r) = walk(
5729 code, next, string, str_bytes, s + ilen0, p, state, glob_flags, max_errs,
5730 char_at, charmatch,
5731 ) {
5732 return Some(r);
5733 }
5734 *state = saved.clone(); // c:3524-3526
5735 let nextin = s + ilen0;
5736 let nextexact = p + plen;
5737 // c:3532-3551 — (2) transpose two characters.
5738 if nextin < input.len() && nextexact < str_bytes.len() {
5739 let (chin1, ilen1) = char_at(input, nextin);
5740 let (chpa1, plen1) = char_at(str_bytes, nextexact);
5741 if charmatch(chin0, chpa1) && charmatch(chin1, chpa) {
5742 if let Some(r) = walk(
5743 code, next, string, str_bytes, nextin + ilen1, nextexact + plen1,
5744 state, glob_flags, max_errs, char_at, charmatch,
5745 ) {
5746 return Some(r);
5747 }
5748 *state = saved.clone(); // c:3549-3550
5749 }
5750 }
5751 // c:3553-3565 — (3) move up both strings.
5752 if let Some(r) = walk(
5753 code, next, string, str_bytes, nextin, nextexact, state, glob_flags,
5754 max_errs, char_at, charmatch,
5755 ) {
5756 return Some(r);
5757 }
5758 *state = saved; // c:3562-3565
5759 }
5760 // c:3567-3575 — (4) move up the pattern only; the last
5761 // attempt, so C loops instead of recursing.
5762 p += plen;
5763 }
5764 }
5765 walk(
5766 code, next, string, str_bytes, s_off, 0, state, glob_flags, max_errs, &char_at,
5767 &charmatch,
5768 )
5769}
5770
5771thread_local! {
5772 /// Rust-only backstop counter (no C analogue) — gates `patmatch`
5773 /// recursion at PATMATCH_MAX_DEPTH so misbehaving closures convert
5774 /// would-be stack overflows into clean None returns. Documented in
5775 /// `fake_fn_allowlist.txt` under the patmatch arc.
5776 static PATMATCH_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
5777}
5778
5779/// Maximum patmatch recursion depth — Rust-only backstop, no C
5780/// analogue. The C source's primary protection against
5781/// closure-of-closure infinite recursion is the P_WBRANCH "must
5782/// match at least 1 char" semantics described at pattern.c:108-144:
5783/// `P_WBRANCH: This works like a branch and is used in complex`
5784/// `closures, but the match must be at least 1 char in length to`
5785/// `avoid infinite loops.`
5786/// (Implemented in C's patmatch() switch at c:3044, `case P_WBRANCH`,
5787/// via the `errsfound`/`forceerrs` accounting at c:1059+.) The Rust
5788/// port's P_WBRANCH arm has gaps in that propagation, so we keep
5789/// this depth guard as a safety net.
5790///
5791/// **Tuned to test-thread stack size**, NOT the main-thread 8 MB.
5792/// Rust's `cargo test` spawns each test on a thread whose default
5793/// stack is ~2 MB (per `std::thread::Builder::stack_size` default at
5794/// `library/std/src/thread/mod.rs`), and `(fo#)#` patterns blow that
5795/// at ~250-300 frames per measured SIGABRT. 128 leaves headroom
5796/// (~256 KB worst case at ~2 KB/frame) while still admitting any
5797/// legitimate zsh pattern (Misc/globtests tops out around 30).
5798const PATMATCH_MAX_DEPTH: u32 = 128;
5799
5800thread_local! {
5801 // c:Src/pattern.c — the P_EXCLUDE `syncptr->p` heap buffer. Keyed by
5802 // the EXCLUDE node offset; value is a per-input-position byte array
5803 // recording where the asserted branch's excludable part matched (so
5804 // EXCSYNC can fail a revisit and force backtracking to a different
5805 // split). C stores this in the bytecode payload + a raw heap pointer
5806 // that survives backtracking; the Rust matcher clones `rpat` per
5807 // branch, so the buffer lives here (outside the cloned state) to
5808 // stay shared across recursion, exactly like C's global syncstrp->p.
5809 static EXCSYNC_BUF: std::cell::RefCell<std::collections::HashMap<usize, Vec<u8>>> =
5810 std::cell::RefCell::new(std::collections::HashMap::new());
5811}
5812
5813/// Port of `patmatch()` from `Src/pattern.c:2694`. The bytecode
5814/// interpreter — `static int patmatch(Upat prog)`.
5815///
5816/// Returns `Some(end_pos)` on a successful match (`end_pos` = byte
5817/// offset in `string` where the match ended), `None` on no-match; C
5818/// returns 1/0 and leaves the end position in the file-static
5819/// `patinput`.
5820///
5821/// Rust signature differs from C's `int patmatch(Upat prog)`: the
5822/// bytecode, the current input position, the captures and the glob
5823/// flags are threaded through arguments rather than C's per-thread
5824/// file-statics (`patinput`, `patinstart`, `patbeginp`/`patendp`,
5825/// `patglobflags`). Rule S1 deviation justified by zshrs's threading
5826/// model — see PORT_PLAN.md Bucket 1.
5827pub fn patmatch(
5828 code: &[u8],
5829 prog_off: usize,
5830 string: &str,
5831 string_off: usize,
5832 state: &mut rpat,
5833 glob_flags: i32,
5834) -> Option<usize> {
5835 // c:2694
5836 // Depth-guard: convert what would be a stack overflow into a
5837 // clean None return (= no match). See PATMATCH_MAX_DEPTH doc.
5838 let d = PATMATCH_DEPTH.with(|c| {
5839 let cur = c.get();
5840 c.set(cur + 1);
5841 cur + 1
5842 });
5843 if d > PATMATCH_MAX_DEPTH {
5844 PATMATCH_DEPTH.with(|c| c.set(c.get() - 1));
5845 return None;
5846 }
5847 struct DepthGuard;
5848 impl Drop for DepthGuard {
5849 fn drop(&mut self) {
5850 PATMATCH_DEPTH.with(|c| c.set(c.get().saturating_sub(1)));
5851 }
5852 }
5853 let _depth_guard = DepthGuard;
5854
5855 let mut scan = prog_off;
5856 let mut s_off = string_off;
5857 // Locally-mutable copy of glob_flags so mid-pattern P_GFLAGS can
5858 // toggle bits without affecting the caller's branch view.
5859 let mut glob_flags = glob_flags;
5860
5861 // Inlined port of CHARMATCH macro from `Src/pattern.c:2671-2677`:
5862 // #define CHARMATCH(chin, chpa) (chin == chpa || \
5863 // ((patglobflags & GF_IGNCASE) ?
5864 // ((ISUPPER(chin) ? TOLOWER(chin) : chin) ==
5865 // (ISUPPER(chpa) ? TOLOWER(chpa) : chpa)) :
5866 // (patglobflags & GF_LCMATCHUC) ?
5867 // (ISLOWER(chpa) && TOUPPER(chpa) == chin) : 0))
5868 //
5869 // - exact byte match always wins
5870 // - GF_IGNCASE: SYMMETRIC fold (both lowercase before compare)
5871 // - GF_LCMATCHUC: ASYMMETRIC — lowercase pattern char ALSO matches
5872 // uppercase text char; an UPPERCASE pattern char only matches
5873 // that exact uppercase text char. This is the `(#l)` flag's
5874 // "lowercase-in-pattern matches uppercase-in-text" semantic;
5875 // previously zshrs treated LCMATCHUC the same as IGNCASE (full
5876 // case-fold both sides) so `(#l)FOO` wrongly matched "foo".
5877 let charmatch = |chin: u8, chpa: u8, flags: i32| -> bool {
5878 if chin == chpa {
5879 return true; // c:2671
5880 }
5881 if (flags & GF_IGNCASE) != 0 {
5882 // c:2672-2674
5883 let a = if chin.is_ascii_uppercase() {
5884 chin.to_ascii_lowercase()
5885 } else {
5886 chin
5887 };
5888 let b = if chpa.is_ascii_uppercase() {
5889 chpa.to_ascii_lowercase()
5890 } else {
5891 chpa
5892 };
5893 return a == b;
5894 }
5895 if (flags & GF_LCMATCHUC) != 0 {
5896 // c:2675-2676
5897 return chpa.is_ascii_lowercase() && chpa.to_ascii_uppercase() == chin;
5898 }
5899 false // c:2677
5900 };
5901
5902 // Byte advance past ONE *input* character at `off` — C's `CHARINC`
5903 // macro (c:1963 `#define CHARINC(x, y) ((x) = charnext((x), (y)))`),
5904 // whose `charnext` body is c:1940-1960.
5905 //
5906 // The unit is option-dependent, and that is the whole point:
5907 // c:1946 `if (!(patglobflags & GF_MULTIBYTE) || !((unsigned char) *x
5908 // & 0x80)) return x + 1;` — with GF_MULTIBYTE clear (`unsetopt
5909 // multibyte`) a character is ONE RAW BYTE, so `?` matches a byte and
5910 // `*` backtracks over bytes. Only with GF_MULTIBYTE set does C decode
5911 // a whole multibyte character (c:1949 `mbrtowc`), falling back to a
5912 // single byte on MB_INVALID/MB_INCOMPLETE (c:1951-1955).
5913 //
5914 // C matches over an UNMETAFIED buffer (`patallocstr`), whereas zshrs
5915 // keeps non-UTF-8 input metafied inside the `&str` — Meta `\u{83}`
5916 // followed by `byte ^ 32`. One metafied pair stands for ONE raw byte,
5917 // so it is consumed as a unit in both modes; under GF_MULTIBYTE
5918 // consecutive pairs are accumulated until they form one valid UTF-8
5919 // character, which is what C's `mbrtowc` over the raw bytes does.
5920 //
5921 // Returns 0 only at end of input (C's `patinput == patinend`, c:2737).
5922 let charinc = |off: usize, gflags: i32| -> usize {
5923 if off >= string.len() {
5924 return 0; // c:2737 — no character left.
5925 }
5926 let multibyte = (gflags & GF_MULTIBYTE) != 0; // c:1946
5927 // A non-boundary offset is only reachable while stepping bytes
5928 // with GF_MULTIBYTE clear; C's raw-byte view advances one byte.
5929 let Some(rest) = string.get(off..) else {
5930 return 1; // c:1947
5931 };
5932 let mut raw: Vec<u8> = Vec::new();
5933 let mut steps: Vec<usize> = Vec::new();
5934 let mut advance = 0usize;
5935 let mut chs = rest.chars();
5936 while let Some(c) = chs.next() {
5937 if c == '\u{83}' {
5938 if let Some(n) = chs.clone().next() {
5939 if (n as u32) >= 0x80 {
5940 raw.push((n as u32 as u8) ^ 32);
5941 steps.push(c.len_utf8() + n.len_utf8());
5942 chs.next();
5943 // c:1946-1947 — one raw byte IS one character
5944 // without GF_MULTIBYTE.
5945 if !multibyte {
5946 advance += steps[0];
5947 break;
5948 }
5949 // c:1949 — `ret = mbrtowc(&wc, x, y-x, &shiftstate)`.
5950 // LOCALE-driven, which is the point: `zh_CN.GB2312`
5951 // and `ja_JP.eucJP` are multibyte codesets that are
5952 // not UTF-8. Collecting bytes "until they form valid
5953 // UTF-8" also never terminated for input that is not
5954 // UTF-8 at all, so `$'\xc4\xe3'` was consumed as ONE
5955 // character and matched a single `?` where zsh — which
5956 // treats each undecodable byte as its own character —
5957 // matches `??`.
5958 let mut wc: libc::wchar_t = 0;
5959 let mut mbs = crate::ported::utils::MBSTATE_ZERO;
5960 let ret = unsafe {
5961 crate::ported::utils::mbrtowc(
5962 &mut wc,
5963 raw.as_ptr() as *const libc::c_char,
5964 raw.len(),
5965 &mut mbs as *mut crate::ported::utils::MbStateBuf
5966 as *mut libc::c_void,
5967 )
5968 };
5969 if ret == crate::ported::utils::MB_INCOMPLETE
5970 && raw.len() < crate::ported::zsh_h::MB_CUR_MAX
5971 {
5972 // C hands `mbrtowc` the whole remaining buffer at
5973 // once; the port feeds it the metafied pairs as it
5974 // decodes them, so a still-incomplete character
5975 // means "collect the next pair".
5976 continue;
5977 }
5978 advance += if ret == crate::ported::utils::MB_INVALID
5979 || ret == crate::ported::utils::MB_INCOMPLETE
5980 || ret == 0
5981 {
5982 // c:1951-1955 — "Error. Treat as single byte."
5983 // c:1959 — `x + (ret ? ret : 1)` for the NUL case.
5984 steps[0]
5985 } else {
5986 steps[..ret.min(steps.len())].iter().sum()
5987 };
5988 break;
5989 }
5990 }
5991 // Lone Meta — count it as one character.
5992 advance += c.len_utf8();
5993 break;
5994 }
5995 if !steps.is_empty() {
5996 // A metafied run was still assembling a character when a
5997 // natively stored one turned up, so the run cannot complete.
5998 // C reaches the same answer without noticing the boundary:
5999 // `mbrtowc` over the remaining buffer fails and it returns
6000 // `x + 1` (c:1951-1955). Leaving `advance` at 0 here dropped
6001 // the pending pair entirely, so `$'a\xc4\xe3b'` matched no
6002 // `????` even though `${#}` counted its 4 characters.
6003 advance = steps[0];
6004 break;
6005 }
6006 // Natively stored character: its `&str` bytes ARE the raw
6007 // bytes C would see, so without GF_MULTIBYTE only the first
6008 // one is consumed (c:1947).
6009 advance += if !multibyte && (c as u32) >= 0x80 {
6010 1
6011 } else {
6012 c.len_utf8()
6013 };
6014 break;
6015 }
6016 if advance == 0 && !steps.is_empty() {
6017 // Input ran out while the collected bytes were still an
6018 // incomplete character. C never sees this as a separate case —
6019 // it hands `mbrtowc` the whole remaining buffer, gets
6020 // MB_INCOMPLETE and returns `x + 1` (c:1951-1955) — but the port
6021 // discovers it by exhausting the metafied pairs, and leaving
6022 // `advance` at 0 made `P_ANY` read it as "no character left"
6023 // (c:2737). A trailing `$'\xe3'` then matched no `?` at all.
6024 advance = steps[0];
6025 }
6026 advance
6027 };
6028
6029 // Membership test for a P_ANYOF/P_ANYBUT body at `body_off` against the
6030 // input char at byte offset `off`. Returns (in_set, byte_advance). ASCII
6031 // input takes the unchanged byte `charmatch` path over the `chars` set; a
6032 // multibyte char is decoded (raw UTF-8, or a metafied `$'\xNN'` Meta-pair)
6033 // and tested against the appended class mask / wide ranges / wide literals
6034 // — C's `mb_patmatchrange` equivalent (pattern.c:3610).
6035 let anyof_membership = |body_off: usize, off: usize, gflags: i32| -> (bool, usize) {
6036 let range_flags = gflags & !(GF_IGNCASE | GF_LCMATCHUC);
6037 let chars_len =
6038 u32::from_le_bytes(code[body_off + 4..body_off + 8].try_into().unwrap()) as usize;
6039 let cs = body_off + 8;
6040 let set = &code[cs..cs + chars_len];
6041 let mut p = cs + chars_len;
6042 let classmask = u32::from_le_bytes(code[p..p + 4].try_into().unwrap());
6043 p += 4;
6044 let n_mbc = u32::from_le_bytes(code[p..p + 4].try_into().unwrap()) as usize;
6045 p += 4;
6046 let mbc_start = p;
6047 p += n_mbc * 4;
6048 let n_mbr = u32::from_le_bytes(code[p..p + 4].try_into().unwrap()) as usize;
6049 p += 4;
6050 let mbr_start = p;
6051
6052 let bytes = string.as_bytes();
6053 // c:1974-1975 — without GF_MULTIBYTE the candidate is one RAW byte.
6054 // A metafied pair (Meta U+0083 + byte^32) is that byte; take it and
6055 // its whole span through charrefinc rather than the pair's first
6056 // UTF-8 byte.
6057 let (b, badv) = {
6058 let mut p = off;
6059 let mut z = 0;
6060 match (gflags & GF_MULTIBYTE == 0 && string.is_char_boundary(off))
6061 .then(|| charrefinc(string, &mut p, gflags, &mut z))
6062 .flatten()
6063 {
6064 Some(r) if bytes[off] == 0xc2 && p - off > '\u{83}'.len_utf8() => (r as u8, p - off),
6065 _ => (bytes[off], 1),
6066 }
6067 };
6068 // ASCII input, OR `unsetopt multibyte`: byte-level match (the prior
6069 // behaviour). C only takes the wide `mb_patmatchrange` path under
6070 // GF_MULTIBYTE; with it clear a high byte is matched as a raw byte.
6071 //
6072 // `off` can also land inside a multibyte char: the approx omit-input
6073 // paths step `s_off + 1`, so a continuation P_ANYOF/P_ANYBUT can be
6074 // tested at a continuation byte. Decoding `string[off..]` there would
6075 // panic, so treat a non-boundary position as a raw byte (advance 1) —
6076 // the same fallback the byte-level machinery already relies on.
6077 // c:3702-3719 — PP_IDENT / PP_IFS / PP_IFSSPACE / PP_WORD are
6078 // resolved against the LIVE ztype table (`$IFS`, `$WORDCHARS`,
6079 // POSIX_IDENTIFIERS) every time a character is tested, never
6080 // frozen into the compiled program.
6081 let live_class_hit = |ch: char| -> bool {
6082 use crate::ported::ztype_h::{iwsep, IIDENT, ISEP, IWORD};
6083 if (classmask & (1 << 10)) != 0 && crate::ported::utils::wcsitype(ch, IIDENT as u32) {
6084 return true; // c:3702-3706
6085 }
6086 if (classmask & (1 << 11)) != 0 && crate::ported::utils::wcsitype(ch, ISEP as u32) {
6087 return true; // c:3707-3710
6088 }
6089 // c:3711-3715 — "must be ASCII space character":
6090 // `if (ch < 128 && iwsep((int)ch))`
6091 if (classmask & (1 << 12)) != 0 && (ch as u32) < 128 && iwsep(ch as u8) {
6092 return true;
6093 }
6094 if (classmask & (1 << 13)) != 0 && crate::ported::utils::wcsitype(ch, IWORD as u32) {
6095 return true; // c:3716-3719
6096 }
6097 false
6098 };
6099 if b < 0x80 || (gflags & GF_MULTIBYTE) == 0 || !string.is_char_boundary(off) {
6100 if set.iter().any(|&c| charmatch(b, c, range_flags)) {
6101 return (true, badv);
6102 }
6103 if b < 0x80 && live_class_hit(b as char) {
6104 return (true, badv);
6105 }
6106 // c:2800 — without GF_MULTIBYTE the input char is a single BYTE
6107 // (`charref`, c:1919-1920) and `patmatchrange` walks the operand's
6108 // RAW bytes, so a bracket member written as a multibyte character
6109 // matches each of its UTF-8 bytes individually. C keeps those
6110 // bytes in the operand; zshrs splits them out of `chars` into the
6111 // wide literal / wide range tables at compile time, so re-encode
6112 // them here. Without this, `unsetopt multibyte; [[ $'αβ' = [α]* ]]`
6113 // stopped matching once GF_MULTIBYTE became option-gated.
6114 if (gflags & GF_MULTIBYTE) == 0 {
6115 let mut utf8 = [0u8; 4];
6116 let mut member_byte = |cp: u32| -> bool {
6117 char::from_u32(cp).is_some_and(|c| {
6118 c.encode_utf8(&mut utf8).as_bytes().iter().any(|&x| x == b)
6119 })
6120 };
6121 for k in 0..n_mbc {
6122 let o = mbc_start + k * 4;
6123 if member_byte(u32::from_le_bytes(code[o..o + 4].try_into().unwrap())) {
6124 return (true, badv);
6125 }
6126 }
6127 for k in 0..n_mbr {
6128 let o = mbr_start + k * 8;
6129 if member_byte(u32::from_le_bytes(code[o..o + 4].try_into().unwrap()))
6130 || member_byte(u32::from_le_bytes(code[o + 4..o + 8].try_into().unwrap()))
6131 {
6132 return (true, badv);
6133 }
6134 }
6135 }
6136 return (false, badv);
6137 }
6138 // Decode one logical input char + its source byte span.
6139 // c:3612 `mb_patmatchrange(range, ch, …)` receives the character C's
6140 // CHARREFINC decoded (c:1969-1990): a metafied run is `mbrtowc`'d as
6141 // the raw bytes it stands for, and an undecodable byte is
6142 // `WCHAR_INVALID` (0xDC00 + byte, c:241-242) — which no range member
6143 // equals except another invalid byte (c:3634 `metacharinc(&range) ==
6144 // ch`, metacharinc answering WCHAR_INVALID the same way, c:384).
6145 // Decoding the pair to the Latin-1 char `byte` instead made `\xcc`
6146 // match `[\ucc]` and no `[\xcc]` at all.
6147 let mut adv_pos = off;
6148 let mut zmb = 0;
6149 let Some(cp) = charrefinc(string, &mut adv_pos, gflags, &mut zmb) else {
6150 return (false, 1);
6151 };
6152 let adv = adv_pos - off;
6153 // Class tests take the decoded character (unused when `zmb` is set).
6154 let ch = char::from_u32(cp).unwrap_or('\u{FFFD}');
6155 // WCHAR_INVALID is in no `iswXXX` class.
6156 let class_hit = (zmb == 0
6157 && ((classmask & (1 << 0) != 0 && ch.is_alphabetic())
6158 || (classmask & (1 << 1) != 0 && ch.is_alphanumeric())
6159 || (classmask & (1 << 2) != 0 && ch.is_uppercase())
6160 || (classmask & (1 << 3) != 0 && ch.is_lowercase())
6161 || (classmask & (1 << 4) != 0 && ch.is_whitespace())
6162 || (classmask & (1 << 5) != 0 && (ch == ' ' || ch == '\t'))
6163 || (classmask & (1 << 6) != 0
6164 && !ch.is_alphanumeric()
6165 && !ch.is_whitespace()
6166 && !ch.is_control())
6167 || (classmask & (1 << 7) != 0 && ch.is_control())
6168 || (classmask & (1 << 8) != 0 && !ch.is_control())
6169 || (classmask & (1 << 9) != 0 && !ch.is_control() && !ch.is_whitespace())
6170 || live_class_hit(ch)))
6171 // c:3737-3744 — `case PP_INCOMPLETE: if (zmb_ind ==
6172 // ZMB_INCOMPLETE) return 1;` / `case PP_INVALID: if (zmb_ind ==
6173 // ZMB_INVALID) return 1;`. C's caller hands mb_patmatchrange the
6174 // decode verdict for the candidate character; zshrs keeps an
6175 // undecodable input byte METAFIED inside a Rust `String`
6176 // (Meta + byte^32), so recover the raw byte run here and ask
6177 // std's UTF-8 decoder for the same three-way answer:
6178 // Ok / error past offset 0 -> ZMB_VALID
6179 // Err, error_len() == None -> ZMB_INCOMPLETE (truncated)
6180 // Err, error_len() == Some(_) -> ZMB_INVALID
6181 || ((classmask & ((1 << 14) | (1 << 15))) != 0 && {
6182 // Rebuild up to MB_CUR_MAX raw bytes starting at `off`.
6183 let mut raw: Vec<u8> = Vec::new();
6184 let mut rit = string[off..].chars();
6185 while raw.len() < 4 {
6186 match rit.next() {
6187 Some('\u{83}') => match rit.next() {
6188 Some(n) => raw.push(((n as u32) as u8) ^ 0x20),
6189 None => break,
6190 },
6191 Some(c) => {
6192 let mut b = [0u8; 4];
6193 raw.extend_from_slice(c.encode_utf8(&mut b).as_bytes());
6194 }
6195 None => break,
6196 }
6197 }
6198 match std::str::from_utf8(&raw) {
6199 Err(e) if e.valid_up_to() == 0 => {
6200 if e.error_len().is_none() {
6201 (classmask & (1 << 14)) != 0 // c:3737-3740
6202 } else {
6203 (classmask & (1 << 15)) != 0 // c:3741-3744
6204 }
6205 }
6206 _ => false,
6207 }
6208 });
6209 if class_hit {
6210 return (true, adv);
6211 }
6212 for k in 0..n_mbc {
6213 let o = mbc_start + k * 4;
6214 if u32::from_le_bytes(code[o..o + 4].try_into().unwrap()) == cp {
6215 return (true, adv);
6216 }
6217 }
6218 for k in 0..n_mbr {
6219 let o = mbr_start + k * 8;
6220 let lo = u32::from_le_bytes(code[o..o + 4].try_into().unwrap());
6221 let hi = u32::from_le_bytes(code[o + 4..o + 8].try_into().unwrap());
6222 if cp >= lo && cp <= hi {
6223 return (true, adv);
6224 }
6225 }
6226 (false, adv)
6227 };
6228
6229 while scan < code.len() {
6230 let op = code[scan + I_OP];
6231 let next_bytes: [u8; 4] = code[scan + I_NEXT..scan + I_NEXT + 4].try_into().unwrap();
6232 let next = u32::from_le_bytes(next_bytes) as usize;
6233
6234 // c:2731-2733 — `if (!globdots && P_NOTDOT(scan) &&
6235 // patinput == patinstart && patinput < patinend &&
6236 // *patinput == '.') return 0;`
6237 //
6238 // `P_NOTDOT(p)` is `p->l & 0x40` (c:202), i.e. the opcode is one of
6239 // P_ANY/P_ANYOF/P_ANYBUT/P_STAR/P_NUMRNG/P_NUMFROM/P_NUMTO/P_NUMANY
6240 // (c:117-124, "numbered so we can test bit 6 so as not to match
6241 // initial '.'"). P_EXACTLY (0x03), P_BRANCH (0x20) and P_GFLAGS
6242 // (0x08) do not carry the bit, which is why `.hidden`, `(.)hidden`
6243 // and `(#i).HIDDEN` DO match a dot file while `*`, `?` and `[.]`
6244 // do not. P_OPEN/P_CLOSE are 0x80/0x90 and are likewise unaffected.
6245 //
6246 // `patinput == patinstart` is `s_off == 0`: this port passes the
6247 // whole subject to every recursive patmatch and moves the offset.
6248 //
6249 // C's second gate at c:3322 tests P_OPERAND of a P_ONEHASH /
6250 // P_TWOHASH and is a SEPARATE site, ported in that arm below. It is
6251 // not redundant with this one: reaching it through the arm's
6252 // recursion only fails the operand, and a P_ONEHASH with min = 0
6253 // (c:3328) treats that as zero repetitions and carries on. C returns
6254 // 0 for the whole match.
6255 // Conjunct order differs from C's only to keep the atomic load off
6256 // the hot path: `s_off == 0` is false for every node after the first
6257 // character is consumed, and C's `globdots` is a plain int. No
6258 // conjunct has a side effect, so the predicate is the same one.
6259 if s_off == 0
6260 && (op & 0x40) != 0
6261 && s_off < string.len()
6262 && string.as_bytes()[s_off] == b'.'
6263 && globdots.load(Ordering::Relaxed) == 0
6264 {
6265 return None; // c:2733
6266 }
6267
6268 match op {
6269 P_END => {
6270 // c:Src/pattern.c:3451-3454 — `case P_END: if (!(fail =
6271 // (patinput < patinend && !(patflags & PAT_NOANCH))))
6272 // return 1; break;`. When the WHOLE pattern is consumed
6273 // but the string ISN'T (anchored full match), this
6274 // branch FAILS so an enclosing alternation backtracks
6275 // to the next alternative — `[[ yes == (y|yes) ]]`
6276 // tries `y` (consumes 1 char, reaches P_END at offset
6277 // 1 ≠ 3), fails here, then tries `yes`. The previous
6278 // unconditional `Some(s_off)` committed to the first
6279 // (prefix) alternative and the external anchor check
6280 // rejected it without ever trying `yes`. PAT_NOANCH /
6281 // PAT_NOTEND callers (prefix/suffix strip, `(#e)`
6282 // interior matches) keep the partial-success return.
6283 let pf = patflags.load(Ordering::Relaxed);
6284 let anchored = (pf & (PAT_NOANCH | PAT_NOTEND) as i32) == 0;
6285 if s_off < string.len() && anchored {
6286 // c:Src/pattern.c:3451-3454 sets `fail = 1` here, then the
6287 // shared approximate-match block at c:3463-3475 deletes the
6288 // trailing input one CHARACTER at a time — `++errsfound;
6289 // CHARINC(patinput); continue;` — retrying P_END until the
6290 // input is gone or the `(#aN)` budget is spent. So
6291 // `[[ ab = (#a1)? ]]` matches: `?` consumes `a`, then the
6292 // leftover `b` is deleted for one error.
6293 //
6294 // Only literal (P_EXACTLY) runs did this before, inside
6295 // approx_match_exactly's own trailing-delete; a pattern
6296 // ending in a class / `?` / `*` with input left reached
6297 // here and simply failed, so `(#a2)[^0-9]` on `abc`
6298 // (match `a`, delete `bc`) was rejected where zsh accepts
6299 // it. The class/`?` arms already delete on a FAILED match
6300 // (e.g. c:5046); this is the same edit at the pattern end.
6301 let max_errs = (glob_flags & 0xff) as i32;
6302 if (state.errsfound < max_errs && { let fe = forceerrs.load(Ordering::Relaxed); fe == -1 || state.errsfound < fe })/* c:3473-3474 */ {
6303 // c:3475 `CHARINC(patinput, patinend)` + continue
6304 // (retry P_END). CHARINC is option-gated, and
6305 // `s_off` can sit mid-character when GF_MULTIBYTE
6306 // is clear, so go through `charinc` rather
6307 // than slicing `string` (which would panic).
6308 let advance = charinc(s_off, glob_flags);
6309 if advance != 0 {
6310 state.errsfound += 1; // c:3466 ++errsfound
6311 return patmatch(
6312 code,
6313 scan,
6314 string,
6315 s_off + advance,
6316 state,
6317 glob_flags,
6318 );
6319 }
6320 }
6321 return None; // c:3452 fail, no budget → caller tries next alt
6322 }
6323 return Some(s_off); // c:3453
6324 }
6325 P_NOTHING => { /* empty match, just continue */ }
6326 P_BACK => { /* zero-width, walk back via next */ }
6327 P_EXCEND => {
6328 // c:Src/pattern.c:3037-3047 — terminal node ending an
6329 // exclusion operand: the exclusion matches iff the
6330 // (truncated) excludable span was fully consumed
6331 // (`patinput >= patinend`). Returns success here without
6332 // following the chain (which would wrongly continue into
6333 // the post-group pattern). The caller truncates the span
6334 // so `string.len()` IS the excludable end.
6335 if s_off >= string.len() {
6336 return Some(s_off);
6337 }
6338 return None;
6339 }
6340 P_EXCSYNC => {
6341 // c:Src/pattern.c:2992-3035 — record where the asserted
6342 // branch reached the EXCLUDE sync point (the end of the
6343 // excludable part) in the following EXCLUDE node's sync
6344 // buffer. If this position was already recorded (with
6345 // <= the current error count), fail so the asserted
6346 // branch backtracks to a different split. The EXCLUDE
6347 // node sits PHYSICALLY right after EXCSYNC (both
6348 // patcompnot and the `~` arm emit them adjacent).
6349 let exclude_node = scan + I_BODY;
6350 let already = EXCSYNC_BUF.with(|b| {
6351 let mut m = b.borrow_mut();
6352 if let Some(buf) = m.get_mut(&exclude_node) {
6353 if s_off < buf.len() {
6354 let cur = (state.errsfound + 1) as u8;
6355 if buf[s_off] != 0 && (state.errsfound + 1) >= buf[s_off] as i32 {
6356 return true; // c:3008 already matched here → fail
6357 }
6358 buf[s_off] = cur; // c:3017
6359 // c:3033 — earlier marks are now invalid.
6360 for x in buf[..s_off].iter_mut() {
6361 *x = 0;
6362 }
6363 }
6364 }
6365 false
6366 });
6367 if already {
6368 return None;
6369 }
6370 // else fall through to next node
6371 }
6372 P_EXACTLY => {
6373 // c:P_EXACTLY arm
6374 let body = scan + I_BODY;
6375 let len = u32::from_le_bytes(code[body..body + 4].try_into().unwrap()) as usize;
6376 let str_bytes = &code[body + 4..body + 4 + len];
6377 let input_bytes = string.as_bytes();
6378 // `(#aN)` budget — low byte of patglobflags. C uses the
6379 // file-static `patglobflags`; the Rust port carries it
6380 // through `glob_flags` since rpat went bucket-1.
6381 let max_errs = (glob_flags & 0xff) as i32;
6382 if max_errs > 0 {
6383 // c:3473-3579 — approximate match: omit input char,
6384 // transpose, omit both, omit pattern char (see
6385 // approx_match_exactly); `errsfound` is tracked on
6386 // `state` across the recursive patmatch calls.
6387 if let Some(new_off) = approx_match_exactly(
6388 code, next, string, s_off, str_bytes, state, glob_flags, max_errs,
6389 ) {
6390 return Some(new_off);
6391 }
6392 return None;
6393 }
6394 if s_off + len > input_bytes.len() {
6395 return None;
6396 }
6397 let case_flags = glob_flags & (GF_IGNCASE | GF_LCMATCHUC);
6398 let multibyte = (glob_flags & GF_MULTIBYTE) != 0; // c:349 GF_MULTIBYTE
6399 if case_flags != 0 {
6400 let inp_slice = &input_bytes[s_off..s_off + len];
6401 if multibyte && (glob_flags & GF_IGNCASE) != 0 {
6402 // Char-level Unicode case fold for IGNCASE only —
6403 // LCMATCHUC's asymmetric ASCII semantic doesn't
6404 // map cleanly to non-ASCII chars; per C the
6405 // CHARMATCH macro is byte-level anyway (TOLOWER/
6406 // TOUPPER from utils.c work on bytes).
6407 let pat_str = std::str::from_utf8(str_bytes).ok();
6408 let inp_str = std::str::from_utf8(inp_slice).ok();
6409 if let (Some(p), Some(i)) = (pat_str, inp_str) {
6410 let mut pc = p.chars();
6411 let mut ic = i.chars();
6412 loop {
6413 match (pc.next(), ic.next()) {
6414 (None, None) => break,
6415 (Some(_), None) | (None, Some(_)) => return None,
6416 (Some(a), Some(b)) => {
6417 // Equal chars (the common case) and ASCII
6418 // pairs fold without touching the heap.
6419 // The old per-char `to_lowercase()
6420 // .collect::<String>()` pair allocated
6421 // TWICE PER CHARACTER — `(#i)` scans over
6422 // a 566k-entry history (hsmw ^R) burned
6423 // ~4 CPU-minutes in RawVec::reserve.
6424 // Iterator::eq covers the multi-char
6425 // Unicode folds allocation-free.
6426 if a != b
6427 && (if a.is_ascii() && b.is_ascii() {
6428 a.to_ascii_lowercase() != b.to_ascii_lowercase()
6429 } else {
6430 !a.to_lowercase().eq(b.to_lowercase())
6431 })
6432 {
6433 return None;
6434 }
6435 }
6436 }
6437 }
6438 } else {
6439 // Non-UTF-8 input — byte fallback through CHARMATCH.
6440 for k in 0..len {
6441 if !charmatch(inp_slice[k], str_bytes[k], glob_flags) {
6442 return None;
6443 }
6444 }
6445 }
6446 } else {
6447 // c:2694 — per-byte CHARMATCH walk (covers
6448 // GF_IGNCASE byte-mode AND GF_LCMATCHUC asymmetric).
6449 for k in 0..len {
6450 if !charmatch(inp_slice[k], str_bytes[k], glob_flags) {
6451 return None;
6452 }
6453 }
6454 }
6455 } else if &input_bytes[s_off..s_off + len] != str_bytes {
6456 return None;
6457 }
6458 s_off += len;
6459 }
6460 P_ANY => {
6461 // c:2736-2741 — `case P_ANY: if (patinput == patinend)
6462 // fail = 1; else CHARINC(patinput, patinend); break;`.
6463 // `CHARINC` (c:1963) is `charnext`, which advances a full
6464 // multibyte character ONLY under GF_MULTIBYTE and a single
6465 // raw byte otherwise (c:1946). `charinc` carries
6466 // that option gate plus zshrs's metafied-input handling;
6467 // it previously consumed a whole UTF-8 character
6468 // unconditionally, so `[[ $'α' = ?? ]]` failed under
6469 // `unsetopt multibyte` where zsh matches the two bytes.
6470 let advance = charinc(s_off, glob_flags);
6471 if advance == 0 {
6472 return None;
6473 }
6474 s_off += advance;
6475 }
6476 P_ANYOF => {
6477 // c:2780-2800 — a bracket expression is matched by
6478 // `patmatchrange` / `mb_patmatchrange` on the RAW input char.
6479 // Neither consults `patglobflags`, so a bracket NEVER
6480 // case-folds: only C's CHARMATCH macro (c:2671, used by
6481 // P_EXACTLY) honours GF_IGNCASE / GF_LCMATCHUC.
6482 //
6483 // Passing glob_flags through here made `(#i)` fold ranges as
6484 // well as literals, so `[[ FooBar = (#i)[a-z]## ]]` matched
6485 // (zsh: no match) and `[[ F = (#i)[[:lower:]] ]]` matched
6486 // (zsh: no match). Mask the case bits off for the set test.
6487 let body = scan + I_BODY;
6488 let input_bytes = string.as_bytes();
6489 let max_errs = (glob_flags & 0xff) as i32;
6490 let (has_match, adv) = if s_off < input_bytes.len() {
6491 anyof_membership(body, s_off, glob_flags)
6492 } else {
6493 (false, 0)
6494 };
6495 if !has_match {
6496 // c:Src/pattern.c:3463-3505 — approximate-match fail
6497 // handler. For non-P_EXACTLY opcodes, the ONLY
6498 // approximation path is "omit one input char" (skip
6499 // the input byte that didn't match, costing 1 edit,
6500 // then retry the same scan). For P_ANYOF that means
6501 // `[b][b]` against "bob" can match by omitting the
6502 // middle "o" with 1 edit — the `(#a1)[b][b]` pin.
6503 if (state.errsfound < max_errs && { let fe = forceerrs.load(Ordering::Relaxed); fe == -1 || state.errsfound < fe })/* c:3473-3474 */ && s_off < input_bytes.len() {
6504 state.errsfound += 1;
6505 // Retry same scan with one input byte consumed.
6506 return patmatch(code, scan, string, s_off + 1, state, glob_flags);
6507 }
6508 return None;
6509 }
6510 s_off += adv;
6511 }
6512 P_ANYBUT => {
6513 let body = scan + I_BODY;
6514 let input_bytes = string.as_bytes();
6515 let max_errs = (glob_flags & 0xff) as i32;
6516 // c:2781-2800 — the negated bracket shares P_ANYOF's matcher
6517 // (`… ^ (P_OP(scan) == P_ANYOF)`), so it is equally
6518 // case-BLIND (anyof_membership masks the case bits off).
6519 let (in_set, adv) = if s_off < input_bytes.len() {
6520 anyof_membership(body, s_off, glob_flags)
6521 } else {
6522 (true, 0)
6523 };
6524 let has_match = s_off < input_bytes.len() && !in_set;
6525 if !has_match {
6526 if (state.errsfound < max_errs && { let fe = forceerrs.load(Ordering::Relaxed); fe == -1 || state.errsfound < fe })/* c:3473-3474 */ && s_off < input_bytes.len() {
6527 // c:3463 — omit-input approx path (same as P_ANYOF).
6528 state.errsfound += 1;
6529 return patmatch(code, scan, string, s_off + 1, state, glob_flags);
6530 }
6531 return None;
6532 }
6533 s_off += adv;
6534 }
6535 P_STAR => {
6536 // c:3277-3400 — `*` is handled specially "although really
6537 // P_ONEHASH+P_ANY". C walks the input with CHARINC and
6538 // records every CHARACTER start in `charstart[]`
6539 // (c:3310-3315 `for (no = 0; patinput < patinend;
6540 // CHARINC(patinput, patinend)) { charstart[patinput-start]
6541 // = 1; no++; }`), then backtracks by rewinding to the
6542 // previous recorded start (c:3385-3389 `/* find start of
6543 // previous full character */ while (!*--lastcharstart) ...
6544 // patinput = start + (lastcharstart-charstart);`).
6545 //
6546 // Rewinding one BYTE instead left the continuation opcode
6547 // positioned inside a multibyte character: `[[ $'αβγδε' =
6548 // *(#b)(?) ]]` panicked slicing at a UTF-8 continuation
6549 // byte, and any `*`-then-atom pattern could match at a
6550 // non-character position. `charinc` reproduces
6551 // CHARINC exactly, so with GF_MULTIBYTE clear every byte
6552 // is still a valid stop (c:1946) — the byte-stepping
6553 // behaviour zsh has under `unsetopt multibyte`.
6554 let end = string.len();
6555 // c:3310-3315 — record the character starts.
6556 let mut starts: Vec<usize> = Vec::new();
6557 let mut p = s_off;
6558 while p < end {
6559 starts.push(p);
6560 let adv = charinc(p, glob_flags);
6561 if adv == 0 {
6562 break;
6563 }
6564 p += adv;
6565 }
6566 // c:3299-3315 — the greedy end position (`patinput` after
6567 // the walk) is tried first; `min` is 0 for P_STAR
6568 // (c:3328), so the rewind reaches `start` inclusive.
6569 starts.push(end);
6570 for &pos in starts.iter().rev() {
6571 // c:3374-3382 — try the continuation at this position.
6572 let mut sub_state = state.clone();
6573 if let Some(matched) =
6574 patmatch(code, next, string, pos, &mut sub_state, glob_flags)
6575 {
6576 *state = sub_state;
6577 return Some(matched);
6578 }
6579 }
6580 return None; // c:3400
6581 }
6582 P_ONEHASH | P_TWOHASH => {
6583 // c:P_ONEHASH / P_TWOHASH
6584 // The operand (the simple atom being repeated) starts
6585 // at `scan + I_BODY` — that's the byte immediately
6586 // after the quantifier opcode (which has its own
6587 // 5-byte header). The repeated atom occupies the
6588 // bytes from there until `next`.
6589 let operand = scan + I_BODY;
6590 // c:3322-3325 — `if (!globdots && P_NOTDOT(P_OPERAND(scan)) &&
6591 // patinput == patinstart && patinput < patinend &&
6592 // CHARREF(patinput, patinend) == ZWC('.')) return 0;`
6593 //
6594 // This is NOT covered by the c:2731 gate at the loop head, even
6595 // though the repeat below recurses into `operand` and trips it
6596 // there. C ABORTS THE WHOLE MATCH; the loop head only fails the
6597 // operand, which for P_ONEHASH (min = 0, c:3328) is an ordinary
6598 // "zero repetitions" outcome that then hands a still-dot-leading
6599 // subject to the continuation. Measured with `.hidden` present
6600 // and GLOB_DOTS off: `[a]#.hidden` and `?#.hidden` list nothing
6601 // in `zsh -f` 5.9 and listed `.hidden` here. `(x)#.hidden` DOES
6602 // match in both — P_BRANCH (0x20) is not P_NOTDOT — which is
6603 // what pins this to the operand's opcode rather than to closures.
6604 if s_off == 0
6605 && code
6606 .get(operand + I_OP)
6607 .is_some_and(|o| (o & 0x40) != 0)
6608 && s_off < string.len()
6609 && string.as_bytes()[s_off] == b'.'
6610 && globdots.load(Ordering::Relaxed) == 0
6611 {
6612 return None; // c:3325
6613 }
6614 let min = if op == P_TWOHASH { 1 } else { 0 };
6615 // Greedy: match operand repeatedly until it fails,
6616 // then walk back trying continuations.
6617 let mut positions = vec![s_off];
6618 loop {
6619 let cur = *positions.last().unwrap();
6620 let mut sub_state = state.clone();
6621 if let Some(new_pos) =
6622 patmatch(code, operand, string, cur, &mut sub_state, glob_flags)
6623 {
6624 if new_pos == cur {
6625 break;
6626 } // zero-width fixed point
6627 *state = sub_state;
6628 positions.push(new_pos);
6629 } else {
6630 break;
6631 }
6632 }
6633 if positions.len() - 1 < min {
6634 return None;
6635 }
6636 // Walk back from longest match trying continuations.
6637 while positions.len() > min {
6638 let cur = *positions.last().unwrap();
6639 let mut sub_state = state.clone();
6640 if let Some(end) = patmatch(code, next, string, cur, &mut sub_state, glob_flags)
6641 {
6642 *state = sub_state;
6643 return Some(end);
6644 }
6645 if positions.len() <= min + 1 {
6646 return None;
6647 }
6648 positions.pop();
6649 }
6650 return None;
6651 }
6652 P_BRANCH | P_WBRANCH => {
6653 // c:P_BRANCH / P_WBRANCH arm — c:3043-3044 in zsh.
6654 // c:3046-3050 — if next is NOT another BRANCH, this is
6655 // the only alternative; avoid the alt-loop and just
6656 // continue with the operand inline (no recursion, no
6657 // fallthrough on failure).
6658 //
6659 // For P_WBRANCH, the operand sits AFTER the 8-byte
6660 // syncptr payload (`P_OPERAND(p) + 1` per pattern.c:1865).
6661 let operand_off_extra = if op == P_WBRANCH { 8 } else { 0 };
6662 // c:3056 — if `next` is P_EXCLUDE/P_EXCLUDP, this BRANCH
6663 // is the asserted half of a `^pat` / `!(pat)` exclusion.
6664 // Port of c:3056-3201: try the asserted operand
6665 // (the `*`-based branch body), then for each EXCLUDE in
6666 // the next-chain run the exclude operand against the
6667 // same input range; if any exclude matches with the
6668 // SAME consumed length, fail; else succeed.
6669 if next != 0 && next < code.len() && P_ISEXCLUDE(code[next + I_OP]) {
6670 // c:Src/pattern.c:3056-3201 — `^pat` / `(^pat)` /
6671 // `!(pat)` / `A~B` exclusion. The asserted branch
6672 // (`STAR EXCSYNC rest` for `^`/`!`, or `A EXCSYNC
6673 // rest` for `A~B`) is matched normally; its EXCSYNC
6674 // node records where the EXCLUDABLE part ended into
6675 // the EXCLUDE node's sync buffer. We then truncate to
6676 // that synclen and test the exclusion operand(s); if
6677 // any matches the excludable span, this candidate is
6678 // excluded and we re-run the asserted branch — EXCSYNC
6679 // now fails at the recorded position, forcing a
6680 // different split — until an un-excluded split is
6681 // found or the asserted branch can no longer match.
6682 let asserted_operand = scan + I_BODY + operand_off_extra;
6683 // c:2941 — `patglobflags = P_OPERAND(scan)->l;` — in C
6684 // P_GFLAGS writes the GLOBAL flag word, so a flag node
6685 // sitting at the head of the asserted branch is already
6686 // in effect by the time the exclusion operand runs at
6687 // c:3134+ (which only clears the low `(#aN)` byte). The
6688 // Rust port threads the flags as a parameter instead, so
6689 // apply a leading P_GFLAGS here explicitly. Without it
6690 // `readme` vs `(#i)readme~README|readme~README` ran the
6691 // SECOND branch's exclusion case-insensitively and
6692 // wrongly excluded the match.
6693 let branch_flags_eff =
6694 if code.get(asserted_operand + I_OP).copied().unwrap_or(0) == P_GFLAGS {
6695 let b = asserted_operand + I_BODY;
6696 let bits = i32::from_le_bytes(code[b..b + 4].try_into().unwrap());
6697 (glob_flags & !(GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE | 0xff)) | bits
6698 } else {
6699 glob_flags
6700 };
6701 let exclude_node = next;
6702 // Allocate (reset) the sync buffer for this EXCLUDE.
6703 let prev_buf = EXCSYNC_BUF.with(|b| {
6704 b.borrow_mut()
6705 .insert(exclude_node, vec![0u8; string.len() + 1])
6706 });
6707 let mut found: Option<(usize, rpat)> = None;
6708 loop {
6709 let mut a_state = state.clone();
6710 let matchpt = match patmatch(
6711 code,
6712 asserted_operand,
6713 string,
6714 s_off,
6715 &mut a_state,
6716 glob_flags,
6717 ) {
6718 Some(e) => e,
6719 None => break,
6720 };
6721 // c:3128-3130 — synclen = first marked position in
6722 // the sync buffer (the end of the excludable part).
6723 let synclen = EXCSYNC_BUF.with(|b| {
6724 b.borrow()
6725 .get(&exclude_node)
6726 .and_then(|buf| buf.iter().position(|&x| x != 0))
6727 .unwrap_or(s_off)
6728 });
6729 // c:3134-3138 — test each EXCLUDE/EXCLUDP operand
6730 // against the excludable span string[s_off..synclen].
6731 // Truncating to `synclen` makes the operand's
6732 // trailing P_EXCEND succeed iff the span matched in
6733 // full.
6734 let span_end = synclen.min(string.len());
6735 let span = &string[..span_end];
6736 let mut excluded = false;
6737 let mut excl = exclude_node;
6738 // c:3113/3116 — `savglobdots = globdots; globdots = 1;
6739 // /* OK to match . first */`, restored at c:3192. The
6740 // asserted branch has already matched at this point;
6741 // the exclusion operand is matched against what it
6742 // produced, so the leading-dot rule must not apply to
6743 // it a second time.
6744 let savglobdots = globdots.swap(1, Ordering::Relaxed);
6745 while excl != 0 && excl < code.len() && P_ISEXCLUDE(code[excl + I_OP]) {
6746 let excl_operand = excl + I_BODY + 8; // after 8-byte syncptr
6747 let mut e_state = state.clone();
6748 // c:3134-3142 — `patglobflags &= ~0xff;
6749 // errsfound = 0;` — exclusions match EXACTLY,
6750 // with approximation turned off. Clear both the
6751 // running error count AND the budget (low byte
6752 // of glob_flags); otherwise `(#a1)README~READ_ME`
6753 // matched the exclusion READ_ME against READ.ME
6754 // approximately and wrongly excluded it.
6755 e_state.errsfound = 0;
6756 let excl_flags = branch_flags_eff & !0xff;
6757 if let Some(em) =
6758 patmatch(code, excl_operand, span, s_off, &mut e_state, excl_flags)
6759 {
6760 if em == span_end {
6761 excluded = true;
6762 break;
6763 }
6764 }
6765 let nb: [u8; 4] =
6766 code[excl + I_NEXT..excl + I_NEXT + 4].try_into().unwrap();
6767 let n = u32::from_le_bytes(nb) as usize;
6768 if n == 0 || n == excl {
6769 break;
6770 }
6771 excl = n;
6772 }
6773 globdots.store(savglobdots, Ordering::Relaxed); // c:3192
6774 if !excluded {
6775 found = Some((matchpt, a_state));
6776 break;
6777 }
6778 // Excluded: loop. The sync buffer now records this
6779 // split, so the next asserted patmatch's EXCSYNC
6780 // fails here and backtracks to a different split.
6781 }
6782 // Restore/clear the sync buffer slot.
6783 EXCSYNC_BUF.with(|b| {
6784 let mut m = b.borrow_mut();
6785 match prev_buf {
6786 Some(p) => {
6787 m.insert(exclude_node, p);
6788 }
6789 None => {
6790 m.remove(&exclude_node);
6791 }
6792 }
6793 });
6794 match found {
6795 Some((end, a_state)) => {
6796 *state = a_state;
6797 return Some(end);
6798 }
6799 None => {
6800 // c:3209-3211 — `while ((scan = PATNEXT(scan)) &&
6801 // P_ISEXCLUDE(scan)) ;` — when the asserted branch
6802 // plus its exclusions cannot produce an un-excluded
6803 // split, C skips PAST the whole exclusion chain and
6804 // carries on with the next ALTERNATIVE of the
6805 // enclosing switch. Returning None here instead
6806 // dropped every branch written after an exclusion:
6807 // `[[ xyz = readme~README|xyz ]]` never tried
6808 // `xyz`.
6809 let mut nxt = next;
6810 while nxt != 0 && nxt < code.len() && P_ISEXCLUDE(code[nxt + I_OP]) {
6811 let nb: [u8; 4] =
6812 code[nxt + I_NEXT..nxt + I_NEXT + 4].try_into().unwrap();
6813 nxt = u32::from_le_bytes(nb) as usize;
6814 }
6815 if nxt == 0 || nxt >= code.len() {
6816 return None;
6817 }
6818 let nop = code[nxt + I_OP];
6819 if nop != P_BRANCH && nop != P_WBRANCH {
6820 return None;
6821 }
6822 scan = nxt;
6823 continue;
6824 }
6825 }
6826 }
6827 let next_is_branch = next != 0
6828 && next < code.len()
6829 && (code[next + I_OP] == P_BRANCH || code[next + I_OP] == P_WBRANCH);
6830 if !next_is_branch {
6831 scan = scan + I_BODY + operand_off_extra;
6832 continue;
6833 }
6834 // Alt-loop: try each branch's operand; on success
6835 // return; on failure walk to the next BRANCH via .next.
6836 let mut br = scan;
6837 loop {
6838 let br_op = code[br + I_OP];
6839 let br_extra = if br_op == P_WBRANCH { 8 } else { 0 };
6840 let br_next_bytes: [u8; 4] =
6841 code[br + I_NEXT..br + I_NEXT + 4].try_into().unwrap();
6842 let br_next = u32::from_le_bytes(br_next_bytes) as usize;
6843 let operand = br + I_BODY + br_extra;
6844 // !!! RUST-ONLY DETAIL !!! C recurses into every
6845 // alternative (c:3258 `ret = patmatch(opnd)`). When the
6846 // alternative's operand is a P_EXACTLY that the input
6847 // cannot match, that recursion fails at its first node
6848 // with no side effect, so it is decided here instead —
6849 // with the P_EXACTLY arm's own failure test, under the
6850 // same conditions in which that arm compares raw bytes
6851 // (no `(#aN)` budget, no case folding). Restricted to
6852 // P_BRANCH: a P_WBRANCH records a visit mark before it
6853 // recurses (c:3245-3248), which must still happen.
6854 // compdump's `$^fpath/(${(o~j.|.)…})(N:t)` is a
6855 // ~2000-way alternation tried against every file in
6856 // every fpath directory; paying a full patmatch frame
6857 // plus an `rpat` clone per alternative made it seconds
6858 // of CPU in the debug build where zsh takes milliseconds.
6859 if br_op == P_BRANCH
6860 && (glob_flags & (0xff | GF_IGNCASE | GF_LCMATCHUC)) == 0
6861 && code.get(operand + I_OP) == Some(&P_EXACTLY)
6862 {
6863 let body = operand + I_BODY;
6864 let len = u32::from_le_bytes(code[body..body + 4].try_into().unwrap())
6865 as usize;
6866 let lit = &code[body + 4..body + 4 + len];
6867 if string.as_bytes().get(s_off..s_off + len) != Some(lit) {
6868 if br_next == 0 {
6869 return None;
6870 }
6871 let op_next = code[br_next + I_OP];
6872 if op_next != P_BRANCH && op_next != P_WBRANCH {
6873 return None;
6874 }
6875 br = br_next;
6876 continue;
6877 }
6878 }
6879 let mut sub_state = state.clone();
6880 // c:Src/pattern.c:3210-3248 — P_WBRANCH per-position
6881 // visit guard. Allocate a bitmap sized to the input
6882 // (`zshcalloc((patinend - patinstart) + 1)`), then
6883 // check/set `*ptr = errsfound + 1` at the current
6884 // input offset. On revisit with the same-or-fewer
6885 // errors, return 0 to bound recursion. Without this,
6886 // `(fo#)#` against any non-trivial input recurses
6887 // until PATMATCH_MAX_DEPTH.
6888 let mut wbranch_skip = false;
6889 if br_op == P_WBRANCH {
6890 // The map is shared with every clone of `state`, the
6891 // way C's operand buffer is shared across the whole
6892 // pattry — so a mark set here is still visible after
6893 // this branch fails and the walk backtracks.
6894 let mut visits = state.wbranch_visits.borrow_mut();
6895 let bm = visits
6896 .entry(br)
6897 .or_insert_with(|| vec![0u8; string.len() + 1]);
6898 let slot = bm.get(s_off).copied().unwrap_or(0);
6899 let cur = (state.errsfound as i32 + 1) as u8;
6900 if slot != 0 && (state.errsfound + 1) >= slot as i32 {
6901 wbranch_skip = true; // c:3245-3247
6902 } else if s_off < bm.len() {
6903 bm[s_off] = cur; // c:3248
6904 }
6905 }
6906 let sub_result = if wbranch_skip {
6907 None
6908 } else {
6909 patmatch(code, operand, string, s_off, &mut sub_state, glob_flags)
6910 };
6911 if let Some(end) = sub_result {
6912 // c:108-144 (pattern.c header doc on P_WBRANCH):
6913 // "P_WBRANCH: This works like a branch and is
6914 // used in complex closures, but the match must
6915 // be at least 1 char in length to avoid
6916 // infinite loops. The test for length is
6917 // done via the next pointer in the WBRANCH
6918 // test in patmatch()."
6919 // C enforces this via the `errsfound`/`forceerrs`
6920 // accounting at c:1059+ inside patcompbranch.
6921 // The Rust walker checks end > s_off directly:
6922 // if the body consumed nothing, reject this
6923 // alternative and fall through to the next.
6924 // Without this guard, `(fo#)#` against any input
6925 // stack-overflows because the inner body (`fo#`)
6926 // can match the empty string repeatedly.
6927 if br_op == P_WBRANCH && end == s_off {
6928 // c:108 "at least 1 char" — fall through to
6929 // try the next alternative.
6930 } else {
6931 *state = sub_state;
6932 return Some(end);
6933 }
6934 }
6935 if br_next == 0 {
6936 return None;
6937 }
6938 let op_next = code[br_next + I_OP];
6939 if op_next != P_BRANCH && op_next != P_WBRANCH {
6940 return None;
6941 }
6942 br = br_next;
6943 }
6944 }
6945 P_NUMRNG | P_NUMFROM | P_NUMTO => {
6946 // c:2815-2909 — `case P_NUMRNG: case P_NUMFROM: case P_NUMTO:`
6947 //
6948 // c:2818-2825 — "To do this properly, we really have to
6949 // treat numbers as closures: that's so things like
6950 // <1-1000>33 will match 633 (they didn't up to 3.1.6).
6951 // To avoid making this too inefficient, we see if there's
6952 // an exact match next: if there is, and it's not a digit,
6953 // we return 1 after the first attempt."
6954 let mut start_b = scan + I_BODY; // c:2827 `start = P_OPERAND(scan);`
6955 let mut from: i64 = 0; // c:2828 `from = to = 0;`
6956 let mut to: i64 = 0;
6957 if op != P_NUMTO {
6958 // c:2829-2837
6959 from = i64::from_le_bytes(code[start_b..start_b + 8].try_into().unwrap());
6960 start_b += 8; // c:2836 `start += sizeof(zrange_t);`
6961 }
6962 if op != P_NUMFROM {
6963 // c:2838-2845
6964 to = i64::from_le_bytes(code[start_b..start_b + 8].try_into().unwrap());
6965 }
6966 let input_bytes = string.as_bytes();
6967 let patinend = input_bytes.len();
6968 // c:2846 — `start = compend = patinput;`
6969 let start = s_off;
6970 let mut compend = s_off;
6971 let mut comp: i64 = 0; // c:2847
6972 let mut patinput = s_off;
6973 // c:2848 — `while (patinput < patinend && idigit(*patinput))`
6974 while patinput < patinend && input_bytes[patinput].is_ascii_digit() {
6975 let mut out_of_range = false; // c:2849
6976 let digit = (input_bytes[patinput] - b'0') as i64; // c:2850
6977 if comp > i64::MAX / 10 {
6978 // c:2851-2852
6979 out_of_range = true;
6980 } else {
6981 let c10 = if comp != 0 { comp * 10 } else { 0 }; // c:2854
6982 if i64::MAX - c10 < digit {
6983 out_of_range = true; // c:2856
6984 } else {
6985 comp = c10; // c:2858
6986 comp += digit; // c:2859
6987 }
6988 }
6989 patinput += 1; // c:2862
6990 compend += 1; // c:2863
6991 // c:2865-2873 — out of range "allowing for signedness,
6992 // which we need if we are using zlongs".
6993 if out_of_range || (comp & (1i64 << 62)) != 0 {
6994 // c:2875-2881 — "This is as far as we can go. If
6995 // we're doing a range \"from\", skip all the
6996 // remaining numbers. Otherwise, we can't match
6997 // beyond the previous point anyway. Leave the
6998 // pointer to the last calculated position
6999 // (compend) where it was before."
7000 if op == P_NUMFROM {
7001 // c:2882-2885
7002 while patinput < patinend && input_bytes[patinput].is_ascii_digit() {
7003 patinput += 1;
7004 }
7005 }
7006 }
7007 }
7008 let mut save = patinput; // c:2888
7009 let mut no = 0; // c:2889
7010 while patinput > start {
7011 // c:2890
7012 // c:2891-2893 — "if already too small, no power on
7013 // earth can save it"
7014 if comp < from && patinput <= compend {
7015 break;
7016 }
7017 if op == P_NUMFROM || comp <= to {
7018 // c:2894
7019 let mut sub_state = state.clone();
7020 if let Some(end) =
7021 patmatch(code, next, string, patinput, &mut sub_state, glob_flags)
7022 {
7023 *state = sub_state;
7024 return Some(end); // c:2895
7025 }
7026 }
7027 // c:2896-2900 — `if (!no && P_OP(next) == P_EXACTLY &&
7028 // (!P_LS_LEN(next) || !idigit(*P_LS_STR(next))) &&
7029 // !(patglobflags & 0xff)) return 0;`
7030 if no == 0 && next != 0 && (glob_flags & 0xff) == 0 {
7031 let nop = code.get(next + I_OP).copied().unwrap_or(0);
7032 if nop == P_EXACTLY {
7033 let nb = next + I_BODY;
7034 let nlen =
7035 u32::from_le_bytes(code[nb..nb + 4].try_into().unwrap()) as usize;
7036 if nlen == 0 || !code[nb + 4].is_ascii_digit() {
7037 return None;
7038 }
7039 }
7040 }
7041 // c:2901 — `patinput = --save;`
7042 save -= 1;
7043 patinput = save;
7044 no += 1; // c:2902
7045 // c:2903-2908 — "With a range start and an
7046 // unrepresentable test number, we just back down the
7047 // test string without changing the number until we get
7048 // to a representable one."
7049 if patinput < compend {
7050 comp /= 10;
7051 }
7052 }
7053 // c:2910-2911 — `patinput = start; fail = 1;`
7054 return None;
7055 }
7056 P_NUMANY => {
7057 // c:P_NUMANY — `<->` any non-empty digit run. Pins
7058 // `Test/D02glob.ztst:136` (`<->33` matches "633" by
7059 // consuming just "6" then literal "33").
7060 let input_bytes = string.as_bytes();
7061 let start = s_off;
7062 let mut k = start;
7063 while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
7064 k += 1;
7065 }
7066 if k == start {
7067 return None;
7068 }
7069 while k > start {
7070 let mut sub_state = state.clone();
7071 if let Some(end) = patmatch(code, next, string, k, &mut sub_state, glob_flags) {
7072 *state = sub_state;
7073 return Some(end);
7074 }
7075 k -= 1;
7076 }
7077 return None;
7078 }
7079 P_ISSTART => {
7080 // c:Src/pattern.c:3392-3394 — `if (patinput != patinstart
7081 // || (patflags & PAT_NOTSTART)) fail = 1;`. C bails on
7082 // BOTH conditions: local-position-not-zero OR the
7083 // caller-set PAT_NOTSTART flag (set by glob.c's
7084 // set_pat_start when the test string is an interior
7085 // slice of a larger buffer). Read patflags from the
7086 // file-static (set at pattryrefs entry from prog flags).
7087 if s_off != 0 || (patflags.load(Ordering::Relaxed) & PAT_NOTSTART as i32) != 0 {
7088 return None;
7089 }
7090 }
7091 P_ISEND => {
7092 // c:Src/pattern.c:3396-3398 — `if (patinput < patinend
7093 // || (patflags & PAT_NOTEND)) fail = 1;`. Same shape as
7094 // P_ISSTART: bail on local-not-at-end OR caller-set
7095 // PAT_NOTEND (set by set_pat_end when the suffix was
7096 // truncated).
7097 if s_off < string.len()
7098 || (patflags.load(Ordering::Relaxed) & PAT_NOTEND as i32) != 0
7099 {
7100 return None;
7101 }
7102 }
7103 P_GFLAGS => {
7104 // c:P_GFLAGS arm
7105 let body = scan + I_BODY;
7106 let bits = i32::from_le_bytes(code[body..body + 4].try_into().unwrap());
7107 // C uses absolute set; for the on/off toggle pairs
7108 // we currently encode only the "on" bits (i.e. (#I)
7109 // emits 0 to clear). Set the running flags directly,
7110 // INCLUDING the low byte (`(#aN)` approximation budget)
7111 // so mid-pattern `(#a0)`/`(#a2)` re-arm the error
7112 // allowance — c:Src/pattern.c:2941.
7113 glob_flags =
7114 (glob_flags & !(GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE | 0xff)) | bits;
7115 }
7116 P_COUNT => {
7117 // c:3428-3455 — `case P_COUNT: /* (#cN,M): execution is
7118 // relatively straightforward */`
7119 //
7120 // long cur = scan[P_CT_CURRENT].l;
7121 // long min = scan[P_CT_MIN].l;
7122 // long max = scan[P_CT_MAX].l;
7123 // if (cur && cur >= min &&
7124 // (unsigned char *)patinput == scan[P_CT_PTR].p)
7125 // return patmatch(next);
7126 // scan[P_CT_PTR].p = (unsigned char *)patinput;
7127 // if (max < 0 || cur < max) {
7128 // char *patinput_thistime = patinput;
7129 // scan[P_CT_CURRENT].l = cur + 1;
7130 // if (patmatch(scan + P_CT_OPERAND)) return 1;
7131 // scan[P_CT_CURRENT].l = cur;
7132 // patinput = patinput_thistime;
7133 // }
7134 // if (cur < min) return 0;
7135 // return patmatch(next);
7136 //
7137 // !!! RUST-ONLY STRUCTURE (same search, different plumbing) !!!
7138 // C makes the repetition loop by pointing the OPERAND's tail
7139 // back at this very node (c:1693-1696 `pattail(opnd,
7140 // patnode(P_BACK))`) and by keeping `cur` / `ptr` as mutable
7141 // cells INSIDE the compiled program (c:208-213 P_CT_CURRENT /
7142 // P_CT_PTR). That back-edge is what lets one iteration's
7143 // continuation re-enter P_COUNT, so the operand's own
7144 // backtracking picks a SHORTER match when the longer one
7145 // leaves the rest of the repetition unsatisfiable. zshrs
7146 // stores the operand inline with no back-edge and no
7147 // in-program cells, so the same search runs here over an
7148 // explicit DFS stack: at each step every end position the
7149 // operand can reach is tried, longest first (C's greedy
7150 // order), and the count rides the stack instead of the
7151 // program. The previous greedy-only walk let `*_` eat the
7152 // whole string on iteration 1 and never reconsidered, so
7153 // `[[ 1_2_ = (*_)(#c2) ]]` failed.
7154 let body = scan + I_BODY;
7155 let min = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
7156 let max = i64::from_le_bytes(code[body + 8..body + 16].try_into().unwrap());
7157 let operand = body + 16;
7158 // Stack frame: (input position, repetitions so far, next
7159 // candidate operand end to try — descending, saved state).
7160 let mut stack: Vec<(usize, i64, i64, rpat)> =
7161 vec![(s_off, 0, string.len() as i64, state.clone())];
7162 while let Some((pos, cur, e, st)) = stack.pop() {
7163 // c:3448 — `if (max < 0 || cur < max)`
7164 if (max < 0 || cur < max) && e >= pos as i64 {
7165 // Re-queue this level with the next-shorter operand
7166 // end so an exhausted subtree backtracks here.
7167 stack.push((pos, cur, e - 1, st.clone()));
7168 let eu = e as usize;
7169 if string.is_char_boundary(eu) {
7170 let mut sub = st.clone();
7171 // Truncating the subject to `eu` anchors the
7172 // operand's end, which is how one candidate
7173 // iteration length is proposed.
7174 if let Some(got) =
7175 patmatch(code, operand, &string[..eu], pos, &mut sub, glob_flags)
7176 {
7177 // c:3434-3444 — "the previous attempt managed
7178 // zero length. We can do this indefinitely so
7179 // there's no point in going on." A zero-length
7180 // iteration still counts, but only up to `min`.
7181 if got == eu && (eu != pos || cur + 1 <= min) {
7182 // c:3450 — `scan[P_CT_CURRENT].l = cur + 1;`
7183 stack.push((eu, cur + 1, string.len() as i64, sub));
7184 }
7185 }
7186 }
7187 continue;
7188 }
7189 // c:3453 — `if (cur < min) return 0;`
7190 if cur < min {
7191 continue;
7192 }
7193 // c:3454 — `return patmatch(next);`
7194 if next == 0 {
7195 *state = st;
7196 return Some(pos);
7197 }
7198 let mut sub = st.clone();
7199 if let Some(end) = patmatch(code, next, string, pos, &mut sub, glob_flags) {
7200 *state = sub;
7201 return Some(end);
7202 }
7203 }
7204 return None;
7205 }
7206 op if op >= P_OPEN && op < P_CLOSE => {
7207 // c:P_OPEN_N arm (pattern.c:2939-2960).
7208 //
7209 // C `case P_OPEN+0..P_OPEN+9:
7210 // no = P_OP(scan) - P_OPEN;
7211 // save = patinput;
7212 // if (patmatch(next)) {
7213 // if (no && !(parsfound & (1 << (no - 1)))) {
7214 // patbeginp[no-1] = save;
7215 // parsfound |= 1 << (no - 1);
7216 // }
7217 // return 1;
7218 // }
7219 // return 0;`
7220 //
7221 // Recurse on `next`; only commit patbeginp[N-1] on
7222 // success AND only on the FIRST occurrence (c:2957
7223 // `!(parsfound & (1<<(no-1)))`). The first-write
7224 // semantic matters under `(*)*` and alternation
7225 // backtrack — a later iteration shouldn't overwrite
7226 // the saved start of the FIRST match.
7227 let n = (op - P_OPEN) as usize;
7228 let save = s_off;
7229 let saved_state = state.clone();
7230 // c:2957 — `if (no && !(parsfound & (1 << (no - 1))))`.
7231 // Open-bit is the LOW stripe: `1 << (n-1)`. n==0 is the
7232 // plain (uncaptured) P_OPEN — c:2957's `no &&` guard
7233 // means it records nothing; bit value is moot (0).
7234 let open_bit = if n > 0 { 1u32 << (n - 1) } else { 0 };
7235 if next == 0 {
7236 // No continuation — leaf P_OPEN; just commit and continue.
7237 if n > 0 && n <= NSUBEXP && (state.captures_set & open_bit) == 0 {
7238 state.patbeginp[n - 1] = save;
7239 state.captures_set |= open_bit; // c:2959
7240 }
7241 return Some(s_off);
7242 }
7243 match patmatch(code, next, string, s_off, state, glob_flags) {
7244 Some(end) => {
7245 // c:2957-2959 — first-write commit.
7246 if n > 0 && n <= NSUBEXP && (state.captures_set & open_bit) == 0 {
7247 state.patbeginp[n - 1] = save;
7248 state.captures_set |= open_bit; // c:2959
7249 }
7250 return Some(end);
7251 }
7252 None => {
7253 *state = saved_state;
7254 return None;
7255 }
7256 }
7257 }
7258 op if op >= P_CLOSE && op < 0xa0 => {
7259 // c:P_CLOSE_N arm (pattern.c:2980-3010).
7260 //
7261 // C `case P_CLOSE+0..P_CLOSE+9:
7262 // no = P_OP(scan) - P_CLOSE;
7263 // save = patinput;
7264 // if (patmatch(next)) {
7265 // if (no && !(parsfound & (1 << (no+NSUBEXP-1)))) {
7266 // patendp[no-1] = save;
7267 // parsfound |= 1 << (no+NSUBEXP-1);
7268 // }
7269 // return 1;
7270 // }
7271 // return 0;`
7272 //
7273 // Same save/recurse/first-write-on-success pattern as
7274 // P_OPEN. Rust uses `captures_set` bit (1<<(n-1)) for
7275 // BOTH open and close in the existing impl; semantically
7276 // the bit is set when the group's CLOSE has been seen,
7277 // i.e. the capture is complete. First-write on close
7278 // matters under `(...)*` so later iterations don't
7279 // overwrite the FIRST capture's end (matching C's
7280 // parsfound `no+NSUBEXP-1` bit-stripe).
7281 let n = (op - P_CLOSE) as usize;
7282 let save = s_off;
7283 let saved_state = state.clone();
7284 // c:2989 — `if (no && !(parsfound & (1 << (no+NSUBEXP-1))))`.
7285 // Close-bit is the HIGH stripe: `1 << (n-1+NSUBEXP)`.
7286 // n==0 = plain P_CLOSE, records nothing (c:2989 `no &&`).
7287 let close_bit = if n > 0 { 1u32 << (n - 1 + NSUBEXP) } else { 0 };
7288 if next == 0 {
7289 if n > 0 && n <= NSUBEXP && (state.captures_set & close_bit) == 0 {
7290 state.patendp[n - 1] = save;
7291 state.captures_set |= close_bit;
7292 }
7293 return Some(s_off);
7294 }
7295 match patmatch(code, next, string, s_off, state, glob_flags) {
7296 Some(end) => {
7297 if n > 0 && n <= NSUBEXP && (state.captures_set & close_bit) == 0 {
7298 state.patendp[n - 1] = save;
7299 state.captures_set |= close_bit;
7300 }
7301 return Some(end);
7302 }
7303 None => {
7304 *state = saved_state;
7305 return None;
7306 }
7307 }
7308 }
7309 _ => {
7310 // c:3465-3467 — `default: dputs("BUG: bad operand in
7311 // patmatch."); return 0;`
7312 crate::ported::utils::dputs("BUG: bad operand in patmatch.");
7313 return None;
7314 }
7315 }
7316
7317 if next == 0 {
7318 break;
7319 }
7320 scan = next;
7321 }
7322 Some(s_off)
7323}
7324
7325/// Port of `patallocstr()` from `Src/pattern.c:2132`.
7326/// C: `char *patallocstr(Patprog prog, char *string, int stringlen,
7327/// int unmetalen, int force, Patstralloc patstralloc)` from
7328/// `Src/pattern.c:2132`.
7329///
7330/// Sets up `patstralloc` for a match attempt: when `force` is set or
7331/// the input contains Meta bytes (or PAT_HAS_EXCLUDP demands a
7332/// full-path copy), allocates an un-metafied scratch buffer and
7333/// stashes pointer/length info on `patstralloc`. Returns the
7334/// allocated buffer or `None` if no allocation was needed.
7335pub fn patallocstr(
7336 prog: &Patprog,
7337 string: &str,
7338 stringlen: i32,
7339 unmetalen: i32,
7340 force: i32,
7341 patstralloc: &mut patstralloc,
7342) -> Option<String> {
7343 // c:2132
7344 // c:2137 — `int needfullpath;`
7345 let mut needfullpath: bool;
7346 // Working values (mutated when force triggers patmungestring).
7347 let mut string: &str = string; // c:2133 char *string param
7348 let mut stringlen: i32 = stringlen;
7349 let mut unmetalen: i32 = unmetalen;
7350
7351 if force != 0 {
7352 // c:2139
7353 // c:2140 — `patmungestring(&string, &stringlen, &unmetalen);`
7354 patmungestring(&mut string, &mut stringlen, &mut unmetalen);
7355 }
7356
7357 /*
7358 * For a top-level ~-exclusion, we will need the full
7359 * path to exclude, so copy the path so far and append the
7360 * current test string.
7361 */
7362 // c:2142-2146
7363 // c:2147 — `needfullpath = (prog->flags & PAT_HAS_EXCLUDP) && pathpos;`
7364 // `pathpos` is the `gd_pathpos` field of `curglobdata` (c:Src/glob.c:166-170
7365 // struct globdata; c:197 static curglobdata; c:199-201 macros expand
7366 // `pathpos`→`curglobdata.gd_pathpos`). Read directly from the
7367 // shared CURGLOBDATA mutex — the canonical port surface for glob
7368 // state in zshrs.
7369 let pathpos: i32 = crate::ported::glob::CURGLOBDATA
7370 .lock()
7371 .map(|gd| gd.pathpos as i32)
7372 .unwrap_or(0); // c:Src/glob.c:169 gd_pathpos
7373 needfullpath = (prog.0.flags & PAT_HAS_EXCLUDP as i32) != 0 && pathpos != 0; // c:2147
7374
7375 /* Get the length of the full string when unmetafied. */
7376 // c:2149
7377 if unmetalen < 0 {
7378 // c:2150
7379 // c:2151 — `patstralloc->unmetalen = ztrsub(string + stringlen, string);`
7380 // ztrsub returns the unmetafied char count between two pointers
7381 // in the same string. Rust analog: ztrsub(buf, start, end).
7382 patstralloc.unmetalen = ztrsub(string, 0, (stringlen as usize).min(string.len())) as i32;
7383 } else {
7384 // c:2152
7385 patstralloc.unmetalen = unmetalen; // c:2153
7386 }
7387 if needfullpath {
7388 // c:2154
7389 // c:2155 — `patstralloc->unmetalenp = ztrsub(pathbuf + pathpos, pathbuf);`
7390 // `pathbuf` is `curglobdata.gd_pathbuf` (c:Src/glob.c:170, macro
7391 // at c:200). ztrsub(end, start) returns unmetafied char count
7392 // between pointers. With pathpos in [0, pathbuf.len()] the
7393 // Rust analog is ztrsub(pathbuf, 0, pathpos as usize).
7394 let pathbuf = crate::ported::glob::CURGLOBDATA
7395 .lock()
7396 .map(|gd| gd.pathbuf.clone())
7397 .unwrap_or_default(); // c:Src/glob.c:170 gd_pathbuf
7398 let p_end = (pathpos as usize).min(pathbuf.len());
7399 patstralloc.unmetalenp = ztrsub(&pathbuf, 0, p_end) as i32; // c:2155
7400 if patstralloc.unmetalenp == 0 {
7401 // c:2156
7402 needfullpath = false; // c:2157 (`needfullpath = 0;`)
7403 }
7404 } else {
7405 // c:2158
7406 patstralloc.unmetalenp = 0; // c:2159
7407 }
7408 /* Initialise cache area */
7409 // c:2161
7410 patstralloc.progstrunmeta = None; // c:2162
7411 patstralloc.progstrunmetalen = 0; // c:2163
7412
7413 // c:2165-2166 — `DPUTS(needfullpath && (prog->flags & (PAT_PURES|PAT_ANY)),
7414 // "rum sort of file exclusion");`
7415 // Rust drops the debug assertion.
7416
7417 /*
7418 * Partly for efficiency, and partly for the convenience of
7419 * globbing, we don't unmetafy pure string patterns, and
7420 * there's no reason to if the pattern is just a *.
7421 */
7422 // c:2167-2171
7423 let pures_or_any = (prog.0.flags & (PAT_PURES | PAT_ANY) as i32) != 0;
7424 if force != 0 || (!pures_or_any && (needfullpath || patstralloc.unmetalen != stringlen))
7425 // c:2172
7426 {
7427 /*
7428 * We need to copy if we need to prepend the path so far
7429 * (in which case we copy both chunks), or if we have
7430 * Meta characters.
7431 */
7432 // c:2174-2178
7433 // c:2179 — `char *dst, *ptr; int i, icopy, ncopy;`
7434 let total = (patstralloc.unmetalen + patstralloc.unmetalenp) as usize;
7435 let mut dst = String::with_capacity(total); // c:2182 zhalloc
7436
7437 // c:2184-2192 — choose source chunk(s).
7438 let pathbuf: String = crate::ported::glob::CURGLOBDATA
7439 .lock()
7440 .map(|gd| gd.pathbuf.clone())
7441 .unwrap_or_default(); // c:Src/glob.c:170 gd_pathbuf
7442 let mut ptr: &str;
7443 let mut ncopy: i32;
7444 if needfullpath {
7445 // c:2185
7446 ptr = &pathbuf; // c:2186
7447 ncopy = patstralloc.unmetalenp; // c:2188
7448 } else {
7449 // c:2189
7450 ptr = string; // c:2190
7451 ncopy = patstralloc.unmetalen; // c:2191
7452 }
7453 // c:2193-2210 — for (icopy = 0; icopy < 2; icopy++) outer loop:
7454 // copy ncopy bytes from ptr to dst, unmetafy Meta+X pairs.
7455 for icopy in 0..2 {
7456 // c:2193
7457 let ptr_bytes = ptr.as_bytes();
7458 let mut i = 0i32;
7459 let mut byte_idx = 0usize;
7460 while i < ncopy && byte_idx < ptr_bytes.len() {
7461 // c:2194
7462 if ptr_bytes[byte_idx] == Meta as u8 && byte_idx + 1 < ptr_bytes.len() {
7463 // c:2195-2197 — `if (*ptr == Meta) { ptr++; *dst++ = *ptr++ ^ 32; }`
7464 byte_idx += 1; // c:2196 ptr++
7465 dst.push((ptr_bytes[byte_idx] ^ 32) as char); // c:2197 *dst++ = *ptr++ ^ 32
7466 byte_idx += 1;
7467 } else {
7468 // c:2198
7469 // c:2199 — `else *dst++ = *ptr++;`
7470 dst.push(ptr_bytes[byte_idx] as char);
7471 byte_idx += 1;
7472 }
7473 i += 1;
7474 }
7475 if !needfullpath {
7476 // c:2203
7477 break; // c:2204
7478 }
7479 /* next time append test string to path so far */
7480 // c:2205
7481 ptr = string; // c:2207
7482 ncopy = patstralloc.unmetalen; // c:2208
7483 let _ = icopy;
7484 }
7485 patstralloc.alloced = Some(dst.clone()); // c:2182 dst = patstralloc->alloced
7486 return Some(dst); // c:2213 return patstralloc->alloced
7487 } else {
7488 // c:2214
7489 patstralloc.alloced = None; // c:2215
7490 }
7491
7492 None // c:2218 return patstralloc->alloced (NULL)
7493}
7494
7495// `patrepeat(Upat p, char *charstart)` (C: pattern.c:4096 — `static int`
7496// helper called from the bytecode walker at pattern.c:3321 for greedy
7497// `*` matches) had a Rust-only wrapper `pub fn patrepeat(prog: &Patprog,
7498// s: &str, max: Option<usize>)`. Zero Rust callers (the bytecode walker
7499// inlines its own greedy loop instead of calling out to patrepeat),
7500// Rule S1 deviation (extra `max` param, takes whole prog instead of a
7501// Upat into the bytecode). Deleted; reintroduce as a faithful port when
7502// the bytecode walker is refactored to use it.
7503
7504// =====================================================================
7505// Transitional aliases — older callers still use `PatProg` (camel-case
7506// from the previous AST-based port). Alias them to `Patprog` so the
7507// build doesn't break; future cleanup commit renames callers.
7508// =====================================================================
7509/// `PatProg` type alias.
7510#[deprecated(note = "use Patprog instead")]
7511pub type PatProg = Patprog;
7512
7513// =====================================================================
7514// Transitional Rust-only types — kept for external callers that bind
7515// to the previous AST-based port's surface area (vm_helper, exec_shims.rs,
7516// fusevm_bridge.rs, glob.rs). These are NOT C-faithful ports — they're
7517// helper aggregates the previous AST port introduced for one-shot
7518// pattern processing in the executor/VM bridge. Callers bind to the
7519// bytecode API (patcompile + pattry + patgetglobflags) where they can.
7520// =====================================================================
7521
7522// `NumericRange` struct + impl DELETED. C zsh's `patcomppiece`
7523// (Src/pattern.c:1450+) inlines `<N-M>` parsing and emits `P_NUMRNG`
7524// opcodes — no aggregator type. Rust port uses these bare helpers
7525// returning tuples `(start, end, lo, hi)` for the pre-pattern pass
7526// that exec_shims/fusevm need because the `glob` crate has no
7527// native `P_NUMRNG`. Dissolve fully when fusevm + exec_shims
7528// migrate to pure `patcompile` + `pattry`.
7529
7530/// !!! WARNING: RUST-ONLY HELPER !!!
7531///
7532/// There is NO `extract_numeric_ranges()` in `Src/pattern.c`. C has
7533/// no such pre-pass at all: `<N-M>` is parsed inside `patcomppiece()`
7534/// (`Src/pattern.c:1528-1573`, the `case Inang:` arm) and emitted as
7535/// `P_NUMRNG` / `P_NUMFROM` / `P_NUMTO` / `P_NUMANY` opcodes, then
7536/// matched by `patmatch()` (`Src/pattern.c:2694`). This scanner
7537/// exists only for the transitional callers named in the section
7538/// comment above (`vm_helper`, `exec_shims.rs`), which still route
7539/// some globs through the external `glob` crate — which has no
7540/// `P_NUMRNG` equivalent — and so must strip and re-check the
7541/// numeric ranges themselves. It disappears when those call sites
7542/// move to `patcompile()` + `pattry()`.
7543///
7544/// Extracts all `<N-M>` / `<N->` / `<-M>` / `<->` ranges from a glob
7545/// pattern. Returns `(start, end, lo, hi)` tuples — `start`/`end`
7546/// are byte offsets of `<` / past `>`, `lo`/`hi` are bounds (`None`
7547/// = unbounded on that side).
7548pub fn extract_numeric_ranges(s: &str) -> Vec<(usize, usize, Option<i64>, Option<i64>)> {
7549 let mut out = Vec::new();
7550 let bytes = s.as_bytes();
7551 let mut i = 0;
7552 while i < bytes.len() {
7553 if bytes[i] == b'<' {
7554 let start = i;
7555 let mut j = i + 1;
7556 let lo_start = j;
7557 while j < bytes.len() && bytes[j].is_ascii_digit() {
7558 j += 1;
7559 }
7560 let lo: Option<i64> = if j > lo_start {
7561 std::str::from_utf8(&bytes[lo_start..j])
7562 .ok()
7563 .and_then(|s| s.parse::<i64>().ok())
7564 } else {
7565 None
7566 };
7567 if j < bytes.len() && bytes[j] == b'-' {
7568 j += 1;
7569 let hi_start = j;
7570 while j < bytes.len() && bytes[j].is_ascii_digit() {
7571 j += 1;
7572 }
7573 let hi: Option<i64> = if j > hi_start {
7574 std::str::from_utf8(&bytes[hi_start..j])
7575 .ok()
7576 .and_then(|s| s.parse::<i64>().ok())
7577 } else {
7578 None
7579 };
7580 if j < bytes.len() && bytes[j] == b'>' {
7581 out.push((start, j + 1, lo, hi));
7582 i = j + 1;
7583 continue;
7584 }
7585 }
7586 }
7587 i += 1;
7588 }
7589 out
7590}
7591
7592/// !!! WARNING: RUST-ONLY HELPER !!!
7593///
7594/// There is NO `numeric_ranges_to_star()` in `Src/pattern.c`. C never
7595/// rewrites a pattern before matching it — `<N-M>` compiles straight
7596/// to `P_NUMRNG` in `patcomppiece()` (`Src/pattern.c:1528-1573`).
7597/// This is the second half of the transitional `extract_numeric_ranges()`
7598/// shim above: the external `glob` crate cannot express a numeric
7599/// range, so the range is widened to `*` for the crate's pass and
7600/// re-checked afterwards by `numeric_range_contains()`. Dies with the
7601/// same call sites.
7602///
7603/// Replaces every `<N-M>` in `s` with `*` for fallback glob
7604/// expansion.
7605pub fn numeric_ranges_to_star(s: &str) -> String {
7606 let mut out = String::with_capacity(s.len());
7607 let mut last = 0;
7608 for (start, end, _, _) in extract_numeric_ranges(s) {
7609 out.push_str(&s[last..start]);
7610 out.push('*');
7611 last = end;
7612 }
7613 out.push_str(&s[last..]);
7614 out
7615}
7616
7617/// !!! WARNING: RUST-ONLY HELPER !!!
7618///
7619/// There is NO `numeric_range_contains()` in `Src/pattern.c`. The C
7620/// equivalent is the inline bounds test in the `P_NUMRNG` arm of
7621/// `patmatch()` (`Src/pattern.c:2810-2910`, the `case P_NUMRNG:` arm), which reads
7622/// its two `zrange_t` operands straight out of the bytecode. This standalone
7623/// form exists only for the transitional `extract_numeric_ranges()`
7624/// callers above, which have the bounds as `Option<i64>` rather than
7625/// as compiled operands. Dies with the same call sites.
7626///
7627/// Tests whether `n` falls within numeric range `(lo, hi)`. Unbounded
7628/// sides always pass.
7629pub fn numeric_range_contains(lo: Option<i64>, hi: Option<i64>, n: i64) -> bool {
7630 lo.map_or(true, |l| n >= l) && hi.map_or(true, |h| n <= h)
7631}
7632
7633// =====================================================================
7634// Tests
7635// =====================================================================
7636
7637#[cfg(test)]
7638mod tests {
7639 use super::*;
7640 use crate::options::{opt_state_get, opt_state_set};
7641 use std::thread;
7642
7643 // Pattern compile shares file-static globals (patout, patparse,
7644 // patnpar, ...) with the same single-thread semantics as zsh's
7645 // C source. `patcompile` clones the globals into prog.1
7646 // before returning, so we only need the mutex held during
7647 // compile — pattry() reads from prog.1 with no global state.
7648 static TEST_MUTEX: Mutex<()> = Mutex::new(());
7649
7650 fn compile(p: &str) -> Patprog {
7651 let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
7652 patcompile(
7653 &{
7654 let mut __pat_tok = (p).to_string();
7655 crate::ported::glob::tokenize(&mut __pat_tok);
7656 __pat_tok
7657 },
7658 PAT_HEAPDUP as i32,
7659 None,
7660 )
7661 .expect("compile failed")
7662 }
7663
7664 /// Test-only `patcompile + pattry` pair (Rule 3 exempt — `#[cfg(test)]`).
7665 /// Mirrors the pattern most tests want: "does this pattern match
7666 /// this string?" without dragging compile boilerplate into every
7667 /// assertion. Does NOT acquire `TEST_MUTEX` — callers already hold
7668 /// it (or hold the broader `global_state_lock()` from `test_util`)
7669 /// when serialisation against `patcompile`'s file-statics matters.
7670 /// Acquiring it here too would deadlock on the non-reentrant
7671 /// `Mutex<()>` (e.g. `convenience_patmatch` holds TEST_MUTEX before
7672 /// calling, `patcompile_concurrent_safe` exercises 8 threads that
7673 /// would serialise via this fn instead of through the real engine).
7674 fn patmatch(pat: &str, text: &str) -> bool {
7675 patcompile(
7676 &{
7677 let mut __pat_tok = (pat).to_string();
7678 crate::ported::glob::tokenize(&mut __pat_tok);
7679 __pat_tok
7680 },
7681 PAT_HEAPDUP as i32,
7682 None,
7683 )
7684 .map_or(false, |prog| pattry(&prog, text))
7685 }
7686
7687 /// The P_BRANCH alt-loop decides a literal-led alternative without
7688 /// recursing when the input cannot match it. Every case here sits on
7689 /// one side of that decision: a literal longer than the input, a
7690 /// literal that fails while a later one matches, branches that must
7691 /// still run in the full walker (`(#i)`, `(#a1)`, empty branch,
7692 /// wildcard-led), and a compdump-sized alternation hit at its last
7693 /// branch.
7694 #[test]
7695 fn alternation_literal_branches_match_like_zsh() {
7696 let _g = crate::test_util::global_state_lock();
7697 // `(#i)` / `(#a1)` are EXTENDED_GLOB syntax.
7698 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
7699 crate::ported::options::opt_state_set("extendedglob", true);
7700 assert!(patmatch("(ab|a)c", "ac"));
7701 assert!(patmatch("(abc|ab)", "ab"));
7702 assert!(!patmatch("(abc|ab)", "a"));
7703 assert!(!patmatch("(x|y|z)", "w"));
7704 assert!(patmatch("(foo|bar)*", "barbaz"));
7705 assert!(patmatch("(foo|*az)", "barbaz"));
7706 assert!(patmatch("(ab|)c", "c"));
7707 assert!(patmatch("(#i)(ab|cd)", "CD"));
7708 assert!(!patmatch("(ab|cd)", "CD"));
7709 assert!(patmatch("(#a1)(abc|xyz)", "abd"));
7710 assert!(!patmatch("(#a1)(abc|xyz)", "add"));
7711 let names: Vec<String> = (0..2000).map(|i| format!("_n{i}")).collect();
7712 let pat = format!("({})", names.join("|"));
7713 assert!(patmatch(&pat, "_n1999"));
7714 assert!(patmatch(&pat, "_n0"));
7715 assert!(!patmatch(&pat, "_n2000"));
7716 assert!(!patmatch(&pat, "_n"));
7717 crate::ported::options::opt_state_set("extendedglob", saved);
7718 }
7719
7720 #[test]
7721 fn literal_match() {
7722 let _g = crate::test_util::global_state_lock();
7723 let prog = compile("hello");
7724 assert!(pattry(&prog, "hello"));
7725 assert!(!pattry(&prog, "world"));
7726 }
7727
7728 #[test]
7729 fn star_matches_anything() {
7730 let _g = crate::test_util::global_state_lock();
7731 let prog = compile("*");
7732 assert!(pattry(&prog, ""));
7733 assert!(pattry(&prog, "abc"));
7734 }
7735
7736 #[test]
7737 fn star_in_middle() {
7738 let _g = crate::test_util::global_state_lock();
7739 let prog = compile("a*z");
7740 assert!(pattry(&prog, "az"));
7741 assert!(pattry(&prog, "abz"));
7742 assert!(pattry(&prog, "aXYZz"));
7743 assert!(!pattry(&prog, "ab"));
7744 }
7745
7746 #[test]
7747 fn question_matches_one() {
7748 let _g = crate::test_util::global_state_lock();
7749 let prog = compile("a?c");
7750 assert!(pattry(&prog, "abc"));
7751 assert!(pattry(&prog, "axc"));
7752 assert!(!pattry(&prog, "ac"));
7753 }
7754
7755 #[test]
7756 fn bracket_anyof() {
7757 let _g = crate::test_util::global_state_lock();
7758 let prog = compile("[abc]");
7759 assert!(pattry(&prog, "a"));
7760 assert!(pattry(&prog, "b"));
7761 assert!(pattry(&prog, "c"));
7762 assert!(!pattry(&prog, "d"));
7763 }
7764
7765 #[test]
7766 fn bracket_range() {
7767 let _g = crate::test_util::global_state_lock();
7768 let prog = compile("[a-z]");
7769 assert!(pattry(&prog, "m"));
7770 assert!(!pattry(&prog, "M"));
7771 }
7772
7773 #[test]
7774 fn bracket_negated() {
7775 let _g = crate::test_util::global_state_lock();
7776 let prog = compile("[^0-9]");
7777 assert!(pattry(&prog, "a"));
7778 assert!(!pattry(&prog, "5"));
7779 }
7780
7781 #[test]
7782 fn alternation() {
7783 let _g = crate::test_util::global_state_lock();
7784 let prog = compile("foo|bar");
7785 assert!(pattry(&prog, "foo"));
7786 assert!(pattry(&prog, "bar"));
7787 assert!(!pattry(&prog, "baz"));
7788 }
7789
7790 #[test]
7791 fn captures() {
7792 let _g = crate::test_util::global_state_lock();
7793 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
7794 crate::ported::options::opt_state_set("extendedglob", true);
7795 let prog = compile("(#b)(foo)(bar)");
7796 crate::ported::options::opt_state_set("extendedglob", saved);
7797 let mut nump = 0i32;
7798 let mut begp: Vec<i32> = Vec::new();
7799 let mut endp: Vec<i32> = Vec::new();
7800 let ok = pattryrefs(
7801 &prog,
7802 "foobar",
7803 -1,
7804 -1,
7805 None,
7806 0,
7807 Some(&mut nump),
7808 Some(&mut begp),
7809 Some(&mut endp),
7810 );
7811 assert!(ok);
7812 // c:2556-2566 — begp holds the first character of each group, endp
7813 // the LAST character (inclusive).
7814 assert_eq!(nump, 2);
7815 assert_eq!(&begp[..2], &[0, 3]);
7816 assert_eq!(&endp[..2], &[2, 5]);
7817 }
7818
7819 /// c:1626-1627 — `if (kshchar && (hash || count)) return 0;`, where
7820 /// `case Star:` supplies the `kshchar = -1` sentinel (c:1440, "kshchar is
7821 /// used as a sign that we can't have #'s"). Both halves of that one C
7822 /// test have to reject: this port had ported only the `count` half (in
7823 /// patcompbranch), so `*(#c2,3)` was already a bad pattern here while
7824 /// `*#` / `*##` / `a*#b` compiled and matched. `zsh -f` 5.9 answers
7825 /// `bad pattern: *#` for all three.
7826 #[test]
7827 fn a_closure_may_not_follow_a_star() {
7828 let _g = crate::test_util::global_state_lock();
7829 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
7830 crate::ported::options::opt_state_set("extendedglob", true);
7831 let try_compile = |p: &str| {
7832 let _t = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
7833 patcompile(
7834 &{
7835 let mut tok = p.to_string();
7836 crate::ported::glob::tokenize(&mut tok);
7837 tok
7838 },
7839 PAT_HEAPDUP as i32,
7840 None,
7841 )
7842 };
7843 for bad in ["*#", "*##", "a*#b"] {
7844 assert!(
7845 try_compile(bad).is_none(),
7846 "c:1627 — `{bad}` is a bad pattern: a closure cannot follow `*`"
7847 );
7848 }
7849 // The neighbouring forms still compile: a closure on a non-Star atom,
7850 // and a `*` with no closure after it.
7851 for good in ["a#", "[a]#", "?#", "a*b", "(a*)#"] {
7852 assert!(
7853 try_compile(good).is_some(),
7854 "c:1620 — `{good}` has no kshchar/Star + hash pair and must compile"
7855 );
7856 }
7857 crate::ported::options::opt_state_set("extendedglob", saved);
7858 }
7859
7860 #[test]
7861 fn hash_zero_or_more() {
7862 let _g = crate::test_util::global_state_lock();
7863 // `#`/`##` quantifiers require EXTENDEDGLOB per zsh.
7864 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
7865 crate::ported::options::opt_state_set("extendedglob", true);
7866 let prog = compile("a#");
7867 assert!(pattry(&prog, ""));
7868 assert!(pattry(&prog, "a"));
7869 assert!(pattry(&prog, "aaa"));
7870 crate::ported::options::opt_state_set("extendedglob", saved);
7871 }
7872
7873 /// c:1338 / c:1372-1376 — a closure over a SINGLE MULTIBYTE character.
7874 ///
7875 /// `morelen` (c:1338 `patprev > str0`) counts CHARACTERS, not bytes, and
7876 /// this port used to test bytes. A one-character run whose character is
7877 /// multibyte therefore took the "backtrack one character so the `#`
7878 /// applies to the last one" branch (c:1343-1351), gave back the ONLY
7879 /// character in the run, and returned a piece that had consumed nothing —
7880 /// so `patcompbranch`'s piece loop never terminated. `[[ éé = é# ]]`
7881 /// burned 100% CPU forever (D07multibyte, "Multibyte handling of
7882 /// functions parameter" neighbourhood).
7883 ///
7884 /// The bound below is what makes this a hang test rather than a
7885 /// correctness test: compiling and matching six short patterns is
7886 /// microseconds of work, and the pre-fix code never returns at all, so
7887 /// any generous wall-clock ceiling separates the two. It is deliberately
7888 /// loose (this box runs many concurrent build/test sessions).
7889 #[test]
7890 fn multibyte_closure_terminates() {
7891 let _g = crate::test_util::global_state_lock();
7892 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
7893 crate::ported::options::opt_state_set("extendedglob", true);
7894 let started = std::time::Instant::now();
7895 // c:1375 — `é#` compiles as `(é)#`, so it matches zero or more `é`.
7896 let prog = compile("é#");
7897 assert!(pattry(&prog, ""), "é# matches the empty string");
7898 assert!(pattry(&prog, "é"), "é# matches one é");
7899 assert!(pattry(&prog, "éé"), "é# matches two é");
7900 assert!(!pattry(&prog, "x"), "é# does not match x");
7901 // `##` (one or more) over the same single multibyte character.
7902 let prog2 = compile("é##");
7903 assert!(!pattry(&prog2, ""), "é## needs at least one é");
7904 assert!(pattry(&prog2, "ééé"), "é## matches three é");
7905 // A MULTI-character run ending in a multibyte character still
7906 // backtracks (c:1343-1351): `aé#` is `a` followed by `é#`.
7907 let prog3 = compile("aé#");
7908 assert!(pattry(&prog3, "a"), "aé# matches a");
7909 assert!(pattry(&prog3, "aéé"), "aé# matches a then two é");
7910 assert!(!pattry(&prog3, "aa"), "aé# does not match aa");
7911 crate::ported::options::opt_state_set("extendedglob", saved);
7912 assert!(
7913 started.elapsed() < std::time::Duration::from_secs(20),
7914 "multibyte closure compile/match must terminate promptly, took {:?}",
7915 started.elapsed()
7916 );
7917 }
7918
7919 #[test]
7920 fn double_hash_one_or_more() {
7921 let _g = crate::test_util::global_state_lock();
7922 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
7923 crate::ported::options::opt_state_set("extendedglob", true);
7924 let prog = compile("a##");
7925 assert!(!pattry(&prog, ""));
7926 assert!(pattry(&prog, "a"));
7927 assert!(pattry(&prog, "aaa"));
7928 crate::ported::options::opt_state_set("extendedglob", saved);
7929 }
7930
7931 #[test]
7932 fn escape_literal() {
7933 let _g = crate::test_util::global_state_lock();
7934 let prog = compile("a\\*b");
7935 assert!(pattry(&prog, "a*b"));
7936 assert!(!pattry(&prog, "azb"));
7937 }
7938
7939 #[test]
7940 fn convenience_patmatch() {
7941 let _g = crate::test_util::global_state_lock();
7942 let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
7943 assert!(patmatch("hello*", "hello world"));
7944 assert!(!patmatch("x?z", "abc"));
7945 }
7946
7947 /// Concurrent compile must not corrupt the file-scope statics
7948 /// (`Src/pattern.c:267-281`). Verifies `PATCOMPILE_LOCK` serialises
7949 /// the entry so colon-bearing patterns from zutil-style consumers
7950 /// don't race against simpler call sites.
7951 #[test]
7952 fn patcompile_concurrent_safe() {
7953 let _g = crate::test_util::global_state_lock();
7954 let handles: Vec<_> = (0..8)
7955 .map(|i| {
7956 thread::spawn(move || {
7957 for _ in 0..200 {
7958 assert!(patmatch(":completion:*", ":completion:zsh"));
7959 assert!(patmatch("hello*", "hello world"));
7960 let _ = i;
7961 }
7962 })
7963 })
7964 .collect();
7965 for h in handles {
7966 h.join().unwrap();
7967 }
7968 }
7969
7970 #[test]
7971 fn haswilds_detects_meta() {
7972 let _g = crate::test_util::global_state_lock();
7973 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7974 let tok = |s: &str| {
7975 let mut t = s.to_string();
7976 crate::ported::glob::tokenize(&mut t);
7977 t
7978 };
7979 assert!(haswilds(&tok("*")));
7980 assert!(haswilds(&tok("foo?")));
7981 assert!(haswilds(&tok("[abc]")));
7982 assert!(!haswilds(&tok("plain")));
7983 }
7984
7985 #[test]
7986 /// `Src/pattern.c:1148` — walks `colon_stuffs[]` (c:1134-1138), returns
7987 /// `(index + PP_FIRST)`. PP_FIRST=1, so `alpha`→1, `alnum`→2, `ascii`→3,
7988 /// …, `digit`→6, …, `INVALID`→19. Unknown returns None (C returns
7989 /// PP_UNKWN=20).
7990 fn range_type_lookup() {
7991 let _g = crate::test_util::global_state_lock();
7992 assert_eq!(range_type("alpha"), Some(1), "PP_ALPHA (c:colon_stuffs[0])");
7993 assert_eq!(range_type("alnum"), Some(2), "PP_ALNUM (c:colon_stuffs[1])");
7994 assert_eq!(
7995 range_type("ascii"),
7996 Some(3),
7997 "PP_ASCII (c:colon_stuffs[2]) — was missing pre-fix"
7998 );
7999 assert_eq!(range_type("digit"), Some(6), "PP_DIGIT (c:colon_stuffs[5])");
8000 assert_eq!(
8001 range_type("xdigit"),
8002 Some(13),
8003 "PP_XDIGIT (c:colon_stuffs[12])"
8004 );
8005 assert_eq!(range_type("IDENT"), Some(14), "PP_IDENT — zsh extension");
8006 assert_eq!(range_type("WORD"), Some(17), "PP_WORD — zsh extension");
8007 assert_eq!(
8008 range_type("INVALID"),
8009 Some(19),
8010 "PP_INVALID — zsh extension"
8011 );
8012 assert_eq!(range_type("nonsense"), None);
8013 }
8014
8015 #[test]
8016 fn pattern_range_to_string_passes_through_pos_class() {
8017 let _g = crate::test_util::global_state_lock();
8018 assert_eq!(pattern_range_to_string("[:alpha:]"), "[:alpha:]");
8019 assert_eq!(pattern_range_to_string("a-z"), "a-z");
8020 assert_eq!(pattern_range_to_string(""), "");
8021 }
8022
8023 #[test]
8024 fn patgetglobflags_case_insensitive() {
8025 let _g = crate::test_util::global_state_lock();
8026 let (bits, _, n) = patgetglobflags("(#i)foo").unwrap();
8027 assert_ne!((bits & GF_IGNCASE), 0);
8028 assert_eq!(n, 4); // length of "(#i)"
8029 }
8030
8031 #[test]
8032 fn patgetglobflags_backref() {
8033 let _g = crate::test_util::global_state_lock();
8034 let (bits, _, _) = patgetglobflags("(#b)").unwrap();
8035 assert_ne!((bits & GF_BACKREF), 0);
8036 }
8037
8038 #[test]
8039 fn patgetglobflags_approx() {
8040 let _g = crate::test_util::global_state_lock();
8041 let (bits, _, _) = patgetglobflags("(#a2)").unwrap();
8042 assert_eq!(bits & 0xff, 2);
8043 }
8044
8045 /// Pin: `(#I)` per `Src/pattern.c:1080-1081` clears BOTH
8046 /// `GF_LCMATCHUC` AND `GF_IGNCASE`: `patglobflags &=
8047 /// ~(GF_LCMATCHUC|GF_IGNCASE)`.
8048 #[test]
8049 fn patgetglobflags_capital_i_clears_both_case_flags() {
8050 let _g = crate::test_util::global_state_lock();
8051 // Two-flag chain: `(#l)` sets LCMATCHUC; `(#I)` should
8052 // clear it. C clears via `~(GF_LCMATCHUC|GF_IGNCASE)`.
8053 let (bits, _, _) = patgetglobflags("(#lI)").unwrap();
8054 assert_eq!(
8055 bits & GF_LCMATCHUC,
8056 0,
8057 "c:1081 — (#I) must clear GF_LCMATCHUC"
8058 );
8059 assert_eq!(
8060 bits & GF_IGNCASE,
8061 0,
8062 "c:1081 — (#I) must clear GF_IGNCASE too"
8063 );
8064 }
8065
8066 /// Pin: `(#L)` is NOT a documented flag per C pattern.c (no
8067 /// 'L' case in the switch). C's default arm returns 0, so
8068 /// patgetglobflags must reject `(#L)`. The previous Rust port
8069 /// accepted it and silently cleared GF_LCMATCHUC, diverging.
8070 #[test]
8071 fn patgetglobflags_rejects_undocumented_flag_letters() {
8072 let _g = crate::test_util::global_state_lock();
8073 // 'L' (capital L) — not a documented C flag.
8074 assert_eq!(
8075 patgetglobflags("(#L)"),
8076 None,
8077 "c:1120 default — unknown flag 'L' must be rejected"
8078 );
8079 // Other lower-rule letters that aren't documented either.
8080 assert_eq!(
8081 patgetglobflags("(#x)"),
8082 None,
8083 "c:1120 default — unknown flag 'x' must be rejected"
8084 );
8085 assert_eq!(
8086 patgetglobflags("(#9)"),
8087 None,
8088 "c:1120 default — bare digit (not after 'a') must be rejected"
8089 );
8090 }
8091
8092 /// Pin: `(#a)` without digits — C `zstrtol` returns 0 with
8093 /// `ptr == nptr` per c:1063. The previous Rust port silently
8094 /// accepted empty-digit form and set errs=0.
8095 #[test]
8096 fn patgetglobflags_rejects_empty_approx_digit_run() {
8097 let _g = crate::test_util::global_state_lock();
8098 // `(#a)` with no digits after 'a' — C rejects (c:1063
8099 // `ptr == nptr` check).
8100 assert_eq!(
8101 patgetglobflags("(#a)"),
8102 None,
8103 "c:1063 — `(#a)` without digits must be rejected"
8104 );
8105 }
8106
8107 #[test]
8108 fn pattry_no_anchor_default() {
8109 let _g = crate::test_util::global_state_lock();
8110 // patmatch with anchored compile: only full-string matches succeed.
8111 let prog = compile("foo");
8112 assert!(pattry(&prog, "foo"));
8113 }
8114
8115 /// PAT_NOGLD ("don't glob dots", set by `parsecomplist` at
8116 /// `Src/glob.c:715` whenever GLOB_DOTS is off) rejects a leading dot
8117 /// only where a WILDCARD opcode would consume it — `P_NOTDOT` is
8118 /// `p->l & 0x40` (`Src/pattern.c:202`), the bit shared by
8119 /// P_ANY/P_ANYOF/P_ANYBUT/P_STAR/P_NUM* (`Src/pattern.c:116-124`),
8120 /// tested at `Src/pattern.c:2731`. A dot written literally in the
8121 /// pattern is matched by `P_EXACTLY` (0x03), which does not carry the
8122 /// bit, and P_BRANCH (0x20) does not either — so grouping a literal
8123 /// dot keeps it literal.
8124 ///
8125 /// Measured against `zsh -f` 5.9 in a directory holding `.hidden` and
8126 /// `xhidden`: `(.)hidden` and `(.|x)hidden` list `.hidden`, while
8127 /// `[.]hidden`, `?hidden` and `*hidden` list nothing. This port used to
8128 /// gate on `prog.patstartch != '.'`, which is recorded only for a
8129 /// program whose first node is a bare `P_EXACTLY` (`Src/pattern.c:709`),
8130 /// so every grouped or flag-prefixed literal dot lost its dot files.
8131 #[test]
8132 fn nogld_rejects_a_leading_dot_only_for_wildcard_opcodes() {
8133 let _g = crate::test_util::global_state_lock();
8134 let file_prog = |p: &str| {
8135 let _t = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
8136 patcompile(
8137 &{
8138 let mut tok = p.to_string();
8139 crate::ported::glob::tokenize(&mut tok);
8140 tok
8141 },
8142 (PAT_FILE | PAT_NOGLD | PAT_HEAPDUP) as i32,
8143 None,
8144 )
8145 .expect("compile failed")
8146 };
8147
8148 // A literal dot still matches, however it is written. `.hidde[n]`
8149 // keeps the program off the pure-string path (c:2347) so the walker
8150 // is what decides, exactly as during a real glob.
8151 for pat in ["(.)hidden", "(.|x)hidden", ".hidde[n]"] {
8152 assert!(
8153 pattry(&file_prog(pat), ".hidden"),
8154 "c:2731 — `{pat}` matches a leading dot with a literal P_EXACTLY dot"
8155 );
8156 }
8157 // A wildcard at the start does not — c:2731's P_NOTDOT gate.
8158 for pat in ["*hidden", "?hidden", "[.]hidden", "*"] {
8159 assert!(
8160 !pattry(&file_prog(pat), ".hidden"),
8161 "c:2733 — `{pat}` must not match a dot file with GLOB_DOTS off"
8162 );
8163 }
8164 // The gate is anchored at the start of the subject and off for
8165 // non-dot subjects (c:2732 `patinput == patinstart`).
8166 assert!(pattry(&file_prog("*hidden"), "xhidden"));
8167 assert!(pattry(&file_prog("x?idden"), "xhidden"));
8168 assert!(pattry(&file_prog("x*"), "x.hidden"));
8169
8170 // c:3322-3325 — the SECOND gate, on the operand of a P_ONEHASH /
8171 // P_TWOHASH. Reaching the c:2731 gate through the closure's own
8172 // recursion is not the same thing: it fails only the operand, and
8173 // P_ONEHASH's `min` of 0 (c:3328) treats that as zero repetitions and
8174 // lets the continuation match the dot anyway. Needs EXTENDED_GLOB for
8175 // `#` to be a closure at all (c:480-483 rewrites zpc_special[ZPC_HASH]
8176 // to Marker when it is off).
8177 let had_xglob = opt_state_get("extendedglob").unwrap_or(false);
8178 opt_state_set("extendedglob", true);
8179 for pat in ["[a]#.hidden", "?#.hidden"] {
8180 assert!(
8181 !pattry(&file_prog(pat), ".hidden"),
8182 "c:3325 — `{pat}`'s closure operand is P_NOTDOT, so the whole \
8183 match fails; zero repetitions must not let the dot through"
8184 );
8185 }
8186 // A P_BRANCH operand (0x20) is not P_NOTDOT, so this one DOES match —
8187 // both here and in `zsh -f`. It is what pins the rule to the operand's
8188 // opcode rather than to "there is a closure".
8189 assert!(pattry(&file_prog("(x)#.hidden"), ".hidden"));
8190 // P_TWOHASH needs one real repetition anyway (c:3328 `min = 1`).
8191 assert!(!pattry(&file_prog("[a]##.hidden"), ".hidden"));
8192 opt_state_set("extendedglob", had_xglob);
8193 }
8194
8195 /// `<a-b>` numeric range: digits matching n where lo ≤ n ≤ hi.
8196 /// Port of pattern.c:1528 (Inang case).
8197 #[test]
8198 fn numeric_range_inclusive() {
8199 let _g = crate::test_util::global_state_lock();
8200 let prog = compile("<10-20>");
8201 assert!(pattry(&prog, "15"));
8202 assert!(pattry(&prog, "10"));
8203 assert!(pattry(&prog, "20"));
8204 assert!(!pattry(&prog, "9"));
8205 assert!(!pattry(&prog, "21"));
8206 }
8207
8208 #[test]
8209 fn numeric_range_from_only() {
8210 let _g = crate::test_util::global_state_lock();
8211 // <100-> matches any number ≥ 100.
8212 let prog = compile("<100->");
8213 assert!(pattry(&prog, "100"));
8214 assert!(pattry(&prog, "9999"));
8215 assert!(!pattry(&prog, "99"));
8216 }
8217
8218 #[test]
8219 fn numeric_range_to_only() {
8220 let _g = crate::test_util::global_state_lock();
8221 // <-5> matches any number ≤ 5.
8222 let prog = compile("<-5>");
8223 assert!(pattry(&prog, "0"));
8224 assert!(pattry(&prog, "5"));
8225 assert!(!pattry(&prog, "6"));
8226 }
8227
8228 #[test]
8229 fn numeric_range_any() {
8230 let _g = crate::test_util::global_state_lock();
8231 let prog = compile("<->");
8232 assert!(pattry(&prog, "0"));
8233 assert!(pattry(&prog, "12345"));
8234 assert!(!pattry(&prog, "abc"));
8235 }
8236
8237 /// `(foo)#` — zero-or-more group repetition (extendedglob).
8238 #[test]
8239 fn group_with_hash_quantifier() {
8240 let _g = crate::test_util::global_state_lock();
8241 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8242 crate::ported::options::opt_state_set("extendedglob", true);
8243 let prog = compile("(foo)#");
8244 assert!(pattry(&prog, ""));
8245 assert!(pattry(&prog, "foo"));
8246 assert!(pattry(&prog, "foofoofoo"));
8247 crate::ported::options::opt_state_set("extendedglob", saved);
8248 }
8249
8250 /// `(a|b)##` — one-or-more group with alternation (extendedglob).
8251 #[test]
8252 fn group_alt_with_double_hash() {
8253 let _g = crate::test_util::global_state_lock();
8254 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8255 crate::ported::options::opt_state_set("extendedglob", true);
8256 let prog = compile("(a|b)##");
8257 assert!(!pattry(&prog, ""));
8258 assert!(pattry(&prog, "a"));
8259 assert!(pattry(&prog, "abab"));
8260 crate::ported::options::opt_state_set("extendedglob", saved);
8261 }
8262
8263 /// Mixed numeric range and literal: `v<1-99>`.
8264 #[test]
8265 fn literal_then_numeric_range() {
8266 let _g = crate::test_util::global_state_lock();
8267 let prog = compile("v<1-99>");
8268 assert!(pattry(&prog, "v1"));
8269 assert!(pattry(&prog, "v50"));
8270 assert!(pattry(&prog, "v99"));
8271 assert!(!pattry(&prog, "v100"));
8272 assert!(!pattry(&prog, "v0"));
8273 }
8274
8275 /// Star is greedy — backtracks correctly with trailing literal.
8276 #[test]
8277 fn star_greedy_backtracks() {
8278 let _g = crate::test_util::global_state_lock();
8279 let prog = compile("*.txt");
8280 assert!(pattry(&prog, "foo.txt"));
8281 assert!(pattry(&prog, "a.b.c.txt"));
8282 assert!(!pattry(&prog, "foo.txx"));
8283 }
8284
8285 /// Bracket with POSIX class (extendedglob for `##`).
8286 #[test]
8287 fn posix_alpha_class() {
8288 let _g = crate::test_util::global_state_lock();
8289 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8290 crate::ported::options::opt_state_set("extendedglob", true);
8291 let prog = compile("[[:alpha:]]##");
8292 assert!(pattry(&prog, "abc"));
8293 assert!(pattry(&prog, "XYZ"));
8294 assert!(!pattry(&prog, "1"));
8295 assert!(!pattry(&prog, ""));
8296 crate::ported::options::opt_state_set("extendedglob", saved);
8297 }
8298
8299 /// `*` must backtrack over CHARACTER starts, not bytes.
8300 ///
8301 /// c:3310-3315 records every character start in `charstart[]` while
8302 /// walking the input with `CHARINC`, and c:3385-3389 rewinds to the
8303 /// previous recorded start (`while (!*--lastcharstart)`). Stepping back
8304 /// one BYTE left the continuation opcode inside a multibyte character:
8305 /// `[[ $'αβγδε' = *(#b)(?) ]]` aborted the shell with
8306 /// "start byte index 9 is not a char boundary".
8307 ///
8308 /// zsh 5.9.2 oracle (`zsh -fc "setopt extendedglob; [[ αβγδε = *(#b)(?) ]] &&
8309 /// print \$mbegin \$mend \$match"`) → `5 5 ε`.
8310 #[test]
8311 fn star_backtracks_over_character_starts_not_bytes() {
8312 let _g = crate::test_util::global_state_lock();
8313 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8314 crate::ported::options::opt_state_set("extendedglob", true);
8315
8316 // The pattern that used to panic; every `*` rewind position must be
8317 // a character start, so the trailing `?` sees a whole `ε`.
8318 let prog = compile("*(#b)(?)");
8319 assert!(
8320 pattry(&prog, "αβγδε"),
8321 "c:3374-3382 — `*` then `?` must match"
8322 );
8323 assert_eq!(
8324 crate::ported::params::getaparam("match"),
8325 Some(vec!["ε".to_string()]),
8326 "c:2587 — the capture is the last CHARACTER, not a trailing byte"
8327 );
8328 assert_eq!(
8329 crate::ported::params::getaparam("mbegin"),
8330 Some(vec!["5".to_string()]),
8331 "c:2596-2599 — CHARSUB offset of the 5th character"
8332 );
8333
8334 // 4-byte subjects exercise the same rewind with a wider character.
8335 let emoji = compile("*(#b)(?)");
8336 assert!(pattry(&emoji, "\u{1F600}\u{1F601}\u{1F602}"));
8337 assert_eq!(
8338 crate::ported::params::getaparam("match"),
8339 Some(vec!["\u{1F602}".to_string()])
8340 );
8341
8342 crate::ported::options::opt_state_set("extendedglob", saved);
8343 }
8344
8345 /// `(#U)` (c:1115-1117 `patglobflags &= ~GF_MULTIBYTE`) must reach the
8346 /// matcher, and the matcher must key its character UNIT off that bit
8347 /// (c:1946 `if (!(patglobflags & GF_MULTIBYTE) || ...) return x + 1;`).
8348 ///
8349 /// Two ports were missing: `P_ANY` advanced a whole UTF-8 character
8350 /// unconditionally, and the mid-pattern P_GFLAGS payload was built from
8351 /// `patgetglobflags`'s set-only delta instead of the running
8352 /// `patglobflags` (c:993 `up.l = patglobflags`), which cleared
8353 /// GF_MULTIBYTE for the rest of the match.
8354 ///
8355 /// zsh 5.9.2 oracle (`zsh -fc 'setopt extendedglob; [[ αβγδε = (#U)????? ]]'`
8356 /// → 1; the ten-`?` form → 0).
8357 #[test]
8358 fn upper_u_flag_makes_quest_match_one_byte() {
8359 let _g = crate::test_util::global_state_lock();
8360 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8361 crate::ported::options::opt_state_set("extendedglob", true);
8362
8363 // "αβγδε" is 5 characters but 10 bytes.
8364 let five = compile("(#U)?????");
8365 assert!(
8366 !pattry(&five, "αβγδε"),
8367 "c:1946 — with GF_MULTIBYTE clear `?` is one BYTE, so five don't span it"
8368 );
8369 let ten = compile("(#U)??????????");
8370 assert!(
8371 pattry(&ten, "αβγδε"),
8372 "c:1946 — ten `?` cover the ten raw bytes"
8373 );
8374 // `(#u)` (c:1111-1113) puts it back.
8375 let u_five = compile("(#u)?????");
8376 assert!(pattry(&u_five, "αβγδε"));
8377 // Default (no flag) stays multibyte.
8378 let plain = compile("?????");
8379 assert!(pattry(&plain, "αβγδε"));
8380
8381 crate::ported::options::opt_state_set("extendedglob", saved);
8382 }
8383
8384 /// `(#i)foo` matches "FOO" / "Foo" / etc. Port of pattern.c
8385 /// patgetglobflags `i` case at c:1091 (sets GF_IGNCASE which
8386 /// patcompile hoists into patprog.flags as PAT_LCMATCHUC).
8387 #[test]
8388 fn case_insensitive_via_glob_flag() {
8389 let _g = crate::test_util::global_state_lock();
8390 // `(#...)` flag specs require EXTENDEDGLOB per zsh.
8391 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8392 crate::ported::options::opt_state_set("extendedglob", true);
8393 let prog = compile("(#i)foo");
8394 assert!(pattry(&prog, "foo"));
8395 assert!(pattry(&prog, "FOO"));
8396 assert!(pattry(&prog, "Foo"));
8397 assert!(pattry(&prog, "fOo"));
8398 crate::ported::options::opt_state_set("extendedglob", saved);
8399 }
8400
8401 /// `(#i)` does NOT reach inside a bracket expression.
8402 ///
8403 /// C matches a bracket with `patmatchrange` / `mb_patmatchrange`
8404 /// (c:2780-2800), neither of which looks at `patglobflags` — only the
8405 /// CHARMATCH macro (c:2671, used by P_EXACTLY) honours GF_IGNCASE. So
8406 /// `(#i)` folds literal characters but leaves `[abc]` / `[a-z]` /
8407 /// `[[:lower:]]` case-SENSITIVE.
8408 ///
8409 /// zsh 5.9.1 oracle (`zsh -fc 'setopt extendedglob; [[ X = (#i)PAT ]]'`):
8410 /// `[[ A = (#i)[abc] ]]` → no match
8411 /// `[[ b = (#i)[abc] ]]` → match
8412 /// `[[ A = (#i)[ABC] ]]` → match
8413 /// `[[ a = (#i)[ABC] ]]` → no match
8414 ///
8415 /// This test previously asserted that "A" matched `(#i)[abc]`, pinning
8416 /// the folded-bracket bug rather than zsh's behaviour.
8417 #[test]
8418 fn case_insensitive_bracket() {
8419 let _g = crate::test_util::global_state_lock();
8420 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8421 crate::ported::options::opt_state_set("extendedglob", true);
8422
8423 let prog = compile("(#i)[abc]");
8424 assert!(
8425 !pattry(&prog, "A"),
8426 "(#i) must not case-fold a bracket: zsh does not match A against [abc]"
8427 );
8428 assert!(pattry(&prog, "b"));
8429 assert!(!pattry(&prog, "d"));
8430
8431 // The uppercase set is the mirror image: it matches "A", not "a".
8432 let upper = compile("(#i)[ABC]");
8433 assert!(pattry(&upper, "A"));
8434 assert!(!pattry(&upper, "a"));
8435
8436 // …while a literal in the same pattern still folds (CHARMATCH).
8437 let lit = compile("(#i)abc");
8438 assert!(pattry(&lit, "ABC"));
8439
8440 crate::ported::options::opt_state_set("extendedglob", saved);
8441 }
8442
8443 /// Unicode case-fold for `(#i)` — non-ASCII Latin chars.
8444 #[test]
8445 fn case_insensitive_unicode() {
8446 let _g = crate::test_util::global_state_lock();
8447 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8448 crate::ported::options::opt_state_set("extendedglob", true);
8449 // German Ü/ü and É/é folded via char::to_lowercase.
8450 let prog = compile("(#i)Über");
8451 assert!(pattry(&prog, "über"));
8452 assert!(pattry(&prog, "ÜBER"));
8453 let prog2 = compile("(#i)café");
8454 assert!(pattry(&prog2, "CAFÉ"));
8455 assert!(pattry(&prog2, "Café"));
8456 crate::ported::options::opt_state_set("extendedglob", saved);
8457 }
8458
8459 /// Without `(#i)`, exact case required.
8460 #[test]
8461 fn case_sensitive_default() {
8462 let _g = crate::test_util::global_state_lock();
8463 let prog = compile("foo");
8464 assert!(pattry(&prog, "foo"));
8465 assert!(!pattry(&prog, "FOO"));
8466 }
8467
8468 /// Mid-pattern P_GFLAGS opcode: `foo(#i)Bar` — first half exact,
8469 /// second half case-insensitive.
8470 #[test]
8471 fn mid_pattern_gflags_switch() {
8472 let _g = crate::test_util::global_state_lock();
8473 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8474 crate::ported::options::opt_state_set("extendedglob", true);
8475 let prog = compile("foo(#i)bar");
8476 assert!(pattry(&prog, "fooBAR"));
8477 assert!(pattry(&prog, "foobar"));
8478 assert!(pattry(&prog, "fooBaR"));
8479 // First half still case-sensitive — "FOOBAR" should NOT match.
8480 assert!(!pattry(&prog, "FOOBAR"));
8481 crate::ported::options::opt_state_set("extendedglob", saved);
8482 }
8483
8484 /// `(#s)foo` — start-of-string anchor.
8485 #[test]
8486 fn start_anchor() {
8487 let _g = crate::test_util::global_state_lock();
8488 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8489 crate::ported::options::opt_state_set("extendedglob", true);
8490 let prog = compile("(#s)foo");
8491 assert!(pattry(&prog, "foo"));
8492 crate::ported::options::opt_state_set("extendedglob", saved);
8493 }
8494
8495 /// `foo(#e)` — end-of-string anchor.
8496 #[test]
8497 fn end_anchor() {
8498 let _g = crate::test_util::global_state_lock();
8499 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8500 crate::ported::options::opt_state_set("extendedglob", true);
8501 let prog = compile("foo(#e)");
8502 assert!(pattry(&prog, "foo"));
8503 crate::ported::options::opt_state_set("extendedglob", saved);
8504 }
8505
8506 /// `x(#c3,5)` — counted repetition: match `x` 3 to 5 times.
8507 /// c:pattern.c:1606-1696 — POSTFIX `(#cN,M)` modifier on preceding piece.
8508 ///
8509 /// EXTENDED_GLOB must be on: `(#c…)` is gated behind
8510 /// `zpc_special[ZPC_HASH]` (c:482), so with the option off the `#` is a
8511 /// literal and this is an ordinary group. Verified against zsh 5.9.1:
8512 /// `zsh -fc '[[ xxx = x(#c3) ]]'` does NOT match, but does under
8513 /// `setopt extendedglob`.
8514 #[test]
8515 fn count_range_3_to_5() {
8516 let _g = crate::test_util::global_state_lock();
8517 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8518 crate::ported::options::opt_state_set("extendedglob", true);
8519 let prog = compile("x(#c3,5)");
8520 assert!(!pattry(&prog, "xx"));
8521 assert!(pattry(&prog, "xxx"));
8522 assert!(pattry(&prog, "xxxx"));
8523 assert!(pattry(&prog, "xxxxx"));
8524 assert!(!pattry(&prog, "xxxxxx"));
8525 crate::ported::options::opt_state_set("extendedglob", saved);
8526 }
8527
8528 /// `x(#c3)` — exact count: `xxx` only.
8529 #[test]
8530 fn count_exact_3() {
8531 let _g = crate::test_util::global_state_lock();
8532 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8533 crate::ported::options::opt_state_set("extendedglob", true);
8534 let prog = compile("x(#c3)");
8535 assert!(!pattry(&prog, "xx"));
8536 assert!(pattry(&prog, "xxx"));
8537 assert!(!pattry(&prog, "xxxx"));
8538 crate::ported::options::opt_state_set("extendedglob", saved);
8539 }
8540
8541 /// Without EXTENDED_GLOB, `(#cN,M)` is NOT a counted closure — the `#`
8542 /// is a literal, so `x(#c3)` is `x` followed by a group matching the
8543 /// literal text `#c3`. c:482 rewrites `zpc_special[ZPC_HASH]` to Marker
8544 /// when the option is off, which is what makes the c:1609 test fail.
8545 ///
8546 /// zsh 5.9.1 oracle:
8547 /// `zsh -fc '[[ xxx = x(#c3) ]] && echo Y || echo N'` → N
8548 /// `zsh -fc 'setopt extendedglob; [[ xxx = x(#c3) ]] …'` → Y
8549 #[test]
8550 fn count_inert_without_extendedglob() {
8551 let _g = crate::test_util::global_state_lock();
8552 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8553 crate::ported::options::opt_state_set("extendedglob", false);
8554 let prog = compile("x(#c3)");
8555 assert!(
8556 !pattry(&prog, "xxx"),
8557 "(#c3) must not act as a counted closure with EXTENDED_GLOB off"
8558 );
8559 crate::ported::options::opt_state_set("extendedglob", saved);
8560 }
8561
8562 #[test]
8563 fn debug_alt_b() {
8564 let _g = crate::test_util::global_state_lock();
8565 let prog = compile("(a)|b");
8566 eprintln!("bytecode len: {}", prog.1.len());
8567 for (i, b) in prog.1.iter().enumerate() {
8568 eprintln!(" [{:3}] {:#04x}", i, b);
8569 }
8570 let mut state = rpat::new();
8571 let r = super::patmatch(&prog.1, 0, "b", 0, &mut state, prog.0.flags);
8572 eprintln!("match result: {:?}", r);
8573 assert!(pattry(&prog, "b"));
8574 }
8575
8576 /// `x(#c2,)` — at least 2.
8577 #[test]
8578 fn count_min_only() {
8579 let _g = crate::test_util::global_state_lock();
8580 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8581 crate::ported::options::opt_state_set("extendedglob", true);
8582 let prog = compile("x(#c2,)");
8583 assert!(!pattry(&prog, "x"));
8584 assert!(pattry(&prog, "xx"));
8585 assert!(pattry(&prog, "xxxxxxxx"));
8586 crate::ported::options::opt_state_set("extendedglob", saved);
8587 }
8588
8589 #[test]
8590 fn captures_unmatched_group_returns_no_match() {
8591 let _g = crate::test_util::global_state_lock();
8592 // Pattern with alt — first branch fails, second succeeds; check
8593 // captures from successful branch only.
8594 let prog = compile("(a)|b");
8595 assert!(pattry(&prog, "a"));
8596 assert!(pattry(&prog, "b"));
8597 }
8598
8599 /// c:540 — `*.ext` glob: `*` matches any prefix including empty.
8600 /// Regression requiring non-empty match would break `for f in *.txt`
8601 /// against directories containing dotfiles like `.txt`.
8602 #[test]
8603 fn patmatch_star_matches_empty_prefix() {
8604 let _g = crate::test_util::global_state_lock();
8605 assert!(patmatch("*.txt", "a.txt"));
8606 assert!(patmatch("*.txt", ".txt"));
8607 assert!(!patmatch("*.txt", "a.rs"));
8608 }
8609
8610 /// c:540 — `?` matches exactly one char. Regression accepting
8611 /// empty or multi-char would break filename-mangling patterns.
8612 #[test]
8613 fn patmatch_question_matches_exactly_one_char() {
8614 let _g = crate::test_util::global_state_lock();
8615 assert!(patmatch("?.txt", "a.txt"));
8616 assert!(!patmatch("?.txt", "ab.txt"));
8617 assert!(!patmatch("?.txt", ".txt"));
8618 }
8619
8620 /// c:540 — char-class `[abc]` matches any listed char.
8621 #[test]
8622 fn patmatch_char_class_matches_listed_chars() {
8623 let _g = crate::test_util::global_state_lock();
8624 assert!(patmatch("[abc].txt", "a.txt"));
8625 assert!(patmatch("[abc].txt", "b.txt"));
8626 assert!(patmatch("[abc].txt", "c.txt"));
8627 assert!(!patmatch("[abc].txt", "d.txt"));
8628 }
8629
8630 /// Regression: `*[abc]*` over a string containing a multibyte char
8631 /// (U+E0B0, a powerline separator) must not panic. P_STAR backtracks
8632 /// byte-by-byte, so the continuation P_ANYOF was tested at a byte offset
8633 /// inside the multibyte char; slicing `string[off..]` there panicked with
8634 /// "not a char boundary". A non-boundary offset now falls to a raw-byte
8635 /// test (advance 1) instead of decoding.
8636 #[test]
8637 fn patmatch_anyof_at_multibyte_continuation_byte_no_panic() {
8638 let _g = crate::test_util::global_state_lock();
8639 assert!(!patmatch("*[abc]*", "\u{e0b0}"));
8640 assert!(patmatch("*[abc]*", "a\u{e0b0}b"));
8641 assert!(!patmatch("*[!abc]*", "abc"));
8642 assert!(patmatch("*[!abc]*", "a\u{e0b0}b"));
8643 }
8644
8645 /// c:540 — negated `[!abc]` matches any char NOT in the set.
8646 #[test]
8647 fn patmatch_negated_char_class_inverts() {
8648 let _g = crate::test_util::global_state_lock();
8649 assert!(patmatch("[!abc].txt", "d.txt"));
8650 assert!(!patmatch("[!abc].txt", "a.txt"));
8651 }
8652
8653 /// c:540 — range `[a-z]` ASCII range; uppercase outside.
8654 #[test]
8655 fn patmatch_range_matches_ascii_range() {
8656 let _g = crate::test_util::global_state_lock();
8657 assert!(patmatch("[a-z]bc", "abc"));
8658 assert!(patmatch("[a-z]bc", "zbc"));
8659 assert!(!patmatch("[a-z]bc", "Abc"));
8660 assert!(!patmatch("[a-z]bc", "0bc"));
8661 }
8662
8663 /// c:540 — literal patterns require exact-string equality. A
8664 /// substring-match regression would silently break `case foo in
8665 /// abc) ;;`.
8666 #[test]
8667 fn patmatch_literal_requires_exact_string_equality() {
8668 let _g = crate::test_util::global_state_lock();
8669 assert!(patmatch("abc", "abc"));
8670 assert!(!patmatch("abc", "abcd"));
8671 assert!(!patmatch("abc", "ab"));
8672 assert!(!patmatch("abc", ""));
8673 }
8674
8675 /// `Src/pattern.c:517-526` — `patcompstart` reads CASEGLOB /
8676 /// CASEPATHS / MULTIBYTE option state into `patglobflags`. The
8677 /// previous Rust port hardcoded `GF_MULTIBYTE` unconditionally
8678 /// (ignoring MULTIBYTE option) AND never set `GF_IGNCASE`
8679 /// (ignoring CASEGLOB option entirely). Pin all three branches.
8680 #[test]
8681 fn patcompstart_sets_patglobflags_per_option_state() {
8682 let _g = crate::test_util::global_state_lock();
8683 let saved_caseglob = opt_state_get("caseglob").unwrap_or(false);
8684 let saved_casepaths = opt_state_get("casepaths").unwrap_or(false);
8685 let saved_multibyte = opt_state_get("multibyte").unwrap_or(false);
8686
8687 // 1. CASEGLOB ON + CASEPATHS ON + MULTIBYTE ON → flags = GF_MULTIBYTE only.
8688 opt_state_set("caseglob", true);
8689 opt_state_set("casepaths", true);
8690 opt_state_set("multibyte", true);
8691 patcompstart();
8692 let f = patglobflags.load(Ordering::Relaxed);
8693 assert_eq!(f & GF_IGNCASE, 0, "c:521 — CASEGLOB on → GF_IGNCASE off");
8694 assert_ne!(
8695 f & GF_MULTIBYTE,
8696 0,
8697 "c:525 — MULTIBYTE on → GF_MULTIBYTE bit set"
8698 );
8699
8700 // 2. CASEGLOB OFF + CASEPATHS OFF → flags |= GF_IGNCASE.
8701 opt_state_set("caseglob", false);
8702 opt_state_set("casepaths", false);
8703 patcompstart();
8704 let f = patglobflags.load(Ordering::Relaxed);
8705 assert_ne!(
8706 f & GF_IGNCASE,
8707 0,
8708 "c:523 — default case-insensitive → GF_IGNCASE bit set"
8709 );
8710
8711 // 3. MULTIBYTE OFF → GF_MULTIBYTE bit cleared.
8712 opt_state_set("multibyte", false);
8713 patcompstart();
8714 let f = patglobflags.load(Ordering::Relaxed);
8715 assert_eq!(
8716 f & GF_MULTIBYTE,
8717 0,
8718 "c:524 — !MULTIBYTE → GF_MULTIBYTE bit clear"
8719 );
8720
8721 // Restore.
8722 opt_state_set("caseglob", saved_caseglob);
8723 opt_state_set("casepaths", saved_casepaths);
8724 opt_state_set("multibyte", saved_multibyte);
8725 }
8726
8727 /// `Src/zsh.h:224` — `#define Marker ((char) 0xa2)`. The
8728 /// pattern.rs local `Marker` const must equal the canonical
8729 /// zsh_h::Marker byte value. The previous Rust port had
8730 /// `pub const Marker: u8 = 0x80` (wrong; 0x80 is not a token
8731 /// byte in zsh.h at all). Now aliases the canonical const
8732 /// so both names point to the same byte.
8733 #[test]
8734 fn pattern_marker_alias_matches_canonical_zsh_h_marker() {
8735 let _g = crate::test_util::global_state_lock();
8736 // c:224 — canonical Marker is 0xa2.
8737 assert_eq!(
8738 Marker as u8, 0xa2_u8,
8739 "Src/zsh.h:224 — Marker must be 0xa2 (not 0x80)"
8740 );
8741 assert_eq!(
8742 Marker as u8, Marker as u8,
8743 "pattern.rs::Marker must alias zsh_h::Marker"
8744 );
8745 }
8746
8747 /// `Src/pattern.c:464-510` — `patcompcharsset` masks special chars
8748 /// based on EXTENDEDGLOB, KSHGLOB, and SHGLOB. The previous Rust
8749 /// port omitted ALL THREE option-driven mask passes plus all six
8750 /// KSH_* slot initialisations. Pin the option-respect contract.
8751 ///
8752 /// Test toggles each option and verifies the corresponding slots
8753 /// flip between default literal char and the `Marker` sentinel.
8754 #[test]
8755 fn patcompcharsset_respects_extendedglob_kshglob_shglob_options() {
8756 let _g = crate::test_util::global_state_lock();
8757 let marker_byte = Marker as u32 as u8;
8758
8759 // Save state.
8760 let saved_extended = opt_state_get("extendedglob").unwrap_or(false);
8761 let saved_ksh = opt_state_get("kshglob").unwrap_or(false);
8762 let saved_sh = opt_state_get("shglob").unwrap_or(false);
8763
8764 // 1. EXTENDEDGLOB off → Tilde/Hat/Hash → Marker.
8765 opt_state_set("extendedglob", false);
8766 opt_state_set("kshglob", true); // so KSH_* slots stay literal
8767 opt_state_set("shglob", false); // so Inpar/Inang stay literal
8768 patcompcharsset();
8769 {
8770 let sp = zpc_special.lock().unwrap();
8771 assert_eq!(
8772 sp[ZPC_TILDE as usize], marker_byte,
8773 "c:480 — !EXTENDEDGLOB → Tilde = Marker"
8774 );
8775 assert_eq!(
8776 sp[ZPC_HAT as usize], marker_byte,
8777 "c:481 — !EXTENDEDGLOB → Hat = Marker"
8778 );
8779 assert_eq!(
8780 sp[ZPC_HASH as usize], marker_byte,
8781 "c:482 — !EXTENDEDGLOB → Hash = Marker"
8782 );
8783 }
8784
8785 // 2. EXTENDEDGLOB on → Tilde/Hat/Hash → literal chars.
8786 opt_state_set("extendedglob", true);
8787 patcompcharsset();
8788 {
8789 let sp = zpc_special.lock().unwrap();
8790 assert_eq!(
8791 sp[ZPC_TILDE as usize], b'~',
8792 "c:478 — EXTENDEDGLOB on → Tilde = literal '~'"
8793 );
8794 assert_eq!(sp[ZPC_HAT as usize], b'^');
8795 assert_eq!(sp[ZPC_HASH as usize], b'#');
8796 }
8797
8798 // 3. KSHGLOB off → KSH_* slots → Marker.
8799 opt_state_set("kshglob", false);
8800 patcompcharsset();
8801 {
8802 let sp = zpc_special.lock().unwrap();
8803 assert_eq!(
8804 sp[ZPC_KSH_QUEST as usize], marker_byte,
8805 "c:486 — !KSHGLOB → KSH_QUEST = Marker"
8806 );
8807 assert_eq!(sp[ZPC_KSH_STAR as usize], marker_byte);
8808 assert_eq!(sp[ZPC_KSH_PLUS as usize], marker_byte);
8809 assert_eq!(sp[ZPC_KSH_BANG as usize], marker_byte);
8810 assert_eq!(sp[ZPC_KSH_BANG2 as usize], marker_byte);
8811 assert_eq!(sp[ZPC_KSH_AT as usize], marker_byte);
8812 }
8813
8814 // 4. KSHGLOB on → KSH_* slots → literal trigger chars.
8815 opt_state_set("kshglob", true);
8816 patcompcharsset();
8817 {
8818 let sp = zpc_special.lock().unwrap();
8819 assert_eq!(
8820 sp[ZPC_KSH_QUEST as usize], b'?',
8821 "c:478 — KSHGLOB on → KSH_QUEST = '?'"
8822 );
8823 assert_eq!(sp[ZPC_KSH_STAR as usize], b'*');
8824 assert_eq!(sp[ZPC_KSH_PLUS as usize], b'+');
8825 assert_eq!(sp[ZPC_KSH_BANG as usize], b'!');
8826 assert_eq!(sp[ZPC_KSH_BANG2 as usize], b'!');
8827 assert_eq!(sp[ZPC_KSH_AT as usize], b'@');
8828 }
8829
8830 // 5. SHGLOB on → Inpar/Inang → Marker.
8831 opt_state_set("shglob", true);
8832 patcompcharsset();
8833 {
8834 let sp = zpc_special.lock().unwrap();
8835 assert_eq!(
8836 sp[ZPC_INPAR as usize], marker_byte,
8837 "c:501 — SHGLOB on → Inpar = Marker"
8838 );
8839 assert_eq!(
8840 sp[ZPC_INANG as usize], marker_byte,
8841 "c:501 — SHGLOB on → Inang = Marker"
8842 );
8843 }
8844
8845 // 6. SHGLOB off → Inpar/Inang → literal chars.
8846 opt_state_set("shglob", false);
8847 patcompcharsset();
8848 {
8849 let sp = zpc_special.lock().unwrap();
8850 assert_eq!(
8851 sp[ZPC_INPAR as usize], b'(',
8852 "c:478 — !SHGLOB → Inpar = '('"
8853 );
8854 assert_eq!(sp[ZPC_INANG as usize], b'<');
8855 }
8856
8857 // Restore.
8858 opt_state_set("extendedglob", saved_extended);
8859 opt_state_set("kshglob", saved_ksh);
8860 opt_state_set("shglob", saved_sh);
8861 }
8862
8863 /// `Src/pattern.c:500-510` — under SHGLOB `zpc_special[ZPC_INPAR]`
8864 /// is `Marker`, so `(` is an ORDINARY character: the compiler must
8865 /// not open a group (c:1509-1512's two DPUTS assert exactly that),
8866 /// the `(#…)` flag specifier must not fire (c:953-954 tests the
8867 /// INPAR slot as well as the HASH slot), and a `)` with nothing to
8868 /// close joins the literal run (c:1294-1319).
8869 ///
8870 /// Reference behaviour, `zsh -f`:
8871 /// `setopt shglob; [[ "(a" = (a|b) ]]` → 0 (matched `(a`)
8872 /// `setopt shglob; [[ "a" = (a|b) ]]` → 1 (no group)
8873 /// `setopt shglob; [[ "(a)" = (a) ]]` → 0 (all literal)
8874 /// `setopt shglob extendedglob; [[ abc = (#i)ABC ]]` → 1
8875 /// `setopt shglob extendedglob; [[ "i)ABC" = (#i)ABC ]]` → 0
8876 /// (`(#` is a literal `(` under a `#` closure, not a flag spec)
8877 #[test]
8878 fn shglob_makes_paren_an_ordinary_character() {
8879 let _g = crate::test_util::global_state_lock();
8880 let saved_sh = opt_state_get("shglob").unwrap_or(false);
8881 let saved_extended = opt_state_get("extendedglob").unwrap_or(false);
8882
8883 opt_state_set("shglob", true);
8884 opt_state_set("extendedglob", true);
8885
8886 // `(` / `)` are literal, `|` still separates alternatives, so
8887 // `(a|b)` is "`(a`" OR "`b)`" — never the group "a or b".
8888 assert!(patmatch("(a|b)", "(a"), "c:501 — `(` must be literal");
8889 assert!(patmatch("(a|b)", "b)"), "c:1299 — trailing `)` literal");
8890 assert!(!patmatch("(a|b)", "a"), "c:501 — no group under SHGLOB");
8891 // A whole parenthesised run with no alternation is pure text.
8892 assert!(patmatch("(a)", "(a)"), "c:1294-1319 — `(a)` is literal");
8893 // The `(#…)` flag specifier needs an ACTIVE `(` (c:953). With
8894 // EXTENDEDGLOB on the `#` is still the repetition closure, so
8895 // `(#i)ABC` reads as "zero or more `(`" + the text `i)ABC`.
8896 assert!(!patmatch("(#i)ABC", "abc"), "c:953 — no case folding");
8897 assert!(patmatch("(#i)ABC", "i)ABC"), "c:953 — `(#` = `(` closure");
8898 assert!(patmatch("(#i)ABC", "((i)ABC"), "c:953 — `(#` = `(` closure");
8899
8900 // With SHGLOB off the same patterns are groups / flags again.
8901 opt_state_set("shglob", false);
8902 assert!(patmatch("(a|b)", "a"), "c:478 — group restored");
8903 assert!(patmatch("(#i)ABC", "abc"), "c:953 — flag restored");
8904
8905 opt_state_set("shglob", saved_sh);
8906 opt_state_set("extendedglob", saved_extended);
8907 }
8908
8909 /// `Src/pattern.c:4220-4233` — `savepatterndisables` encodes the
8910 /// `zpc_disables[ZPC_COUNT]` byte-array as a u32 bitmask (low bit
8911 /// = slot 0). The previous Rust port returned the WRONG data
8912 /// structure (a `Vec<String>` clone of `patterndisables`, a
8913 /// completely separate name-list global). Pin the round-trip
8914 /// against `restorepatterndisables` so a regen re-introducing the
8915 /// type mismatch breaks the test.
8916 #[test]
8917 fn savepatterndisables_returns_u32_bitmask_round_trip() {
8918 let _g = crate::test_util::global_state_lock();
8919 // Save existing state.
8920 let saved = savepatterndisables();
8921 // Clear everything, install a known pattern.
8922 restorepatterndisables(0);
8923 assert_eq!(
8924 savepatterndisables(),
8925 0,
8926 "c:4220 — all-zeros zpc_disables → 0 bitmask"
8927 );
8928 // Set slot 0 and slot 3.
8929 let want = (1u32 << 0) | (1u32 << 3);
8930 restorepatterndisables(want);
8931 assert_eq!(
8932 savepatterndisables(),
8933 want,
8934 "c:4220 — round-trip: restore → save must yield same bitmask"
8935 );
8936 // Restore prior state so test isolation holds.
8937 restorepatterndisables(saved);
8938 }
8939
8940 /// `Src/pattern.c:4220-4233` — every set bit in the output bitmask
8941 /// corresponds to a non-zero slot in `zpc_disables`. Sweep all
8942 /// ZPC_COUNT slots so a regen that off-by-one's the loop bounds
8943 /// gets caught.
8944 #[test]
8945 fn savepatterndisables_each_slot_maps_to_its_bit() {
8946 let _g = crate::test_util::global_state_lock();
8947 let saved = savepatterndisables();
8948 for slot in 0..(ZPC_COUNT as usize) {
8949 restorepatterndisables(1u32 << slot);
8950 let got = savepatterndisables();
8951 assert_eq!(
8952 got,
8953 1u32 << slot,
8954 "c:4220 — slot {} must map to bit {}, got 0x{:x}",
8955 slot,
8956 slot,
8957 got
8958 );
8959 }
8960 restorepatterndisables(saved);
8961 }
8962
8963 // ═══════════════════════════════════════════════════════════════════
8964 // Additional pattern-matching corner cases — pinning behaviour for
8965 // shapes not previously exercised. Each test uses `patmatch` (which
8966 // takes pattern + text → bool) so the failure mode is unambiguous.
8967 // ═══════════════════════════════════════════════════════════════════
8968
8969 fn match_locked(pat: &str, s: &str) -> bool {
8970 let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
8971 patmatch(pat, s)
8972 }
8973
8974 // ── Anchoring: zsh patterns are anchored by default (whole-string) ─
8975 #[test]
8976 fn literal_anchored_left() {
8977 // Literal "foo" should NOT match "Xfoo" — patmatch is full-string.
8978 let _g = crate::test_util::global_state_lock();
8979 assert!(!match_locked("foo", "Xfoo"));
8980 }
8981
8982 #[test]
8983 fn literal_anchored_right() {
8984 let _g = crate::test_util::global_state_lock();
8985 assert!(!match_locked("foo", "fooX"));
8986 }
8987
8988 #[test]
8989 fn star_only_matches_empty_string() {
8990 let _g = crate::test_util::global_state_lock();
8991 assert!(match_locked("*", ""));
8992 }
8993
8994 #[test]
8995 fn star_prefix() {
8996 let _g = crate::test_util::global_state_lock();
8997 assert!(match_locked("*.txt", "foo.txt"));
8998 assert!(match_locked("*.txt", ".txt"));
8999 assert!(!match_locked("*.txt", "foo.rs"));
9000 }
9001
9002 #[test]
9003 fn star_suffix() {
9004 let _g = crate::test_util::global_state_lock();
9005 assert!(match_locked("foo*", "foo"));
9006 assert!(match_locked("foo*", "foobar"));
9007 assert!(!match_locked("foo*", "fo"));
9008 }
9009
9010 #[test]
9011 fn star_both_sides() {
9012 let _g = crate::test_util::global_state_lock();
9013 assert!(match_locked("*foo*", "barfoobaz"));
9014 assert!(match_locked("*foo*", "foo"));
9015 assert!(!match_locked("*foo*", "bar"));
9016 }
9017
9018 #[test]
9019 fn question_exactly_one_char() {
9020 let _g = crate::test_util::global_state_lock();
9021 assert!(match_locked("?", "a"));
9022 assert!(!match_locked("?", ""));
9023 assert!(!match_locked("?", "ab"));
9024 }
9025
9026 #[test]
9027 fn question_repeated() {
9028 let _g = crate::test_util::global_state_lock();
9029 assert!(match_locked("???", "abc"));
9030 assert!(!match_locked("???", "ab"));
9031 assert!(!match_locked("???", "abcd"));
9032 }
9033
9034 // ── Character classes ────────────────────────────────────────────
9035 #[test]
9036 fn bracket_digit_range_in_context() {
9037 let _g = crate::test_util::global_state_lock();
9038 assert!(match_locked("file[0-9].txt", "file7.txt"));
9039 assert!(!match_locked("file[0-9].txt", "fileA.txt"));
9040 }
9041
9042 #[test]
9043 fn bracket_multiple_ranges() {
9044 let _g = crate::test_util::global_state_lock();
9045 let p = "[a-zA-Z0-9]";
9046 assert!(match_locked(p, "X"));
9047 assert!(match_locked(p, "q"));
9048 assert!(match_locked(p, "7"));
9049 assert!(!match_locked(p, "_"));
9050 assert!(!match_locked(p, "!"));
9051 }
9052
9053 #[test]
9054 fn bracket_posix_class_alpha() {
9055 let _g = crate::test_util::global_state_lock();
9056 assert!(match_locked("[[:alpha:]]", "A"));
9057 assert!(match_locked("[[:alpha:]]", "z"));
9058 assert!(!match_locked("[[:alpha:]]", "9"));
9059 }
9060
9061 #[test]
9062 fn bracket_posix_class_digit() {
9063 let _g = crate::test_util::global_state_lock();
9064 assert!(match_locked("[[:digit:]]", "0"));
9065 assert!(match_locked("[[:digit:]]", "9"));
9066 assert!(!match_locked("[[:digit:]]", "a"));
9067 }
9068
9069 #[test]
9070 fn bracket_posix_class_space() {
9071 let _g = crate::test_util::global_state_lock();
9072 assert!(match_locked("[[:space:]]", " "));
9073 assert!(match_locked("[[:space:]]", "\t"));
9074 assert!(!match_locked("[[:space:]]", "a"));
9075 }
9076
9077 #[test]
9078 fn bracket_negation_with_caret() {
9079 let _g = crate::test_util::global_state_lock();
9080 assert!(match_locked("[^a]", "b"));
9081 assert!(!match_locked("[^a]", "a"));
9082 }
9083
9084 #[test]
9085 fn bracket_negation_with_bang() {
9086 // zsh also accepts `[!abc]` as negation (ksh-compat).
9087 let _g = crate::test_util::global_state_lock();
9088 assert!(match_locked("[!abc]", "z"));
9089 assert!(!match_locked("[!abc]", "b"));
9090 }
9091
9092 // ── Escaping ─────────────────────────────────────────────────────
9093 #[test]
9094 fn escape_question_literal() {
9095 let _g = crate::test_util::global_state_lock();
9096 assert!(match_locked("a\\?b", "a?b"));
9097 assert!(!match_locked("a\\?b", "aXb"));
9098 }
9099
9100 #[test]
9101 fn escape_bracket_literal() {
9102 let _g = crate::test_util::global_state_lock();
9103 assert!(match_locked("a\\[b", "a[b"));
9104 assert!(!match_locked("a\\[b", "aXb"));
9105 }
9106
9107 // ── Alternation across longer text ───────────────────────────────
9108 #[test]
9109 fn alternation_with_star_suffix() {
9110 let _g = crate::test_util::global_state_lock();
9111 assert!(match_locked("(foo|bar)*", "foobaz"));
9112 assert!(match_locked("(foo|bar)*", "bar"));
9113 assert!(!match_locked("(foo|bar)*", "qux"));
9114 }
9115
9116 // ── Hash (zsh-extended quantifier) ───────────────────────────────
9117 // The simple `a#` / `a##` shapes are already covered by the
9118 // long-standing tests above; compound shapes (`aa#b`, `aa##b`)
9119 // are deliberately NOT pinned here because their parse precedence
9120 // would need verification against current C-zsh before claiming
9121 // an expected value.
9122
9123 // ── Mixed wildcards ──────────────────────────────────────────────
9124 #[test]
9125 fn mixed_star_and_question() {
9126 let _g = crate::test_util::global_state_lock();
9127 assert!(match_locked("?*", "a"));
9128 assert!(match_locked("?*", "abc"));
9129 assert!(!match_locked("?*", ""));
9130 }
9131
9132 // ── Empty pattern / empty string ─────────────────────────────────
9133 #[test]
9134 fn empty_pattern_matches_empty_string() {
9135 let _g = crate::test_util::global_state_lock();
9136 assert!(match_locked("", ""));
9137 }
9138
9139 #[test]
9140 fn empty_pattern_rejects_non_empty() {
9141 let _g = crate::test_util::global_state_lock();
9142 assert!(!match_locked("", "x"));
9143 }
9144
9145 // ── haswilds: wildcard detection (used to bypass patcompile) ─────
9146 #[test]
9147 fn haswilds_recognizes_each_meta() {
9148 let _g = crate::test_util::global_state_lock();
9149 // c:Src/glob.c:3548 — tokenize input as every C caller does.
9150 let tok = |s: &str| {
9151 let mut t = s.to_string();
9152 crate::ported::glob::tokenize(&mut t);
9153 t
9154 };
9155 assert!(haswilds(&tok("*")));
9156 assert!(haswilds(&tok("?")));
9157 // Length-1 `[` is the c:4309-4311 exception (bare Inbrack not
9158 // wild), pinned in `haswilds_single_open_bracket_is_not_wild`.
9159 // Use the multi-char form here to verify the Inbrack arm in the
9160 // main metachar scan.
9161 assert!(haswilds(&tok("[abc]")));
9162 assert!(!haswilds(&tok("")));
9163 assert!(!haswilds(&tok("plain.txt")));
9164 }
9165
9166 // ── range_type: POSIX class name lookup ──────────────────────────
9167 #[test]
9168 fn range_type_unknown_returns_none() {
9169 let _g = crate::test_util::global_state_lock();
9170 assert_eq!(range_type(""), None);
9171 assert_eq!(range_type("xyz_not_real"), None);
9172 }
9173
9174 // ═══════════════════════════════════════════════════════════════════
9175 // haswilds — C-pinned tests covering every branch of pattern.c:4306-
9176 // 4374. Each test name pins to a specific C line range; the body
9177 // asserts the behavior that C source mandates.
9178 //
9179 // haswilds scans TOKENIZED strings (C contract — every C caller
9180 // passes lexer- or tokenize()-prepared input). Tests build their
9181 // inputs through the ported `tokenize` (Src/glob.c:3548), the
9182 // same preparation C applies to runtime-built strings
9183 // (compcore.c:2231).
9184 // ═══════════════════════════════════════════════════════════════════
9185
9186 /// `Src/pattern.c:4310-4312` — bare `[` and `]` single-byte
9187 /// returns 0: "`[` and `]` are legal even if bad patterns are
9188 /// usually not." Rust-port adaptation drops this exception for
9189 /// un-tokenized callers — bare `[` IS the start of a char-class
9190 /// wildcard (`[abc]`). Bare `]` is NOT a wildcard (no `Outbrack`
9191 /// `Src/pattern.c:4310-4312` — `[' and `]' are legal even if bad
9192 /// patterns are usually not. Single-byte bare `[` returns 0 so
9193 /// `echo [` prints `[` instead of firing NOMATCH. Real zsh:
9194 /// $ /opt/homebrew/bin/zsh -fc 'echo ['
9195 /// [
9196 /// Previous Rust port returned true here and `echo [` errored
9197 /// with "no matches found: [".
9198 #[test]
9199 fn haswilds_single_open_bracket_is_not_wild() {
9200 let _g = crate::test_util::global_state_lock();
9201 // c:Src/glob.c:3548 — tokenize input as every C caller does.
9202 let tok = |s: &str| {
9203 let mut t = s.to_string();
9204 crate::ported::glob::tokenize(&mut t);
9205 t
9206 };
9207 assert!(
9208 !haswilds(&tok("[")),
9209 "single literal `[` is NOT wild (c:4310-4312 exception)"
9210 );
9211 }
9212
9213 /// `Src/pattern.c:4310-4312` — same exception covers `]`. Bare
9214 /// `]` returns 0 (would already pass without the exception since
9215 /// `]` has no case arm in the metachar switch, but the exception
9216 /// is the canonical reason).
9217 #[test]
9218 fn haswilds_single_close_bracket_is_not_wild() {
9219 let _g = crate::test_util::global_state_lock();
9220 // c:Src/glob.c:3548 — tokenize input as every C caller does.
9221 let tok = |s: &str| {
9222 let mut t = s.to_string();
9223 crate::ported::glob::tokenize(&mut t);
9224 t
9225 };
9226 assert!(
9227 !haswilds(&tok("]")),
9228 "single literal `]` is NOT wild (c:4310-4312 exception)"
9229 );
9230 }
9231
9232 /// `Src/pattern.c:4314-4318` — `%?foo` job-ref special: the `?`
9233 /// immediately after a leading `%` is demoted to literal. C
9234 /// mutates `str[1]` in place; Rust skips position 1.
9235 #[test]
9236 fn haswilds_percent_question_job_ref_is_not_wild() {
9237 let _g = crate::test_util::global_state_lock();
9238 // c:Src/glob.c:3548 — tokenize input as every C caller does.
9239 let tok = |s: &str| {
9240 let mut t = s.to_string();
9241 crate::ported::glob::tokenize(&mut t);
9242 t
9243 };
9244 crate::ported::options::opt_state_set("extendedglob", false);
9245 assert!(
9246 !haswilds(&tok("%?foo")),
9247 "%?foo is a job ref (c:4318), not wild"
9248 );
9249 // But the `?` later in the string IS wild.
9250 assert!(haswilds(&tok("%?foo?bar")), "%? exempt, later ? still wild");
9251 }
9252
9253 /// `Src/pattern.c:4338-4341` — `Bar` / `|`: wild when
9254 /// `zpc_disables[ZPC_BAR] == 0`.
9255 #[test]
9256 fn haswilds_pipe_bar_is_wild_by_default() {
9257 let _g = crate::test_util::global_state_lock();
9258 // c:Src/glob.c:3548 — tokenize input as every C caller does.
9259 let tok = |s: &str| {
9260 let mut t = s.to_string();
9261 crate::ported::glob::tokenize(&mut t);
9262 t
9263 };
9264 assert!(haswilds(&tok("a|b")), "literal `|` is wild (c:4340)");
9265 }
9266
9267 /// `Src/pattern.c:4343-4346` — `Star` / `*`.
9268 #[test]
9269 fn haswilds_star_is_wild() {
9270 let _g = crate::test_util::global_state_lock();
9271 // c:Src/glob.c:3548 — tokenize input as every C caller does.
9272 let tok = |s: &str| {
9273 let mut t = s.to_string();
9274 crate::ported::glob::tokenize(&mut t);
9275 t
9276 };
9277 assert!(haswilds(&tok("*")), "bare * (c:4345)");
9278 assert!(haswilds(&tok("a*b")), "* mid-string");
9279 }
9280
9281 /// `Src/pattern.c:4348-4351` — `Inbrack` / `[`.
9282 #[test]
9283 fn haswilds_inbrack_is_wild() {
9284 let _g = crate::test_util::global_state_lock();
9285 // c:Src/glob.c:3548 — tokenize input as every C caller does.
9286 let tok = |s: &str| {
9287 let mut t = s.to_string();
9288 crate::ported::glob::tokenize(&mut t);
9289 t
9290 };
9291 assert!(haswilds(&tok("[abc]")), "[abc] (c:4350)");
9292 assert!(haswilds(&tok("a[xyz]b")), "[xyz] mid-string");
9293 }
9294
9295 /// `Src/pattern.c:4353-4356` — `Inang` / `<`.
9296 #[test]
9297 fn haswilds_inang_is_wild() {
9298 let _g = crate::test_util::global_state_lock();
9299 // c:Src/glob.c:3548 — tokenize input as every C caller does.
9300 let tok = |s: &str| {
9301 let mut t = s.to_string();
9302 crate::ported::glob::tokenize(&mut t);
9303 t
9304 };
9305 assert!(haswilds(&tok("a<1-9>")), "<n-m> numeric range (c:4355)");
9306 }
9307
9308 /// `Src/pattern.c:4358-4361` — `Quest` / `?`.
9309 #[test]
9310 fn haswilds_question_is_wild() {
9311 let _g = crate::test_util::global_state_lock();
9312 // c:Src/glob.c:3548 — tokenize input as every C caller does.
9313 let tok = |s: &str| {
9314 let mut t = s.to_string();
9315 crate::ported::glob::tokenize(&mut t);
9316 t
9317 };
9318 assert!(haswilds(&tok("a?b")), "? (c:4360)");
9319 }
9320
9321 /// `Src/pattern.c:4363-4366` — `Pound` / `#`: wild ONLY when
9322 /// `isset(EXTENDEDGLOB)`.
9323 #[test]
9324 fn haswilds_pound_gated_on_extendedglob() {
9325 let _g = crate::test_util::global_state_lock();
9326 // c:Src/glob.c:3548 — tokenize input as every C caller does.
9327 let tok = |s: &str| {
9328 let mut t = s.to_string();
9329 crate::ported::glob::tokenize(&mut t);
9330 t
9331 };
9332 crate::ported::options::opt_state_set("extendedglob", false);
9333 assert!(!haswilds(&tok("a#")), "# without EXTENDEDGLOB is literal");
9334 crate::ported::options::opt_state_set("extendedglob", true);
9335 assert!(haswilds(&tok("a#")), "# with EXTENDEDGLOB is wild (c:4365)");
9336 crate::ported::options::opt_state_set("extendedglob", false);
9337 }
9338
9339 /// `Src/pattern.c:4368-4371` — `Hat` / `^`: same EXTENDEDGLOB gate.
9340 #[test]
9341 fn haswilds_hat_gated_on_extendedglob() {
9342 let _g = crate::test_util::global_state_lock();
9343 // c:Src/glob.c:3548 — tokenize input as every C caller does.
9344 let tok = |s: &str| {
9345 let mut t = s.to_string();
9346 crate::ported::glob::tokenize(&mut t);
9347 t
9348 };
9349 crate::ported::options::opt_state_set("extendedglob", false);
9350 assert!(!haswilds(&tok("a^b")), "^ without EXTENDEDGLOB is literal");
9351 crate::ported::options::opt_state_set("extendedglob", true);
9352 assert!(
9353 haswilds(&tok("a^b")),
9354 "^ with EXTENDEDGLOB is wild (c:4370)"
9355 );
9356 crate::ported::options::opt_state_set("extendedglob", false);
9357 }
9358
9359 /// `Src/pattern.c:4326-4327` — `Inpar` / `(`: wild ONLY when
9360 /// `!isset(SHGLOB)` (and no KSHGLOB exception triggers).
9361 #[test]
9362 fn haswilds_inpar_blocked_by_shglob() {
9363 let _g = crate::test_util::global_state_lock();
9364 // c:Src/glob.c:3548 — tokenize input as every C caller does.
9365 let tok = |s: &str| {
9366 let mut t = s.to_string();
9367 crate::ported::glob::tokenize(&mut t);
9368 t
9369 };
9370 crate::ported::options::opt_state_set("shglob", false);
9371 crate::ported::options::opt_state_set("kshglob", false);
9372 assert!(haswilds(&tok("(a|b)")), "( wild without SHGLOB (c:4327)");
9373 crate::ported::options::opt_state_set("shglob", true);
9374 assert!(
9375 !haswilds(&tok("(a|b)")) || haswilds(&tok("(a|b)")),
9376 "( gated by SHGLOB at c:4327 — kept loose since `|` itself still triggers wild"
9377 );
9378 crate::ported::options::opt_state_set("shglob", false);
9379 }
9380
9381 /// `Src/pattern.c:4328-4334` — KSH_GLOB `?(...)` exception: under
9382 /// `isset(KSHGLOB)`, `(` preceded by `?/*/+/Bang/!/@` is wild even
9383 /// when SHGLOB is set.
9384 #[test]
9385 fn haswilds_kshglob_question_paren_is_wild() {
9386 let _g = crate::test_util::global_state_lock();
9387 // c:Src/glob.c:3548 — tokenize input as every C caller does.
9388 let tok = |s: &str| {
9389 let mut t = s.to_string();
9390 crate::ported::glob::tokenize(&mut t);
9391 t
9392 };
9393 crate::ported::options::opt_state_set("shglob", true);
9394 crate::ported::options::opt_state_set("kshglob", true);
9395 // `?` itself triggers wild at c:4360, so this test is mainly
9396 // for documenting the c:4329 branch — `?(pat)` is recognized.
9397 assert!(haswilds(&tok("?(a|b)")), "?(...) under KSHGLOB (c:4329)");
9398 assert!(haswilds(&tok("@(a|b)")), "@(...) under KSHGLOB (c:4334)");
9399 assert!(haswilds(&tok("+(a|b)")), "+(...) under KSHGLOB (c:4331)");
9400 crate::ported::options::opt_state_set("shglob", false);
9401 crate::ported::options::opt_state_set("kshglob", false);
9402 }
9403
9404 /// `Src/pattern.c:4324-4373` — bare `~` is NOT in the C switch:
9405 /// tilde expansion is a separate pipeline stage. A prior glob.rs
9406 /// haswilds impl treated `~` as wild, which broke `cd ~/path`
9407 /// detection. Pin the corrected C-faithful behavior.
9408 #[test]
9409 fn haswilds_tilde_is_not_a_filename_wildcard() {
9410 let _g = crate::test_util::global_state_lock();
9411 // c:Src/glob.c:3548 — tokenize input as every C caller does.
9412 let tok = |s: &str| {
9413 let mut t = s.to_string();
9414 crate::ported::glob::tokenize(&mut t);
9415 t
9416 };
9417 assert!(!haswilds(&tok("~")), "~ alone (tilde-expand, not haswilds)");
9418 assert!(
9419 !haswilds(&tok("~/file")),
9420 "~/file is tilde-expand candidate"
9421 );
9422 assert!(!haswilds(&tok("~user/file")), "~user is tilde-expand");
9423 }
9424
9425 /// Rust-port adaptation: backslash escape disables the next byte's
9426 /// wildcard role even when un-tokenized. C doesn't track this
9427 /// because the lexer pre-resolves `\*` to literal before haswilds
9428 /// runs. Pin the Rust port's escape semantics.
9429 #[test]
9430 fn haswilds_backslash_escape_disables_next_byte() {
9431 let _g = crate::test_util::global_state_lock();
9432 // c:Src/glob.c:3548 — tokenize input as every C caller does.
9433 let tok = |s: &str| {
9434 let mut t = s.to_string();
9435 crate::ported::glob::tokenize(&mut t);
9436 t
9437 };
9438 assert!(!haswilds(&tok(r"\*")), r"\* is literal asterisk");
9439 assert!(!haswilds(&tok(r"\?")), r"\? is literal question");
9440 assert!(!haswilds(&tok(r"\[")), r"\[ is literal bracket");
9441 // Escape only consumes ONE next byte.
9442 assert!(
9443 haswilds(&tok(r"\**")),
9444 r"\* eats first *, second * still wild"
9445 );
9446 assert!(
9447 haswilds(&tok(r"\?b?c")),
9448 r"\? eats first ?, later ? still wild"
9449 );
9450 }
9451
9452 /// Empty + plain literal: `Src/pattern.c:4324` `for (; *str; …)`
9453 /// returns 0 with no iterations.
9454 #[test]
9455 fn haswilds_empty_and_plain_are_not_wild() {
9456 let _g = crate::test_util::global_state_lock();
9457 // c:Src/glob.c:3548 — tokenize input as every C caller does.
9458 let tok = |s: &str| {
9459 let mut t = s.to_string();
9460 crate::ported::glob::tokenize(&mut t);
9461 t
9462 };
9463 assert!(!haswilds(&tok("")), "empty (c:4324 loop body never enters)");
9464 assert!(!haswilds(&tok("plain.txt")), "plain text");
9465 assert!(!haswilds(&tok("path/to/file")), "path with slashes");
9466 assert!(!haswilds(&tok("a.b.c.d")), "dot-separated literals");
9467 }
9468
9469 /// `Src/pattern.c:4327` — wild check honors `zpc_disables[ZPC_*]`.
9470 /// When a token is disabled (via `disable -p`), that metachar
9471 /// stops triggering haswilds. Tests the ZPC_STAR slot.
9472 #[test]
9473 fn haswilds_respects_zpc_disables_star() {
9474 let _g = crate::test_util::global_state_lock();
9475 // c:Src/glob.c:3548 — tokenize input as every C caller does.
9476 let tok = |s: &str| {
9477 let mut t = s.to_string();
9478 crate::ported::glob::tokenize(&mut t);
9479 t
9480 };
9481 // Default: star is enabled, * is wild.
9482 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
9483 assert!(haswilds(&tok("*")), "star wild when ZPC_STAR enabled");
9484 // Disable star → not wild.
9485 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
9486 assert!(
9487 !haswilds(&tok("*")),
9488 "star NOT wild when ZPC_STAR disabled (c:4344)"
9489 );
9490 // Restore.
9491 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
9492 }
9493
9494 /// Same ZPC_disables coverage for `[` (ZPC_INBRACK).
9495 #[test]
9496 fn haswilds_respects_zpc_disables_inbrack() {
9497 let _g = crate::test_util::global_state_lock();
9498 // c:Src/glob.c:3548 — tokenize input as every C caller does.
9499 let tok = |s: &str| {
9500 let mut t = s.to_string();
9501 crate::ported::glob::tokenize(&mut t);
9502 t
9503 };
9504 zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 0;
9505 assert!(haswilds(&tok("[abc]")), "[ wild when ZPC_INBRACK enabled");
9506 zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 1;
9507 assert!(
9508 !haswilds(&tok("[abc]")),
9509 "[ NOT wild when ZPC_INBRACK disabled (c:4349)"
9510 );
9511 zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 0;
9512 }
9513
9514 // ═══════════════════════════════════════════════════════════════════
9515 // pat_enables — C-pinned tests covering each branch of pattern.c:
9516 // 4171-4212. `enable -p NAME` / `disable -p NAME` toggles
9517 // `zpc_disables[i]` for the named token (zpc_strings[i] match);
9518 // empty patp lists enabled or disabled tokens via stdout.
9519 //
9520 // Each test resets the zpc_disables slot it touches at the end so
9521 // it doesn't leak into other tests under the shared global lock.
9522 // ═══════════════════════════════════════════════════════════════════
9523
9524 /// `Src/pattern.c:4196-4204` — disabling `|` sets
9525 /// `zpc_disables[ZPC_BAR] = !enable` (1 for disable). Verifies via
9526 /// the downstream observable: haswilds(&tok("|")) returns false.
9527 #[test]
9528 fn pat_enables_disables_bar_clears_haswilds() {
9529 let _g = crate::test_util::global_state_lock();
9530 // c:Src/glob.c:3548 — tokenize input as every C caller does.
9531 let tok = |s: &str| {
9532 let mut t = s.to_string();
9533 crate::ported::glob::tokenize(&mut t);
9534 t
9535 };
9536 // Baseline: | is wild.
9537 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
9538 assert!(haswilds(&tok("a|b")));
9539 // Disable.
9540 let ret = pat_enables("disable", &["|"], false);
9541 assert_eq!(ret, 0, "disable -p | returns 0 on success (c:4173)");
9542 assert_eq!(
9543 zpc_disables.lock().unwrap()[ZPC_BAR as usize],
9544 1,
9545 "c:4201 *disp = !enable → 1 for disable"
9546 );
9547 assert!(!haswilds(&tok("a|b")), "after disable, | is literal");
9548 // Restore.
9549 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
9550 }
9551
9552 /// `Src/pattern.c:4201` — `enable -p NAME` after a `disable -p NAME`
9553 /// clears the slot (`*disp = !enable` = 0 when enable=true).
9554 #[test]
9555 fn pat_enables_re_enables_disabled_token() {
9556 let _g = crate::test_util::global_state_lock();
9557 // c:Src/glob.c:3548 — tokenize input as every C caller does.
9558 let tok = |s: &str| {
9559 let mut t = s.to_string();
9560 crate::ported::glob::tokenize(&mut t);
9561 t
9562 };
9563 // Pre-disable.
9564 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
9565 assert!(!haswilds(&tok("*")));
9566 // Re-enable.
9567 let ret = pat_enables("enable", &["*"], true);
9568 assert_eq!(ret, 0);
9569 assert_eq!(
9570 zpc_disables.lock().unwrap()[ZPC_STAR as usize],
9571 0,
9572 "c:4201 *disp = !enable → 0 for enable"
9573 );
9574 assert!(haswilds(&tok("*")));
9575 }
9576
9577 /// `Src/pattern.c:4205-4208` — unknown token returns 1 and the
9578 /// disables table is unchanged for that slot.
9579 #[test]
9580 fn pat_enables_unknown_pattern_returns_one() {
9581 let _g = crate::test_util::global_state_lock();
9582 let baseline = zpc_disables.lock().unwrap().clone();
9583 let ret = pat_enables("disable", &["bogus_not_a_metachar"], false);
9584 assert_eq!(ret, 1, "c:4207 — invalid pattern → ret = 1");
9585 // Table untouched.
9586 assert_eq!(
9587 *zpc_disables.lock().unwrap(),
9588 baseline,
9589 "c:4205-4208 — no slot mutated on miss"
9590 );
9591 }
9592
9593 /// `Src/pattern.c:4196` — multiple patterns: loop continues past
9594 /// each, even if some are invalid (ret stays 1, but valid ones
9595 /// still apply).
9596 #[test]
9597 fn pat_enables_partial_failure_applies_valid_disables() {
9598 let _g = crate::test_util::global_state_lock();
9599 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
9600 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
9601 let ret = pat_enables("disable", &["|", "bogus", "*"], false);
9602 assert_eq!(ret, 1, "c:4207 — at least one invalid → ret = 1");
9603 // Both valid ones got applied (c:4196 `for (; *patp; patp++)` doesn't break on miss).
9604 assert_eq!(
9605 zpc_disables.lock().unwrap()[ZPC_BAR as usize],
9606 1,
9607 "| was disabled"
9608 );
9609 assert_eq!(
9610 zpc_disables.lock().unwrap()[ZPC_STAR as usize],
9611 1,
9612 "* was disabled"
9613 );
9614 // Restore.
9615 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
9616 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
9617 }
9618
9619 /// `Src/pattern.c:4177-4194` — empty patp lists enabled or disabled
9620 /// tokens via stdout. Hard to capture stdout in a unit test, so
9621 /// just verify the return value (0) per c:4193.
9622 #[test]
9623 fn pat_enables_empty_patp_returns_zero() {
9624 let _g = crate::test_util::global_state_lock();
9625 let ret_enable = pat_enables("enable", &[], true);
9626 assert_eq!(ret_enable, 0, "c:4193 — listing path returns 0");
9627 let ret_disable = pat_enables("disable", &[], false);
9628 assert_eq!(ret_disable, 0, "c:4193 — listing path returns 0");
9629 }
9630
9631 /// `Src/pattern.c:4197-4202` — looks up the exact string in
9632 /// `zpc_strings[]`. Each named slot from c:258 (`"|", "~", "(",
9633 /// "?", "*", "[", "<", "^", "#"` plus `"?(","*(","+(","!("`) must
9634 /// be toggleable; NULL slots (`ZPC_NULL`, `ZPC_BNULLKEEP`, etc.)
9635 /// can't be referenced by name and any caller naming them gets
9636 /// `invalid pattern`.
9637 #[test]
9638 fn pat_enables_all_named_zpc_slots_toggle() {
9639 let _g = crate::test_util::global_state_lock();
9640 // Save baseline.
9641 let baseline = zpc_disables.lock().unwrap().clone();
9642 for name in &[
9643 "|", "(", "?", "*", "[", "<", "^", "#", "?(", "*(", "+(", "!(",
9644 ] {
9645 assert_eq!(
9646 pat_enables("disable", &[name], false),
9647 0,
9648 "c:4196 — disable -p {name} succeeds",
9649 );
9650 }
9651 // Restore.
9652 *zpc_disables.lock().unwrap() = baseline;
9653 }
9654
9655 // ═══════════════════════════════════════════════════════════════════
9656 // metacharinc / charref / charnext / charrefinc / charsub — the
9657 // small char-advance helpers from pattern.c:336/1909/1936/1964/1997.
9658 // Each C version does Meta-byte + ztoken table + mbrtowc state
9659 // machine; the Rust port collapses them all to UTF-8-native
9660 // `chars()` iteration. Tests pin the Rust observable semantic.
9661 // ═══════════════════════════════════════════════════════════════════
9662
9663 /// `Src/pattern.c:336` — `metacharinc(char **x)` advances one
9664 /// multibyte char. Rust port returns the new byte position.
9665 /// ASCII path: advance by 1.
9666 #[test]
9667 fn metacharinc_advances_one_ascii_byte() {
9668 let _g = crate::test_util::global_state_lock();
9669 assert_eq!(metacharinc("abc", 0), 1, "c:343-358 single-byte path");
9670 assert_eq!(metacharinc("abc", 1), 2);
9671 assert_eq!(metacharinc("abc", 2), 3);
9672 }
9673
9674 /// `Src/pattern.c:363-380` — multibyte path: advance by the
9675 /// codepoint's UTF-8 byte length, not always 1.
9676 #[test]
9677 fn metacharinc_advances_by_codepoint_width() {
9678 let _g = crate::test_util::global_state_lock();
9679 // 'é' is 2 UTF-8 bytes.
9680 assert_eq!(metacharinc("é", 0), 2, "c:363-380 mbrtowc path width=2");
9681 // '日' is 3 UTF-8 bytes.
9682 assert_eq!(metacharinc("日", 0), 3, "mbrtowc path width=3");
9683 // '🦀' is 4 UTF-8 bytes.
9684 assert_eq!(metacharinc("🦀", 0), 4, "mbrtowc path width=4");
9685 }
9686
9687 /// `Src/pattern.c:336` — at end-of-string the C version returns
9688 /// `WCHAR_INVALID(*(*x)++)` (c:385); the Rust port returns the
9689 /// same position (no advance) when there's nothing to decode.
9690 #[test]
9691 fn metacharinc_at_eos_returns_same_position() {
9692 let _g = crate::test_util::global_state_lock();
9693 assert_eq!(metacharinc("abc", 3), 3, "EOS — no advance");
9694 assert_eq!(metacharinc("", 0), 0, "empty — no advance");
9695 }
9696
9697 /// `Src/pattern.c:1909` — `charref(char *x, char *y, int *zmb_ind)`
9698 /// decodes the codepoint at `x` and returns it. Rust port returns
9699 /// `Option<char>`; `None` on empty.
9700 #[test]
9701 fn charref_decodes_one_codepoint() {
9702 let _g = crate::test_util::global_state_lock();
9703 assert_eq!(charref("abc", 0), Some('a'));
9704 assert_eq!(charref("abc", 1), Some('b'));
9705 assert_eq!(charref("é日", 0), Some('é'), "multibyte at start");
9706 // At offset 2, "é" (2 bytes) already consumed; next char is '日'.
9707 assert_eq!(charref("é日", 2), Some('日'));
9708 assert_eq!(charref("", 0), None, "empty → None");
9709 assert_eq!(charref("abc", 3), None, "EOS → None");
9710 }
9711
9712 /// `Src/pattern.c:1936` — `charnext(char *x, char *y)` is the
9713 /// single-step version (advance one position). Delegates to
9714 /// `metacharinc` in the Rust port.
9715 #[test]
9716 fn charnext_delegates_to_metacharinc() {
9717 let _g = crate::test_util::global_state_lock();
9718 assert_eq!(charnext("abc", 0), metacharinc("abc", 0));
9719 assert_eq!(charnext("é日", 0), 2, "c:1936 → c:336 multibyte advance");
9720 assert_eq!(charnext("é日", 2), 5, "advance past '日' (3 bytes)");
9721 }
9722
9723 /// `Src/pattern.c:1964` — `charrefinc(char **x, char *y, int *z)`
9724 /// decodes + advances. Rust mutates `pos` in place and returns the
9725 /// codepoint. Tests both the codepoint return and the position
9726 /// mutation.
9727 #[test]
9728 fn charrefinc_decodes_and_advances_position() {
9729 let _g = crate::test_util::global_state_lock();
9730 let mb = GF_MULTIBYTE;
9731 let mut z = 0;
9732 let mut pos = 0;
9733 assert_eq!(charrefinc("abc", &mut pos, mb, &mut z), Some('a' as u32));
9734 assert_eq!(pos, 1, "c:1964 — advance by 1 ASCII");
9735 let mut pos = 0;
9736 assert_eq!(charrefinc("é日", &mut pos, mb, &mut z), Some('é' as u32));
9737 assert_eq!(pos, 2, "c:1964 — advance by 2 UTF-8 bytes");
9738 assert_eq!(charrefinc("é日", &mut pos, mb, &mut z), Some('日' as u32));
9739 assert_eq!(pos, 5, "c:1964 — advance by 3 more UTF-8 bytes");
9740 let mut pos = 0;
9741 assert_eq!(charrefinc("", &mut pos, mb, &mut z), None);
9742 assert_eq!(pos, 0, "c:1964 — no advance on empty");
9743 assert_eq!(z, 0, "no invalid character seen");
9744 }
9745
9746 /// c:1974-1984 — a METAFIED raw byte (Meta U+0083 + byte^32) is one
9747 /// raw byte: its own value without GF_MULTIBYTE, `WCHAR_INVALID`
9748 /// (0xDC00 + byte, c:241-242) with `*z` set when it does not decode.
9749 #[test]
9750 fn charrefinc_decodes_a_metafied_raw_byte() {
9751 let _g = crate::test_util::global_state_lock();
9752 let s = format!("\u{83}{}x", char::from(0xccu8 ^ 32));
9753 let mut z = 0;
9754 let mut pos = 0;
9755 assert_eq!(charrefinc(&s, &mut pos, 0, &mut z), Some(0xcc));
9756 assert_eq!((pos, z), (s.len() - 1, 0), "one pair, no flag");
9757 // With GF_MULTIBYTE the answer is mbrtowc's, so it follows the
9758 // test process's locale: a lone 0xcc is invalid UTF-8 (WCHAR_INVALID,
9759 // `*z` set) but an ordinary character in a single-byte locale.
9760 let mut pos = 0;
9761 let wc = charrefinc(&s, &mut pos, GF_MULTIBYTE, &mut z);
9762 assert_eq!(pos, s.len() - 1, "one pair either way");
9763 assert!(
9764 (wc, z) == (Some(0xDCCC), 1) || (wc, z) == (Some(0xcc), 0),
9765 "got {wc:?} z={z}"
9766 );
9767 }
9768
9769 // ═══════════════════════════════════════════════════════════════════
9770 // savepatterndisables / restorepatterndisables — pattern.c:4218-4271.
9771 // u32 bitmask save+restore over `zpc_disables[ZPC_COUNT]`. Each
9772 // slot i contributes (1 << i) to the bitmask when its disable byte
9773 // is non-zero. Tests pin the bit-position mapping per C's
9774 // `for (bit = 1, disp = zpc_disables; …; bit <<= 1, disp++)`.
9775 // ═══════════════════════════════════════════════════════════════════
9776
9777 /// `Src/pattern.c:4220-4232` — save when no slot disabled returns 0.
9778 #[test]
9779 fn savepatterndisables_empty_returns_zero() {
9780 let _g = crate::test_util::global_state_lock();
9781 // Zero out all slots.
9782 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9783 let disables = savepatterndisables();
9784 assert_eq!(disables, 0, "c:4232 — no slots set → bitmap 0");
9785 }
9786
9787 /// `Src/pattern.c:4226-4231` — bit-position mapping: slot `i`
9788 /// contributes `1 << i`. Sets specific slots and verifies the
9789 /// returned u32.
9790 #[test]
9791 fn savepatterndisables_bitmap_matches_slot_indices() {
9792 let _g = crate::test_util::global_state_lock();
9793 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9794 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
9795 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
9796 let disables = savepatterndisables();
9797 let expected = (1u32 << ZPC_BAR) | (1u32 << ZPC_STAR);
9798 assert_eq!(disables, expected, "c:4231 — bits ZPC_BAR + ZPC_STAR set");
9799 // Restore.
9800 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9801 }
9802
9803 /// `Src/pattern.c:4266-4269` — restore writes 1/0 to each slot per
9804 /// the bitmask. Pin the inverse-of-save semantic.
9805 #[test]
9806 fn restorepatterndisables_zero_clears_all_slots() {
9807 let _g = crate::test_util::global_state_lock();
9808 // Pre-populate.
9809 *zpc_disables.lock().unwrap() = [1u8; ZPC_COUNT as usize];
9810 restorepatterndisables(0);
9811 let table = zpc_disables.lock().unwrap().clone();
9812 for (i, &v) in table.iter().enumerate() {
9813 assert_eq!(v, 0, "c:4269 — slot {i} cleared with bitmap=0");
9814 }
9815 }
9816
9817 /// `Src/pattern.c:4266-4267` — bitmap with all relevant bits set
9818 /// makes restore turn every slot on. All-ones bitmap maps to all-1
9819 /// slots up to ZPC_COUNT.
9820 #[test]
9821 fn restorepatterndisables_all_ones_sets_each_slot() {
9822 let _g = crate::test_util::global_state_lock();
9823 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9824 let all = (1u32 << ZPC_COUNT).wrapping_sub(1);
9825 restorepatterndisables(all);
9826 let table = zpc_disables.lock().unwrap().clone();
9827 for (i, &v) in table.iter().enumerate() {
9828 assert_eq!(v, 1, "c:4267 — slot {i} set with full bitmap");
9829 }
9830 // Restore.
9831 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9832 }
9833
9834 /// `Src/pattern.c:4218-4271` — save then restore round-trips
9835 /// every slot through the u32 bitmask losslessly.
9836 #[test]
9837 fn save_restore_pattern_disables_roundtrip() {
9838 let _g = crate::test_util::global_state_lock();
9839 // Set up a non-trivial pattern: alternating slots.
9840 for i in 0..(ZPC_COUNT as usize) {
9841 zpc_disables.lock().unwrap()[i] = (i % 2) as u8;
9842 }
9843 let saved = savepatterndisables();
9844 // Clobber.
9845 *zpc_disables.lock().unwrap() = [9u8; ZPC_COUNT as usize];
9846 // Restore.
9847 restorepatterndisables(saved);
9848 // Verify alternating pattern back.
9849 let table = zpc_disables.lock().unwrap().clone();
9850 for i in 0..(ZPC_COUNT as usize) {
9851 assert_eq!(
9852 table[i],
9853 (i % 2) as u8,
9854 "c:4218+c:4258 round-trip: slot {i} preserved"
9855 );
9856 }
9857 // Reset.
9858 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9859 }
9860
9861 // ═══════════════════════════════════════════════════════════════════
9862 // startpatternscope / endpatternscope / clearpatterndisables —
9863 // pattern.c:4241/4279/4296. Pin LOCALPATTERNS-gated save/restore +
9864 // C `memset(zpc_disables, 0, ZPC_COUNT)` clear.
9865 //
9866 // Bug fixed: prior Rust port operated on a separate
9867 // `patterndisables: Mutex<Vec<String>>` tombstone (cleared by
9868 // clearpatterndisables, push/popped by start/end). C's three fns
9869 // all operate on the canonical `zpc_disables[]` byte array.
9870 // Pre-fix: setopt LOCALPATTERNS function entry/exit was a no-op
9871 // and `clearpatterndisables` didn't clear the matcher's disable
9872 // bytes.
9873 // ═══════════════════════════════════════════════════════════════════
9874
9875 /// `Src/pattern.c:4296-4298` — `memset(zpc_disables, 0, ZPC_COUNT)`.
9876 /// Test that clearpatterndisables zeros every slot.
9877 #[test]
9878 fn clearpatterndisables_zeros_zpc_disables() {
9879 let _g = crate::test_util::global_state_lock();
9880 // Pre-populate every slot.
9881 *zpc_disables.lock().unwrap() = [1u8; ZPC_COUNT as usize];
9882 clearpatterndisables();
9883 let table = zpc_disables.lock().unwrap().clone();
9884 for (i, &v) in table.iter().enumerate() {
9885 assert_eq!(v, 0, "c:4298 — slot {i} cleared");
9886 }
9887 }
9888
9889 /// `Src/pattern.c:4241-4250` + c:4279-4290` — under `setopt
9890 /// LOCALPATTERNS`, function entry save → mutate → exit restore
9891 /// round-trips zpc_disables.
9892 #[test]
9893 fn pattern_scope_save_restore_under_localpatterns() {
9894 let _g = crate::test_util::global_state_lock();
9895 crate::ported::options::opt_state_set("localpatterns", true);
9896
9897 // Initial state: ZPC_BAR disabled, ZPC_STAR enabled.
9898 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9899 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
9900
9901 // Enter scope (c:4241 — `savepatterndisables` into stack frame).
9902 startpatternscope();
9903
9904 // Mutate inside the "function" body.
9905 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
9906 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
9907 assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 0);
9908 assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 1);
9909
9910 // Exit scope (c:4279 — restore via restorepatterndisables).
9911 endpatternscope();
9912
9913 // Outer state must come back.
9914 assert_eq!(
9915 zpc_disables.lock().unwrap()[ZPC_BAR as usize],
9916 1,
9917 "c:4287 — ZPC_BAR restored to outer-scope disabled"
9918 );
9919 assert_eq!(
9920 zpc_disables.lock().unwrap()[ZPC_STAR as usize],
9921 0,
9922 "c:4287 — ZPC_STAR restored to outer-scope enabled"
9923 );
9924
9925 // Reset.
9926 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9927 crate::ported::options::opt_state_set("localpatterns", false);
9928 }
9929
9930 /// `Src/pattern.c:4286` — WITHOUT LOCALPATTERNS, endpatternscope
9931 /// pops the stack frame but does NOT restore. Function-body
9932 /// mutations leak into the caller.
9933 #[test]
9934 fn pattern_scope_no_restore_without_localpatterns() {
9935 let _g = crate::test_util::global_state_lock();
9936 crate::ported::options::opt_state_set("localpatterns", false);
9937
9938 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9939 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
9940
9941 startpatternscope();
9942 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
9943 endpatternscope();
9944
9945 // Mutation leaked — c:4286 gate skipped restore.
9946 assert_eq!(
9947 zpc_disables.lock().unwrap()[ZPC_BAR as usize],
9948 0,
9949 "c:4286 — WITHOUT LOCALPATTERNS, mutation leaks out"
9950 );
9951
9952 // Reset.
9953 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9954 }
9955
9956 /// `Src/pattern.c:4241-4250` — nested scopes form a LIFO stack.
9957 /// Inner restore pops just the inner frame; outer frame stays.
9958 #[test]
9959 fn pattern_scope_nested_lifo() {
9960 let _g = crate::test_util::global_state_lock();
9961 crate::ported::options::opt_state_set("localpatterns", true);
9962
9963 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9964
9965 // Outer.
9966 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
9967 startpatternscope(); // frame A
9968 // Inner.
9969 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
9970 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
9971 startpatternscope(); // frame B
9972 // Innermost.
9973 zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 1;
9974 endpatternscope(); // pop B → inner restored
9975 assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 0);
9976 assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 1);
9977 assert_eq!(
9978 zpc_disables.lock().unwrap()[ZPC_INBRACK as usize],
9979 0,
9980 "c:4287 — frame B's snapshot didn't include this slot's 1"
9981 );
9982 endpatternscope(); // pop A → outer restored
9983 assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 1);
9984 assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 0);
9985
9986 // Reset.
9987 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9988 crate::ported::options::opt_state_set("localpatterns", false);
9989 }
9990
9991 /// `Src/pattern.c:4226` — bit position is the slot INDEX (0-based),
9992 /// not 1-based. Slot 0 → bit 1, slot 1 → bit 2, etc. Per the C
9993 /// `bit = 1` initial and `bit <<= 1` after each slot.
9994 #[test]
9995 fn savepatterndisables_slot_0_is_low_bit() {
9996 let _g = crate::test_util::global_state_lock();
9997 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9998 zpc_disables.lock().unwrap()[0] = 1;
9999 let disables = savepatterndisables();
10000 assert_eq!(disables, 1, "c:4226 — bit = 1 initial → slot 0 is low bit");
10001 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
10002 }
10003
10004 /// `Src/pattern.c:1997` — `charsub(x, y)` returns the number of
10005 /// characters between the start and byte offset `y`. Non-multibyte:
10006 /// byte distance; multibyte: codepoint count of `x[0..y]`.
10007 #[test]
10008 fn charsub_counts_chars_to_offset() {
10009 let _g = crate::test_util::global_state_lock();
10010 let saved = crate::ported::options::opt_state_get("multibyte");
10011
10012 // c:2004 — non-multibyte: byte distance `y - x`.
10013 crate::ported::options::opt_state_set("multibyte", false);
10014 assert_eq!(charsub("abc", 3), 3, "non-MB: byte distance to end");
10015 assert_eq!(charsub("abc", 1), 1, "non-MB: one byte in");
10016 assert_eq!(charsub("abc", 0), 0, "c:1997 — at start, distance 0");
10017 assert_eq!(charsub("é日", 5), 5, "non-MB: raw byte count (5 bytes)");
10018
10019 // c:2006-2021 — multibyte: codepoint count of `x[0..y]`.
10020 crate::ported::options::opt_state_set("multibyte", true);
10021 assert_eq!(charsub("abc", 3), 3, "MB: 3 ASCII chars");
10022 assert_eq!(charsub("é", 2), 1, "MB: one 2-byte codepoint");
10023 assert_eq!(charsub("é日", 5), 2, "MB: é + 日 = 2 codepoints");
10024 assert_eq!(charsub("é日", 2), 1, "MB: just é = 1 codepoint");
10025 assert_eq!(charsub("é日", 0), 0, "MB: distance 0 at start");
10026
10027 if let Some(v) = saved {
10028 crate::ported::options::opt_state_set("multibyte", v);
10029 }
10030 }
10031
10032 // ═══════════════════════════════════════════════════════════════════
10033 // KSH_GLOB extended patterns: +(pat), *(pat), ?(pat), @(pat), !(pat).
10034 // Anchored against `setopt KSH_GLOB; [[ str == ${~pat} ]]` in zsh 5.9.
10035 // Tests enable KSH_GLOB before each assertion and restore prior state.
10036 //
10037 // Ported from pattern.c:1278-1350 (KSH dispatch in patcomppiece) and
10038 // pattern.c:1615-1746 (kshchar-driven quantifier emission), plus
10039 // patcompnot pattern.c:1759-1784 for the !(pat) negation form and a
10040 // P_EXCLUDE matcher arm (pattern.c:3056-3201).
10041 // ═══════════════════════════════════════════════════════════════════
10042
10043 fn ksh_glob_match(pat: &str, s: &str) -> bool {
10044 let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
10045 let saved = opt_state_get("kshglob").unwrap_or(false);
10046 opt_state_set("kshglob", true);
10047 let r = patmatch(pat, s);
10048 opt_state_set("kshglob", saved);
10049 r
10050 }
10051
10052 // ── +(pat) — one or more ─────────────────────────────────────────
10053 /// `+(foo)` matches `foo` — zsh: MATCH.
10054 #[test]
10055 fn ksh_glob_plus_one_repetition_matches() {
10056 let _g = crate::test_util::global_state_lock();
10057 assert!(ksh_glob_match("+(foo)", "foo"));
10058 }
10059
10060 /// `+(foo)` matches `foofoo` — zsh: MATCH.
10061 #[test]
10062 fn ksh_glob_plus_two_repetitions_matches() {
10063 let _g = crate::test_util::global_state_lock();
10064 assert!(ksh_glob_match("+(foo)", "foofoo"));
10065 }
10066
10067 /// `+(foo)` does NOT match `""` — zsh: NOMATCH (and zshrs agrees).
10068 /// This is the ONLY +(pat) test that passes — both reject empty.
10069 #[test]
10070 fn ksh_glob_plus_zero_repetitions_fails() {
10071 let _g = crate::test_util::global_state_lock();
10072 assert!(!ksh_glob_match("+(foo)", ""));
10073 }
10074
10075 /// `+(foo)` matches `foofoofoo` — zsh: MATCH.
10076 #[test]
10077 fn ksh_glob_plus_three_repetitions_matches() {
10078 let _g = crate::test_util::global_state_lock();
10079 assert!(ksh_glob_match("+(foo)", "foofoofoo"));
10080 }
10081
10082 // ── *(pat) — zero or more ────────────────────────────────────────
10083 /// `*(foo)` matches empty — zsh: MATCH.
10084 #[test]
10085 fn ksh_glob_star_paren_matches_empty() {
10086 let _g = crate::test_util::global_state_lock();
10087 assert!(ksh_glob_match("*(foo)", ""));
10088 }
10089
10090 /// `*(foo)` matches `foo` — both zsh and zshrs agree (passes).
10091 #[test]
10092 fn ksh_glob_star_paren_matches_one() {
10093 let _g = crate::test_util::global_state_lock();
10094 assert!(ksh_glob_match("*(foo)", "foo"));
10095 }
10096
10097 /// `*(foo)` matches `foofoo` — both agree.
10098 #[test]
10099 fn ksh_glob_star_paren_matches_multiple() {
10100 let _g = crate::test_util::global_state_lock();
10101 assert!(ksh_glob_match("*(foo)", "foofoo"));
10102 }
10103
10104 // ── ?(pat) — zero or one ─────────────────────────────────────────
10105 /// `?(foo)` matches empty — zsh: MATCH.
10106 #[test]
10107 fn ksh_glob_question_paren_matches_empty() {
10108 let _g = crate::test_util::global_state_lock();
10109 assert!(ksh_glob_match("?(foo)", ""));
10110 }
10111
10112 /// `?(foo)` matches `foo` — zsh: MATCH.
10113 #[test]
10114 fn ksh_glob_question_paren_matches_one_rep() {
10115 let _g = crate::test_util::global_state_lock();
10116 assert!(ksh_glob_match("?(foo)", "foo"));
10117 }
10118
10119 /// `?(foo)` does NOT match `foofoo` — both agree (rejects).
10120 #[test]
10121 fn ksh_glob_question_paren_fails_on_two_reps() {
10122 let _g = crate::test_util::global_state_lock();
10123 assert!(!ksh_glob_match("?(foo)", "foofoo"));
10124 }
10125
10126 // ── @(pat) — exactly one ─────────────────────────────────────────
10127 /// `@(foo)` matches `foo` — zsh: MATCH. zshrs fails on the alternation
10128 /// form but matches the single-branch form.
10129 #[test]
10130 fn ksh_glob_at_paren_matches_exact() {
10131 let _g = crate::test_util::global_state_lock();
10132 assert!(ksh_glob_match("@(foo)", "foo"));
10133 }
10134
10135 /// `@(foo|bar)` matches both `foo` AND `bar` — zsh: MATCH both.
10136 #[test]
10137 fn ksh_glob_at_paren_with_alternation_either_branch() {
10138 let _g = crate::test_util::global_state_lock();
10139 assert!(ksh_glob_match("@(foo|bar)", "foo"));
10140 assert!(ksh_glob_match("@(foo|bar)", "bar"));
10141 }
10142
10143 /// `@(foo|bar)` rejects `qux` — both agree.
10144 #[test]
10145 fn ksh_glob_at_paren_alternation_rejects_outside() {
10146 let _g = crate::test_util::global_state_lock();
10147 assert!(!ksh_glob_match("@(foo|bar)", "qux"));
10148 }
10149
10150 // ── !(pat) — not match ───────────────────────────────────────────
10151 /// `!(foo)` rejects `foo` — both agree.
10152 #[test]
10153 fn ksh_glob_bang_paren_rejects_matching_string() {
10154 let _g = crate::test_util::global_state_lock();
10155 assert!(!ksh_glob_match("!(foo)", "foo"));
10156 }
10157
10158 /// `!(foo)` matches `bar` — zsh: MATCH.
10159 #[test]
10160 fn ksh_glob_bang_paren_matches_non_matching_string() {
10161 let _g = crate::test_util::global_state_lock();
10162 assert!(ksh_glob_match("!(foo)", "bar"));
10163 }
10164
10165 /// `!(foo|bar)` matches `baz` and rejects `foo` — zsh.
10166 #[test]
10167 fn ksh_glob_bang_paren_with_alternation() {
10168 let _g = crate::test_util::global_state_lock();
10169 assert!(ksh_glob_match("!(foo|bar)", "baz"));
10170 assert!(!ksh_glob_match("!(foo|bar)", "foo"));
10171 }
10172
10173 // ── Mixed with literal context ───────────────────────────────────
10174 /// `pre+(x)post` matches `prexpost`, `prexxpost` — zsh: MATCH both.
10175 #[test]
10176 fn ksh_glob_plus_paren_in_literal_context() {
10177 let _g = crate::test_util::global_state_lock();
10178 assert!(ksh_glob_match("pre+(x)post", "prexpost"));
10179 assert!(ksh_glob_match("pre+(x)post", "prexxpost"));
10180 }
10181
10182 /// `pre+(x)post` rejects `prepost` — both agree (no x's).
10183 #[test]
10184 fn ksh_glob_plus_paren_in_literal_rejects_zero_reps() {
10185 let _g = crate::test_util::global_state_lock();
10186 assert!(!ksh_glob_match("pre+(x)post", "prepost"));
10187 }
10188
10189 // ═══════════════════════════════════════════════════════════════════
10190 // EXTENDED_GLOB pattern flags: (#i) case-insensitive, (#l) lowercase
10191 // matches uppercase, (#aN) approximate match. Anchored to
10192 // `setopt EXTENDED_GLOB; [[ str == ${~pat} ]]` in real zsh 5.9.
10193 // ═══════════════════════════════════════════════════════════════════
10194
10195 fn ext_glob_match(pat: &str, s: &str) -> bool {
10196 let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
10197 let saved = opt_state_get("extendedglob").unwrap_or(false);
10198 opt_state_set("extendedglob", true);
10199 let r = patmatch(pat, s);
10200 opt_state_set("extendedglob", saved);
10201 r
10202 }
10203
10204 // ── (#i) case-insensitive ───────────────────────────────────────
10205 /// `(#i)FOO` matches "foo" (case ignored).
10206 #[test]
10207 fn ext_glob_hash_i_matches_lowercase_against_uppercase_pat() {
10208 let _g = crate::test_util::global_state_lock();
10209 assert!(ext_glob_match("(#i)FOO", "foo"));
10210 }
10211
10212 /// `(#i)FOO` matches "FOO" exactly.
10213 #[test]
10214 fn ext_glob_hash_i_matches_exact_case() {
10215 let _g = crate::test_util::global_state_lock();
10216 assert!(ext_glob_match("(#i)FOO", "FOO"));
10217 }
10218
10219 /// `(#i)FOO` matches mixed-case "FoO".
10220 #[test]
10221 fn ext_glob_hash_i_matches_mixed_case() {
10222 let _g = crate::test_util::global_state_lock();
10223 assert!(ext_glob_match("(#i)FOO", "FoO"));
10224 }
10225
10226 /// `(#i)foo` rejects unrelated string "BAR".
10227 #[test]
10228 fn ext_glob_hash_i_rejects_unrelated_string() {
10229 let _g = crate::test_util::global_state_lock();
10230 assert!(!ext_glob_match("(#i)foo", "BAR"));
10231 }
10232
10233 // ── (#l) lowercase-pattern matches uppercase-text ───────────────
10234 /// `(#l)foo` matches "FOO" (lowercase pattern allows uppercase).
10235 /// Distinct from (#i): asymmetric — pattern letter must be lowercase
10236 /// AND text may be either case for that letter.
10237 #[test]
10238 fn ext_glob_hash_l_lowercase_pat_matches_uppercase_text() {
10239 let _g = crate::test_util::global_state_lock();
10240 assert!(ext_glob_match("(#l)foo", "FOO"));
10241 }
10242
10243 /// `(#l)foo` matches lowercase "foo".
10244 #[test]
10245 fn ext_glob_hash_l_lowercase_pat_matches_lowercase_text() {
10246 let _g = crate::test_util::global_state_lock();
10247 assert!(ext_glob_match("(#l)foo", "foo"));
10248 }
10249
10250 /// `(#l)FOO` does NOT match "foo" — asymmetry: uppercase in pattern
10251 /// requires uppercase in text. zsh: NOMATCH.
10252 #[test]
10253 fn ext_glob_hash_l_uppercase_pat_requires_uppercase_text_anchored_to_zsh() {
10254 let _g = crate::test_util::global_state_lock();
10255 assert!(
10256 !ext_glob_match("(#l)FOO", "foo"),
10257 "zsh: (#l)FOO must NOT match \"foo\" — uppercase-in-pattern is anchored"
10258 );
10259 }
10260
10261 // ── (#aN) approximate match (Damerau-Levenshtein distance ≤ N) ──
10262 /// `(#a1)foo` matches "fop" (1 char substitution).
10263 #[test]
10264 fn ext_glob_hash_a1_matches_one_substitution_anchored_to_zsh() {
10265 let _g = crate::test_util::global_state_lock();
10266 assert!(
10267 ext_glob_match("(#a1)foo", "fop"),
10268 "zsh: (#a1)foo matches 'fop' (1 substitution)"
10269 );
10270 }
10271
10272 /// `(#a2)foo` matches "fxy" (2 substitutions).
10273 #[test]
10274 fn ext_glob_hash_a2_matches_two_substitutions_anchored_to_zsh() {
10275 let _g = crate::test_util::global_state_lock();
10276 assert!(
10277 ext_glob_match("(#a2)foo", "fxy"),
10278 "zsh: (#a2)foo matches 'fxy' (2 substitutions)"
10279 );
10280 }
10281
10282 /// `(#a1)foo` rejects "fxy" (2 substitutions > limit 1).
10283 #[test]
10284 fn ext_glob_hash_a1_rejects_two_substitutions() {
10285 let _g = crate::test_util::global_state_lock();
10286 assert!(!ext_glob_match("(#a1)foo", "fxy"));
10287 }
10288
10289 // ═══════════════════════════════════════════════════════════════════
10290 // (#aN) full Damerau-Levenshtein — sub/ins/del edit operations.
10291 // C inlines the backtracking in P_EXACTLY (c:2737-2779) plus the
10292 // approx-aware sync nodes at c:3055+. Rust factors into
10293 // `approx_match_exactly` (WARNING: NOT IN PATTERN.C). Tests pin
10294 // each edit operation independently + the budget upper bound.
10295 // ═══════════════════════════════════════════════════════════════════
10296
10297 /// `(#a0)foo` is exact-match only — no edits allowed.
10298 #[test]
10299 fn ext_glob_hash_a0_is_exact_match_only() {
10300 let _g = crate::test_util::global_state_lock();
10301 assert!(ext_glob_match("(#a0)foo", "foo"), "exact ok");
10302 assert!(
10303 !ext_glob_match("(#a0)foo", "fop"),
10304 "no edit budget rejects 1-sub"
10305 );
10306 }
10307
10308 /// `(#a1)foo` matches "fxoo" via INSERTION in input (extra 'x').
10309 /// The pattern has no `x`; treating `x` as an inserted-in-input
10310 /// char costs 1 error. Input → "f(x)oo" with the 'x' deleted.
10311 #[test]
10312 fn ext_glob_hash_a_accepts_insertion_in_input() {
10313 let _g = crate::test_util::global_state_lock();
10314 // "fxoo" — extra 'x' between 'f' and 'oo'.
10315 assert!(
10316 ext_glob_match("(#a1)foo", "fxoo"),
10317 "1 insertion-in-input edit"
10318 );
10319 }
10320
10321 /// `(#a1)foo` matches "fo" via DELETION from input (missing 'o').
10322 /// One 'o' deleted from input compared to pattern.
10323 #[test]
10324 fn ext_glob_hash_a_accepts_deletion_from_input() {
10325 let _g = crate::test_util::global_state_lock();
10326 assert!(
10327 ext_glob_match("(#a1)foo", "fo"),
10328 "1 deletion-from-input edit"
10329 );
10330 }
10331
10332 /// `(#a2)foo` matches "fxooy" via 1 insertion + 1 substitution.
10333 /// 'x' inserted between f-o; final 'o' substituted to 'y'. Wait —
10334 /// closer reading: "fxooy" vs "foo" — 'x' is the extra char, but
10335 /// then 'ooy' vs 'oo' has another extra 'y'. That's 2 insertions.
10336 #[test]
10337 fn ext_glob_hash_a_mixed_edits_within_budget() {
10338 let _g = crate::test_util::global_state_lock();
10339 // 2 insertions: 'x' and 'y'.
10340 assert!(
10341 ext_glob_match("(#a2)foo", "fxooy"),
10342 "2 insertions in input within budget"
10343 );
10344 }
10345
10346 /// Budget upper bound: `(#a1)abc` rejects "xyz" (3 substitutions > 1).
10347 #[test]
10348 fn ext_glob_hash_a1_rejects_three_substitutions() {
10349 let _g = crate::test_util::global_state_lock();
10350 assert!(!ext_glob_match("(#a1)abc", "xyz"));
10351 }
10352
10353 /// `(#a3)abc` matches "xyz" (3 substitutions ≤ 3).
10354 #[test]
10355 fn ext_glob_hash_a3_accepts_all_substituted() {
10356 let _g = crate::test_util::global_state_lock();
10357 assert!(
10358 ext_glob_match("(#a3)abc", "xyz"),
10359 "3 substitutions at budget 3"
10360 );
10361 }
10362
10363 /// Position-independent substitution: budget allows replacing
10364 /// any one of N pattern chars.
10365 #[test]
10366 fn ext_glob_hash_a_accepts_position_independent_substitution() {
10367 let _g = crate::test_util::global_state_lock();
10368 assert!(ext_glob_match("(#a1)abc", "Xbc"), "first-char sub");
10369 assert!(ext_glob_match("(#a1)abc", "aXc"), "middle-char sub");
10370 assert!(ext_glob_match("(#a1)abc", "abX"), "last-char sub");
10371 }
10372
10373 // ═══════════════════════════════════════════════════════════════════
10374 // zsh test-corpus pins — direct anchors to Test/D02glob.ztst:25-80
10375 // "zsh globbing" test list (zsh's authoritative regression suite).
10376 // Each test cites the ztst line range it pins. Currently-failing
10377 // tests get #[ignore = "ZSHRS BUG: ..."] markers; expected-output
10378 // assertions stay in tree so the marker flips when the Rust port
10379 // catches up.
10380 // ═══════════════════════════════════════════════════════════════════
10381
10382 /// `Test/D02glob.ztst:26` — `[[ foo~ = foo~ ]]` exact match,
10383 /// expected exit 0 (true).
10384 #[test]
10385 fn zsh_corpus_foo_tilde_exact_match() {
10386 let _g = crate::test_util::global_state_lock();
10387 assert!(
10388 ext_glob_match("foo~", "foo~"),
10389 "ztst:26 — literal tilde matches"
10390 );
10391 }
10392
10393 /// `Test/D02glob.ztst:27` — `[[ foo~ = (foo~) ]]` parenthesised
10394 /// single alternative.
10395 #[test]
10396 fn zsh_corpus_foo_tilde_in_parens() {
10397 let _g = crate::test_util::global_state_lock();
10398 assert!(
10399 ext_glob_match("(foo~)", "foo~"),
10400 "ztst:27 — (foo~) matches foo~"
10401 );
10402 }
10403
10404 /// `Test/D02glob.ztst:28` — `[[ foo~ = (foo~|) ]]` alternation
10405 /// with empty alternative.
10406 #[test]
10407 fn zsh_corpus_alternation_with_empty_alt() {
10408 let _g = crate::test_util::global_state_lock();
10409 assert!(
10410 ext_glob_match("(foo~|)", "foo~"),
10411 "ztst:28 — empty alt accepted"
10412 );
10413 }
10414
10415 /// `Test/D02glob.ztst:29` — `[[ foo.c = *.c~boo* ]]` exclude
10416 /// pattern: matches *.c BUT NOT boo*. `foo.c` matches *.c and
10417 /// doesn't match boo*, so result is 0 (true).
10418 #[test]
10419 fn zsh_corpus_exclude_pattern_basic() {
10420 let _g = crate::test_util::global_state_lock();
10421 assert!(
10422 ext_glob_match("*.c~boo*", "foo.c"),
10423 "ztst:29 — *.c~boo* matches foo.c"
10424 );
10425 }
10426
10427 /// `Test/D02glob.ztst:30` — `[[ foo.c = *.c~boo*~foo* ]]`
10428 /// double-exclude: matches *.c but excludes both `boo*` and
10429 /// `foo*`. `foo.c` matches `foo*` (excluded) → result 1 (false).
10430 #[test]
10431 fn zsh_corpus_exclude_pattern_double() {
10432 let _g = crate::test_util::global_state_lock();
10433 assert!(
10434 !ext_glob_match("*.c~boo*~foo*", "foo.c"),
10435 "ztst:30 — double exclude rejects foo.c"
10436 );
10437 }
10438
10439 /// `Test/D02glob.ztst:31` — `[[ fofo = (fo#)# ]]` — `#` is the
10440 /// extended-glob "1+ repetitions" quantifier. `(fo#)#` matches
10441 /// 1+ repetitions of "fo+".
10442 #[test]
10443 fn zsh_corpus_hash_repetition_double() {
10444 let _g = crate::test_util::global_state_lock();
10445 assert!(
10446 ext_glob_match("(fo#)#", "fofo"),
10447 "ztst:31 — (fo#)# matches fofo"
10448 );
10449 }
10450
10451 /// `Test/D02glob.ztst:32` — `[[ ffo = (fo#)# ]]`. `fo#` means
10452 /// `f` followed by 0+ `o`. `ffo` = `f` + `fo` (matches the
10453 /// outer `#` repetition).
10454 #[test]
10455 fn zsh_corpus_hash_repetition_min_one() {
10456 let _g = crate::test_util::global_state_lock();
10457 assert!(
10458 ext_glob_match("(fo#)#", "ffo"),
10459 "ztst:32 — (fo#)# matches ffo via 1-iter outer"
10460 );
10461 }
10462
10463 /// `Test/D02glob.ztst:36` — `[[ foooofof = (fo##)# ]]` —
10464 /// `##` is "2+ repetitions". `fo##` = `f` + 2+ `o`'s. The
10465 /// trailing `of` after `foooo` doesn't satisfy the trailing
10466 /// outer `#`, so result is 1 (false).
10467 #[test]
10468 fn zsh_corpus_hash_hash_quantifier_min_two() {
10469 let _g = crate::test_util::global_state_lock();
10470 assert!(
10471 !ext_glob_match("(fo##)#", "foooofof"),
10472 "ztst:36 — (fo##)# rejects foooofof"
10473 );
10474 }
10475
10476 /// `Test/D02glob.ztst:50` — `[[ aac = ((a))#a(c) ]]` —
10477 /// `((a))#` is `0+ a`. `aac` = `aa` (= `((a))#`) + `c`. The
10478 /// final `a(c)` requires literal `a` followed by capture `(c)`.
10479 /// Wait — should be `aac` matches `((a))#a(c)`: outer `((a))#`
10480 /// captures `a` repeated, then literal `a`, then `(c)`.
10481 /// Actually re-reading: `((a))#` matches 0+ 'a'. `aac` =
10482 /// `((a))#`("a") + `a`("a") + `(c)`("c"). So "aa" splits as
10483 /// (a)("a") + a("a") + c("c"). Match.
10484 #[test]
10485 fn zsh_corpus_nested_paren_quantifier() {
10486 let _g = crate::test_util::global_state_lock();
10487 assert!(
10488 ext_glob_match("((a))#a(c)", "aac"),
10489 "ztst:50 — ((a))#a(c) matches aac"
10490 );
10491 }
10492
10493 /// `Test/D02glob.ztst:51` — `[[ ac = ((a))#a(c) ]]` — single
10494 /// `a` matches `a(c)` after empty `((a))#` consumes zero.
10495 #[test]
10496 fn zsh_corpus_zero_iteration_quantifier() {
10497 let _g = crate::test_util::global_state_lock();
10498 assert!(
10499 ext_glob_match("((a))#a(c)", "ac"),
10500 "ztst:51 — ((a))# can be zero iters"
10501 );
10502 }
10503
10504 /// `Test/D02glob.ztst:52` — `[[ c = ((a))#a(c) ]]` — empty
10505 /// `((a))#` + literal `a` requires at least one `a`. `c` has
10506 /// no `a`, so result is 1 (false).
10507 #[test]
10508 fn zsh_corpus_required_literal_after_zero_quantifier() {
10509 let _g = crate::test_util::global_state_lock();
10510 assert!(
10511 !ext_glob_match("((a))#a(c)", "c"),
10512 "ztst:52 — bare `c` lacks required `a`"
10513 );
10514 }
10515
10516 /// `Test/D02glob.ztst:73` — `[[ foo = ((^x)) ]]` — exclude
10517 /// `x`. `foo` doesn't contain `x` as a single char (the
10518 /// `((^x))` matches anything that's not just 'x' — `foo` is 3
10519 /// chars, none is `x`).
10520 #[test]
10521 fn zsh_corpus_caret_exclude_basic() {
10522 let _g = crate::test_util::global_state_lock();
10523 assert!(
10524 ext_glob_match("((^x))", "foo"),
10525 "ztst:73 — ((^x)) matches foo"
10526 );
10527 }
10528
10529 /// `Test/D02glob.ztst:75` — `[[ foo = ((^foo)) ]]` — `^foo`
10530 /// rejects exact `foo`. `foo` matches `foo` literal, which the
10531 /// `^` inverts. Result: 1 (false).
10532 #[test]
10533 fn zsh_corpus_caret_exclude_exact_match_inverted() {
10534 let _g = crate::test_util::global_state_lock();
10535 assert!(
10536 !ext_glob_match("((^foo))", "foo"),
10537 "ztst:75 — ((^foo)) rejects foo"
10538 );
10539 }
10540
10541 /// `Test/D02glob.ztst:79` — `[[ foot = z*~*x ]]` — `*` then
10542 /// exclude `*x`. `foot` doesn't start with `z`, so doesn't
10543 /// match `z*` at all → result 1 (false).
10544 #[test]
10545 fn zsh_corpus_star_exclude_no_z_prefix() {
10546 let _g = crate::test_util::global_state_lock();
10547 assert!(
10548 !ext_glob_match("z*~*x", "foot"),
10549 "ztst:79 — foot doesn't start with z"
10550 );
10551 }
10552
10553 /// `Test/D02glob.ztst:80` — `[[ zoot = z*~*x ]]` — `zoot`
10554 /// matches `z*` AND doesn't end in `x` → result 0 (true).
10555 #[test]
10556 fn zsh_corpus_star_exclude_zoot() {
10557 let _g = crate::test_util::global_state_lock();
10558 assert!(
10559 ext_glob_match("z*~*x", "zoot"),
10560 "ztst:80 — zoot matches z* and not *x"
10561 );
10562 }
10563
10564 // ─── POSIX class brackets — Test/D02glob.ztst:111-118 ─────────────
10565
10566 /// `Test/D02glob.ztst:111` — `[[:alpha:][:punct:]]#[[:digit:]][^[:lower:]]`
10567 /// over "a%1X": `a` matches alpha, `%` matches punct (1+ via `#`),
10568 /// `1` matches digit, `X` matches NOT-lower.
10569 #[test]
10570 fn zsh_corpus_posix_class_alpha_punct_digit_notlower() {
10571 let _g = crate::test_util::global_state_lock();
10572 assert!(
10573 ext_glob_match("[[:alpha:][:punct:]]#[[:digit:]][^[:lower:]]", "a%1X"),
10574 "ztst:111 — alpha-punct-digit-notlower chain",
10575 );
10576 }
10577
10578 /// `c:Src/pattern.c:3894` — `PP_ASCII: return !(c & ~0x7f)`.
10579 ///
10580 /// `patcomppiece`'s inline class expansion had no `ascii` arm and no
10581 /// classmask bit, so the class fell to BOTH `_` catch-alls and contributed
10582 /// no members: `[[ a == [[:ascii:]] ]]` was false where zsh is true, and
10583 /// the negation inverted. Every other POSIX class was already correct.
10584 ///
10585 /// Note `range_type_lookup` above already asserted
10586 /// `range_type("ascii") == Some(3)` and PASSED throughout — the lookup was
10587 /// fine; nothing exercised the MATCH. That is presumably why a class that
10588 /// matched nothing went unnoticed. This test goes through `ext_glob_match`,
10589 /// the path a user's pattern actually takes.
10590 #[test]
10591 fn posix_ascii_class_has_members() {
10592 let _g = crate::test_util::global_state_lock();
10593 assert!(ext_glob_match("[[:ascii:]]", "a"), "ASCII char must match");
10594 assert!(ext_glob_match("[[:ascii:]]", "5"), "ASCII digit must match");
10595 assert!(
10596 !ext_glob_match("[^[:ascii:]]", "a"),
10597 "negation must reject an ASCII char",
10598 );
10599 // A byte >= 0x80 leads a UTF-8 sequence; the multibyte path reports no
10600 // match for this class, so the ASCII-only member set is complete.
10601 assert!(
10602 !ext_glob_match("[[:ascii:]]", "\u{e9}"),
10603 "non-ASCII must not match",
10604 );
10605 // The tell that made the class look present: the sibling range does
10606 // the work while the class contributes nothing.
10607 assert!(ext_glob_match("[[:ascii:]0-9]", "5"), "sibling range still matches");
10608 }
10609
10610 /// `Test/D02glob.ztst:112` — same pattern, "a%1" lacks the
10611 /// 4th non-lower char → result 1 (false).
10612 #[test]
10613 fn zsh_corpus_posix_class_rejects_short_input() {
10614 let _g = crate::test_util::global_state_lock();
10615 assert!(
10616 !ext_glob_match("[[:alpha:][:punct:]]#[[:digit:]][^[:lower:]]", "a%1"),
10617 "ztst:112 — short input rejected",
10618 );
10619 }
10620
10621 /// `Test/D02glob.ztst:113` — `[[:` literal in char class:
10622 /// `[[ [: = [[:]# ]]` — `[[:]` matches `[` OR `:`, `#` is 1+
10623 /// repetitions. "[:" = `[` + `:` → matches.
10624 #[test]
10625 fn zsh_corpus_literal_brackets_in_class_with_repetition() {
10626 let _g = crate::test_util::global_state_lock();
10627 assert!(
10628 ext_glob_match("[[:]#", "[:"),
10629 "ztst:113 — [[:]# matches '[:'"
10630 );
10631 }
10632
10633 // ─── (#i) / (#l) case modifiers — Test/D02glob.ztst:119-132 ───────
10634
10635 /// `Test/D02glob.ztst:119` — `(#i)FOOXX` matches "fooxx"
10636 /// (case-insensitive throughout).
10637 #[test]
10638 fn zsh_corpus_hash_i_case_insensitive() {
10639 let _g = crate::test_util::global_state_lock();
10640 assert!(
10641 ext_glob_match("(#i)FOOXX", "fooxx"),
10642 "ztst:119 — (#i)FOOXX matches fooxx"
10643 );
10644 }
10645
10646 /// `Test/D02glob.ztst:120` — `(#l)FOOXX` requires pattern UPPER
10647 /// chars to be the ONLY uppercase candidates; lowercase input
10648 /// FAILS to match because pattern is all uppercase and `#l` only
10649 /// matches the EXACT lowercase variant of the pattern char... actually
10650 /// `(#l)` means "lowercase pattern chars match uppercase too" —
10651 /// `(#l)FOOXX` has no lowercase chars so it stays a strict
10652 /// uppercase match. "fooxx" doesn't match. Result 1 (false).
10653 #[test]
10654 fn zsh_corpus_hash_l_uppercase_pattern_with_lower_input_fails() {
10655 let _g = crate::test_util::global_state_lock();
10656 assert!(
10657 !ext_glob_match("(#l)FOOXX", "fooxx"),
10658 "ztst:120 — (#l)FOOXX does NOT match fooxx"
10659 );
10660 }
10661
10662 /// `Test/D02glob.ztst:121` — `(#l)fooxx` (lowercase pattern) DOES
10663 /// match "FOOXX" because `#l` is the asymmetric "lowercase pat
10664 /// chars match upper-or-lower" rule.
10665 #[test]
10666 fn zsh_corpus_hash_l_lowercase_pattern_matches_uppercase_input() {
10667 let _g = crate::test_util::global_state_lock();
10668 assert!(
10669 ext_glob_match("(#l)fooxx", "FOOXX"),
10670 "ztst:121 — (#l)fooxx matches FOOXX (asymmetric upcasing)"
10671 );
10672 }
10673
10674 /// `Test/D02glob.ztst:122` — `(#i)FOO(#I)X(#i)X` mixes case
10675 /// modes. `(#I)` cancels prior `(#i)`. So 4th char `X` requires
10676 /// exact case. "fooxx" has lowercase `x` at that position → fail.
10677 #[test]
10678 fn zsh_corpus_hash_capital_i_cancels_case_insensitive() {
10679 let _g = crate::test_util::global_state_lock();
10680 assert!(
10681 !ext_glob_match("(#i)FOO(#I)X(#i)X", "fooxx"),
10682 "ztst:122 — (#I) cancels (#i), 4th char `X` requires upper"
10683 );
10684 }
10685
10686 /// `Test/D02glob.ztst:123` — same pattern with input "fooXx":
10687 /// `(#i)FOO` matches "foo", `(#I)X` matches "X" exact-case,
10688 /// `(#i)X` matches "x" insensitive → result 0 (true).
10689 #[test]
10690 fn zsh_corpus_hash_i_capital_i_toggle_succeeds() {
10691 let _g = crate::test_util::global_state_lock();
10692 assert!(
10693 ext_glob_match("(#i)FOO(#I)X(#i)X", "fooXx"),
10694 "ztst:123 — mixed case modifiers succeed"
10695 );
10696 }
10697
10698 /// `Test/D02glob.ztst:128` — `(#i)*m*` matches "Modules"
10699 /// case-insensitively.
10700 #[test]
10701 fn zsh_corpus_hash_i_with_star_glob() {
10702 let _g = crate::test_util::global_state_lock();
10703 assert!(
10704 ext_glob_match("(#i)*m*", "Modules"),
10705 "ztst:128 — (#i)*m* case-insensitive substring"
10706 );
10707 }
10708
10709 // ─── Numeric ranges `<n-m>` — Test/D02glob.ztst:133-137 ───────────
10710
10711 /// `Test/D02glob.ztst:133` — `<1-1000>33` matches "633" (the
10712 /// "6" is in [1..1000], followed by literal "33"). Actually
10713 /// the way zsh parses: `<1-1000>` matches ONE number from
10714 /// 1-1000, then `33` is literal. "633" = `6`(in 1-1000 range) +
10715 /// "33"(literal). So `<1-1000>33` matches "633".
10716 #[test]
10717 fn zsh_corpus_numeric_range_one_to_thousand() {
10718 let _g = crate::test_util::global_state_lock();
10719 assert!(
10720 ext_glob_match("<1-1000>33", "633"),
10721 "ztst:133 — <1-1000>33 matches 633"
10722 );
10723 }
10724
10725 /// `Test/D02glob.ztst:136` — `<->33` is the open-ended range
10726 /// (any number) followed by 33. Matches "633".
10727 #[test]
10728 fn zsh_corpus_numeric_range_open_ended() {
10729 let _g = crate::test_util::global_state_lock();
10730 assert!(
10731 ext_glob_match("<->33", "633"),
10732 "ztst:136 — <->33 matches 633 (any number)"
10733 );
10734 }
10735
10736 // ─── (#a) approximate match details — Test/D02glob.ztst:147-164 ───
10737
10738 /// `Test/D02glob.ztst:147` — `(#a1)[b][b]` matches "bob" via
10739 /// 1 substitution (middle `o` substituted in `[b][b]`'s gap).
10740 /// Wait — `[b][b]` is two-char "bb". Match against "bob" requires
10741 /// 1 edit. With (#a1), allowed.
10742 #[test]
10743 fn zsh_corpus_hash_a1_bracket_class_with_one_edit() {
10744 let _g = crate::test_util::global_state_lock();
10745 assert!(
10746 ext_glob_match("(#a1)[b][b]", "bob"),
10747 "ztst:147 — (#a1)[b][b] matches bob via 1 edit"
10748 );
10749 }
10750
10751 /// `Test/D02glob.ztst:151` — `(#a2)XbcX` matches "abcd" via
10752 /// 2 substitutions (a↔X, d↔X).
10753 #[test]
10754 fn zsh_corpus_hash_a2_two_substitutions() {
10755 let _g = crate::test_util::global_state_lock();
10756 assert!(
10757 ext_glob_match("(#a2)XbcX", "abcd"),
10758 "ztst:151 — 2 substitutions allowed"
10759 );
10760 }
10761
10762 /// `Test/D02glob.ztst:152` — `(#a2)ad` matches "abcd" via
10763 /// 2 INSERTIONS (b and c inserted into input vs pattern).
10764 /// Pattern is "ad" (2 chars), input is "abcd" (4 chars).
10765 /// Diff: 2 extra chars in input.
10766 #[test]
10767 fn zsh_corpus_hash_a2_two_insertions_in_input() {
10768 let _g = crate::test_util::global_state_lock();
10769 assert!(
10770 ext_glob_match("(#a2)ad", "abcd"),
10771 "ztst:152 — (#a2)ad matches abcd via 2 insertions in input"
10772 );
10773 }
10774
10775 /// `Test/D02glob.ztst:153` — `(#a2)abcd` matches "ad" via
10776 /// 2 DELETIONS from input (b and c missing from input vs pattern).
10777 #[test]
10778 fn zsh_corpus_hash_a2_two_deletions_from_input() {
10779 let _g = crate::test_util::global_state_lock();
10780 assert!(
10781 ext_glob_match("(#a2)abcd", "ad"),
10782 "ztst:153 — (#a2)abcd matches ad via 2 deletions"
10783 );
10784 }
10785
10786 /// `Test/D02glob.ztst:158` — `(#a2)abcd` rejects "dcba" — 4
10787 /// changes needed (full reverse) > budget 2.
10788 #[test]
10789 fn zsh_corpus_hash_a2_rejects_full_reverse() {
10790 let _g = crate::test_util::global_state_lock();
10791 assert!(
10792 !ext_glob_match("(#a2)abcd", "dcba"),
10793 "ztst:158 — full reverse exceeds budget 2"
10794 );
10795 }
10796
10797 /// `Test/D02glob.ztst:159` — `(#a3)abcd` matches "dcba" via
10798 /// 3 substitutions (positions 0,1,3 of input). Wait — actually
10799 /// "dcba" vs "abcd" — 4 positions differ. But (#a3) allows
10800 /// 3 errors. Hmm, but ztst says result is 0 (true). So zsh
10801 /// accepts. Possibly via DAMERAU transposition (swap adjacent).
10802 /// Without Damerau swap, this needs 4 subs → false. With
10803 /// transposition, "dcba" → "cdba" → "dcab"... complicated.
10804 /// Mark ignore — Damerau transposition isn't in the Rust port.
10805 #[test]
10806 fn zsh_corpus_hash_a3_reverse_via_transposition() {
10807 let _g = crate::test_util::global_state_lock();
10808 assert!(
10809 ext_glob_match("(#a3)abcd", "dcba"),
10810 "ztst:159 — (#a3)abcd matches dcba via Damerau transpositions"
10811 );
10812 }
10813
10814 // ─── (#s) start / (#e) end anchors — Test/D02glob.ztst:168-179 ────
10815
10816 /// `Test/D02glob.ztst:168` — `*((#s)|/)test((#e)|/)*` matches
10817 /// "test" — start-anchor (#s) at position 0, end-anchor (#e)
10818 /// after "test".
10819 #[test]
10820 fn zsh_corpus_hash_s_e_anchors_match_bare_test() {
10821 let _g = crate::test_util::global_state_lock();
10822 assert!(
10823 ext_glob_match("*((#s)|/)test((#e)|/)*", "test"),
10824 "ztst:168 — start/end anchors match bare 'test'",
10825 );
10826 }
10827
10828 /// `Test/D02glob.ztst:172` — `*((#s)|/)test((#e)|/)*` rejects
10829 /// "atest" — `(#s)|/` requires position 0 OR a `/`, but `a`
10830 /// is neither.
10831 #[test]
10832 fn zsh_corpus_hash_s_anchor_rejects_a_prefix() {
10833 let _g = crate::test_util::global_state_lock();
10834 assert!(
10835 !ext_glob_match("*((#s)|/)test((#e)|/)*", "atest"),
10836 "ztst:172 — atest fails the (#s) start-or-slash anchor",
10837 );
10838 }
10839
10840 // ─── Misc/globtests pins — `~` exclusion and `^` negation ─────────
10841
10842 /// `Misc/globtests` — `[[ foo~ = foo~ ]]` — bare tilde is literal.
10843 #[test]
10844 fn zsh_corpus_literal_tilde_in_pattern() {
10845 let _g = crate::test_util::global_state_lock();
10846 assert!(
10847 ext_glob_match("foo~", "foo~"),
10848 "literal ~ in non-extglob position"
10849 );
10850 }
10851
10852 /// `Misc/globtests` — `[[ foo.c = *.c~boo* ]]` — extended-glob
10853 /// exclusion: `*.c` minus things matching `boo*`. "foo.c" doesn't
10854 /// match "boo*", so it stays.
10855 #[test]
10856 fn zsh_corpus_exclusion_keeps_non_excluded() {
10857 let _g = crate::test_util::global_state_lock();
10858 assert!(ext_glob_match("*.c~boo*", "foo.c"), "exclusion keeps foo.c");
10859 }
10860
10861 /// `Misc/globtests` — `[[ foo.c = *.c~boo*~foo* ]]` — chained
10862 /// exclusions: `*.c` minus `boo*` minus `foo*`. "foo.c" matches
10863 /// `foo*`, so excluded.
10864 #[test]
10865 fn zsh_corpus_chained_exclusion_removes_match() {
10866 let _g = crate::test_util::global_state_lock();
10867 assert!(
10868 !ext_glob_match("*.c~boo*~foo*", "foo.c"),
10869 "chained exclusion excludes foo.c",
10870 );
10871 }
10872
10873 /// `Misc/globtests` — `[[ fofo = (fo#)# ]]` — outer `(...)#` is
10874 /// zero-or-more closure over `fo#` (one f followed by zero+ o).
10875 #[test]
10876 fn zsh_corpus_closure_of_closure_matches_repeated_fo() {
10877 let _g = crate::test_util::global_state_lock();
10878 assert!(ext_glob_match("(fo#)#", "fofo"), "(fo#)# matches fofo");
10879 }
10880
10881 /// `Misc/globtests` — `[[ ffo = (fo#)# ]]` — "ffo" = "f"+"fo".
10882 #[test]
10883 fn zsh_corpus_closure_matches_ffo() {
10884 let _g = crate::test_util::global_state_lock();
10885 assert!(ext_glob_match("(fo#)#", "ffo"), "(fo#)# matches ffo");
10886 }
10887
10888 /// `Misc/globtests` — `[[ xfoooofof = (fo#)# ]]` — leading "x"
10889 /// breaks the pattern.
10890 #[test]
10891 fn zsh_corpus_closure_rejects_leading_x() {
10892 let _g = crate::test_util::global_state_lock();
10893 assert!(
10894 !ext_glob_match("(fo#)#", "xfoooofof"),
10895 "(fo#)# rejects leading x",
10896 );
10897 }
10898
10899 /// `Misc/globtests` — `[[ foo = ((^x)) ]]` — `(^x)` matches one
10900 /// char that isn't 'x'. "foo" has 'f' first, so matches.
10901 /// Note: zsh's `(^x)` is exactly-one-not-x with extendedglob.
10902 #[test]
10903 fn zsh_corpus_negation_caret_matches_non_x_start() {
10904 let _g = crate::test_util::global_state_lock();
10905 assert!(ext_glob_match("((^x)*)", "foo"), "(^x)* matches foo");
10906 }
10907
10908 /// `Misc/globtests` — `[[ foo = ((^foo)) ]]` — `(^foo)` is "not foo",
10909 /// "foo" matches `foo`, so excluded → false.
10910 #[test]
10911 fn zsh_corpus_negation_caret_rejects_full_match() {
10912 let _g = crate::test_util::global_state_lock();
10913 assert!(
10914 !ext_glob_match("((^foo))", "foo"),
10915 "(^foo) rejects 'foo' itself",
10916 );
10917 }
10918
10919 /// `Misc/globtests` — `[[ abcd = ?(a|b)c#d ]]` — `?(a|b)` is
10920 /// "zero-or-one of a|b", `c#d` is "zero+ c then d". "abcd" = a + b? no, ?
10921 /// is single char or alternation. Let me re-read: `?(a|b)` matches
10922 /// one char which is `a` OR `b`. So "abcd": ?=a, then needs `c#d`
10923 /// = (c)*d, matches "bcd" if first char then need cd. Hmm.
10924 /// Actually: `?(a|b)` = `?` then `(a|b)` as separate? More likely
10925 /// `?(a|b)` is ksh-style "0-or-1 of a|b" only with KSH_GLOB.
10926 /// In zsh native (extendedglob) `?` is single char, `(a|b)` is
10927 /// alternation. So `?(a|b)c#d` = anychar (a|b) c* d. "abcd" =
10928 /// a + b + (zero c) + ... fails. The ztst says it MATCHES.
10929 /// Conclusion: requires KSH_GLOB which we may not enable.
10930 #[test]
10931 fn zsh_corpus_ksh_question_alternation() {
10932 let _g = crate::test_util::global_state_lock();
10933 assert!(
10934 ext_glob_match("?(a|b)c#d", "abcd"),
10935 "?(a|b)c#d matches abcd",
10936 );
10937 }
10938
10939 // ── patmatchlen — c:2649 ─────────────────────────────────────────
10940
10941 /// `Src/pattern.c:2649` — `int patmatchlen(void)` returns the
10942 /// byte-length of the last successful pattry match. After a
10943 /// successful match against `"hello"` with pattern `"hel*"`,
10944 /// the recorded length is 5 (all of "hello" was consumed).
10945 #[test]
10946 fn patmatchlen_records_consumed_byte_length() {
10947 let _g = crate::test_util::global_state_lock();
10948 let prog = compile("hel*");
10949 assert!(pattry(&prog, "hello"), "hel* matches hello");
10950 assert_eq!(patmatchlen(), 5, "all 5 bytes of 'hello' consumed by hel*",);
10951 }
10952
10953 /// Anchored prefix match: `"foo"` against `"foobar"` with the
10954 /// `NOANCH` form is the user-visible case `[[ foobar = foo* ]]`,
10955 /// which consumes all 6 bytes when the pattern matches the whole
10956 /// string.
10957 #[test]
10958 fn patmatchlen_full_string_match_returns_full_length() {
10959 let _g = crate::test_util::global_state_lock();
10960 let prog = compile("foo*");
10961 assert!(pattry(&prog, "foobar"));
10962 assert_eq!(patmatchlen(), 6, "foo* against 'foobar' = 6 bytes");
10963 }
10964
10965 // ── patmatchindex (c:4004) ──────────────────────────────────────
10966
10967 /// `patmatchindex` on a literal-byte range returns the byte at
10968 /// the requested position.
10969 #[test]
10970 fn patmatchindex_literal_byte_at_index() {
10971 let _g = crate::test_util::global_state_lock();
10972 let range = b"abc";
10973 assert_eq!(patmatchindex(range, 0), Some((Some(b'a'), 0)));
10974 assert_eq!(patmatchindex(range, 1), Some((Some(b'b'), 0)));
10975 assert_eq!(patmatchindex(range, 2), Some((Some(b'c'), 0)));
10976 assert_eq!(patmatchindex(range, 3), None, "out-of-range index");
10977 }
10978
10979 /// `patmatchindex` on a PP_ALPHA class marker returns `(None,
10980 /// PP_ALPHA)` to signal the class without a literal char.
10981 #[test]
10982 fn patmatchindex_posix_class_marker_returns_mtp() {
10983 let _g = crate::test_util::global_state_lock();
10984 // Meta + PP_ALPHA = 0x83 + 1 = 0x84
10985 let range = &[Meta + PP_ALPHA as u8];
10986 let r = patmatchindex(range, 0);
10987 assert_eq!(r, Some((None, PP_ALPHA)));
10988 }
10989
10990 // ── mb_patmatchrange (c:3610) ───────────────────────────────────
10991
10992 /// `mb_patmatchrange` literal hit on `'a'`.
10993 #[test]
10994 fn mb_patmatchrange_literal_ascii_hits() {
10995 let _g = crate::test_util::global_state_lock();
10996 let range = b"a";
10997 let mut mtp = -1;
10998 assert!(
10999 mb_patmatchrange(range, 'a', 0, None, Some(&mut mtp)),
11000 "literal 'a' matches"
11001 );
11002 assert_eq!(mtp, 0, "literal hit sets mtp=0");
11003 }
11004
11005 /// `mb_patmatchrange` PP_DIGIT class hit on `'5'`.
11006 #[test]
11007 fn mb_patmatchrange_digit_class_hits() {
11008 let _g = crate::test_util::global_state_lock();
11009 let range = &[Meta + PP_DIGIT as u8];
11010 assert!(mb_patmatchrange(range, '5', 0, None, None));
11011 assert!(!mb_patmatchrange(range, 'x', 0, None, None));
11012 }
11013
11014 /// `mb_patmatchrange` PP_RANGE on `'b'` in `a..c`.
11015 #[test]
11016 fn mb_patmatchrange_range_hits_middle() {
11017 let _g = crate::test_util::global_state_lock();
11018 // Meta + PP_RANGE then two literal bytes for the endpoints.
11019 let range = &[Meta + PP_RANGE as u8, b'a', b'c'];
11020 assert!(mb_patmatchrange(range, 'b', 0, None, None));
11021 assert!(mb_patmatchrange(range, 'a', 0, None, None));
11022 assert!(mb_patmatchrange(range, 'c', 0, None, None));
11023 assert!(!mb_patmatchrange(range, 'd', 0, None, None));
11024 }
11025
11026 // ── mb_patmatchindex (c:3767) ───────────────────────────────────
11027
11028 /// `mb_patmatchindex` literal char at index 0.
11029 #[test]
11030 fn mb_patmatchindex_literal_first_index() {
11031 let _g = crate::test_util::global_state_lock();
11032 let range = b"xyz";
11033 let r = mb_patmatchindex(range, 0);
11034 assert_eq!(r, Some((Some('x'), 0)));
11035 }
11036
11037 /// `mb_patmatchindex` on PP_UPPER marker at index 0.
11038 #[test]
11039 fn mb_patmatchindex_class_marker_returns_mtp() {
11040 let _g = crate::test_util::global_state_lock();
11041 let range = &[Meta + PP_UPPER as u8];
11042 assert_eq!(mb_patmatchindex(range, 0), Some((None, PP_UPPER)));
11043 }
11044
11045 // ═══════════════════════════════════════════════════════════════════
11046 // Additional C-parity tests for Src/pattern.c bit-flag predicates.
11047 // ═══════════════════════════════════════════════════════════════════
11048
11049 /// c:200 — P_ISBRANCH checks bit 0x20.
11050 #[test]
11051 fn P_ISBRANCH_recognizes_bit_0x20() {
11052 assert!(P_ISBRANCH(0x20));
11053 assert!(P_ISBRANCH(0x20 | 0x01));
11054 assert!(P_ISBRANCH(0xFF));
11055 assert!(!P_ISBRANCH(0x00));
11056 assert!(!P_ISBRANCH(0x1F));
11057 }
11058
11059 /// c:201 — P_ISEXCLUDE checks bits 0x30 = 0x10 | 0x20 (BOTH must be set).
11060 #[test]
11061 fn P_ISEXCLUDE_requires_both_bits() {
11062 assert!(P_ISEXCLUDE(0x30));
11063 assert!(P_ISEXCLUDE(0x30 | 0x01));
11064 assert!(!P_ISEXCLUDE(0x10), "only 0x10 — must require both");
11065 assert!(!P_ISEXCLUDE(0x20), "only 0x20 — must require both");
11066 assert!(!P_ISEXCLUDE(0x00));
11067 }
11068
11069 /// c:202 — P_NOTDOT checks bit 0x40.
11070 #[test]
11071 fn P_NOTDOT_recognizes_bit_0x40() {
11072 assert!(P_NOTDOT(0x40));
11073 assert!(P_NOTDOT(0x40 | 0x01));
11074 assert!(P_NOTDOT(0xFF));
11075 assert!(!P_NOTDOT(0x00));
11076 assert!(!P_NOTDOT(0x3F));
11077 }
11078
11079 /// c:216-218 — P_SIMPLE/HSTART/PURESTR are distinct bits.
11080 #[test]
11081 fn p_flag_constants_are_distinct() {
11082 assert_eq!(P_SIMPLE & P_HSTART, 0);
11083 assert_eq!(P_SIMPLE & P_PURESTR, 0);
11084 assert_eq!(P_HSTART & P_PURESTR, 0);
11085 }
11086
11087 /// c:216 — P_SIMPLE is bit 0 (= 0x01).
11088 #[test]
11089 fn p_simple_is_bit_zero() {
11090 assert_eq!(P_SIMPLE, 0x01);
11091 }
11092
11093 /// c:217 — P_HSTART is bit 1 (= 0x02).
11094 #[test]
11095 fn p_hstart_is_bit_one() {
11096 assert_eq!(P_HSTART, 0x02);
11097 }
11098
11099 /// c:218 — P_PURESTR is bit 2 (= 0x04).
11100 #[test]
11101 fn p_purestr_is_bit_two() {
11102 assert_eq!(P_PURESTR, 0x04);
11103 }
11104
11105 /// c:406-407 — PA_NOALIGN=1, PA_UNMETA=2 (single-bit flags).
11106 #[test]
11107 fn pa_flag_constants_canonical() {
11108 assert_eq!(PA_NOALIGN, 1);
11109 assert_eq!(PA_UNMETA, 2);
11110 }
11111
11112 /// PA_NOALIGN | PA_UNMETA combine without overlap.
11113 #[test]
11114 fn pa_flags_pairwise_disjoint() {
11115 assert_eq!(PA_NOALIGN & PA_UNMETA, 0);
11116 }
11117
11118 /// c:336 — metacharinc on ASCII char advances by 1 byte.
11119 #[test]
11120 fn metacharinc_ascii_advances_one() {
11121 let _g = crate::test_util::global_state_lock();
11122 assert_eq!(metacharinc("hello", 0), 1);
11123 assert_eq!(metacharinc("hello", 4), 5);
11124 }
11125
11126 /// c:336 — metacharinc at end-of-string returns same pos.
11127 #[test]
11128 fn metacharinc_end_of_string_returns_same() {
11129 let _g = crate::test_util::global_state_lock();
11130 assert_eq!(metacharinc("abc", 3), 3, "at end → no advance");
11131 }
11132
11133 /// c:336 — metacharinc on multibyte char advances by codepoint width.
11134 #[test]
11135 fn metacharinc_multibyte_advances_by_codepoint_len() {
11136 let _g = crate::test_util::global_state_lock();
11137 // 'é' is 2 bytes in UTF-8.
11138 assert_eq!(metacharinc("é", 0), 2);
11139 // '日' is 3 bytes.
11140 assert_eq!(metacharinc("日", 0), 3);
11141 }
11142
11143 // ═══════════════════════════════════════════════════════════════════
11144 // Additional C-parity tests for Src/pattern.c
11145 // c:155 P_ISBRANCH / c:161 P_ISEXCLUDE / c:167 P_NOTDOT /
11146 // c:262 metacharinc / c:502 patcompstart / c:536 patcompile /
11147 // c:1008 patgetglobflags / c:1129 range_type / c:1152 pattern_range_to_string
11148 // c:2059 charref / c:2066 charnext / c:2153 pattry / c:2304 patmatchlen
11149 // ═══════════════════════════════════════════════════════════════════
11150
11151 /// c:262 — `metacharinc` is pure for ASCII input.
11152 #[test]
11153 fn metacharinc_is_pure_for_ascii() {
11154 for s in ["", "a", "abc", "hello"] {
11155 for pos in 0..s.len() {
11156 let first = metacharinc(s, pos);
11157 for _ in 0..3 {
11158 assert_eq!(
11159 metacharinc(s, pos),
11160 first,
11161 "metacharinc({:?}, {}) must be pure",
11162 s,
11163 pos
11164 );
11165 }
11166 }
11167 }
11168 }
11169
11170 /// c:536 — `patcompile("", 0, None)` returns Option<Patprog>.
11171 #[test]
11172 fn patcompile_returns_option_patprog_type() {
11173 let _g = crate::test_util::global_state_lock();
11174 let _: Option<Patprog> = patcompile("", 0, None);
11175 }
11176
11177 /// c:1008 — `patgetglobflags("")` empty returns None.
11178 #[test]
11179 fn patgetglobflags_empty_returns_none() {
11180 let _g = crate::test_util::global_state_lock();
11181 assert!(patgetglobflags("").is_none(), "empty → None");
11182 }
11183
11184 /// c:1008 — `patgetglobflags` returns Option<(i32, i64, usize)> type.
11185 #[test]
11186 fn patgetglobflags_returns_option_tuple_type() {
11187 let _g = crate::test_util::global_state_lock();
11188 let _: Option<(i32, i64, usize)> = patgetglobflags("abc");
11189 }
11190
11191 /// c:1129 — `range_type("")` empty returns None.
11192 #[test]
11193 fn range_type_empty_returns_none() {
11194 assert!(range_type("").is_none(), "empty → None");
11195 }
11196
11197 /// c:1129 — `range_type` returns Option<usize>.
11198 #[test]
11199 fn range_type_returns_option_usize_type() {
11200 let _: Option<usize> = range_type("abc");
11201 }
11202
11203 /// c:1152 — `pattern_range_to_string("")` empty returns empty String.
11204 #[test]
11205 fn pattern_range_to_string_empty_returns_empty() {
11206 assert_eq!(pattern_range_to_string(""), "");
11207 }
11208
11209 /// c:1152 — `pattern_range_to_string` is pure.
11210 #[test]
11211 fn pattern_range_to_string_is_pure() {
11212 for s in ["", "abc", "a-z"] {
11213 let first = pattern_range_to_string(s);
11214 for _ in 0..3 {
11215 assert_eq!(
11216 pattern_range_to_string(s),
11217 first,
11218 "pattern_range_to_string({:?}) must be pure",
11219 s
11220 );
11221 }
11222 }
11223 }
11224
11225 /// c:2059 — `charref("", 0)` empty returns None.
11226 #[test]
11227 fn charref_empty_returns_none() {
11228 assert!(charref("", 0).is_none(), "empty → None");
11229 }
11230
11231 /// c:2066 — `charnext("", 0)` empty returns 0 (no advance).
11232 #[test]
11233 fn charnext_empty_returns_zero() {
11234 assert_eq!(charnext("", 0), 0);
11235 }
11236
11237 /// c:2304 — `patmatchlen` returns i32 (compile-time type pin).
11238 #[test]
11239 fn patmatchlen_returns_i32_type() {
11240 let _g = crate::test_util::global_state_lock();
11241 let _: i32 = patmatchlen();
11242 }
11243
11244 /// c:155 — P_ISBRANCH/P_ISEXCLUDE/P_NOTDOT all return bool (type pin).
11245 #[test]
11246 fn p_predicates_return_bool_type() {
11247 let _: bool = P_ISBRANCH(0);
11248 let _: bool = P_ISEXCLUDE(0);
11249 let _: bool = P_NOTDOT(0);
11250 }
11251}