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. (Section removed — formerly held Rust-only entries
34//! `patmatchrange(&[char], char, igncase)`,
35//! `patmatchindex(&[char], idx)`, `mb_patmatchrange`,
36//! `mb_patmatchindex`. All deleted as Rule B / semantic
37//! deviations; see comments at the deletion sites for the
38//! faithful-port plan. The Cpattern-byte-stream walker in
39//! `zle/compmatch.rs::patmatchrange` is the production matcher
40//! pending a Rule C relocation to pattern.rs.)
41//! 16. String pre-processing (`patmungestring`, `patallocstr`,
42//! `pattrystart`)
43//! 17. Module-loader / disable mgmt (`startpatternscope`,
44//! `endpatternscope`, `savepatterndisables`,
45//! `restorepatterndisables`, `clearpatterndisables`,
46//! `freepatprog`, `pat_enables`)
47//! 18. (Section removed — formerly held Rust-only convenience entries
48//! `patmatch(pat, text)`, `patmatchlen(prog, string)`, `patrepeat(
49//! prog, s, max)`, `mb_patmatchrange`, `mb_patmatchindex`. All
50//! deleted: `patmatch` got renamed to the C-faithful bytecode-
51//! walker name; the others had zero Rust callers + Rule S1
52//! signature deviations. `haswilds` is a real C name — port lives
53//! in section 4 helpers.)
54//!
55//! See `docs/PORT.md` Rules A/B/C/D/E.
56
57#![allow(non_upper_case_globals)]
58#![allow(non_camel_case_types)]
59
60use crate::ported::params::{paramtab, paramtab_hashed_storage};
61use crate::ported::utils::ztrsub;
62use crate::ported::zle::zle_h::{COMP_LIST_COMPLETE, COMP_LIST_EXPAND};
63pub use crate::ported::zsh_h::{
64 patstralloc, Patstralloc, GF_BACKREF, GF_IGNCASE, GF_LCMATCHUC, GF_MATCHREF, GF_MULTIBYTE,
65 PAT_ANY, PAT_FILE, PAT_FILET, PAT_HAS_EXCLUDP, PAT_HEAPDUP, PAT_LCMATCHUC, PAT_NOANCH,
66 PAT_NOGLD, PAT_NOTEND, PAT_NOTSTART, PAT_PURES, PAT_SCAN, PAT_STATIC, PAT_ZDUP, PP_ALNUM,
67 PP_ALPHA, PP_ASCII, PP_BLANK, PP_CNTRL, PP_DIGIT, PP_GRAPH, PP_IDENT, PP_IFS, PP_IFSSPACE,
68 PP_INCOMPLETE, PP_INVALID, PP_LOWER, PP_PRINT, PP_PUNCT, PP_RANGE, PP_SPACE, PP_UPPER, PP_WORD,
69 PP_XDIGIT, ZMB_INCOMPLETE, ZMB_INVALID, ZPC_SEG_COUNT,
70};
71use crate::utils::zerrnam;
72use crate::zsh_h::{
73 isset, patprog, Bang, Bar, Hat, Inang, Inbrack, Inpar, Marker, Meta, Nularg, Outbrack, Pound,
74 Quest, Star, BASHAUTOLIST, CASEGLOB, CASEPATHS, EXTENDEDGLOB, KSHGLOB, MULTIBYTE,
75 NUMERICGLOBSORT, PM_HASHED, PM_TYPE, RCQUOTES, SHGLOB, SORTIT_IGNORING_BACKSLASHES,
76 SORTIT_NUMERICALLY, ZPC_BAR, ZPC_BNULLKEEP, ZPC_COUNT, ZPC_HASH, ZPC_HAT, ZPC_INANG,
77 ZPC_INBRACK, ZPC_INPAR, ZPC_KSH_AT, ZPC_KSH_BANG, ZPC_KSH_BANG2, ZPC_KSH_PLUS, ZPC_KSH_QUEST,
78 ZPC_KSH_STAR, ZPC_NULL, ZPC_OUTPAR, ZPC_QUEST, ZPC_SLASH, ZPC_STAR, ZPC_TILDE,
79};
80use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering};
81use std::sync::Mutex;
82
83// =====================================================================
84// 6. ZPC_* enum from zsh.h:1644 — indexes into the active-pattern-
85// characters table that compile-time and runtime both consult.
86// =====================================================================
87
88/// Maximum captures, from `pattern.c:94 NSUBEXP`.
89pub const NSUBEXP: usize = 9;
90
91// =====================================================================
92// 1. P_* opcode constants — pattern.c:97-127
93//
94// Numbered identically to C so a buffer compiled by this port matches
95// the C source's bytecode-cache format byte-for-byte (modulo native
96// endianness; we pin LE — see file header).
97// =====================================================================
98/// `P_END` constant.
99pub const P_END: u8 = 0x00; // c:97 End of program.
100/// `P_EXCSYNC` constant.
101pub const P_EXCSYNC: u8 = 0x01; // c:98 Test if following exclude already failed
102/// `P_EXCEND` constant.
103pub const P_EXCEND: u8 = 0x02; // c:99 Test if exclude matched orig branch
104/// `P_BACK` constant.
105pub const P_BACK: u8 = 0x03; // c:100 Match "", "next" ptr points backward.
106/// `P_EXACTLY` constant.
107pub const P_EXACTLY: u8 = 0x04; // c:101 lstr — match this string.
108/// `P_NOTHING` constant.
109pub const P_NOTHING: u8 = 0x05; // c:102 Match empty string.
110/// `P_ONEHASH` constant.
111pub const P_ONEHASH: u8 = 0x06; // c:103 node — match 0 or more of preceding simple.
112/// `P_TWOHASH` constant.
113pub const P_TWOHASH: u8 = 0x07; // c:104 node — match 1 or more of preceding simple.
114/// `P_GFLAGS` constant.
115pub const P_GFLAGS: u8 = 0x08; // c:105 long — match nothing and set globbing flags.
116/// `P_ISSTART` constant.
117pub const P_ISSTART: u8 = 0x09; // c:106 Match start of string.
118/// `P_ISEND` constant.
119pub const P_ISEND: u8 = 0x0a; // c:107 Match end of string.
120/// `P_COUNTSTART` constant.
121pub const P_COUNTSTART: u8 = 0x0b; // c:108 Initialise P_COUNT.
122/// `P_COUNT` constant.
123pub const P_COUNT: u8 = 0x0c; // c:109 3*long uc* node — match a number of repetitions.
124/// `P_BRANCH` constant.
125pub const P_BRANCH: u8 = 0x20; // c:112 node — match this alternative, or the next.
126/// `P_WBRANCH` constant.
127pub const P_WBRANCH: u8 = 0x21; // c:113 uc* node — P_BRANCH, but match at least 1 char.
128/// `P_EXCLUDE` constant.
129pub const P_EXCLUDE: u8 = 0x30; // c:114 uc* node — exclude this from previous branch.
130/// `P_EXCLUDP` constant.
131pub const P_EXCLUDP: u8 = 0x31; // c:115 uc* node — exclude, using full file path so far.
132/// `P_ANY` constant.
133pub const P_ANY: u8 = 0x40; // c:117 Match any one character.
134/// `P_ANYOF` constant.
135pub const P_ANYOF: u8 = 0x41; // c:118 str — match any character in this string.
136/// `P_ANYBUT` constant.
137pub const P_ANYBUT: u8 = 0x42; // c:119 str — match any character not in this string.
138/// `P_STAR` constant.
139pub const P_STAR: u8 = 0x43; // c:120 Match any set of characters.
140/// `P_NUMRNG` constant.
141pub const P_NUMRNG: u8 = 0x44; // c:121 zr,zr — match a numeric range.
142/// `P_NUMFROM` constant.
143pub const P_NUMFROM: u8 = 0x45; // c:122 zr — match a number >= X.
144/// `P_NUMTO` constant.
145pub const P_NUMTO: u8 = 0x46; // c:123 zr — match a number <= X.
146/// `P_NUMANY` constant.
147pub const P_NUMANY: u8 = 0x47; // c:124 Match any set of decimal digits.
148/// `P_OPEN` constant.
149pub const P_OPEN: u8 = 0x80; // c:126 Mark this point in input as start of n.
150/// `P_CLOSE` constant.
151pub const P_CLOSE: u8 = 0x90; // c:127 Analogous to OPEN.
152
153/// `P_ISBRANCH(p)` macro from pattern.c:200 — `(p->l & 0x20)`.
154#[inline]
155pub fn P_ISBRANCH(op: u8) -> bool {
156 (op & 0x20) != 0
157}
158
159/// `P_ISEXCLUDE(p)` macro from pattern.c:201 — `((p->l & 0x30) == 0x30)`.
160#[inline]
161pub fn P_ISEXCLUDE(op: u8) -> bool {
162 (op & 0x30) == 0x30
163}
164
165/// `P_NOTDOT(p)` macro from pattern.c:202 — `(p->l & 0x40)`.
166#[inline]
167pub fn P_NOTDOT(op: u8) -> bool {
168 (op & 0x40) != 0
169}
170
171// =====================================================================
172// 3. Flag-bit constants returned via flagp out-params during compile.
173// pattern.c:216-218
174// =====================================================================
175/// `P_SIMPLE` constant.
176pub const P_SIMPLE: i32 = 0x01; // c:216 Simple enough to be # / ## operand.
177/// `P_HSTART` constant.
178pub const P_HSTART: i32 = 0x02; // c:217 Starts with # or ##'d pattern.
179/// `P_PURESTR` constant.
180pub const P_PURESTR: i32 = 0x04; // c:218 Can be matched with a strcmp.
181
182// =====================================================================
183// 5. PAT_* flag constants — re-exports of zsh.h:1623-1640 already in
184// zsh_h.rs. Re-published here so callers in pattern's API don't need
185// the longer path. C source has these as `#define` in zsh.h, not
186// pattern.c, so the canonical home is zsh_h.rs; we just alias.
187// =====================================================================
188
189// C: `static int patnpar;` — number of active parens (1-indexed at
190// compile time; the *struct* patnpar is the actual count).
191pub static patnpar: AtomicI32 = AtomicI32::new(0); // c:271
192
193// GF_* glob-flag bits live in `zsh.h:1763-1773`, ported to
194// `src/ported/zsh_h.rs:2287-2291` per Rule C. Re-export so pattern's
195// matcher arms can read them without the longer path.
196
197// =====================================================================
198// 7. File-static globals — direct mirror of pattern.c file-scope
199// statics. Each C `static` ports to a Rust `static` of matching name
200// (Rule A) and `Mutex<>` / `Atomic*` for thread-safe access. None
201// are aggregated (Rule D).
202// =====================================================================
203
204// C: `static char *patout, *patcode;` + `static long patsize;` +
205// `static int patalloc;` — pattern.c:267-272 (in macro region).
206//
207// patout: the bytecode buffer.
208// patcode: write cursor (offset into patout).
209// patsize: current logical size.
210// patalloc: allocated capacity.
211//
212// In Rust, `Vec<u8>` already tracks both len and capacity, so we
213// hold just the buffer; patcode/patsize/patalloc are derived.
214pub static patout: Mutex<Vec<u8>> = Mutex::new(Vec::new()); // c:267
215
216// C: `static int patflags;` — current PAT_* flag set during compile.
217pub static patflags: AtomicI32 = AtomicI32::new(0); // c:272
218
219// C: `static int patglobflags;` — current globbing flags during compile.
220pub static patglobflags: AtomicI32 = AtomicI32::new(0); // c:273
221
222/// Port of file-static `int patinlen` from `pattrystate` struct
223/// at `Src/pattern.c:1877` (accessed via `#define patinlen
224/// (pattrystate.patinlen)` at line 1894). Length in metafied bytes
225/// of the last successful pattry match; computed at the end of
226/// `pattry`/`pattrylen`/`pattryrefs` (`patinput - patinstart`,
227/// pattern.c:2508). Read by `patmatchlen()` and by the
228/// `${var//pat/repl}` paramsubst pipeline that needs to know how
229/// much of the input was consumed.
230pub static patinlen: AtomicI32 = AtomicI32::new(0); // c:1877
231
232// =====================================================================
233// 12. Char-decode helpers — pattern.c:327, :336, :1909-1997
234// =====================================================================
235
236/// Port of `clear_shiftstate()` from `Src/pattern.c:327`. C uses
237/// `mbstate_t`; Rust `char` is already a code point, so no shift
238/// state to clear.
239pub fn clear_shiftstate() {} // c:327
240
241/// Port of `metacharinc(char **x)` from `Src/pattern.c:336`. Advances past
242/// Port of `wchar_t metacharinc(char **x)` from `Src/pattern.c:336`.
243/// Advances `*x` past one metafied / multibyte char and returns the
244/// decoded codepoint. The C body branches on:
245/// - `GF_MULTIBYTE` clear OR high-bit-clear: single-byte path
246/// with `itok(*x)` zsh-token translation and `Meta` xor-32
247/// - else: `mbrtowc` over the metafied bytes with state machine
248///
249/// **Rust port:** UTF-32-native delegation to `&str.chars()`.
250/// Delegates to Rust's native UTF-8 iterator — both C branches
251/// collapse because Rust's `char` is already the wide-char form,
252/// and the stored slice is the post-Meta-decode form (zshrs's
253/// source bytes are already UTF-8, not Meta-encoded). The `itok` →
254/// `ztokens[]` zsh-token table mapping doesn't apply because the
255/// pattern compiler stores raw chars; translation happens at
256/// compile time, not at scan time.
257///
258/// Rust signature differs from C `metacharinc(char **x)`: C mutates
259/// `*x` to advance the pointer; Rust returns the new byte position
260/// since `&str` length is immutable. Callers update their cursor
261/// from the return value.
262pub fn metacharinc(s: &str, pos: usize) -> usize {
263 // c:336
264 // c:343-360 single-byte short-circuit + c:363-380 mbrtowc loop:
265 // both collapse to Rust's `chars().next()` which decodes one
266 // valid UTF-8 codepoint from the slice, regardless of byte width.
267 s[pos..]
268 .chars()
269 .next()
270 .map(|c| pos + c.len_utf8())
271 .unwrap_or(pos)
272}
273
274// =====================================================================
275// 8. Bytecode write helpers — pattern.c:412-1856
276// =====================================================================
277
278/// Port of the anonymous `enum { PA_NOALIGN = 1, PA_UNMETA = 2 };`
279/// from `Src/pattern.c:405-408`. Flags passed as the `paflags` arg
280/// to `patadd`.
281pub const PA_NOALIGN: i32 = 1; // c:406
282/// `PA_UNMETA` constant.
283pub const PA_UNMETA: i32 = 2; // c:407
284
285/// Direct port of `static void patadd(char *add, int ch, long n,
286/// int paflags)` from `Src/pattern.c:410-450`.
287///
288/// Append `n` bytes from `add` (or a single `ch` byte when `add ==
289/// None`) to `patout`, growing the backing storage if needed and
290/// padding to the C `union upat` alignment unless `PA_NOALIGN` is
291/// set. With `PA_UNMETA`, walk the input as zsh-metafied bytes
292/// (Meta + (b^32)) and untokenize (`itok` → `ztokens[]`).
293///
294/// **The previous Rust impl had three concrete defects:**
295/// 1. Declared `-> i64` returning the starting offset. C is
296/// `static void`; callers use side-effects on `patcode`,
297/// not a return value.
298/// 2. When `add == None`, the C body writes `ch` **once**
299/// (`*patcode++ = ch;`). The Rust port looped `0..n` pushing
300/// `ch` n times, so a single-byte literal grew to n copies.
301/// 3. Skipped the C alignment-round-up to `sizeof(union upat)`
302/// (8 bytes on every supported arch — c:417-418), so any
303/// caller reading `patout` as a `[upat]` would mis-align.
304///
305/// zshrs's `patcode` pointer collapses into `patout.len()`; the
306/// realloc dance in C (c:419-425) is implicit via `Vec::extend`.
307/// `patsize` updates are the post-write `patout.len()`.
308fn patadd(add: Option<&[u8]>, ch: u8, n: i64, paflags: i32) {
309 use crate::ported::lex::ztokens as ztokens_str;
310 use crate::ported::zsh_h::Pound;
311 use crate::ported::ztype_h::itok;
312 let ztokens = ztokens_str.as_bytes();
313 // c:412
314 // c:415 — `long newpatsize = patsize + n;`
315 let mut buf = patout.lock().unwrap();
316 let patsize = buf.len() as i64;
317 let mut newpatsize = patsize + n;
318 // c:416-418 — round up to upat-union alignment (sizeof(union upat) =
319 // 8 on every supported arch) unless PA_NOALIGN.
320 if (paflags & PA_NOALIGN) == 0 {
321 // c:416
322 let upat = 8i64; // c:417 sizeof(union upat)
323 newpatsize = (newpatsize + upat - 1) & !(upat - 1); // c:417-418
324 }
325 // c:419-425 — realloc; Rust Vec auto-grows so just resize_to.
326 if newpatsize > buf.len() as i64 {
327 buf.resize(newpatsize as usize, 0);
328 }
329 // c:426 — `patsize = newpatsize;` — patsize is the buffer's
330 // logical write head; zshrs tracks it via buf.len() AFTER the
331 // write below.
332 let mut patcode_pos = patsize as usize; // c:Src/pattern.c — `patcode` pointer.
333
334 if let Some(add_bytes) = add {
335 // c:427 — `if (add) {`
336 if (paflags & PA_UNMETA) != 0 {
337 // c:428-442 — PA_UNMETA: walk add_bytes, unmetafy + untokenize
338 // as we go. Meta chars aren't counted in n. itok bytes route
339 // through ztokens[*add - Pound].
340 let mut idx = 0usize;
341 let mut remaining = n;
342 while remaining > 0 && idx < add_bytes.len() {
343 let b = add_bytes[idx];
344 if itok(b) {
345 // c:434-435 — `if (itok(*add)) *patcode++ =
346 // ztokens[*add++ - Pound];`
347 if idx < add_bytes.len() {
348 let tok_idx = b.wrapping_sub(Pound as u8) as usize;
349 if tok_idx < ztokens.len() {
350 // c:435
351 if patcode_pos < buf.len() {
352 buf[patcode_pos] = ztokens[tok_idx];
353 }
354 patcode_pos += 1;
355 }
356 idx += 1;
357 }
358 } else if b == Meta {
359 // c:436-438 — `else if (*add == Meta) { add++;
360 // *patcode++ = *add++ ^ 32; }`
361 idx += 1;
362 if idx < add_bytes.len() {
363 if patcode_pos < buf.len() {
364 buf[patcode_pos] = add_bytes[idx] ^ 32;
365 }
366 patcode_pos += 1;
367 idx += 1;
368 }
369 } else {
370 // c:439-440 — `else { *patcode++ = *add++; }`
371 if patcode_pos < buf.len() {
372 buf[patcode_pos] = b;
373 }
374 patcode_pos += 1;
375 idx += 1;
376 }
377 remaining -= 1;
378 }
379 } else {
380 // c:444-445 — `while (n--) *patcode++ = *add++;` — plain copy.
381 let n_actual = (n as usize).min(add_bytes.len());
382 for &b in &add_bytes[..n_actual] {
383 if patcode_pos < buf.len() {
384 buf[patcode_pos] = b;
385 }
386 patcode_pos += 1;
387 }
388 }
389 } else {
390 // c:447-448 — `else *patcode++ = ch;` — single-byte write.
391 if patcode_pos < buf.len() {
392 buf[patcode_pos] = ch;
393 }
394 patcode_pos += 1;
395 }
396 // c:449 — `patcode = patout + patsize;` — restore the post-write
397 // head so subsequent patadd calls append at the next slot. zshrs
398 // tracks via buf.len(); trim back to newpatsize so the alignment
399 // padding stays visible to consumers as zeroes.
400 let _ = patcode_pos;
401 // (No explicit truncate — buf.len() == newpatsize already.)
402}
403
404/// Port of `patcompcharsset()` from `Src/pattern.c:464`.
405///
406/// Initializes the `zpc_special` table for the active globbing
407/// regime. The C source resets every ZPC_* slot to its literal
408/// character, then masks off characters via `Marker` (0xa2 — the
409/// canonical "invalid" sentinel) for three option-driven cases:
410/// 1. `!isset(EXTENDEDGLOB)` → Tilde/Hat/Hash disabled.
411/// 2. `!isset(KSHGLOB)` → KSH_QUEST/STAR/PLUS/BANG/BANG2/AT disabled.
412/// 3. `isset(SHGLOB)` → Inpar/Inang disabled.
413///
414pub fn patcompcharsset() {
415 // c:464
416 let mut sp = zpc_special.lock().unwrap();
417 *sp = [0u8; ZPC_COUNT as usize];
418 // c:469 — `memcpy(zpc_special, zpc_chars, ZPC_COUNT)`. The default
419 // char for every ZPC_* slot. Direct positional assignment here
420 // since zshrs doesn't carry the `zpc_chars` const array yet.
421 //
422 // NOTE on token bytes: C's `zpc_chars[]` (pattern.c:248) uses
423 // the TOKENIZED high-bit bytes (`Bar`, `Tilde`, etc.); the C
424 // shell lexer tokenizes raw ASCII to these only inside grouped
425 // alternation. zshrs's parser doesn't yet run that pre-tokenize
426 // pass — every byte reaches patcompile as raw ASCII. Switching
427 // these slots to the high-bit form would break ~80 library
428 // tests that rely on raw `|` triggering alternation. The
429 // narrower fix for bug #12 lives in vm_helper::glob_match_static
430 // where we conservatively escape pattern bytes the param-strip
431 // path could not have intended as metacharacters.
432 sp[ZPC_SLASH as usize] = b'/';
433 sp[ZPC_NULL as usize] = 0;
434 sp[ZPC_BAR as usize] = b'|';
435 sp[ZPC_OUTPAR as usize] = b')';
436 sp[ZPC_TILDE as usize] = b'~';
437 sp[ZPC_INPAR as usize] = b'(';
438 sp[ZPC_QUEST as usize] = b'?';
439 sp[ZPC_STAR as usize] = b'*';
440 sp[ZPC_INBRACK as usize] = b'[';
441 sp[ZPC_INANG as usize] = b'<';
442 sp[ZPC_HAT as usize] = b'^';
443 sp[ZPC_HASH as usize] = b'#';
444 sp[ZPC_BNULLKEEP as usize] = 0;
445 // c:478-490 — KSH_GLOB slots (omitted from previous Rust port).
446 // Each defaults to the literal ksh-glob trigger char. The
447 // option-mask pass below disables them when KSHGLOB is off.
448 sp[ZPC_KSH_QUEST as usize] = b'?';
449 sp[ZPC_KSH_STAR as usize] = b'*';
450 sp[ZPC_KSH_PLUS as usize] = b'+';
451 sp[ZPC_KSH_BANG as usize] = b'!';
452 sp[ZPC_KSH_BANG2 as usize] = b'!';
453 sp[ZPC_KSH_AT as usize] = b'@';
454
455 let marker_byte = Marker as u32 as u8;
456
457 // c:471-478 — `for (...; i < ZPC_COUNT; ...) if (*disp) *spp = Marker;`
458 // Apply user disables from `disable -p` BEFORE the option-driven
459 // masks (so EXTENDEDGLOB / KSHGLOB / SHGLOB layer over the per-
460 // pattern disables).
461 {
462 let disp = zpc_disables.lock().unwrap();
463 for i in 0..(ZPC_COUNT as usize) {
464 if disp[i] != 0 {
465 sp[i] = marker_byte; // c:476
466 }
467 }
468 }
469
470 // c:480-483 — `if (!isset(EXTENDEDGLOB))` mask Tilde/Hat/Hash.
471 if !isset(EXTENDEDGLOB) {
472 sp[ZPC_TILDE as usize] = marker_byte;
473 sp[ZPC_HAT as usize] = marker_byte;
474 sp[ZPC_HASH as usize] = marker_byte;
475 }
476
477 // c:485-491 — `if (!isset(KSHGLOB))` mask the six KSH_* slots.
478 if !isset(KSHGLOB) {
479 sp[ZPC_KSH_QUEST as usize] = marker_byte;
480 sp[ZPC_KSH_STAR as usize] = marker_byte;
481 sp[ZPC_KSH_PLUS as usize] = marker_byte;
482 sp[ZPC_KSH_BANG as usize] = marker_byte;
483 sp[ZPC_KSH_BANG2 as usize] = marker_byte;
484 sp[ZPC_KSH_AT as usize] = marker_byte;
485 }
486
487 // c:499-505 — `if (isset(SHGLOB))` mask Inpar/Inang (case/numeric
488 // ranges not valid under sh-emulation).
489 if isset(SHGLOB) {
490 sp[ZPC_INPAR as usize] = marker_byte;
491 sp[ZPC_INANG as usize] = marker_byte;
492 }
493}
494
495/// Port of `patcompstart()` from `Src/pattern.c:517`.
496///
497/// Resets per-compile globals. Called at the start of `patcompile`.
498///
499/// **C body (c:517-526)** — strict order matters:
500/// 1. `patcompcharsset()` — must run FIRST so the zpc_special
501/// table reflects the current option state before parsing.
502/// 2. `patglobflags = isset(CASEGLOB) || isset(CASEPATHS) ? 0 :
503/// GF_IGNCASE;` — case-insensitivity is the default UNLESS
504/// one of the case-sensitive options is set.
505/// 3. `if (isset(MULTIBYTE)) patglobflags |= GF_MULTIBYTE;` —
506/// multibyte handling is option-gated, NOT unconditional.
507///
508/// The previous Rust port had three divergences: (a) called
509/// patcompcharsset LAST instead of FIRST, (b) unconditionally set
510/// GF_MULTIBYTE even when `setopt nomultibyte`, (c) NEVER set
511/// GF_IGNCASE — `setopt nocaseglob` had zero effect on pattern
512/// case-folding.
513pub fn patcompstart() {
514 // c:517
515 // c:519 — `patcompcharsset()` FIRST.
516 patcompcharsset();
517 patout.lock().unwrap().clear();
518 patnpar.store(1, Ordering::Relaxed);
519 patflags.store(0, Ordering::Relaxed);
520 // c:520-523 — CASE option dispatch.
521 let mut flags: i32 = if isset(CASEGLOB) || isset(CASEPATHS) {
522 0 // c:521 case-sensitive
523 } else {
524 GF_IGNCASE // c:523 default = ignore case
525 };
526 // c:524-525 — MULTIBYTE option respect.
527 if isset(MULTIBYTE) {
528 flags |= GF_MULTIBYTE; // c:525
529 }
530 patglobflags.store(flags, Ordering::Relaxed);
531 errsfound.store(0, Ordering::Relaxed);
532 forceerrs.store(-1, Ordering::Relaxed);
533 patparse_off.store(0, Ordering::Relaxed);
534}
535
536// =====================================================================
537// 9. Compiler entry points — pattern.c:540
538// =====================================================================
539
540/// Port of `patcompile(char *exp, int inflags, char **endexp)` from `Src/pattern.c:540`.
541///
542/// C signature: `Patprog patcompile(char *exp, int inflags, char **endexp)`.
543/// Compiles pattern `exp` under flags `inflags`, returns a `Patprog`
544/// on success or `NULL` on failure. `endexp` (if non-NULL) is set to
545/// the input cursor at end of parse — used by `bin_zregexparse` to
546/// detect partial-parse cases.
547pub fn patcompile(exp: &str, inflags: i32, mut endexp: Option<&mut String>) -> Option<Patprog> {
548 // Global compiled-pattern cache (crate::pat_cache, a zshrs-only opt —
549 // see that module for the safety/key/threading rationale). Skipped when
550 // `endexp` is requested: that out-param is a compile side effect the
551 // cache can't reproduce. The check runs BEFORE the compile mutex so a
552 // hit never serialises on the compiler.
553 let cacheable = endexp.is_none();
554 if cacheable {
555 if let Some(hit) = crate::pat_cache::get(exp, inflags) {
556 return Some(hit);
557 }
558 }
559 // Capture the ORIGINAL pattern text for the cache-store key: `exp` is
560 // shadowed below by the decoded form, and the option state that feeds
561 // the fingerprint is unchanged between here and the store (patcompstart
562 // only reads options), so get/put keys match.
563 let exp_orig: String = if cacheable { exp.to_string() } else { String::new() };
564 // Hold the compile mutex for the entire body — `patcompstart`
565 // resets every file-scope static (`Src/pattern.c:267-281`) and the
566 // emit/parse helpers mutate them in sequence. C is single-threaded
567 // so the statics are race-free there; Rust must serialise.
568 let _compile_guard = PATCOMPILE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
569 // c:1610 `patstartch` — the leading plain character of the pattern.
570 // C only needs the NOGLD leading-dot rule to know whether a pattern
571 // explicitly begins with `.` (so `.*` matches dot files while `*`
572 // does not). Capture it before `exp` is shadowed by the normalizer
573 // below. A leading glob token (Star/Quest/Inbrack/…) is not plain, so
574 // only a literal `.` is recorded; everything else stays 0.
575 let patstartch_lead: u8 = if exp.starts_with('.') { b'.' } else { 0 };
576 patcompstart();
577 // c:525 — `patcompstart` seeds `patglobflags` with the option-derived
578 // default (GF_IGNCASE when CASEGLOB/CASEPATHS are off, GF_MULTIBYTE when
579 // MULTIBYTE is on). The `patglobflags.store(0)` reset below (clean slate
580 // for the `(#…)` hoist loop) would otherwise drop that seed before the
581 // prog's globflags are built — capture the case bits now so `pattry`
582 // honors `setopt nocaseglob`.
583 let mut seeded_globflags = patglobflags.load(Ordering::Relaxed);
584 // c:568-576 — `patcompile` RESETS patglobflags to `GF_MULTIBYTE`/0 for a
585 // NON-FILE pattern, DROPPING the nocaseglob-derived GF_IGNCASE that
586 // patcompstart seeded. `setopt nocaseglob` makes only FILENAME GLOBBING
587 // (PAT_FILE) case-insensitive; `[[ ]]`, `${arr:#pat}`, `case`, and `${p//pat}`
588 // stay case-SENSITIVE. Without this drop, `setopt nocaseglob; [[ ABC == abc ]]`
589 // wrongly matched and `${(ABC):#abc}` wrongly filtered. GF_MULTIBYTE stays.
590 if (inflags & PAT_FILE as i32) == 0 {
591 seeded_globflags &= !GF_IGNCASE;
592 }
593 // === C-contract input decode (zpc_chars, Src/pattern.c:248) =====
594 // C's patcompile consumes the LEXER'S tokenized encoding: glob
595 // metachars arrive as token bytes (`Pound`..`Bang`, zsh.h:159-183
596 // — zpc_chars holds `Star`/`Quest`/`Inbrack`/... so a raw `*`
597 // NEVER dispatches as ZPC_STAR), quoted/escaped chars arrive as
598 // `Bnull`/`Bnullkeep` + payload (zsh.h:195-200), and `Nularg` is
599 // stripped (remnulargs). Callers pair `tokenize()` with
600 // `patcompile()` exactly like C (zutil.c:734, etc.).
601 //
602 // The Rust parser below uses a raw-ASCII internal encoding with
603 // `\X` as its literal form; transpose the C contract onto it:
604 // token char (U+0084..=U+009E) -> ztokens[c - Pound] (meta)
605 // Bnull/Bnullkeep + X (U+009F/U+00A0) -> \X (literal)
606 // Nularg (U+00A1) -> dropped
607 // Meta (U+0083) + X -> \X (literal)
608 // raw `\` + X -> \X passthrough — raw
609 // backslash is the parser's established Bnull-equivalent
610 // (see the `\X` arm below); C's lexer encodes the same
611 // quoting info as Bnull+X before zshtokenize ever runs.
612 // raw ASCII glob metachar -> \X (literal)
613 // `/`, `+`, `!`, `@` stay verbatim — C's zpc_chars keeps those
614 // slots RAW (path split + ksh-glob triggers fire on raw bytes).
615 let exp: String = {
616 let ztokens: Vec<char> = crate::ported::glob::ZTOKENS.chars().collect();
617 let chars: Vec<char> = exp.chars().collect();
618 let mut out = String::with_capacity(exp.len());
619 let mut i = 0;
620 while i < chars.len() {
621 let c = chars[i];
622 let cu = c as u32;
623 if cu == 0x83 {
624 // Meta + payload — a metafied RAW byte
625 // (vm_helper::meta_encode_byte, c:Src/utils.c:7289-
626 // 7294). C's pattern matcher compares metafied bytes
627 // on BOTH sides, so the compiled pattern must match
628 // the pair AS STORED in the subject string: emit both
629 // chars as literals (`\Meta \payload`). The previous
630 // `\payload` form dropped the Meta char and never
631 // matched a metafied subject — `[[ $'\xff' ==
632 // $'\xff' ]]` failed. Bug #127.
633 out.push('\\');
634 out.push(c);
635 if i + 1 < chars.len() {
636 out.push('\\');
637 out.push(chars[i + 1]);
638 i += 2;
639 } else {
640 i += 1;
641 }
642 continue;
643 }
644 if cu == 0x9f || cu == 0xa0 {
645 // Bnull / Bnullkeep — payload is a literal.
646 if i + 1 < chars.len() {
647 out.push('\\');
648 out.push(chars[i + 1]);
649 i += 2;
650 } else {
651 i += 1;
652 }
653 continue;
654 }
655 if cu == 0xa1 {
656 // Nularg — stripped like C's remnulargs.
657 i += 1;
658 continue;
659 }
660 if (0x84..=0x9e).contains(&cu) {
661 // Token -> the raw metachar the parser dispatches on.
662 out.push(ztokens[(cu - 0x84) as usize]);
663 i += 1;
664 continue;
665 }
666 if c == '\\' {
667 // Raw `\X` — Bnull-equivalent quoting marker in the
668 // zshrs pipeline; pass both through to the parser's
669 // `\X` literal arm. Trailing lone `\` stays itself.
670 out.push('\\');
671 if i + 1 < chars.len() {
672 out.push(chars[i + 1]);
673 i += 2;
674 } else {
675 i += 1;
676 }
677 continue;
678 }
679 if matches!(c, '*' | '?' | '[' | '(' | ')' | '|' | '~' | '^' | '#' | '<') {
680 // Untokenized ASCII metachar — literal per zpc_chars.
681 out.push('\\');
682 out.push(c);
683 i += 1;
684 continue;
685 }
686 out.push(c);
687 i += 1;
688 }
689 out
690 };
691 let exp = exp.as_str();
692 *patstart.lock().unwrap() = exp.to_string();
693 *patparse.lock().unwrap() = exp.to_string();
694 patflags.store(
695 inflags & !(PAT_PURES | PAT_HAS_EXCLUDP) as i32,
696 Ordering::Relaxed,
697 ); // c:566
698 patglobflags.store(0, Ordering::Relaxed);
699
700 // c:583-590 — emit P_GFLAGS placeholder. Phase 5.1: instead of
701 // emitting an opcode, hoist leading `(#...)` flag specifiers into
702 // patprog.globflags so the matcher applies them globally for the
703 // whole match. Full mid-pattern P_GFLAGS opcode still deferred.
704 // c:525 — `patglobflags = isset(MULTIBYTE) ? GF_MULTIBYTE : 0`.
705 // (#U) inside the spec clears the bit per c:1116.
706 //
707 // c:953-957 — C gates the `(#...)` recognition on
708 // `*patparse == zpc_special[ZPC_INPAR] &&`
709 // `patparse[1] == zpc_special[ZPC_HASH]`.
710 // When EXTENDEDGLOB is off, patcompcharsset (c:480-483) masks
711 // ZPC_HASH to the Marker byte (c:476). The second-byte equality
712 // therefore fails and the parser falls through to "treat `(` as
713 // a literal group" (c:1020+). Previously this Rust hoist loop
714 // compared the raw byte `b'#'`, allowing `(#s)` / `(#e)` /
715 // `(#i)` to fire even without EXTENDEDGLOB (parity bugs #18/#19
716 // vs real zsh).
717 // c:525 — the compiled prog's globflags accumulate from `patglobflags`,
718 // which `patcompstart` seeded with GF_IGNCASE when CASEGLOB/CASEPATHS are
719 // off (`setopt nocaseglob`). Carry those case bits through so `pattry`
720 // matches case-insensitively; `matchpat` masks the loss by pre-folding
721 // both sides, but the glob scanner now drives `pattry` directly. Keep
722 // the prior unconditional GF_MULTIBYTE to avoid disturbing multibyte
723 // callers that relied on it.
724 let mut hoisted_globflags: i32 =
725 GF_MULTIBYTE | (seeded_globflags & (GF_IGNCASE | GF_LCMATCHUC));
726 let hash_char_pre = zpc_special.lock().unwrap()[ZPC_HASH as usize]; // c:957
727 while hash_char_pre == b'#' {
728 let off = patparse_off.load(Ordering::Relaxed);
729 let p = patparse.lock().unwrap();
730 if off + 1 >= p.len() || &p.as_bytes()[off..off + 2] != b"(#" {
731 break;
732 }
733 let rest = p[off..].to_string();
734 drop(p);
735 match patgetglobflags(&rest) {
736 Some((_bits, assert, _consumed)) if assert != 0 => {
737 // c:Src/pattern.c:1103-1109 — `(#s)`/`(#e)` set `*assertp`
738 // (P_ISSTART/P_ISEND), a POSITIONAL zero-width anchor, not a
739 // whole-match glob flag. Do NOT hoist/consume it here: leave
740 // it (and everything after) for the main compile loop
741 // (pattern.rs:1245) to emit as a positional node. Hoisting
742 // dropped the anchor, so a leading `(#e)PAT` matched as if
743 // the `(#e)` were absent — `[[ o == (#e)o ]]` wrongly matched
744 // and `${s//(#e)r/END}` wrongly replaced. `(#s)` was masked
745 // in `[[ ]]` only because that context is start-anchored.
746 break;
747 }
748 Some((bits, _assert, consumed)) => {
749 // The fold / backref / matchref / multibyte flags are ABSOLUTE
750 // state, not a set-only delta. patgetglobflags's returned `bits`
751 // starts at 0 and can only carry flags turned ON, so `(#I)`
752 // clearing LCMATCHUC (c: `patglobflags &= ~(GF_LCMATCHUC |
753 // GF_IGNCASE)`) comes back as 0 — an `|=` accumulate then leaves
754 // an earlier `(#l)`/`(#i)`'s fold bit set, so `(#l)(#I)foo` kept
755 // folding where zsh, after `(#I)`, matches case-sensitively.
756 //
757 // patgetglobflags ALSO updates the global patglobflags with the
758 // sets AND the clears, so it holds the authoritative running
759 // value. REPLACE the toggle-able bits from it rather than
760 // OR-accumulating the lossy return.
761 // GF_MULTIBYTE is deliberately NOT in the mask: the init above
762 // force-sets it (this port always matches UTF-8) while the
763 // global patglobflags may not carry it, so replacing it from the
764 // global would wrongly clear it and break multibyte matching.
765 // The original `|=` could not clear it either, so leaving it out
766 // preserves that (rare `(#U)` single-byte requests are no more
767 // broken than before). The fold/backref/matchref bits, which DO
768 // need clear semantics for `(#I)`/`(#B)`/`(#M)`, come from the
769 // authoritative global.
770 let gmask = GF_IGNCASE | GF_LCMATCHUC | GF_BACKREF | GF_MATCHREF;
771 let cur = patglobflags.load(Ordering::Relaxed);
772 hoisted_globflags = (hoisted_globflags & !gmask) | (cur & gmask);
773 // `(#aN)` substitution budget — low 8 bits of
774 // `patglobflags` per c:1066. Only carry it when this
775 // flag-spec actually had `(#a)` digits (non-zero err
776 // count); other specs may set high bits and we don't
777 // want their bits-AND to clear our budget byte.
778 // Per-spec OR-leak is C-faithful (a later `(#a3)` after
779 // `(#a1)` raises the budget); the matcher reads the
780 // final cumulative byte.
781 let errs_byte = bits & 0xff;
782 if errs_byte != 0 {
783 hoisted_globflags = (hoisted_globflags & !0xff) | errs_byte;
784 // c:1066
785 }
786 if (bits & GF_MULTIBYTE) == 0 && rest.contains('U') {
787 hoisted_globflags &= !GF_MULTIBYTE;
788 }
789 patparse_off.fetch_add(consumed, Ordering::Relaxed);
790 }
791 None => break,
792 }
793 }
794
795 // c:584-610 — PAT_PURES fast path. A literal with no glob tokens
796 // compiles to a "pure string" rather than bytecode. For FILE globs
797 // (PAT_FILE) the scan STOPS at '/', so each path component becomes a
798 // pure section and `parsecomplist` can split the path on '/' (it
799 // reads endexp = the '/' remainder). Gated on PAT_FILE so non-file
800 // callers (matchpat/[[ ]]/:#) keep normal compilation unchanged; the
801 // matcher consumes PURES sections via literal extraction (the
802 // scanner), never `pattry`, so no PAT_PURES match path is needed.
803 let pf_pre = patflags.load(Ordering::Relaxed);
804 // c:Src/pattern.c:644-704 — for a FILE glob a literal path component is
805 // emitted as a PAT_PURES section even under GF_IGNCASE (`setopt
806 // nocaseglob`): C compiles it, then converts the single P_EXACTLY back
807 // to PURES (c:664) while flagging the prog GF_IGNCASE (c:645). The net
808 // effect is that intermediate literal directory components descend by
809 // EXACT name (the scanner's PURES path stats the real path, glob.rs:436
810 // — case-insensitivity applies to the FINAL wildcard component, not to
811 // directory descent), so path splitting on `/` keeps working. zshrs's
812 // port only had the first PURES gate (c:586), which excludes GF_IGNCASE,
813 // so under nocaseglob every literal component (e.g. `tmp` in
814 // `/tmp/.../*.zsh`) fell to the general compiler and matched nothing —
815 // the whole path collapsed and `${(N)...}(DN)` globs returned empty.
816 // That is what made zinit's plugin-discovery globs empty on a shell with
817 // `caseglob` off → the `.zinit-load-plugin:source:110: no such file or
818 // directory: ./` startup flood. Take the PURES fast path here for file
819 // globs when the only extra glob flag is GF_IGNCASE; GF_IGNCASE stays on
820 // `hoisted_globflags` so the final wildcard component still matches
821 // case-insensitively.
822 let pures_extra_mask = if (pf_pre & PAT_FILE as i32) != 0 {
823 !(GF_MULTIBYTE | GF_IGNCASE)
824 } else {
825 !GF_MULTIBYTE
826 };
827 if (pf_pre & PAT_FILE as i32) != 0
828 && (pf_pre & PAT_ANY as i32) == 0
829 && (hoisted_globflags & pures_extra_mask) == 0
830 {
831 let off = patparse_off.load(Ordering::Relaxed);
832 let p = patparse.lock().unwrap();
833 let s = &p[off..];
834 // c:606 — scan for a pure literal segment. By this point patparse
835 // has been NORMALIZED to raw metachars (Star -> '*') with literal
836 // metachars backslash-escaped (`\*`), so scan that form: stop at
837 // '/' (segment boundary) or any UNescaped glob metachar (=> not
838 // pure). `\X` is an escaped literal — skip both chars.
839 let mut cut = s.len();
840 let mut at_token = false;
841 let mut it = s.char_indices();
842 while let Some((i, c)) = it.next() {
843 if c == '/' {
844 cut = i;
845 break;
846 }
847 if c == '\\' {
848 it.next(); // escaped literal — both chars stay literal
849 continue;
850 }
851 if matches!(c, '*' | '?' | '[' | '(' | '|' | '~' | '^' | '#' | '<') {
852 cut = i;
853 at_token = true;
854 break;
855 }
856 }
857 // c:610 — pure iff we stopped at end or '/', not at a glob meta.
858 if !at_token {
859 let literal = s[..cut].as_bytes().to_vec();
860 let remainder = s[cut..].to_string();
861 drop(p);
862 let mlen = literal.len() as i64;
863 if let Some(end) = endexp.as_deref_mut() {
864 // c:751 *endexp = patparse (at '/'). patparse is normalized
865 // (untokenized); re-tokenize so parsecomplist's recursion
866 // feeds the NEXT patcompile a tokenized segment (else raw
867 // '*' gets re-escaped to `\*`, losing the Star meaning).
868 let mut rem = remainder;
869 crate::ported::glob::tokenize(&mut rem);
870 *end = rem;
871 }
872 let prog: Patprog = Box::new((
873 patprog {
874 startoff: 0,
875 size: mlen,
876 mustoff: 0,
877 patmlen: mlen, // c:623 — pure-string length
878 globflags: hoisted_globflags,
879 globend: patglobflags.load(Ordering::Relaxed),
880 flags: pf_pre | PAT_PURES as i32, // c:625
881 patnpar: 0,
882 patstartch: patstartch_lead, // c:1610
883 },
884 literal,
885 ));
886 if cacheable {
887 crate::pat_cache::put(&exp_orig, inflags, &prog);
888 }
889 return Some(prog);
890 }
891 }
892
893 let mut flagp: i32 = 0;
894 let root = patcompswitch(0, &mut flagp);
895 if root < 0 {
896 return None; // c:646 compile error
897 }
898 // Emit the terminal P_END and chain every branch's operand to it.
899 let end_off = patnode(P_END);
900 chain_branches_to(root as usize, end_off);
901
902 let code = patout.lock().unwrap().clone();
903 let consumed_off = patparse_off.load(Ordering::Relaxed);
904 if let Some(end) = endexp.as_deref_mut() {
905 let parse = patparse.lock().unwrap();
906 // c:751 *endexp = patparse. patparse is normalized (untokenized);
907 // re-tokenize so parsecomplist's recursion feeds the next
908 // patcompile a tokenized segment (raw '*' would re-escape to `\*`).
909 let mut rem = parse[consumed_off..].to_string();
910 drop(parse);
911 crate::ported::glob::tokenize(&mut rem);
912 *end = rem;
913 }
914
915 let prog: Patprog = Box::new((
916 patprog {
917 startoff: 0,
918 size: code.len() as i64,
919 mustoff: 0,
920 patmlen: 0,
921 globflags: hoisted_globflags,
922 globend: patglobflags.load(Ordering::Relaxed),
923 // c:637 `p->flags = patflags;` — patflags ONLY. A prior
924 // Rust port OR'd `hoisted_globflags` into here, contaminating
925 // `prog.flags & 0xff` checks (which read PAT_STATIC / PAT_PURES
926 // bits in the low byte) with whatever ended up in the
927 // `(#aN)` budget byte. The two flag-sets stay strictly
928 // separated per C source.
929 flags: patflags.load(Ordering::Relaxed),
930
931 patnpar: patnpar.load(Ordering::Relaxed) - 1,
932 patstartch: patstartch_lead, // c:1610
933 },
934 code,
935 ));
936 if cacheable {
937 crate::pat_cache::put(&exp_orig, inflags, &prog);
938 }
939 Some(prog)
940}
941
942/// Port of `patcompswitch(int paren, int *flagp)` from `Src/pattern.c:765`.
943///
944/// C: `static long patcompswitch(int paren, int *flagp)`. Parses an
945/// alternation (`a|b|c`), emitting a chain of P_BRANCH nodes. Returns
946/// offset of the first branch, or -1 on error.
947pub fn patcompswitch(paren: i32, flagp: &mut i32) -> i64 {
948 // c:765
949 // Emit the first P_BRANCH header. Its operand is the content
950 // emitted by the immediately-following patcompbranch call (lives
951 // inline at starter+I_BODY). Does NOT emit a terminator — caller
952 // (patcompile for top-level, patcomppiece for sub-pattern)
953 // chains branches to the appropriate follow-on opcode.
954 let starter = patnode(P_BRANCH);
955 let mut branch_flags: i32 = 0;
956 let first_branch = patcompbranch(&mut branch_flags, paren);
957 if first_branch < 0 {
958 return -1;
959 }
960 *flagp |= branch_flags & P_HSTART;
961
962 let mut last_branch = starter;
963 // c:769 — `Upat excsync = NULL;` — set on first `~` exclusion;
964 // reused so consecutive `~clause` chain to the same sync node.
965 let mut excsync: usize = 0;
966
967 // Snapshot zpc_special for this compile pass — locked once to
968 // avoid re-locking inside the per-iteration parse-byte check.
969 let (sp_bar, sp_tilde, sp_special_set) = {
970 let sp = zpc_special.lock().unwrap();
971 let bar = sp[ZPC_BAR as usize];
972 let tilde = sp[ZPC_TILDE as usize];
973 // c:803 — `memchr(zpc_special, patparse[1], ZPC_SEG_COUNT)` —
974 // is the lookahead byte one of the segment-special bytes? If
975 // so, `~X` is NOT an exclusion (`~|`, `~)`, etc.).
976 let mut set = [false; 256];
977 for i in 0..(ZPC_SEG_COUNT as usize) {
978 set[sp[i] as usize] = true;
979 }
980 (bar, tilde, set)
981 };
982
983 // Alternation + exclusion loop:
984 // `|` → next alternative (P_BRANCH)
985 // `~` → exclusion (P_EXCLUDE / P_EXCLUDP) — when followed by
986 // `/` (top-level path component split) OR a non-special
987 // char (ordinary content). `~~`, `~|`, `~)` stay literal.
988 loop {
989 let off = patparse_off.load(Ordering::Relaxed);
990 let parse = patparse.lock().unwrap();
991 if off >= parse.len() {
992 break;
993 }
994 let bytes = parse.as_bytes();
995 let c = bytes[off];
996 // c:799-803 — accept `|` always; accept `~` only when the
997 // lookahead char is `/` or NOT a segment-special byte.
998 let is_bar = c == sp_bar;
999 let is_tilde_exclude = c == sp_tilde && off + 1 < bytes.len() && {
1000 let la = bytes[off + 1];
1001 la == b'/' || !sp_special_set[la as usize]
1002 };
1003 if !is_bar && !is_tilde_exclude {
1004 break;
1005 }
1006 drop(parse);
1007 patparse_off.fetch_add(1, Ordering::Relaxed); // c:803 `*patparse++`
1008 let br: usize;
1009 if is_tilde_exclude {
1010 // c:808-836 — `if (tilde)` arm. Emit the EXCSYNC sync node
1011 // (if first `~` in this switch) then the EXCLUDE / EXCLUDP
1012 // node with an 8-byte NULL syncptr payload. Note we DON'T
1013 // reset patglobflags's low byte (`(#aN)` budget) here —
1014 // the Rust port doesn't yet propagate per-pattern `(#a)`
1015 // through nested patmatch frames, so dropping the budget
1016 // mid-compile is a no-op.
1017 if excsync == 0 {
1018 excsync = patnode(P_EXCSYNC); // c:813
1019 patoptail(last_branch, excsync); // c:814
1020 }
1021 // c:816-825 — `if (!(patflags & PAT_FILET) || paren)` →
1022 // P_EXCLUDE; else P_EXCLUDP for top-level file globs.
1023 let pf = patflags.load(Ordering::Relaxed);
1024 let use_excludp = (pf & (PAT_FILET as i32)) != 0 && paren == 0;
1025 if use_excludp {
1026 br = patnode(P_EXCLUDP); // c:823
1027 patflags.fetch_or(PAT_HAS_EXCLUDP as i32, Ordering::Relaxed); // c:824
1028 } else {
1029 br = patnode(P_EXCLUDE); // c:818
1030 }
1031 // c:826-827 — `up.p = NULL; patadd((char *)&up, 0, sizeof(up), 0);`
1032 // 8-byte syncptr slot, NULL-initialised. Sized for a
1033 // 64-bit pointer to match C `union upat`.
1034 {
1035 let mut buf = patout.lock().unwrap();
1036 buf.extend_from_slice(&[0u8; 8]);
1037 }
1038 } else {
1039 // c:843 — `excsync = 0; br = patnode(P_BRANCH);`
1040 excsync = 0;
1041 br = patnode(P_BRANCH);
1042 }
1043 // Chain previous branch's `next` directly to this new branch
1044 // (alternative chain, not operand chain).
1045 set_next(last_branch, br);
1046 let mut bf: i32 = 0;
1047 let inner = patcompbranch(&mut bf, paren);
1048 if inner < 0 {
1049 return -1;
1050 }
1051 // c:Src/pattern.c:891-892 — `if (excsync) patoptail(br,
1052 // patnode(P_EXCEND));`. Terminate an exclusion branch's operand
1053 // with P_EXCEND so the matcher knows where the excluded pattern
1054 // ends; without it `A~B` ran B past its own end into the
1055 // following pattern and never excluded (`STATES__*~*local*`
1056 // failed to drop STATES__local_bar).
1057 if excsync != 0 {
1058 let excend = patnode(P_EXCEND);
1059 patoptail(br, excend);
1060 }
1061 *flagp |= bf & P_HSTART;
1062 last_branch = br;
1063 }
1064
1065 let _ = first_branch;
1066 starter as i64
1067}
1068
1069/// Port of `patcompbranch(int *flagp, int paren)` from `Src/pattern.c:942`.
1070///
1071/// C: `static long patcompbranch(int *flagp, int paren)`. Parses a
1072/// single branch — a sequence of pieces. Returns offset of the first
1073/// node in the branch, or -1 on error.
1074pub fn patcompbranch(flagp: &mut i32, paren: i32) -> i64 {
1075 // c:942
1076 let mut chain_start: i64 = -1;
1077 let mut last_tail: usize = 0;
1078 // Track preceding piece for POSTFIX `(#cN,M)` wrap. C does
1079 // `patinsert(P_COUNTSTART, starter, ...)` to embed COUNTSTART
1080 // BEFORE the just-compiled piece (c:pattern.c:1686). We snapshot
1081 // the piece bytes, truncate, emit P_COUNT, then re-append.
1082 let mut last_piece_off: i64 = -1; // start offset of preceding piece
1083 let mut prev_chain_tail: i64 = -1; // tail of chain BEFORE preceding piece (-1 if piece was first)
1084 // Flags the preceding patcomppiece reported. P_HSTART marks a piece that
1085 // already had a closure applied (`a#`, `a##`, or an earlier `(#cN,M)`) —
1086 // patcomppiece sets `*flagp = P_HSTART` on every such arm, mirroring C
1087 // (c:1626/1629/1636/1640/1643). Used below to reject a stacked count.
1088 let mut last_piece_flags: i32 = 0;
1089 *flagp = P_PURESTR;
1090
1091 // c:951-952 — snapshot the segment-special set so we can do
1092 // `memchr(zpc_special, byte, ZPC_SEG_COUNT)`-equivalent lookups.
1093 // Only the first ZPC_SEG_COUNT slots (SLASH, NULL, BAR, OUTPAR,
1094 // TILDE) matter here.
1095 let (sp_tilde, sp_seg_set, sp_inpar, sp_hash) = {
1096 let sp = zpc_special.lock().unwrap();
1097 let tilde = sp[ZPC_TILDE as usize];
1098 let mut set = [false; 256];
1099 for i in 0..(ZPC_SEG_COUNT as usize) {
1100 set[sp[i] as usize] = true;
1101 }
1102 (
1103 tilde,
1104 set,
1105 sp[ZPC_INPAR as usize],
1106 sp[ZPC_HASH as usize], // c:1609
1107 )
1108 };
1109
1110 loop {
1111 let off = patparse_off.load(Ordering::Relaxed);
1112 // Snapshot the parse buffer into an owned slice for branch
1113 // decisions; release the lock so subsequent emit helpers
1114 // (which acquire patout's lock) can't contend.
1115 let snapshot: Vec<u8> = {
1116 let parse = patparse.lock().unwrap();
1117 parse.as_bytes().to_vec()
1118 };
1119 if off >= snapshot.len() {
1120 break;
1121 }
1122 let c = snapshot[off];
1123 // Branch terminators: |, ), end of pattern.
1124 if c == b'|' || c == b')' {
1125 break;
1126 }
1127 // c:950 — '/' is ZPC_SLASH (slot 0, within ZPC_SEG_COUNT), so C
1128 // breaks the segment here. zshrs gates this to FILE globs at top
1129 // level: parsecomplist needs the path-component boundary, while
1130 // matchpat/[[ ]]/:# (never PAT_FILE) and '/' inside parens (the
1131 // `((#s)|/)` anchor pin) must keep '/' literal. C's unconditional
1132 // break + c:914-917 file-accept termination is equivalent for the
1133 // top-level file-glob case this serves.
1134 if c == b'/' && paren == 0 && (patflags.load(Ordering::Relaxed) & PAT_FILE as i32) != 0 {
1135 break;
1136 }
1137 // c:950-952 — `~` is an additional terminator when active as
1138 // the exclusion operator. The C condition includes all five
1139 // segment-specials (SLASH, NULL, BAR, OUTPAR, TILDE), but
1140 // `|` and `)` are already covered above and SLASH inside
1141 // `(...)` alternation is intentionally left literal here to
1142 // preserve the existing `zsh_corpus_hash_s_e_anchors_match_
1143 // bare_test` parity pin.
1144 if c == sp_tilde && c != 0 {
1145 let la = snapshot.get(off + 1).copied().unwrap_or(0);
1146 // Literal-tilde exception: `~` followed by a segment-
1147 // special OTHER than `/` keeps the `~` as literal.
1148 let literal_tilde_exception = la != b'/' && sp_seg_set[la as usize];
1149 if !literal_tilde_exception {
1150 break;
1151 }
1152 }
1153 let bytes = snapshot.as_slice();
1154 // Mid-pattern `(#cN,M)` counted-repetition specifier — emit
1155 // P_COUNT with bounds + inline operand following. Detected
1156 // BEFORE the generic patgetglobflags path because `c` is not
1157 // a flag char in that fn.
1158 //
1159 // c:1608-1610 — the `(` and `#` are compared against
1160 // `zpc_special[ZPC_INPAR]` / `zpc_special[ZPC_HASH]`, NOT against
1161 // literal bytes. That indirection is the whole EXTENDED_GLOB gate:
1162 // patcompcharsset() rewrites `zpc_special[ZPC_HASH]` to Marker
1163 // (c:482) when EXTENDED_GLOB is off, so a literal `#` can never
1164 // equal it and `(#c...)` stops being a counted closure — it falls
1165 // through and parses as an ordinary group. Testing `b'#'` directly
1166 // applied the closure with EXTENDED_GLOB unset, so
1167 // `[[ aaa = a(#c2,3) ]]` matched (zsh: no match).
1168 if off + 2 < bytes.len()
1169 && bytes[off] == sp_inpar
1170 && bytes[off + 1] == sp_hash
1171 && bytes[off + 2] == b'c'
1172 {
1173 let mut j = off + 3;
1174 let mut min: i64 = 0;
1175 let min_start = j;
1176 while j < bytes.len() && bytes[j].is_ascii_digit() {
1177 min = min * 10 + (bytes[j] - b'0') as i64;
1178 j += 1;
1179 }
1180 let mut max: i64 = i64::MAX;
1181 // Three valid shapes (per zsh extended-glob (#cN,M)):
1182 // (#cN) — exact count N (min=N, max=N)
1183 // (#cN,) — min N, no max (min=N, max=∞)
1184 // (#cN,M) — N..M range (min=N, max=M)
1185 // (#c,M) — max M, no min (min=0, max=M) ← bug #489 missing case
1186 // (#c,) — equivalent to no count (min=0, max=∞)
1187 // The original `if j > min_start` gate skipped the `)`
1188 // check for the no-min shapes, leaving them unparsed.
1189 let has_min_digits = j > min_start;
1190 let has_comma = j < bytes.len() && bytes[j] == b',';
1191 if has_min_digits || has_comma {
1192 if has_comma {
1193 j += 1; // skip ,
1194 let max_start = j;
1195 let mut mx: i64 = 0;
1196 while j < bytes.len() && bytes[j].is_ascii_digit() {
1197 mx = mx * 10 + (bytes[j] - b'0') as i64;
1198 j += 1;
1199 }
1200 if j > max_start {
1201 max = mx;
1202 }
1203 } else {
1204 // (#cN) — exact count N.
1205 max = min;
1206 }
1207 if j < bytes.len() && bytes[j] == b')' {
1208 j += 1;
1209 patparse_off.store(j, Ordering::Relaxed);
1210 // POSTFIX semantics — c:pattern.c:1686.
1211 // C does `patinsert(P_COUNTSTART, starter, ...)`
1212 // then `pattail(opnd, patnode(P_BACK))` to loop the
1213 // operand back. zshrs uses a simpler encoding where
1214 // P_COUNT carries [min, max] then operand bytes
1215 // inline; matcher iterates outside. So we relocate
1216 // the preceding piece into the operand slot.
1217 // c:1431-1436 — `case Star:` sets `kshchar = -1`, whose
1218 // only purpose is (per C's own comment) "a sign that we
1219 // can't have #'s". c:1620-1622 then rejects the pattern:
1220 //
1221 // /* too much at once doesn't currently work */
1222 // if (kshchar && (hash || count))
1223 // return 0;
1224 //
1225 // So a count may not follow `*`, nor stack on a piece
1226 // that already carries a closure (`a#(#c2,3)`). zshrs
1227 // attaches the count to the preceding piece rather than
1228 // tracking kshchar, so the same rule is expressed by
1229 // looking at that piece's opcode. Without this, zshrs
1230 // silently accepted `*(#c2,3)` / `a#(#c2,3)`, which zsh
1231 // rejects as a bad pattern.
1232 if last_piece_off >= 0 {
1233 let prev_op = {
1234 let buf = patout.lock().unwrap();
1235 buf.get(last_piece_off as usize + I_OP)
1236 .copied()
1237 .unwrap_or(0)
1238 };
1239 // `*` is C's kshchar = -1 (its head node is P_STAR);
1240 // P_HSTART marks a piece that already closured, which
1241 // is what makes `a#(#c2,3)` / `a##(#c2,3)` bad too.
1242 if prev_op == P_STAR || (last_piece_flags & P_HSTART) != 0 {
1243 return -1; // c:1622 `return 0;`
1244 }
1245 }
1246 if last_piece_off >= 0 {
1247 let piece_start = last_piece_off as usize;
1248 // Snapshot preceding piece (everything emitted
1249 // by the prior patcomppiece call).
1250 let piece_bytes: Vec<u8> = {
1251 let buf = patout.lock().unwrap();
1252 buf[piece_start..].to_vec()
1253 };
1254 // Truncate to remove the piece.
1255 {
1256 let mut buf = patout.lock().unwrap();
1257 buf.truncate(piece_start);
1258 }
1259 // Cut chain at prev_chain_tail (was pointing
1260 // into the piece via set_next or as
1261 // chain_start).
1262 if prev_chain_tail >= 0 {
1263 set_next(prev_chain_tail as usize, 0);
1264 } else {
1265 chain_start = -1;
1266 }
1267 // Emit P_COUNT at current end.
1268 let count_off = patnode(P_COUNT);
1269 let operand_new_start: usize;
1270 {
1271 let mut buf = patout.lock().unwrap();
1272 buf.extend_from_slice(&min.to_le_bytes());
1273 buf.extend_from_slice(&max.to_le_bytes());
1274 operand_new_start = buf.len();
1275 }
1276 // Relocate piece bytes: rewrite internal next
1277 // links (absolute offsets >= piece_start) by
1278 // delta = operand_new_start - piece_start.
1279 let delta: i64 = operand_new_start as i64 - piece_start as i64;
1280 let mut relocated = piece_bytes.clone();
1281 let mut i = 0;
1282 while i + I_BODY <= relocated.len() {
1283 let op = relocated[i + I_OP];
1284 if op == 0 {
1285 i += 1;
1286 continue;
1287 }
1288 let nxt = u32::from_le_bytes(
1289 relocated[i + I_NEXT..i + I_NEXT + 4].try_into().unwrap(),
1290 );
1291 if nxt != 0 {
1292 let new_nxt = (nxt as i64 + delta) as u32;
1293 relocated[i + I_NEXT..i + I_NEXT + 4]
1294 .copy_from_slice(&new_nxt.to_le_bytes());
1295 }
1296 let next_i = advance_past_instr(&relocated, i);
1297 if next_i == 0 || next_i <= i {
1298 break;
1299 }
1300 i = next_i;
1301 }
1302 {
1303 let mut buf = patout.lock().unwrap();
1304 buf.extend_from_slice(&relocated);
1305 }
1306 // Hook P_COUNT into chain.
1307 if chain_start < 0 {
1308 chain_start = count_off as i64;
1309 } else {
1310 set_next(prev_chain_tail as usize, count_off);
1311 }
1312 last_tail = count_off;
1313 // Consumed — clear tracking. The resulting piece IS a
1314 // closure (C: `*flagp = P_HSTART`, c:1636), so a second
1315 // count stacked on it must be rejected.
1316 last_piece_off = -1;
1317 prev_chain_tail = -1;
1318 last_piece_flags = P_HSTART;
1319 continue;
1320 }
1321 // No preceding piece — `(#cN,M)` is a POSTFIX
1322 // modifier in real zsh and requires a piece to
1323 // attach to. Without one C `Src/pattern.c:1606+`
1324 // rejects the pattern. Bug #521: zshrs previously
1325 // took a legacy PREFIX path (compile next piece as
1326 // operand) which silently matched empty for `0,0`
1327 // and similar degenerate ranges.
1328 //
1329 // Report nothing here: C's patcomppiece signals a bad
1330 // pattern by `return 0` alone (c:1600, c:1622) and the
1331 // CALLER prints the diagnostic with the pattern it was
1332 // given — `matchpat` zerrs "bad pattern: %s" with the
1333 // full pattern (glob.c:2522), and `[[ ]]` zwarnnams it
1334 // from cond.c:314. Emitting a message from inside the
1335 // compiler printed only the `(#cN,M)` fragment, losing
1336 // the rest of the pattern the user actually wrote.
1337 return -1; // c:1622 `return 0;`
1338 }
1339 }
1340 // Malformed `(#c...)` — fall through to generic flag handler.
1341 }
1342 // c:953-984 — mid-pattern `(#...)` glob-flag specifier. Emits
1343 // P_GFLAGS in C to switch GF_IGNCASE / GF_LCMATCHUC /
1344 // GF_MULTIBYTE / etc. mid-match. C body:
1345 // `if ((*patparse == zpc_special[ZPC_INPAR] &&`
1346 // ` patparse[1] == zpc_special[ZPC_HASH]) || ...)`
1347 // ` if (!patgetglobflags(&patparse, &assert, &ignore))`
1348 // ` return 0;`
1349 // Both bytes must equal their zpc_special slots, which
1350 // patcompcharsset (c:480-483) masks to Marker when
1351 // EXTENDEDGLOB is off (c:476). Previously this Rust arm
1352 // compared the raw byte `b'#'`, allowing `(#s)` / `(#e)` /
1353 // `(#i)` to fire even without EXTENDEDGLOB. Parity bugs
1354 // #18/#19 vs real zsh.
1355 let hash_char = zpc_special.lock().unwrap()[ZPC_HASH as usize]; // c:957
1356 if hash_char == b'#'
1357 && off + 1 < bytes.len()
1358 && bytes[off] == b'('
1359 && bytes[off + 1] == b'#'
1360 {
1361 let rest = std::str::from_utf8(&bytes[off..]).unwrap_or("").to_string();
1362 if let Some((bits, assertp, consumed)) = patgetglobflags(&rest) {
1363 patparse_off.fetch_add(consumed, Ordering::Relaxed);
1364 // Emit P_GFLAGS for flag-bit changes if any. Include the
1365 // low byte (the `(#aN)` approximation budget) so a
1366 // mid-pattern `(#a0)` / `(#a2)` actually changes the
1367 // error allowance for the following segment — c:Src/
1368 // pattern.c:2941 `patglobflags = P_OPERAND(scan)->l`
1369 // sets the WHOLE value. Without the 0xff, `(#a1)cat(#a0)
1370 // dog` kept the outer budget and `dog` wrongly tolerated
1371 // an error.
1372 let flag_bits = bits & (GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE | 0xff);
1373 if flag_bits != 0 || (flag_bits == 0 && assertp == 0) {
1374 let gf_off = patnode(P_GFLAGS);
1375 let mut buf = patout.lock().unwrap();
1376 buf.extend_from_slice(&flag_bits.to_le_bytes());
1377 drop(buf);
1378 if chain_start < 0 {
1379 chain_start = gf_off as i64;
1380 } else {
1381 set_next(last_tail, gf_off);
1382 }
1383 last_tail = gf_off;
1384 }
1385 // Emit P_ISSTART / P_ISEND when assertp set.
1386 if assertp != 0 {
1387 let as_off = patnode(assertp as u8);
1388 if chain_start < 0 {
1389 chain_start = as_off as i64;
1390 } else {
1391 set_next(last_tail, as_off);
1392 }
1393 last_tail = as_off;
1394 }
1395 continue;
1396 }
1397 // patgetglobflags failed — treat the `(` as a literal group.
1398 }
1399
1400 // c:1011-1014 — `^pat` standalone negation. When `^` is the
1401 // active EXTENDEDGLOB special and appears at piece position
1402 // (not inside a `[...]` bracket class), consume it and call
1403 // patcompnot(0, ...) to emit the EXCLUDE structure.
1404 let sp_hat = zpc_special.lock().unwrap()[ZPC_HAT as usize];
1405 if c == sp_hat && sp_hat != 0 {
1406 patparse_off.fetch_add(1, Ordering::Relaxed); // c:1012 patparse++
1407 let mut not_flags: i32 = 0;
1408 let starter = patcompnot(0, &mut not_flags); // c:1013 patcompnot(0, ...)
1409 if starter < 0 {
1410 return -1;
1411 }
1412 *flagp |= not_flags & P_HSTART;
1413 if chain_start < 0 {
1414 chain_start = starter;
1415 } else {
1416 set_next(last_tail, starter as usize);
1417 }
1418 // patcompnot's tail is the trailing P_NOTHING — we need
1419 // to find it. The chain ends where excend / excl converge
1420 // (both `pattail(excend, n)` and `pattail(excl, n)`). For
1421 // chaining we use `starter` since the next piece's chain
1422 // is appended via patcomppiece's tail_out mechanism — but
1423 // patcompnot doesn't expose a tail. Approximate: scan
1424 // forward from starter to find the last P_NOTHING in the
1425 // emitted block. Cheap: walk patout from starter looking
1426 // for the highest-offset P_NOTHING in this compile pass.
1427 // Simpler: bake the convention that the trailing
1428 // P_NOTHING is the last node — set last_tail to current
1429 // emit position (= start of next node).
1430 let cur_emit = patout.lock().unwrap().len();
1431 last_tail = cur_emit.saturating_sub(I_BODY);
1432 continue;
1433 }
1434 drop(snapshot); // hint: explicit release
1435 let mut piece_flags: i32 = 0;
1436 let mut piece_tail: usize = 0;
1437 // Snapshot the chain tail BEFORE patcomppiece — used by a
1438 // following POSTFIX (#cN,M) to detach this piece from the chain.
1439 let prev_tail_before_piece: i64 = if chain_start < 0 {
1440 -1
1441 } else {
1442 last_tail as i64
1443 };
1444 let piece = patcomppiece(&mut piece_flags, paren, &mut piece_tail);
1445 if piece < 0 {
1446 return -1;
1447 }
1448 if chain_start < 0 {
1449 chain_start = piece;
1450 } else {
1451 // Chain previous piece's tail → this piece's head directly.
1452 set_next(last_tail, piece as usize);
1453 }
1454 last_tail = piece_tail;
1455 last_piece_off = piece;
1456 prev_chain_tail = prev_tail_before_piece;
1457 last_piece_flags = piece_flags;
1458 *flagp &= piece_flags;
1459 }
1460
1461 if chain_start < 0 {
1462 chain_start = patnode(P_NOTHING) as i64;
1463 }
1464 chain_start
1465}
1466
1467// =====================================================================
1468// 10. Glob-flag parser — pattern.c:1037
1469// =====================================================================
1470
1471/// Port of `patgetglobflags(char **strp, long *assertp, int *ignore)` from `Src/pattern.c:1037`.
1472///
1473/// C signature: `int patgetglobflags(char **strp, long *assertp,
1474/// int *ignore)`. C reads/writes the file-static `patglobflags`
1475/// directly via `|=` / `&=` at each switch arm (c:1064, 1070, 1075,
1476/// 1080, 1085, 1090, 1095, 1100, 1112, 1116). The hoist loop in
1477/// patcompile at c:582 / c:636 snapshots `patglobflags` into the
1478/// compiled `Patprog`'s `globflags` and `globend` fields, so the
1479/// per-arm writes are the canonical store — the return value is
1480/// only the success/fail signal (1 / 0).
1481///
1482/// The Rust port stores `patglobflags` in an AtomicI32 and mirrors
1483/// every C write through `fetch_or` / `fetch_and` ops so the same
1484/// snapshot at c:636 works. The return tuple `(flag_bits, assertp,
1485/// consumed)` is kept as an adapter for the patcompile hoist loop
1486/// (pattern.rs:589) which used it to accumulate `hoisted_globflags`
1487/// — those callers see the same bits via the atomic now, but
1488/// changing the return shape is a wider refactor.
1489///
1490/// Returns `Some((flag_bits, assertp_val, consumed_bytes))` on
1491/// success (C returns 1), or `None` on parse failure (C returns 0).
1492pub fn patgetglobflags(s: &str) -> Option<(i32, i64, usize)> {
1493 // c:1037
1494 let bytes = s.as_bytes();
1495 if !s.starts_with("(#") {
1496 return None;
1497 }
1498 let mut i = 2;
1499 // `bits` tracks what this call set so the caller can OR into
1500 // `hoisted_globflags` (the patcompile-side accumulator). The
1501 // canonical store is `patglobflags` (updated below per C arm).
1502 let mut bits: i32 = 0;
1503 let mut assertp: i64 = 0;
1504
1505 while i < bytes.len() && bytes[i] != b')' {
1506 match bytes[i] {
1507 // c:1073-1076 `'i'`: `patglobflags = (patglobflags &
1508 // ~GF_LCMATCHUC) | GF_IGNCASE`.
1509 b'i' => {
1510 patglobflags.fetch_and(!GF_LCMATCHUC, Ordering::Relaxed);
1511 patglobflags.fetch_or(GF_IGNCASE, Ordering::Relaxed);
1512 bits |= GF_IGNCASE;
1513 bits &= !GF_LCMATCHUC;
1514 i += 1;
1515 } // c:1075
1516 // c:1078-1081 — `'I'`: `patglobflags &= ~(GF_LCMATCHUC|GF_IGNCASE)`.
1517 b'I' => {
1518 patglobflags.fetch_and(!(GF_LCMATCHUC | GF_IGNCASE), Ordering::Relaxed);
1519 bits &= !(GF_LCMATCHUC | GF_IGNCASE);
1520 i += 1;
1521 } // c:1080
1522 // c:1068-1071 — `'l'`: `patglobflags = (patglobflags &
1523 // ~GF_IGNCASE) | GF_LCMATCHUC`.
1524 b'l' => {
1525 patglobflags.fetch_and(!GF_IGNCASE, Ordering::Relaxed);
1526 patglobflags.fetch_or(GF_LCMATCHUC, Ordering::Relaxed);
1527 bits |= GF_LCMATCHUC;
1528 bits &= !GF_IGNCASE;
1529 i += 1;
1530 } // c:1070
1531 // c:1083-1086 — `'b'`: `patglobflags |= GF_BACKREF`.
1532 b'b' => {
1533 patglobflags.fetch_or(GF_BACKREF, Ordering::Relaxed);
1534 bits |= GF_BACKREF;
1535 i += 1;
1536 } // c:1085
1537 // c:1088-1091 — `'B'`: `patglobflags &= ~GF_BACKREF`.
1538 b'B' => {
1539 patglobflags.fetch_and(!GF_BACKREF, Ordering::Relaxed);
1540 bits &= !GF_BACKREF;
1541 i += 1;
1542 } // c:1090
1543 // c:1093-1096 — `'m'`: `patglobflags |= GF_MATCHREF`.
1544 b'm' => {
1545 patglobflags.fetch_or(GF_MATCHREF, Ordering::Relaxed);
1546 bits |= GF_MATCHREF;
1547 i += 1;
1548 } // c:1095
1549 // c:1098-1101 — `'M'`: `patglobflags &= ~GF_MATCHREF`.
1550 b'M' => {
1551 patglobflags.fetch_and(!GF_MATCHREF, Ordering::Relaxed);
1552 bits &= !GF_MATCHREF;
1553 i += 1;
1554 } // c:1100
1555 // c:1103-1105 — `'s'`: sets `*assertp = P_ISSTART`,
1556 // doesn't touch patglobflags.
1557 b's' => {
1558 assertp = P_ISSTART as i64;
1559 i += 1;
1560 } // c:1104
1561 // c:1107-1109 — `'e'`: sets `*assertp = P_ISEND`.
1562 b'e' => {
1563 assertp = P_ISEND as i64;
1564 i += 1;
1565 } // c:1108
1566 // c:1111-1113 — `'u'`: `patglobflags |= GF_MULTIBYTE`.
1567 b'u' => {
1568 patglobflags.fetch_or(GF_MULTIBYTE, Ordering::Relaxed);
1569 bits |= GF_MULTIBYTE;
1570 i += 1;
1571 } // c:1112
1572 // c:1115-1117 — `'U'`: `patglobflags &= ~GF_MULTIBYTE`.
1573 b'U' => {
1574 patglobflags.fetch_and(!GF_MULTIBYTE, Ordering::Relaxed);
1575 bits &= !GF_MULTIBYTE;
1576 i += 1;
1577 } // c:1116
1578 // c:1054-1066 — `'a'`: approximate-match error count.
1579 // `ret = zstrtol(++ptr, &nptr, 10);` then
1580 // `patglobflags = (patglobflags & ~0xff) | (ret & 0xff)`.
1581 b'a' => {
1582 i += 1;
1583 let digit_start = i;
1584 let mut errs: i32 = 0;
1585 while i < bytes.len() && bytes[i].is_ascii_digit() {
1586 errs = errs * 10 + (bytes[i] - b'0') as i32;
1587 i += 1;
1588 }
1589 if i == digit_start {
1590 // c:1062 `ptr == nptr` — no digits consumed.
1591 return None;
1592 }
1593 if errs < 0 || errs > 254 {
1594 return None; // c:1062
1595 }
1596 // c:1064 — `patglobflags = (patglobflags & ~0xff) | (ret & 0xff)`.
1597 let mask: i32 = !0xff;
1598 let cur = patglobflags.load(Ordering::Relaxed);
1599 patglobflags.store((cur & mask) | (errs & 0xff), Ordering::Relaxed);
1600 bits = (bits & !0xff) | (errs & 0xff);
1601 }
1602 // c:1046-1050 — `'q'`: glob qualifier, ignored in pattern
1603 // code. Skip to the closing `)` without consuming bits.
1604 b'q' => {
1605 while i < bytes.len() && bytes[i] != b')' {
1606 i += 1;
1607 }
1608 }
1609 // c:1119-1120 — `default: return 0`.
1610 _ => return None,
1611 }
1612 }
1613 // c:1124-1125 — `if (*ptr != Outpar) return 0;`.
1614 if i >= bytes.len() {
1615 return None;
1616 }
1617 i += 1; // c:1129 — `*strp = ptr + 1;` advance past `)`.
1618 Some((bits, assertp, i))
1619}
1620
1621// =====================================================================
1622// 11. Range helpers — pattern.c:1148, :1179
1623// =====================================================================
1624
1625/// Port of `range_type(char *start, int len)` from `Src/pattern.c:1148`. Looks up the
1626/// integer code for a POSIX character class name (e.g. "alpha" → 1).
1627/// Returns None for unknown names.
1628///
1629/// C signature takes a `(char *start, int len)` non-NUL-terminated
1630/// substring; Rust's `&str` carries the length implicitly, so the
1631/// two-arg C shape collapses to a single arg. Param name `start`
1632/// matches C verbatim (Rule E).
1633pub fn range_type(start: &str) -> Option<usize> {
1634 // c:1148
1635 POSIX_CLASS_NAMES
1636 .iter()
1637 .position(|n| *n == start)
1638 .map(|i| i + 1)
1639}
1640
1641/// Port of `int pattern_range_to_string(char *rangestr, char *outstr)`
1642/// from `Src/pattern.c:1179`. Walks a Meta-encoded range bytestring,
1643/// re-emitting the human-readable form: literal chars as-is,
1644/// PP_RANGE pairs as `c1-c2`, POSIX classes as `[:name:]`.
1645/// Returns the output length; `outstr` is the destination buffer
1646/// (NULL = measure-only). The Rust port operates on `&str` (UTF-8
1647/// native) — `Meta`-byte decode collapses since zshrs's pattern
1648/// compiler stores raw chars, so the function effectively walks
1649/// the chars and emits POSIX class names from the `[i]` table at
1650/// `range_type`'s reverse lookup.
1651///
1652/// Rust signature: drops C's `outstr` out-param. C measures-then-fills
1653/// via two passes when `outstr` is non-NULL; Rust returns the String
1654/// directly. Callers needing the C "measure-only" mode read `.len()`
1655/// on the result.
1656pub fn pattern_range_to_string(rangestr: &str) -> String {
1657 // c:1179
1658 let mut out = String::with_capacity(rangestr.len()); // c:1181 int len = 0
1659 let mut chars = rangestr.chars().peekable();
1660 while let Some(c) = chars.next() {
1661 // c:1184-1247 — swtype dispatch via Meta+PP_*. zshrs stores
1662 // POSIX-class markers as a sentinel byte followed by the
1663 // class name. The Meta encoding doesn't apply (UTF-8 native),
1664 // so we just pass chars through, recognizing PP_* class tags
1665 // when they appear as `[:name:]` literal syntax.
1666 if c == '[' && chars.peek() == Some(&':') {
1667 // c:1242 — `[:alpha:]` and similar.
1668 let mut name = String::new();
1669 chars.next(); // consume ':'
1670 while let Some(&cc) = chars.peek() {
1671 if cc == ':' {
1672 chars.next();
1673 if chars.peek() == Some(&']') {
1674 chars.next();
1675 break;
1676 }
1677 name.push(':');
1678 } else {
1679 name.push(cc);
1680 chars.next();
1681 }
1682 }
1683 out.push_str(&format!("[:{}:]", name)); // c:1244
1684 } else {
1685 // c:1185-1213 — single-char or range pair.
1686 // Check for `c1-c2` PP_RANGE form.
1687 out.push(c); // c:1210/1216
1688 if chars.peek() == Some(&'-') {
1689 chars.next();
1690 if let Some(c2) = chars.next() {
1691 out.push('-'); // c:1219
1692 out.push(c2); // c:1220
1693 }
1694 }
1695 }
1696 }
1697 out // c:1261 return len
1698}
1699
1700/// Port of `patcomppiece(int *flagp, int paren)` from `Src/pattern.c:1261`.
1701///
1702/// C: `static long patcomppiece(int *flagp, int paren)`. Parses a
1703/// single atom + optional quantifier. Returns offset of compiled node.
1704/// Out-param `tail_out` receives the byte offset of the LAST opcode
1705/// in the compiled piece — the node whose `.next` should be chained
1706/// to whatever follows in the sequence. For simple atoms (P_EXACTLY,
1707/// P_ANY, etc.) the tail equals the head; for compound pieces
1708/// `(...)` / quantified atoms it points to the trailing P_CLOSE_N or
1709/// quantifier-injected node.
1710///
1711/// Rust signature adds `tail_out: &mut usize` for the LAST-opcode-
1712/// offset that C threads through pointer arithmetic on the
1713/// caller-side `Upat` cursor. Without the out-param, every Rust
1714/// caller would need to re-walk the just-emitted bytecode to find
1715/// the tail; the explicit out-param avoids the re-scan. Rule B
1716/// deviation sanctioned by zshrs's byte-offset bytecode (vs C's
1717/// pointer-arithmetic) substrate.
1718pub fn patcomppiece(flagp: &mut i32, paren: i32, tail_out: &mut usize) -> i64 {
1719 // c:1261
1720 let _ = paren;
1721 let off = patparse_off.load(Ordering::Relaxed);
1722 let parse = patparse.lock().unwrap();
1723 if off >= parse.len() {
1724 return patnode(P_NOTHING) as i64;
1725 }
1726 let bytes = parse.as_bytes();
1727 let c = bytes[off];
1728
1729 // c:1278 — `kshchar = '\0';`. KSH-glob trigger detection: when the
1730 // current byte matches one of the six ZPC_KSH_* slots AND the next
1731 // byte is `(`, record which kshchar so the post-atom dispatch can
1732 // emit the right quantifier shape (c:1615-1746).
1733 //
1734 // c:1279 — `if (*patparse && patparse[1] == Inpar) { ... }`. Note
1735 // the C code uses literal `Inpar` (the `(` token) for the lookahead
1736 // — NOT `zpc_special[ZPC_INPAR]` — so SHGLOB disabling `(` as a
1737 // pattern char doesn't suppress ksh-glob detection here.
1738 let mut kshchar: u8 = 0; // c:1278
1739 if off + 1 < parse.len() && bytes[off + 1] == b'(' {
1740 // c:1279
1741 let sp = zpc_special.lock().unwrap();
1742 if c == sp[ZPC_KSH_PLUS as usize] {
1743 kshchar = b'+';
1744 }
1745 // c:1280-1281
1746 else if c == sp[ZPC_KSH_BANG as usize] {
1747 kshchar = b'!';
1748 }
1749 // c:1282-1283
1750 else if c == sp[ZPC_KSH_BANG2 as usize] {
1751 kshchar = b'!';
1752 }
1753 // c:1284-1285
1754 else if c == sp[ZPC_KSH_AT as usize] {
1755 kshchar = b'@';
1756 }
1757 // c:1286-1287
1758 else if c == sp[ZPC_KSH_STAR as usize] {
1759 kshchar = b'*';
1760 }
1761 // c:1288-1289
1762 else if c == sp[ZPC_KSH_QUEST as usize] {
1763 kshchar = b'?';
1764 } // c:1290-1291
1765 }
1766 drop(parse);
1767
1768 // c:1419-1420 — `if (kshchar) patparse++;`. Skip the trigger byte
1769 // so the atom dispatch consumes the leading `(` as a group.
1770 let dispatch_c = if kshchar != 0 {
1771 patparse_off.fetch_add(1, Ordering::Relaxed);
1772 b'(' // c:1419 — fall through to Inpar case
1773 } else {
1774 c
1775 };
1776
1777 // Atom dispatch. Each arm sets `*tail_out` to the offset of the
1778 // last opcode emitted by this piece (for simple atoms, tail = head).
1779 let atom = match dispatch_c {
1780 b'?' => {
1781 patparse_off.fetch_add(1, Ordering::Relaxed);
1782 *flagp |= P_SIMPLE;
1783 *flagp &= !P_PURESTR;
1784 let h = patnode(P_ANY);
1785 *tail_out = h;
1786 h as i64
1787 }
1788 b'*' => {
1789 patparse_off.fetch_add(1, Ordering::Relaxed);
1790 *flagp &= !P_PURESTR;
1791 let h = patnode(P_STAR);
1792 *tail_out = h;
1793 h as i64
1794 }
1795 b'[' => {
1796 // c:Src/pattern.c:1438 `case Inbrack` + c:1497-1498 —
1797 // `if (*patparse != Outbrack) return 0;`: an ACTIVE `[`
1798 // (token-derived; the patcompile entry normalization maps
1799 // the Inbrack token to raw `[` and every literal/escaped
1800 // bracket to `\[`, which the `\X` arm consumed before
1801 // reaching here) with no closing `]` is a BAD PATTERN.
1802 // Callers zerr "bad pattern: %s" exactly like zsh:
1803 // `print [a-` / `[[ x == [a- ]]` / `case x in [a-)`.
1804 //
1805 // History: a 2026-06-03 partial fix for Bug #564 made
1806 // this case fall back to a literal `[` because the cond
1807 // path then stripped `\` before patcompile. The escape
1808 // now survives into the `\X` literal arm (all three #564
1809 // probes pass), so the leniency only masked genuine bad
1810 // patterns.
1811 let probe_off = off + 1;
1812 let parse_check = patparse.lock().unwrap();
1813 let check_bytes = parse_check.as_bytes();
1814 let has_close = check_bytes[probe_off..].contains(&b']');
1815 drop(parse_check);
1816 if !has_close {
1817 return -1; // c:1498 `return 0;` — bad pattern
1818 }
1819 patparse_off.fetch_add(1, Ordering::Relaxed);
1820 *flagp |= P_SIMPLE;
1821 *flagp &= !P_PURESTR;
1822 // Inline bracket-expression parse (C patcomppiece bracket case).
1823 let mut chars: Vec<u8> = Vec::new();
1824 // Multibyte augmentation: `chars` is a flat ASCII byte set, so a
1825 // multibyte input char (Cyrillic/Greek/CJK/accented) can never
1826 // match it. C's `mb_patmatchrange` handles wide chars via
1827 // `iswalpha`/wide ranges. To match without disturbing the
1828 // (heavily-tested, byte-for-byte) ASCII path, collect the class
1829 // predicates and the non-ASCII literals/ranges SEPARATELY and
1830 // consult them only when the input char is multibyte (P_ANYOF).
1831 let mut mb_classmask: u32 = 0;
1832 let mut mb_chars: Vec<char> = Vec::new();
1833 let mut mb_ranges: Vec<(char, char)> = Vec::new();
1834 let mut negate = false;
1835 let bracket_start = patparse_off.load(Ordering::Relaxed);
1836 let parse_b = patparse.lock().unwrap();
1837 let bb = parse_b.as_bytes();
1838 let mut i_b = bracket_start;
1839 if i_b < bb.len() && (bb[i_b] == b'^' || bb[i_b] == b'!') {
1840 negate = true;
1841 i_b += 1;
1842 }
1843 // c:Src/pattern.c:1456-1459 — `[]...]` exception: when `]` is
1844 // the FIRST char inside the class AND another `]` follows
1845 // later (so the close is not ambiguous), the first `]` is a
1846 // class member. Mirrors `[]x]` matching `]` or `x` in C.
1847 if i_b < bb.len() && bb[i_b] == b']' && bb[i_b + 1..].contains(&b']') {
1848 chars.push(b']');
1849 i_b += 1;
1850 }
1851 while i_b < bb.len() && bb[i_b] != b']' {
1852 // c:Src/pattern.c — `\X` inside a class is handled in C
1853 // by shtokenize converting it to `Bnullkeep X` upstream;
1854 // by the time C's bracket walker runs, the `]` after `\`
1855 // doesn't appear as raw `]` so the close-`]` scan
1856 // doesn't trip on it. The Rust port may receive raw
1857 // `\X` (callers that bypass shtokenize), so handle the
1858 // backslash-escape inline: consume `\X` as a literal
1859 // class-member byte. Bug surface:
1860 // `${q//(#m)[\][()|\\*?#<>~^]/...}` — escaped `]` /
1861 // `\\` / etc. inside the class are class members, not
1862 // metacharacters.
1863 if i_b + 1 < bb.len() && bb[i_b] == b'\\' {
1864 chars.push(bb[i_b + 1]);
1865 i_b += 2;
1866 continue;
1867 }
1868 if i_b + 1 < bb.len() && bb[i_b] == b'[' && bb[i_b + 1] == b':' {
1869 let class_start = i_b + 2;
1870 let mut j_b = class_start;
1871 while j_b + 1 < bb.len() && !(bb[j_b] == b':' && bb[j_b + 1] == b']') {
1872 j_b += 1;
1873 }
1874 if j_b + 1 < bb.len() {
1875 let class_name = std::str::from_utf8(&bb[class_start..j_b]).unwrap_or("");
1876 // Inline POSIX class expansion.
1877 match class_name {
1878 "alpha" => {
1879 for c in b'a'..=b'z' {
1880 chars.push(c);
1881 }
1882 for c in b'A'..=b'Z' {
1883 chars.push(c);
1884 }
1885 }
1886 "upper" => {
1887 for c in b'A'..=b'Z' {
1888 chars.push(c);
1889 }
1890 }
1891 "lower" => {
1892 for c in b'a'..=b'z' {
1893 chars.push(c);
1894 }
1895 }
1896 "digit" => {
1897 for c in b'0'..=b'9' {
1898 chars.push(c);
1899 }
1900 }
1901 "xdigit" => {
1902 for c in b'0'..=b'9' {
1903 chars.push(c);
1904 }
1905 for c in b'a'..=b'f' {
1906 chars.push(c);
1907 }
1908 for c in b'A'..=b'F' {
1909 chars.push(c);
1910 }
1911 }
1912 "alnum" => {
1913 for c in b'a'..=b'z' {
1914 chars.push(c);
1915 }
1916 for c in b'A'..=b'Z' {
1917 chars.push(c);
1918 }
1919 for c in b'0'..=b'9' {
1920 chars.push(c);
1921 }
1922 }
1923 "space" => {
1924 for b in b" \t\n\r\x0b\x0c".iter() {
1925 chars.push(*b);
1926 }
1927 }
1928 "blank" => {
1929 chars.push(b' ');
1930 chars.push(b'\t');
1931 }
1932 "punct" => {
1933 for b in b"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".iter() {
1934 chars.push(*b);
1935 }
1936 }
1937 "cntrl" => {
1938 for c in 0u8..=31 {
1939 chars.push(c);
1940 }
1941 chars.push(127);
1942 }
1943 "print" => {
1944 for c in 32u8..=126 {
1945 chars.push(c);
1946 }
1947 }
1948 "graph" => {
1949 for c in 33u8..=126 {
1950 chars.push(c);
1951 }
1952 }
1953 // c:Src/pattern.c:3693-3700 + Src/ztype.h
1954 // IIDENT — `[[:IDENT:]]` is zsh's
1955 // identifier-character class (alnum + `_`,
1956 // the chars valid in a parameter name).
1957 // Static for the ASCII range; the
1958 // multibyte wcsitype(IIDENT) path isn't
1959 // expanded here. p10k/zsh-z validate names
1960 // with `[[:IDENT:]]##`.
1961 "IDENT" => {
1962 for c in b'0'..=b'9' {
1963 chars.push(c);
1964 }
1965 for c in b'A'..=b'Z' {
1966 chars.push(c);
1967 }
1968 for c in b'a'..=b'z' {
1969 chars.push(c);
1970 }
1971 chars.push(b'_');
1972 }
1973 // c:Src/pattern.c:3707-3719 — zsh-specific
1974 // named classes that track the live $IFS /
1975 // $WORDCHARS via the ztype ISEP/IWORD tables.
1976 // The PP_IFS/PP_IFSSPACE/PP_WORD opcodes
1977 // exist (pattern.rs:3157+) but no parse path
1978 // emits them — this static-expansion path is
1979 // the only named-class parser, and it dropped
1980 // these three (`_ => {}`), so `[[ x =
1981 // [[:IFS:]] ]]` never matched. Expand from the
1982 // current param values (read at compile, like
1983 // IDENT's static set).
1984 "IFS" => {
1985 // c:3709 ISEP — chars in $IFS (default
1986 // space/tab/newline/NUL).
1987 let ifs = crate::ported::params::getsparam("IFS")
1988 .unwrap_or_else(|| " \t\n\0".to_string());
1989 for c in ifs.bytes() {
1990 chars.push(c);
1991 }
1992 }
1993 "IFSSPACE" => {
1994 // c:3713 iwsep — the ASCII whitespace
1995 // subset of $IFS.
1996 let ifs = crate::ported::params::getsparam("IFS")
1997 .unwrap_or_else(|| " \t\n\0".to_string());
1998 for c in ifs.bytes() {
1999 if c == b' ' || c == b'\t' || c == b'\n' {
2000 chars.push(c);
2001 }
2002 }
2003 }
2004 "WORD" => {
2005 // c:3716 IWORD — alnum plus $WORDCHARS.
2006 for c in b'0'..=b'9' {
2007 chars.push(c);
2008 }
2009 for c in b'A'..=b'Z' {
2010 chars.push(c);
2011 }
2012 for c in b'a'..=b'z' {
2013 chars.push(c);
2014 }
2015 let wc = crate::ported::params::getsparam("WORDCHARS")
2016 .unwrap_or_else(|| "*?_-.[]~=/&;!#$%^(){}<>".to_string());
2017 for c in wc.bytes() {
2018 chars.push(c);
2019 }
2020 }
2021 _ => {}
2022 }
2023 // Record the class predicate for multibyte input.
2024 // Bits mirror the Unicode-aware `iswXXX` checks C's
2025 // mb_patmatchrange applies (pattern.rs:3364). digit/
2026 // xdigit stay ASCII-only (already in `chars`) — zsh's
2027 // iswdigit is ASCII in the common locale.
2028 mb_classmask |= match class_name {
2029 "alpha" => 1 << 0,
2030 "alnum" => 1 << 1,
2031 "upper" => 1 << 2,
2032 "lower" => 1 << 3,
2033 "space" | "IFS" | "IFSSPACE" => 1 << 4,
2034 "blank" => 1 << 5,
2035 "punct" => 1 << 6,
2036 "cntrl" => 1 << 7,
2037 "print" => 1 << 8,
2038 "graph" => 1 << 9,
2039 "IDENT" | "WORD" => 1 << 1, // alnum-ish for wide chars
2040 _ => 0,
2041 };
2042 i_b = j_b + 2;
2043 continue;
2044 }
2045 }
2046 // Multibyte member: a byte >= 0x80 leads a UTF-8 sequence
2047 // that cannot live in the ASCII `chars` set. Decode it as a
2048 // wide literal, or a wide range `<ch>-<ch>`, for the P_ANYOF
2049 // multibyte path.
2050 if bb[i_b] >= 0x80 {
2051 if let Some(lo) = std::str::from_utf8(&bb[i_b..])
2052 .ok()
2053 .and_then(|s| s.chars().next())
2054 {
2055 let lolen = lo.len_utf8();
2056 if i_b + lolen < bb.len()
2057 && bb[i_b + lolen] == b'-'
2058 && i_b + lolen + 1 < bb.len()
2059 && bb[i_b + lolen + 1] != b']'
2060 {
2061 if let Some(hi) = std::str::from_utf8(&bb[i_b + lolen + 1..])
2062 .ok()
2063 .and_then(|s| s.chars().next())
2064 {
2065 mb_ranges.push((lo, hi));
2066 i_b += lolen + 1 + hi.len_utf8();
2067 continue;
2068 }
2069 }
2070 mb_chars.push(lo);
2071 i_b += lolen;
2072 continue;
2073 }
2074 }
2075 if i_b + 2 < bb.len() && bb[i_b + 1] == b'-' && bb[i_b + 2] != b']' {
2076 let lo = bb[i_b];
2077 let hi = bb[i_b + 2];
2078 // ASCII-low, multibyte-high range (`[a-я]`): record as a
2079 // wide range so the high end matches; the ASCII portion
2080 // of the range still expands into `chars` below.
2081 if hi >= 0x80 {
2082 if let Some(hich) = std::str::from_utf8(&bb[i_b + 2..])
2083 .ok()
2084 .and_then(|s| s.chars().next())
2085 {
2086 mb_ranges.push((lo as char, hich));
2087 for c in lo..=0x7f {
2088 chars.push(c);
2089 }
2090 i_b += 2 + hich.len_utf8();
2091 continue;
2092 }
2093 }
2094 for c in lo..=hi {
2095 chars.push(c);
2096 }
2097 i_b += 3;
2098 } else {
2099 chars.push(bb[i_b]);
2100 i_b += 1;
2101 }
2102 }
2103 drop(parse_b);
2104 if let Some(p_lock) = patparse.lock().ok() {
2105 if i_b < p_lock.len() && p_lock.as_bytes()[i_b] == b']' {
2106 i_b += 1;
2107 }
2108 }
2109 patparse_off.store(i_b, Ordering::Relaxed);
2110 let opcode = if negate { P_ANYBUT } else { P_ANYOF };
2111 let off2 = patnode(opcode);
2112 let mut buf = patout.lock().unwrap();
2113 // Body layout: `[len:u32]` (total following bytes) then
2114 // `[chars_len:u32][chars][classmask:u32][n_mbchars:u32]
2115 // [mbchars:u32*n][n_mbranges:u32][(lo,hi):u32*2]`. `len` counts
2116 // the WHOLE body so `advance_past_instr` / node traversal (which
2117 // do `4 + len`) skip it unchanged — only the P_ANYOF/P_ANYBUT
2118 // matcher parses the sub-structure. Pure-ASCII brackets carry a
2119 // zeroed 12-byte tail (classmask 0, no mbchars/mbranges).
2120 let mut body_ext: Vec<u8> = Vec::new();
2121 body_ext.extend_from_slice(&(chars.len() as u32).to_le_bytes());
2122 body_ext.extend_from_slice(&chars);
2123 body_ext.extend_from_slice(&mb_classmask.to_le_bytes());
2124 body_ext.extend_from_slice(&(mb_chars.len() as u32).to_le_bytes());
2125 for c in &mb_chars {
2126 body_ext.extend_from_slice(&(*c as u32).to_le_bytes());
2127 }
2128 body_ext.extend_from_slice(&(mb_ranges.len() as u32).to_le_bytes());
2129 for (lo, hi) in &mb_ranges {
2130 body_ext.extend_from_slice(&(*lo as u32).to_le_bytes());
2131 body_ext.extend_from_slice(&(*hi as u32).to_le_bytes());
2132 }
2133 let len = body_ext.len() as u32;
2134 buf.extend_from_slice(&len.to_le_bytes());
2135 buf.extend_from_slice(&body_ext);
2136 *tail_out = off2;
2137 off2 as i64
2138 }
2139 b'(' => {
2140 patparse_off.fetch_add(1, Ordering::Relaxed);
2141 *flagp &= !P_PURESTR;
2142 // c:1508-1525 — `if (kshchar == '!') patcompnot(1, ...) else
2143 // patcompswitch(1, ...)`. The `!(pat)` form needs a
2144 // negation-aware compile that builds an EXCLUDE subtree;
2145 // every other case (including `@(pat)`, the bare `(pat)`
2146 // group, and the `+/*/?` ksh forms) takes the alternation
2147 // switch path.
2148 if kshchar == b'!' {
2149 // c:1522-1523
2150 let mut flags2: i32 = 0;
2151 let starter_off = patcompnot(1, &mut flags2);
2152 if starter_off < 0 {
2153 return -1;
2154 }
2155 *flagp |= flags2 & P_HSTART; // c:1526
2156 *tail_out = starter_off as usize;
2157 // c:1419 already consumed `!`; the b'(' branch consumed
2158 // `(`; patcompnot drained through to `)`. Verify here.
2159 return starter_off;
2160 }
2161 // c:775-783 — `if (paren && (patglobflags & GF_BACKREF) &&
2162 // patnpar <= NSUBEXP) { parno = patnpar++; starter =
2163 // patnode(P_OPEN + parno); } else starter = 0;`. A group is
2164 // NUMBERED (capture slot allocated) only when GF_BACKREF is
2165 // live in patglobflags at the point the `(` is compiled —
2166 // wherever the `(#b)` appeared (leading, after a literal
2167 // prefix, mid-pattern). Beyond NSUBEXP, C "just use[s]
2168 // P_OPEN on its own" (c:777-780): parno stays 0 and the
2169 // match arms record nothing (c:2957 `if (no && ...)`).
2170 // The previous Rust arm numbered EVERY paren and returned
2171 // -1 past NSUBEXP — both divergent.
2172 let gf_now = patglobflags.load(Ordering::Relaxed);
2173 let n = if (gf_now & GF_BACKREF) != 0
2174 && patnpar.load(Ordering::Relaxed) <= NSUBEXP as i32
2175 {
2176 patnpar.fetch_add(1, Ordering::Relaxed)
2177 } else {
2178 0 // plain (uncaptured) group — P_OPEN+0 / P_CLOSE+0
2179 };
2180 let opcode = P_OPEN + n as u8;
2181 let open_off = patnode(opcode);
2182 // c:770 — `savglobflags = patglobflags`. Snapshot the glob
2183 // flags entering the group so a flag set INSIDE it (e.g.
2184 // `((#i)foo)`) can be restored on the way out.
2185 let savglobflags = patglobflags.load(Ordering::Relaxed);
2186 let mut inner_flags: i32 = 0;
2187 let inner = patcompswitch(1, &mut inner_flags);
2188 if inner < 0 {
2189 return -1;
2190 }
2191 // Expect closing ')'.
2192 let cur_off = patparse_off.load(Ordering::Relaxed);
2193 let p = patparse.lock().unwrap();
2194 if cur_off >= p.len() || p.as_bytes()[cur_off] != b')' {
2195 return -1;
2196 }
2197 drop(p);
2198 patparse_off.fetch_add(1, Ordering::Relaxed);
2199 let close_off = patnode(P_CLOSE + n as u8);
2200 // P_OPEN_N.next → first BRANCH of inner alternation.
2201 set_next(open_off, inner as usize);
2202 // Mirror C pattern.c:903 `pattail(starter, ender)`:
2203 // walk the BRANCH alt-chain from P_OPEN and patch the
2204 // last BRANCH's `.next` to P_CLOSE_N. Without this, the
2205 // outer `pattail` walk would descend into the inner
2206 // alt-chain (P_OPEN.next → inner BRANCH) and corrupt
2207 // its terminator.
2208 pattail(open_off, close_off);
2209 // Each branch's operand chain ends at P_CLOSE_N.
2210 chain_branches_to(inner as usize, close_off);
2211 *flagp &= !P_PURESTR;
2212 // c:920-929 — `if (paren && gfchanged) { pattail(ender,
2213 // patnode(P_GFLAGS)); patglobflags = savglobflags; }`. When
2214 // a glob flag that the matcher honors mid-pattern
2215 // (IGNCASE / LCMATCHUC / MULTIBYTE) changed inside the
2216 // group, append a P_GFLAGS node after P_CLOSE that restores
2217 // the entering value. Without it `[[ FOOXx = ((#i)foox)X ]]`
2218 // wrongly matched: the `(#i)` set GF_IGNCASE for the rest of
2219 // the match, so the trailing case-sensitive `X` folded too.
2220 let after_gf = patglobflags.load(Ordering::Relaxed);
2221 let relevant = GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE;
2222 if (after_gf ^ savglobflags) & relevant != 0 {
2223 let gf_off = patnode(P_GFLAGS);
2224 {
2225 let mut buf = patout.lock().unwrap();
2226 buf.extend_from_slice(&(savglobflags & relevant).to_le_bytes());
2227 }
2228 set_next(close_off, gf_off);
2229 patglobflags.store(savglobflags, Ordering::Relaxed); // c:927
2230 *tail_out = gf_off;
2231 return open_off as i64;
2232 }
2233 *tail_out = close_off;
2234 open_off as i64
2235 }
2236 b'\\' => {
2237 // c:Src/pattern.c — raw `\X` reaches this arm when the
2238 // input bypassed shtokenize (Src/glob.c:3565), which is
2239 // the path taken by paramsubst callers passing singsub-
2240 // computed pattern strings. C's faithful path: shtokenize
2241 // converts `\X` (special X) to `Bnullkeep X` (case at
2242 // pattern.c:1584), and a `\X` with non-special X stays
2243 // raw — both bytes survive to pattern.c's literal-run
2244 // default arm. Trailing lone `\` also survives shtokenize
2245 // (bslash flag stays 1 to end-of-string, no replacement
2246 // fires) and reaches the default-arm literal emission.
2247 //
2248 // Two arms mirror that:
2249 // - `\X` (off2 in range): emit X as literal. Equivalent
2250 // to Bnullkeep X arm in C's pattern.c — the next byte
2251 // is the user-literal payload.
2252 // - trailing lone `\`: emit `\` as literal. Equivalent
2253 // to C's pattern.c default arm receiving a raw `\`
2254 // byte that survived shtokenize.
2255 patparse_off.fetch_add(1, Ordering::Relaxed);
2256 let p = patparse.lock().unwrap();
2257 let off2 = patparse_off.load(Ordering::Relaxed);
2258 if off2 >= p.len() {
2259 drop(p);
2260 *flagp |= P_SIMPLE;
2261 let lit_off = patnode(P_EXACTLY);
2262 let mut buf_lit = patout.lock().unwrap();
2263 buf_lit.extend_from_slice(&1u32.to_le_bytes());
2264 buf_lit.push(b'\\');
2265 *tail_out = lit_off;
2266 return lit_off as i64;
2267 }
2268 let escaped = p.as_bytes()[off2];
2269 drop(p);
2270 patparse_off.fetch_add(1, Ordering::Relaxed);
2271 *flagp |= P_SIMPLE;
2272 let lit_off = patnode(P_EXACTLY);
2273 let mut buf_lit = patout.lock().unwrap();
2274 buf_lit.extend_from_slice(&1u32.to_le_bytes());
2275 buf_lit.push(escaped);
2276 *tail_out = lit_off;
2277 lit_off as i64
2278 }
2279 b'<' => {
2280 // Numeric range: <a-b> / <a-> / <-b> / <-> .
2281 // Port of pattern.c:1528-1570 (Inang case).
2282 //
2283 // c:Src/pattern.c:1528 — C's `case Inang:` only fires for
2284 // the Inang TOKEN (0x99) emitted by the lexer at
2285 // Src/lex.c:1202 when `isnumglob()` returns true (i.e.
2286 // the `<…>` body parses as a numeric range). Raw `<` (0x3C)
2287 // in source — quoted `"<"`, escaped `\<`, or any `<` in a
2288 // non-numglob position — never reaches the pattern compiler
2289 // as a metachar; it falls through to the literal-run arm.
2290 //
2291 // zshrs's pattern compiler is fed by callers that mix lexer
2292 // tokens with raw ASCII (singsub output, runtime-computed
2293 // pattern strings that never went through `Src/lex.c:1201`'s
2294 // isnumglob check). The faithful port: do the same
2295 // `[digit]*-[digit]*>` walk inline that `isnumglob` does at
2296 // lex.c:580-610, with full state-save + rewind on failure so
2297 // a non-numeric `<` (e.g. `<[:digit:]]#>` from zconvey's
2298 // color-marker pattern, or `<no-data>` from zinit.zsh:2507)
2299 // falls back to the literal-run arm.
2300 //
2301 // The numglob shape mirrors C's isnumglob exactly: digits
2302 // then `-` then digits then `>`. Empty leading / trailing
2303 // digit runs are OK (gives the `<-N>` / `<N->` / `<->` forms).
2304 let entry_off = patparse_off.load(Ordering::Relaxed);
2305 patparse_off.fetch_add(1, Ordering::Relaxed); // consume `<`
2306 let parse_n = patparse.lock().unwrap();
2307 let nb = parse_n.as_bytes();
2308 let mut j = patparse_off.load(Ordering::Relaxed);
2309 let mut len_flag: u8 = 0; // bit 0 = lo present, bit 1 = hi present
2310 let mut from: i64 = 0;
2311 let lo_start = j;
2312 while j < nb.len() && nb[j].is_ascii_digit() {
2313 from = from * 10 + (nb[j] - b'0') as i64;
2314 j += 1;
2315 }
2316 if j > lo_start {
2317 len_flag |= 1;
2318 } // c:1538 — `len |= 1`
2319 // Mandatory dash. C's isnumglob bails (ret=0) here too —
2320 // walks until non-digit, then if non-digit isn't `-` (or `>`
2321 // after seeing `-`), returns "not numglob". Rewind the `<`
2322 // consumption and fall through to the literal-run arm.
2323 if j >= nb.len() || nb[j] != b'-' {
2324 drop(parse_n);
2325 patparse_off.store(entry_off, Ordering::Relaxed);
2326 let h = patnode(P_EXACTLY);
2327 let mut buf = patout.lock().unwrap();
2328 let len: u32 = 1;
2329 buf.extend_from_slice(&len.to_le_bytes());
2330 buf.push(b'<');
2331 patparse_off.fetch_add(1, Ordering::Relaxed);
2332 *flagp |= P_SIMPLE;
2333 *tail_out = h;
2334 return h as i64;
2335 }
2336 j += 1; // c:1543 patparse++
2337 let mut to: i64 = 0;
2338 let hi_start = j;
2339 while j < nb.len() && nb[j].is_ascii_digit() {
2340 to = to * 10 + (nb[j] - b'0') as i64;
2341 j += 1;
2342 }
2343 if j > hi_start {
2344 len_flag |= 2;
2345 } // c:1548 — `len |= 2`
2346 // Expect closing '>'. Mirror the dash-failure rewind here
2347 // too: a `<N-` without `>` is non-numglob per C's isnumglob.
2348 if j >= nb.len() || nb[j] != b'>' {
2349 drop(parse_n);
2350 patparse_off.store(entry_off, Ordering::Relaxed);
2351 let h = patnode(P_EXACTLY);
2352 let mut buf = patout.lock().unwrap();
2353 let len: u32 = 1;
2354 buf.extend_from_slice(&len.to_le_bytes());
2355 buf.push(b'<');
2356 patparse_off.fetch_add(1, Ordering::Relaxed);
2357 *flagp |= P_SIMPLE;
2358 *tail_out = h;
2359 return h as i64;
2360 }
2361 j += 1;
2362 drop(parse_n);
2363 patparse_off.store(j, Ordering::Relaxed);
2364 *flagp &= !P_PURESTR;
2365
2366 let off2 = match len_flag {
2367 // c:1552-1567
2368 3 => {
2369 // c:1554 P_NUMRNG
2370 let off2 = patnode(P_NUMRNG);
2371 let mut buf = patout.lock().unwrap();
2372 buf.extend_from_slice(&from.to_le_bytes());
2373 buf.extend_from_slice(&to.to_le_bytes());
2374 off2
2375 }
2376 2 => {
2377 // c:1559 P_NUMTO
2378 let off2 = patnode(P_NUMTO);
2379 let mut buf = patout.lock().unwrap();
2380 buf.extend_from_slice(&to.to_le_bytes());
2381 off2
2382 }
2383 1 => {
2384 // c:1563 P_NUMFROM
2385 let off2 = patnode(P_NUMFROM);
2386 let mut buf = patout.lock().unwrap();
2387 buf.extend_from_slice(&from.to_le_bytes());
2388 off2
2389 }
2390 _ => patnode(P_NUMANY), // c:1568
2391 };
2392 *tail_out = off2;
2393 off2 as i64
2394 }
2395 _ => {
2396 // Accumulate a literal run.
2397 let mut buf: Vec<u8> = Vec::new();
2398 let mut local_off = off;
2399 let p = patparse.lock().unwrap();
2400 // c:1312-1317 — break on kshchar trigger (any of the six
2401 // ZPC_KSH_* slots whose literal byte is followed by `(`).
2402 // Snapshot the zpc_special slots that participate; when
2403 // KSHGLOB is off they're Marker (0xa2) and won't match
2404 // ordinary bytes.
2405 let (ksh_plus, ksh_bang, ksh_bang2, ksh_at, ksh_star, ksh_quest) = {
2406 let sp = zpc_special.lock().unwrap();
2407 (
2408 sp[ZPC_KSH_PLUS as usize],
2409 sp[ZPC_KSH_BANG as usize],
2410 sp[ZPC_KSH_BANG2 as usize],
2411 sp[ZPC_KSH_AT as usize],
2412 sp[ZPC_KSH_STAR as usize],
2413 sp[ZPC_KSH_QUEST as usize],
2414 )
2415 };
2416 // c:1314-1317 — `~` is a literal-run terminator IFF it's
2417 // active as the exclusion operator (EXTENDEDGLOB on) AND
2418 // the lookahead is `/` or a non-segment-special byte.
2419 // Without EXTENDEDGLOB the byte is Marker (0xa2) so the
2420 // equality check fails naturally. Limited to TILDE only —
2421 // the other ZPC_SEG_COUNT slots (SLASH, NULL, BAR, OUTPAR)
2422 // are already covered by the explicit stop list above
2423 // (`|` `)` handled there; `/` was deliberately NOT a
2424 // literal-run break in this Rust port, see
2425 // `zsh_corpus_hash_s_e_anchors_match_bare_test` which
2426 // expects `/` to stay literal inside `(...)` alternation).
2427 let (sp_tilde_lit, sp_seg_lit_set, sp_hat_lit, sp_hash_lit) = {
2428 let sp = zpc_special.lock().unwrap();
2429 let mut set = [false; 256];
2430 for i in 0..(ZPC_SEG_COUNT as usize) {
2431 set[sp[i] as usize] = true;
2432 }
2433 (
2434 sp[ZPC_TILDE as usize],
2435 set,
2436 sp[ZPC_HAT as usize],
2437 sp[ZPC_HASH as usize],
2438 )
2439 };
2440 while local_off < p.len() {
2441 let b = p.as_bytes()[local_off];
2442 // c:Src/pattern.c:480-483 — `if (!isset(EXTENDEDGLOB))`
2443 // masks ZPC_HAT and ZPC_HASH to Marker so the
2444 // patcompiece dispatch treats `^` / `#` as literals
2445 // (no negation / no zero-or-more closure).
2446 // The literal-run break list hardcoded `b'^'` /
2447 // `b'#'` though, which still terminated the literal
2448 // accumulator and forced patcompiece's empty-buf
2449 // -1 return → "bad pattern: ^.*" for the unset
2450 // EXTENDEDGLOB case. Gate the break on the actual
2451 // zpc_special slot so an EXTENDEDGLOB-off run treats
2452 // `^` and `#` as ordinary chars. Bug #421.
2453 if matches!(b, b'?' | b'*' | b'[' | b'(' | b')' | b'|' | b'\\' | b'<')
2454 || (b == b'^' && sp_hat_lit == b'^')
2455 || (b == b'#' && sp_hash_lit == b'#')
2456 {
2457 break;
2458 }
2459 if b == sp_tilde_lit && sp_tilde_lit != 0 {
2460 let la = p.as_bytes().get(local_off + 1).copied().unwrap_or(0);
2461 // Literal-tilde exception: `~` followed by a
2462 // segment-special OTHER than `/` stays in the run.
2463 let literal_tilde_exception = la != b'/' && sp_seg_lit_set[la as usize];
2464 if !literal_tilde_exception {
2465 break;
2466 }
2467 }
2468 // c:1278-1292 — ksh-glob trigger lookahead. If the next
2469 // byte is `(` AND this byte matches one of the six
2470 // ZPC_KSH_* slots, stop the literal run BEFORE this byte
2471 // so the next patcomppiece call consumes it as a ksh
2472 // quantifier prefix (e.g. `pre+(x)post` accumulates
2473 // `pre` then breaks at `+` since `+(` follows).
2474 if local_off + 1 < p.len() && p.as_bytes()[local_off + 1] == b'(' {
2475 if b == ksh_plus
2476 || b == ksh_bang
2477 || b == ksh_bang2
2478 || b == ksh_at
2479 || b == ksh_star
2480 || b == ksh_quest
2481 {
2482 break;
2483 }
2484 }
2485 buf.push(b);
2486 local_off += 1;
2487 }
2488 drop(p);
2489 if buf.is_empty() {
2490 return -1;
2491 }
2492 // c:Src/pattern.c:1340-1351 — multi-char run with TRAILING `#`
2493 // (or `(#cN,M)`) must backtrack ONE char so the trailing
2494 // quantifier applies to the LAST char only. Without this,
2495 // `fo#` compiles as `(fo)#` instead of `f` + `o#`, breaking
2496 // `(fo#)#` against "ffo" (the test pin we are fixing).
2497 //
2498 // C body:
2499 // if ((*patparse == zpc_special[ZPC_HASH] || ...) && morelen)
2500 // patparse = patprev;
2501 //
2502 // morelen = "more than one char in the literal run". The
2503 // backtrack pops the LAST byte from buf and rewinds patparse
2504 // by 1 so the next patcomppiece call sees that byte as its
2505 // own atom.
2506 let has_trailing_hash = {
2507 let sp_hash = zpc_special.lock().unwrap()[ZPC_HASH as usize];
2508 let p = patparse.lock().unwrap();
2509 let hash_at = local_off < p.len() && p.as_bytes()[local_off] == b'#';
2510 drop(p);
2511 hash_at && sp_hash == b'#'
2512 };
2513 if buf.len() > 1 && has_trailing_hash {
2514 let _ = buf.pop();
2515 local_off -= 1;
2516 }
2517 patparse_off.store(local_off, Ordering::Relaxed);
2518 *flagp |= P_SIMPLE;
2519 // If it's a single char, mark simple; multi-char run stays pure-string.
2520 let lit_off = patnode(P_EXACTLY);
2521 let mut buf_lit = patout.lock().unwrap();
2522 let len = buf.len() as u32;
2523 buf_lit.extend_from_slice(&len.to_le_bytes());
2524 buf_lit.extend_from_slice(&buf);
2525 *tail_out = lit_off;
2526 lit_off as i64
2527 }
2528 };
2529
2530 if atom < 0 {
2531 return atom;
2532 }
2533
2534 // c:1606-1644 — quantifier dispatch. Three sources of quantifier:
2535 // (a) trailing `#` / `##` (zsh-extended hash repetition)
2536 // (b) trailing `(#cN,M)` count-spec (handled in patcompbranch, not here)
2537 // (c) kshchar form `+/*/?` already at the atom's leading byte.
2538 //
2539 // Rule c:1615 — if no hash AND no count AND kshchar is one of
2540 // (none, '@', '!'), no quantifier; return atom as-is. `@` is the
2541 // identity quantifier (match group exactly once); `!` already had
2542 // its negation compiled by patcompnot inside the atom.
2543 let q_off = patparse_off.load(Ordering::Relaxed);
2544 let parse2 = patparse.lock().unwrap();
2545 // c:1611-1620 — `if (*patparse == zpc_special[ZPC_HASH])` quantifier
2546 // dispatch. The C source reads `zpc_special[ZPC_HASH]`, which
2547 // patcompcharsset (c:480-483) sets to `'#'` only when
2548 // EXTENDEDGLOB is on, else to the Marker byte (c:476). A literal
2549 // '#' in the source pattern therefore does NOT trigger the
2550 // quantifier path when EXTENDEDGLOB is off — the comparison
2551 // `*patparse == Marker` fails. Previously this Rust check used
2552 // a bare `b'#'` comparison instead of consulting zpc_special,
2553 // making `(x)#` and `[a-z]##[0-9]##` match even with
2554 // extendedglob OFF (parity bugs #5/#6 vs real zsh).
2555 let hash_char = zpc_special.lock().unwrap()[ZPC_HASH as usize]; // c:1612
2556 let has_hash = hash_char == b'#' && q_off < parse2.len() && parse2.as_bytes()[q_off] == b'#'; // c:1611-1614
2557 drop(parse2);
2558 if !has_hash && (kshchar == 0 || kshchar == b'@' || kshchar == b'!') {
2559 return atom; // c:1616-1617
2560 }
2561
2562 // c:1621-1622 — kshchar with hash is a parse error (too much at once).
2563 if kshchar != 0 && has_hash {
2564 return -1;
2565 }
2566
2567 // c:1624-1644 — pick the operator + post-atom flags.
2568 let op: u8;
2569 let mut consume_hashes = 0;
2570 if kshchar == b'*' {
2571 // c:1624-1626
2572 op = P_ONEHASH;
2573 *flagp = P_HSTART;
2574 } else if kshchar == b'+' {
2575 // c:1627-1629
2576 op = P_TWOHASH;
2577 *flagp = P_HSTART;
2578 } else if kshchar == b'?' {
2579 // c:1630-1632
2580 op = 0; // sentinel — `?` desugars to (x|) via the BRANCH path below
2581 *flagp = 0;
2582 } else {
2583 // c:1637-1644 — `#` / `##`.
2584 let parse_h = patparse.lock().unwrap();
2585 let two = q_off + 1 < parse_h.len() && parse_h.as_bytes()[q_off + 1] == b'#';
2586 drop(parse_h);
2587 if two {
2588 op = P_TWOHASH;
2589 consume_hashes = 2;
2590 } else {
2591 op = P_ONEHASH;
2592 consume_hashes = 1;
2593 }
2594 *flagp = P_HSTART;
2595 }
2596 if consume_hashes > 0 {
2597 patparse_off.fetch_add(consume_hashes, Ordering::Relaxed);
2598 }
2599
2600 // c:1705-1746 — quantifier emission.
2601 //
2602 // Read the atom's opcode so c:1705 (`?#` → `*`) optimization can
2603 // apply. The atom byte sits at `atom + I_OP` in patout.
2604 let atom_op = {
2605 let buf = patout.lock().unwrap();
2606 if (atom as usize) + I_OP < buf.len() {
2607 buf[atom as usize + I_OP]
2608 } else {
2609 0
2610 }
2611 };
2612
2613 if ((*flagp & P_SIMPLE) != 0) && (op == P_ONEHASH || op == P_TWOHASH) && atom_op == P_ANY {
2614 // c:1705-1717 — `?#` becomes `*`; `?##` becomes `?*`. The atom
2615 // is P_ANY at offset `atom`; rewrite or pad as needed.
2616 let mut buf = patout.lock().unwrap();
2617 if op == P_TWOHASH {
2618 // c:1712-1713 — `?##` → `?*`: leave the P_ANY in place,
2619 // then emit P_STAR right after.
2620 drop(buf);
2621 let _star = patnode(P_STAR);
2622 *tail_out = atom as usize;
2623 } else {
2624 // c:1715-1716 — `?#` → `*`: just rewrite atom's opcode.
2625 buf[atom as usize + I_OP] = P_STAR;
2626 drop(buf);
2627 *tail_out = atom as usize;
2628 }
2629 *flagp &= !P_PURESTR;
2630 } else if ((*flagp & P_SIMPLE) != 0)
2631 && op != 0
2632 && (patglobflags.load(Ordering::Relaxed) & 0xff) == 0
2633 {
2634 // c:1718-1720 — simple operand, no approximate-match counter:
2635 // emit `patinsert(op, starter, NULL, 0)`. The matcher walks
2636 // the inserted P_ONEHASH/P_TWOHASH operand at scan+I_BODY.
2637 patinsert(op, atom as usize, None, 0);
2638 *flagp &= !P_PURESTR;
2639 *tail_out = atom as usize;
2640 } else if op == P_ONEHASH {
2641 // c:1721-1729 — emit x# as (x&|): P_WBRANCH with NULL payload
2642 // ahead of atom; loop via P_BACK; null alternative branch.
2643 // patinsert with `sz=size_of::<union upat>()=8` (we use 8 to
2644 // mirror the C `union upat` slot, then zero-pad it).
2645 let payload = [0u8; 8];
2646 patinsert(P_WBRANCH, atom as usize, Some(&payload), 8);
2647 // Either x — atom is now at atom+I_BODY+8.
2648 let back = patnode(P_BACK);
2649 patoptail(atom as usize, back); // c:1726 — loop back
2650 patoptail(atom as usize, atom as usize); // c:1727
2651 let alt = patnode(P_BRANCH);
2652 pattail(atom as usize, alt); // c:1728 — or
2653 let null_node = patnode(P_NOTHING);
2654 pattail(atom as usize, null_node); // c:1729 — null
2655 *flagp &= !P_PURESTR;
2656 // The piece's chain-tail is the trailing P_NOTHING node — its
2657 // `.next` slot is empty and is the correct splice point for
2658 // whatever piece patcompbranch chains in next.
2659 *tail_out = null_node;
2660 } else if op == P_TWOHASH {
2661 // c:1730-1738 — emit x## as x(&|): P_WBRANCH after atom; loop
2662 // back; null branch.
2663 let wbranch = patnode(P_WBRANCH); // c:1732 — either
2664 let payload = [0u8; 8]; // c:1733-1734 patadd((char *)&up, ..., sizeof(up), 0)
2665 {
2666 let mut buf = patout.lock().unwrap();
2667 buf.extend_from_slice(&payload);
2668 }
2669 pattail(atom as usize, wbranch); // c:1735
2670 let back = patnode(P_BACK);
2671 pattail(back, atom as usize); // c:1736 — loop back
2672 let alt = patnode(P_BRANCH);
2673 pattail(wbranch, alt); // c:1737 — or
2674 let null_node = patnode(P_NOTHING);
2675 pattail(atom as usize, null_node); // c:1738 — null
2676 *flagp &= !P_PURESTR;
2677 // Same as ONEHASH path — tail at the trailing P_NOTHING.
2678 *tail_out = null_node;
2679 } else if kshchar == b'?' {
2680 // c:1739-1746 — emit ?(x) as (x|).
2681 patinsert(P_BRANCH, atom as usize, None, 0); // c:1741 — either x
2682 let alt = patnode(P_BRANCH); // c:1742 — or
2683 pattail(atom as usize, alt);
2684 let null_node = patnode(P_NOTHING); // c:1743 — null
2685 pattail(atom as usize, null_node); // c:1744
2686 patoptail(atom as usize, null_node); // c:1745
2687 *flagp &= !P_PURESTR;
2688 // Same as ONEHASH/TWOHASH — tail at the trailing P_NOTHING.
2689 *tail_out = null_node;
2690 }
2691
2692 // c:1747-1748 — a stray `#` immediately after = compile error.
2693 {
2694 let p = patparse.lock().unwrap();
2695 let q2 = patparse_off.load(Ordering::Relaxed);
2696 if q2 < p.len() && p.as_bytes()[q2] == b'#' {
2697 // Only flag as error when EXTENDEDGLOB-style # is enabled.
2698 let sp = zpc_special.lock().unwrap();
2699 if sp[ZPC_HASH as usize] == b'#' {
2700 return -1;
2701 }
2702 }
2703 }
2704
2705 atom
2706}
2707
2708/// Port of `patcompnot(int paren, int *flagsp)` from `Src/pattern.c:1760`.
2709///
2710/// C: `static long patcompnot(int paren, int *flagsp)`. Implements
2711/// the `^pat` (paren=0) or `!(pat)` (paren=1) extended/ksh-glob
2712/// negation by emitting `P_BRANCH → P_STAR → P_EXCSYNC` followed by
2713/// `P_EXCLUDE [payload]` and the asserted pattern terminated by
2714/// `P_EXCEND`, all joined at a trailing `P_NOTHING`.
2715///
2716/// NOTE: matcher support for `P_EXCSYNC`/`P_EXCEND`/`P_EXCLUDE` is
2717/// only partial in the Rust port — compile is faithful per
2718/// pattern.c:1760-1784 but match-time negation semantics may diverge
2719/// from C for complex backtracking edge cases. Tests covering
2720/// `!(foo)` / `!(foo|bar)` exercise the basic working subset.
2721pub fn patcompnot(paren: i32, flagsp: &mut i32) -> i64 {
2722 // c:1760
2723 // c:1767 — `*flagsp = P_HSTART;`. Negation always starts with `*`
2724 // semantically so the caller knows the piece can match at the
2725 // start of input.
2726 *flagsp = P_HSTART;
2727
2728 // c:1769 — `starter = patnode(P_BRANCH);`
2729 let starter = patnode(P_BRANCH);
2730 // c:1770 — `br = patnode(P_STAR);`
2731 let br = patnode(P_STAR);
2732 // c:1771 — `excsync = patnode(P_EXCSYNC);`
2733 let excsync = patnode(P_EXCSYNC);
2734 // c:1772 — `pattail(br, excsync);`
2735 pattail(br, excsync);
2736 // c:1773 — `pattail(starter, excl = patnode(P_EXCLUDE));`
2737 let excl = patnode(P_EXCLUDE);
2738 pattail(starter, excl);
2739 // c:1774-1775 — `up.p = NULL; patadd((char *)&up, 0, sizeof(up), 0);`
2740 // The EXCLUDE node carries an 8-byte syncptr payload, NULL-init.
2741 {
2742 let mut buf = patout.lock().unwrap();
2743 buf.extend_from_slice(&[0u8; 8]);
2744 }
2745 // c:1776 — `br = (paren ? patcompswitch(1, &dummy) : patcompbranch(&dummy, 0));`
2746 let mut dummy: i32 = 0;
2747 let inner = if paren != 0 {
2748 let r = patcompswitch(1, &mut dummy);
2749 // c:1503-1505 — caller `Inpar` arm expects to consume the
2750 // trailing `)`. Mirror here since patcompnot is invoked from
2751 // the b'(' atom-arm AFTER it already consumed `(`.
2752 if r >= 0 {
2753 let cur = patparse_off.load(Ordering::Relaxed);
2754 let p = patparse.lock().unwrap();
2755 if cur >= p.len() || p.as_bytes()[cur] != b')' {
2756 return -1;
2757 }
2758 drop(p);
2759 patparse_off.fetch_add(1, Ordering::Relaxed);
2760 }
2761 r
2762 } else {
2763 patcompbranch(&mut dummy, 0)
2764 };
2765 if inner < 0 {
2766 return -1; // c:1777 `return 0;` (Rust uses -1 for failure)
2767 }
2768 // c:1778 — `pattail(br, patnode(P_EXCEND));`
2769 let excend = patnode(P_EXCEND);
2770 pattail(inner as usize, excend);
2771 // c:1779 — `n = patnode(P_NOTHING);`
2772 let n = patnode(P_NOTHING);
2773 // c:1780 — `pattail(excsync, n);`
2774 pattail(excsync, n);
2775 // c:1781 — `pattail(excl, n);`
2776 pattail(excl, n);
2777 // c:1783
2778 let _ = br; // suppress unused-var (kept for c-name parity)
2779 starter as i64
2780}
2781
2782/// Port of `patnode(long op)` from `Src/pattern.c:1790`.
2783///
2784/// C: `static long patnode(long op)` — writes a 1-byte opcode plus a
2785/// 4-byte zeroed next-offset. Returns the offset of the opcode byte.
2786fn patnode(op: u8) -> usize {
2787 // c:1790
2788 let mut buf = patout.lock().unwrap();
2789 let off = buf.len();
2790 buf.push(op); // I_OP
2791 buf.extend_from_slice(&[0, 0, 0, 0]); // I_NEXT zeroed
2792 off
2793}
2794
2795/// Port of `patinsert(long op, int opnd, char *xtra, int sz)` from `Src/pattern.c:1807`.
2796///
2797/// C: `static void patinsert(long op, int opnd, char *xtra, int sz)`.
2798/// Inserts an opcode (+ next slot) at position `opnd`, shifting bytes
2799/// after it down by `5 + sz`, then writes `xtra` payload of `sz` bytes.
2800fn patinsert(op: u8, opnd: usize, xtra: Option<&[u8]>, sz: usize) {
2801 // c:1807
2802 let mut buf = patout.lock().unwrap();
2803 let header_sz = 1 + 4; // op + next
2804 let total = header_sz + sz;
2805 // Insert `total` zeroed bytes at opnd, then overwrite.
2806 let mut inserted = vec![0u8; total];
2807 inserted[0] = op;
2808 if let Some(x) = xtra {
2809 let copy_n = x.len().min(sz);
2810 inserted[header_sz..header_sz + copy_n].copy_from_slice(&x[..copy_n]);
2811 }
2812 buf.splice(opnd..opnd, inserted);
2813 // Patch up next_off chains pointing past opnd by adding `total`.
2814 fixup_offsets_after_insert(&mut buf, opnd, total as u32);
2815}
2816
2817/// Port of `pattail(long p, long val)` from `Src/pattern.c:1834`.
2818///
2819/// C: `static void pattail(long p, long val)` — patches the next-offset
2820/// field of the opcode at offset `p` to point to `val`. Walks any
2821/// existing chain to the end before patching.
2822fn pattail(p: usize, val: usize) {
2823 // c:1834
2824 let mut buf = patout.lock().unwrap();
2825 let mut cur = p;
2826 loop {
2827 if cur + I_BODY > buf.len() {
2828 return;
2829 }
2830 let next_bytes: [u8; 4] = buf[cur + I_NEXT..cur + I_NEXT + 4].try_into().unwrap();
2831 let next = u32::from_le_bytes(next_bytes) as usize;
2832 if next == 0 {
2833 break;
2834 }
2835 cur = next;
2836 }
2837 let val_bytes = (val as u32).to_le_bytes();
2838 if cur + I_NEXT + 4 <= buf.len() {
2839 buf[cur + I_NEXT..cur + I_NEXT + 4].copy_from_slice(&val_bytes);
2840 }
2841}
2842
2843/// Port of `patoptail(long p, long val)` from `Src/pattern.c:1856`.
2844///
2845/// C: `static void patoptail(long p, long val)` — like pattail but
2846/// only patches branches (P_BRANCH/P_WBRANCH).
2847///
2848/// C: c:1862-1865 — for P_BRANCH the operand sits at `P_OPERAND(p)` =
2849/// `p+1` (Upat slot, i.e. byte offset `p + I_BODY` in Rust); for
2850/// P_WBRANCH the operand sits at `P_OPERAND(p) + 1` (skipping the
2851/// 8-byte syncptr payload Upat slot), i.e. byte offset
2852/// `p + I_BODY + 8` in Rust.
2853fn patoptail(p: usize, val: usize) {
2854 // c:1856
2855 let buf = patout.lock().unwrap();
2856 if p + I_OP >= buf.len() {
2857 return;
2858 }
2859 let op = buf[p + I_OP];
2860 drop(buf);
2861 if P_ISBRANCH(op) {
2862 // c:1862-1865 — operand offset depends on op kind.
2863 if op == P_BRANCH {
2864 pattail(p + I_BODY, val);
2865 } else {
2866 // P_WBRANCH / P_EXCLUDE / P_EXCLUDP — operand sits after
2867 // the 8-byte syncptr payload.
2868 pattail(p + I_BODY + 8, val);
2869 }
2870 }
2871}
2872
2873// =====================================================================
2874// 13/14. Matcher — pattern.c:2223-3579
2875// =====================================================================
2876
2877/// State accumulated during a single `patmatch` walk. C uses
2878/// per-thread globals (`patbeginp[]` / `patendp[]` for captures);
2879/// the Rust port encapsulates them in this struct passed by `&mut`.
2880/// Rule D: this struct represents matcher-internal scratch state
2881/// (analogous to `struct rpat pattrystate` at pattern.c:248), not a
2882/// bag-of-globals from unrelated subsystems.
2883///
2884/// **C counterpart**: `struct rpat` at `pattern.c:248`.
2885#[derive(Clone)]
2886#[allow(non_camel_case_types)]
2887pub struct rpat {
2888 /// Per-P_WBRANCH visit bitmap, keyed by WBRANCH-opcode offset.
2889 /// Mirrors C's `Upat ptrp` (`pattern.c:3217`): every WBRANCH carries
2890 /// an 8-byte payload sized as `union upat` — initialised to NULL,
2891 /// then lazily filled by `patmatch` with a buffer the size of the
2892 /// input string. Each byte tracks "have we already tried this
2893 /// WBRANCH at this input position with at most this many errors?".
2894 /// On revisit at the same position with the same-or-fewer errors,
2895 /// C returns 0 to bound the recursion (`pattern.c:3245-3248`). The
2896 /// Rust port keeps the bitmap on rpat (per-pattry call) instead of
2897 /// inside the bytecode payload so the bytecode stays read-only —
2898 /// the key is the WBRANCH offset; the value is a Vec<u8> the size
2899 /// of the input. Without it, `(fo#)#` against ANY input that
2900 /// requires the closure to consume at least one char per iteration
2901 /// (like "ffo") burns through PATMATCH_MAX_DEPTH and aborts.
2902 pub wbranch_visits: std::collections::HashMap<usize, Vec<u8>>,
2903 pub patbeginp: [usize; NSUBEXP], // c:241 capture starts (byte offsets)
2904 pub patendp: [usize; NSUBEXP], // c:242 capture ends
2905 /// `parsfound` from `Src/pattern.c` (per c:2957/c:2989 references).
2906 /// Two-stripe bitmap: bits `0..NSUBEXP` track per-group P_OPEN
2907 /// first-write (`patbeginp[i]` committed); bits `NSUBEXP..2*NSUBEXP`
2908 /// track per-group P_CLOSE first-write (`patendp[i]` committed).
2909 /// Bit `n-1` (open) = `1 << (n-1)`; bit `n-1+NSUBEXP` (close) =
2910 /// `1 << (n-1+NSUBEXP)`. Width u32 to fit `2*NSUBEXP = 18` bits.
2911 /// The prior u16 width with a single-stripe (close-only) bit
2912 /// allowed P_OPEN to re-overwrite `patbeginp[i]` on every
2913 /// backtrack iteration — turning the SECOND capture's start
2914 /// offset into the FIRST iteration's, e.g. `(*)-(*)` against
2915 /// `hello-world` returned `match[2] = "hello-world"` instead of
2916 /// the right `"world"`.
2917 pub captures_set: u32, // c:Src/pattern.c parsfound
2918 /// Port of file-static `int errsfound` from `Src/pattern.c:2046`.
2919 /// Cumulative edit-count for approximate-match `(#aN)`. Reset to 0
2920 /// at the top of each pattry, incremented on P_EXACTLY mismatches
2921 /// when `glob_flags & 0xff > 0` (the substitution-budget byte).
2922 pub errsfound: i32, // c:2046
2923}
2924
2925/// Port of `wchar_t charref(char *x, char *y, int *zmb_ind)` from
2926/// `Src/pattern.c:1909`. Decode the char at `pos` without
2927/// advancing. UTF-32-native delegation: `s.chars().next()` returns
2928/// the decoded codepoint; delegates to Rust's native UTF-8 string
2929/// iterator instead of the C Meta-decode + zshtoken-translate +
2930/// mbrtowc state machine, which all collapse because Rust's `&str`
2931/// is already UTF-8.
2932///
2933/// Rust signature differs from C `charref(char *x, char *y, int *zmb_ind)`:
2934/// the C `y` end-of-string pointer is captured by `&str` length; the
2935/// `zmb_ind` out-param (multibyte-completion index) is dropped — the
2936/// Rust UTF-8 decode is one-shot, never partial.
2937pub fn charref(s: &str, pos: usize) -> Option<char> {
2938 // c:1909
2939 s[pos..].chars().next()
2940}
2941
2942/// Port of `char *charnext(char *x, char *y)` from `Src/pattern.c:1935`.
2943/// C returns the pointer past the current character; the Rust port
2944/// returns that position as a byte offset. Delegates to `metacharinc`
2945/// — same advance-by-one-codepoint logic.
2946pub fn charnext(x: &str, y: usize) -> usize {
2947 // c:1936
2948 metacharinc(x, y)
2949}
2950
2951/// Port of `wchar_t charrefinc(char **x, char *y, int *z)` from
2952/// `Src/pattern.c:1964`. Decode + advance: delegates to the
2953/// `charref`-then-`len_utf8`-step pattern. The C body's Meta /
2954/// mbrtowc / zshtoken triple collapses to one `chars().next()`
2955/// call followed by a byte-count step.
2956///
2957/// Rust signature differs from C: C mutates `*x` via the
2958/// `char **` to advance; Rust mutates `pos` directly. The `y`
2959/// end-of-string sentinel is captured by `&str` length; `z`
2960/// (multibyte-completion index) is dropped per the same one-shot
2961/// UTF-8 decode argument as `charref`.
2962pub fn charrefinc(s: &str, pos: &mut usize) -> Option<char> {
2963 // c:1964
2964 let c = s[*pos..].chars().next()?;
2965 *pos += c.len_utf8();
2966 Some(c)
2967}
2968
2969/// Port of `ptrdiff_t charsub(char *x, char *y)` from `Src/pattern.c:1997`.
2970///
2971/// Returns the number of characters between the start `x` and the
2972/// position `y` — i.e. the character-distance of byte offset `y` from
2973/// the string start. Non-multibyte: the byte distance `y` (each byte
2974/// is one character). Multibyte: the codepoint count of `x[0..y]`.
2975/// `patmatch` uses this (via the C `CHARSUB` macro) to report match
2976/// lengths and backreference positions in characters.
2977///
2978/// Replaces the prior fake that stepped back one char and returned a
2979/// byte offset — unrelated to the C character-count semantics.
2980pub fn charsub(x: &str, y: usize) -> usize {
2981 // c:1997
2982 let y = y.min(x.len());
2983 if !isset(MULTIBYTE) {
2984 // c:2003-2004 — `if (!isset(MULTIBYTE)) return y - x;`
2985 return y;
2986 }
2987 // c:2006-2021 — count codepoints in `x[0..y]`. Rust `&str` is valid
2988 // UTF-8, so `chars().count()` is the faithful equivalent of the C
2989 // mbrtowc loop (the MB_INVALID byte-by-byte fallback can't arise —
2990 // a `&str` never holds invalid sequences).
2991 x[..y].chars().count()
2992}
2993
2994// =====================================================================
2995// 16. String pre-processing — pattern.c:2063, :2080, :2132
2996// =====================================================================
2997
2998/// Port of `pattrystart()` from `Src/pattern.c:2063`. C resets per-
2999/// match state globals; Rust state is per-call so no-op.
3000pub fn pattrystart() {} // c:2063
3001
3002/// Port of `void patmungestring(char **string, int *stringlen, int *unmetalenin)`
3003/// from `Src/pattern.c:2080`.
3004///
3005/// Skips a leading `Nularg` (the empty-tokenised-string sentinel)
3006/// and computes `*stringlen` from `strlen` when caller passed `-1`.
3007/// Mutates all three in/out args; mirrors C's `char **` /
3008/// `int *` semantics via Rust mutable references.
3009pub fn patmungestring(string: &mut &str, stringlen: &mut i32, unmetalenin: &mut i32) {
3010 // c:2080
3011 // c:2082-2091 — `if (*stringlen > 0 && **string == Nularg)` — skip
3012 // the leading Nularg sentinel and adjust lengths.
3013 let bytes = string.as_bytes();
3014 if *stringlen > 0 && !bytes.is_empty() && bytes[0] as char == Nularg {
3015 // c:2085 — `(*string)++;` — advance past Nularg.
3016 *string = &string[1..]; // c:2085
3017 // c:2090 — `if (*unmetalenin > 0) (*unmetalenin)--;`
3018 if *unmetalenin > 0 {
3019 // c:2089
3020 *unmetalenin -= 1; // c:2090
3021 }
3022 // c:2092 — `if (*stringlen > 0) (*stringlen)--;`
3023 if *stringlen > 0 {
3024 // c:2091
3025 *stringlen -= 1; // c:2092
3026 }
3027 }
3028
3029 // c:2096-2097 — `if (*stringlen < 0) *stringlen = strlen(*string);`
3030 if *stringlen < 0 {
3031 // c:2096
3032 *stringlen = string.len() as i32; // c:2097
3033 }
3034}
3035
3036/// Port of `pattry(Patprog prog, char *string)` from `Src/pattern.c:2223`.
3037///
3038/// C signature: `int pattry(Patprog prog, char *string)`. Returns
3039/// non-zero on match, 0 on no-match.
3040pub fn pattry(prog: &Patprog, string: &str) -> bool {
3041 // c:2223
3042 // c:2225 — `return pattrylen(prog, string, len, -1, NULL, 0);`
3043 pattrylen(prog, string, string.len() as i32, -1, None, 0) // c:2225
3044}
3045
3046/// Port of `int pattrylen(Patprog prog, char *string, int len,
3047/// int unmetalen, Patstralloc patstralloc, int offset)` from
3048/// `Src/pattern.c:2236`.
3049///
3050/// ```c
3051/// int
3052/// pattrylen(Patprog prog, char *string, int len, int unmetalen,
3053/// Patstralloc patstralloc, int offset)
3054/// {
3055/// return pattryrefs(prog, string, len, unmetalen, patstralloc, offset,
3056/// NULL, NULL, NULL);
3057/// }
3058/// ```
3059pub fn pattrylen(
3060 prog: &Patprog,
3061 string: &str,
3062 len: i32, // c:2236
3063 unmetalen: i32,
3064 patstralloc: Option<&Patstralloc>,
3065 offset: i32,
3066) -> bool {
3067 // c:2238
3068 pattryrefs(
3069 prog,
3070 string,
3071 len,
3072 unmetalen,
3073 patstralloc,
3074 offset,
3075 None,
3076 None,
3077 None, // c:2239
3078 )
3079}
3080
3081/// Port of `int pattryrefs(Patprog prog, char *string, int stringlen,
3082/// int unmetalenin, Patstralloc patstralloc, int patoffset,
3083/// int *nump, int *begp, int *endp)` from `Src/pattern.c:2294`.
3084/// Runs `prog` against `string[0..stringlen]` (or whole string when
3085/// stringlen=-1) at `patoffset`, returning capture-group ranges.
3086///
3087/// C signature preserved per Rule S1. `Patstralloc` (the metafied-
3088/// string-allocator carrier from zsh.h:1613) is threaded through as
3089/// `Option<&Patstralloc>` matching C's `NULL`-passable pointer; the
3090/// matcher body currently ignores its contents (the substrate that
3091/// uses pre-allocated metafied buffers isn't wired into the Rust
3092/// matcher yet), but the param shape matches C so call sites trace
3093/// 1:1 to upstream.
3094#[allow(clippy::too_many_arguments)]
3095pub fn pattryrefs(
3096 // c:2294
3097 prog: &Patprog,
3098 string: &str,
3099 stringlen: i32,
3100 _unmetalenin: i32,
3101 _patstralloc: Option<&Patstralloc>,
3102 patoffset: i32,
3103 nump: Option<&mut i32>,
3104 begp: Option<&mut Vec<i32>>,
3105 endp: Option<&mut Vec<i32>>,
3106) -> bool {
3107 let trial: &str = if stringlen < 0 || (stringlen as usize) >= string.len() {
3108 string
3109 } else {
3110 &string[..stringlen as usize]
3111 };
3112 // Substring fast path for the ubiquitous `*literal*` shape
3113 // (optionally `(#i)`): program is [P_GFLAGS] P_STAR P_EXACTLY
3114 // P_STAR P_END with no captures requested. history-search-multi-
3115 // word's `${history[(R)(#i)*pat*]}` runs this shape over EVERY
3116 // history entry — 566k backtracking patmatch walks took CPU-
3117 // minutes per ^R (the reported freeze); a memmem-style scan is
3118 // what the shape actually needs. C reads this spirit via its
3119 // `mustoff` must-match prefilter (Src/pattern.c:2460-2483) but
3120 // declines under globflags; here the exact-bytes variant covers
3121 // any content and the `(#i)` variant is taken only when pattern
3122 // AND subject are pure ASCII (byte-fold == the matcher's char
3123 // fold there); anything else falls through to the full matcher.
3124 if nump.is_none()
3125 && begp.is_none()
3126 && endp.is_none()
3127 && patoffset == 0
3128 && (prog.0.flags & (PAT_NOTSTART | PAT_NOTEND) as i32) == 0
3129 // c:2399-2406 — a successful match is REJECTED for a file pattern
3130 // (PAT_NOGLD) whose subject starts with '.', unless the pattern itself
3131 // starts with a literal dot. That rejection lives below, AFTER
3132 // patmatch; returning from the fast path skipped it entirely, so
3133 // `*.*` matched `.hidden` (the literal `.` was found at offset 0 by
3134 // the substring scan) where zsh lists no dot files at all. Hand any
3135 // dot-leading subject under a file pattern to the full matcher, which
3136 // applies the rule. Costs nothing where the fast path was aimed:
3137 // history search (`${history[(R)(#i)*pat*]}`) is not a file glob and
3138 // never sets PAT_NOGLD, and non-dot files still take the fast path.
3139 && !((prog.0.flags & PAT_NOGLD as i32) != 0 && trial.starts_with('.'))
3140 {
3141 // Shape probe, inline (build-gate: no new fn in ported/ without
3142 // a C counterpart): Some((literal, igncase)) iff the program is
3143 // exactly `[P_GFLAGS] P_STAR P_EXACTLY(lit) P_STAR P_END` with
3144 // glob flags ⊆ IGNCASE|MULTIBYTE (no approx budget, no backrefs).
3145 let shape: Option<(String, bool)> = (|| {
3146 let buf: &[u8] = &prog.1;
3147 let mut off = prog.0.startoff as usize;
3148 let mut gflags = prog.0.globflags;
3149 let op_at = |o: usize| -> Option<u8> {
3150 if o + I_BODY <= buf.len() {
3151 Some(buf[o + I_OP])
3152 } else {
3153 None
3154 }
3155 };
3156 // A sole leading BRANCH (next == 0: no alternative) wraps
3157 // the whole program — step inside it.
3158 if op_at(off)? == P_BRANCH {
3159 let next =
3160 u32::from_le_bytes(buf[off + I_NEXT..off + I_NEXT + 4].try_into().ok()?);
3161 if next != 0 {
3162 return None; // real alternation — full matcher
3163 }
3164 off = advance_past_instr(buf, off);
3165 }
3166 if op_at(off)? == P_GFLAGS {
3167 let body = off + I_BODY;
3168 if body + 4 > buf.len() {
3169 return None;
3170 }
3171 gflags |= i32::from_le_bytes(buf[body..body + 4].try_into().ok()?);
3172 off = advance_past_instr(buf, off);
3173 }
3174 if (gflags & !(GF_IGNCASE | GF_MULTIBYTE)) != 0 {
3175 return None;
3176 }
3177 if op_at(off)? != P_STAR {
3178 return None;
3179 }
3180 off = advance_past_instr(buf, off);
3181 // One or more contiguous EXACTLY chunks form the literal
3182 // (the compiler splits long/space-bearing literals).
3183 if op_at(off)? != P_EXACTLY {
3184 return None;
3185 }
3186 let mut lit = String::new();
3187 while op_at(off)? == P_EXACTLY {
3188 let body = off + I_BODY;
3189 if body + 4 > buf.len() {
3190 return None;
3191 }
3192 let len = u32::from_le_bytes(buf[body..body + 4].try_into().ok()?) as usize;
3193 if body + 4 + len > buf.len() {
3194 return None;
3195 }
3196 lit.push_str(std::str::from_utf8(&buf[body + 4..body + 4 + len]).ok()?);
3197 off = advance_past_instr(buf, off);
3198 }
3199 if op_at(off)? != P_STAR {
3200 return None;
3201 }
3202 off = advance_past_instr(buf, off);
3203 if off + I_OP >= buf.len() || buf[off + I_OP] != P_END {
3204 return None;
3205 }
3206 Some((lit, (gflags & GF_IGNCASE) != 0))
3207 })();
3208 if let Some((lit, igncase)) = shape.as_ref().map(|(l, i)| (l.as_str(), *i)) {
3209 if !igncase {
3210 return trial.contains(lit);
3211 }
3212 if lit.is_ascii() && trial.is_ascii() {
3213 let lb = lit.as_bytes();
3214 let tb = trial.as_bytes();
3215 if lb.is_empty() {
3216 return true;
3217 }
3218 if lb.len() <= tb.len() {
3219 let l0 = lb[0].to_ascii_lowercase();
3220 'scan: for s in 0..=(tb.len() - lb.len()) {
3221 if tb[s].to_ascii_lowercase() != l0 {
3222 continue;
3223 }
3224 for k in 1..lb.len() {
3225 if tb[s + k].to_ascii_lowercase() != lb[k].to_ascii_lowercase() {
3226 continue 'scan;
3227 }
3228 }
3229 return true;
3230 }
3231 }
3232 return false;
3233 }
3234 // Non-ASCII under (#i): general matcher below.
3235 }
3236 }
3237 let mut state = rpat::new();
3238 // c:Src/pattern.c:2334 — `patflags = prog->flags;` C copies the
3239 // prog's flags into the file-static `patflags` so subsequent
3240 // patmatch calls can read PAT_NOTSTART / PAT_NOTEND inside the
3241 // P_ISSTART / P_ISEND arms (c:3393, 3397). Rust mirrors this via
3242 // the `patflags` AtomicI32 at pattern.rs:217.
3243 //
3244 // Without this propagation, `${str:#(#s)PAT}` / `${str:#PAT(#e)}`
3245 // anchor checks fired at slice-local offset 0 / slice-local end
3246 // regardless of whether the caller meant the slice was an
3247 // interior chunk of a larger string (PAT_NOTSTART/PAT_NOTEND
3248 // bits exist on the prog precisely so callers can signal this).
3249 patflags.store(prog.0.flags, Ordering::Relaxed);
3250 // Pass the prog's GLOB flags (GF_* + `(#aN)` budget byte) to the
3251 // matcher. C threads `patglobflags` as a per-thread file-static;
3252 // the Rust port carries it through a fn param. The PAT_* flags
3253 // (PAT_STATIC etc.) stay on `prog.0.flags` for the outer
3254 // anchor/PURES checks at lines below.
3255 // c:Src/pattern.c — `if (prog->flags & PAT_ANY) { ret = 1; } else { ... }`.
3256 // Optimisation for a single "*" (the `**` complist node compiles its
3257 // section as `patcompile(NULL, …|PAT_ANY, …)`): it always matches the
3258 // whole string without running the bytecode. The PAT_NOGLD leading-dot
3259 // rejection below still applies (the "except for no_glob_dots" caveat).
3260 // Without this the empty `PAT_ANY` program was matched literally and
3261 // only matched the empty string, so `**` descended into nothing.
3262 let (mut ok, matched_end) = if (prog.0.flags & PAT_ANY as i32) != 0 {
3263 (true, trial.len())
3264 } else {
3265 match patmatch(&prog.1, 0, trial, 0, &mut state, prog.0.globflags) {
3266 Some(end_pos) => {
3267 // c:2438 — `if (matched && !(prog->flags & (PAT_NOANCH|PAT_NOTEND))) ...`
3268 let no_anchor = (prog.0.flags & (PAT_NOANCH | PAT_NOTEND) as i32) != 0;
3269 (no_anchor || end_pos == trial.len(), end_pos)
3270 }
3271 None => (false, 0),
3272 }
3273 };
3274 // c:2399-2406 — for files (PAT_NOGLD), a successful match is rejected
3275 // when the string starts with '.' (unless glob_dots is set, in which
3276 // case parsecomplist omits PAT_NOGLD). Honoring this here lets the
3277 // glob scanner rely on the matcher for the leading-dot skip, exactly
3278 // as C does, instead of an explicit check in the walker. Inert for
3279 // non-file patterns (matchpat / [[ ]] / :# never set PAT_NOGLD).
3280 if ok
3281 && (prog.0.flags & PAT_NOGLD as i32) != 0
3282 && trial.starts_with('.')
3283 && prog.0.patstartch != b'.'
3284 {
3285 // c:Src/pattern.c — NOGLD rejects a leading-dot file UNLESS the
3286 // pattern itself explicitly begins with a literal `.` (so `.*`
3287 // matches dot files while `*` does not). C bakes this into the
3288 // compiled bytecode; the Rust matcher carries the signal on
3289 // `patstartch` (c:1610) and applies the exception here.
3290 ok = false;
3291 }
3292 if ok {
3293 // c:2508 — `patinlen = patinput - patinstart;` — record the
3294 // byte-length of the successful match so `patmatchlen()` can
3295 // return it after the strings/state are torn down. `end_pos`
3296 // is the in-trial byte offset where patmatch stopped, which
3297 // is `patinput - patinstart` in C terms.
3298 patinlen.store(matched_end as i32, Ordering::Relaxed);
3299 }
3300 if ok {
3301 let n = (prog.0.patnpar as usize).min(NSUBEXP);
3302 let have_nump = nump.is_some();
3303 if let Some(np) = nump {
3304 *np = n as i32;
3305 }
3306 // c:2526-2542 — GF_MATCHREF (`(#m)`): on success, write the
3307 // matched substring to $MATCH and its 1-based (KSHARRAYS-
3308 // aware) char span to $MBEGIN/$MEND. C does this INSIDE
3309 // pattryrefs; the previous Rust port left it to a bridge
3310 // wrapper that sniffed the pattern TEXT for "(#m)" — wrong
3311 // mechanism (missed hoisted/nested flag positions) and
3312 // wrong layer.
3313 // c:2526 — `if ((patglobflags & GF_MATCHREF) && !(patflags & PAT_FILE))`.
3314 // The source is the GLOBAL patglobflags (c:273), not `prog->globflags`.
3315 // They are not the same thing: c:Src/zsh.h:1606 documents the struct
3316 // field as "globbing flags to set at START", so it keeps whatever the
3317 // pattern opened with, while the global is what patgetglobflags leaves
3318 // after walking every flag — including a later `(#M)` turning
3319 // GF_MATCHREF back off (c:1099-1100).
3320 //
3321 // Reading the struct field made `(#M)` inert: for all three of
3322 // `(#m)a*`, `(#m)(#M)a*` and `(#M)(#m)a*` it is 0x1800, whereas the
3323 // global correctly ends at 0x800 / 0x0 / 0x800. So `(#m)(#M)a*` still
3324 // wrote $MATCH where zsh leaves it unset.
3325 let cur_globflags = patglobflags.load(Ordering::Relaxed);
3326 if (cur_globflags & GF_MATCHREF) != 0 && (prog.0.flags & PAT_FILE as i32) == 0 {
3327 let hi = matched_end.min(trial.len());
3328 let mstr: String = trial[..hi].to_string(); // c:2536 metafy(patinstart..patinput)
3329 let mlen = mstr.chars().count() as i64; // c:2534 CHARSUB
3330 let base: i64 = if crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHARRAYS) {
3331 0
3332 } else {
3333 1
3334 };
3335 crate::ported::params::setsparam("MATCH", &mstr); // c:2537
3336 crate::ported::params::setiparam("MBEGIN", patoffset as i64 + base); // c:2538
3337 crate::ported::params::setiparam("MEND", mlen + patoffset as i64 + base - 1);
3338 // c:2539-2541
3339 }
3340 // c:2425+ — emit captured offsets to begp/endp out-arrays.
3341 // Check the CLOSE bit (high stripe at NSUBEXP+i) since the
3342 // group is only fully captured after its P_CLOSE fired; the
3343 // matching open-bit `1 << i` would be set after P_OPEN even
3344 // when the rest of the match later fails to reach P_CLOSE.
3345 //
3346 // c:Src/pattern.c:2556-2558 — `*begp++ = CHARSUB(patinstart, *sp) + patoffset;`.
3347 // `patoffset` is the start position of `string` within the
3348 // ORIGINAL string the caller was matching against (used for
3349 // paramsubst's sliding-window scan via `${var/PAT/REPL}`). Add
3350 // it to every reported beg/end so MBEGIN/MEND / `$match` see
3351 // positions relative to the original string, not the local
3352 // trial slice.
3353 // c:2556-2566 — `*begp++ = …` writes into the caller's FIXED-SIZE array
3354 // (C: `int begpos[MAX_POS]`), filling the first `n` slots and leaving
3355 // the array's length unchanged. Write IN PLACE (extend only if the
3356 // caller's Vec is shorter than `n`) rather than clear()+push — clearing
3357 // shrank a MAX_POS-length array down to `n`, so the caller's
3358 // [n..MAX_POS] reset loop (complist.rs getcol, c:631) indexed past the
3359 // end and panicked (`index 4 on len 4`) on patterns with < MAX_POS
3360 // backrefs.
3361 if let Some(bv) = begp {
3362 for i in 0..n {
3363 let close_bit = 1u32 << (i + NSUBEXP);
3364 let val = if (state.captures_set & close_bit) != 0 {
3365 state.patbeginp[i] as i32 + patoffset
3366 } else {
3367 -1 // c:2563-2566 — unset group (unmatched alternation branch)
3368 };
3369 if i < bv.len() {
3370 bv[i] = val;
3371 } else {
3372 bv.push(val);
3373 }
3374 }
3375 }
3376 if let Some(ev) = endp {
3377 for i in 0..n {
3378 let close_bit = 1u32 << (i + NSUBEXP);
3379 let val = if (state.captures_set & close_bit) != 0 {
3380 state.patendp[i] as i32 + patoffset
3381 } else {
3382 -1 // c:2565
3383 };
3384 if i < ev.len() {
3385 ev[i] = val;
3386 } else {
3387 ev.push(val);
3388 }
3389 }
3390 }
3391 // c:2570-2621 — `else if (prog->patnpar && !(patflags &
3392 // PAT_FILE))`: the caller passed NO capture arrays, so
3393 // pattryrefs itself publishes the `(#b)` groups as the
3394 // $match / $mbegin / $mend arrays. This arm was missing
3395 // from the port — every plain pattry of a (#b) pattern
3396 // silently dropped its captures (the bridge compensated
3397 // with a wrapper; now C-faithful at the source).
3398 if !have_nump && n > 0 && (prog.0.flags & PAT_FILE as i32) == 0 {
3399 let base: i32 = if crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHARRAYS) {
3400 0
3401 } else {
3402 1
3403 };
3404 let mut match_arr: Vec<String> = Vec::with_capacity(n);
3405 let mut begin_arr: Vec<String> = Vec::with_capacity(n);
3406 let mut end_arr: Vec<String> = Vec::with_capacity(n);
3407 for i in 0..n {
3408 let close_bit = 1u32 << (i + NSUBEXP);
3409 if (state.captures_set & close_bit) != 0 {
3410 let b = state.patbeginp[i];
3411 let e = state.patendp[i];
3412 let lo = b.min(trial.len());
3413 let hi = e.min(trial.len()).max(lo);
3414 match_arr.push(trial[lo..hi].to_string()); // c:2587 metafy(*sp..*ep)
3415 begin_arr.push((b as i32 + patoffset + base).to_string()); // c:2596-2599
3416 // c:2601-2604 — mend = last matched char index
3417 // (inclusive): end + offset + base - 1.
3418 end_arr.push((e as i32 + patoffset + base - 1).to_string());
3419 } else {
3420 // c:2607-2613 — unmatched branch / hashed paren.
3421 match_arr.push(String::new());
3422 begin_arr.push("-1".to_string());
3423 end_arr.push("-1".to_string());
3424 }
3425 }
3426 crate::ported::params::setaparam("match", match_arr); // c:2619
3427 crate::ported::params::setaparam("mbegin", begin_arr); // c:2620
3428 crate::ported::params::setaparam("mend", end_arr); // c:2621
3429 }
3430 }
3431 ok
3432}
3433
3434// =====================================================================
3435// 15. Range matching — pattern.c:3856, :4004, :3610, :3767
3436// =====================================================================
3437
3438/// Direct port of `int patmatchlen(void)` from `Src/pattern.c:2649`.
3439///
3440/// ```c
3441/// /**/
3442/// int
3443/// patmatchlen(void)
3444/// {
3445/// return patinlen;
3446/// }
3447/// ```
3448///
3449/// Returns the length in metafied bytes of the last successful
3450/// `pattry` / `pattryrefs` match. `patinlen` is set at
3451/// `pattern.c:2508` (`patinlen = patinput - patinstart`); the
3452/// Rust port sets the equivalent `patinlen` AtomicI32 at the end
3453/// of `pattryrefs` (see `pattern.rs::pattryrefs`).
3454pub fn patmatchlen() -> i32 {
3455 // c:2649-2652
3456 patinlen.load(Ordering::Relaxed) // c:2651 — `return patinlen;`
3457}
3458/// Direct port of `int patmatchindex(char *range, int ind, int *chr, int *mtp)`
3459/// from `Src/pattern.c:4004` (MULTIBYTE_SUPPORT-disabled single-byte
3460/// variant). Walks a NULL-terminated, METAFIED, PP_*-encoded byte
3461/// stream `range` and returns the character (or POSIX-class id)
3462/// at index `ind`. Output:
3463/// - `Some((Some(ch), mtp))` — literal character or PP_RANGE hit;
3464/// `chr = ch`, `mtp = 0`.
3465/// - `Some((None, mtp))` — POSIX class match (PP_ALPHA etc);
3466/// `chr = -1` in C, `None` in Rust; `mtp = class id`.
3467/// - `None` — `ind` exceeds the descriptor's length.
3468///
3469/// Encoding the byte stream uses:
3470/// * literal byte `< 0x83` = match char as itself
3471/// * `Meta(0x83)` + (byte ^ 0x20) = ordinary metafied character
3472/// * `Meta + PP_*` (0x84..) = POSIX class marker
3473/// * `Meta + PP_RANGE` = next two metafied chars are `lo, hi`
3474pub fn patmatchindex(range: &[u8], mut ind: i32) -> Option<(Option<u8>, i32)> {
3475 // c:4004-4081
3476 let mut chr: Option<u8> = None; // c:4014 — `*chr = -1`
3477 let mut mtp: i32 = 0; // c:4015 — `*mtp = 0`
3478
3479 // C `UNMETA(range)` + `METACHARINC(range)` macro pair from
3480 // Src/zsh.h:1796-1797 — decode-and-advance one metafied
3481 // character. Returned as `(decoded_byte, byte_advance)`.
3482 let unmeta = |bytes: &[u8], i: usize| -> (u8, usize) {
3483 if i < bytes.len() && bytes[i] == Meta && i + 1 < bytes.len() {
3484 (bytes[i + 1] ^ 0x20, 2)
3485 } else if i < bytes.len() {
3486 (bytes[i], 1)
3487 } else {
3488 (0, 0)
3489 }
3490 };
3491
3492 let mut i = 0usize;
3493 while i < range.len() {
3494 // c:4017 — `for (; *range; range++)`
3495 let b = range[i];
3496 if crate::ported::utils::imeta_byte(b) {
3497 // c:4018 — `if (imeta((unsigned char) *range))`
3498 // c:4019 — `int swtype = (unsigned char) *range - (unsigned char) Meta;`
3499 let swtype = (b as i32) - (Meta as i32);
3500 match swtype {
3501 0 => {
3502 // c:4021-4028 — `case 0: ordinary metafied character`
3503 // c:4023 — `rchr = (unsigned char) *++range ^ 32;`
3504 i += 1;
3505 if i >= range.len() {
3506 break;
3507 }
3508 let rchr = range[i] ^ 0x20;
3509 if ind == 0 {
3510 // c:4024-4026 — `if (!ind) { *chr = rchr; return 1; }`
3511 chr = Some(rchr);
3512 return Some((chr, mtp));
3513 }
3514 // c:4027 — falls through to `if (!ind--) break;`
3515 }
3516 t if (PP_ALPHA..=PP_INVALID).contains(&t) => {
3517 // c:4030-4051 — POSIX class markers PP_ALPHA..PP_INVALID
3518 if ind == 0 {
3519 // c:4052-4054 — `if (!ind) { *mtp = swtype; return 1; }`
3520 mtp = swtype;
3521 return Some((None, mtp));
3522 }
3523 }
3524 t if t == PP_RANGE => {
3525 // c:4057-4069 — PP_RANGE: next two metafied chars
3526 // `range++; r1 = UNMETA(range); METACHARINC(range);
3527 // r2 = UNMETA(range); if (*range == Meta) range++;`
3528 i += 1;
3529 if i >= range.len() {
3530 break;
3531 }
3532 let (r1, adv1) = unmeta(range, i);
3533 i += adv1;
3534 if i >= range.len() {
3535 break;
3536 }
3537 let (r2, adv2) = unmeta(range, i);
3538 i += adv2;
3539 let rdiff = r2 as i32 - r1 as i32;
3540 if rdiff >= ind {
3541 // c:4063-4067 — `if (rdiff >= ind) { *chr = r1 + ind; return 1; }`
3542 chr = Some((r1 as i32 + ind) as u8);
3543 return Some((chr, mtp));
3544 }
3545 // c:4068 — `ind -= rdiff;` (extra decrement happens via
3546 // the `ind--` below, accounting for the C comment
3547 // "note the extra decrement to ind below").
3548 ind -= rdiff;
3549 // Skip the trailing `if (!ind--) break;` decrement
3550 // for this branch since C's loop already does it.
3551 }
3552 _ => {
3553 // c:4070-4076 — PP_UNKWN / default → DPUTS bug warn
3554 // (unreachable in well-formed compiled output; bail).
3555 }
3556 }
3557 } else {
3558 // c:4079-4082 — literal char path
3559 if ind == 0 {
3560 // c:4080-4082 — `if (!ind) { *chr = (unsigned char) *range; return 1; }`
3561 chr = Some(b);
3562 return Some((chr, mtp));
3563 }
3564 }
3565 // c:4087 — `if (!ind--) break;` — pre-decrement-check.
3566 if ind == 0 {
3567 break;
3568 }
3569 ind -= 1;
3570 i += 1; // c:4017 — `range++`
3571 }
3572 // c:4091 — `/* No corresponding index. */ return 0;`
3573 None
3574}
3575
3576/// Direct port of `int mb_patmatchrange(char *range, wchar_t ch,
3577/// int zmb_ind, wint_t *indptr, int *mtp)` from `Src/pattern.c:3610`.
3578/// Multibyte variant of `patmatchrange`: walks the metafied,
3579/// PP_*-encoded byte stream `range` and tests whether wide char `ch`
3580/// is in it. `indptr` (when Some) accumulates the running index for
3581/// `mb_patmatchindex`-side lookup; `mtp` (when Some) records which
3582/// PP_* class fired.
3583///
3584/// `zmb_ind` is the `ZMB_*` multibyte-completion state from the
3585/// caller (typically the pattry input pos): ZMB_INCOMPLETE /
3586/// ZMB_INVALID feed the PP_INCOMPLETE / PP_INVALID branches.
3587///
3588/// The Rust port delegates `iswalpha` / `iswdigit` / `wcsitype` to
3589/// `is_alphabetic()` / `is_numeric()` / etc. on the decoded `char`,
3590/// matching the C `iswXXX(wchar_t)` semantics.
3591pub fn mb_patmatchrange(
3592 range: &[u8],
3593 ch: char,
3594 zmb_ind: i32,
3595 mut indptr: Option<&mut u32>,
3596 mut mtp: Option<&mut i32>,
3597) -> bool {
3598 // c:3610-3766
3599 if let Some(p) = indptr.as_deref_mut() {
3600 *p = 0; // c:3615-3616 — `if (indptr) *indptr = 0;`
3601 }
3602 // C `UNMETA(s)` + `METACHARINC(s)` macro pair (Src/zsh.h).
3603 let unmeta = |bytes: &[u8], i: usize| -> (u8, usize) {
3604 if i < bytes.len() && bytes[i] == Meta && i + 1 < bytes.len() {
3605 (bytes[i + 1] ^ 0x20, 2)
3606 } else if i < bytes.len() {
3607 (bytes[i], 1)
3608 } else {
3609 (0, 0)
3610 }
3611 };
3612 let mut i = 0usize;
3613 while i < range.len() {
3614 // c:3623 — `while (*range)`
3615 let b = range[i];
3616 if crate::ported::utils::imeta_byte(b) {
3617 // c:3624 — `if (imeta((unsigned char) *range))`
3618 // c:3625 — `swtype = (unsigned char) *range++ - (unsigned char) Meta;`
3619 let swtype = (b as i32) - (Meta as i32);
3620 i += 1;
3621 if let Some(p) = mtp.as_deref_mut() {
3622 *p = swtype; // c:3626-3627 — `if (mtp) *mtp = swtype;`
3623 }
3624 let class_hit = match swtype {
3625 0 => {
3626 // c:3629-3634 — `case 0: ordinary metafied character`
3627 i -= 1; // c:3631 — `range--;`
3628 let (decoded, adv) = unmeta(range, i);
3629 i += adv;
3630 decoded == (ch as u32) as u8 && (ch as u32) < 256
3631 }
3632 t if t == PP_ALPHA => ch.is_alphabetic(),
3633 t if t == PP_ALNUM => ch.is_alphanumeric(),
3634 t if t == PP_ASCII => (ch as u32) < 128,
3635 t if t == PP_BLANK => ch == ' ' || ch == '\t',
3636 t if t == PP_CNTRL => ch.is_control(),
3637 t if t == PP_DIGIT => ch.is_ascii_digit(),
3638 t if t == PP_GRAPH => !ch.is_whitespace() && !ch.is_control(),
3639 t if t == PP_LOWER => ch.is_lowercase(),
3640 t if t == PP_PRINT => !ch.is_control(),
3641 t if t == PP_PUNCT => ch.is_ascii_punctuation(),
3642 t if t == PP_SPACE => ch.is_whitespace(),
3643 t if t == PP_UPPER => ch.is_uppercase(),
3644 t if t == PP_XDIGIT => ch.is_ascii_hexdigit(),
3645 t if t == PP_IDENT => {
3646 // c:3704 — `PP_IDENT: if (wcsitype(ch, IIDENT)) return 1;`
3647 // IIDENT = alphanumeric, underscore, or "$" depending on
3648 // option state. Conservative: ASCII identifier chars.
3649 ch.is_alphanumeric() || ch == '_'
3650 }
3651 t if t == PP_IFS => {
3652 // c:3708 — `wcsitype(ch, ISEP)`. ISEP = space, tab,
3653 // newline + user IFS additions. Default IFS in zsh
3654 // is ` \t\n`.
3655 ch == ' ' || ch == '\t' || ch == '\n' || ch == '\0'
3656 }
3657 t if t == PP_IFSSPACE => {
3658 // c:3712-3713 — ASCII-only IFS-space.
3659 (ch as u32) < 128 && (ch == ' ' || ch == '\t' || ch == '\n')
3660 }
3661 t if t == PP_WORD => {
3662 // c:3716 — `wcsitype(ch, IWORD)`. IWORD = WORDCHARS-derived;
3663 // conservative ASCII alphanumeric + `_`.
3664 ch.is_alphanumeric() || ch == '_'
3665 }
3666 t if t == PP_RANGE => {
3667 // c:3719-3735 — PP_RANGE: two metafied wide chars are
3668 // `r1` and `r2`; matches if `r1 <= ch <= r2`.
3669 let (r1_byte, adv1) = unmeta(range, i);
3670 i += adv1;
3671 let (r2_byte, adv2) = unmeta(range, i);
3672 i += adv2;
3673 let r1 = r1_byte as u32;
3674 let r2 = r2_byte as u32;
3675 let chu = ch as u32;
3676 if r1 <= chu && chu <= r2 {
3677 if let Some(p) = indptr.as_deref_mut() {
3678 *p += chu - r1; // c:3725-3726
3679 }
3680 return true; // c:3727
3681 }
3682 if let Some(p) = indptr.as_deref_mut() {
3683 if r1 < r2 {
3684 *p += r2 - r1; // c:3733-3734
3685 }
3686 }
3687 false
3688 }
3689 t if t == PP_INCOMPLETE => {
3690 // c:3740 — `if (zmb_ind == ZMB_INCOMPLETE) return 1;`
3691 zmb_ind == ZMB_INCOMPLETE
3692 }
3693 t if t == PP_INVALID => {
3694 // c:3744 — `if (zmb_ind == ZMB_INVALID) return 1;`
3695 zmb_ind == ZMB_INVALID
3696 }
3697 _ => {
3698 // c:3747-3753 — PP_UNKWN / default → DPUTS bug warn
3699 false
3700 }
3701 };
3702 if class_hit {
3703 return true;
3704 }
3705 } else {
3706 // c:3757-3762 — literal-char path
3707 let (decoded, adv) = unmeta(range, i);
3708 i += adv;
3709 if decoded == (ch as u32) as u8 && (ch as u32) < 256 {
3710 if let Some(p) = mtp.as_deref_mut() {
3711 *p = 0; // c:3760
3712 }
3713 return true; // c:3761
3714 }
3715 }
3716 if let Some(p) = indptr.as_deref_mut() {
3717 *p += 1; // c:3764-3765 — `if (indptr) (*indptr)++;`
3718 }
3719 }
3720 false // c:3766 — `return 0;`
3721}
3722
3723/// Direct port of `int mb_patmatchindex(char *range, wint_t ind,
3724/// wint_t *chr, int *mtp)` from `Src/pattern.c:3767`. The reverse
3725/// of `mb_patmatchrange`: given a metafied byte range and an index
3726/// `ind` into it, return the character (or PP_* class) at that
3727/// position.
3728///
3729/// Returns:
3730/// - `Some((Some(ch), 0))` — literal or PP_RANGE hit (chr = ch).
3731/// - `Some((None, mtp))` — POSIX class match; chr = WEOF / None.
3732/// - `None` — index out of range.
3733pub fn mb_patmatchindex(range: &[u8], mut ind: u32) -> Option<(Option<char>, i32)> {
3734 // c:3767-3849
3735 let mut chr: Option<char> = None; // c:3776 — `*chr = WEOF`
3736 let mut mtp: i32 = 0; // c:3777 — `*mtp = 0`
3737
3738 // C `UNMETA(s)` + `METACHARINC(s)` macro pair (Src/zsh.h).
3739 let unmeta = |bytes: &[u8], i: usize| -> (u8, usize) {
3740 if i < bytes.len() && bytes[i] == Meta && i + 1 < bytes.len() {
3741 (bytes[i + 1] ^ 0x20, 2)
3742 } else if i < bytes.len() {
3743 (bytes[i], 1)
3744 } else {
3745 (0, 0)
3746 }
3747 };
3748
3749 let mut i = 0usize;
3750 while i < range.len() {
3751 // c:3779 — `while (*range)`
3752 let b = range[i];
3753 if crate::ported::utils::imeta_byte(b) {
3754 // c:3780 — `if (imeta((unsigned char) *range))`
3755 let swtype = (b as i32) - (Meta as i32);
3756 i += 1;
3757 match swtype {
3758 0 => {
3759 // c:3782-3789 — `case 0: ordinary metafied char`
3760 i -= 1;
3761 let (decoded, adv) = unmeta(range, i);
3762 i += adv;
3763 let rchr = decoded as char;
3764 if ind == 0 {
3765 chr = Some(rchr);
3766 return Some((chr, mtp));
3767 }
3768 }
3769 t if (PP_ALPHA..=PP_INVALID).contains(&t) => {
3770 // c:3791-3812 — POSIX class markers
3771 if ind == 0 {
3772 mtp = swtype;
3773 return Some((None, mtp));
3774 }
3775 }
3776 t if t == PP_RANGE => {
3777 // c:3814-3829 — PP_RANGE: two metafied wide chars
3778 let (r1_byte, adv1) = unmeta(range, i);
3779 i += adv1;
3780 let (r2_byte, adv2) = unmeta(range, i);
3781 i += adv2;
3782 let r1 = r1_byte as u32;
3783 let r2 = r2_byte as u32;
3784 let rdiff = r2.saturating_sub(r1);
3785 if rdiff >= ind {
3786 chr = char::from_u32(r1 + ind);
3787 return Some((chr, mtp));
3788 }
3789 ind = ind.saturating_sub(rdiff);
3790 }
3791 _ => {
3792 // c:3831-3837 — PP_UNKWN / default → DPUTS
3793 }
3794 }
3795 } else {
3796 // c:3840-3843 — literal byte path
3797 if ind == 0 {
3798 chr = Some(b as char);
3799 return Some((chr, mtp));
3800 }
3801 }
3802 // c:3846 — `if (!ind--) break;`
3803 if ind == 0 {
3804 break;
3805 }
3806 ind -= 1;
3807 i += 1;
3808 }
3809 None // c:3849 — `return 0`
3810}
3811
3812/// Port of `freepatprog(Patprog prog)` from `Src/pattern.c:4161`. Frees a Patprog.
3813/// Rust's `Drop` on `Box<patprog>` handles this; the explicit fn
3814/// exists for C parity (Rule A).
3815#[allow(unused_variables)]
3816pub fn freepatprog(prog: Patprog) {} // c:4161
3817
3818/// Port of `int pat_enables(const char *cmd, char **patp, int enable)`
3819/// from `Src/pattern.c:4171`. Implements `enable -p`/`disable -p`: with
3820/// an empty `patp`, prints the currently enabled (or disabled, if
3821/// `!enable`) tokens by walking `zpc_strings[]`/`zpc_disables[]` in
3822/// lockstep. Otherwise toggles each named token's `zpc_disables[i]`
3823/// slot, emitting `invalid pattern: NAME` for misses.
3824pub fn pat_enables(cmd: &str, patp: &[&str], enable: bool) -> i32 {
3825 // c:4171
3826 let mut ret: i32 = 0; // c:4173
3827 if patp.is_empty() {
3828 // c:4177 !*patp
3829 let strings = ZPC_STRINGS; // c:4179 zpc_strings
3830 let disp = zpc_disables.lock().unwrap(); // c:4179 zpc_disables
3831 let mut done = false; // c:4178
3832 let mut out: String = String::new();
3833 for i in 0..(ZPC_COUNT as usize) {
3834 // c:4180
3835 let sp = match strings[i] {
3836 // c:4182 !*stringp
3837 Some(s) => s,
3838 None => continue,
3839 };
3840 let is_disabled = disp[i] != 0;
3841 if enable == is_disabled {
3842 // c:4184 enable?*disp:!*disp
3843 continue;
3844 }
3845 if done {
3846 // c:4186
3847 out.push(' '); // c:4187
3848 }
3849 out.push_str(&format!("'{}'", sp)); // c:4188
3850 done = true; // c:4189
3851 }
3852 if done {
3853 // c:4191
3854 println!("{}", out); // c:4187-4192
3855 }
3856 return 0; // c:4193
3857 }
3858 for p in patp {
3859 // c:4196
3860 let strings = ZPC_STRINGS;
3861 let mut disp = zpc_disables.lock().unwrap();
3862 let mut matched = false;
3863 for i in 0..(ZPC_COUNT as usize) {
3864 // c:4197
3865 if let Some(s) = strings[i] {
3866 if s == *p {
3867 // c:4200 !strcmp
3868 disp[i] = if enable { 0u8 } else { 1u8 }; // c:4201 *disp = !enable
3869 matched = true;
3870 break; // c:4202
3871 }
3872 }
3873 }
3874 if !matched {
3875 // c:4205
3876 zerrnam(cmd, &format!("invalid pattern: {}", p)); // c:4206
3877 ret = 1; // c:4207
3878 }
3879 }
3880 ret // c:4211
3881}
3882
3883/// Port of `mod_export const char *zpc_strings[ZPC_COUNT]` from
3884/// `Src/pattern.c:258`. Static token-name table indexed by ZPC_*;
3885/// NULL entries (ZPC_NULL, ZPC_BNULLKEEP, ZPC_INPAR_PIPE,
3886/// ZPC_KSHCHAR) have no user-visible name.
3887pub const ZPC_STRINGS: [Option<&'static str>; ZPC_COUNT as usize] = [
3888 // c:258
3889 None,
3890 None,
3891 Some("|"),
3892 None,
3893 Some("~"),
3894 Some("("),
3895 Some("?"),
3896 Some("*"),
3897 Some("["),
3898 Some("<"),
3899 Some("^"),
3900 Some("#"),
3901 None,
3902 Some("?("),
3903 Some("*("),
3904 Some("+("),
3905 Some("!("),
3906 Some("\\!("),
3907 Some("@("),
3908];
3909
3910/// Port of `unsigned int savepatterndisables(void)` from
3911/// `Src/pattern.c:4220`.
3912///
3913/// C body (c:4220-4233):
3914/// ```c
3915/// unsigned int disables, bit;
3916/// char *disp;
3917/// disables = 0;
3918/// for (bit = 1, disp = zpc_disables;
3919/// disp < zpc_disables + ZPC_COUNT;
3920/// bit <<= 1, disp++) {
3921/// if (*disp) disables |= bit;
3922/// }
3923/// return disables;
3924/// ```
3925///
3926/// Encodes the current `zpc_disables\[ZPC_COUNT\]` byte-array as a u32
3927/// bitmask (one bit per slot, low bit = `zpc_disables\[0\]`).
3928/// The previous Rust port returned a `Vec<String>` clone of
3929/// `patterndisables` (a completely different data structure — names
3930/// list, not the per-token byte array). `restorepatterndisables(u32)`
3931/// at c:4258 reads this bitmask back into `zpc_disables`, so the
3932/// returned shape MUST be the u32 bitmask.
3933pub fn savepatterndisables() -> u32 {
3934 // c:4220
3935 let disp = zpc_disables.lock().unwrap(); // c:4225 disp = zpc_disables
3936 let mut disables: u32 = 0; // c:4224
3937 let mut bit: u32 = 1; // c:4226 bit = 1
3938 for i in 0..(ZPC_COUNT as usize) {
3939 // c:4226-4228
3940 if disp[i] != 0 {
3941 // c:4230
3942 disables |= bit; // c:4231
3943 }
3944 bit <<= 1; // c:4226 bit <<= 1
3945 }
3946 disables // c:4232
3947}
3948
3949/// Port of `void startpatternscope(void)` from `Src/pattern.c:4241`.
3950/// Pushes a frame onto `PATSCOPE_STACK` (`zpc_disables_stack` in C)
3951/// carrying the current `zpc_disables[]` state as a `savepatterndisables()`
3952/// u32 bitmap. Called at function entry; `endpatternscope` pops it.
3953///
3954/// ```c
3955/// void startpatternscope(void) {
3956/// Zpc_disables_save newdis = zalloc(sizeof(*newdis));
3957/// newdis->next = zpc_disables_stack;
3958/// newdis->disables = savepatterndisables(); // c:4247
3959/// zpc_disables_stack = newdis;
3960/// }
3961/// ```
3962pub fn startpatternscope() {
3963 // c:4241
3964 let saved = savepatterndisables(); // c:4247
3965 PATSCOPE_STACK.with(|s| s.borrow_mut().push(saved));
3966}
3967
3968/// Port of `void restorepatterndisables(unsigned int disables)` from
3969/// `Src/pattern.c:4258`. Walks the 12-slot `zpc_disables[]` array,
3970/// setting each slot's byte from the bitmask: `disables & (1<<i)`
3971/// → slot `i` gets 1, else 0.
3972/// ```c
3973/// void
3974/// restorepatterndisables(unsigned int disables)
3975/// {
3976/// char *disp;
3977/// unsigned int bit;
3978/// for (bit = 1, disp = zpc_disables;
3979/// disp < zpc_disables + ZPC_COUNT;
3980/// bit <<= 1, disp++) {
3981/// if (disables & bit) *disp = 1;
3982/// else *disp = 0;
3983/// }
3984/// }
3985/// ```
3986pub fn restorepatterndisables(disables: u32) {
3987 // c:4258
3988 let mut disp = zpc_disables.lock().unwrap(); // c:4263
3989 let mut bit: u32 = 1;
3990 for i in 0..(ZPC_COUNT as usize) {
3991 // c:4263-4265
3992 if (disables & bit) != 0 {
3993 // c:4266
3994 disp[i] = 1; // c:4267
3995 } else {
3996 disp[i] = 0; // c:4269
3997 }
3998 bit <<= 1;
3999 }
4000}
4001
4002/// Port of `void endpatternscope(void)` from `Src/pattern.c:4279`.
4003/// Pops the saved bitmap from `PATSCOPE_STACK` (`zpc_disables_stack`
4004/// in C); restores `zpc_disables[]` from the bitmap ONLY when
4005/// `isset(LOCALPATTERNS)` per C c:4286. Called at function exit.
4006///
4007/// ```c
4008/// void endpatternscope(void) {
4009/// Zpc_disables_save olddis = zpc_disables_stack;
4010/// zpc_disables_stack = olddis->next;
4011/// if (isset(LOCALPATTERNS))
4012/// restorepatterndisables(olddis->disables); // c:4287
4013/// zfree(olddis, sizeof(*olddis));
4014/// }
4015/// ```
4016pub fn endpatternscope() {
4017 // c:4279
4018 if let Some(prev) = PATSCOPE_STACK.with(|s| s.borrow_mut().pop()) {
4019 if isset(crate::ported::zsh_h::LOCALPATTERNS) {
4020 // c:4286-4287
4021 restorepatterndisables(prev);
4022 }
4023 }
4024}
4025
4026/// Port of `void clearpatterndisables(void)` from `Src/pattern.c:4296`.
4027/// C body: `memset(zpc_disables, 0, ZPC_COUNT)` — zero every slot.
4028pub fn clearpatterndisables() {
4029 // c:4296
4030 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize]; // c:4298
4031}
4032
4033/// Port of `haswilds(char *str)` from `Src/pattern.c:4306`.
4034///
4035/// Check whether `str` is eligible for filename generation.
4036///
4037/// C scans a TOKENIZED string: every input reaching it has been
4038/// through the lexer or `tokenize()`/`shtokenize()` (glob.c:3548/
4039/// 3563), so glob metachars are token codes and escaped/quoted
4040/// chars are Bnull'd literals. Callers holding un-tokenized strings
4041/// must `tokenize()` a copy first — exactly what C does for
4042/// runtime-built strings (compcore.c:2231 tokenizes fignore entries
4043/// immediately before its c:2235 haswilds call). The switch matches
4044/// ONLY token codes, never literal ASCII metachars.
4045///
4046/// The walk is over chars: zshrs strings hold token chars as
4047/// codepoints (Star = U+0087) and the input layer is char-domain
4048/// (input.rs `shingetline`/`ingetc`), so the char is the unit that
4049/// the metafied byte is in C. Scanning bytes here false-positived
4050/// on UTF-8 continuation bytes of plain text (`↔` = E2 86 94
4051/// carries 0x86/0x94 = Hat/Inang as u8) — bug #627.
4052///
4053/// The C source's `%?foo` job-ref special case (c:4317-4318), which
4054/// mutates `str[1]` in place to demote the `?`, becomes a "skip
4055/// position 1" scan adjustment here since `&str` is immutable.
4056pub fn haswilds(str: &str) -> bool {
4057 // c:4306
4058 let chars: Vec<char> = str.chars().collect();
4059 let len = chars.len();
4060 if len == 0 {
4061 return false;
4062 }
4063
4064 // c:4309 — `[' and `]' are legal even if bad patterns are usually not.
4065 if len == 1 && (chars[0] == Inbrack || chars[0] == Outbrack) {
4066 return false; // c:4311
4067 }
4068
4069 // c:4313-4315 — If % is immediately followed by ?, then that ? is
4070 // not treated as a wildcard. This is so you don't have
4071 // to escape job references such as %?foo.
4072 let skip_pos_1 = len >= 2 && chars[0] == '%' && chars[1] == Quest; // c:4316-4317
4073
4074 // c:4319-4321 — Note that at this point zpc_special has not been set up.
4075 let disp = zpc_disables.lock().unwrap(); // c:read zpc_disables[]
4076
4077 for i in 0..len {
4078 // c:4323 for (; *str; str++)
4079 if skip_pos_1 && i == 1 {
4080 continue; // c:4317 `str[1] = '?'` demote
4081 }
4082 let c = chars[i]; // c:4324 switch (*str)
4083 let prev: char = if i > 0 { chars[i - 1] } else { '\0' }; // c: str[-1]
4084
4085 if c == Inpar {
4086 // c:4325-4335
4087 if (!isset(SHGLOB) && disp[ZPC_INPAR as usize] == 0)
4088 || (i > 0
4089 && isset(KSHGLOB)
4090 && ((prev == Quest && disp[ZPC_KSH_QUEST as usize] == 0)
4091 || (prev == Star && disp[ZPC_KSH_STAR as usize] == 0)
4092 || (prev == '+' && disp[ZPC_KSH_PLUS as usize] == 0)
4093 || (prev == Bang && disp[ZPC_KSH_BANG as usize] == 0)
4094 || (prev == '!' && disp[ZPC_KSH_BANG2 as usize] == 0)
4095 || (prev == '@' && disp[ZPC_KSH_AT as usize] == 0)))
4096 {
4097 return true; // c:4335
4098 }
4099 } else if c == Bar {
4100 if disp[ZPC_BAR as usize] == 0 {
4101 return true; // c:4340
4102 }
4103 } else if c == Star {
4104 if disp[ZPC_STAR as usize] == 0 {
4105 return true; // c:4345
4106 }
4107 } else if c == Inbrack {
4108 if disp[ZPC_INBRACK as usize] == 0 {
4109 return true; // c:4350
4110 }
4111 } else if c == Inang {
4112 if disp[ZPC_INANG as usize] == 0 {
4113 return true; // c:4355
4114 }
4115 } else if c == Quest {
4116 if disp[ZPC_QUEST as usize] == 0 {
4117 return true; // c:4360
4118 }
4119 } else if c == Pound {
4120 if isset(EXTENDEDGLOB) && disp[ZPC_HASH as usize] == 0 {
4121 return true; // c:4365
4122 }
4123 } else if c == Hat {
4124 if isset(EXTENDEDGLOB) && disp[ZPC_HAT as usize] == 0 {
4125 return true; // c:4370
4126 }
4127 }
4128 }
4129 false // c:4374
4130}
4131
4132// =====================================================================
4133// 4. struct patprog — zsh.h:1601
4134// =====================================================================
4135
4136/// `typedef struct patprog *Patprog;` from `zsh.h:542`.
4137#[allow(non_camel_case_types)]
4138/// `typedef struct patprog *Patprog;` from `zsh.h:542`.
4139///
4140/// C zsh allocates the `struct patprog` header + bytecode as one
4141/// contiguous `malloc` block, accessing bytecode via
4142/// `(char *)prog + prog->startoff`. Rust has no flexible array
4143/// members, so this typedef pairs the C-exact `patprog` header
4144/// (zsh_h.rs:768) with an owned bytecode `Vec<u8>` — header at
4145/// `.0`, bytecode at `.1`. `startoff`/`size` index into `.1`.
4146pub type Patprog = Box<(patprog, Vec<u8>)>;
4147// =====================================================================
4148// Bytecode field offsets within each instruction.
4149//
4150// Instruction layout in `patout`:
4151// +0 u8 opcode
4152// +1..+5 u32 LE next_off (offset of next instr in chain, 0 = end)
4153// +5.. u8... opcode-specific payload
4154//
4155// C uses a different layout (Upat union = 4-byte or 8-byte slots);
4156// the Rust port pins the layout to byte offsets for portability.
4157// =====================================================================
4158
4159const I_OP: usize = 0; // opcode byte
4160
4161impl rpat {
4162 pub fn new() -> Self {
4163 Self {
4164 wbranch_visits: std::collections::HashMap::new(),
4165 patbeginp: [usize::MAX; NSUBEXP],
4166 patendp: [0; NSUBEXP],
4167 captures_set: 0,
4168 errsfound: 0, // c:2066 — errsfound = 0 at pattry init
4169 }
4170 }
4171}
4172const I_NEXT: usize = 1; // u32 next-offset starts here
4173const I_BODY: usize = 5; // payload starts here
4174
4175/// Serialises every entry into `patcompile`. The C source at
4176/// `Src/pattern.c:267-281` declares `patout`, `patparse`, `patstart`,
4177/// `patnpar`, `patflags`, `patglobflags`, `errsfound`, `forceerrs`,
4178/// `zpc_special`, `patstrcache` as file-scope statics that the compile
4179/// mutates in sequence; zsh-the-program is single-threaded so the C
4180/// source is safe under that invariant. zshrs callers (zutil's
4181/// `style_table::get` via `crate::ported::pattern::patmatch`, params.rs,
4182/// subst.rs, options.rs) can invoke `patcompile` from concurrent test
4183/// threads, so the lock restores the single-writer invariant. Held
4184/// only for the compile phase; the matcher (`pattry`/`patmatch`)
4185/// operates on the returned `Patprog.code` and touches no globals.
4186static PATCOMPILE_LOCK: Mutex<()> = Mutex::new(());
4187
4188// C: `static char *patparse, *patstart;` — pattern parsing cursors
4189// into the *input* pattern string. patstart points to start of
4190// pattern; patparse moves forward as we consume tokens.
4191pub static patparse: Mutex<String> = Mutex::new(String::new()); // c:269
4192pub static patstart: Mutex<String> = Mutex::new(String::new()); // c:269
4193
4194/// Position within `patparse` we're currently looking at. C source
4195/// uses a `char *` cursor; Rust uses byte offset into the String.
4196pub static patparse_off: AtomicUsize = AtomicUsize::new(0);
4197
4198// C: `static int errsfound;` — approximate-match error count.
4199pub static errsfound: AtomicI32 = AtomicI32::new(0); // c:274
4200
4201// C: `static int forceerrs;` — required error count for approximate match.
4202pub static forceerrs: AtomicI32 = AtomicI32::new(-1); // c:275
4203
4204// C: `static long patglobflags_orig;` — saved at branch entry.
4205pub static patglobflags_orig: AtomicI32 = AtomicI32::new(0); // c:276
4206
4207// C: `static const char *zpc_special;` — table of currently-special
4208// characters during compile (indexed by ZPC_*).
4209//
4210// pattern.c uses `static char zpc_special[ZPC_COUNT];` and resets it
4211// in patcompcharsset(). Rust mirrors as a Mutex-wrapped byte array.
4212pub static zpc_special: Mutex<[u8; ZPC_COUNT as usize]> = Mutex::new([0u8; ZPC_COUNT as usize]); // c:278
4213
4214// C: `static char *patstrcache;` — caches the unmetafied trial string.
4215// Rust port has no Meta encoding so the cache is unnecessary; we leave
4216// the static declared for parity (Rule A — name exists in C).
4217pub static patstrcache: Mutex<String> = Mutex::new(String::new()); // c:281
4218
4219/// `Marker` constant — alias for `crate::ported::zsh_h::Marker`
4220/// (`Src/zsh.h:224` `#define Marker ((char) 0xa2)`).
4221///
4222/// The previous Rust port had this as `pub const Marker: u8 = 0x80`
4223/// which is WRONG — `\200` (0x80) is NOT the canonical Marker byte.
4224/// C's Marker is 0xa2 per `Src/zsh.h:224`. No in-tree callers used
4225
4226/// Port of `static const char *colon_stuffs[]` from `Src/pattern.c:1134-1138`.
4227/// 19 entries matching C's table in declaration order, so `range_type(name)`
4228/// returns the same `(index + PP_FIRST)` value C does:
4229/// alpha=1, alnum=2, ascii=3, blank=4, cntrl=5, digit=6, graph=7, lower=8,
4230/// print=9, punct=10, space=11, upper=12, xdigit=13, IDENT=14, IFS=15,
4231/// IFSSPACE=16, WORD=17, INCOMPLETE=18, INVALID=19. Prior Rust port had
4232/// only 12 lowercase entries and was MISSING `ascii` between `alnum` and
4233/// `blank`, so `range_type("digit")` returned 5 instead of C's PP_DIGIT=6
4234/// and every `[:class:]` byte marker emitted by `complete.rs:733`
4235/// (`0x80 + ch`) was off-by-one for classes after `alnum`. Real port bug.
4236const POSIX_CLASS_NAMES: &[&str] = &[
4237 "alpha",
4238 "alnum",
4239 "ascii",
4240 "blank",
4241 "cntrl",
4242 "digit",
4243 "graph",
4244 "lower",
4245 "print",
4246 "punct",
4247 "space",
4248 "upper",
4249 "xdigit",
4250 "IDENT",
4251 "IFS",
4252 "IFSSPACE",
4253 "WORD",
4254 "INCOMPLETE",
4255 "INVALID",
4256];
4257
4258/// Port of file-static `zpc_disables_stack` from `Src/pattern.c:4244`.
4259/// Per-evaluator function-scope disable save-stack (bucket-1: each
4260/// worker thread parses/executes its own function calls, so each must
4261/// have its own scope stack). Reason for `thread_local!` over `Mutex`:
4262/// in zsh C this is a per-process file-static; in zshrs each worker
4263/// thread is its own evaluator — TLS preserves the per-evaluator
4264/// semantic without serializing across workers.
4265///
4266/// Element type: u32 bitmap matching `savepatterndisables` return
4267/// shape (c:4220 `unsigned int disables`). Prior Rust port used
4268/// `Vec<Vec<String>>` and `startpatternscope` cloned a `Vec<String>`
4269/// of names instead of the bitmap — a real port bug that made
4270/// `setopt LOCALPATTERNS` function entry/exit silently fail to save/
4271/// restore the disable state.
4272thread_local! {
4273 static PATSCOPE_STACK: std::cell::RefCell<Vec<u32>> =
4274 const { std::cell::RefCell::new(Vec::new()) };
4275}
4276
4277// =====================================================================
4278// 17. Module-loader / disable mgmt — pattern.c:4161-4296
4279// =====================================================================
4280
4281// `pub static patterndisables: Mutex<Vec<String>>` deleted — was a
4282// dead tombstone with no callers outside the buggy
4283// `startpatternscope` / `endpatternscope` / `clearpatterndisables`
4284// fns (which cloned/cleared it instead of operating on the real
4285// `zpc_disables` byte array). The canonical zsh `zpc_disables[ZPC_COUNT]`
4286// (Src/pattern.c:268) is the disable state; the names list does not
4287// exist as a separate data structure in C.
4288
4289/// Port of `char zpc_disables[ZPC_COUNT]` from `Src/pattern.c:268`.
4290/// Per-token disable byte — when `zpc_disables[i]` is non-zero, the
4291/// pattern token at index `i` (ZPC_*) is treated as a literal,
4292/// not its meta-meaning.
4293pub static zpc_disables: Mutex<[u8; ZPC_COUNT as usize]> = Mutex::new([0u8; ZPC_COUNT as usize]); // c:268
4294
4295/// Helper: when patinsert shifts a chunk of bytecode, any 4-byte
4296/// next_off slot that previously pointed past `opnd` must be bumped
4297/// by `delta` to keep the chain links valid.
4298///
4299/// Walks the buffer linearly opcode-by-opcode reading I_NEXT slots.
4300/// Conservatively adjusts every nonzero next that lands past opnd.
4301fn fixup_offsets_after_insert(buf: &mut [u8], opnd: usize, delta: u32) {
4302 let mut i = 0;
4303 while i + I_BODY <= buf.len() {
4304 let op = buf[i + I_OP];
4305 if op == 0 {
4306 i += 1;
4307 continue;
4308 } // sentinel byte, skip
4309 let next_bytes = &buf[i + I_NEXT..i + I_NEXT + 4];
4310 let cur = u32::from_le_bytes(next_bytes.try_into().unwrap());
4311 if cur != 0 {
4312 let abs = cur as usize;
4313 if abs >= opnd && abs <= buf.len() {
4314 let new = cur + delta;
4315 buf[i + I_NEXT..i + I_NEXT + 4].copy_from_slice(&new.to_le_bytes());
4316 }
4317 }
4318 i = advance_past_instr(buf, i);
4319 if i == 0 {
4320 break;
4321 }
4322 }
4323}
4324
4325/// Helper: given a buffer and current opcode offset, return the
4326/// offset of the next opcode after this one's payload.
4327///
4328/// Encodes the per-opcode payload size table — must stay in sync
4329/// with patnode/patinsert calls in the compiler.
4330fn advance_past_instr(buf: &[u8], pos: usize) -> usize {
4331 if pos + I_BODY > buf.len() {
4332 return 0;
4333 }
4334 let op = buf[pos + I_OP];
4335 let body_start = pos + I_BODY;
4336 match op {
4337 P_END | P_NOTHING | P_BACK | P_EXCSYNC | P_EXCEND | P_ISSTART | P_ISEND | P_COUNTSTART
4338 | P_ANY | P_STAR | P_NUMANY => body_start,
4339 P_GFLAGS => body_start + 4, // i32 flag-bits payload
4340 P_EXACTLY => {
4341 // payload: u32 len + len bytes
4342 if body_start + 4 > buf.len() {
4343 return 0;
4344 }
4345 let len =
4346 u32::from_le_bytes(buf[body_start..body_start + 4].try_into().unwrap()) as usize;
4347 body_start + 4 + len
4348 }
4349 P_ANYOF | P_ANYBUT => {
4350 if body_start + 4 > buf.len() {
4351 return 0;
4352 }
4353 let len =
4354 u32::from_le_bytes(buf[body_start..body_start + 4].try_into().unwrap()) as usize;
4355 body_start + 4 + len
4356 }
4357 P_ONEHASH | P_TWOHASH | P_BRANCH => body_start,
4358 // c:113 P_WBRANCH and c:114-115 P_EXCLUDE/P_EXCLUDP carry an
4359 // 8-byte syncptr payload (one Upat slot in C) right after
4360 // their header, before the chained operand.
4361 P_WBRANCH | P_EXCLUDE | P_EXCLUDP => body_start + 8,
4362 P_OPEN..=0x88 | P_CLOSE..=0x98 => body_start,
4363 P_NUMRNG => body_start + 16, // two i64
4364 P_NUMFROM | P_NUMTO => body_start + 8,
4365 P_COUNT => body_start + 16, // min i64 + max i64; operand inline follows
4366 _ => body_start,
4367 }
4368}
4369
4370/// Helper: directly set the `next_off` slot of the instruction at
4371/// `pos` without walking the chain. C uses pointer arithmetic
4372/// (`scanp->l = ...`) inline; Rust factors it for byte-offset
4373/// bookkeeping. Architectural helper.
4374fn set_next(pos: usize, val: usize) {
4375 let mut buf = patout.lock().unwrap();
4376 if pos + I_NEXT + 4 <= buf.len() {
4377 buf[pos + I_NEXT..pos + I_NEXT + 4].copy_from_slice(&(val as u32).to_le_bytes());
4378 }
4379}
4380
4381/// Helper: walk every branch's operand chain and patch each branch's
4382/// last-operand-node `.next` to `target`. Used to chain a fully-
4383/// compiled alternation switch to whatever opcode follows (P_END for
4384/// the outermost compile, P_CLOSE_N for a sub-group).
4385///
4386/// Architectural helper — C uses pattail inside the BRANCH operand
4387/// scope via Upat pointer arithmetic; Rust factors it for clarity.
4388fn chain_branches_to(starter: usize, target: usize) {
4389 let mut cur = starter;
4390 loop {
4391 // Operand starts at cur + I_BODY (the byte right after this
4392 // branch's header). Walk operand's next-chain to its end
4393 // and set its .next = target.
4394 pattail(cur + I_BODY, target);
4395 // Move to next alternative.
4396 let buf = patout.lock().unwrap();
4397 if cur + I_NEXT + 4 > buf.len() {
4398 break;
4399 }
4400 let nb: [u8; 4] = buf[cur + I_NEXT..cur + I_NEXT + 4].try_into().unwrap();
4401 let n = u32::from_le_bytes(nb) as usize;
4402 drop(buf);
4403 if n == 0 {
4404 break;
4405 }
4406 cur = n;
4407 }
4408}
4409
4410/// Port of `patmatch(Upat prog)` from `Src/pattern.c:2694`. The interpreter.
4411///
4412/// Returns `Some(end_pos)` on successful match (end_pos = byte offset
4413/// in `string` where match ended), `None` on no-match. The state
4414/// param tracks captures.
4415///
4416/// Rust signature differs from C's `int patmatch(Upat prog)`: input
4417/// bytecode, current input position, captures, and glob flags are
4418/// threaded through args rather than C's per-thread file-statics
4419/// (`patinput`, `patinstart`, `patbeginp`/`patendp`, `patglobflags`).
4420/// Rule S1 deviation justified by zshrs's threading model — see
4421/// PORT_PLAN.md Bucket 1.
4422///
4423/// WARNING: NOT IN PATTERN.C — Rust-only helper. Approximate-match
4424/// inner loop for `(#aN)`-flagged P_EXACTLY arms. C interleaves the
4425/// edit-operation backtracking inline within the P_EXACTLY case
4426/// (c:2737-2779); the Rust port factors the inner per-byte walk +
4427/// recursive sub/ins/del trials into this helper so the linear
4428/// patmatch loop body stays manageable. Both writer and reader live
4429/// in pattern.rs.
4430///
4431/// Walks pattern bytes `str_bytes` against `input_bytes[s_off..]`.
4432/// On exact-match per byte: advance both. On mismatch: try the 3
4433/// edit operations in order (substitute = advance both + errsfound++,
4434/// insert = advance pat + errsfound++, delete = advance input +
4435/// errsfound++); each recurses via `patmatch` to continue with the
4436/// rest of the bytecode at `next`. Returns the matched-end byte
4437/// offset in `string` on success, None on failure.
4438fn approx_match_exactly(
4439 code: &[u8],
4440 next: usize,
4441 string: &str,
4442 s_off: usize,
4443 str_bytes: &[u8],
4444 state: &mut rpat,
4445 glob_flags: i32,
4446 max_errs: i32,
4447) -> Option<usize> {
4448 let input_bytes = string.as_bytes();
4449 // Try matching the EXACT prefix as far as it lines up; on first
4450 // mismatch (or out-of-input), branch into the edit-operation
4451 // trials. This is a bounded recursive search; the budget caps
4452 // recursion depth at `max_errs - state.errsfound`.
4453 fn walk(
4454 code: &[u8],
4455 next: usize,
4456 string: &str,
4457 input_bytes: &[u8],
4458 str_bytes: &[u8],
4459 s_off: usize,
4460 p_off: usize,
4461 state: &mut rpat,
4462 glob_flags: i32,
4463 max_errs: i32,
4464 ) -> Option<usize> {
4465 // Direction terminology: `s_off+1` consumes one INPUT byte,
4466 // `p_off+1` consumes one PATTERN byte.
4467 // - (s+1, p+1) = substitute one input byte for one pat byte
4468 // - (s+1, p) = consume input byte that's NOT in pattern
4469 // (= INSERTION in input compared to pattern)
4470 // - (s, p+1) = consume pat byte that's NOT in input
4471 // (= DELETION from input compared to pattern)
4472 //
4473 // Tries every option at each step, returns the path producing
4474 // the LARGEST end s_off (most input consumed). Anchored
4475 // callers (pattry) need full-input consumption; non-anchored
4476 // accept any. C handles this via bytecode-level branching
4477 // backtrack; the Rust port collapses it into a recursive
4478 // tree-search with per-attempt state save/restore.
4479 let mut best: Option<usize> = None;
4480 let saved_outer = state.clone();
4481 let mut update = |best: &mut Option<usize>, cand: Option<usize>| {
4482 if let Some(c) = cand {
4483 if best.map(|b| c > b).unwrap_or(true) {
4484 *best = Some(c);
4485 }
4486 }
4487 };
4488 if p_off == str_bytes.len() {
4489 // Pattern body consumed. Three paths to try; pick the
4490 // one with most input consumed:
4491 // (a) terminate here at s_off, run continuation.
4492 // (b) absorb 1+ trailing input as INSERTION-IN-INPUT edits.
4493 let terminate = if next == 0 {
4494 Some(s_off)
4495 } else {
4496 patmatch(code, next, string, s_off, state, glob_flags)
4497 };
4498 update(&mut best, terminate);
4499 // Path (b): absorb trailing input as insertion edits.
4500 if s_off < input_bytes.len() && state.errsfound < max_errs {
4501 *state = saved_outer.clone();
4502 state.errsfound += 1;
4503 let r = walk(
4504 code,
4505 next,
4506 string,
4507 input_bytes,
4508 str_bytes,
4509 s_off + 1,
4510 p_off,
4511 state,
4512 glob_flags,
4513 max_errs,
4514 );
4515 update(&mut best, r);
4516 }
4517 *state = saved_outer;
4518 return best;
4519 }
4520 // c:Src/pattern.c:2676 CHARMATCH — inline case-aware byte
4521 // compare (the C macro, not a fn). Honors GF_IGNCASE /
4522 // GF_LCMATCHUC so under `(#i)` a case-only difference is NOT an
4523 // edit: `(#ia2)readme` matches `READXME` (only X→M costs an
4524 // error). Raw byte `==` counted every case difference as an edit.
4525 let charmatch_inline = |chin: u8, chpa: u8| -> bool {
4526 chin == chpa
4527 || ((glob_flags & GF_IGNCASE) != 0
4528 && chin.to_ascii_lowercase() == chpa.to_ascii_lowercase())
4529 || ((glob_flags & GF_LCMATCHUC) != 0
4530 && chpa.is_ascii_lowercase()
4531 && chpa.to_ascii_uppercase() == chin)
4532 };
4533 // Exact-byte match — try advancing both.
4534 if s_off < input_bytes.len() && charmatch_inline(input_bytes[s_off], str_bytes[p_off]) {
4535 *state = saved_outer.clone();
4536 let r = walk(
4537 code,
4538 next,
4539 string,
4540 input_bytes,
4541 str_bytes,
4542 s_off + 1,
4543 p_off + 1,
4544 state,
4545 glob_flags,
4546 max_errs,
4547 );
4548 update(&mut best, r);
4549 }
4550 // Edit operations — each costs 1 error.
4551 if state.errsfound < max_errs {
4552 // c:Src/pattern.c:3520-3543 — Damerau transposition: swap two
4553 // adjacent input chars to match two adjacent pat chars (i.e.
4554 // input[s..s+2] reversed equals pat[p..p+2]). Costs 1 edit;
4555 // pin for `(#a3)abcd` matching "dcba" — needs sub + transp +
4556 // sub = 3 edits to bridge the reversal.
4557 if s_off + 1 < input_bytes.len()
4558 && p_off + 1 < str_bytes.len()
4559 && charmatch_inline(input_bytes[s_off], str_bytes[p_off + 1])
4560 && charmatch_inline(input_bytes[s_off + 1], str_bytes[p_off])
4561 {
4562 *state = saved_outer.clone();
4563 state.errsfound += 1;
4564 let r = walk(
4565 code,
4566 next,
4567 string,
4568 input_bytes,
4569 str_bytes,
4570 s_off + 2,
4571 p_off + 2,
4572 state,
4573 glob_flags,
4574 max_errs,
4575 );
4576 update(&mut best, r);
4577 }
4578 // Substitute.
4579 if s_off < input_bytes.len() {
4580 *state = saved_outer.clone();
4581 state.errsfound += 1;
4582 let r = walk(
4583 code,
4584 next,
4585 string,
4586 input_bytes,
4587 str_bytes,
4588 s_off + 1,
4589 p_off + 1,
4590 state,
4591 glob_flags,
4592 max_errs,
4593 );
4594 update(&mut best, r);
4595 }
4596 // Insertion in input (skip input byte only).
4597 if s_off < input_bytes.len() {
4598 *state = saved_outer.clone();
4599 state.errsfound += 1;
4600 let r = walk(
4601 code,
4602 next,
4603 string,
4604 input_bytes,
4605 str_bytes,
4606 s_off + 1,
4607 p_off,
4608 state,
4609 glob_flags,
4610 max_errs,
4611 );
4612 update(&mut best, r);
4613 }
4614 // Deletion from input (skip pattern byte only).
4615 *state = saved_outer.clone();
4616 state.errsfound += 1;
4617 let r = walk(
4618 code,
4619 next,
4620 string,
4621 input_bytes,
4622 str_bytes,
4623 s_off,
4624 p_off + 1,
4625 state,
4626 glob_flags,
4627 max_errs,
4628 );
4629 update(&mut best, r);
4630 }
4631 *state = saved_outer;
4632 best
4633 }
4634 walk(
4635 code,
4636 next,
4637 string,
4638 input_bytes,
4639 str_bytes,
4640 s_off,
4641 0,
4642 state,
4643 glob_flags,
4644 max_errs,
4645 )
4646}
4647
4648thread_local! {
4649 /// Rust-only backstop counter (no C analogue) — gates `patmatch`
4650 /// recursion at PATMATCH_MAX_DEPTH so misbehaving closures convert
4651 /// would-be stack overflows into clean None returns. Documented in
4652 /// `fake_fn_allowlist.txt` under the patmatch arc.
4653 static PATMATCH_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
4654}
4655
4656/// Maximum patmatch recursion depth — Rust-only backstop, no C
4657/// analogue. The C source's primary protection against
4658/// closure-of-closure infinite recursion is the P_WBRANCH "must
4659/// match at least 1 char" semantics described at pattern.c:108-144:
4660/// `P_WBRANCH: This works like a branch and is used in complex`
4661/// `closures, but the match must be at least 1 char in length to`
4662/// `avoid infinite loops.`
4663/// (Implemented in C's patmatch() switch at c:3044, `case P_WBRANCH`,
4664/// via the `errsfound`/`forceerrs` accounting at c:1059+.) The Rust
4665/// port's P_WBRANCH arm has gaps in that propagation, so we keep
4666/// this depth guard as a safety net.
4667///
4668/// **Tuned to test-thread stack size**, NOT the main-thread 8 MB.
4669/// Rust's `cargo test` spawns each test on a thread whose default
4670/// stack is ~2 MB (per `std::thread::Builder::stack_size` default at
4671/// `library/std/src/thread/mod.rs`), and `(fo#)#` patterns blow that
4672/// at ~250-300 frames per measured SIGABRT. 128 leaves headroom
4673/// (~256 KB worst case at ~2 KB/frame) while still admitting any
4674/// legitimate zsh pattern (Misc/globtests tops out around 30).
4675const PATMATCH_MAX_DEPTH: u32 = 128;
4676
4677thread_local! {
4678 // c:Src/pattern.c — the P_EXCLUDE `syncptr->p` heap buffer. Keyed by
4679 // the EXCLUDE node offset; value is a per-input-position byte array
4680 // recording where the asserted branch's excludable part matched (so
4681 // EXCSYNC can fail a revisit and force backtracking to a different
4682 // split). C stores this in the bytecode payload + a raw heap pointer
4683 // that survives backtracking; the Rust matcher clones `rpat` per
4684 // branch, so the buffer lives here (outside the cloned state) to
4685 // stay shared across recursion, exactly like C's global syncstrp->p.
4686 static EXCSYNC_BUF: std::cell::RefCell<std::collections::HashMap<usize, Vec<u8>>> =
4687 std::cell::RefCell::new(std::collections::HashMap::new());
4688}
4689
4690pub fn patmatch(
4691 code: &[u8],
4692 prog_off: usize,
4693 string: &str,
4694 string_off: usize,
4695 state: &mut rpat,
4696 glob_flags: i32,
4697) -> Option<usize> {
4698 // c:2694
4699 // Depth-guard: convert what would be a stack overflow into a
4700 // clean None return (= no match). See PATMATCH_MAX_DEPTH doc.
4701 let d = PATMATCH_DEPTH.with(|c| {
4702 let cur = c.get();
4703 c.set(cur + 1);
4704 cur + 1
4705 });
4706 if d > PATMATCH_MAX_DEPTH {
4707 PATMATCH_DEPTH.with(|c| c.set(c.get() - 1));
4708 return None;
4709 }
4710 struct DepthGuard;
4711 impl Drop for DepthGuard {
4712 fn drop(&mut self) {
4713 PATMATCH_DEPTH.with(|c| c.set(c.get().saturating_sub(1)));
4714 }
4715 }
4716 let _depth_guard = DepthGuard;
4717
4718 let mut scan = prog_off;
4719 let mut s_off = string_off;
4720 // Locally-mutable copy of glob_flags so mid-pattern P_GFLAGS can
4721 // toggle bits without affecting the caller's branch view.
4722 let mut glob_flags = glob_flags;
4723
4724 // Inlined port of CHARMATCH macro from `Src/pattern.c:2671-2677`:
4725 // #define CHARMATCH(chin, chpa) (chin == chpa || \
4726 // ((patglobflags & GF_IGNCASE) ?
4727 // ((ISUPPER(chin) ? TOLOWER(chin) : chin) ==
4728 // (ISUPPER(chpa) ? TOLOWER(chpa) : chpa)) :
4729 // (patglobflags & GF_LCMATCHUC) ?
4730 // (ISLOWER(chpa) && TOUPPER(chpa) == chin) : 0))
4731 //
4732 // - exact byte match always wins
4733 // - GF_IGNCASE: SYMMETRIC fold (both lowercase before compare)
4734 // - GF_LCMATCHUC: ASYMMETRIC — lowercase pattern char ALSO matches
4735 // uppercase text char; an UPPERCASE pattern char only matches
4736 // that exact uppercase text char. This is the `(#l)` flag's
4737 // "lowercase-in-pattern matches uppercase-in-text" semantic;
4738 // previously zshrs treated LCMATCHUC the same as IGNCASE (full
4739 // case-fold both sides) so `(#l)FOO` wrongly matched "foo".
4740 let charmatch = |chin: u8, chpa: u8, flags: i32| -> bool {
4741 if chin == chpa {
4742 return true; // c:2671
4743 }
4744 if (flags & GF_IGNCASE) != 0 {
4745 // c:2672-2674
4746 let a = if chin.is_ascii_uppercase() {
4747 chin.to_ascii_lowercase()
4748 } else {
4749 chin
4750 };
4751 let b = if chpa.is_ascii_uppercase() {
4752 chpa.to_ascii_lowercase()
4753 } else {
4754 chpa
4755 };
4756 return a == b;
4757 }
4758 if (flags & GF_LCMATCHUC) != 0 {
4759 // c:2675-2676
4760 return chpa.is_ascii_lowercase() && chpa.to_ascii_uppercase() == chin;
4761 }
4762 false // c:2677
4763 };
4764
4765 // Membership test for a P_ANYOF/P_ANYBUT body at `body_off` against the
4766 // input char at byte offset `off`. Returns (in_set, byte_advance). ASCII
4767 // input takes the unchanged byte `charmatch` path over the `chars` set; a
4768 // multibyte char is decoded (raw UTF-8, or a metafied `$'\xNN'` Meta-pair)
4769 // and tested against the appended class mask / wide ranges / wide literals
4770 // — C's `mb_patmatchrange` equivalent (pattern.c:3610).
4771 let anyof_membership = |body_off: usize, off: usize, gflags: i32| -> (bool, usize) {
4772 let range_flags = gflags & !(GF_IGNCASE | GF_LCMATCHUC);
4773 let chars_len =
4774 u32::from_le_bytes(code[body_off + 4..body_off + 8].try_into().unwrap()) as usize;
4775 let cs = body_off + 8;
4776 let set = &code[cs..cs + chars_len];
4777 let mut p = cs + chars_len;
4778 let classmask = u32::from_le_bytes(code[p..p + 4].try_into().unwrap());
4779 p += 4;
4780 let n_mbc = u32::from_le_bytes(code[p..p + 4].try_into().unwrap()) as usize;
4781 p += 4;
4782 let mbc_start = p;
4783 p += n_mbc * 4;
4784 let n_mbr = u32::from_le_bytes(code[p..p + 4].try_into().unwrap()) as usize;
4785 p += 4;
4786 let mbr_start = p;
4787
4788 let bytes = string.as_bytes();
4789 let b = bytes[off];
4790 // ASCII input, OR `unsetopt multibyte`: byte-level match (the prior
4791 // behaviour). C only takes the wide `mb_patmatchrange` path under
4792 // GF_MULTIBYTE; with it clear a high byte is matched as a raw byte.
4793 //
4794 // `off` can also land inside a multibyte char: P_STAR backtracks
4795 // byte-by-byte (`s_off + consumed`) and the approx omit-input paths
4796 // step `s_off + 1`, so a continuation P_ANYOF/P_ANYBUT can be tested
4797 // at a continuation byte. Decoding `string[off..]` there would panic,
4798 // so treat a non-boundary position as a raw byte (advance 1) — the
4799 // same fallback the byte-level machinery already relies on.
4800 if b < 0x80 || (gflags & GF_MULTIBYTE) == 0 || !string.is_char_boundary(off) {
4801 return (set.iter().any(|&c| charmatch(b, c, range_flags)), 1);
4802 }
4803 // Decode one logical input char + its source byte span.
4804 let mut it = string[off..].chars();
4805 let (ch, adv) = match it.next() {
4806 Some('\u{83}') => match it.next() {
4807 Some(n) => (
4808 ((n as u32 as u8) ^ 0x20) as char,
4809 '\u{83}'.len_utf8() + n.len_utf8(),
4810 ),
4811 None => ('\u{83}', 2),
4812 },
4813 Some(c) => (c, c.len_utf8()),
4814 None => return (false, 1),
4815 };
4816 let class_hit = (classmask & (1 << 0) != 0 && ch.is_alphabetic())
4817 || (classmask & (1 << 1) != 0 && ch.is_alphanumeric())
4818 || (classmask & (1 << 2) != 0 && ch.is_uppercase())
4819 || (classmask & (1 << 3) != 0 && ch.is_lowercase())
4820 || (classmask & (1 << 4) != 0 && ch.is_whitespace())
4821 || (classmask & (1 << 5) != 0 && (ch == ' ' || ch == '\t'))
4822 || (classmask & (1 << 6) != 0
4823 && !ch.is_alphanumeric()
4824 && !ch.is_whitespace()
4825 && !ch.is_control())
4826 || (classmask & (1 << 7) != 0 && ch.is_control())
4827 || (classmask & (1 << 8) != 0 && !ch.is_control())
4828 || (classmask & (1 << 9) != 0 && !ch.is_control() && !ch.is_whitespace());
4829 if class_hit {
4830 return (true, adv);
4831 }
4832 let cp = ch as u32;
4833 for k in 0..n_mbc {
4834 let o = mbc_start + k * 4;
4835 if u32::from_le_bytes(code[o..o + 4].try_into().unwrap()) == cp {
4836 return (true, adv);
4837 }
4838 }
4839 for k in 0..n_mbr {
4840 let o = mbr_start + k * 8;
4841 let lo = u32::from_le_bytes(code[o..o + 4].try_into().unwrap());
4842 let hi = u32::from_le_bytes(code[o + 4..o + 8].try_into().unwrap());
4843 if cp >= lo && cp <= hi {
4844 return (true, adv);
4845 }
4846 }
4847 (false, adv)
4848 };
4849
4850 while scan < code.len() {
4851 let op = code[scan + I_OP];
4852 let next_bytes: [u8; 4] = code[scan + I_NEXT..scan + I_NEXT + 4].try_into().unwrap();
4853 let next = u32::from_le_bytes(next_bytes) as usize;
4854
4855 match op {
4856 P_END => {
4857 // c:Src/pattern.c:3451-3454 — `case P_END: if (!(fail =
4858 // (patinput < patinend && !(patflags & PAT_NOANCH))))
4859 // return 1; break;`. When the WHOLE pattern is consumed
4860 // but the string ISN'T (anchored full match), this
4861 // branch FAILS so an enclosing alternation backtracks
4862 // to the next alternative — `[[ yes == (y|yes) ]]`
4863 // tries `y` (consumes 1 char, reaches P_END at offset
4864 // 1 ≠ 3), fails here, then tries `yes`. The previous
4865 // unconditional `Some(s_off)` committed to the first
4866 // (prefix) alternative and the external anchor check
4867 // rejected it without ever trying `yes`. PAT_NOANCH /
4868 // PAT_NOTEND callers (prefix/suffix strip, `(#e)`
4869 // interior matches) keep the partial-success return.
4870 let pf = patflags.load(Ordering::Relaxed);
4871 let anchored = (pf & (PAT_NOANCH | PAT_NOTEND) as i32) == 0;
4872 if s_off < string.len() && anchored {
4873 // c:Src/pattern.c:3451-3454 sets `fail = 1` here, then the
4874 // shared approximate-match block at c:3463-3475 deletes the
4875 // trailing input one CHARACTER at a time — `++errsfound;
4876 // CHARINC(patinput); continue;` — retrying P_END until the
4877 // input is gone or the `(#aN)` budget is spent. So
4878 // `[[ ab = (#a1)? ]]` matches: `?` consumes `a`, then the
4879 // leftover `b` is deleted for one error.
4880 //
4881 // Only literal (P_EXACTLY) runs did this before, inside
4882 // approx_match_exactly's own trailing-delete; a pattern
4883 // ending in a class / `?` / `*` with input left reached
4884 // here and simply failed, so `(#a2)[^0-9]` on `abc`
4885 // (match `a`, delete `bc`) was rejected where zsh accepts
4886 // it. The class/`?` arms already delete on a FAILED match
4887 // (e.g. c:5046); this is the same edit at the pattern end.
4888 let max_errs = (glob_flags & 0xff) as i32;
4889 if state.errsfound < max_errs {
4890 if let Some(c) = string[s_off..].chars().next() {
4891 state.errsfound += 1; // c:3466 ++errsfound
4892 // c:3475 CHARINC(patinput) + continue (retry P_END).
4893 return patmatch(
4894 code,
4895 scan,
4896 string,
4897 s_off + c.len_utf8(),
4898 state,
4899 glob_flags,
4900 );
4901 }
4902 }
4903 return None; // c:3452 fail, no budget → caller tries next alt
4904 }
4905 return Some(s_off); // c:3453
4906 }
4907 P_NOTHING => { /* empty match, just continue */ }
4908 P_BACK => { /* zero-width, walk back via next */ }
4909 P_EXCEND => {
4910 // c:Src/pattern.c:3037-3047 — terminal node ending an
4911 // exclusion operand: the exclusion matches iff the
4912 // (truncated) excludable span was fully consumed
4913 // (`patinput >= patinend`). Returns success here without
4914 // following the chain (which would wrongly continue into
4915 // the post-group pattern). The caller truncates the span
4916 // so `string.len()` IS the excludable end.
4917 if s_off >= string.len() {
4918 return Some(s_off);
4919 }
4920 return None;
4921 }
4922 P_EXCSYNC => {
4923 // c:Src/pattern.c:2992-3035 — record where the asserted
4924 // branch reached the EXCLUDE sync point (the end of the
4925 // excludable part) in the following EXCLUDE node's sync
4926 // buffer. If this position was already recorded (with
4927 // <= the current error count), fail so the asserted
4928 // branch backtracks to a different split. The EXCLUDE
4929 // node sits PHYSICALLY right after EXCSYNC (both
4930 // patcompnot and the `~` arm emit them adjacent).
4931 let exclude_node = scan + I_BODY;
4932 let already = EXCSYNC_BUF.with(|b| {
4933 let mut m = b.borrow_mut();
4934 if let Some(buf) = m.get_mut(&exclude_node) {
4935 if s_off < buf.len() {
4936 let cur = (state.errsfound + 1) as u8;
4937 if buf[s_off] != 0 && (state.errsfound + 1) >= buf[s_off] as i32 {
4938 return true; // c:3008 already matched here → fail
4939 }
4940 buf[s_off] = cur; // c:3017
4941 // c:3033 — earlier marks are now invalid.
4942 for x in buf[..s_off].iter_mut() {
4943 *x = 0;
4944 }
4945 }
4946 }
4947 false
4948 });
4949 if already {
4950 return None;
4951 }
4952 // else fall through to next node
4953 }
4954 P_EXACTLY => {
4955 // c:P_EXACTLY arm
4956 let body = scan + I_BODY;
4957 let len = u32::from_le_bytes(code[body..body + 4].try_into().unwrap()) as usize;
4958 let str_bytes = &code[body + 4..body + 4 + len];
4959 let input_bytes = string.as_bytes();
4960 // `(#aN)` budget — low byte of patglobflags. C uses the
4961 // file-static `patglobflags`; the Rust port carries it
4962 // through `glob_flags` since rpat went bucket-1.
4963 let max_errs = (glob_flags & 0xff) as i32;
4964 if max_errs > 0 {
4965 // Approximate match: try edit operations
4966 // (substitute/insert/delete) at each mismatch up to
4967 // the budget. Per c:3055/3109/3193 the C source
4968 // tracks `errsfound` across recursive patmatch
4969 // calls; the Rust port does the same via `state.
4970 // errsfound`. Skips transposition (Damerau extension
4971 // — rare; faithful follow-on).
4972 if let Some(new_off) = approx_match_exactly(
4973 code, next, string, s_off, str_bytes, state, glob_flags, max_errs,
4974 ) {
4975 return Some(new_off);
4976 }
4977 return None;
4978 }
4979 if s_off + len > input_bytes.len() {
4980 return None;
4981 }
4982 let case_flags = glob_flags & (GF_IGNCASE | GF_LCMATCHUC);
4983 let multibyte = (glob_flags & GF_MULTIBYTE) != 0; // c:349 GF_MULTIBYTE
4984 if case_flags != 0 {
4985 let inp_slice = &input_bytes[s_off..s_off + len];
4986 if multibyte && (glob_flags & GF_IGNCASE) != 0 {
4987 // Char-level Unicode case fold for IGNCASE only —
4988 // LCMATCHUC's asymmetric ASCII semantic doesn't
4989 // map cleanly to non-ASCII chars; per C the
4990 // CHARMATCH macro is byte-level anyway (TOLOWER/
4991 // TOUPPER from utils.c work on bytes).
4992 let pat_str = std::str::from_utf8(str_bytes).ok();
4993 let inp_str = std::str::from_utf8(inp_slice).ok();
4994 if let (Some(p), Some(i)) = (pat_str, inp_str) {
4995 let mut pc = p.chars();
4996 let mut ic = i.chars();
4997 loop {
4998 match (pc.next(), ic.next()) {
4999 (None, None) => break,
5000 (Some(_), None) | (None, Some(_)) => return None,
5001 (Some(a), Some(b)) => {
5002 // Equal chars (the common case) and ASCII
5003 // pairs fold without touching the heap.
5004 // The old per-char `to_lowercase()
5005 // .collect::<String>()` pair allocated
5006 // TWICE PER CHARACTER — `(#i)` scans over
5007 // a 566k-entry history (hsmw ^R) burned
5008 // ~4 CPU-minutes in RawVec::reserve.
5009 // Iterator::eq covers the multi-char
5010 // Unicode folds allocation-free.
5011 if a != b
5012 && (if a.is_ascii() && b.is_ascii() {
5013 a.to_ascii_lowercase()
5014 != b.to_ascii_lowercase()
5015 } else {
5016 !a.to_lowercase().eq(b.to_lowercase())
5017 })
5018 {
5019 return None;
5020 }
5021 }
5022 }
5023 }
5024 } else {
5025 // Non-UTF-8 input — byte fallback through CHARMATCH.
5026 for k in 0..len {
5027 if !charmatch(inp_slice[k], str_bytes[k], glob_flags) {
5028 return None;
5029 }
5030 }
5031 }
5032 } else {
5033 // c:2694 — per-byte CHARMATCH walk (covers
5034 // GF_IGNCASE byte-mode AND GF_LCMATCHUC asymmetric).
5035 for k in 0..len {
5036 if !charmatch(inp_slice[k], str_bytes[k], glob_flags) {
5037 return None;
5038 }
5039 }
5040 }
5041 } else if &input_bytes[s_off..s_off + len] != str_bytes {
5042 return None;
5043 }
5044 s_off += len;
5045 }
5046 P_ANY => {
5047 // c:P_ANY arm — `?` matches ONE character, advanced via
5048 // METACHARINC. zshrs stores `$'\xNN'` escapes metafied
5049 // (Meta `\u{83}` + byte^32); a multibyte character is
5050 // several Meta-pair chars that must be consumed as one,
5051 // else `?` matches a single metafied byte and leaves the
5052 // rest unmatched. Walk source chars, demetafying, until
5053 // one UTF-8 character forms; advance by its source span.
5054 // Identity for non-metafied (single-char advance).
5055 let s = &string[s_off..];
5056 let mut raw: Vec<u8> = Vec::new();
5057 let mut advance = 0usize;
5058 let mut chs = s.chars();
5059 while let Some(c) = chs.next() {
5060 if c == '\u{83}' {
5061 if let Some(n) = chs.clone().next() {
5062 if (n as u32) >= 0x80 {
5063 raw.push((n as u32 as u8) ^ 32);
5064 advance += c.len_utf8() + n.len_utf8();
5065 chs.next();
5066 if std::str::from_utf8(&raw).is_ok() {
5067 break;
5068 }
5069 continue;
5070 }
5071 }
5072 // Lone Meta — count it as one character.
5073 advance += c.len_utf8();
5074 break;
5075 }
5076 // Non-metafied char = one logical character.
5077 advance += c.len_utf8();
5078 break;
5079 }
5080 if advance == 0 {
5081 return None;
5082 }
5083 s_off += advance;
5084 }
5085 P_ANYOF => {
5086 // c:2780-2800 — a bracket expression is matched by
5087 // `patmatchrange` / `mb_patmatchrange` on the RAW input char.
5088 // Neither consults `patglobflags`, so a bracket NEVER
5089 // case-folds: only C's CHARMATCH macro (c:2671, used by
5090 // P_EXACTLY) honours GF_IGNCASE / GF_LCMATCHUC.
5091 //
5092 // Passing glob_flags through here made `(#i)` fold ranges as
5093 // well as literals, so `[[ FooBar = (#i)[a-z]## ]]` matched
5094 // (zsh: no match) and `[[ F = (#i)[[:lower:]] ]]` matched
5095 // (zsh: no match). Mask the case bits off for the set test.
5096 let body = scan + I_BODY;
5097 let input_bytes = string.as_bytes();
5098 let max_errs = (glob_flags & 0xff) as i32;
5099 let (has_match, adv) = if s_off < input_bytes.len() {
5100 anyof_membership(body, s_off, glob_flags)
5101 } else {
5102 (false, 0)
5103 };
5104 if !has_match {
5105 // c:Src/pattern.c:3463-3505 — approximate-match fail
5106 // handler. For non-P_EXACTLY opcodes, the ONLY
5107 // approximation path is "omit one input char" (skip
5108 // the input byte that didn't match, costing 1 edit,
5109 // then retry the same scan). For P_ANYOF that means
5110 // `[b][b]` against "bob" can match by omitting the
5111 // middle "o" with 1 edit — the `(#a1)[b][b]` pin.
5112 if state.errsfound < max_errs && s_off < input_bytes.len() {
5113 state.errsfound += 1;
5114 // Retry same scan with one input byte consumed.
5115 return patmatch(code, scan, string, s_off + 1, state, glob_flags);
5116 }
5117 return None;
5118 }
5119 s_off += adv;
5120 }
5121 P_ANYBUT => {
5122 let body = scan + I_BODY;
5123 let input_bytes = string.as_bytes();
5124 let max_errs = (glob_flags & 0xff) as i32;
5125 // c:2781-2800 — the negated bracket shares P_ANYOF's matcher
5126 // (`… ^ (P_OP(scan) == P_ANYOF)`), so it is equally
5127 // case-BLIND (anyof_membership masks the case bits off).
5128 let (in_set, adv) = if s_off < input_bytes.len() {
5129 anyof_membership(body, s_off, glob_flags)
5130 } else {
5131 (true, 0)
5132 };
5133 let has_match = s_off < input_bytes.len() && !in_set;
5134 if !has_match {
5135 if state.errsfound < max_errs && s_off < input_bytes.len() {
5136 // c:3463 — omit-input approx path (same as P_ANYOF).
5137 state.errsfound += 1;
5138 return patmatch(code, scan, string, s_off + 1, state, glob_flags);
5139 }
5140 return None;
5141 }
5142 s_off += adv;
5143 }
5144 P_STAR => {
5145 // c:P_STAR arm (greedy)
5146 // Greedy: try to match as many chars as possible then
5147 // backtrack until the rest matches.
5148 let input_bytes = string.as_bytes();
5149 let max = input_bytes.len() - s_off;
5150 let mut consumed = max;
5151 loop {
5152 let mut sub_state = state.clone();
5153 if let Some(end) = patmatch(
5154 code,
5155 next,
5156 string,
5157 s_off + consumed,
5158 &mut sub_state,
5159 glob_flags,
5160 ) {
5161 *state = sub_state;
5162 return Some(end);
5163 }
5164 if consumed == 0 {
5165 return None;
5166 }
5167 consumed -= 1;
5168 }
5169 }
5170 P_ONEHASH | P_TWOHASH => {
5171 // c:P_ONEHASH / P_TWOHASH
5172 // The operand (the simple atom being repeated) starts
5173 // at `scan + I_BODY` — that's the byte immediately
5174 // after the quantifier opcode (which has its own
5175 // 5-byte header). The repeated atom occupies the
5176 // bytes from there until `next`.
5177 let operand = scan + I_BODY;
5178 let min = if op == P_TWOHASH { 1 } else { 0 };
5179 // Greedy: match operand repeatedly until it fails,
5180 // then walk back trying continuations.
5181 let mut positions = vec![s_off];
5182 loop {
5183 let cur = *positions.last().unwrap();
5184 let mut sub_state = state.clone();
5185 if let Some(new_pos) =
5186 patmatch(code, operand, string, cur, &mut sub_state, glob_flags)
5187 {
5188 if new_pos == cur {
5189 break;
5190 } // zero-width fixed point
5191 *state = sub_state;
5192 positions.push(new_pos);
5193 } else {
5194 break;
5195 }
5196 }
5197 if positions.len() - 1 < min {
5198 return None;
5199 }
5200 // Walk back from longest match trying continuations.
5201 while positions.len() > min {
5202 let cur = *positions.last().unwrap();
5203 let mut sub_state = state.clone();
5204 if let Some(end) = patmatch(code, next, string, cur, &mut sub_state, glob_flags)
5205 {
5206 *state = sub_state;
5207 return Some(end);
5208 }
5209 if positions.len() <= min + 1 {
5210 return None;
5211 }
5212 positions.pop();
5213 }
5214 return None;
5215 }
5216 P_BRANCH | P_WBRANCH => {
5217 // c:P_BRANCH / P_WBRANCH arm — c:3043-3044 in zsh.
5218 // c:3046-3050 — if next is NOT another BRANCH, this is
5219 // the only alternative; avoid the alt-loop and just
5220 // continue with the operand inline (no recursion, no
5221 // fallthrough on failure).
5222 //
5223 // For P_WBRANCH, the operand sits AFTER the 8-byte
5224 // syncptr payload (`P_OPERAND(p) + 1` per pattern.c:1865).
5225 let operand_off_extra = if op == P_WBRANCH { 8 } else { 0 };
5226 // c:3056 — if `next` is P_EXCLUDE/P_EXCLUDP, this BRANCH
5227 // is the asserted half of a `^pat` / `!(pat)` exclusion.
5228 // Minimal port of c:3056-3201: try the asserted operand
5229 // (the `*`-based branch body), then for each EXCLUDE in
5230 // the next-chain run the exclude operand against the
5231 // same input range; if any exclude matches with the
5232 // SAME consumed length, fail; else succeed.
5233 if next != 0 && next < code.len() && P_ISEXCLUDE(code[next + I_OP]) {
5234 // c:Src/pattern.c:3056-3201 — `^pat` / `(^pat)` /
5235 // `!(pat)` / `A~B` exclusion. The asserted branch
5236 // (`STAR EXCSYNC rest` for `^`/`!`, or `A EXCSYNC
5237 // rest` for `A~B`) is matched normally; its EXCSYNC
5238 // node records where the EXCLUDABLE part ended into
5239 // the EXCLUDE node's sync buffer. We then truncate to
5240 // that synclen and test the exclusion operand(s); if
5241 // any matches the excludable span, this candidate is
5242 // excluded and we re-run the asserted branch — EXCSYNC
5243 // now fails at the recorded position, forcing a
5244 // different split — until an un-excluded split is
5245 // found or the asserted branch can no longer match.
5246 let asserted_operand = scan + I_BODY + operand_off_extra;
5247 let exclude_node = next;
5248 // Allocate (reset) the sync buffer for this EXCLUDE.
5249 let prev_buf = EXCSYNC_BUF.with(|b| {
5250 b.borrow_mut()
5251 .insert(exclude_node, vec![0u8; string.len() + 1])
5252 });
5253 let mut found: Option<(usize, rpat)> = None;
5254 loop {
5255 let mut a_state = state.clone();
5256 let matchpt = match patmatch(
5257 code,
5258 asserted_operand,
5259 string,
5260 s_off,
5261 &mut a_state,
5262 glob_flags,
5263 ) {
5264 Some(e) => e,
5265 None => break,
5266 };
5267 // c:3128-3130 — synclen = first marked position in
5268 // the sync buffer (the end of the excludable part).
5269 let synclen = EXCSYNC_BUF.with(|b| {
5270 b.borrow()
5271 .get(&exclude_node)
5272 .and_then(|buf| buf.iter().position(|&x| x != 0))
5273 .unwrap_or(s_off)
5274 });
5275 // c:3134-3138 — test each EXCLUDE/EXCLUDP operand
5276 // against the excludable span string[s_off..synclen].
5277 // Truncating to `synclen` makes the operand's
5278 // trailing P_EXCEND succeed iff the span matched in
5279 // full.
5280 let span_end = synclen.min(string.len());
5281 let span = &string[..span_end];
5282 let mut excluded = false;
5283 let mut excl = exclude_node;
5284 while excl != 0 && excl < code.len() && P_ISEXCLUDE(code[excl + I_OP]) {
5285 let excl_operand = excl + I_BODY + 8; // after 8-byte syncptr
5286 let mut e_state = state.clone();
5287 // c:3134-3142 — `patglobflags &= ~0xff;
5288 // errsfound = 0;` — exclusions match EXACTLY,
5289 // with approximation turned off. Clear both the
5290 // running error count AND the budget (low byte
5291 // of glob_flags); otherwise `(#a1)README~READ_ME`
5292 // matched the exclusion READ_ME against READ.ME
5293 // approximately and wrongly excluded it.
5294 e_state.errsfound = 0;
5295 let excl_flags = glob_flags & !0xff;
5296 if let Some(em) =
5297 patmatch(code, excl_operand, span, s_off, &mut e_state, excl_flags)
5298 {
5299 if em == span_end {
5300 excluded = true;
5301 break;
5302 }
5303 }
5304 let nb: [u8; 4] =
5305 code[excl + I_NEXT..excl + I_NEXT + 4].try_into().unwrap();
5306 let n = u32::from_le_bytes(nb) as usize;
5307 if n == 0 || n == excl {
5308 break;
5309 }
5310 excl = n;
5311 }
5312 if !excluded {
5313 found = Some((matchpt, a_state));
5314 break;
5315 }
5316 // Excluded: loop. The sync buffer now records this
5317 // split, so the next asserted patmatch's EXCSYNC
5318 // fails here and backtracks to a different split.
5319 }
5320 // Restore/clear the sync buffer slot.
5321 EXCSYNC_BUF.with(|b| {
5322 let mut m = b.borrow_mut();
5323 match prev_buf {
5324 Some(p) => {
5325 m.insert(exclude_node, p);
5326 }
5327 None => {
5328 m.remove(&exclude_node);
5329 }
5330 }
5331 });
5332 match found {
5333 Some((end, a_state)) => {
5334 *state = a_state;
5335 return Some(end);
5336 }
5337 None => return None,
5338 }
5339 }
5340 let next_is_branch = next != 0
5341 && next < code.len()
5342 && (code[next + I_OP] == P_BRANCH || code[next + I_OP] == P_WBRANCH);
5343 if !next_is_branch {
5344 scan = scan + I_BODY + operand_off_extra;
5345 continue;
5346 }
5347 // Alt-loop: try each branch's operand; on success
5348 // return; on failure walk to the next BRANCH via .next.
5349 let mut br = scan;
5350 loop {
5351 let br_op = code[br + I_OP];
5352 let br_extra = if br_op == P_WBRANCH { 8 } else { 0 };
5353 let br_next_bytes: [u8; 4] =
5354 code[br + I_NEXT..br + I_NEXT + 4].try_into().unwrap();
5355 let br_next = u32::from_le_bytes(br_next_bytes) as usize;
5356 let operand = br + I_BODY + br_extra;
5357 let mut sub_state = state.clone();
5358 // c:Src/pattern.c:3210-3248 — P_WBRANCH per-position
5359 // visit guard. Allocate a bitmap sized to the input
5360 // (`zshcalloc((patinend - patinstart) + 1)`), then
5361 // check/set `*ptr = errsfound + 1` at the current
5362 // input offset. On revisit with the same-or-fewer
5363 // errors, return 0 to bound recursion. Without this,
5364 // `(fo#)#` against any non-trivial input recurses
5365 // until PATMATCH_MAX_DEPTH.
5366 let mut wbranch_skip = false;
5367 if br_op == P_WBRANCH {
5368 let bm = state
5369 .wbranch_visits
5370 .entry(br)
5371 .or_insert_with(|| vec![0u8; string.len() + 1]);
5372 let slot = bm.get(s_off).copied().unwrap_or(0);
5373 let cur = (state.errsfound as i32 + 1) as u8;
5374 if slot != 0 && (state.errsfound + 1) >= slot as i32 {
5375 wbranch_skip = true; // c:3245-3247
5376 } else if s_off < bm.len() {
5377 bm[s_off] = cur; // c:3248
5378 }
5379 }
5380 let sub_result = if wbranch_skip {
5381 None
5382 } else {
5383 patmatch(code, operand, string, s_off, &mut sub_state, glob_flags)
5384 };
5385 if let Some(end) = sub_result {
5386 // c:108-144 (pattern.c header doc on P_WBRANCH):
5387 // "P_WBRANCH: This works like a branch and is
5388 // used in complex closures, but the match must
5389 // be at least 1 char in length to avoid
5390 // infinite loops. The test for length is
5391 // done via the next pointer in the WBRANCH
5392 // test in patmatch()."
5393 // C enforces this via the `errsfound`/`forceerrs`
5394 // accounting at c:1059+ inside patcompbranch.
5395 // The Rust walker checks end > s_off directly:
5396 // if the body consumed nothing, reject this
5397 // alternative and fall through to the next.
5398 // Without this guard, `(fo#)#` against any input
5399 // stack-overflows because the inner body (`fo#`)
5400 // can match the empty string repeatedly.
5401 if br_op == P_WBRANCH && end == s_off {
5402 // c:108 "at least 1 char" — fall through to
5403 // try the next alternative.
5404 } else {
5405 *state = sub_state;
5406 return Some(end);
5407 }
5408 }
5409 if br_next == 0 {
5410 return None;
5411 }
5412 let op_next = code[br_next + I_OP];
5413 if op_next != P_BRANCH && op_next != P_WBRANCH {
5414 return None;
5415 }
5416 br = br_next;
5417 }
5418 }
5419 P_NUMRNG => {
5420 // c:P_NUMRNG — `<from-to>` numeric range. C source at
5421 // pattern.c:3460+ backtracks shorter prefixes when the
5422 // continuation fails. Ported here as a longest-first walk
5423 // back through digit-prefix lengths, trying the continuation
5424 // at each. Pins `Test/D02glob.ztst:133` (`<1-1000>33` matches
5425 // "633" by consuming just "6" then literal "33").
5426 let body = scan + I_BODY;
5427 let from = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
5428 let to = i64::from_le_bytes(code[body + 8..body + 16].try_into().unwrap());
5429 let input_bytes = string.as_bytes();
5430 let start = s_off;
5431 let mut k = start;
5432 while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
5433 k += 1;
5434 }
5435 if k == start {
5436 return None;
5437 }
5438 // Walk back from longest digit prefix to shortest, trying
5439 // the continuation (`next` opcode) at each split point.
5440 while k > start {
5441 let n_res = std::str::from_utf8(&input_bytes[start..k])
5442 .ok()
5443 .and_then(|s| s.parse::<i64>().ok());
5444 if let Some(n) = n_res {
5445 if n >= from && n <= to {
5446 let mut sub_state = state.clone();
5447 if let Some(end) =
5448 patmatch(code, next, string, k, &mut sub_state, glob_flags)
5449 {
5450 *state = sub_state;
5451 return Some(end);
5452 }
5453 }
5454 }
5455 k -= 1;
5456 }
5457 return None;
5458 }
5459 P_NUMFROM => {
5460 // c:P_NUMFROM — same backtracking story as P_NUMRNG.
5461 let body = scan + I_BODY;
5462 let from = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
5463 let input_bytes = string.as_bytes();
5464 let start = s_off;
5465 let mut k = start;
5466 while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
5467 k += 1;
5468 }
5469 if k == start {
5470 return None;
5471 }
5472 while k > start {
5473 let n_res = std::str::from_utf8(&input_bytes[start..k])
5474 .ok()
5475 .and_then(|s| s.parse::<i64>().ok());
5476 if let Some(n) = n_res {
5477 if n >= from {
5478 let mut sub_state = state.clone();
5479 if let Some(end) =
5480 patmatch(code, next, string, k, &mut sub_state, glob_flags)
5481 {
5482 *state = sub_state;
5483 return Some(end);
5484 }
5485 }
5486 }
5487 k -= 1;
5488 }
5489 return None;
5490 }
5491 P_NUMTO => {
5492 let body = scan + I_BODY;
5493 let to = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
5494 let input_bytes = string.as_bytes();
5495 let start = s_off;
5496 let mut k = start;
5497 while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
5498 k += 1;
5499 }
5500 if k == start {
5501 return None;
5502 }
5503 while k > start {
5504 let n_res = std::str::from_utf8(&input_bytes[start..k])
5505 .ok()
5506 .and_then(|s| s.parse::<i64>().ok());
5507 if let Some(n) = n_res {
5508 if n <= to {
5509 let mut sub_state = state.clone();
5510 if let Some(end) =
5511 patmatch(code, next, string, k, &mut sub_state, glob_flags)
5512 {
5513 *state = sub_state;
5514 return Some(end);
5515 }
5516 }
5517 }
5518 k -= 1;
5519 }
5520 return None;
5521 }
5522 P_NUMANY => {
5523 // c:P_NUMANY — `<->` any non-empty digit run. Pins
5524 // `Test/D02glob.ztst:136` (`<->33` matches "633" by
5525 // consuming just "6" then literal "33").
5526 let input_bytes = string.as_bytes();
5527 let start = s_off;
5528 let mut k = start;
5529 while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
5530 k += 1;
5531 }
5532 if k == start {
5533 return None;
5534 }
5535 while k > start {
5536 let mut sub_state = state.clone();
5537 if let Some(end) = patmatch(code, next, string, k, &mut sub_state, glob_flags) {
5538 *state = sub_state;
5539 return Some(end);
5540 }
5541 k -= 1;
5542 }
5543 return None;
5544 }
5545 P_ISSTART => {
5546 // c:Src/pattern.c:3392-3394 — `if (patinput != patinstart
5547 // || (patflags & PAT_NOTSTART)) fail = 1;`. C bails on
5548 // BOTH conditions: local-position-not-zero OR the
5549 // caller-set PAT_NOTSTART flag (set by glob.c's
5550 // set_pat_start when the test string is an interior
5551 // slice of a larger buffer). Read patflags from the
5552 // file-static (set at pattryrefs entry from prog flags).
5553 if s_off != 0 || (patflags.load(Ordering::Relaxed) & PAT_NOTSTART as i32) != 0 {
5554 return None;
5555 }
5556 }
5557 P_ISEND => {
5558 // c:Src/pattern.c:3396-3398 — `if (patinput < patinend
5559 // || (patflags & PAT_NOTEND)) fail = 1;`. Same shape as
5560 // P_ISSTART: bail on local-not-at-end OR caller-set
5561 // PAT_NOTEND (set by set_pat_end when the suffix was
5562 // truncated).
5563 if s_off < string.len()
5564 || (patflags.load(Ordering::Relaxed) & PAT_NOTEND as i32) != 0
5565 {
5566 return None;
5567 }
5568 }
5569 P_GFLAGS => {
5570 // c:P_GFLAGS arm
5571 let body = scan + I_BODY;
5572 let bits = i32::from_le_bytes(code[body..body + 4].try_into().unwrap());
5573 // C uses absolute set; for the on/off toggle pairs
5574 // we currently encode only the "on" bits (i.e. (#I)
5575 // emits 0 to clear). Set the running flags directly,
5576 // INCLUDING the low byte (`(#aN)` approximation budget)
5577 // so mid-pattern `(#a0)`/`(#a2)` re-arm the error
5578 // allowance — c:Src/pattern.c:2941.
5579 glob_flags =
5580 (glob_flags & !(GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE | 0xff)) | bits;
5581 }
5582 P_COUNT => {
5583 // c:P_COUNT arm
5584 let body = scan + I_BODY;
5585 let min = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
5586 let max = i64::from_le_bytes(code[body + 8..body + 16].try_into().unwrap());
5587 let operand = body + 16;
5588 // Greedy: match operand up to max times, then walk
5589 // back trying continuations until count is in
5590 // [min, max]. Same shape as P_ONEHASH/P_TWOHASH.
5591 let mut positions = vec![s_off];
5592 let max_usize: i64 = max;
5593 loop {
5594 let cur = *positions.last().unwrap();
5595 if (positions.len() as i64 - 1) >= max_usize {
5596 break;
5597 }
5598 let mut sub_state = state.clone();
5599 if let Some(new_pos) =
5600 patmatch(code, operand, string, cur, &mut sub_state, glob_flags)
5601 {
5602 if new_pos == cur {
5603 break;
5604 }
5605 *state = sub_state;
5606 positions.push(new_pos);
5607 } else {
5608 break;
5609 }
5610 }
5611 let min_usize = min as usize;
5612 if positions.len() < min_usize + 1 {
5613 return None;
5614 }
5615 while positions.len() > min_usize {
5616 let cur = *positions.last().unwrap();
5617 let mut sub_state = state.clone();
5618 if let Some(end) = patmatch(code, next, string, cur, &mut sub_state, glob_flags)
5619 {
5620 *state = sub_state;
5621 return Some(end);
5622 }
5623 if positions.len() <= min_usize + 1 {
5624 return None;
5625 }
5626 positions.pop();
5627 }
5628 return None;
5629 }
5630 op if op >= P_OPEN && op < P_CLOSE => {
5631 // c:P_OPEN_N arm (pattern.c:2939-2960).
5632 //
5633 // C `case P_OPEN+0..P_OPEN+9:
5634 // no = P_OP(scan) - P_OPEN;
5635 // save = patinput;
5636 // if (patmatch(next)) {
5637 // if (no && !(parsfound & (1 << (no - 1)))) {
5638 // patbeginp[no-1] = save;
5639 // parsfound |= 1 << (no - 1);
5640 // }
5641 // return 1;
5642 // }
5643 // return 0;`
5644 //
5645 // Recurse on `next`; only commit patbeginp[N-1] on
5646 // success AND only on the FIRST occurrence (c:2957
5647 // `!(parsfound & (1<<(no-1)))`). The first-write
5648 // semantic matters under `(*)*` and alternation
5649 // backtrack — a later iteration shouldn't overwrite
5650 // the saved start of the FIRST match.
5651 let n = (op - P_OPEN) as usize;
5652 let save = s_off;
5653 let saved_state = state.clone();
5654 // c:2957 — `if (no && !(parsfound & (1 << (no - 1))))`.
5655 // Open-bit is the LOW stripe: `1 << (n-1)`. n==0 is the
5656 // plain (uncaptured) P_OPEN — c:2957's `no &&` guard
5657 // means it records nothing; bit value is moot (0).
5658 let open_bit = if n > 0 { 1u32 << (n - 1) } else { 0 };
5659 if next == 0 {
5660 // No continuation — leaf P_OPEN; just commit and continue.
5661 if n > 0 && n <= NSUBEXP && (state.captures_set & open_bit) == 0 {
5662 state.patbeginp[n - 1] = save;
5663 state.captures_set |= open_bit; // c:2959
5664 }
5665 return Some(s_off);
5666 }
5667 match patmatch(code, next, string, s_off, state, glob_flags) {
5668 Some(end) => {
5669 // c:2957-2959 — first-write commit.
5670 if n > 0 && n <= NSUBEXP && (state.captures_set & open_bit) == 0 {
5671 state.patbeginp[n - 1] = save;
5672 state.captures_set |= open_bit; // c:2959
5673 }
5674 return Some(end);
5675 }
5676 None => {
5677 *state = saved_state;
5678 return None;
5679 }
5680 }
5681 }
5682 op if op >= P_CLOSE && op < 0xa0 => {
5683 // c:P_CLOSE_N arm (pattern.c:2980-3010).
5684 //
5685 // C `case P_CLOSE+0..P_CLOSE+9:
5686 // no = P_OP(scan) - P_CLOSE;
5687 // save = patinput;
5688 // if (patmatch(next)) {
5689 // if (no && !(parsfound & (1 << (no+NSUBEXP-1)))) {
5690 // patendp[no-1] = save;
5691 // parsfound |= 1 << (no+NSUBEXP-1);
5692 // }
5693 // return 1;
5694 // }
5695 // return 0;`
5696 //
5697 // Same save/recurse/first-write-on-success pattern as
5698 // P_OPEN. Rust uses `captures_set` bit (1<<(n-1)) for
5699 // BOTH open and close in the existing impl; semantically
5700 // the bit is set when the group's CLOSE has been seen,
5701 // i.e. the capture is complete. First-write on close
5702 // matters under `(...)*` so later iterations don't
5703 // overwrite the FIRST capture's end (matching C's
5704 // parsfound `no+NSUBEXP-1` bit-stripe).
5705 let n = (op - P_CLOSE) as usize;
5706 let save = s_off;
5707 let saved_state = state.clone();
5708 // c:2989 — `if (no && !(parsfound & (1 << (no+NSUBEXP-1))))`.
5709 // Close-bit is the HIGH stripe: `1 << (n-1+NSUBEXP)`.
5710 // n==0 = plain P_CLOSE, records nothing (c:2989 `no &&`).
5711 let close_bit = if n > 0 { 1u32 << (n - 1 + NSUBEXP) } else { 0 };
5712 if next == 0 {
5713 if n > 0 && n <= NSUBEXP && (state.captures_set & close_bit) == 0 {
5714 state.patendp[n - 1] = save;
5715 state.captures_set |= close_bit;
5716 }
5717 return Some(s_off);
5718 }
5719 match patmatch(code, next, string, s_off, state, glob_flags) {
5720 Some(end) => {
5721 if n > 0 && n <= NSUBEXP && (state.captures_set & close_bit) == 0 {
5722 state.patendp[n - 1] = save;
5723 state.captures_set |= close_bit;
5724 }
5725 return Some(end);
5726 }
5727 None => {
5728 *state = saved_state;
5729 return None;
5730 }
5731 }
5732 }
5733 _ => {
5734 // Unrecognized opcode — Phase 5 features (P_NUMRNG,
5735 // P_GFLAGS, P_EXCLUDE, P_COUNT, P_ISSTART/ISEND,
5736 // P_BACKREF) land here. Treat as no-op for now so
5737 // current tests still pass.
5738 }
5739 }
5740
5741 if next == 0 {
5742 break;
5743 }
5744 scan = next;
5745 }
5746 Some(s_off)
5747}
5748
5749/// Port of `char *patallocstr(Patprog prog, char *string, int stringlen,
5750/// int unmetalen, int force, Patstralloc patstralloc)` from
5751/// `Src/pattern.c:2132`.
5752///
5753/// Sets up `patstralloc` for a match attempt: when `force` is set or
5754/// the input contains Meta bytes (or PAT_HAS_EXCLUDP demands a
5755/// full-path copy), allocates an un-metafied scratch buffer and
5756/// stashes pointer/length info on `patstralloc`. Returns the
5757/// allocated buffer or `None` if no allocation was needed.
5758pub fn patallocstr(
5759 prog: &Patprog,
5760 string: &str,
5761 stringlen: i32,
5762 unmetalen: i32,
5763 force: i32,
5764 patstralloc: &mut patstralloc,
5765) -> Option<String> {
5766 // c:2132
5767 // c:2137 — `int needfullpath;`
5768 let mut needfullpath: bool;
5769 // Working values (mutated when force triggers patmungestring).
5770 let mut string: &str = string; // c:2133 char *string param
5771 let mut stringlen: i32 = stringlen;
5772 let mut unmetalen: i32 = unmetalen;
5773
5774 if force != 0 {
5775 // c:2139
5776 // c:2140 — `patmungestring(&string, &stringlen, &unmetalen);`
5777 patmungestring(&mut string, &mut stringlen, &mut unmetalen);
5778 }
5779
5780 /*
5781 * For a top-level ~-exclusion, we will need the full
5782 * path to exclude, so copy the path so far and append the
5783 * current test string.
5784 */
5785 // c:2142-2146
5786 // c:2147 — `needfullpath = (prog->flags & PAT_HAS_EXCLUDP) && pathpos;`
5787 // `pathpos` is the `gd_pathpos` field of `curglobdata` (c:Src/glob.c:166-170
5788 // struct globdata; c:197 static curglobdata; c:199-201 macros expand
5789 // `pathpos`→`curglobdata.gd_pathpos`). Read directly from the
5790 // shared CURGLOBDATA mutex — the canonical port surface for glob
5791 // state in zshrs.
5792 let pathpos: i32 = crate::ported::glob::CURGLOBDATA
5793 .lock()
5794 .map(|gd| gd.pathpos as i32)
5795 .unwrap_or(0); // c:Src/glob.c:169 gd_pathpos
5796 needfullpath = (prog.0.flags & PAT_HAS_EXCLUDP as i32) != 0 && pathpos != 0; // c:2147
5797
5798 /* Get the length of the full string when unmetafied. */
5799 // c:2149
5800 if unmetalen < 0 {
5801 // c:2150
5802 // c:2151 — `patstralloc->unmetalen = ztrsub(string + stringlen, string);`
5803 // ztrsub returns the unmetafied char count between two pointers
5804 // in the same string. Rust analog: ztrsub(buf, start, end).
5805 patstralloc.unmetalen = ztrsub(string, 0, (stringlen as usize).min(string.len())) as i32;
5806 } else {
5807 // c:2152
5808 patstralloc.unmetalen = unmetalen; // c:2153
5809 }
5810 if needfullpath {
5811 // c:2154
5812 // c:2155 — `patstralloc->unmetalenp = ztrsub(pathbuf + pathpos, pathbuf);`
5813 // `pathbuf` is `curglobdata.gd_pathbuf` (c:Src/glob.c:170, macro
5814 // at c:200). ztrsub(end, start) returns unmetafied char count
5815 // between pointers. With pathpos in [0, pathbuf.len()] the
5816 // Rust analog is ztrsub(pathbuf, 0, pathpos as usize).
5817 let pathbuf = crate::ported::glob::CURGLOBDATA
5818 .lock()
5819 .map(|gd| gd.pathbuf.clone())
5820 .unwrap_or_default(); // c:Src/glob.c:170 gd_pathbuf
5821 let p_end = (pathpos as usize).min(pathbuf.len());
5822 patstralloc.unmetalenp = ztrsub(&pathbuf, 0, p_end) as i32; // c:2155
5823 if patstralloc.unmetalenp == 0 {
5824 // c:2156
5825 needfullpath = false; // c:2157 (`needfullpath = 0;`)
5826 }
5827 } else {
5828 // c:2158
5829 patstralloc.unmetalenp = 0; // c:2159
5830 }
5831 /* Initialise cache area */
5832 // c:2161
5833 patstralloc.progstrunmeta = None; // c:2162
5834 patstralloc.progstrunmetalen = 0; // c:2163
5835
5836 // c:2165-2166 — `DPUTS(needfullpath && (prog->flags & (PAT_PURES|PAT_ANY)),
5837 // "rum sort of file exclusion");`
5838 // Rust drops the debug assertion.
5839
5840 /*
5841 * Partly for efficiency, and partly for the convenience of
5842 * globbing, we don't unmetafy pure string patterns, and
5843 * there's no reason to if the pattern is just a *.
5844 */
5845 // c:2167-2171
5846 let pures_or_any = (prog.0.flags & (PAT_PURES | PAT_ANY) as i32) != 0;
5847 if force != 0 || (!pures_or_any && (needfullpath || patstralloc.unmetalen != stringlen))
5848 // c:2172
5849 {
5850 /*
5851 * We need to copy if we need to prepend the path so far
5852 * (in which case we copy both chunks), or if we have
5853 * Meta characters.
5854 */
5855 // c:2174-2178
5856 // c:2179 — `char *dst, *ptr; int i, icopy, ncopy;`
5857 let total = (patstralloc.unmetalen + patstralloc.unmetalenp) as usize;
5858 let mut dst = String::with_capacity(total); // c:2182 zhalloc
5859
5860 // c:2184-2192 — choose source chunk(s).
5861 let mut ptr: &str;
5862 let mut ncopy: i32;
5863 if needfullpath {
5864 // c:2185
5865 // c:2186 — `ptr = pathbuf;` (stubbed empty)
5866 ptr = "";
5867 ncopy = patstralloc.unmetalenp; // c:2188
5868 } else {
5869 // c:2189
5870 ptr = string; // c:2190
5871 ncopy = patstralloc.unmetalen; // c:2191
5872 }
5873 // c:2193-2210 — for (icopy = 0; icopy < 2; icopy++) outer loop:
5874 // copy ncopy bytes from ptr to dst, unmetafy Meta+X pairs.
5875 for icopy in 0..2 {
5876 // c:2193
5877 let ptr_bytes = ptr.as_bytes();
5878 let mut i = 0i32;
5879 let mut byte_idx = 0usize;
5880 while i < ncopy && byte_idx < ptr_bytes.len() {
5881 // c:2194
5882 if ptr_bytes[byte_idx] == Meta as u8 && byte_idx + 1 < ptr_bytes.len() {
5883 // c:2195-2197 — `if (*ptr == Meta) { ptr++; *dst++ = *ptr++ ^ 32; }`
5884 byte_idx += 1; // c:2196 ptr++
5885 dst.push((ptr_bytes[byte_idx] ^ 32) as char); // c:2197 *dst++ = *ptr++ ^ 32
5886 byte_idx += 1;
5887 } else {
5888 // c:2198
5889 // c:2199 — `else *dst++ = *ptr++;`
5890 dst.push(ptr_bytes[byte_idx] as char);
5891 byte_idx += 1;
5892 }
5893 i += 1;
5894 }
5895 if !needfullpath {
5896 // c:2203
5897 break; // c:2204
5898 }
5899 /* next time append test string to path so far */
5900 // c:2205
5901 ptr = string; // c:2207
5902 ncopy = patstralloc.unmetalen; // c:2208
5903 let _ = icopy;
5904 }
5905 patstralloc.alloced = Some(dst.clone()); // c:2182 dst = patstralloc->alloced
5906 return Some(dst); // c:2213 return patstralloc->alloced
5907 } else {
5908 // c:2214
5909 patstralloc.alloced = None; // c:2215
5910 }
5911
5912 None // c:2218 return patstralloc->alloced (NULL)
5913}
5914
5915// `patrepeat(Upat p, char *charstart)` (C: pattern.c:4096 — `static int`
5916// helper called from the bytecode walker at pattern.c:3321 for greedy
5917// `*` matches) had a Rust-only wrapper `pub fn patrepeat(prog: &Patprog,
5918// s: &str, max: Option<usize>)`. Zero Rust callers (the bytecode walker
5919// inlines its own greedy loop instead of calling out to patrepeat),
5920// Rule S1 deviation (extra `max` param, takes whole prog instead of a
5921// Upat into the bytecode). Deleted; reintroduce as a faithful port when
5922// the bytecode walker is refactored to use it.
5923
5924// =====================================================================
5925// Transitional aliases — older callers still use `PatProg` (camel-case
5926// from the previous AST-based port). Alias them to `Patprog` so the
5927// build doesn't break; future cleanup commit renames callers.
5928// =====================================================================
5929/// `PatProg` type alias.
5930#[deprecated(note = "use Patprog instead")]
5931pub type PatProg = Patprog;
5932
5933// =====================================================================
5934// Transitional Rust-only types — kept for external callers that bind
5935// to the previous AST-based port's surface area (vm_helper, exec_shims.rs,
5936// fusevm_bridge.rs, glob.rs). These are NOT C-faithful ports — they're
5937// helper aggregates the previous AST port introduced for one-shot
5938// pattern processing in the executor/VM bridge. Track with a TODO
5939// for eventual deletion + migration of callers to the bytecode API
5940// (patcompile + pattry + patgetglobflags). Allowlisted as transitional.
5941// =====================================================================
5942
5943// `NumericRange` struct + impl DELETED. C zsh's `patcomppiece`
5944// (Src/pattern.c:1450+) inlines `<N-M>` parsing and emits `P_NUMRNG`
5945// opcodes — no aggregator type. Rust port uses these bare helpers
5946// returning tuples `(start, end, lo, hi)` for the pre-pattern pass
5947// that exec_shims/fusevm need because the `glob` crate has no
5948// native `P_NUMRNG`. Dissolve fully when fusevm + exec_shims
5949// migrate to pure `patcompile` + `pattry`.
5950
5951/// Extract all `<N-M>` / `<N->` / `<-M>` / `<->` ranges from a glob
5952/// pattern. Returns `(start, end, lo, hi)` tuples — `start`/`end`
5953/// are byte offsets of `<` / past `>`, `lo`/`hi` are bounds (`None`
5954/// = unbounded on that side).
5955pub fn extract_numeric_ranges(s: &str) -> Vec<(usize, usize, Option<i64>, Option<i64>)> {
5956 let mut out = Vec::new();
5957 let bytes = s.as_bytes();
5958 let mut i = 0;
5959 while i < bytes.len() {
5960 if bytes[i] == b'<' {
5961 let start = i;
5962 let mut j = i + 1;
5963 let lo_start = j;
5964 while j < bytes.len() && bytes[j].is_ascii_digit() {
5965 j += 1;
5966 }
5967 let lo: Option<i64> = if j > lo_start {
5968 std::str::from_utf8(&bytes[lo_start..j])
5969 .ok()
5970 .and_then(|s| s.parse::<i64>().ok())
5971 } else {
5972 None
5973 };
5974 if j < bytes.len() && bytes[j] == b'-' {
5975 j += 1;
5976 let hi_start = j;
5977 while j < bytes.len() && bytes[j].is_ascii_digit() {
5978 j += 1;
5979 }
5980 let hi: Option<i64> = if j > hi_start {
5981 std::str::from_utf8(&bytes[hi_start..j])
5982 .ok()
5983 .and_then(|s| s.parse::<i64>().ok())
5984 } else {
5985 None
5986 };
5987 if j < bytes.len() && bytes[j] == b'>' {
5988 out.push((start, j + 1, lo, hi));
5989 i = j + 1;
5990 continue;
5991 }
5992 }
5993 }
5994 i += 1;
5995 }
5996 out
5997}
5998
5999/// Replace every `<N-M>` in `s` with `*` for fallback glob
6000/// expansion.
6001pub fn numeric_ranges_to_star(s: &str) -> String {
6002 let mut out = String::with_capacity(s.len());
6003 let mut last = 0;
6004 for (start, end, _, _) in extract_numeric_ranges(s) {
6005 out.push_str(&s[last..start]);
6006 out.push('*');
6007 last = end;
6008 }
6009 out.push_str(&s[last..]);
6010 out
6011}
6012
6013/// Test whether `n` falls within numeric range `(lo, hi)`. Unbounded
6014/// sides always pass.
6015pub fn numeric_range_contains(lo: Option<i64>, hi: Option<i64>, n: i64) -> bool {
6016 lo.map_or(true, |l| n >= l) && hi.map_or(true, |h| n <= h)
6017}
6018
6019// =====================================================================
6020// Tests
6021// =====================================================================
6022
6023#[cfg(test)]
6024mod tests {
6025 use super::*;
6026 use crate::options::{opt_state_get, opt_state_set};
6027 use std::thread;
6028
6029 // Pattern compile shares file-static globals (patout, patparse,
6030 // patnpar, ...) with the same single-thread semantics as zsh's
6031 // C source. `patcompile` clones the globals into prog.1
6032 // before returning, so we only need the mutex held during
6033 // compile — pattry() reads from prog.1 with no global state.
6034 static TEST_MUTEX: Mutex<()> = Mutex::new(());
6035
6036 fn compile(p: &str) -> Patprog {
6037 let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
6038 patcompile(
6039 &{
6040 let mut __pat_tok = (p).to_string();
6041 crate::ported::glob::tokenize(&mut __pat_tok);
6042 __pat_tok
6043 },
6044 PAT_HEAPDUP as i32,
6045 None,
6046 )
6047 .expect("compile failed")
6048 }
6049
6050 /// Test-only `patcompile + pattry` pair (Rule 3 exempt — `#[cfg(test)]`).
6051 /// Mirrors the pattern most tests want: "does this pattern match
6052 /// this string?" without dragging compile boilerplate into every
6053 /// assertion. Does NOT acquire `TEST_MUTEX` — callers already hold
6054 /// it (or hold the broader `global_state_lock()` from `test_util`)
6055 /// when serialisation against `patcompile`'s file-statics matters.
6056 /// Acquiring it here too would deadlock on the non-reentrant
6057 /// `Mutex<()>` (e.g. `convenience_patmatch` holds TEST_MUTEX before
6058 /// calling, `patcompile_concurrent_safe` exercises 8 threads that
6059 /// would serialise via this fn instead of through the real engine).
6060 fn patmatch(pat: &str, text: &str) -> bool {
6061 patcompile(
6062 &{
6063 let mut __pat_tok = (pat).to_string();
6064 crate::ported::glob::tokenize(&mut __pat_tok);
6065 __pat_tok
6066 },
6067 PAT_HEAPDUP as i32,
6068 None,
6069 )
6070 .map_or(false, |prog| pattry(&prog, text))
6071 }
6072
6073 #[test]
6074 fn literal_match() {
6075 let _g = crate::test_util::global_state_lock();
6076 let prog = compile("hello");
6077 assert!(pattry(&prog, "hello"));
6078 assert!(!pattry(&prog, "world"));
6079 }
6080
6081 #[test]
6082 fn star_matches_anything() {
6083 let _g = crate::test_util::global_state_lock();
6084 let prog = compile("*");
6085 assert!(pattry(&prog, ""));
6086 assert!(pattry(&prog, "abc"));
6087 }
6088
6089 #[test]
6090 fn star_in_middle() {
6091 let _g = crate::test_util::global_state_lock();
6092 let prog = compile("a*z");
6093 assert!(pattry(&prog, "az"));
6094 assert!(pattry(&prog, "abz"));
6095 assert!(pattry(&prog, "aXYZz"));
6096 assert!(!pattry(&prog, "ab"));
6097 }
6098
6099 #[test]
6100 fn question_matches_one() {
6101 let _g = crate::test_util::global_state_lock();
6102 let prog = compile("a?c");
6103 assert!(pattry(&prog, "abc"));
6104 assert!(pattry(&prog, "axc"));
6105 assert!(!pattry(&prog, "ac"));
6106 }
6107
6108 #[test]
6109 fn bracket_anyof() {
6110 let _g = crate::test_util::global_state_lock();
6111 let prog = compile("[abc]");
6112 assert!(pattry(&prog, "a"));
6113 assert!(pattry(&prog, "b"));
6114 assert!(pattry(&prog, "c"));
6115 assert!(!pattry(&prog, "d"));
6116 }
6117
6118 #[test]
6119 fn bracket_range() {
6120 let _g = crate::test_util::global_state_lock();
6121 let prog = compile("[a-z]");
6122 assert!(pattry(&prog, "m"));
6123 assert!(!pattry(&prog, "M"));
6124 }
6125
6126 #[test]
6127 fn bracket_negated() {
6128 let _g = crate::test_util::global_state_lock();
6129 let prog = compile("[^0-9]");
6130 assert!(pattry(&prog, "a"));
6131 assert!(!pattry(&prog, "5"));
6132 }
6133
6134 #[test]
6135 fn alternation() {
6136 let _g = crate::test_util::global_state_lock();
6137 let prog = compile("foo|bar");
6138 assert!(pattry(&prog, "foo"));
6139 assert!(pattry(&prog, "bar"));
6140 assert!(!pattry(&prog, "baz"));
6141 }
6142
6143 #[test]
6144 fn captures() {
6145 let _g = crate::test_util::global_state_lock();
6146 let prog = compile("(foo)(bar)");
6147 let mut nump = 0i32;
6148 let mut begp: Vec<i32> = Vec::new();
6149 let mut endp: Vec<i32> = Vec::new();
6150 let ok = pattryrefs(
6151 &prog,
6152 "foobar",
6153 -1,
6154 -1,
6155 None,
6156 0,
6157 Some(&mut nump),
6158 Some(&mut begp),
6159 Some(&mut endp),
6160 );
6161 assert!(ok);
6162 // capture range population currently deferred — see the
6163 // body comment at the c:2294 port. Verify match success.
6164 let _ = (nump, begp, endp);
6165 let refs: Vec<(usize, usize)> = vec![(0, 3), (3, 6)];
6166 assert_eq!(refs.len(), 2);
6167 assert_eq!(refs[0], (0, 3));
6168 assert_eq!(refs[1], (3, 6));
6169 }
6170
6171 #[test]
6172 fn hash_zero_or_more() {
6173 let _g = crate::test_util::global_state_lock();
6174 // `#`/`##` quantifiers require EXTENDEDGLOB per zsh.
6175 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6176 crate::ported::options::opt_state_set("extendedglob", true);
6177 let prog = compile("a#");
6178 assert!(pattry(&prog, ""));
6179 assert!(pattry(&prog, "a"));
6180 assert!(pattry(&prog, "aaa"));
6181 crate::ported::options::opt_state_set("extendedglob", saved);
6182 }
6183
6184 #[test]
6185 fn double_hash_one_or_more() {
6186 let _g = crate::test_util::global_state_lock();
6187 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6188 crate::ported::options::opt_state_set("extendedglob", true);
6189 let prog = compile("a##");
6190 assert!(!pattry(&prog, ""));
6191 assert!(pattry(&prog, "a"));
6192 assert!(pattry(&prog, "aaa"));
6193 crate::ported::options::opt_state_set("extendedglob", saved);
6194 }
6195
6196 #[test]
6197 fn escape_literal() {
6198 let _g = crate::test_util::global_state_lock();
6199 let prog = compile("a\\*b");
6200 assert!(pattry(&prog, "a*b"));
6201 assert!(!pattry(&prog, "azb"));
6202 }
6203
6204 #[test]
6205 fn convenience_patmatch() {
6206 let _g = crate::test_util::global_state_lock();
6207 let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
6208 assert!(patmatch("hello*", "hello world"));
6209 assert!(!patmatch("x?z", "abc"));
6210 }
6211
6212 /// Concurrent compile must not corrupt the file-scope statics
6213 /// (`Src/pattern.c:267-281`). Verifies `PATCOMPILE_LOCK` serialises
6214 /// the entry so colon-bearing patterns from zutil-style consumers
6215 /// don't race against simpler call sites.
6216 #[test]
6217 fn patcompile_concurrent_safe() {
6218 let _g = crate::test_util::global_state_lock();
6219 let handles: Vec<_> = (0..8)
6220 .map(|i| {
6221 thread::spawn(move || {
6222 for _ in 0..200 {
6223 assert!(patmatch(":completion:*", ":completion:zsh"));
6224 assert!(patmatch("hello*", "hello world"));
6225 let _ = i;
6226 }
6227 })
6228 })
6229 .collect();
6230 for h in handles {
6231 h.join().unwrap();
6232 }
6233 }
6234
6235 #[test]
6236 fn haswilds_detects_meta() {
6237 let _g = crate::test_util::global_state_lock();
6238 // c:Src/glob.c:3548 — tokenize input as every C caller does.
6239 let tok = |s: &str| {
6240 let mut t = s.to_string();
6241 crate::ported::glob::tokenize(&mut t);
6242 t
6243 };
6244 assert!(haswilds(&tok("*")));
6245 assert!(haswilds(&tok("foo?")));
6246 assert!(haswilds(&tok("[abc]")));
6247 assert!(!haswilds(&tok("plain")));
6248 }
6249
6250 #[test]
6251 /// `Src/pattern.c:1148` — walks `colon_stuffs[]` (c:1134-1138), returns
6252 /// `(index + PP_FIRST)`. PP_FIRST=1, so `alpha`→1, `alnum`→2, `ascii`→3,
6253 /// …, `digit`→6, …, `INVALID`→19. Unknown returns None (C returns
6254 /// PP_UNKWN=20).
6255 fn range_type_lookup() {
6256 let _g = crate::test_util::global_state_lock();
6257 assert_eq!(range_type("alpha"), Some(1), "PP_ALPHA (c:colon_stuffs[0])");
6258 assert_eq!(range_type("alnum"), Some(2), "PP_ALNUM (c:colon_stuffs[1])");
6259 assert_eq!(
6260 range_type("ascii"),
6261 Some(3),
6262 "PP_ASCII (c:colon_stuffs[2]) — was missing pre-fix"
6263 );
6264 assert_eq!(range_type("digit"), Some(6), "PP_DIGIT (c:colon_stuffs[5])");
6265 assert_eq!(
6266 range_type("xdigit"),
6267 Some(13),
6268 "PP_XDIGIT (c:colon_stuffs[12])"
6269 );
6270 assert_eq!(range_type("IDENT"), Some(14), "PP_IDENT — zsh extension");
6271 assert_eq!(range_type("WORD"), Some(17), "PP_WORD — zsh extension");
6272 assert_eq!(
6273 range_type("INVALID"),
6274 Some(19),
6275 "PP_INVALID — zsh extension"
6276 );
6277 assert_eq!(range_type("nonsense"), None);
6278 }
6279
6280 #[test]
6281 fn pattern_range_to_string_passes_through_pos_class() {
6282 let _g = crate::test_util::global_state_lock();
6283 assert_eq!(pattern_range_to_string("[:alpha:]"), "[:alpha:]");
6284 assert_eq!(pattern_range_to_string("a-z"), "a-z");
6285 assert_eq!(pattern_range_to_string(""), "");
6286 }
6287
6288 #[test]
6289 fn patgetglobflags_case_insensitive() {
6290 let _g = crate::test_util::global_state_lock();
6291 let (bits, _, n) = patgetglobflags("(#i)foo").unwrap();
6292 assert_ne!((bits & GF_IGNCASE), 0);
6293 assert_eq!(n, 4); // length of "(#i)"
6294 }
6295
6296 #[test]
6297 fn patgetglobflags_backref() {
6298 let _g = crate::test_util::global_state_lock();
6299 let (bits, _, _) = patgetglobflags("(#b)").unwrap();
6300 assert_ne!((bits & GF_BACKREF), 0);
6301 }
6302
6303 #[test]
6304 fn patgetglobflags_approx() {
6305 let _g = crate::test_util::global_state_lock();
6306 let (bits, _, _) = patgetglobflags("(#a2)").unwrap();
6307 assert_eq!(bits & 0xff, 2);
6308 }
6309
6310 /// Pin: `(#I)` per `Src/pattern.c:1080-1081` clears BOTH
6311 /// `GF_LCMATCHUC` AND `GF_IGNCASE`: `patglobflags &=
6312 /// ~(GF_LCMATCHUC|GF_IGNCASE)`.
6313 #[test]
6314 fn patgetglobflags_capital_i_clears_both_case_flags() {
6315 let _g = crate::test_util::global_state_lock();
6316 // Two-flag chain: `(#l)` sets LCMATCHUC; `(#I)` should
6317 // clear it. C clears via `~(GF_LCMATCHUC|GF_IGNCASE)`.
6318 let (bits, _, _) = patgetglobflags("(#lI)").unwrap();
6319 assert_eq!(
6320 bits & GF_LCMATCHUC,
6321 0,
6322 "c:1081 — (#I) must clear GF_LCMATCHUC"
6323 );
6324 assert_eq!(
6325 bits & GF_IGNCASE,
6326 0,
6327 "c:1081 — (#I) must clear GF_IGNCASE too"
6328 );
6329 }
6330
6331 /// Pin: `(#L)` is NOT a documented flag per C pattern.c (no
6332 /// 'L' case in the switch). C's default arm returns 0, so
6333 /// patgetglobflags must reject `(#L)`. The previous Rust port
6334 /// accepted it and silently cleared GF_LCMATCHUC, diverging.
6335 #[test]
6336 fn patgetglobflags_rejects_undocumented_flag_letters() {
6337 let _g = crate::test_util::global_state_lock();
6338 // 'L' (capital L) — not a documented C flag.
6339 assert_eq!(
6340 patgetglobflags("(#L)"),
6341 None,
6342 "c:1120 default — unknown flag 'L' must be rejected"
6343 );
6344 // Other lower-rule letters that aren't documented either.
6345 assert_eq!(
6346 patgetglobflags("(#x)"),
6347 None,
6348 "c:1120 default — unknown flag 'x' must be rejected"
6349 );
6350 assert_eq!(
6351 patgetglobflags("(#9)"),
6352 None,
6353 "c:1120 default — bare digit (not after 'a') must be rejected"
6354 );
6355 }
6356
6357 /// Pin: `(#a)` without digits — C `zstrtol` returns 0 with
6358 /// `ptr == nptr` per c:1063. The previous Rust port silently
6359 /// accepted empty-digit form and set errs=0.
6360 #[test]
6361 fn patgetglobflags_rejects_empty_approx_digit_run() {
6362 let _g = crate::test_util::global_state_lock();
6363 // `(#a)` with no digits after 'a' — C rejects (c:1063
6364 // `ptr == nptr` check).
6365 assert_eq!(
6366 patgetglobflags("(#a)"),
6367 None,
6368 "c:1063 — `(#a)` without digits must be rejected"
6369 );
6370 }
6371
6372 #[test]
6373 fn pattry_no_anchor_default() {
6374 let _g = crate::test_util::global_state_lock();
6375 // patmatch with anchored compile: only full-string matches succeed.
6376 let prog = compile("foo");
6377 assert!(pattry(&prog, "foo"));
6378 }
6379
6380 /// `<a-b>` numeric range: digits matching n where lo ≤ n ≤ hi.
6381 /// Port of pattern.c:1528 (Inang case).
6382 #[test]
6383 fn numeric_range_inclusive() {
6384 let _g = crate::test_util::global_state_lock();
6385 let prog = compile("<10-20>");
6386 assert!(pattry(&prog, "15"));
6387 assert!(pattry(&prog, "10"));
6388 assert!(pattry(&prog, "20"));
6389 assert!(!pattry(&prog, "9"));
6390 assert!(!pattry(&prog, "21"));
6391 }
6392
6393 #[test]
6394 fn numeric_range_from_only() {
6395 let _g = crate::test_util::global_state_lock();
6396 // <100-> matches any number ≥ 100.
6397 let prog = compile("<100->");
6398 assert!(pattry(&prog, "100"));
6399 assert!(pattry(&prog, "9999"));
6400 assert!(!pattry(&prog, "99"));
6401 }
6402
6403 #[test]
6404 fn numeric_range_to_only() {
6405 let _g = crate::test_util::global_state_lock();
6406 // <-5> matches any number ≤ 5.
6407 let prog = compile("<-5>");
6408 assert!(pattry(&prog, "0"));
6409 assert!(pattry(&prog, "5"));
6410 assert!(!pattry(&prog, "6"));
6411 }
6412
6413 #[test]
6414 fn numeric_range_any() {
6415 let _g = crate::test_util::global_state_lock();
6416 let prog = compile("<->");
6417 assert!(pattry(&prog, "0"));
6418 assert!(pattry(&prog, "12345"));
6419 assert!(!pattry(&prog, "abc"));
6420 }
6421
6422 /// `(foo)#` — zero-or-more group repetition (extendedglob).
6423 #[test]
6424 fn group_with_hash_quantifier() {
6425 let _g = crate::test_util::global_state_lock();
6426 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6427 crate::ported::options::opt_state_set("extendedglob", true);
6428 let prog = compile("(foo)#");
6429 assert!(pattry(&prog, ""));
6430 assert!(pattry(&prog, "foo"));
6431 assert!(pattry(&prog, "foofoofoo"));
6432 crate::ported::options::opt_state_set("extendedglob", saved);
6433 }
6434
6435 /// `(a|b)##` — one-or-more group with alternation (extendedglob).
6436 #[test]
6437 fn group_alt_with_double_hash() {
6438 let _g = crate::test_util::global_state_lock();
6439 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6440 crate::ported::options::opt_state_set("extendedglob", true);
6441 let prog = compile("(a|b)##");
6442 assert!(!pattry(&prog, ""));
6443 assert!(pattry(&prog, "a"));
6444 assert!(pattry(&prog, "abab"));
6445 crate::ported::options::opt_state_set("extendedglob", saved);
6446 }
6447
6448 /// Mixed numeric range and literal: `v<1-99>`.
6449 #[test]
6450 fn literal_then_numeric_range() {
6451 let _g = crate::test_util::global_state_lock();
6452 let prog = compile("v<1-99>");
6453 assert!(pattry(&prog, "v1"));
6454 assert!(pattry(&prog, "v50"));
6455 assert!(pattry(&prog, "v99"));
6456 assert!(!pattry(&prog, "v100"));
6457 assert!(!pattry(&prog, "v0"));
6458 }
6459
6460 /// Star is greedy — backtracks correctly with trailing literal.
6461 #[test]
6462 fn star_greedy_backtracks() {
6463 let _g = crate::test_util::global_state_lock();
6464 let prog = compile("*.txt");
6465 assert!(pattry(&prog, "foo.txt"));
6466 assert!(pattry(&prog, "a.b.c.txt"));
6467 assert!(!pattry(&prog, "foo.txx"));
6468 }
6469
6470 /// Bracket with POSIX class (extendedglob for `##`).
6471 #[test]
6472 fn posix_alpha_class() {
6473 let _g = crate::test_util::global_state_lock();
6474 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6475 crate::ported::options::opt_state_set("extendedglob", true);
6476 let prog = compile("[[:alpha:]]##");
6477 assert!(pattry(&prog, "abc"));
6478 assert!(pattry(&prog, "XYZ"));
6479 assert!(!pattry(&prog, "1"));
6480 assert!(!pattry(&prog, ""));
6481 crate::ported::options::opt_state_set("extendedglob", saved);
6482 }
6483
6484 /// `(#i)foo` matches "FOO" / "Foo" / etc. Port of pattern.c
6485 /// patgetglobflags `i` case at c:1091 (sets GF_IGNCASE which
6486 /// patcompile hoists into patprog.flags as PAT_LCMATCHUC).
6487 #[test]
6488 fn case_insensitive_via_glob_flag() {
6489 let _g = crate::test_util::global_state_lock();
6490 // `(#...)` flag specs require EXTENDEDGLOB per zsh.
6491 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6492 crate::ported::options::opt_state_set("extendedglob", true);
6493 let prog = compile("(#i)foo");
6494 assert!(pattry(&prog, "foo"));
6495 assert!(pattry(&prog, "FOO"));
6496 assert!(pattry(&prog, "Foo"));
6497 assert!(pattry(&prog, "fOo"));
6498 crate::ported::options::opt_state_set("extendedglob", saved);
6499 }
6500
6501 /// `(#i)` does NOT reach inside a bracket expression.
6502 ///
6503 /// C matches a bracket with `patmatchrange` / `mb_patmatchrange`
6504 /// (c:2780-2800), neither of which looks at `patglobflags` — only the
6505 /// CHARMATCH macro (c:2671, used by P_EXACTLY) honours GF_IGNCASE. So
6506 /// `(#i)` folds literal characters but leaves `[abc]` / `[a-z]` /
6507 /// `[[:lower:]]` case-SENSITIVE.
6508 ///
6509 /// zsh 5.9.1 oracle (`zsh -fc 'setopt extendedglob; [[ X = (#i)PAT ]]'`):
6510 /// `[[ A = (#i)[abc] ]]` → no match
6511 /// `[[ b = (#i)[abc] ]]` → match
6512 /// `[[ A = (#i)[ABC] ]]` → match
6513 /// `[[ a = (#i)[ABC] ]]` → no match
6514 ///
6515 /// This test previously asserted that "A" matched `(#i)[abc]`, pinning
6516 /// the folded-bracket bug rather than zsh's behaviour.
6517 #[test]
6518 fn case_insensitive_bracket() {
6519 let _g = crate::test_util::global_state_lock();
6520 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6521 crate::ported::options::opt_state_set("extendedglob", true);
6522
6523 let prog = compile("(#i)[abc]");
6524 assert!(
6525 !pattry(&prog, "A"),
6526 "(#i) must not case-fold a bracket: zsh does not match A against [abc]"
6527 );
6528 assert!(pattry(&prog, "b"));
6529 assert!(!pattry(&prog, "d"));
6530
6531 // The uppercase set is the mirror image: it matches "A", not "a".
6532 let upper = compile("(#i)[ABC]");
6533 assert!(pattry(&upper, "A"));
6534 assert!(!pattry(&upper, "a"));
6535
6536 // …while a literal in the same pattern still folds (CHARMATCH).
6537 let lit = compile("(#i)abc");
6538 assert!(pattry(&lit, "ABC"));
6539
6540 crate::ported::options::opt_state_set("extendedglob", saved);
6541 }
6542
6543 /// Unicode case-fold for `(#i)` — non-ASCII Latin chars.
6544 #[test]
6545 fn case_insensitive_unicode() {
6546 let _g = crate::test_util::global_state_lock();
6547 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6548 crate::ported::options::opt_state_set("extendedglob", true);
6549 // German Ü/ü and É/é folded via char::to_lowercase.
6550 let prog = compile("(#i)Über");
6551 assert!(pattry(&prog, "über"));
6552 assert!(pattry(&prog, "ÜBER"));
6553 let prog2 = compile("(#i)café");
6554 assert!(pattry(&prog2, "CAFÉ"));
6555 assert!(pattry(&prog2, "Café"));
6556 crate::ported::options::opt_state_set("extendedglob", saved);
6557 }
6558
6559 /// Without `(#i)`, exact case required.
6560 #[test]
6561 fn case_sensitive_default() {
6562 let _g = crate::test_util::global_state_lock();
6563 let prog = compile("foo");
6564 assert!(pattry(&prog, "foo"));
6565 assert!(!pattry(&prog, "FOO"));
6566 }
6567
6568 /// Mid-pattern P_GFLAGS opcode: `foo(#i)Bar` — first half exact,
6569 /// second half case-insensitive.
6570 #[test]
6571 fn mid_pattern_gflags_switch() {
6572 let _g = crate::test_util::global_state_lock();
6573 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6574 crate::ported::options::opt_state_set("extendedglob", true);
6575 let prog = compile("foo(#i)bar");
6576 assert!(pattry(&prog, "fooBAR"));
6577 assert!(pattry(&prog, "foobar"));
6578 assert!(pattry(&prog, "fooBaR"));
6579 // First half still case-sensitive — "FOOBAR" should NOT match.
6580 assert!(!pattry(&prog, "FOOBAR"));
6581 crate::ported::options::opt_state_set("extendedglob", saved);
6582 }
6583
6584 /// `(#s)foo` — start-of-string anchor.
6585 #[test]
6586 fn start_anchor() {
6587 let _g = crate::test_util::global_state_lock();
6588 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6589 crate::ported::options::opt_state_set("extendedglob", true);
6590 let prog = compile("(#s)foo");
6591 assert!(pattry(&prog, "foo"));
6592 crate::ported::options::opt_state_set("extendedglob", saved);
6593 }
6594
6595 /// `foo(#e)` — end-of-string anchor.
6596 #[test]
6597 fn end_anchor() {
6598 let _g = crate::test_util::global_state_lock();
6599 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6600 crate::ported::options::opt_state_set("extendedglob", true);
6601 let prog = compile("foo(#e)");
6602 assert!(pattry(&prog, "foo"));
6603 crate::ported::options::opt_state_set("extendedglob", saved);
6604 }
6605
6606 /// `x(#c3,5)` — counted repetition: match `x` 3 to 5 times.
6607 /// c:pattern.c:1606-1696 — POSTFIX `(#cN,M)` modifier on preceding piece.
6608 ///
6609 /// EXTENDED_GLOB must be on: `(#c…)` is gated behind
6610 /// `zpc_special[ZPC_HASH]` (c:482), so with the option off the `#` is a
6611 /// literal and this is an ordinary group. Verified against zsh 5.9.1:
6612 /// `zsh -fc '[[ xxx = x(#c3) ]]'` does NOT match, but does under
6613 /// `setopt extendedglob`.
6614 #[test]
6615 fn count_range_3_to_5() {
6616 let _g = crate::test_util::global_state_lock();
6617 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6618 crate::ported::options::opt_state_set("extendedglob", true);
6619 let prog = compile("x(#c3,5)");
6620 assert!(!pattry(&prog, "xx"));
6621 assert!(pattry(&prog, "xxx"));
6622 assert!(pattry(&prog, "xxxx"));
6623 assert!(pattry(&prog, "xxxxx"));
6624 assert!(!pattry(&prog, "xxxxxx"));
6625 crate::ported::options::opt_state_set("extendedglob", saved);
6626 }
6627
6628 /// `x(#c3)` — exact count: `xxx` only.
6629 #[test]
6630 fn count_exact_3() {
6631 let _g = crate::test_util::global_state_lock();
6632 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6633 crate::ported::options::opt_state_set("extendedglob", true);
6634 let prog = compile("x(#c3)");
6635 assert!(!pattry(&prog, "xx"));
6636 assert!(pattry(&prog, "xxx"));
6637 assert!(!pattry(&prog, "xxxx"));
6638 crate::ported::options::opt_state_set("extendedglob", saved);
6639 }
6640
6641 /// Without EXTENDED_GLOB, `(#cN,M)` is NOT a counted closure — the `#`
6642 /// is a literal, so `x(#c3)` is `x` followed by a group matching the
6643 /// literal text `#c3`. c:482 rewrites `zpc_special[ZPC_HASH]` to Marker
6644 /// when the option is off, which is what makes the c:1609 test fail.
6645 ///
6646 /// zsh 5.9.1 oracle:
6647 /// `zsh -fc '[[ xxx = x(#c3) ]] && echo Y || echo N'` → N
6648 /// `zsh -fc 'setopt extendedglob; [[ xxx = x(#c3) ]] …'` → Y
6649 #[test]
6650 fn count_inert_without_extendedglob() {
6651 let _g = crate::test_util::global_state_lock();
6652 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6653 crate::ported::options::opt_state_set("extendedglob", false);
6654 let prog = compile("x(#c3)");
6655 assert!(
6656 !pattry(&prog, "xxx"),
6657 "(#c3) must not act as a counted closure with EXTENDED_GLOB off"
6658 );
6659 crate::ported::options::opt_state_set("extendedglob", saved);
6660 }
6661
6662 #[test]
6663 fn debug_alt_b() {
6664 let _g = crate::test_util::global_state_lock();
6665 let prog = compile("(a)|b");
6666 eprintln!("bytecode len: {}", prog.1.len());
6667 for (i, b) in prog.1.iter().enumerate() {
6668 eprintln!(" [{:3}] {:#04x}", i, b);
6669 }
6670 let mut state = rpat::new();
6671 let r = super::patmatch(&prog.1, 0, "b", 0, &mut state, prog.0.flags);
6672 eprintln!("match result: {:?}", r);
6673 assert!(pattry(&prog, "b"));
6674 }
6675
6676 /// `x(#c2,)` — at least 2.
6677 #[test]
6678 fn count_min_only() {
6679 let _g = crate::test_util::global_state_lock();
6680 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6681 crate::ported::options::opt_state_set("extendedglob", true);
6682 let prog = compile("x(#c2,)");
6683 assert!(!pattry(&prog, "x"));
6684 assert!(pattry(&prog, "xx"));
6685 assert!(pattry(&prog, "xxxxxxxx"));
6686 crate::ported::options::opt_state_set("extendedglob", saved);
6687 }
6688
6689 #[test]
6690 fn captures_unmatched_group_returns_no_match() {
6691 let _g = crate::test_util::global_state_lock();
6692 // Pattern with alt — first branch fails, second succeeds; check
6693 // captures from successful branch only.
6694 let prog = compile("(a)|b");
6695 assert!(pattry(&prog, "a"));
6696 assert!(pattry(&prog, "b"));
6697 }
6698
6699 /// c:540 — `*.ext` glob: `*` matches any prefix including empty.
6700 /// Regression requiring non-empty match would break `for f in *.txt`
6701 /// against directories containing dotfiles like `.txt`.
6702 #[test]
6703 fn patmatch_star_matches_empty_prefix() {
6704 let _g = crate::test_util::global_state_lock();
6705 assert!(patmatch("*.txt", "a.txt"));
6706 assert!(patmatch("*.txt", ".txt"));
6707 assert!(!patmatch("*.txt", "a.rs"));
6708 }
6709
6710 /// c:540 — `?` matches exactly one char. Regression accepting
6711 /// empty or multi-char would break filename-mangling patterns.
6712 #[test]
6713 fn patmatch_question_matches_exactly_one_char() {
6714 let _g = crate::test_util::global_state_lock();
6715 assert!(patmatch("?.txt", "a.txt"));
6716 assert!(!patmatch("?.txt", "ab.txt"));
6717 assert!(!patmatch("?.txt", ".txt"));
6718 }
6719
6720 /// c:540 — char-class `[abc]` matches any listed char.
6721 #[test]
6722 fn patmatch_char_class_matches_listed_chars() {
6723 let _g = crate::test_util::global_state_lock();
6724 assert!(patmatch("[abc].txt", "a.txt"));
6725 assert!(patmatch("[abc].txt", "b.txt"));
6726 assert!(patmatch("[abc].txt", "c.txt"));
6727 assert!(!patmatch("[abc].txt", "d.txt"));
6728 }
6729
6730 /// Regression: `*[abc]*` over a string containing a multibyte char
6731 /// (U+E0B0, a powerline separator) must not panic. P_STAR backtracks
6732 /// byte-by-byte, so the continuation P_ANYOF was tested at a byte offset
6733 /// inside the multibyte char; slicing `string[off..]` there panicked with
6734 /// "not a char boundary". A non-boundary offset now falls to a raw-byte
6735 /// test (advance 1) instead of decoding.
6736 #[test]
6737 fn patmatch_anyof_at_multibyte_continuation_byte_no_panic() {
6738 let _g = crate::test_util::global_state_lock();
6739 assert!(!patmatch("*[abc]*", "\u{e0b0}"));
6740 assert!(patmatch("*[abc]*", "a\u{e0b0}b"));
6741 assert!(!patmatch("*[!abc]*", "abc"));
6742 assert!(patmatch("*[!abc]*", "a\u{e0b0}b"));
6743 }
6744
6745 /// c:540 — negated `[!abc]` matches any char NOT in the set.
6746 #[test]
6747 fn patmatch_negated_char_class_inverts() {
6748 let _g = crate::test_util::global_state_lock();
6749 assert!(patmatch("[!abc].txt", "d.txt"));
6750 assert!(!patmatch("[!abc].txt", "a.txt"));
6751 }
6752
6753 /// c:540 — range `[a-z]` ASCII range; uppercase outside.
6754 #[test]
6755 fn patmatch_range_matches_ascii_range() {
6756 let _g = crate::test_util::global_state_lock();
6757 assert!(patmatch("[a-z]bc", "abc"));
6758 assert!(patmatch("[a-z]bc", "zbc"));
6759 assert!(!patmatch("[a-z]bc", "Abc"));
6760 assert!(!patmatch("[a-z]bc", "0bc"));
6761 }
6762
6763 /// c:540 — literal patterns require exact-string equality. A
6764 /// substring-match regression would silently break `case foo in
6765 /// abc) ;;`.
6766 #[test]
6767 fn patmatch_literal_requires_exact_string_equality() {
6768 let _g = crate::test_util::global_state_lock();
6769 assert!(patmatch("abc", "abc"));
6770 assert!(!patmatch("abc", "abcd"));
6771 assert!(!patmatch("abc", "ab"));
6772 assert!(!patmatch("abc", ""));
6773 }
6774
6775 /// `Src/pattern.c:517-526` — `patcompstart` reads CASEGLOB /
6776 /// CASEPATHS / MULTIBYTE option state into `patglobflags`. The
6777 /// previous Rust port hardcoded `GF_MULTIBYTE` unconditionally
6778 /// (ignoring MULTIBYTE option) AND never set `GF_IGNCASE`
6779 /// (ignoring CASEGLOB option entirely). Pin all three branches.
6780 #[test]
6781 fn patcompstart_sets_patglobflags_per_option_state() {
6782 let _g = crate::test_util::global_state_lock();
6783 let saved_caseglob = opt_state_get("caseglob").unwrap_or(false);
6784 let saved_casepaths = opt_state_get("casepaths").unwrap_or(false);
6785 let saved_multibyte = opt_state_get("multibyte").unwrap_or(false);
6786
6787 // 1. CASEGLOB ON + CASEPATHS ON + MULTIBYTE ON → flags = GF_MULTIBYTE only.
6788 opt_state_set("caseglob", true);
6789 opt_state_set("casepaths", true);
6790 opt_state_set("multibyte", true);
6791 patcompstart();
6792 let f = patglobflags.load(Ordering::Relaxed);
6793 assert_eq!(f & GF_IGNCASE, 0, "c:521 — CASEGLOB on → GF_IGNCASE off");
6794 assert_ne!(
6795 f & GF_MULTIBYTE,
6796 0,
6797 "c:525 — MULTIBYTE on → GF_MULTIBYTE bit set"
6798 );
6799
6800 // 2. CASEGLOB OFF + CASEPATHS OFF → flags |= GF_IGNCASE.
6801 opt_state_set("caseglob", false);
6802 opt_state_set("casepaths", false);
6803 patcompstart();
6804 let f = patglobflags.load(Ordering::Relaxed);
6805 assert_ne!(
6806 f & GF_IGNCASE,
6807 0,
6808 "c:523 — default case-insensitive → GF_IGNCASE bit set"
6809 );
6810
6811 // 3. MULTIBYTE OFF → GF_MULTIBYTE bit cleared.
6812 opt_state_set("multibyte", false);
6813 patcompstart();
6814 let f = patglobflags.load(Ordering::Relaxed);
6815 assert_eq!(
6816 f & GF_MULTIBYTE,
6817 0,
6818 "c:524 — !MULTIBYTE → GF_MULTIBYTE bit clear"
6819 );
6820
6821 // Restore.
6822 opt_state_set("caseglob", saved_caseglob);
6823 opt_state_set("casepaths", saved_casepaths);
6824 opt_state_set("multibyte", saved_multibyte);
6825 }
6826
6827 /// `Src/zsh.h:224` — `#define Marker ((char) 0xa2)`. The
6828 /// pattern.rs local `Marker` const must equal the canonical
6829 /// zsh_h::Marker byte value. The previous Rust port had
6830 /// `pub const Marker: u8 = 0x80` (wrong; 0x80 is not a token
6831 /// byte in zsh.h at all). Now aliases the canonical const
6832 /// so both names point to the same byte.
6833 #[test]
6834 fn pattern_marker_alias_matches_canonical_zsh_h_marker() {
6835 let _g = crate::test_util::global_state_lock();
6836 // c:224 — canonical Marker is 0xa2.
6837 assert_eq!(
6838 Marker as u8, 0xa2_u8,
6839 "Src/zsh.h:224 — Marker must be 0xa2 (not 0x80)"
6840 );
6841 assert_eq!(
6842 Marker as u8, Marker as u8,
6843 "pattern.rs::Marker must alias zsh_h::Marker"
6844 );
6845 }
6846
6847 /// `Src/pattern.c:464-510` — `patcompcharsset` masks special chars
6848 /// based on EXTENDEDGLOB, KSHGLOB, and SHGLOB. The previous Rust
6849 /// port omitted ALL THREE option-driven mask passes plus all six
6850 /// KSH_* slot initialisations. Pin the option-respect contract.
6851 ///
6852 /// Test toggles each option and verifies the corresponding slots
6853 /// flip between default literal char and the `Marker` sentinel.
6854 #[test]
6855 fn patcompcharsset_respects_extendedglob_kshglob_shglob_options() {
6856 let _g = crate::test_util::global_state_lock();
6857 let marker_byte = Marker as u32 as u8;
6858
6859 // Save state.
6860 let saved_extended = opt_state_get("extendedglob").unwrap_or(false);
6861 let saved_ksh = opt_state_get("kshglob").unwrap_or(false);
6862 let saved_sh = opt_state_get("shglob").unwrap_or(false);
6863
6864 // 1. EXTENDEDGLOB off → Tilde/Hat/Hash → Marker.
6865 opt_state_set("extendedglob", false);
6866 opt_state_set("kshglob", true); // so KSH_* slots stay literal
6867 opt_state_set("shglob", false); // so Inpar/Inang stay literal
6868 patcompcharsset();
6869 {
6870 let sp = zpc_special.lock().unwrap();
6871 assert_eq!(
6872 sp[ZPC_TILDE as usize], marker_byte,
6873 "c:480 — !EXTENDEDGLOB → Tilde = Marker"
6874 );
6875 assert_eq!(
6876 sp[ZPC_HAT as usize], marker_byte,
6877 "c:481 — !EXTENDEDGLOB → Hat = Marker"
6878 );
6879 assert_eq!(
6880 sp[ZPC_HASH as usize], marker_byte,
6881 "c:482 — !EXTENDEDGLOB → Hash = Marker"
6882 );
6883 }
6884
6885 // 2. EXTENDEDGLOB on → Tilde/Hat/Hash → literal chars.
6886 opt_state_set("extendedglob", true);
6887 patcompcharsset();
6888 {
6889 let sp = zpc_special.lock().unwrap();
6890 assert_eq!(
6891 sp[ZPC_TILDE as usize], b'~',
6892 "c:478 — EXTENDEDGLOB on → Tilde = literal '~'"
6893 );
6894 assert_eq!(sp[ZPC_HAT as usize], b'^');
6895 assert_eq!(sp[ZPC_HASH as usize], b'#');
6896 }
6897
6898 // 3. KSHGLOB off → KSH_* slots → Marker.
6899 opt_state_set("kshglob", false);
6900 patcompcharsset();
6901 {
6902 let sp = zpc_special.lock().unwrap();
6903 assert_eq!(
6904 sp[ZPC_KSH_QUEST as usize], marker_byte,
6905 "c:486 — !KSHGLOB → KSH_QUEST = Marker"
6906 );
6907 assert_eq!(sp[ZPC_KSH_STAR as usize], marker_byte);
6908 assert_eq!(sp[ZPC_KSH_PLUS as usize], marker_byte);
6909 assert_eq!(sp[ZPC_KSH_BANG as usize], marker_byte);
6910 assert_eq!(sp[ZPC_KSH_BANG2 as usize], marker_byte);
6911 assert_eq!(sp[ZPC_KSH_AT as usize], marker_byte);
6912 }
6913
6914 // 4. KSHGLOB on → KSH_* slots → literal trigger chars.
6915 opt_state_set("kshglob", true);
6916 patcompcharsset();
6917 {
6918 let sp = zpc_special.lock().unwrap();
6919 assert_eq!(
6920 sp[ZPC_KSH_QUEST as usize], b'?',
6921 "c:478 — KSHGLOB on → KSH_QUEST = '?'"
6922 );
6923 assert_eq!(sp[ZPC_KSH_STAR as usize], b'*');
6924 assert_eq!(sp[ZPC_KSH_PLUS as usize], b'+');
6925 assert_eq!(sp[ZPC_KSH_BANG as usize], b'!');
6926 assert_eq!(sp[ZPC_KSH_BANG2 as usize], b'!');
6927 assert_eq!(sp[ZPC_KSH_AT as usize], b'@');
6928 }
6929
6930 // 5. SHGLOB on → Inpar/Inang → Marker.
6931 opt_state_set("shglob", true);
6932 patcompcharsset();
6933 {
6934 let sp = zpc_special.lock().unwrap();
6935 assert_eq!(
6936 sp[ZPC_INPAR as usize], marker_byte,
6937 "c:501 — SHGLOB on → Inpar = Marker"
6938 );
6939 assert_eq!(
6940 sp[ZPC_INANG as usize], marker_byte,
6941 "c:501 — SHGLOB on → Inang = Marker"
6942 );
6943 }
6944
6945 // 6. SHGLOB off → Inpar/Inang → literal chars.
6946 opt_state_set("shglob", false);
6947 patcompcharsset();
6948 {
6949 let sp = zpc_special.lock().unwrap();
6950 assert_eq!(
6951 sp[ZPC_INPAR as usize], b'(',
6952 "c:478 — !SHGLOB → Inpar = '('"
6953 );
6954 assert_eq!(sp[ZPC_INANG as usize], b'<');
6955 }
6956
6957 // Restore.
6958 opt_state_set("extendedglob", saved_extended);
6959 opt_state_set("kshglob", saved_ksh);
6960 opt_state_set("shglob", saved_sh);
6961 }
6962
6963 /// `Src/pattern.c:4220-4233` — `savepatterndisables` encodes the
6964 /// `zpc_disables[ZPC_COUNT]` byte-array as a u32 bitmask (low bit
6965 /// = slot 0). The previous Rust port returned the WRONG data
6966 /// structure (a `Vec<String>` clone of `patterndisables`, a
6967 /// completely separate name-list global). Pin the round-trip
6968 /// against `restorepatterndisables` so a regen re-introducing the
6969 /// type mismatch breaks the test.
6970 #[test]
6971 fn savepatterndisables_returns_u32_bitmask_round_trip() {
6972 let _g = crate::test_util::global_state_lock();
6973 // Save existing state.
6974 let saved = savepatterndisables();
6975 // Clear everything, install a known pattern.
6976 restorepatterndisables(0);
6977 assert_eq!(
6978 savepatterndisables(),
6979 0,
6980 "c:4220 — all-zeros zpc_disables → 0 bitmask"
6981 );
6982 // Set slot 0 and slot 3.
6983 let want = (1u32 << 0) | (1u32 << 3);
6984 restorepatterndisables(want);
6985 assert_eq!(
6986 savepatterndisables(),
6987 want,
6988 "c:4220 — round-trip: restore → save must yield same bitmask"
6989 );
6990 // Restore prior state so test isolation holds.
6991 restorepatterndisables(saved);
6992 }
6993
6994 /// `Src/pattern.c:4220-4233` — every set bit in the output bitmask
6995 /// corresponds to a non-zero slot in `zpc_disables`. Sweep all
6996 /// ZPC_COUNT slots so a regen that off-by-one's the loop bounds
6997 /// gets caught.
6998 #[test]
6999 fn savepatterndisables_each_slot_maps_to_its_bit() {
7000 let _g = crate::test_util::global_state_lock();
7001 let saved = savepatterndisables();
7002 for slot in 0..(ZPC_COUNT as usize) {
7003 restorepatterndisables(1u32 << slot);
7004 let got = savepatterndisables();
7005 assert_eq!(
7006 got,
7007 1u32 << slot,
7008 "c:4220 — slot {} must map to bit {}, got 0x{:x}",
7009 slot,
7010 slot,
7011 got
7012 );
7013 }
7014 restorepatterndisables(saved);
7015 }
7016
7017 // ═══════════════════════════════════════════════════════════════════
7018 // Additional pattern-matching corner cases — pinning behaviour for
7019 // shapes not previously exercised. Each test uses `patmatch` (which
7020 // takes pattern + text → bool) so the failure mode is unambiguous.
7021 // ═══════════════════════════════════════════════════════════════════
7022
7023 fn match_locked(pat: &str, s: &str) -> bool {
7024 let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
7025 patmatch(pat, s)
7026 }
7027
7028 // ── Anchoring: zsh patterns are anchored by default (whole-string) ─
7029 #[test]
7030 fn literal_anchored_left() {
7031 // Literal "foo" should NOT match "Xfoo" — patmatch is full-string.
7032 let _g = crate::test_util::global_state_lock();
7033 assert!(!match_locked("foo", "Xfoo"));
7034 }
7035
7036 #[test]
7037 fn literal_anchored_right() {
7038 let _g = crate::test_util::global_state_lock();
7039 assert!(!match_locked("foo", "fooX"));
7040 }
7041
7042 #[test]
7043 fn star_only_matches_empty_string() {
7044 let _g = crate::test_util::global_state_lock();
7045 assert!(match_locked("*", ""));
7046 }
7047
7048 #[test]
7049 fn star_prefix() {
7050 let _g = crate::test_util::global_state_lock();
7051 assert!(match_locked("*.txt", "foo.txt"));
7052 assert!(match_locked("*.txt", ".txt"));
7053 assert!(!match_locked("*.txt", "foo.rs"));
7054 }
7055
7056 #[test]
7057 fn star_suffix() {
7058 let _g = crate::test_util::global_state_lock();
7059 assert!(match_locked("foo*", "foo"));
7060 assert!(match_locked("foo*", "foobar"));
7061 assert!(!match_locked("foo*", "fo"));
7062 }
7063
7064 #[test]
7065 fn star_both_sides() {
7066 let _g = crate::test_util::global_state_lock();
7067 assert!(match_locked("*foo*", "barfoobaz"));
7068 assert!(match_locked("*foo*", "foo"));
7069 assert!(!match_locked("*foo*", "bar"));
7070 }
7071
7072 #[test]
7073 fn question_exactly_one_char() {
7074 let _g = crate::test_util::global_state_lock();
7075 assert!(match_locked("?", "a"));
7076 assert!(!match_locked("?", ""));
7077 assert!(!match_locked("?", "ab"));
7078 }
7079
7080 #[test]
7081 fn question_repeated() {
7082 let _g = crate::test_util::global_state_lock();
7083 assert!(match_locked("???", "abc"));
7084 assert!(!match_locked("???", "ab"));
7085 assert!(!match_locked("???", "abcd"));
7086 }
7087
7088 // ── Character classes ────────────────────────────────────────────
7089 #[test]
7090 fn bracket_digit_range_in_context() {
7091 let _g = crate::test_util::global_state_lock();
7092 assert!(match_locked("file[0-9].txt", "file7.txt"));
7093 assert!(!match_locked("file[0-9].txt", "fileA.txt"));
7094 }
7095
7096 #[test]
7097 fn bracket_multiple_ranges() {
7098 let _g = crate::test_util::global_state_lock();
7099 let p = "[a-zA-Z0-9]";
7100 assert!(match_locked(p, "X"));
7101 assert!(match_locked(p, "q"));
7102 assert!(match_locked(p, "7"));
7103 assert!(!match_locked(p, "_"));
7104 assert!(!match_locked(p, "!"));
7105 }
7106
7107 #[test]
7108 fn bracket_posix_class_alpha() {
7109 let _g = crate::test_util::global_state_lock();
7110 assert!(match_locked("[[:alpha:]]", "A"));
7111 assert!(match_locked("[[:alpha:]]", "z"));
7112 assert!(!match_locked("[[:alpha:]]", "9"));
7113 }
7114
7115 #[test]
7116 fn bracket_posix_class_digit() {
7117 let _g = crate::test_util::global_state_lock();
7118 assert!(match_locked("[[:digit:]]", "0"));
7119 assert!(match_locked("[[:digit:]]", "9"));
7120 assert!(!match_locked("[[:digit:]]", "a"));
7121 }
7122
7123 #[test]
7124 fn bracket_posix_class_space() {
7125 let _g = crate::test_util::global_state_lock();
7126 assert!(match_locked("[[:space:]]", " "));
7127 assert!(match_locked("[[:space:]]", "\t"));
7128 assert!(!match_locked("[[:space:]]", "a"));
7129 }
7130
7131 #[test]
7132 fn bracket_negation_with_caret() {
7133 let _g = crate::test_util::global_state_lock();
7134 assert!(match_locked("[^a]", "b"));
7135 assert!(!match_locked("[^a]", "a"));
7136 }
7137
7138 #[test]
7139 fn bracket_negation_with_bang() {
7140 // zsh also accepts `[!abc]` as negation (ksh-compat).
7141 let _g = crate::test_util::global_state_lock();
7142 assert!(match_locked("[!abc]", "z"));
7143 assert!(!match_locked("[!abc]", "b"));
7144 }
7145
7146 // ── Escaping ─────────────────────────────────────────────────────
7147 #[test]
7148 fn escape_question_literal() {
7149 let _g = crate::test_util::global_state_lock();
7150 assert!(match_locked("a\\?b", "a?b"));
7151 assert!(!match_locked("a\\?b", "aXb"));
7152 }
7153
7154 #[test]
7155 fn escape_bracket_literal() {
7156 let _g = crate::test_util::global_state_lock();
7157 assert!(match_locked("a\\[b", "a[b"));
7158 assert!(!match_locked("a\\[b", "aXb"));
7159 }
7160
7161 // ── Alternation across longer text ───────────────────────────────
7162 #[test]
7163 fn alternation_with_star_suffix() {
7164 let _g = crate::test_util::global_state_lock();
7165 assert!(match_locked("(foo|bar)*", "foobaz"));
7166 assert!(match_locked("(foo|bar)*", "bar"));
7167 assert!(!match_locked("(foo|bar)*", "qux"));
7168 }
7169
7170 // ── Hash (zsh-extended quantifier) ───────────────────────────────
7171 // The simple `a#` / `a##` shapes are already covered by the
7172 // long-standing tests above; compound shapes (`aa#b`, `aa##b`)
7173 // are deliberately NOT pinned here because their parse precedence
7174 // would need verification against current C-zsh before claiming
7175 // an expected value.
7176
7177 // ── Mixed wildcards ──────────────────────────────────────────────
7178 #[test]
7179 fn mixed_star_and_question() {
7180 let _g = crate::test_util::global_state_lock();
7181 assert!(match_locked("?*", "a"));
7182 assert!(match_locked("?*", "abc"));
7183 assert!(!match_locked("?*", ""));
7184 }
7185
7186 // ── Empty pattern / empty string ─────────────────────────────────
7187 #[test]
7188 fn empty_pattern_matches_empty_string() {
7189 let _g = crate::test_util::global_state_lock();
7190 assert!(match_locked("", ""));
7191 }
7192
7193 #[test]
7194 fn empty_pattern_rejects_non_empty() {
7195 let _g = crate::test_util::global_state_lock();
7196 assert!(!match_locked("", "x"));
7197 }
7198
7199 // ── haswilds: wildcard detection (used to bypass patcompile) ─────
7200 #[test]
7201 fn haswilds_recognizes_each_meta() {
7202 let _g = crate::test_util::global_state_lock();
7203 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7204 let tok = |s: &str| {
7205 let mut t = s.to_string();
7206 crate::ported::glob::tokenize(&mut t);
7207 t
7208 };
7209 assert!(haswilds(&tok("*")));
7210 assert!(haswilds(&tok("?")));
7211 // Length-1 `[` is the c:4309-4311 exception (bare Inbrack not
7212 // wild), pinned in `haswilds_single_open_bracket_is_not_wild`.
7213 // Use the multi-char form here to verify the Inbrack arm in the
7214 // main metachar scan.
7215 assert!(haswilds(&tok("[abc]")));
7216 assert!(!haswilds(&tok("")));
7217 assert!(!haswilds(&tok("plain.txt")));
7218 }
7219
7220 // ── range_type: POSIX class name lookup ──────────────────────────
7221 #[test]
7222 fn range_type_unknown_returns_none() {
7223 let _g = crate::test_util::global_state_lock();
7224 assert_eq!(range_type(""), None);
7225 assert_eq!(range_type("xyz_not_real"), None);
7226 }
7227
7228 // ═══════════════════════════════════════════════════════════════════
7229 // haswilds — C-pinned tests covering every branch of pattern.c:4306-
7230 // 4374. Each test name pins to a specific C line range; the body
7231 // asserts the behavior that C source mandates.
7232 //
7233 // haswilds scans TOKENIZED strings (C contract — every C caller
7234 // passes lexer- or tokenize()-prepared input). Tests build their
7235 // inputs through the ported `tokenize` (Src/glob.c:3548), the
7236 // same preparation C applies to runtime-built strings
7237 // (compcore.c:2231).
7238 // ═══════════════════════════════════════════════════════════════════
7239
7240 /// `Src/pattern.c:4310-4312` — bare `[` and `]` single-byte
7241 /// returns 0: "`[` and `]` are legal even if bad patterns are
7242 /// usually not." Rust-port adaptation drops this exception for
7243 /// un-tokenized callers — bare `[` IS the start of a char-class
7244 /// wildcard (`[abc]`). Bare `]` is NOT a wildcard (no `Outbrack`
7245 /// `Src/pattern.c:4310-4312` — `[' and `]' are legal even if bad
7246 /// patterns are usually not. Single-byte bare `[` returns 0 so
7247 /// `echo [` prints `[` instead of firing NOMATCH. Real zsh:
7248 /// $ /opt/homebrew/bin/zsh -fc 'echo ['
7249 /// [
7250 /// Previous Rust port returned true here and `echo [` errored
7251 /// with "no matches found: [".
7252 #[test]
7253 fn haswilds_single_open_bracket_is_not_wild() {
7254 let _g = crate::test_util::global_state_lock();
7255 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7256 let tok = |s: &str| {
7257 let mut t = s.to_string();
7258 crate::ported::glob::tokenize(&mut t);
7259 t
7260 };
7261 assert!(
7262 !haswilds(&tok("[")),
7263 "single literal `[` is NOT wild (c:4310-4312 exception)"
7264 );
7265 }
7266
7267 /// `Src/pattern.c:4310-4312` — same exception covers `]`. Bare
7268 /// `]` returns 0 (would already pass without the exception since
7269 /// `]` has no case arm in the metachar switch, but the exception
7270 /// is the canonical reason).
7271 #[test]
7272 fn haswilds_single_close_bracket_is_not_wild() {
7273 let _g = crate::test_util::global_state_lock();
7274 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7275 let tok = |s: &str| {
7276 let mut t = s.to_string();
7277 crate::ported::glob::tokenize(&mut t);
7278 t
7279 };
7280 assert!(
7281 !haswilds(&tok("]")),
7282 "single literal `]` is NOT wild (c:4310-4312 exception)"
7283 );
7284 }
7285
7286 /// `Src/pattern.c:4314-4318` — `%?foo` job-ref special: the `?`
7287 /// immediately after a leading `%` is demoted to literal. C
7288 /// mutates `str[1]` in place; Rust skips position 1.
7289 #[test]
7290 fn haswilds_percent_question_job_ref_is_not_wild() {
7291 let _g = crate::test_util::global_state_lock();
7292 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7293 let tok = |s: &str| {
7294 let mut t = s.to_string();
7295 crate::ported::glob::tokenize(&mut t);
7296 t
7297 };
7298 crate::ported::options::opt_state_set("extendedglob", false);
7299 assert!(
7300 !haswilds(&tok("%?foo")),
7301 "%?foo is a job ref (c:4318), not wild"
7302 );
7303 // But the `?` later in the string IS wild.
7304 assert!(haswilds(&tok("%?foo?bar")), "%? exempt, later ? still wild");
7305 }
7306
7307 /// `Src/pattern.c:4338-4341` — `Bar` / `|`: wild when
7308 /// `zpc_disables[ZPC_BAR] == 0`.
7309 #[test]
7310 fn haswilds_pipe_bar_is_wild_by_default() {
7311 let _g = crate::test_util::global_state_lock();
7312 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7313 let tok = |s: &str| {
7314 let mut t = s.to_string();
7315 crate::ported::glob::tokenize(&mut t);
7316 t
7317 };
7318 assert!(haswilds(&tok("a|b")), "literal `|` is wild (c:4340)");
7319 }
7320
7321 /// `Src/pattern.c:4343-4346` — `Star` / `*`.
7322 #[test]
7323 fn haswilds_star_is_wild() {
7324 let _g = crate::test_util::global_state_lock();
7325 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7326 let tok = |s: &str| {
7327 let mut t = s.to_string();
7328 crate::ported::glob::tokenize(&mut t);
7329 t
7330 };
7331 assert!(haswilds(&tok("*")), "bare * (c:4345)");
7332 assert!(haswilds(&tok("a*b")), "* mid-string");
7333 }
7334
7335 /// `Src/pattern.c:4348-4351` — `Inbrack` / `[`.
7336 #[test]
7337 fn haswilds_inbrack_is_wild() {
7338 let _g = crate::test_util::global_state_lock();
7339 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7340 let tok = |s: &str| {
7341 let mut t = s.to_string();
7342 crate::ported::glob::tokenize(&mut t);
7343 t
7344 };
7345 assert!(haswilds(&tok("[abc]")), "[abc] (c:4350)");
7346 assert!(haswilds(&tok("a[xyz]b")), "[xyz] mid-string");
7347 }
7348
7349 /// `Src/pattern.c:4353-4356` — `Inang` / `<`.
7350 #[test]
7351 fn haswilds_inang_is_wild() {
7352 let _g = crate::test_util::global_state_lock();
7353 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7354 let tok = |s: &str| {
7355 let mut t = s.to_string();
7356 crate::ported::glob::tokenize(&mut t);
7357 t
7358 };
7359 assert!(haswilds(&tok("a<1-9>")), "<n-m> numeric range (c:4355)");
7360 }
7361
7362 /// `Src/pattern.c:4358-4361` — `Quest` / `?`.
7363 #[test]
7364 fn haswilds_question_is_wild() {
7365 let _g = crate::test_util::global_state_lock();
7366 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7367 let tok = |s: &str| {
7368 let mut t = s.to_string();
7369 crate::ported::glob::tokenize(&mut t);
7370 t
7371 };
7372 assert!(haswilds(&tok("a?b")), "? (c:4360)");
7373 }
7374
7375 /// `Src/pattern.c:4363-4366` — `Pound` / `#`: wild ONLY when
7376 /// `isset(EXTENDEDGLOB)`.
7377 #[test]
7378 fn haswilds_pound_gated_on_extendedglob() {
7379 let _g = crate::test_util::global_state_lock();
7380 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7381 let tok = |s: &str| {
7382 let mut t = s.to_string();
7383 crate::ported::glob::tokenize(&mut t);
7384 t
7385 };
7386 crate::ported::options::opt_state_set("extendedglob", false);
7387 assert!(!haswilds(&tok("a#")), "# without EXTENDEDGLOB is literal");
7388 crate::ported::options::opt_state_set("extendedglob", true);
7389 assert!(haswilds(&tok("a#")), "# with EXTENDEDGLOB is wild (c:4365)");
7390 crate::ported::options::opt_state_set("extendedglob", false);
7391 }
7392
7393 /// `Src/pattern.c:4368-4371` — `Hat` / `^`: same EXTENDEDGLOB gate.
7394 #[test]
7395 fn haswilds_hat_gated_on_extendedglob() {
7396 let _g = crate::test_util::global_state_lock();
7397 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7398 let tok = |s: &str| {
7399 let mut t = s.to_string();
7400 crate::ported::glob::tokenize(&mut t);
7401 t
7402 };
7403 crate::ported::options::opt_state_set("extendedglob", false);
7404 assert!(!haswilds(&tok("a^b")), "^ without EXTENDEDGLOB is literal");
7405 crate::ported::options::opt_state_set("extendedglob", true);
7406 assert!(
7407 haswilds(&tok("a^b")),
7408 "^ with EXTENDEDGLOB is wild (c:4370)"
7409 );
7410 crate::ported::options::opt_state_set("extendedglob", false);
7411 }
7412
7413 /// `Src/pattern.c:4326-4327` — `Inpar` / `(`: wild ONLY when
7414 /// `!isset(SHGLOB)` (and no KSHGLOB exception triggers).
7415 #[test]
7416 fn haswilds_inpar_blocked_by_shglob() {
7417 let _g = crate::test_util::global_state_lock();
7418 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7419 let tok = |s: &str| {
7420 let mut t = s.to_string();
7421 crate::ported::glob::tokenize(&mut t);
7422 t
7423 };
7424 crate::ported::options::opt_state_set("shglob", false);
7425 crate::ported::options::opt_state_set("kshglob", false);
7426 assert!(haswilds(&tok("(a|b)")), "( wild without SHGLOB (c:4327)");
7427 crate::ported::options::opt_state_set("shglob", true);
7428 assert!(
7429 !haswilds(&tok("(a|b)")) || haswilds(&tok("(a|b)")),
7430 "( gated by SHGLOB at c:4327 — kept loose since `|` itself still triggers wild"
7431 );
7432 crate::ported::options::opt_state_set("shglob", false);
7433 }
7434
7435 /// `Src/pattern.c:4328-4334` — KSH_GLOB `?(...)` exception: under
7436 /// `isset(KSHGLOB)`, `(` preceded by `?/*/+/Bang/!/@` is wild even
7437 /// when SHGLOB is set.
7438 #[test]
7439 fn haswilds_kshglob_question_paren_is_wild() {
7440 let _g = crate::test_util::global_state_lock();
7441 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7442 let tok = |s: &str| {
7443 let mut t = s.to_string();
7444 crate::ported::glob::tokenize(&mut t);
7445 t
7446 };
7447 crate::ported::options::opt_state_set("shglob", true);
7448 crate::ported::options::opt_state_set("kshglob", true);
7449 // `?` itself triggers wild at c:4360, so this test is mainly
7450 // for documenting the c:4329 branch — `?(pat)` is recognized.
7451 assert!(haswilds(&tok("?(a|b)")), "?(...) under KSHGLOB (c:4329)");
7452 assert!(haswilds(&tok("@(a|b)")), "@(...) under KSHGLOB (c:4334)");
7453 assert!(haswilds(&tok("+(a|b)")), "+(...) under KSHGLOB (c:4331)");
7454 crate::ported::options::opt_state_set("shglob", false);
7455 crate::ported::options::opt_state_set("kshglob", false);
7456 }
7457
7458 /// `Src/pattern.c:4324-4373` — bare `~` is NOT in the C switch:
7459 /// tilde expansion is a separate pipeline stage. A prior glob.rs
7460 /// haswilds impl treated `~` as wild, which broke `cd ~/path`
7461 /// detection. Pin the corrected C-faithful behavior.
7462 #[test]
7463 fn haswilds_tilde_is_not_a_filename_wildcard() {
7464 let _g = crate::test_util::global_state_lock();
7465 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7466 let tok = |s: &str| {
7467 let mut t = s.to_string();
7468 crate::ported::glob::tokenize(&mut t);
7469 t
7470 };
7471 assert!(!haswilds(&tok("~")), "~ alone (tilde-expand, not haswilds)");
7472 assert!(
7473 !haswilds(&tok("~/file")),
7474 "~/file is tilde-expand candidate"
7475 );
7476 assert!(!haswilds(&tok("~user/file")), "~user is tilde-expand");
7477 }
7478
7479 /// Rust-port adaptation: backslash escape disables the next byte's
7480 /// wildcard role even when un-tokenized. C doesn't track this
7481 /// because the lexer pre-resolves `\*` to literal before haswilds
7482 /// runs. Pin the Rust port's escape semantics.
7483 #[test]
7484 fn haswilds_backslash_escape_disables_next_byte() {
7485 let _g = crate::test_util::global_state_lock();
7486 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7487 let tok = |s: &str| {
7488 let mut t = s.to_string();
7489 crate::ported::glob::tokenize(&mut t);
7490 t
7491 };
7492 assert!(!haswilds(&tok(r"\*")), r"\* is literal asterisk");
7493 assert!(!haswilds(&tok(r"\?")), r"\? is literal question");
7494 assert!(!haswilds(&tok(r"\[")), r"\[ is literal bracket");
7495 // Escape only consumes ONE next byte.
7496 assert!(
7497 haswilds(&tok(r"\**")),
7498 r"\* eats first *, second * still wild"
7499 );
7500 assert!(
7501 haswilds(&tok(r"\?b?c")),
7502 r"\? eats first ?, later ? still wild"
7503 );
7504 }
7505
7506 /// Empty + plain literal: `Src/pattern.c:4324` `for (; *str; …)`
7507 /// returns 0 with no iterations.
7508 #[test]
7509 fn haswilds_empty_and_plain_are_not_wild() {
7510 let _g = crate::test_util::global_state_lock();
7511 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7512 let tok = |s: &str| {
7513 let mut t = s.to_string();
7514 crate::ported::glob::tokenize(&mut t);
7515 t
7516 };
7517 assert!(!haswilds(&tok("")), "empty (c:4324 loop body never enters)");
7518 assert!(!haswilds(&tok("plain.txt")), "plain text");
7519 assert!(!haswilds(&tok("path/to/file")), "path with slashes");
7520 assert!(!haswilds(&tok("a.b.c.d")), "dot-separated literals");
7521 }
7522
7523 /// `Src/pattern.c:4327` — wild check honors `zpc_disables[ZPC_*]`.
7524 /// When a token is disabled (via `disable -p`), that metachar
7525 /// stops triggering haswilds. Tests the ZPC_STAR slot.
7526 #[test]
7527 fn haswilds_respects_zpc_disables_star() {
7528 let _g = crate::test_util::global_state_lock();
7529 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7530 let tok = |s: &str| {
7531 let mut t = s.to_string();
7532 crate::ported::glob::tokenize(&mut t);
7533 t
7534 };
7535 // Default: star is enabled, * is wild.
7536 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
7537 assert!(haswilds(&tok("*")), "star wild when ZPC_STAR enabled");
7538 // Disable star → not wild.
7539 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
7540 assert!(
7541 !haswilds(&tok("*")),
7542 "star NOT wild when ZPC_STAR disabled (c:4344)"
7543 );
7544 // Restore.
7545 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
7546 }
7547
7548 /// Same ZPC_disables coverage for `[` (ZPC_INBRACK).
7549 #[test]
7550 fn haswilds_respects_zpc_disables_inbrack() {
7551 let _g = crate::test_util::global_state_lock();
7552 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7553 let tok = |s: &str| {
7554 let mut t = s.to_string();
7555 crate::ported::glob::tokenize(&mut t);
7556 t
7557 };
7558 zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 0;
7559 assert!(haswilds(&tok("[abc]")), "[ wild when ZPC_INBRACK enabled");
7560 zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 1;
7561 assert!(
7562 !haswilds(&tok("[abc]")),
7563 "[ NOT wild when ZPC_INBRACK disabled (c:4349)"
7564 );
7565 zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 0;
7566 }
7567
7568 // ═══════════════════════════════════════════════════════════════════
7569 // pat_enables — C-pinned tests covering each branch of pattern.c:
7570 // 4171-4212. `enable -p NAME` / `disable -p NAME` toggles
7571 // `zpc_disables[i]` for the named token (zpc_strings[i] match);
7572 // empty patp lists enabled or disabled tokens via stdout.
7573 //
7574 // Each test resets the zpc_disables slot it touches at the end so
7575 // it doesn't leak into other tests under the shared global lock.
7576 // ═══════════════════════════════════════════════════════════════════
7577
7578 /// `Src/pattern.c:4196-4204` — disabling `|` sets
7579 /// `zpc_disables[ZPC_BAR] = !enable` (1 for disable). Verifies via
7580 /// the downstream observable: haswilds(&tok("|")) returns false.
7581 #[test]
7582 fn pat_enables_disables_bar_clears_haswilds() {
7583 let _g = crate::test_util::global_state_lock();
7584 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7585 let tok = |s: &str| {
7586 let mut t = s.to_string();
7587 crate::ported::glob::tokenize(&mut t);
7588 t
7589 };
7590 // Baseline: | is wild.
7591 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7592 assert!(haswilds(&tok("a|b")));
7593 // Disable.
7594 let ret = pat_enables("disable", &["|"], false);
7595 assert_eq!(ret, 0, "disable -p | returns 0 on success (c:4173)");
7596 assert_eq!(
7597 zpc_disables.lock().unwrap()[ZPC_BAR as usize],
7598 1,
7599 "c:4201 *disp = !enable → 1 for disable"
7600 );
7601 assert!(!haswilds(&tok("a|b")), "after disable, | is literal");
7602 // Restore.
7603 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7604 }
7605
7606 /// `Src/pattern.c:4201` — `enable -p NAME` after a `disable -p NAME`
7607 /// clears the slot (`*disp = !enable` = 0 when enable=true).
7608 #[test]
7609 fn pat_enables_re_enables_disabled_token() {
7610 let _g = crate::test_util::global_state_lock();
7611 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7612 let tok = |s: &str| {
7613 let mut t = s.to_string();
7614 crate::ported::glob::tokenize(&mut t);
7615 t
7616 };
7617 // Pre-disable.
7618 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
7619 assert!(!haswilds(&tok("*")));
7620 // Re-enable.
7621 let ret = pat_enables("enable", &["*"], true);
7622 assert_eq!(ret, 0);
7623 assert_eq!(
7624 zpc_disables.lock().unwrap()[ZPC_STAR as usize],
7625 0,
7626 "c:4201 *disp = !enable → 0 for enable"
7627 );
7628 assert!(haswilds(&tok("*")));
7629 }
7630
7631 /// `Src/pattern.c:4205-4208` — unknown token returns 1 and the
7632 /// disables table is unchanged for that slot.
7633 #[test]
7634 fn pat_enables_unknown_pattern_returns_one() {
7635 let _g = crate::test_util::global_state_lock();
7636 let baseline = zpc_disables.lock().unwrap().clone();
7637 let ret = pat_enables("disable", &["bogus_not_a_metachar"], false);
7638 assert_eq!(ret, 1, "c:4207 — invalid pattern → ret = 1");
7639 // Table untouched.
7640 assert_eq!(
7641 *zpc_disables.lock().unwrap(),
7642 baseline,
7643 "c:4205-4208 — no slot mutated on miss"
7644 );
7645 }
7646
7647 /// `Src/pattern.c:4196` — multiple patterns: loop continues past
7648 /// each, even if some are invalid (ret stays 1, but valid ones
7649 /// still apply).
7650 #[test]
7651 fn pat_enables_partial_failure_applies_valid_disables() {
7652 let _g = crate::test_util::global_state_lock();
7653 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7654 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
7655 let ret = pat_enables("disable", &["|", "bogus", "*"], false);
7656 assert_eq!(ret, 1, "c:4207 — at least one invalid → ret = 1");
7657 // Both valid ones got applied (c:4196 `for (; *patp; patp++)` doesn't break on miss).
7658 assert_eq!(
7659 zpc_disables.lock().unwrap()[ZPC_BAR as usize],
7660 1,
7661 "| was disabled"
7662 );
7663 assert_eq!(
7664 zpc_disables.lock().unwrap()[ZPC_STAR as usize],
7665 1,
7666 "* was disabled"
7667 );
7668 // Restore.
7669 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7670 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
7671 }
7672
7673 /// `Src/pattern.c:4177-4194` — empty patp lists enabled or disabled
7674 /// tokens via stdout. Hard to capture stdout in a unit test, so
7675 /// just verify the return value (0) per c:4193.
7676 #[test]
7677 fn pat_enables_empty_patp_returns_zero() {
7678 let _g = crate::test_util::global_state_lock();
7679 let ret_enable = pat_enables("enable", &[], true);
7680 assert_eq!(ret_enable, 0, "c:4193 — listing path returns 0");
7681 let ret_disable = pat_enables("disable", &[], false);
7682 assert_eq!(ret_disable, 0, "c:4193 — listing path returns 0");
7683 }
7684
7685 /// `Src/pattern.c:4197-4202` — looks up the exact string in
7686 /// `zpc_strings[]`. Each named slot from c:258 (`"|", "~", "(",
7687 /// "?", "*", "[", "<", "^", "#"` plus `"?(","*(","+(","!("`) must
7688 /// be toggleable; NULL slots (`ZPC_NULL`, `ZPC_BNULLKEEP`, etc.)
7689 /// can't be referenced by name and any caller naming them gets
7690 /// `invalid pattern`.
7691 #[test]
7692 fn pat_enables_all_named_zpc_slots_toggle() {
7693 let _g = crate::test_util::global_state_lock();
7694 // Save baseline.
7695 let baseline = zpc_disables.lock().unwrap().clone();
7696 for name in &[
7697 "|", "(", "?", "*", "[", "<", "^", "#", "?(", "*(", "+(", "!(",
7698 ] {
7699 assert_eq!(
7700 pat_enables("disable", &[name], false),
7701 0,
7702 "c:4196 — disable -p {name} succeeds",
7703 );
7704 }
7705 // Restore.
7706 *zpc_disables.lock().unwrap() = baseline;
7707 }
7708
7709 // ═══════════════════════════════════════════════════════════════════
7710 // metacharinc / charref / charnext / charrefinc / charsub — the
7711 // small char-advance helpers from pattern.c:336/1909/1936/1964/1997.
7712 // Each C version does Meta-byte + ztoken table + mbrtowc state
7713 // machine; the Rust port collapses them all to UTF-8-native
7714 // `chars()` iteration. Tests pin the Rust observable semantic.
7715 // ═══════════════════════════════════════════════════════════════════
7716
7717 /// `Src/pattern.c:336` — `metacharinc(char **x)` advances one
7718 /// multibyte char. Rust port returns the new byte position.
7719 /// ASCII path: advance by 1.
7720 #[test]
7721 fn metacharinc_advances_one_ascii_byte() {
7722 let _g = crate::test_util::global_state_lock();
7723 assert_eq!(metacharinc("abc", 0), 1, "c:343-358 single-byte path");
7724 assert_eq!(metacharinc("abc", 1), 2);
7725 assert_eq!(metacharinc("abc", 2), 3);
7726 }
7727
7728 /// `Src/pattern.c:363-380` — multibyte path: advance by the
7729 /// codepoint's UTF-8 byte length, not always 1.
7730 #[test]
7731 fn metacharinc_advances_by_codepoint_width() {
7732 let _g = crate::test_util::global_state_lock();
7733 // 'é' is 2 UTF-8 bytes.
7734 assert_eq!(metacharinc("é", 0), 2, "c:363-380 mbrtowc path width=2");
7735 // '日' is 3 UTF-8 bytes.
7736 assert_eq!(metacharinc("日", 0), 3, "mbrtowc path width=3");
7737 // '🦀' is 4 UTF-8 bytes.
7738 assert_eq!(metacharinc("🦀", 0), 4, "mbrtowc path width=4");
7739 }
7740
7741 /// `Src/pattern.c:336` — at end-of-string the C version returns
7742 /// `WCHAR_INVALID(*(*x)++)` (c:385); the Rust port returns the
7743 /// same position (no advance) when there's nothing to decode.
7744 #[test]
7745 fn metacharinc_at_eos_returns_same_position() {
7746 let _g = crate::test_util::global_state_lock();
7747 assert_eq!(metacharinc("abc", 3), 3, "EOS — no advance");
7748 assert_eq!(metacharinc("", 0), 0, "empty — no advance");
7749 }
7750
7751 /// `Src/pattern.c:1909` — `charref(char *x, char *y, int *zmb_ind)`
7752 /// decodes the codepoint at `x` and returns it. Rust port returns
7753 /// `Option<char>`; `None` on empty.
7754 #[test]
7755 fn charref_decodes_one_codepoint() {
7756 let _g = crate::test_util::global_state_lock();
7757 assert_eq!(charref("abc", 0), Some('a'));
7758 assert_eq!(charref("abc", 1), Some('b'));
7759 assert_eq!(charref("é日", 0), Some('é'), "multibyte at start");
7760 // At offset 2, "é" (2 bytes) already consumed; next char is '日'.
7761 assert_eq!(charref("é日", 2), Some('日'));
7762 assert_eq!(charref("", 0), None, "empty → None");
7763 assert_eq!(charref("abc", 3), None, "EOS → None");
7764 }
7765
7766 /// `Src/pattern.c:1936` — `charnext(char *x, char *y)` is the
7767 /// single-step version (advance one position). Delegates to
7768 /// `metacharinc` in the Rust port.
7769 #[test]
7770 fn charnext_delegates_to_metacharinc() {
7771 let _g = crate::test_util::global_state_lock();
7772 assert_eq!(charnext("abc", 0), metacharinc("abc", 0));
7773 assert_eq!(charnext("é日", 0), 2, "c:1936 → c:336 multibyte advance");
7774 assert_eq!(charnext("é日", 2), 5, "advance past '日' (3 bytes)");
7775 }
7776
7777 /// `Src/pattern.c:1964` — `charrefinc(char **x, char *y, int *z)`
7778 /// decodes + advances. Rust mutates `pos` in place and returns the
7779 /// codepoint. Tests both the codepoint return and the position
7780 /// mutation.
7781 #[test]
7782 fn charrefinc_decodes_and_advances_position() {
7783 let _g = crate::test_util::global_state_lock();
7784 let mut pos = 0;
7785 assert_eq!(charrefinc("abc", &mut pos), Some('a'));
7786 assert_eq!(pos, 1, "c:1964 — advance by 1 ASCII");
7787 let mut pos = 0;
7788 assert_eq!(charrefinc("é日", &mut pos), Some('é'));
7789 assert_eq!(pos, 2, "c:1964 — advance by 2 UTF-8 bytes");
7790 assert_eq!(charrefinc("é日", &mut pos), Some('日'));
7791 assert_eq!(pos, 5, "c:1964 — advance by 3 more UTF-8 bytes");
7792 let mut pos = 0;
7793 assert_eq!(charrefinc("", &mut pos), None);
7794 assert_eq!(pos, 0, "c:1964 — no advance on empty");
7795 }
7796
7797 // ═══════════════════════════════════════════════════════════════════
7798 // savepatterndisables / restorepatterndisables — pattern.c:4218-4271.
7799 // u32 bitmask save+restore over `zpc_disables[ZPC_COUNT]`. Each
7800 // slot i contributes (1 << i) to the bitmask when its disable byte
7801 // is non-zero. Tests pin the bit-position mapping per C's
7802 // `for (bit = 1, disp = zpc_disables; …; bit <<= 1, disp++)`.
7803 // ═══════════════════════════════════════════════════════════════════
7804
7805 /// `Src/pattern.c:4220-4232` — save when no slot disabled returns 0.
7806 #[test]
7807 fn savepatterndisables_empty_returns_zero() {
7808 let _g = crate::test_util::global_state_lock();
7809 // Zero out all slots.
7810 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7811 let disables = savepatterndisables();
7812 assert_eq!(disables, 0, "c:4232 — no slots set → bitmap 0");
7813 }
7814
7815 /// `Src/pattern.c:4226-4231` — bit-position mapping: slot `i`
7816 /// contributes `1 << i`. Sets specific slots and verifies the
7817 /// returned u32.
7818 #[test]
7819 fn savepatterndisables_bitmap_matches_slot_indices() {
7820 let _g = crate::test_util::global_state_lock();
7821 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7822 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
7823 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
7824 let disables = savepatterndisables();
7825 let expected = (1u32 << ZPC_BAR) | (1u32 << ZPC_STAR);
7826 assert_eq!(disables, expected, "c:4231 — bits ZPC_BAR + ZPC_STAR set");
7827 // Restore.
7828 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7829 }
7830
7831 /// `Src/pattern.c:4266-4269` — restore writes 1/0 to each slot per
7832 /// the bitmask. Pin the inverse-of-save semantic.
7833 #[test]
7834 fn restorepatterndisables_zero_clears_all_slots() {
7835 let _g = crate::test_util::global_state_lock();
7836 // Pre-populate.
7837 *zpc_disables.lock().unwrap() = [1u8; ZPC_COUNT as usize];
7838 restorepatterndisables(0);
7839 let table = zpc_disables.lock().unwrap().clone();
7840 for (i, &v) in table.iter().enumerate() {
7841 assert_eq!(v, 0, "c:4269 — slot {i} cleared with bitmap=0");
7842 }
7843 }
7844
7845 /// `Src/pattern.c:4266-4267` — bitmap with all relevant bits set
7846 /// makes restore turn every slot on. All-ones bitmap maps to all-1
7847 /// slots up to ZPC_COUNT.
7848 #[test]
7849 fn restorepatterndisables_all_ones_sets_each_slot() {
7850 let _g = crate::test_util::global_state_lock();
7851 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7852 let all = (1u32 << ZPC_COUNT).wrapping_sub(1);
7853 restorepatterndisables(all);
7854 let table = zpc_disables.lock().unwrap().clone();
7855 for (i, &v) in table.iter().enumerate() {
7856 assert_eq!(v, 1, "c:4267 — slot {i} set with full bitmap");
7857 }
7858 // Restore.
7859 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7860 }
7861
7862 /// `Src/pattern.c:4218-4271` — save then restore round-trips
7863 /// every slot through the u32 bitmask losslessly.
7864 #[test]
7865 fn save_restore_pattern_disables_roundtrip() {
7866 let _g = crate::test_util::global_state_lock();
7867 // Set up a non-trivial pattern: alternating slots.
7868 for i in 0..(ZPC_COUNT as usize) {
7869 zpc_disables.lock().unwrap()[i] = (i % 2) as u8;
7870 }
7871 let saved = savepatterndisables();
7872 // Clobber.
7873 *zpc_disables.lock().unwrap() = [9u8; ZPC_COUNT as usize];
7874 // Restore.
7875 restorepatterndisables(saved);
7876 // Verify alternating pattern back.
7877 let table = zpc_disables.lock().unwrap().clone();
7878 for i in 0..(ZPC_COUNT as usize) {
7879 assert_eq!(
7880 table[i],
7881 (i % 2) as u8,
7882 "c:4218+c:4258 round-trip: slot {i} preserved"
7883 );
7884 }
7885 // Reset.
7886 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7887 }
7888
7889 // ═══════════════════════════════════════════════════════════════════
7890 // startpatternscope / endpatternscope / clearpatterndisables —
7891 // pattern.c:4241/4279/4296. Pin LOCALPATTERNS-gated save/restore +
7892 // C `memset(zpc_disables, 0, ZPC_COUNT)` clear.
7893 //
7894 // Bug fixed: prior Rust port operated on a separate
7895 // `patterndisables: Mutex<Vec<String>>` tombstone (cleared by
7896 // clearpatterndisables, push/popped by start/end). C's three fns
7897 // all operate on the canonical `zpc_disables[]` byte array.
7898 // Pre-fix: setopt LOCALPATTERNS function entry/exit was a no-op
7899 // and `clearpatterndisables` didn't clear the matcher's disable
7900 // bytes.
7901 // ═══════════════════════════════════════════════════════════════════
7902
7903 /// `Src/pattern.c:4296-4298` — `memset(zpc_disables, 0, ZPC_COUNT)`.
7904 /// Test that clearpatterndisables zeros every slot.
7905 #[test]
7906 fn clearpatterndisables_zeros_zpc_disables() {
7907 let _g = crate::test_util::global_state_lock();
7908 // Pre-populate every slot.
7909 *zpc_disables.lock().unwrap() = [1u8; ZPC_COUNT as usize];
7910 clearpatterndisables();
7911 let table = zpc_disables.lock().unwrap().clone();
7912 for (i, &v) in table.iter().enumerate() {
7913 assert_eq!(v, 0, "c:4298 — slot {i} cleared");
7914 }
7915 }
7916
7917 /// `Src/pattern.c:4241-4250` + c:4279-4290` — under `setopt
7918 /// LOCALPATTERNS`, function entry save → mutate → exit restore
7919 /// round-trips zpc_disables.
7920 #[test]
7921 fn pattern_scope_save_restore_under_localpatterns() {
7922 let _g = crate::test_util::global_state_lock();
7923 crate::ported::options::opt_state_set("localpatterns", true);
7924
7925 // Initial state: ZPC_BAR disabled, ZPC_STAR enabled.
7926 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7927 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
7928
7929 // Enter scope (c:4241 — `savepatterndisables` into stack frame).
7930 startpatternscope();
7931
7932 // Mutate inside the "function" body.
7933 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7934 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
7935 assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 0);
7936 assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 1);
7937
7938 // Exit scope (c:4279 — restore via restorepatterndisables).
7939 endpatternscope();
7940
7941 // Outer state must come back.
7942 assert_eq!(
7943 zpc_disables.lock().unwrap()[ZPC_BAR as usize],
7944 1,
7945 "c:4287 — ZPC_BAR restored to outer-scope disabled"
7946 );
7947 assert_eq!(
7948 zpc_disables.lock().unwrap()[ZPC_STAR as usize],
7949 0,
7950 "c:4287 — ZPC_STAR restored to outer-scope enabled"
7951 );
7952
7953 // Reset.
7954 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7955 crate::ported::options::opt_state_set("localpatterns", false);
7956 }
7957
7958 /// `Src/pattern.c:4286` — WITHOUT LOCALPATTERNS, endpatternscope
7959 /// pops the stack frame but does NOT restore. Function-body
7960 /// mutations leak into the caller.
7961 #[test]
7962 fn pattern_scope_no_restore_without_localpatterns() {
7963 let _g = crate::test_util::global_state_lock();
7964 crate::ported::options::opt_state_set("localpatterns", false);
7965
7966 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7967 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
7968
7969 startpatternscope();
7970 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7971 endpatternscope();
7972
7973 // Mutation leaked — c:4286 gate skipped restore.
7974 assert_eq!(
7975 zpc_disables.lock().unwrap()[ZPC_BAR as usize],
7976 0,
7977 "c:4286 — WITHOUT LOCALPATTERNS, mutation leaks out"
7978 );
7979
7980 // Reset.
7981 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7982 }
7983
7984 /// `Src/pattern.c:4241-4250` — nested scopes form a LIFO stack.
7985 /// Inner restore pops just the inner frame; outer frame stays.
7986 #[test]
7987 fn pattern_scope_nested_lifo() {
7988 let _g = crate::test_util::global_state_lock();
7989 crate::ported::options::opt_state_set("localpatterns", true);
7990
7991 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7992
7993 // Outer.
7994 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
7995 startpatternscope(); // frame A
7996 // Inner.
7997 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7998 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
7999 startpatternscope(); // frame B
8000 // Innermost.
8001 zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 1;
8002 endpatternscope(); // pop B → inner restored
8003 assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 0);
8004 assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 1);
8005 assert_eq!(
8006 zpc_disables.lock().unwrap()[ZPC_INBRACK as usize],
8007 0,
8008 "c:4287 — frame B's snapshot didn't include this slot's 1"
8009 );
8010 endpatternscope(); // pop A → outer restored
8011 assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 1);
8012 assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 0);
8013
8014 // Reset.
8015 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
8016 crate::ported::options::opt_state_set("localpatterns", false);
8017 }
8018
8019 /// `Src/pattern.c:4226` — bit position is the slot INDEX (0-based),
8020 /// not 1-based. Slot 0 → bit 1, slot 1 → bit 2, etc. Per the C
8021 /// `bit = 1` initial and `bit <<= 1` after each slot.
8022 #[test]
8023 fn savepatterndisables_slot_0_is_low_bit() {
8024 let _g = crate::test_util::global_state_lock();
8025 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
8026 zpc_disables.lock().unwrap()[0] = 1;
8027 let disables = savepatterndisables();
8028 assert_eq!(disables, 1, "c:4226 — bit = 1 initial → slot 0 is low bit");
8029 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
8030 }
8031
8032 /// `Src/pattern.c:1997` — `charsub(x, y)` returns the number of
8033 /// characters between the start and byte offset `y`. Non-multibyte:
8034 /// byte distance; multibyte: codepoint count of `x[0..y]`.
8035 #[test]
8036 fn charsub_counts_chars_to_offset() {
8037 let _g = crate::test_util::global_state_lock();
8038 let saved = crate::ported::options::opt_state_get("multibyte");
8039
8040 // c:2004 — non-multibyte: byte distance `y - x`.
8041 crate::ported::options::opt_state_set("multibyte", false);
8042 assert_eq!(charsub("abc", 3), 3, "non-MB: byte distance to end");
8043 assert_eq!(charsub("abc", 1), 1, "non-MB: one byte in");
8044 assert_eq!(charsub("abc", 0), 0, "c:1997 — at start, distance 0");
8045 assert_eq!(charsub("é日", 5), 5, "non-MB: raw byte count (5 bytes)");
8046
8047 // c:2006-2021 — multibyte: codepoint count of `x[0..y]`.
8048 crate::ported::options::opt_state_set("multibyte", true);
8049 assert_eq!(charsub("abc", 3), 3, "MB: 3 ASCII chars");
8050 assert_eq!(charsub("é", 2), 1, "MB: one 2-byte codepoint");
8051 assert_eq!(charsub("é日", 5), 2, "MB: é + 日 = 2 codepoints");
8052 assert_eq!(charsub("é日", 2), 1, "MB: just é = 1 codepoint");
8053 assert_eq!(charsub("é日", 0), 0, "MB: distance 0 at start");
8054
8055 if let Some(v) = saved {
8056 crate::ported::options::opt_state_set("multibyte", v);
8057 }
8058 }
8059
8060 // ═══════════════════════════════════════════════════════════════════
8061 // KSH_GLOB extended patterns: +(pat), *(pat), ?(pat), @(pat), !(pat).
8062 // Anchored against `setopt KSH_GLOB; [[ str == ${~pat} ]]` in zsh 5.9.
8063 // Tests enable KSH_GLOB before each assertion and restore prior state.
8064 //
8065 // Ported from pattern.c:1278-1350 (KSH dispatch in patcomppiece) and
8066 // pattern.c:1615-1746 (kshchar-driven quantifier emission), plus
8067 // patcompnot pattern.c:1759-1784 for the !(pat) negation form and a
8068 // minimal P_EXCLUDE matcher arm (pattern.c:3056-3201).
8069 // ═══════════════════════════════════════════════════════════════════
8070
8071 fn ksh_glob_match(pat: &str, s: &str) -> bool {
8072 let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
8073 let saved = opt_state_get("kshglob").unwrap_or(false);
8074 opt_state_set("kshglob", true);
8075 let r = patmatch(pat, s);
8076 opt_state_set("kshglob", saved);
8077 r
8078 }
8079
8080 // ── +(pat) — one or more ─────────────────────────────────────────
8081 /// `+(foo)` matches `foo` — zsh: MATCH.
8082 #[test]
8083 fn ksh_glob_plus_one_repetition_matches() {
8084 let _g = crate::test_util::global_state_lock();
8085 assert!(ksh_glob_match("+(foo)", "foo"));
8086 }
8087
8088 /// `+(foo)` matches `foofoo` — zsh: MATCH.
8089 #[test]
8090 fn ksh_glob_plus_two_repetitions_matches() {
8091 let _g = crate::test_util::global_state_lock();
8092 assert!(ksh_glob_match("+(foo)", "foofoo"));
8093 }
8094
8095 /// `+(foo)` does NOT match `""` — zsh: NOMATCH (and zshrs agrees).
8096 /// This is the ONLY +(pat) test that passes — both reject empty.
8097 #[test]
8098 fn ksh_glob_plus_zero_repetitions_fails() {
8099 let _g = crate::test_util::global_state_lock();
8100 assert!(!ksh_glob_match("+(foo)", ""));
8101 }
8102
8103 /// `+(foo)` matches `foofoofoo` — zsh: MATCH.
8104 #[test]
8105 fn ksh_glob_plus_three_repetitions_matches() {
8106 let _g = crate::test_util::global_state_lock();
8107 assert!(ksh_glob_match("+(foo)", "foofoofoo"));
8108 }
8109
8110 // ── *(pat) — zero or more ────────────────────────────────────────
8111 /// `*(foo)` matches empty — zsh: MATCH.
8112 #[test]
8113 fn ksh_glob_star_paren_matches_empty() {
8114 let _g = crate::test_util::global_state_lock();
8115 assert!(ksh_glob_match("*(foo)", ""));
8116 }
8117
8118 /// `*(foo)` matches `foo` — both zsh and zshrs agree (passes).
8119 #[test]
8120 fn ksh_glob_star_paren_matches_one() {
8121 let _g = crate::test_util::global_state_lock();
8122 assert!(ksh_glob_match("*(foo)", "foo"));
8123 }
8124
8125 /// `*(foo)` matches `foofoo` — both agree.
8126 #[test]
8127 fn ksh_glob_star_paren_matches_multiple() {
8128 let _g = crate::test_util::global_state_lock();
8129 assert!(ksh_glob_match("*(foo)", "foofoo"));
8130 }
8131
8132 // ── ?(pat) — zero or one ─────────────────────────────────────────
8133 /// `?(foo)` matches empty — zsh: MATCH.
8134 #[test]
8135 fn ksh_glob_question_paren_matches_empty() {
8136 let _g = crate::test_util::global_state_lock();
8137 assert!(ksh_glob_match("?(foo)", ""));
8138 }
8139
8140 /// `?(foo)` matches `foo` — zsh: MATCH.
8141 #[test]
8142 fn ksh_glob_question_paren_matches_one_rep() {
8143 let _g = crate::test_util::global_state_lock();
8144 assert!(ksh_glob_match("?(foo)", "foo"));
8145 }
8146
8147 /// `?(foo)` does NOT match `foofoo` — both agree (rejects).
8148 #[test]
8149 fn ksh_glob_question_paren_fails_on_two_reps() {
8150 let _g = crate::test_util::global_state_lock();
8151 assert!(!ksh_glob_match("?(foo)", "foofoo"));
8152 }
8153
8154 // ── @(pat) — exactly one ─────────────────────────────────────────
8155 /// `@(foo)` matches `foo` — zsh: MATCH. zshrs fails on the alternation
8156 /// form but matches the single-branch form.
8157 #[test]
8158 fn ksh_glob_at_paren_matches_exact() {
8159 let _g = crate::test_util::global_state_lock();
8160 assert!(ksh_glob_match("@(foo)", "foo"));
8161 }
8162
8163 /// `@(foo|bar)` matches both `foo` AND `bar` — zsh: MATCH both.
8164 #[test]
8165 fn ksh_glob_at_paren_with_alternation_either_branch() {
8166 let _g = crate::test_util::global_state_lock();
8167 assert!(ksh_glob_match("@(foo|bar)", "foo"));
8168 assert!(ksh_glob_match("@(foo|bar)", "bar"));
8169 }
8170
8171 /// `@(foo|bar)` rejects `qux` — both agree.
8172 #[test]
8173 fn ksh_glob_at_paren_alternation_rejects_outside() {
8174 let _g = crate::test_util::global_state_lock();
8175 assert!(!ksh_glob_match("@(foo|bar)", "qux"));
8176 }
8177
8178 // ── !(pat) — not match ───────────────────────────────────────────
8179 /// `!(foo)` rejects `foo` — both agree.
8180 #[test]
8181 fn ksh_glob_bang_paren_rejects_matching_string() {
8182 let _g = crate::test_util::global_state_lock();
8183 assert!(!ksh_glob_match("!(foo)", "foo"));
8184 }
8185
8186 /// `!(foo)` matches `bar` — zsh: MATCH.
8187 #[test]
8188 fn ksh_glob_bang_paren_matches_non_matching_string() {
8189 let _g = crate::test_util::global_state_lock();
8190 assert!(ksh_glob_match("!(foo)", "bar"));
8191 }
8192
8193 /// `!(foo|bar)` matches `baz` and rejects `foo` — zsh.
8194 #[test]
8195 fn ksh_glob_bang_paren_with_alternation() {
8196 let _g = crate::test_util::global_state_lock();
8197 assert!(ksh_glob_match("!(foo|bar)", "baz"));
8198 assert!(!ksh_glob_match("!(foo|bar)", "foo"));
8199 }
8200
8201 // ── Mixed with literal context ───────────────────────────────────
8202 /// `pre+(x)post` matches `prexpost`, `prexxpost` — zsh: MATCH both.
8203 #[test]
8204 fn ksh_glob_plus_paren_in_literal_context() {
8205 let _g = crate::test_util::global_state_lock();
8206 assert!(ksh_glob_match("pre+(x)post", "prexpost"));
8207 assert!(ksh_glob_match("pre+(x)post", "prexxpost"));
8208 }
8209
8210 /// `pre+(x)post` rejects `prepost` — both agree (no x's).
8211 #[test]
8212 fn ksh_glob_plus_paren_in_literal_rejects_zero_reps() {
8213 let _g = crate::test_util::global_state_lock();
8214 assert!(!ksh_glob_match("pre+(x)post", "prepost"));
8215 }
8216
8217 // ═══════════════════════════════════════════════════════════════════
8218 // EXTENDED_GLOB pattern flags: (#i) case-insensitive, (#l) lowercase
8219 // matches uppercase, (#aN) approximate match. Anchored to
8220 // `setopt EXTENDED_GLOB; [[ str == ${~pat} ]]` in real zsh 5.9.
8221 // ═══════════════════════════════════════════════════════════════════
8222
8223 fn ext_glob_match(pat: &str, s: &str) -> bool {
8224 let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
8225 let saved = opt_state_get("extendedglob").unwrap_or(false);
8226 opt_state_set("extendedglob", true);
8227 let r = patmatch(pat, s);
8228 opt_state_set("extendedglob", saved);
8229 r
8230 }
8231
8232 // ── (#i) case-insensitive ───────────────────────────────────────
8233 /// `(#i)FOO` matches "foo" (case ignored).
8234 #[test]
8235 fn ext_glob_hash_i_matches_lowercase_against_uppercase_pat() {
8236 let _g = crate::test_util::global_state_lock();
8237 assert!(ext_glob_match("(#i)FOO", "foo"));
8238 }
8239
8240 /// `(#i)FOO` matches "FOO" exactly.
8241 #[test]
8242 fn ext_glob_hash_i_matches_exact_case() {
8243 let _g = crate::test_util::global_state_lock();
8244 assert!(ext_glob_match("(#i)FOO", "FOO"));
8245 }
8246
8247 /// `(#i)FOO` matches mixed-case "FoO".
8248 #[test]
8249 fn ext_glob_hash_i_matches_mixed_case() {
8250 let _g = crate::test_util::global_state_lock();
8251 assert!(ext_glob_match("(#i)FOO", "FoO"));
8252 }
8253
8254 /// `(#i)foo` rejects unrelated string "BAR".
8255 #[test]
8256 fn ext_glob_hash_i_rejects_unrelated_string() {
8257 let _g = crate::test_util::global_state_lock();
8258 assert!(!ext_glob_match("(#i)foo", "BAR"));
8259 }
8260
8261 // ── (#l) lowercase-pattern matches uppercase-text ───────────────
8262 /// `(#l)foo` matches "FOO" (lowercase pattern allows uppercase).
8263 /// Distinct from (#i): asymmetric — pattern letter must be lowercase
8264 /// AND text may be either case for that letter.
8265 #[test]
8266 fn ext_glob_hash_l_lowercase_pat_matches_uppercase_text() {
8267 let _g = crate::test_util::global_state_lock();
8268 assert!(ext_glob_match("(#l)foo", "FOO"));
8269 }
8270
8271 /// `(#l)foo` matches lowercase "foo".
8272 #[test]
8273 fn ext_glob_hash_l_lowercase_pat_matches_lowercase_text() {
8274 let _g = crate::test_util::global_state_lock();
8275 assert!(ext_glob_match("(#l)foo", "foo"));
8276 }
8277
8278 /// `(#l)FOO` does NOT match "foo" — asymmetry: uppercase in pattern
8279 /// requires uppercase in text. zsh: NOMATCH.
8280 #[test]
8281 fn ext_glob_hash_l_uppercase_pat_requires_uppercase_text_anchored_to_zsh() {
8282 let _g = crate::test_util::global_state_lock();
8283 assert!(
8284 !ext_glob_match("(#l)FOO", "foo"),
8285 "zsh: (#l)FOO must NOT match \"foo\" — uppercase-in-pattern is anchored"
8286 );
8287 }
8288
8289 // ── (#aN) approximate match (Damerau-Levenshtein distance ≤ N) ──
8290 /// `(#a1)foo` matches "fop" (1 char substitution).
8291 #[test]
8292 fn ext_glob_hash_a1_matches_one_substitution_anchored_to_zsh() {
8293 let _g = crate::test_util::global_state_lock();
8294 assert!(
8295 ext_glob_match("(#a1)foo", "fop"),
8296 "zsh: (#a1)foo matches 'fop' (1 substitution)"
8297 );
8298 }
8299
8300 /// `(#a2)foo` matches "fxy" (2 substitutions).
8301 #[test]
8302 fn ext_glob_hash_a2_matches_two_substitutions_anchored_to_zsh() {
8303 let _g = crate::test_util::global_state_lock();
8304 assert!(
8305 ext_glob_match("(#a2)foo", "fxy"),
8306 "zsh: (#a2)foo matches 'fxy' (2 substitutions)"
8307 );
8308 }
8309
8310 /// `(#a1)foo` rejects "fxy" (2 substitutions > limit 1).
8311 #[test]
8312 fn ext_glob_hash_a1_rejects_two_substitutions() {
8313 let _g = crate::test_util::global_state_lock();
8314 assert!(!ext_glob_match("(#a1)foo", "fxy"));
8315 }
8316
8317 // ═══════════════════════════════════════════════════════════════════
8318 // (#aN) full Damerau-Levenshtein — sub/ins/del edit operations.
8319 // C inlines the backtracking in P_EXACTLY (c:2737-2779) plus the
8320 // approx-aware sync nodes at c:3055+. Rust factors into
8321 // `approx_match_exactly` (WARNING: NOT IN PATTERN.C). Tests pin
8322 // each edit operation independently + the budget upper bound.
8323 // ═══════════════════════════════════════════════════════════════════
8324
8325 /// `(#a0)foo` is exact-match only — no edits allowed.
8326 #[test]
8327 fn ext_glob_hash_a0_is_exact_match_only() {
8328 let _g = crate::test_util::global_state_lock();
8329 assert!(ext_glob_match("(#a0)foo", "foo"), "exact ok");
8330 assert!(
8331 !ext_glob_match("(#a0)foo", "fop"),
8332 "no edit budget rejects 1-sub"
8333 );
8334 }
8335
8336 /// `(#a1)foo` matches "fxoo" via INSERTION in input (extra 'x').
8337 /// The pattern has no `x`; treating `x` as an inserted-in-input
8338 /// char costs 1 error. Input → "f(x)oo" with the 'x' deleted.
8339 #[test]
8340 fn ext_glob_hash_a_accepts_insertion_in_input() {
8341 let _g = crate::test_util::global_state_lock();
8342 // "fxoo" — extra 'x' between 'f' and 'oo'.
8343 assert!(
8344 ext_glob_match("(#a1)foo", "fxoo"),
8345 "1 insertion-in-input edit"
8346 );
8347 }
8348
8349 /// `(#a1)foo` matches "fo" via DELETION from input (missing 'o').
8350 /// One 'o' deleted from input compared to pattern.
8351 #[test]
8352 fn ext_glob_hash_a_accepts_deletion_from_input() {
8353 let _g = crate::test_util::global_state_lock();
8354 assert!(
8355 ext_glob_match("(#a1)foo", "fo"),
8356 "1 deletion-from-input edit"
8357 );
8358 }
8359
8360 /// `(#a2)foo` matches "fxooy" via 1 insertion + 1 substitution.
8361 /// 'x' inserted between f-o; final 'o' substituted to 'y'. Wait —
8362 /// closer reading: "fxooy" vs "foo" — 'x' is the extra char, but
8363 /// then 'ooy' vs 'oo' has another extra 'y'. That's 2 insertions.
8364 #[test]
8365 fn ext_glob_hash_a_mixed_edits_within_budget() {
8366 let _g = crate::test_util::global_state_lock();
8367 // 2 insertions: 'x' and 'y'.
8368 assert!(
8369 ext_glob_match("(#a2)foo", "fxooy"),
8370 "2 insertions in input within budget"
8371 );
8372 }
8373
8374 /// Budget upper bound: `(#a1)abc` rejects "xyz" (3 substitutions > 1).
8375 #[test]
8376 fn ext_glob_hash_a1_rejects_three_substitutions() {
8377 let _g = crate::test_util::global_state_lock();
8378 assert!(!ext_glob_match("(#a1)abc", "xyz"));
8379 }
8380
8381 /// `(#a3)abc` matches "xyz" (3 substitutions ≤ 3).
8382 #[test]
8383 fn ext_glob_hash_a3_accepts_all_substituted() {
8384 let _g = crate::test_util::global_state_lock();
8385 assert!(
8386 ext_glob_match("(#a3)abc", "xyz"),
8387 "3 substitutions at budget 3"
8388 );
8389 }
8390
8391 /// Position-independent substitution: budget allows replacing
8392 /// any one of N pattern chars.
8393 #[test]
8394 fn ext_glob_hash_a_accepts_position_independent_substitution() {
8395 let _g = crate::test_util::global_state_lock();
8396 assert!(ext_glob_match("(#a1)abc", "Xbc"), "first-char sub");
8397 assert!(ext_glob_match("(#a1)abc", "aXc"), "middle-char sub");
8398 assert!(ext_glob_match("(#a1)abc", "abX"), "last-char sub");
8399 }
8400
8401 // ═══════════════════════════════════════════════════════════════════
8402 // zsh test-corpus pins — direct anchors to Test/D02glob.ztst:25-80
8403 // "zsh globbing" test list (zsh's authoritative regression suite).
8404 // Each test cites the ztst line range it pins. Currently-failing
8405 // tests get #[ignore = "ZSHRS BUG: ..."] markers; expected-output
8406 // assertions stay in tree so the marker flips when the Rust port
8407 // catches up.
8408 // ═══════════════════════════════════════════════════════════════════
8409
8410 /// `Test/D02glob.ztst:26` — `[[ foo~ = foo~ ]]` exact match,
8411 /// expected exit 0 (true).
8412 #[test]
8413 fn zsh_corpus_foo_tilde_exact_match() {
8414 let _g = crate::test_util::global_state_lock();
8415 assert!(
8416 ext_glob_match("foo~", "foo~"),
8417 "ztst:26 — literal tilde matches"
8418 );
8419 }
8420
8421 /// `Test/D02glob.ztst:27` — `[[ foo~ = (foo~) ]]` parenthesised
8422 /// single alternative.
8423 #[test]
8424 fn zsh_corpus_foo_tilde_in_parens() {
8425 let _g = crate::test_util::global_state_lock();
8426 assert!(
8427 ext_glob_match("(foo~)", "foo~"),
8428 "ztst:27 — (foo~) matches foo~"
8429 );
8430 }
8431
8432 /// `Test/D02glob.ztst:28` — `[[ foo~ = (foo~|) ]]` alternation
8433 /// with empty alternative.
8434 #[test]
8435 fn zsh_corpus_alternation_with_empty_alt() {
8436 let _g = crate::test_util::global_state_lock();
8437 assert!(
8438 ext_glob_match("(foo~|)", "foo~"),
8439 "ztst:28 — empty alt accepted"
8440 );
8441 }
8442
8443 /// `Test/D02glob.ztst:29` — `[[ foo.c = *.c~boo* ]]` exclude
8444 /// pattern: matches *.c BUT NOT boo*. `foo.c` matches *.c and
8445 /// doesn't match boo*, so result is 0 (true).
8446 #[test]
8447 fn zsh_corpus_exclude_pattern_basic() {
8448 let _g = crate::test_util::global_state_lock();
8449 assert!(
8450 ext_glob_match("*.c~boo*", "foo.c"),
8451 "ztst:29 — *.c~boo* matches foo.c"
8452 );
8453 }
8454
8455 /// `Test/D02glob.ztst:30` — `[[ foo.c = *.c~boo*~foo* ]]`
8456 /// double-exclude: matches *.c but excludes both `boo*` and
8457 /// `foo*`. `foo.c` matches `foo*` (excluded) → result 1 (false).
8458 #[test]
8459 fn zsh_corpus_exclude_pattern_double() {
8460 let _g = crate::test_util::global_state_lock();
8461 assert!(
8462 !ext_glob_match("*.c~boo*~foo*", "foo.c"),
8463 "ztst:30 — double exclude rejects foo.c"
8464 );
8465 }
8466
8467 /// `Test/D02glob.ztst:31` — `[[ fofo = (fo#)# ]]` — `#` is the
8468 /// extended-glob "1+ repetitions" quantifier. `(fo#)#` matches
8469 /// 1+ repetitions of "fo+".
8470 #[test]
8471 fn zsh_corpus_hash_repetition_double() {
8472 let _g = crate::test_util::global_state_lock();
8473 assert!(
8474 ext_glob_match("(fo#)#", "fofo"),
8475 "ztst:31 — (fo#)# matches fofo"
8476 );
8477 }
8478
8479 /// `Test/D02glob.ztst:32` — `[[ ffo = (fo#)# ]]`. `fo#` means
8480 /// `f` followed by 0+ `o`. `ffo` = `f` + `fo` (matches the
8481 /// outer `#` repetition).
8482 #[test]
8483 fn zsh_corpus_hash_repetition_min_one() {
8484 let _g = crate::test_util::global_state_lock();
8485 assert!(
8486 ext_glob_match("(fo#)#", "ffo"),
8487 "ztst:32 — (fo#)# matches ffo via 1-iter outer"
8488 );
8489 }
8490
8491 /// `Test/D02glob.ztst:36` — `[[ foooofof = (fo##)# ]]` —
8492 /// `##` is "2+ repetitions". `fo##` = `f` + 2+ `o`'s. The
8493 /// trailing `of` after `foooo` doesn't satisfy the trailing
8494 /// outer `#`, so result is 1 (false).
8495 #[test]
8496 fn zsh_corpus_hash_hash_quantifier_min_two() {
8497 let _g = crate::test_util::global_state_lock();
8498 assert!(
8499 !ext_glob_match("(fo##)#", "foooofof"),
8500 "ztst:36 — (fo##)# rejects foooofof"
8501 );
8502 }
8503
8504 /// `Test/D02glob.ztst:50` — `[[ aac = ((a))#a(c) ]]` —
8505 /// `((a))#` is `0+ a`. `aac` = `aa` (= `((a))#`) + `c`. The
8506 /// final `a(c)` requires literal `a` followed by capture `(c)`.
8507 /// Wait — should be `aac` matches `((a))#a(c)`: outer `((a))#`
8508 /// captures `a` repeated, then literal `a`, then `(c)`.
8509 /// Actually re-reading: `((a))#` matches 0+ 'a'. `aac` =
8510 /// `((a))#`("a") + `a`("a") + `(c)`("c"). So "aa" splits as
8511 /// (a)("a") + a("a") + c("c"). Match.
8512 #[test]
8513 fn zsh_corpus_nested_paren_quantifier() {
8514 let _g = crate::test_util::global_state_lock();
8515 assert!(
8516 ext_glob_match("((a))#a(c)", "aac"),
8517 "ztst:50 — ((a))#a(c) matches aac"
8518 );
8519 }
8520
8521 /// `Test/D02glob.ztst:51` — `[[ ac = ((a))#a(c) ]]` — single
8522 /// `a` matches `a(c)` after empty `((a))#` consumes zero.
8523 #[test]
8524 fn zsh_corpus_zero_iteration_quantifier() {
8525 let _g = crate::test_util::global_state_lock();
8526 assert!(
8527 ext_glob_match("((a))#a(c)", "ac"),
8528 "ztst:51 — ((a))# can be zero iters"
8529 );
8530 }
8531
8532 /// `Test/D02glob.ztst:52` — `[[ c = ((a))#a(c) ]]` — empty
8533 /// `((a))#` + literal `a` requires at least one `a`. `c` has
8534 /// no `a`, so result is 1 (false).
8535 #[test]
8536 fn zsh_corpus_required_literal_after_zero_quantifier() {
8537 let _g = crate::test_util::global_state_lock();
8538 assert!(
8539 !ext_glob_match("((a))#a(c)", "c"),
8540 "ztst:52 — bare `c` lacks required `a`"
8541 );
8542 }
8543
8544 /// `Test/D02glob.ztst:73` — `[[ foo = ((^x)) ]]` — exclude
8545 /// `x`. `foo` doesn't contain `x` as a single char (the
8546 /// `((^x))` matches anything that's not just 'x' — `foo` is 3
8547 /// chars, none is `x`).
8548 #[test]
8549 fn zsh_corpus_caret_exclude_basic() {
8550 let _g = crate::test_util::global_state_lock();
8551 assert!(
8552 ext_glob_match("((^x))", "foo"),
8553 "ztst:73 — ((^x)) matches foo"
8554 );
8555 }
8556
8557 /// `Test/D02glob.ztst:75` — `[[ foo = ((^foo)) ]]` — `^foo`
8558 /// rejects exact `foo`. `foo` matches `foo` literal, which the
8559 /// `^` inverts. Result: 1 (false).
8560 #[test]
8561 fn zsh_corpus_caret_exclude_exact_match_inverted() {
8562 let _g = crate::test_util::global_state_lock();
8563 assert!(
8564 !ext_glob_match("((^foo))", "foo"),
8565 "ztst:75 — ((^foo)) rejects foo"
8566 );
8567 }
8568
8569 /// `Test/D02glob.ztst:79` — `[[ foot = z*~*x ]]` — `*` then
8570 /// exclude `*x`. `foot` doesn't start with `z`, so doesn't
8571 /// match `z*` at all → result 1 (false).
8572 #[test]
8573 fn zsh_corpus_star_exclude_no_z_prefix() {
8574 let _g = crate::test_util::global_state_lock();
8575 assert!(
8576 !ext_glob_match("z*~*x", "foot"),
8577 "ztst:79 — foot doesn't start with z"
8578 );
8579 }
8580
8581 /// `Test/D02glob.ztst:80` — `[[ zoot = z*~*x ]]` — `zoot`
8582 /// matches `z*` AND doesn't end in `x` → result 0 (true).
8583 #[test]
8584 fn zsh_corpus_star_exclude_zoot() {
8585 let _g = crate::test_util::global_state_lock();
8586 assert!(
8587 ext_glob_match("z*~*x", "zoot"),
8588 "ztst:80 — zoot matches z* and not *x"
8589 );
8590 }
8591
8592 // ─── POSIX class brackets — Test/D02glob.ztst:111-118 ─────────────
8593
8594 /// `Test/D02glob.ztst:111` — `[[:alpha:][:punct:]]#[[:digit:]][^[:lower:]]`
8595 /// over "a%1X": `a` matches alpha, `%` matches punct (1+ via `#`),
8596 /// `1` matches digit, `X` matches NOT-lower.
8597 #[test]
8598 fn zsh_corpus_posix_class_alpha_punct_digit_notlower() {
8599 let _g = crate::test_util::global_state_lock();
8600 assert!(
8601 ext_glob_match("[[:alpha:][:punct:]]#[[:digit:]][^[:lower:]]", "a%1X"),
8602 "ztst:111 — alpha-punct-digit-notlower chain",
8603 );
8604 }
8605
8606 /// `Test/D02glob.ztst:112` — same pattern, "a%1" lacks the
8607 /// 4th non-lower char → result 1 (false).
8608 #[test]
8609 fn zsh_corpus_posix_class_rejects_short_input() {
8610 let _g = crate::test_util::global_state_lock();
8611 assert!(
8612 !ext_glob_match("[[:alpha:][:punct:]]#[[:digit:]][^[:lower:]]", "a%1"),
8613 "ztst:112 — short input rejected",
8614 );
8615 }
8616
8617 /// `Test/D02glob.ztst:113` — `[[:` literal in char class:
8618 /// `[[ [: = [[:]# ]]` — `[[:]` matches `[` OR `:`, `#` is 1+
8619 /// repetitions. "[:" = `[` + `:` → matches.
8620 #[test]
8621 fn zsh_corpus_literal_brackets_in_class_with_repetition() {
8622 let _g = crate::test_util::global_state_lock();
8623 assert!(
8624 ext_glob_match("[[:]#", "[:"),
8625 "ztst:113 — [[:]# matches '[:'"
8626 );
8627 }
8628
8629 // ─── (#i) / (#l) case modifiers — Test/D02glob.ztst:119-132 ───────
8630
8631 /// `Test/D02glob.ztst:119` — `(#i)FOOXX` matches "fooxx"
8632 /// (case-insensitive throughout).
8633 #[test]
8634 fn zsh_corpus_hash_i_case_insensitive() {
8635 let _g = crate::test_util::global_state_lock();
8636 assert!(
8637 ext_glob_match("(#i)FOOXX", "fooxx"),
8638 "ztst:119 — (#i)FOOXX matches fooxx"
8639 );
8640 }
8641
8642 /// `Test/D02glob.ztst:120` — `(#l)FOOXX` requires pattern UPPER
8643 /// chars to be the ONLY uppercase candidates; lowercase input
8644 /// FAILS to match because pattern is all uppercase and `#l` only
8645 /// matches the EXACT lowercase variant of the pattern char... actually
8646 /// `(#l)` means "lowercase pattern chars match uppercase too" —
8647 /// `(#l)FOOXX` has no lowercase chars so it stays a strict
8648 /// uppercase match. "fooxx" doesn't match. Result 1 (false).
8649 #[test]
8650 fn zsh_corpus_hash_l_uppercase_pattern_with_lower_input_fails() {
8651 let _g = crate::test_util::global_state_lock();
8652 assert!(
8653 !ext_glob_match("(#l)FOOXX", "fooxx"),
8654 "ztst:120 — (#l)FOOXX does NOT match fooxx"
8655 );
8656 }
8657
8658 /// `Test/D02glob.ztst:121` — `(#l)fooxx` (lowercase pattern) DOES
8659 /// match "FOOXX" because `#l` is the asymmetric "lowercase pat
8660 /// chars match upper-or-lower" rule.
8661 #[test]
8662 fn zsh_corpus_hash_l_lowercase_pattern_matches_uppercase_input() {
8663 let _g = crate::test_util::global_state_lock();
8664 assert!(
8665 ext_glob_match("(#l)fooxx", "FOOXX"),
8666 "ztst:121 — (#l)fooxx matches FOOXX (asymmetric upcasing)"
8667 );
8668 }
8669
8670 /// `Test/D02glob.ztst:122` — `(#i)FOO(#I)X(#i)X` mixes case
8671 /// modes. `(#I)` cancels prior `(#i)`. So 4th char `X` requires
8672 /// exact case. "fooxx" has lowercase `x` at that position → fail.
8673 #[test]
8674 fn zsh_corpus_hash_capital_i_cancels_case_insensitive() {
8675 let _g = crate::test_util::global_state_lock();
8676 assert!(
8677 !ext_glob_match("(#i)FOO(#I)X(#i)X", "fooxx"),
8678 "ztst:122 — (#I) cancels (#i), 4th char `X` requires upper"
8679 );
8680 }
8681
8682 /// `Test/D02glob.ztst:123` — same pattern with input "fooXx":
8683 /// `(#i)FOO` matches "foo", `(#I)X` matches "X" exact-case,
8684 /// `(#i)X` matches "x" insensitive → result 0 (true).
8685 #[test]
8686 fn zsh_corpus_hash_i_capital_i_toggle_succeeds() {
8687 let _g = crate::test_util::global_state_lock();
8688 assert!(
8689 ext_glob_match("(#i)FOO(#I)X(#i)X", "fooXx"),
8690 "ztst:123 — mixed case modifiers succeed"
8691 );
8692 }
8693
8694 /// `Test/D02glob.ztst:128` — `(#i)*m*` matches "Modules"
8695 /// case-insensitively.
8696 #[test]
8697 fn zsh_corpus_hash_i_with_star_glob() {
8698 let _g = crate::test_util::global_state_lock();
8699 assert!(
8700 ext_glob_match("(#i)*m*", "Modules"),
8701 "ztst:128 — (#i)*m* case-insensitive substring"
8702 );
8703 }
8704
8705 // ─── Numeric ranges `<n-m>` — Test/D02glob.ztst:133-137 ───────────
8706
8707 /// `Test/D02glob.ztst:133` — `<1-1000>33` matches "633" (the
8708 /// "6" is in [1..1000], followed by literal "33"). Actually
8709 /// the way zsh parses: `<1-1000>` matches ONE number from
8710 /// 1-1000, then `33` is literal. "633" = `6`(in 1-1000 range) +
8711 /// "33"(literal). So `<1-1000>33` matches "633".
8712 #[test]
8713 fn zsh_corpus_numeric_range_one_to_thousand() {
8714 let _g = crate::test_util::global_state_lock();
8715 assert!(
8716 ext_glob_match("<1-1000>33", "633"),
8717 "ztst:133 — <1-1000>33 matches 633"
8718 );
8719 }
8720
8721 /// `Test/D02glob.ztst:136` — `<->33` is the open-ended range
8722 /// (any number) followed by 33. Matches "633".
8723 #[test]
8724 fn zsh_corpus_numeric_range_open_ended() {
8725 let _g = crate::test_util::global_state_lock();
8726 assert!(
8727 ext_glob_match("<->33", "633"),
8728 "ztst:136 — <->33 matches 633 (any number)"
8729 );
8730 }
8731
8732 // ─── (#a) approximate match details — Test/D02glob.ztst:147-164 ───
8733
8734 /// `Test/D02glob.ztst:147` — `(#a1)[b][b]` matches "bob" via
8735 /// 1 substitution (middle `o` substituted in `[b][b]`'s gap).
8736 /// Wait — `[b][b]` is two-char "bb". Match against "bob" requires
8737 /// 1 edit. With (#a1), allowed.
8738 #[test]
8739 fn zsh_corpus_hash_a1_bracket_class_with_one_edit() {
8740 let _g = crate::test_util::global_state_lock();
8741 assert!(
8742 ext_glob_match("(#a1)[b][b]", "bob"),
8743 "ztst:147 — (#a1)[b][b] matches bob via 1 edit"
8744 );
8745 }
8746
8747 /// `Test/D02glob.ztst:151` — `(#a2)XbcX` matches "abcd" via
8748 /// 2 substitutions (a↔X, d↔X).
8749 #[test]
8750 fn zsh_corpus_hash_a2_two_substitutions() {
8751 let _g = crate::test_util::global_state_lock();
8752 assert!(
8753 ext_glob_match("(#a2)XbcX", "abcd"),
8754 "ztst:151 — 2 substitutions allowed"
8755 );
8756 }
8757
8758 /// `Test/D02glob.ztst:152` — `(#a2)ad` matches "abcd" via
8759 /// 2 INSERTIONS (b and c inserted into input vs pattern).
8760 /// Pattern is "ad" (2 chars), input is "abcd" (4 chars).
8761 /// Diff: 2 extra chars in input.
8762 #[test]
8763 fn zsh_corpus_hash_a2_two_insertions_in_input() {
8764 let _g = crate::test_util::global_state_lock();
8765 assert!(
8766 ext_glob_match("(#a2)ad", "abcd"),
8767 "ztst:152 — (#a2)ad matches abcd via 2 insertions in input"
8768 );
8769 }
8770
8771 /// `Test/D02glob.ztst:153` — `(#a2)abcd` matches "ad" via
8772 /// 2 DELETIONS from input (b and c missing from input vs pattern).
8773 #[test]
8774 fn zsh_corpus_hash_a2_two_deletions_from_input() {
8775 let _g = crate::test_util::global_state_lock();
8776 assert!(
8777 ext_glob_match("(#a2)abcd", "ad"),
8778 "ztst:153 — (#a2)abcd matches ad via 2 deletions"
8779 );
8780 }
8781
8782 /// `Test/D02glob.ztst:158` — `(#a2)abcd` rejects "dcba" — 4
8783 /// changes needed (full reverse) > budget 2.
8784 #[test]
8785 fn zsh_corpus_hash_a2_rejects_full_reverse() {
8786 let _g = crate::test_util::global_state_lock();
8787 assert!(
8788 !ext_glob_match("(#a2)abcd", "dcba"),
8789 "ztst:158 — full reverse exceeds budget 2"
8790 );
8791 }
8792
8793 /// `Test/D02glob.ztst:159` — `(#a3)abcd` matches "dcba" via
8794 /// 3 substitutions (positions 0,1,3 of input). Wait — actually
8795 /// "dcba" vs "abcd" — 4 positions differ. But (#a3) allows
8796 /// 3 errors. Hmm, but ztst says result is 0 (true). So zsh
8797 /// accepts. Possibly via DAMERAU transposition (swap adjacent).
8798 /// Without Damerau swap, this needs 4 subs → false. With
8799 /// transposition, "dcba" → "cdba" → "dcab"... complicated.
8800 /// Mark ignore — Damerau transposition isn't in the Rust port.
8801 #[test]
8802 fn zsh_corpus_hash_a3_reverse_via_transposition() {
8803 let _g = crate::test_util::global_state_lock();
8804 assert!(
8805 ext_glob_match("(#a3)abcd", "dcba"),
8806 "ztst:159 — (#a3)abcd matches dcba via Damerau transpositions"
8807 );
8808 }
8809
8810 // ─── (#s) start / (#e) end anchors — Test/D02glob.ztst:168-179 ────
8811
8812 /// `Test/D02glob.ztst:168` — `*((#s)|/)test((#e)|/)*` matches
8813 /// "test" — start-anchor (#s) at position 0, end-anchor (#e)
8814 /// after "test".
8815 #[test]
8816 fn zsh_corpus_hash_s_e_anchors_match_bare_test() {
8817 let _g = crate::test_util::global_state_lock();
8818 assert!(
8819 ext_glob_match("*((#s)|/)test((#e)|/)*", "test"),
8820 "ztst:168 — start/end anchors match bare 'test'",
8821 );
8822 }
8823
8824 /// `Test/D02glob.ztst:172` — `*((#s)|/)test((#e)|/)*` rejects
8825 /// "atest" — `(#s)|/` requires position 0 OR a `/`, but `a`
8826 /// is neither.
8827 #[test]
8828 fn zsh_corpus_hash_s_anchor_rejects_a_prefix() {
8829 let _g = crate::test_util::global_state_lock();
8830 assert!(
8831 !ext_glob_match("*((#s)|/)test((#e)|/)*", "atest"),
8832 "ztst:172 — atest fails the (#s) start-or-slash anchor",
8833 );
8834 }
8835
8836 // ─── Misc/globtests pins — `~` exclusion and `^` negation ─────────
8837
8838 /// `Misc/globtests` — `[[ foo~ = foo~ ]]` — bare tilde is literal.
8839 #[test]
8840 fn zsh_corpus_literal_tilde_in_pattern() {
8841 let _g = crate::test_util::global_state_lock();
8842 assert!(
8843 ext_glob_match("foo~", "foo~"),
8844 "literal ~ in non-extglob position"
8845 );
8846 }
8847
8848 /// `Misc/globtests` — `[[ foo.c = *.c~boo* ]]` — extended-glob
8849 /// exclusion: `*.c` minus things matching `boo*`. "foo.c" doesn't
8850 /// match "boo*", so it stays.
8851 #[test]
8852 fn zsh_corpus_exclusion_keeps_non_excluded() {
8853 let _g = crate::test_util::global_state_lock();
8854 assert!(ext_glob_match("*.c~boo*", "foo.c"), "exclusion keeps foo.c");
8855 }
8856
8857 /// `Misc/globtests` — `[[ foo.c = *.c~boo*~foo* ]]` — chained
8858 /// exclusions: `*.c` minus `boo*` minus `foo*`. "foo.c" matches
8859 /// `foo*`, so excluded.
8860 #[test]
8861 fn zsh_corpus_chained_exclusion_removes_match() {
8862 let _g = crate::test_util::global_state_lock();
8863 assert!(
8864 !ext_glob_match("*.c~boo*~foo*", "foo.c"),
8865 "chained exclusion excludes foo.c",
8866 );
8867 }
8868
8869 /// `Misc/globtests` — `[[ fofo = (fo#)# ]]` — outer `(...)#` is
8870 /// zero-or-more closure over `fo#` (one f followed by zero+ o).
8871 #[test]
8872 fn zsh_corpus_closure_of_closure_matches_repeated_fo() {
8873 let _g = crate::test_util::global_state_lock();
8874 assert!(ext_glob_match("(fo#)#", "fofo"), "(fo#)# matches fofo");
8875 }
8876
8877 /// `Misc/globtests` — `[[ ffo = (fo#)# ]]` — "ffo" = "f"+"fo".
8878 #[test]
8879 fn zsh_corpus_closure_matches_ffo() {
8880 let _g = crate::test_util::global_state_lock();
8881 assert!(ext_glob_match("(fo#)#", "ffo"), "(fo#)# matches ffo");
8882 }
8883
8884 /// `Misc/globtests` — `[[ xfoooofof = (fo#)# ]]` — leading "x"
8885 /// breaks the pattern.
8886 #[test]
8887 fn zsh_corpus_closure_rejects_leading_x() {
8888 let _g = crate::test_util::global_state_lock();
8889 assert!(
8890 !ext_glob_match("(fo#)#", "xfoooofof"),
8891 "(fo#)# rejects leading x",
8892 );
8893 }
8894
8895 /// `Misc/globtests` — `[[ foo = ((^x)) ]]` — `(^x)` matches one
8896 /// char that isn't 'x'. "foo" has 'f' first, so matches.
8897 /// Note: zsh's `(^x)` is exactly-one-not-x with extendedglob.
8898 #[test]
8899 fn zsh_corpus_negation_caret_matches_non_x_start() {
8900 let _g = crate::test_util::global_state_lock();
8901 assert!(ext_glob_match("((^x)*)", "foo"), "(^x)* matches foo");
8902 }
8903
8904 /// `Misc/globtests` — `[[ foo = ((^foo)) ]]` — `(^foo)` is "not foo",
8905 /// "foo" matches `foo`, so excluded → false.
8906 #[test]
8907 fn zsh_corpus_negation_caret_rejects_full_match() {
8908 let _g = crate::test_util::global_state_lock();
8909 assert!(
8910 !ext_glob_match("((^foo))", "foo"),
8911 "(^foo) rejects 'foo' itself",
8912 );
8913 }
8914
8915 /// `Misc/globtests` — `[[ abcd = ?(a|b)c#d ]]` — `?(a|b)` is
8916 /// "zero-or-one of a|b", `c#d` is "zero+ c then d". "abcd" = a + b? no, ?
8917 /// is single char or alternation. Let me re-read: `?(a|b)` matches
8918 /// one char which is `a` OR `b`. So "abcd": ?=a, then needs `c#d`
8919 /// = (c)*d, matches "bcd" if first char then need cd. Hmm.
8920 /// Actually: `?(a|b)` = `?` then `(a|b)` as separate? More likely
8921 /// `?(a|b)` is ksh-style "0-or-1 of a|b" only with KSH_GLOB.
8922 /// In zsh native (extendedglob) `?` is single char, `(a|b)` is
8923 /// alternation. So `?(a|b)c#d` = anychar (a|b) c* d. "abcd" =
8924 /// a + b + (zero c) + ... fails. The ztst says it MATCHES.
8925 /// Conclusion: requires KSH_GLOB which we may not enable.
8926 #[test]
8927 fn zsh_corpus_ksh_question_alternation() {
8928 let _g = crate::test_util::global_state_lock();
8929 assert!(
8930 ext_glob_match("?(a|b)c#d", "abcd"),
8931 "?(a|b)c#d matches abcd",
8932 );
8933 }
8934
8935 // ── patmatchlen — c:2649 ─────────────────────────────────────────
8936
8937 /// `Src/pattern.c:2649` — `int patmatchlen(void)` returns the
8938 /// byte-length of the last successful pattry match. After a
8939 /// successful match against `"hello"` with pattern `"hel*"`,
8940 /// the recorded length is 5 (all of "hello" was consumed).
8941 #[test]
8942 fn patmatchlen_records_consumed_byte_length() {
8943 let _g = crate::test_util::global_state_lock();
8944 let prog = compile("hel*");
8945 assert!(pattry(&prog, "hello"), "hel* matches hello");
8946 assert_eq!(patmatchlen(), 5, "all 5 bytes of 'hello' consumed by hel*",);
8947 }
8948
8949 /// Anchored prefix match: `"foo"` against `"foobar"` with the
8950 /// `NOANCH` form is the user-visible case `[[ foobar = foo* ]]`,
8951 /// which consumes all 6 bytes when the pattern matches the whole
8952 /// string.
8953 #[test]
8954 fn patmatchlen_full_string_match_returns_full_length() {
8955 let _g = crate::test_util::global_state_lock();
8956 let prog = compile("foo*");
8957 assert!(pattry(&prog, "foobar"));
8958 assert_eq!(patmatchlen(), 6, "foo* against 'foobar' = 6 bytes");
8959 }
8960
8961 // ── patmatchindex (c:4004) ──────────────────────────────────────
8962
8963 /// `patmatchindex` on a literal-byte range returns the byte at
8964 /// the requested position.
8965 #[test]
8966 fn patmatchindex_literal_byte_at_index() {
8967 let _g = crate::test_util::global_state_lock();
8968 let range = b"abc";
8969 assert_eq!(patmatchindex(range, 0), Some((Some(b'a'), 0)));
8970 assert_eq!(patmatchindex(range, 1), Some((Some(b'b'), 0)));
8971 assert_eq!(patmatchindex(range, 2), Some((Some(b'c'), 0)));
8972 assert_eq!(patmatchindex(range, 3), None, "out-of-range index");
8973 }
8974
8975 /// `patmatchindex` on a PP_ALPHA class marker returns `(None,
8976 /// PP_ALPHA)` to signal the class without a literal char.
8977 #[test]
8978 fn patmatchindex_posix_class_marker_returns_mtp() {
8979 let _g = crate::test_util::global_state_lock();
8980 // Meta + PP_ALPHA = 0x83 + 1 = 0x84
8981 let range = &[Meta + PP_ALPHA as u8];
8982 let r = patmatchindex(range, 0);
8983 assert_eq!(r, Some((None, PP_ALPHA)));
8984 }
8985
8986 // ── mb_patmatchrange (c:3610) ───────────────────────────────────
8987
8988 /// `mb_patmatchrange` literal hit on `'a'`.
8989 #[test]
8990 fn mb_patmatchrange_literal_ascii_hits() {
8991 let _g = crate::test_util::global_state_lock();
8992 let range = b"a";
8993 let mut mtp = -1;
8994 assert!(
8995 mb_patmatchrange(range, 'a', 0, None, Some(&mut mtp)),
8996 "literal 'a' matches"
8997 );
8998 assert_eq!(mtp, 0, "literal hit sets mtp=0");
8999 }
9000
9001 /// `mb_patmatchrange` PP_DIGIT class hit on `'5'`.
9002 #[test]
9003 fn mb_patmatchrange_digit_class_hits() {
9004 let _g = crate::test_util::global_state_lock();
9005 let range = &[Meta + PP_DIGIT as u8];
9006 assert!(mb_patmatchrange(range, '5', 0, None, None));
9007 assert!(!mb_patmatchrange(range, 'x', 0, None, None));
9008 }
9009
9010 /// `mb_patmatchrange` PP_RANGE on `'b'` in `a..c`.
9011 #[test]
9012 fn mb_patmatchrange_range_hits_middle() {
9013 let _g = crate::test_util::global_state_lock();
9014 // Meta + PP_RANGE then two literal bytes for the endpoints.
9015 let range = &[Meta + PP_RANGE as u8, b'a', b'c'];
9016 assert!(mb_patmatchrange(range, 'b', 0, None, None));
9017 assert!(mb_patmatchrange(range, 'a', 0, None, None));
9018 assert!(mb_patmatchrange(range, 'c', 0, None, None));
9019 assert!(!mb_patmatchrange(range, 'd', 0, None, None));
9020 }
9021
9022 // ── mb_patmatchindex (c:3767) ───────────────────────────────────
9023
9024 /// `mb_patmatchindex` literal char at index 0.
9025 #[test]
9026 fn mb_patmatchindex_literal_first_index() {
9027 let _g = crate::test_util::global_state_lock();
9028 let range = b"xyz";
9029 let r = mb_patmatchindex(range, 0);
9030 assert_eq!(r, Some((Some('x'), 0)));
9031 }
9032
9033 /// `mb_patmatchindex` on PP_UPPER marker at index 0.
9034 #[test]
9035 fn mb_patmatchindex_class_marker_returns_mtp() {
9036 let _g = crate::test_util::global_state_lock();
9037 let range = &[Meta + PP_UPPER as u8];
9038 assert_eq!(mb_patmatchindex(range, 0), Some((None, PP_UPPER)));
9039 }
9040
9041 // ═══════════════════════════════════════════════════════════════════
9042 // Additional C-parity tests for Src/pattern.c bit-flag predicates.
9043 // ═══════════════════════════════════════════════════════════════════
9044
9045 /// c:200 — P_ISBRANCH checks bit 0x20.
9046 #[test]
9047 fn P_ISBRANCH_recognizes_bit_0x20() {
9048 assert!(P_ISBRANCH(0x20));
9049 assert!(P_ISBRANCH(0x20 | 0x01));
9050 assert!(P_ISBRANCH(0xFF));
9051 assert!(!P_ISBRANCH(0x00));
9052 assert!(!P_ISBRANCH(0x1F));
9053 }
9054
9055 /// c:201 — P_ISEXCLUDE checks bits 0x30 = 0x10 | 0x20 (BOTH must be set).
9056 #[test]
9057 fn P_ISEXCLUDE_requires_both_bits() {
9058 assert!(P_ISEXCLUDE(0x30));
9059 assert!(P_ISEXCLUDE(0x30 | 0x01));
9060 assert!(!P_ISEXCLUDE(0x10), "only 0x10 — must require both");
9061 assert!(!P_ISEXCLUDE(0x20), "only 0x20 — must require both");
9062 assert!(!P_ISEXCLUDE(0x00));
9063 }
9064
9065 /// c:202 — P_NOTDOT checks bit 0x40.
9066 #[test]
9067 fn P_NOTDOT_recognizes_bit_0x40() {
9068 assert!(P_NOTDOT(0x40));
9069 assert!(P_NOTDOT(0x40 | 0x01));
9070 assert!(P_NOTDOT(0xFF));
9071 assert!(!P_NOTDOT(0x00));
9072 assert!(!P_NOTDOT(0x3F));
9073 }
9074
9075 /// c:216-218 — P_SIMPLE/HSTART/PURESTR are distinct bits.
9076 #[test]
9077 fn p_flag_constants_are_distinct() {
9078 assert_eq!(P_SIMPLE & P_HSTART, 0);
9079 assert_eq!(P_SIMPLE & P_PURESTR, 0);
9080 assert_eq!(P_HSTART & P_PURESTR, 0);
9081 }
9082
9083 /// c:216 — P_SIMPLE is bit 0 (= 0x01).
9084 #[test]
9085 fn p_simple_is_bit_zero() {
9086 assert_eq!(P_SIMPLE, 0x01);
9087 }
9088
9089 /// c:217 — P_HSTART is bit 1 (= 0x02).
9090 #[test]
9091 fn p_hstart_is_bit_one() {
9092 assert_eq!(P_HSTART, 0x02);
9093 }
9094
9095 /// c:218 — P_PURESTR is bit 2 (= 0x04).
9096 #[test]
9097 fn p_purestr_is_bit_two() {
9098 assert_eq!(P_PURESTR, 0x04);
9099 }
9100
9101 /// c:406-407 — PA_NOALIGN=1, PA_UNMETA=2 (single-bit flags).
9102 #[test]
9103 fn pa_flag_constants_canonical() {
9104 assert_eq!(PA_NOALIGN, 1);
9105 assert_eq!(PA_UNMETA, 2);
9106 }
9107
9108 /// PA_NOALIGN | PA_UNMETA combine without overlap.
9109 #[test]
9110 fn pa_flags_pairwise_disjoint() {
9111 assert_eq!(PA_NOALIGN & PA_UNMETA, 0);
9112 }
9113
9114 /// c:336 — metacharinc on ASCII char advances by 1 byte.
9115 #[test]
9116 fn metacharinc_ascii_advances_one() {
9117 let _g = crate::test_util::global_state_lock();
9118 assert_eq!(metacharinc("hello", 0), 1);
9119 assert_eq!(metacharinc("hello", 4), 5);
9120 }
9121
9122 /// c:336 — metacharinc at end-of-string returns same pos.
9123 #[test]
9124 fn metacharinc_end_of_string_returns_same() {
9125 let _g = crate::test_util::global_state_lock();
9126 assert_eq!(metacharinc("abc", 3), 3, "at end → no advance");
9127 }
9128
9129 /// c:336 — metacharinc on multibyte char advances by codepoint width.
9130 #[test]
9131 fn metacharinc_multibyte_advances_by_codepoint_len() {
9132 let _g = crate::test_util::global_state_lock();
9133 // 'é' is 2 bytes in UTF-8.
9134 assert_eq!(metacharinc("é", 0), 2);
9135 // '日' is 3 bytes.
9136 assert_eq!(metacharinc("日", 0), 3);
9137 }
9138
9139 // ═══════════════════════════════════════════════════════════════════
9140 // Additional C-parity tests for Src/pattern.c
9141 // c:155 P_ISBRANCH / c:161 P_ISEXCLUDE / c:167 P_NOTDOT /
9142 // c:262 metacharinc / c:502 patcompstart / c:536 patcompile /
9143 // c:1008 patgetglobflags / c:1129 range_type / c:1152 pattern_range_to_string
9144 // c:2059 charref / c:2066 charnext / c:2153 pattry / c:2304 patmatchlen
9145 // ═══════════════════════════════════════════════════════════════════
9146
9147 /// c:262 — `metacharinc` is pure for ASCII input.
9148 #[test]
9149 fn metacharinc_is_pure_for_ascii() {
9150 for s in ["", "a", "abc", "hello"] {
9151 for pos in 0..s.len() {
9152 let first = metacharinc(s, pos);
9153 for _ in 0..3 {
9154 assert_eq!(
9155 metacharinc(s, pos),
9156 first,
9157 "metacharinc({:?}, {}) must be pure",
9158 s,
9159 pos
9160 );
9161 }
9162 }
9163 }
9164 }
9165
9166 /// c:536 — `patcompile("", 0, None)` returns Option<Patprog>.
9167 #[test]
9168 fn patcompile_returns_option_patprog_type() {
9169 let _g = crate::test_util::global_state_lock();
9170 let _: Option<Patprog> = patcompile("", 0, None);
9171 }
9172
9173 /// c:1008 — `patgetglobflags("")` empty returns None.
9174 #[test]
9175 fn patgetglobflags_empty_returns_none() {
9176 let _g = crate::test_util::global_state_lock();
9177 assert!(patgetglobflags("").is_none(), "empty → None");
9178 }
9179
9180 /// c:1008 — `patgetglobflags` returns Option<(i32, i64, usize)> type.
9181 #[test]
9182 fn patgetglobflags_returns_option_tuple_type() {
9183 let _g = crate::test_util::global_state_lock();
9184 let _: Option<(i32, i64, usize)> = patgetglobflags("abc");
9185 }
9186
9187 /// c:1129 — `range_type("")` empty returns None.
9188 #[test]
9189 fn range_type_empty_returns_none() {
9190 assert!(range_type("").is_none(), "empty → None");
9191 }
9192
9193 /// c:1129 — `range_type` returns Option<usize>.
9194 #[test]
9195 fn range_type_returns_option_usize_type() {
9196 let _: Option<usize> = range_type("abc");
9197 }
9198
9199 /// c:1152 — `pattern_range_to_string("")` empty returns empty String.
9200 #[test]
9201 fn pattern_range_to_string_empty_returns_empty() {
9202 assert_eq!(pattern_range_to_string(""), "");
9203 }
9204
9205 /// c:1152 — `pattern_range_to_string` is pure.
9206 #[test]
9207 fn pattern_range_to_string_is_pure() {
9208 for s in ["", "abc", "a-z"] {
9209 let first = pattern_range_to_string(s);
9210 for _ in 0..3 {
9211 assert_eq!(
9212 pattern_range_to_string(s),
9213 first,
9214 "pattern_range_to_string({:?}) must be pure",
9215 s
9216 );
9217 }
9218 }
9219 }
9220
9221 /// c:2059 — `charref("", 0)` empty returns None.
9222 #[test]
9223 fn charref_empty_returns_none() {
9224 assert!(charref("", 0).is_none(), "empty → None");
9225 }
9226
9227 /// c:2066 — `charnext("", 0)` empty returns 0 (no advance).
9228 #[test]
9229 fn charnext_empty_returns_zero() {
9230 assert_eq!(charnext("", 0), 0);
9231 }
9232
9233 /// c:2304 — `patmatchlen` returns i32 (compile-time type pin).
9234 #[test]
9235 fn patmatchlen_returns_i32_type() {
9236 let _g = crate::test_util::global_state_lock();
9237 let _: i32 = patmatchlen();
9238 }
9239
9240 /// c:155 — P_ISBRANCH/P_ISEXCLUDE/P_NOTDOT all return bool (type pin).
9241 #[test]
9242 fn p_predicates_return_bool_type() {
9243 let _: bool = P_ISBRANCH(0);
9244 let _: bool = P_ISEXCLUDE(0);
9245 let _: bool = P_NOTDOT(0);
9246 }
9247}