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 // c:Src/pattern.c:499-506 — `if (isset(SHGLOB))
2305 // zpc_special[ZPC_INPAR] = zpc_special[ZPC_INANG] = Marker;`
2306 // ("Grouping and numeric ranges are not valid. We do allow
2307 // alternation, however; it's needed for case."). patcompcharsset
2308 // (pattern.rs:487) already applies that mask, but this arm never
2309 // consulted the table: it ran its inline isnumglob walk
2310 // unconditionally, so `<->` stayed an ACTIVE numeric range under
2311 // `emulate sh` / `emulate ksh`. C asserts the case is unreachable
2312 // there — `DPUTS(isset(SHGLOB), "Treating <..> as numeric range
2313 // with SHGLOB")` (c:1532). A disabled slot also covers user
2314 // `disable -p '<'`. Fall through to the same literal-`<` emit the
2315 // non-numeric rewind path uses. Bug #1053-B.
2316 let inang_disabled = { zpc_special.lock().unwrap()[ZPC_INANG as usize] != b'<' };
2317 if inang_disabled {
2318 let h = patnode(P_EXACTLY);
2319 let mut buf = patout.lock().unwrap();
2320 let len: u32 = 1;
2321 buf.extend_from_slice(&len.to_le_bytes());
2322 buf.push(b'<');
2323 drop(buf);
2324 patparse_off.fetch_add(1, Ordering::Relaxed);
2325 *flagp |= P_SIMPLE;
2326 *tail_out = h;
2327 return h as i64;
2328 }
2329 let entry_off = patparse_off.load(Ordering::Relaxed);
2330 patparse_off.fetch_add(1, Ordering::Relaxed); // consume `<`
2331 let parse_n = patparse.lock().unwrap();
2332 let nb = parse_n.as_bytes();
2333 let mut j = patparse_off.load(Ordering::Relaxed);
2334 let mut len_flag: u8 = 0; // bit 0 = lo present, bit 1 = hi present
2335 let mut from: i64 = 0;
2336 let lo_start = j;
2337 while j < nb.len() && nb[j].is_ascii_digit() {
2338 from = from * 10 + (nb[j] - b'0') as i64;
2339 j += 1;
2340 }
2341 if j > lo_start {
2342 len_flag |= 1;
2343 } // c:1538 — `len |= 1`
2344 // Mandatory dash. C's isnumglob bails (ret=0) here too —
2345 // walks until non-digit, then if non-digit isn't `-` (or `>`
2346 // after seeing `-`), returns "not numglob". Rewind the `<`
2347 // consumption and fall through to the literal-run arm.
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; // c:1543 patparse++
2362 let mut to: i64 = 0;
2363 let hi_start = j;
2364 while j < nb.len() && nb[j].is_ascii_digit() {
2365 to = to * 10 + (nb[j] - b'0') as i64;
2366 j += 1;
2367 }
2368 if j > hi_start {
2369 len_flag |= 2;
2370 } // c:1548 — `len |= 2`
2371 // Expect closing '>'. Mirror the dash-failure rewind here
2372 // too: a `<N-` without `>` is non-numglob per C's isnumglob.
2373 if j >= nb.len() || nb[j] != b'>' {
2374 drop(parse_n);
2375 patparse_off.store(entry_off, Ordering::Relaxed);
2376 let h = patnode(P_EXACTLY);
2377 let mut buf = patout.lock().unwrap();
2378 let len: u32 = 1;
2379 buf.extend_from_slice(&len.to_le_bytes());
2380 buf.push(b'<');
2381 patparse_off.fetch_add(1, Ordering::Relaxed);
2382 *flagp |= P_SIMPLE;
2383 *tail_out = h;
2384 return h as i64;
2385 }
2386 j += 1;
2387 drop(parse_n);
2388 patparse_off.store(j, Ordering::Relaxed);
2389 *flagp &= !P_PURESTR;
2390
2391 let off2 = match len_flag {
2392 // c:1552-1567
2393 3 => {
2394 // c:1554 P_NUMRNG
2395 let off2 = patnode(P_NUMRNG);
2396 let mut buf = patout.lock().unwrap();
2397 buf.extend_from_slice(&from.to_le_bytes());
2398 buf.extend_from_slice(&to.to_le_bytes());
2399 off2
2400 }
2401 2 => {
2402 // c:1559 P_NUMTO
2403 let off2 = patnode(P_NUMTO);
2404 let mut buf = patout.lock().unwrap();
2405 buf.extend_from_slice(&to.to_le_bytes());
2406 off2
2407 }
2408 1 => {
2409 // c:1563 P_NUMFROM
2410 let off2 = patnode(P_NUMFROM);
2411 let mut buf = patout.lock().unwrap();
2412 buf.extend_from_slice(&from.to_le_bytes());
2413 off2
2414 }
2415 _ => patnode(P_NUMANY), // c:1568
2416 };
2417 *tail_out = off2;
2418 off2 as i64
2419 }
2420 _ => {
2421 // Accumulate a literal run.
2422 let mut buf: Vec<u8> = Vec::new();
2423 let mut local_off = off;
2424 let p = patparse.lock().unwrap();
2425 // c:1312-1317 — break on kshchar trigger (any of the six
2426 // ZPC_KSH_* slots whose literal byte is followed by `(`).
2427 // Snapshot the zpc_special slots that participate; when
2428 // KSHGLOB is off they're Marker (0xa2) and won't match
2429 // ordinary bytes.
2430 let (ksh_plus, ksh_bang, ksh_bang2, ksh_at, ksh_star, ksh_quest) = {
2431 let sp = zpc_special.lock().unwrap();
2432 (
2433 sp[ZPC_KSH_PLUS as usize],
2434 sp[ZPC_KSH_BANG as usize],
2435 sp[ZPC_KSH_BANG2 as usize],
2436 sp[ZPC_KSH_AT as usize],
2437 sp[ZPC_KSH_STAR as usize],
2438 sp[ZPC_KSH_QUEST as usize],
2439 )
2440 };
2441 // c:1314-1317 — `~` is a literal-run terminator IFF it's
2442 // active as the exclusion operator (EXTENDEDGLOB on) AND
2443 // the lookahead is `/` or a non-segment-special byte.
2444 // Without EXTENDEDGLOB the byte is Marker (0xa2) so the
2445 // equality check fails naturally. Limited to TILDE only —
2446 // the other ZPC_SEG_COUNT slots (SLASH, NULL, BAR, OUTPAR)
2447 // are already covered by the explicit stop list above
2448 // (`|` `)` handled there; `/` was deliberately NOT a
2449 // literal-run break in this Rust port, see
2450 // `zsh_corpus_hash_s_e_anchors_match_bare_test` which
2451 // expects `/` to stay literal inside `(...)` alternation).
2452 let (sp_tilde_lit, sp_seg_lit_set, sp_hat_lit, sp_hash_lit) = {
2453 let sp = zpc_special.lock().unwrap();
2454 let mut set = [false; 256];
2455 for i in 0..(ZPC_SEG_COUNT as usize) {
2456 set[sp[i] as usize] = true;
2457 }
2458 (
2459 sp[ZPC_TILDE as usize],
2460 set,
2461 sp[ZPC_HAT as usize],
2462 sp[ZPC_HASH as usize],
2463 )
2464 };
2465 while local_off < p.len() {
2466 let b = p.as_bytes()[local_off];
2467 // c:Src/pattern.c:480-483 — `if (!isset(EXTENDEDGLOB))`
2468 // masks ZPC_HAT and ZPC_HASH to Marker so the
2469 // patcompiece dispatch treats `^` / `#` as literals
2470 // (no negation / no zero-or-more closure).
2471 // The literal-run break list hardcoded `b'^'` /
2472 // `b'#'` though, which still terminated the literal
2473 // accumulator and forced patcompiece's empty-buf
2474 // -1 return → "bad pattern: ^.*" for the unset
2475 // EXTENDEDGLOB case. Gate the break on the actual
2476 // zpc_special slot so an EXTENDEDGLOB-off run treats
2477 // `^` and `#` as ordinary chars. Bug #421.
2478 if matches!(b, b'?' | b'*' | b'[' | b'(' | b')' | b'|' | b'\\' | b'<')
2479 || (b == b'^' && sp_hat_lit == b'^')
2480 || (b == b'#' && sp_hash_lit == b'#')
2481 {
2482 break;
2483 }
2484 if b == sp_tilde_lit && sp_tilde_lit != 0 {
2485 let la = p.as_bytes().get(local_off + 1).copied().unwrap_or(0);
2486 // Literal-tilde exception: `~` followed by a
2487 // segment-special OTHER than `/` stays in the run.
2488 let literal_tilde_exception = la != b'/' && sp_seg_lit_set[la as usize];
2489 if !literal_tilde_exception {
2490 break;
2491 }
2492 }
2493 // c:1278-1292 — ksh-glob trigger lookahead. If the next
2494 // byte is `(` AND this byte matches one of the six
2495 // ZPC_KSH_* slots, stop the literal run BEFORE this byte
2496 // so the next patcomppiece call consumes it as a ksh
2497 // quantifier prefix (e.g. `pre+(x)post` accumulates
2498 // `pre` then breaks at `+` since `+(` follows).
2499 if local_off + 1 < p.len() && p.as_bytes()[local_off + 1] == b'(' {
2500 if b == ksh_plus
2501 || b == ksh_bang
2502 || b == ksh_bang2
2503 || b == ksh_at
2504 || b == ksh_star
2505 || b == ksh_quest
2506 {
2507 break;
2508 }
2509 }
2510 buf.push(b);
2511 local_off += 1;
2512 }
2513 drop(p);
2514 if buf.is_empty() {
2515 return -1;
2516 }
2517 // c:Src/pattern.c:1340-1351 — multi-char run with TRAILING `#`
2518 // (or `(#cN,M)`) must backtrack ONE char so the trailing
2519 // quantifier applies to the LAST char only. Without this,
2520 // `fo#` compiles as `(fo)#` instead of `f` + `o#`, breaking
2521 // `(fo#)#` against "ffo" (the test pin we are fixing).
2522 //
2523 // C body:
2524 // if ((*patparse == zpc_special[ZPC_HASH] || ...) && morelen)
2525 // patparse = patprev;
2526 //
2527 // morelen = "more than one char in the literal run". The
2528 // backtrack pops the LAST byte from buf and rewinds patparse
2529 // by 1 so the next patcomppiece call sees that byte as its
2530 // own atom.
2531 let has_trailing_hash = {
2532 let sp_hash = zpc_special.lock().unwrap()[ZPC_HASH as usize];
2533 let p = patparse.lock().unwrap();
2534 let hash_at = local_off < p.len() && p.as_bytes()[local_off] == b'#';
2535 drop(p);
2536 hash_at && sp_hash == b'#'
2537 };
2538 if buf.len() > 1 && has_trailing_hash {
2539 let _ = buf.pop();
2540 local_off -= 1;
2541 }
2542 patparse_off.store(local_off, Ordering::Relaxed);
2543 *flagp |= P_SIMPLE;
2544 // If it's a single char, mark simple; multi-char run stays pure-string.
2545 let lit_off = patnode(P_EXACTLY);
2546 let mut buf_lit = patout.lock().unwrap();
2547 let len = buf.len() as u32;
2548 buf_lit.extend_from_slice(&len.to_le_bytes());
2549 buf_lit.extend_from_slice(&buf);
2550 *tail_out = lit_off;
2551 lit_off as i64
2552 }
2553 };
2554
2555 if atom < 0 {
2556 return atom;
2557 }
2558
2559 // c:1606-1644 — quantifier dispatch. Three sources of quantifier:
2560 // (a) trailing `#` / `##` (zsh-extended hash repetition)
2561 // (b) trailing `(#cN,M)` count-spec (handled in patcompbranch, not here)
2562 // (c) kshchar form `+/*/?` already at the atom's leading byte.
2563 //
2564 // Rule c:1615 — if no hash AND no count AND kshchar is one of
2565 // (none, '@', '!'), no quantifier; return atom as-is. `@` is the
2566 // identity quantifier (match group exactly once); `!` already had
2567 // its negation compiled by patcompnot inside the atom.
2568 let q_off = patparse_off.load(Ordering::Relaxed);
2569 let parse2 = patparse.lock().unwrap();
2570 // c:1611-1620 — `if (*patparse == zpc_special[ZPC_HASH])` quantifier
2571 // dispatch. The C source reads `zpc_special[ZPC_HASH]`, which
2572 // patcompcharsset (c:480-483) sets to `'#'` only when
2573 // EXTENDEDGLOB is on, else to the Marker byte (c:476). A literal
2574 // '#' in the source pattern therefore does NOT trigger the
2575 // quantifier path when EXTENDEDGLOB is off — the comparison
2576 // `*patparse == Marker` fails. Previously this Rust check used
2577 // a bare `b'#'` comparison instead of consulting zpc_special,
2578 // making `(x)#` and `[a-z]##[0-9]##` match even with
2579 // extendedglob OFF (parity bugs #5/#6 vs real zsh).
2580 let hash_char = zpc_special.lock().unwrap()[ZPC_HASH as usize]; // c:1612
2581 let has_hash = hash_char == b'#' && q_off < parse2.len() && parse2.as_bytes()[q_off] == b'#'; // c:1611-1614
2582 drop(parse2);
2583 if !has_hash && (kshchar == 0 || kshchar == b'@' || kshchar == b'!') {
2584 return atom; // c:1616-1617
2585 }
2586
2587 // c:1621-1622 — kshchar with hash is a parse error (too much at once).
2588 if kshchar != 0 && has_hash {
2589 return -1;
2590 }
2591
2592 // c:1624-1644 — pick the operator + post-atom flags.
2593 let op: u8;
2594 let mut consume_hashes = 0;
2595 if kshchar == b'*' {
2596 // c:1624-1626
2597 op = P_ONEHASH;
2598 *flagp = P_HSTART;
2599 } else if kshchar == b'+' {
2600 // c:1627-1629
2601 op = P_TWOHASH;
2602 *flagp = P_HSTART;
2603 } else if kshchar == b'?' {
2604 // c:1630-1632
2605 op = 0; // sentinel — `?` desugars to (x|) via the BRANCH path below
2606 *flagp = 0;
2607 } else {
2608 // c:1637-1644 — `#` / `##`.
2609 let parse_h = patparse.lock().unwrap();
2610 let two = q_off + 1 < parse_h.len() && parse_h.as_bytes()[q_off + 1] == b'#';
2611 drop(parse_h);
2612 if two {
2613 op = P_TWOHASH;
2614 consume_hashes = 2;
2615 } else {
2616 op = P_ONEHASH;
2617 consume_hashes = 1;
2618 }
2619 *flagp = P_HSTART;
2620 }
2621 if consume_hashes > 0 {
2622 patparse_off.fetch_add(consume_hashes, Ordering::Relaxed);
2623 }
2624
2625 // c:1705-1746 — quantifier emission.
2626 //
2627 // Read the atom's opcode so c:1705 (`?#` → `*`) optimization can
2628 // apply. The atom byte sits at `atom + I_OP` in patout.
2629 let atom_op = {
2630 let buf = patout.lock().unwrap();
2631 if (atom as usize) + I_OP < buf.len() {
2632 buf[atom as usize + I_OP]
2633 } else {
2634 0
2635 }
2636 };
2637
2638 if ((*flagp & P_SIMPLE) != 0) && (op == P_ONEHASH || op == P_TWOHASH) && atom_op == P_ANY {
2639 // c:1705-1717 — `?#` becomes `*`; `?##` becomes `?*`. The atom
2640 // is P_ANY at offset `atom`; rewrite or pad as needed.
2641 let mut buf = patout.lock().unwrap();
2642 if op == P_TWOHASH {
2643 // c:1712-1713 — `?##` → `?*`: leave the P_ANY in place,
2644 // then emit P_STAR right after.
2645 drop(buf);
2646 let _star = patnode(P_STAR);
2647 *tail_out = atom as usize;
2648 } else {
2649 // c:1715-1716 — `?#` → `*`: just rewrite atom's opcode.
2650 buf[atom as usize + I_OP] = P_STAR;
2651 drop(buf);
2652 *tail_out = atom as usize;
2653 }
2654 *flagp &= !P_PURESTR;
2655 } else if ((*flagp & P_SIMPLE) != 0)
2656 && op != 0
2657 && (patglobflags.load(Ordering::Relaxed) & 0xff) == 0
2658 {
2659 // c:1718-1720 — simple operand, no approximate-match counter:
2660 // emit `patinsert(op, starter, NULL, 0)`. The matcher walks
2661 // the inserted P_ONEHASH/P_TWOHASH operand at scan+I_BODY.
2662 patinsert(op, atom as usize, None, 0);
2663 *flagp &= !P_PURESTR;
2664 *tail_out = atom as usize;
2665 } else if op == P_ONEHASH {
2666 // c:1721-1729 — emit x# as (x&|): P_WBRANCH with NULL payload
2667 // ahead of atom; loop via P_BACK; null alternative branch.
2668 // patinsert with `sz=size_of::<union upat>()=8` (we use 8 to
2669 // mirror the C `union upat` slot, then zero-pad it).
2670 let payload = [0u8; 8];
2671 patinsert(P_WBRANCH, atom as usize, Some(&payload), 8);
2672 // Either x — atom is now at atom+I_BODY+8.
2673 let back = patnode(P_BACK);
2674 patoptail(atom as usize, back); // c:1726 — loop back
2675 patoptail(atom as usize, atom as usize); // c:1727
2676 let alt = patnode(P_BRANCH);
2677 pattail(atom as usize, alt); // c:1728 — or
2678 let null_node = patnode(P_NOTHING);
2679 pattail(atom as usize, null_node); // c:1729 — null
2680 *flagp &= !P_PURESTR;
2681 // The piece's chain-tail is the trailing P_NOTHING node — its
2682 // `.next` slot is empty and is the correct splice point for
2683 // whatever piece patcompbranch chains in next.
2684 *tail_out = null_node;
2685 } else if op == P_TWOHASH {
2686 // c:1730-1738 — emit x## as x(&|): P_WBRANCH after atom; loop
2687 // back; null branch.
2688 let wbranch = patnode(P_WBRANCH); // c:1732 — either
2689 let payload = [0u8; 8]; // c:1733-1734 patadd((char *)&up, ..., sizeof(up), 0)
2690 {
2691 let mut buf = patout.lock().unwrap();
2692 buf.extend_from_slice(&payload);
2693 }
2694 pattail(atom as usize, wbranch); // c:1735
2695 let back = patnode(P_BACK);
2696 pattail(back, atom as usize); // c:1736 — loop back
2697 let alt = patnode(P_BRANCH);
2698 pattail(wbranch, alt); // c:1737 — or
2699 let null_node = patnode(P_NOTHING);
2700 pattail(atom as usize, null_node); // c:1738 — null
2701 *flagp &= !P_PURESTR;
2702 // Same as ONEHASH path — tail at the trailing P_NOTHING.
2703 *tail_out = null_node;
2704 } else if kshchar == b'?' {
2705 // c:1739-1746 — emit ?(x) as (x|).
2706 patinsert(P_BRANCH, atom as usize, None, 0); // c:1741 — either x
2707 let alt = patnode(P_BRANCH); // c:1742 — or
2708 pattail(atom as usize, alt);
2709 let null_node = patnode(P_NOTHING); // c:1743 — null
2710 pattail(atom as usize, null_node); // c:1744
2711 patoptail(atom as usize, null_node); // c:1745
2712 *flagp &= !P_PURESTR;
2713 // Same as ONEHASH/TWOHASH — tail at the trailing P_NOTHING.
2714 *tail_out = null_node;
2715 }
2716
2717 // c:1747-1748 — a stray `#` immediately after = compile error.
2718 {
2719 let p = patparse.lock().unwrap();
2720 let q2 = patparse_off.load(Ordering::Relaxed);
2721 if q2 < p.len() && p.as_bytes()[q2] == b'#' {
2722 // Only flag as error when EXTENDEDGLOB-style # is enabled.
2723 let sp = zpc_special.lock().unwrap();
2724 if sp[ZPC_HASH as usize] == b'#' {
2725 return -1;
2726 }
2727 }
2728 }
2729
2730 atom
2731}
2732
2733/// Port of `patcompnot(int paren, int *flagsp)` from `Src/pattern.c:1760`.
2734///
2735/// C: `static long patcompnot(int paren, int *flagsp)`. Implements
2736/// the `^pat` (paren=0) or `!(pat)` (paren=1) extended/ksh-glob
2737/// negation by emitting `P_BRANCH → P_STAR → P_EXCSYNC` followed by
2738/// `P_EXCLUDE [payload]` and the asserted pattern terminated by
2739/// `P_EXCEND`, all joined at a trailing `P_NOTHING`.
2740///
2741/// NOTE: matcher support for `P_EXCSYNC`/`P_EXCEND`/`P_EXCLUDE` is
2742/// only partial in the Rust port — compile is faithful per
2743/// pattern.c:1760-1784 but match-time negation semantics may diverge
2744/// from C for complex backtracking edge cases. Tests covering
2745/// `!(foo)` / `!(foo|bar)` exercise the basic working subset.
2746pub fn patcompnot(paren: i32, flagsp: &mut i32) -> i64 {
2747 // c:1760
2748 // c:1767 — `*flagsp = P_HSTART;`. Negation always starts with `*`
2749 // semantically so the caller knows the piece can match at the
2750 // start of input.
2751 *flagsp = P_HSTART;
2752
2753 // c:1769 — `starter = patnode(P_BRANCH);`
2754 let starter = patnode(P_BRANCH);
2755 // c:1770 — `br = patnode(P_STAR);`
2756 let br = patnode(P_STAR);
2757 // c:1771 — `excsync = patnode(P_EXCSYNC);`
2758 let excsync = patnode(P_EXCSYNC);
2759 // c:1772 — `pattail(br, excsync);`
2760 pattail(br, excsync);
2761 // c:1773 — `pattail(starter, excl = patnode(P_EXCLUDE));`
2762 let excl = patnode(P_EXCLUDE);
2763 pattail(starter, excl);
2764 // c:1774-1775 — `up.p = NULL; patadd((char *)&up, 0, sizeof(up), 0);`
2765 // The EXCLUDE node carries an 8-byte syncptr payload, NULL-init.
2766 {
2767 let mut buf = patout.lock().unwrap();
2768 buf.extend_from_slice(&[0u8; 8]);
2769 }
2770 // c:1776 — `br = (paren ? patcompswitch(1, &dummy) : patcompbranch(&dummy, 0));`
2771 let mut dummy: i32 = 0;
2772 let inner = if paren != 0 {
2773 let r = patcompswitch(1, &mut dummy);
2774 // c:1503-1505 — caller `Inpar` arm expects to consume the
2775 // trailing `)`. Mirror here since patcompnot is invoked from
2776 // the b'(' atom-arm AFTER it already consumed `(`.
2777 if r >= 0 {
2778 let cur = patparse_off.load(Ordering::Relaxed);
2779 let p = patparse.lock().unwrap();
2780 if cur >= p.len() || p.as_bytes()[cur] != b')' {
2781 return -1;
2782 }
2783 drop(p);
2784 patparse_off.fetch_add(1, Ordering::Relaxed);
2785 }
2786 r
2787 } else {
2788 patcompbranch(&mut dummy, 0)
2789 };
2790 if inner < 0 {
2791 return -1; // c:1777 `return 0;` (Rust uses -1 for failure)
2792 }
2793 // c:1778 — `pattail(br, patnode(P_EXCEND));`
2794 let excend = patnode(P_EXCEND);
2795 pattail(inner as usize, excend);
2796 // c:1779 — `n = patnode(P_NOTHING);`
2797 let n = patnode(P_NOTHING);
2798 // c:1780 — `pattail(excsync, n);`
2799 pattail(excsync, n);
2800 // c:1781 — `pattail(excl, n);`
2801 pattail(excl, n);
2802 // c:1783
2803 let _ = br; // suppress unused-var (kept for c-name parity)
2804 starter as i64
2805}
2806
2807/// Port of `patnode(long op)` from `Src/pattern.c:1790`.
2808///
2809/// C: `static long patnode(long op)` — writes a 1-byte opcode plus a
2810/// 4-byte zeroed next-offset. Returns the offset of the opcode byte.
2811fn patnode(op: u8) -> usize {
2812 // c:1790
2813 let mut buf = patout.lock().unwrap();
2814 let off = buf.len();
2815 buf.push(op); // I_OP
2816 buf.extend_from_slice(&[0, 0, 0, 0]); // I_NEXT zeroed
2817 off
2818}
2819
2820/// Port of `patinsert(long op, int opnd, char *xtra, int sz)` from `Src/pattern.c:1807`.
2821///
2822/// C: `static void patinsert(long op, int opnd, char *xtra, int sz)`.
2823/// Inserts an opcode (+ next slot) at position `opnd`, shifting bytes
2824/// after it down by `5 + sz`, then writes `xtra` payload of `sz` bytes.
2825fn patinsert(op: u8, opnd: usize, xtra: Option<&[u8]>, sz: usize) {
2826 // c:1807
2827 let mut buf = patout.lock().unwrap();
2828 let header_sz = 1 + 4; // op + next
2829 let total = header_sz + sz;
2830 // Insert `total` zeroed bytes at opnd, then overwrite.
2831 let mut inserted = vec![0u8; total];
2832 inserted[0] = op;
2833 if let Some(x) = xtra {
2834 let copy_n = x.len().min(sz);
2835 inserted[header_sz..header_sz + copy_n].copy_from_slice(&x[..copy_n]);
2836 }
2837 buf.splice(opnd..opnd, inserted);
2838 // Patch up next_off chains pointing past opnd by adding `total`.
2839 fixup_offsets_after_insert(&mut buf, opnd, total as u32);
2840}
2841
2842/// Port of `pattail(long p, long val)` from `Src/pattern.c:1834`.
2843///
2844/// C: `static void pattail(long p, long val)` — patches the next-offset
2845/// field of the opcode at offset `p` to point to `val`. Walks any
2846/// existing chain to the end before patching.
2847fn pattail(p: usize, val: usize) {
2848 // c:1834
2849 let mut buf = patout.lock().unwrap();
2850 let mut cur = p;
2851 loop {
2852 if cur + I_BODY > buf.len() {
2853 return;
2854 }
2855 let next_bytes: [u8; 4] = buf[cur + I_NEXT..cur + I_NEXT + 4].try_into().unwrap();
2856 let next = u32::from_le_bytes(next_bytes) as usize;
2857 if next == 0 {
2858 break;
2859 }
2860 cur = next;
2861 }
2862 let val_bytes = (val as u32).to_le_bytes();
2863 if cur + I_NEXT + 4 <= buf.len() {
2864 buf[cur + I_NEXT..cur + I_NEXT + 4].copy_from_slice(&val_bytes);
2865 }
2866}
2867
2868/// Port of `patoptail(long p, long val)` from `Src/pattern.c:1856`.
2869///
2870/// C: `static void patoptail(long p, long val)` — like pattail but
2871/// only patches branches (P_BRANCH/P_WBRANCH).
2872///
2873/// C: c:1862-1865 — for P_BRANCH the operand sits at `P_OPERAND(p)` =
2874/// `p+1` (Upat slot, i.e. byte offset `p + I_BODY` in Rust); for
2875/// P_WBRANCH the operand sits at `P_OPERAND(p) + 1` (skipping the
2876/// 8-byte syncptr payload Upat slot), i.e. byte offset
2877/// `p + I_BODY + 8` in Rust.
2878fn patoptail(p: usize, val: usize) {
2879 // c:1856
2880 let buf = patout.lock().unwrap();
2881 if p + I_OP >= buf.len() {
2882 return;
2883 }
2884 let op = buf[p + I_OP];
2885 drop(buf);
2886 if P_ISBRANCH(op) {
2887 // c:1862-1865 — operand offset depends on op kind.
2888 if op == P_BRANCH {
2889 pattail(p + I_BODY, val);
2890 } else {
2891 // P_WBRANCH / P_EXCLUDE / P_EXCLUDP — operand sits after
2892 // the 8-byte syncptr payload.
2893 pattail(p + I_BODY + 8, val);
2894 }
2895 }
2896}
2897
2898// =====================================================================
2899// 13/14. Matcher — pattern.c:2223-3579
2900// =====================================================================
2901
2902/// State accumulated during a single `patmatch` walk. C uses
2903/// per-thread globals (`patbeginp[]` / `patendp[]` for captures);
2904/// the Rust port encapsulates them in this struct passed by `&mut`.
2905/// Rule D: this struct represents matcher-internal scratch state
2906/// (analogous to `struct rpat pattrystate` at pattern.c:248), not a
2907/// bag-of-globals from unrelated subsystems.
2908///
2909/// **C counterpart**: `struct rpat` at `pattern.c:248`.
2910#[derive(Clone)]
2911#[allow(non_camel_case_types)]
2912pub struct rpat {
2913 /// Per-P_WBRANCH visit bitmap, keyed by WBRANCH-opcode offset.
2914 /// Mirrors C's `Upat ptrp` (`pattern.c:3217`): every WBRANCH carries
2915 /// an 8-byte payload sized as `union upat` — initialised to NULL,
2916 /// then lazily filled by `patmatch` with a buffer the size of the
2917 /// input string. Each byte tracks "have we already tried this
2918 /// WBRANCH at this input position with at most this many errors?".
2919 /// On revisit at the same position with the same-or-fewer errors,
2920 /// C returns 0 to bound the recursion (`pattern.c:3245-3248`). The
2921 /// Rust port keeps the bitmap on rpat (per-pattry call) instead of
2922 /// inside the bytecode payload so the bytecode stays read-only —
2923 /// the key is the WBRANCH offset; the value is a Vec<u8> the size
2924 /// of the input. Without it, `(fo#)#` against ANY input that
2925 /// requires the closure to consume at least one char per iteration
2926 /// (like "ffo") burns through PATMATCH_MAX_DEPTH and aborts.
2927 pub wbranch_visits: std::collections::HashMap<usize, Vec<u8>>,
2928 pub patbeginp: [usize; NSUBEXP], // c:241 capture starts (byte offsets)
2929 pub patendp: [usize; NSUBEXP], // c:242 capture ends
2930 /// `parsfound` from `Src/pattern.c` (per c:2957/c:2989 references).
2931 /// Two-stripe bitmap: bits `0..NSUBEXP` track per-group P_OPEN
2932 /// first-write (`patbeginp[i]` committed); bits `NSUBEXP..2*NSUBEXP`
2933 /// track per-group P_CLOSE first-write (`patendp[i]` committed).
2934 /// Bit `n-1` (open) = `1 << (n-1)`; bit `n-1+NSUBEXP` (close) =
2935 /// `1 << (n-1+NSUBEXP)`. Width u32 to fit `2*NSUBEXP = 18` bits.
2936 /// The prior u16 width with a single-stripe (close-only) bit
2937 /// allowed P_OPEN to re-overwrite `patbeginp[i]` on every
2938 /// backtrack iteration — turning the SECOND capture's start
2939 /// offset into the FIRST iteration's, e.g. `(*)-(*)` against
2940 /// `hello-world` returned `match[2] = "hello-world"` instead of
2941 /// the right `"world"`.
2942 pub captures_set: u32, // c:Src/pattern.c parsfound
2943 /// Port of file-static `int errsfound` from `Src/pattern.c:2046`.
2944 /// Cumulative edit-count for approximate-match `(#aN)`. Reset to 0
2945 /// at the top of each pattry, incremented on P_EXACTLY mismatches
2946 /// when `glob_flags & 0xff > 0` (the substitution-budget byte).
2947 pub errsfound: i32, // c:2046
2948}
2949
2950/// Port of `wchar_t charref(char *x, char *y, int *zmb_ind)` from
2951/// `Src/pattern.c:1909`. Decode the char at `pos` without
2952/// advancing. UTF-32-native delegation: `s.chars().next()` returns
2953/// the decoded codepoint; delegates to Rust's native UTF-8 string
2954/// iterator instead of the C Meta-decode + zshtoken-translate +
2955/// mbrtowc state machine, which all collapse because Rust's `&str`
2956/// is already UTF-8.
2957///
2958/// Rust signature differs from C `charref(char *x, char *y, int *zmb_ind)`:
2959/// the C `y` end-of-string pointer is captured by `&str` length; the
2960/// `zmb_ind` out-param (multibyte-completion index) is dropped — the
2961/// Rust UTF-8 decode is one-shot, never partial.
2962pub fn charref(s: &str, pos: usize) -> Option<char> {
2963 // c:1909
2964 s[pos..].chars().next()
2965}
2966
2967/// Port of `char *charnext(char *x, char *y)` from `Src/pattern.c:1935`.
2968/// C returns the pointer past the current character; the Rust port
2969/// returns that position as a byte offset. Delegates to `metacharinc`
2970/// — same advance-by-one-codepoint logic.
2971pub fn charnext(x: &str, y: usize) -> usize {
2972 // c:1936
2973 metacharinc(x, y)
2974}
2975
2976/// Port of `wchar_t charrefinc(char **x, char *y, int *z)` from
2977/// `Src/pattern.c:1964`. Decode + advance: delegates to the
2978/// `charref`-then-`len_utf8`-step pattern. The C body's Meta /
2979/// mbrtowc / zshtoken triple collapses to one `chars().next()`
2980/// call followed by a byte-count step.
2981///
2982/// Rust signature differs from C: C mutates `*x` via the
2983/// `char **` to advance; Rust mutates `pos` directly. The `y`
2984/// end-of-string sentinel is captured by `&str` length; `z`
2985/// (multibyte-completion index) is dropped per the same one-shot
2986/// UTF-8 decode argument as `charref`.
2987pub fn charrefinc(s: &str, pos: &mut usize) -> Option<char> {
2988 // c:1964
2989 let c = s[*pos..].chars().next()?;
2990 *pos += c.len_utf8();
2991 Some(c)
2992}
2993
2994/// Port of `ptrdiff_t charsub(char *x, char *y)` from `Src/pattern.c:1997`.
2995///
2996/// Returns the number of characters between the start `x` and the
2997/// position `y` — i.e. the character-distance of byte offset `y` from
2998/// the string start. Non-multibyte: the byte distance `y` (each byte
2999/// is one character). Multibyte: the codepoint count of `x[0..y]`.
3000/// `patmatch` uses this (via the C `CHARSUB` macro) to report match
3001/// lengths and backreference positions in characters.
3002///
3003/// Replaces the prior fake that stepped back one char and returned a
3004/// byte offset — unrelated to the C character-count semantics.
3005pub fn charsub(x: &str, y: usize) -> usize {
3006 // c:1997
3007 let y = y.min(x.len());
3008 if !isset(MULTIBYTE) {
3009 // c:2003-2004 — `if (!isset(MULTIBYTE)) return y - x;`
3010 return y;
3011 }
3012 // c:2006-2021 — count codepoints in `x[0..y]`. Rust `&str` is valid
3013 // UTF-8, so `chars().count()` is the faithful equivalent of the C
3014 // mbrtowc loop (the MB_INVALID byte-by-byte fallback can't arise —
3015 // a `&str` never holds invalid sequences).
3016 x[..y].chars().count()
3017}
3018
3019// =====================================================================
3020// 16. String pre-processing — pattern.c:2063, :2080, :2132
3021// =====================================================================
3022
3023/// Port of `pattrystart()` from `Src/pattern.c:2063`. C resets per-
3024/// match state globals; Rust state is per-call so no-op.
3025pub fn pattrystart() {} // c:2063
3026
3027/// Port of `void patmungestring(char **string, int *stringlen, int *unmetalenin)`
3028/// from `Src/pattern.c:2080`.
3029///
3030/// Skips a leading `Nularg` (the empty-tokenised-string sentinel)
3031/// and computes `*stringlen` from `strlen` when caller passed `-1`.
3032/// Mutates all three in/out args; mirrors C's `char **` /
3033/// `int *` semantics via Rust mutable references.
3034pub fn patmungestring(string: &mut &str, stringlen: &mut i32, unmetalenin: &mut i32) {
3035 // c:2080
3036 // c:2082-2091 — `if (*stringlen > 0 && **string == Nularg)` — skip
3037 // the leading Nularg sentinel and adjust lengths.
3038 let bytes = string.as_bytes();
3039 if *stringlen > 0 && !bytes.is_empty() && bytes[0] as char == Nularg {
3040 // c:2085 — `(*string)++;` — advance past Nularg.
3041 *string = &string[1..]; // c:2085
3042 // c:2090 — `if (*unmetalenin > 0) (*unmetalenin)--;`
3043 if *unmetalenin > 0 {
3044 // c:2089
3045 *unmetalenin -= 1; // c:2090
3046 }
3047 // c:2092 — `if (*stringlen > 0) (*stringlen)--;`
3048 if *stringlen > 0 {
3049 // c:2091
3050 *stringlen -= 1; // c:2092
3051 }
3052 }
3053
3054 // c:2096-2097 — `if (*stringlen < 0) *stringlen = strlen(*string);`
3055 if *stringlen < 0 {
3056 // c:2096
3057 *stringlen = string.len() as i32; // c:2097
3058 }
3059}
3060
3061/// Port of `pattry(Patprog prog, char *string)` from `Src/pattern.c:2223`.
3062///
3063/// C signature: `int pattry(Patprog prog, char *string)`. Returns
3064/// non-zero on match, 0 on no-match.
3065pub fn pattry(prog: &Patprog, string: &str) -> bool {
3066 // c:2223
3067 // c:2225 — `return pattrylen(prog, string, len, -1, NULL, 0);`
3068 pattrylen(prog, string, string.len() as i32, -1, None, 0) // c:2225
3069}
3070
3071/// Port of `int pattrylen(Patprog prog, char *string, int len,
3072/// int unmetalen, Patstralloc patstralloc, int offset)` from
3073/// `Src/pattern.c:2236`.
3074///
3075/// ```c
3076/// int
3077/// pattrylen(Patprog prog, char *string, int len, int unmetalen,
3078/// Patstralloc patstralloc, int offset)
3079/// {
3080/// return pattryrefs(prog, string, len, unmetalen, patstralloc, offset,
3081/// NULL, NULL, NULL);
3082/// }
3083/// ```
3084pub fn pattrylen(
3085 prog: &Patprog,
3086 string: &str,
3087 len: i32, // c:2236
3088 unmetalen: i32,
3089 patstralloc: Option<&Patstralloc>,
3090 offset: i32,
3091) -> bool {
3092 // c:2238
3093 pattryrefs(
3094 prog,
3095 string,
3096 len,
3097 unmetalen,
3098 patstralloc,
3099 offset,
3100 None,
3101 None,
3102 None, // c:2239
3103 )
3104}
3105
3106/// Port of `int pattryrefs(Patprog prog, char *string, int stringlen,
3107/// int unmetalenin, Patstralloc patstralloc, int patoffset,
3108/// int *nump, int *begp, int *endp)` from `Src/pattern.c:2294`.
3109/// Runs `prog` against `string[0..stringlen]` (or whole string when
3110/// stringlen=-1) at `patoffset`, returning capture-group ranges.
3111///
3112/// C signature preserved per Rule S1. `Patstralloc` (the metafied-
3113/// string-allocator carrier from zsh.h:1613) is threaded through as
3114/// `Option<&Patstralloc>` matching C's `NULL`-passable pointer; the
3115/// matcher body currently ignores its contents (the substrate that
3116/// uses pre-allocated metafied buffers isn't wired into the Rust
3117/// matcher yet), but the param shape matches C so call sites trace
3118/// 1:1 to upstream.
3119#[allow(clippy::too_many_arguments)]
3120pub fn pattryrefs(
3121 // c:2294
3122 prog: &Patprog,
3123 string: &str,
3124 stringlen: i32,
3125 _unmetalenin: i32,
3126 _patstralloc: Option<&Patstralloc>,
3127 patoffset: i32,
3128 nump: Option<&mut i32>,
3129 begp: Option<&mut Vec<i32>>,
3130 endp: Option<&mut Vec<i32>>,
3131) -> bool {
3132 let trial: &str = if stringlen < 0 || (stringlen as usize) >= string.len() {
3133 string
3134 } else {
3135 &string[..stringlen as usize]
3136 };
3137 // Substring fast path for the ubiquitous `*literal*` shape
3138 // (optionally `(#i)`): program is [P_GFLAGS] P_STAR P_EXACTLY
3139 // P_STAR P_END with no captures requested. history-search-multi-
3140 // word's `${history[(R)(#i)*pat*]}` runs this shape over EVERY
3141 // history entry — 566k backtracking patmatch walks took CPU-
3142 // minutes per ^R (the reported freeze); a memmem-style scan is
3143 // what the shape actually needs. C reads this spirit via its
3144 // `mustoff` must-match prefilter (Src/pattern.c:2460-2483) but
3145 // declines under globflags; here the exact-bytes variant covers
3146 // any content and the `(#i)` variant is taken only when pattern
3147 // AND subject are pure ASCII (byte-fold == the matcher's char
3148 // fold there); anything else falls through to the full matcher.
3149 if nump.is_none()
3150 && begp.is_none()
3151 && endp.is_none()
3152 && patoffset == 0
3153 && (prog.0.flags & (PAT_NOTSTART | PAT_NOTEND) as i32) == 0
3154 // c:2399-2406 — a successful match is REJECTED for a file pattern
3155 // (PAT_NOGLD) whose subject starts with '.', unless the pattern itself
3156 // starts with a literal dot. That rejection lives below, AFTER
3157 // patmatch; returning from the fast path skipped it entirely, so
3158 // `*.*` matched `.hidden` (the literal `.` was found at offset 0 by
3159 // the substring scan) where zsh lists no dot files at all. Hand any
3160 // dot-leading subject under a file pattern to the full matcher, which
3161 // applies the rule. Costs nothing where the fast path was aimed:
3162 // history search (`${history[(R)(#i)*pat*]}`) is not a file glob and
3163 // never sets PAT_NOGLD, and non-dot files still take the fast path.
3164 && !((prog.0.flags & PAT_NOGLD as i32) != 0 && trial.starts_with('.'))
3165 {
3166 // Shape probe, inline (build-gate: no new fn in ported/ without
3167 // a C counterpart): Some((literal, igncase)) iff the program is
3168 // exactly `[P_GFLAGS] P_STAR P_EXACTLY(lit) P_STAR P_END` with
3169 // glob flags ⊆ IGNCASE|MULTIBYTE (no approx budget, no backrefs).
3170 let shape: Option<(String, bool)> = (|| {
3171 let buf: &[u8] = &prog.1;
3172 let mut off = prog.0.startoff as usize;
3173 let mut gflags = prog.0.globflags;
3174 let op_at = |o: usize| -> Option<u8> {
3175 if o + I_BODY <= buf.len() {
3176 Some(buf[o + I_OP])
3177 } else {
3178 None
3179 }
3180 };
3181 // A sole leading BRANCH (next == 0: no alternative) wraps
3182 // the whole program — step inside it.
3183 if op_at(off)? == P_BRANCH {
3184 let next =
3185 u32::from_le_bytes(buf[off + I_NEXT..off + I_NEXT + 4].try_into().ok()?);
3186 if next != 0 {
3187 return None; // real alternation — full matcher
3188 }
3189 off = advance_past_instr(buf, off);
3190 }
3191 if op_at(off)? == P_GFLAGS {
3192 let body = off + I_BODY;
3193 if body + 4 > buf.len() {
3194 return None;
3195 }
3196 gflags |= i32::from_le_bytes(buf[body..body + 4].try_into().ok()?);
3197 off = advance_past_instr(buf, off);
3198 }
3199 if (gflags & !(GF_IGNCASE | GF_MULTIBYTE)) != 0 {
3200 return None;
3201 }
3202 if op_at(off)? != P_STAR {
3203 return None;
3204 }
3205 off = advance_past_instr(buf, off);
3206 // One or more contiguous EXACTLY chunks form the literal
3207 // (the compiler splits long/space-bearing literals).
3208 if op_at(off)? != P_EXACTLY {
3209 return None;
3210 }
3211 let mut lit = String::new();
3212 while op_at(off)? == P_EXACTLY {
3213 let body = off + I_BODY;
3214 if body + 4 > buf.len() {
3215 return None;
3216 }
3217 let len = u32::from_le_bytes(buf[body..body + 4].try_into().ok()?) as usize;
3218 if body + 4 + len > buf.len() {
3219 return None;
3220 }
3221 lit.push_str(std::str::from_utf8(&buf[body + 4..body + 4 + len]).ok()?);
3222 off = advance_past_instr(buf, off);
3223 }
3224 if op_at(off)? != P_STAR {
3225 return None;
3226 }
3227 off = advance_past_instr(buf, off);
3228 if off + I_OP >= buf.len() || buf[off + I_OP] != P_END {
3229 return None;
3230 }
3231 Some((lit, (gflags & GF_IGNCASE) != 0))
3232 })();
3233 if let Some((lit, igncase)) = shape.as_ref().map(|(l, i)| (l.as_str(), *i)) {
3234 if !igncase {
3235 return trial.contains(lit);
3236 }
3237 if lit.is_ascii() && trial.is_ascii() {
3238 let lb = lit.as_bytes();
3239 let tb = trial.as_bytes();
3240 if lb.is_empty() {
3241 return true;
3242 }
3243 if lb.len() <= tb.len() {
3244 let l0 = lb[0].to_ascii_lowercase();
3245 'scan: for s in 0..=(tb.len() - lb.len()) {
3246 if tb[s].to_ascii_lowercase() != l0 {
3247 continue;
3248 }
3249 for k in 1..lb.len() {
3250 if tb[s + k].to_ascii_lowercase() != lb[k].to_ascii_lowercase() {
3251 continue 'scan;
3252 }
3253 }
3254 return true;
3255 }
3256 }
3257 return false;
3258 }
3259 // Non-ASCII under (#i): general matcher below.
3260 }
3261 }
3262 let mut state = rpat::new();
3263 // c:Src/pattern.c:2334 — `patflags = prog->flags;` C copies the
3264 // prog's flags into the file-static `patflags` so subsequent
3265 // patmatch calls can read PAT_NOTSTART / PAT_NOTEND inside the
3266 // P_ISSTART / P_ISEND arms (c:3393, 3397). Rust mirrors this via
3267 // the `patflags` AtomicI32 at pattern.rs:217.
3268 //
3269 // Without this propagation, `${str:#(#s)PAT}` / `${str:#PAT(#e)}`
3270 // anchor checks fired at slice-local offset 0 / slice-local end
3271 // regardless of whether the caller meant the slice was an
3272 // interior chunk of a larger string (PAT_NOTSTART/PAT_NOTEND
3273 // bits exist on the prog precisely so callers can signal this).
3274 patflags.store(prog.0.flags, Ordering::Relaxed);
3275 // Pass the prog's GLOB flags (GF_* + `(#aN)` budget byte) to the
3276 // matcher. C threads `patglobflags` as a per-thread file-static;
3277 // the Rust port carries it through a fn param. The PAT_* flags
3278 // (PAT_STATIC etc.) stay on `prog.0.flags` for the outer
3279 // anchor/PURES checks at lines below.
3280 // c:Src/pattern.c — `if (prog->flags & PAT_ANY) { ret = 1; } else { ... }`.
3281 // Optimisation for a single "*" (the `**` complist node compiles its
3282 // section as `patcompile(NULL, …|PAT_ANY, …)`): it always matches the
3283 // whole string without running the bytecode. The PAT_NOGLD leading-dot
3284 // rejection below still applies (the "except for no_glob_dots" caveat).
3285 // Without this the empty `PAT_ANY` program was matched literally and
3286 // only matched the empty string, so `**` descended into nothing.
3287 let (mut ok, matched_end) = if (prog.0.flags & PAT_ANY as i32) != 0 {
3288 (true, trial.len())
3289 } else {
3290 match patmatch(&prog.1, 0, trial, 0, &mut state, prog.0.globflags) {
3291 Some(end_pos) => {
3292 // c:2438 — `if (matched && !(prog->flags & (PAT_NOANCH|PAT_NOTEND))) ...`
3293 let no_anchor = (prog.0.flags & (PAT_NOANCH | PAT_NOTEND) as i32) != 0;
3294 (no_anchor || end_pos == trial.len(), end_pos)
3295 }
3296 None => (false, 0),
3297 }
3298 };
3299 // c:2399-2406 — for files (PAT_NOGLD), a successful match is rejected
3300 // when the string starts with '.' (unless glob_dots is set, in which
3301 // case parsecomplist omits PAT_NOGLD). Honoring this here lets the
3302 // glob scanner rely on the matcher for the leading-dot skip, exactly
3303 // as C does, instead of an explicit check in the walker. Inert for
3304 // non-file patterns (matchpat / [[ ]] / :# never set PAT_NOGLD).
3305 if ok
3306 && (prog.0.flags & PAT_NOGLD as i32) != 0
3307 && trial.starts_with('.')
3308 && prog.0.patstartch != b'.'
3309 {
3310 // c:Src/pattern.c — NOGLD rejects a leading-dot file UNLESS the
3311 // pattern itself explicitly begins with a literal `.` (so `.*`
3312 // matches dot files while `*` does not). C bakes this into the
3313 // compiled bytecode; the Rust matcher carries the signal on
3314 // `patstartch` (c:1610) and applies the exception here.
3315 ok = false;
3316 }
3317 if ok {
3318 // c:2508 — `patinlen = patinput - patinstart;` — record the
3319 // byte-length of the successful match so `patmatchlen()` can
3320 // return it after the strings/state are torn down. `end_pos`
3321 // is the in-trial byte offset where patmatch stopped, which
3322 // is `patinput - patinstart` in C terms.
3323 patinlen.store(matched_end as i32, Ordering::Relaxed);
3324 }
3325 if ok {
3326 let n = (prog.0.patnpar as usize).min(NSUBEXP);
3327 let have_nump = nump.is_some();
3328 if let Some(np) = nump {
3329 *np = n as i32;
3330 }
3331 // c:2526-2542 — GF_MATCHREF (`(#m)`): on success, write the
3332 // matched substring to $MATCH and its 1-based (KSHARRAYS-
3333 // aware) char span to $MBEGIN/$MEND. C does this INSIDE
3334 // pattryrefs; the previous Rust port left it to a bridge
3335 // wrapper that sniffed the pattern TEXT for "(#m)" — wrong
3336 // mechanism (missed hoisted/nested flag positions) and
3337 // wrong layer.
3338 // c:2526 — `if ((patglobflags & GF_MATCHREF) && !(patflags & PAT_FILE))`.
3339 // The source is the GLOBAL patglobflags (c:273), not `prog->globflags`.
3340 // They are not the same thing: c:Src/zsh.h:1606 documents the struct
3341 // field as "globbing flags to set at START", so it keeps whatever the
3342 // pattern opened with, while the global is what patgetglobflags leaves
3343 // after walking every flag — including a later `(#M)` turning
3344 // GF_MATCHREF back off (c:1099-1100).
3345 //
3346 // Reading the struct field made `(#M)` inert: for all three of
3347 // `(#m)a*`, `(#m)(#M)a*` and `(#M)(#m)a*` it is 0x1800, whereas the
3348 // global correctly ends at 0x800 / 0x0 / 0x800. So `(#m)(#M)a*` still
3349 // wrote $MATCH where zsh leaves it unset.
3350 let cur_globflags = patglobflags.load(Ordering::Relaxed);
3351 if (cur_globflags & GF_MATCHREF) != 0 && (prog.0.flags & PAT_FILE as i32) == 0 {
3352 let hi = matched_end.min(trial.len());
3353 let mstr: String = trial[..hi].to_string(); // c:2536 metafy(patinstart..patinput)
3354 let mlen = mstr.chars().count() as i64; // c:2534 CHARSUB
3355 let base: i64 = if crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHARRAYS) {
3356 0
3357 } else {
3358 1
3359 };
3360 crate::ported::params::setsparam("MATCH", &mstr); // c:2537
3361 crate::ported::params::setiparam("MBEGIN", patoffset as i64 + base); // c:2538
3362 crate::ported::params::setiparam("MEND", mlen + patoffset as i64 + base - 1);
3363 // c:2539-2541
3364 }
3365 // c:2425+ — emit captured offsets to begp/endp out-arrays.
3366 // Check the CLOSE bit (high stripe at NSUBEXP+i) since the
3367 // group is only fully captured after its P_CLOSE fired; the
3368 // matching open-bit `1 << i` would be set after P_OPEN even
3369 // when the rest of the match later fails to reach P_CLOSE.
3370 //
3371 // c:Src/pattern.c:2556-2558 — `*begp++ = CHARSUB(patinstart, *sp) + patoffset;`.
3372 // `patoffset` is the start position of `string` within the
3373 // ORIGINAL string the caller was matching against (used for
3374 // paramsubst's sliding-window scan via `${var/PAT/REPL}`). Add
3375 // it to every reported beg/end so MBEGIN/MEND / `$match` see
3376 // positions relative to the original string, not the local
3377 // trial slice.
3378 // c:2556-2566 — `*begp++ = …` writes into the caller's FIXED-SIZE array
3379 // (C: `int begpos[MAX_POS]`), filling the first `n` slots and leaving
3380 // the array's length unchanged. Write IN PLACE (extend only if the
3381 // caller's Vec is shorter than `n`) rather than clear()+push — clearing
3382 // shrank a MAX_POS-length array down to `n`, so the caller's
3383 // [n..MAX_POS] reset loop (complist.rs getcol, c:631) indexed past the
3384 // end and panicked (`index 4 on len 4`) on patterns with < MAX_POS
3385 // backrefs.
3386 if let Some(bv) = begp {
3387 for i in 0..n {
3388 let close_bit = 1u32 << (i + NSUBEXP);
3389 let val = if (state.captures_set & close_bit) != 0 {
3390 state.patbeginp[i] as i32 + patoffset
3391 } else {
3392 -1 // c:2563-2566 — unset group (unmatched alternation branch)
3393 };
3394 if i < bv.len() {
3395 bv[i] = val;
3396 } else {
3397 bv.push(val);
3398 }
3399 }
3400 }
3401 if let Some(ev) = endp {
3402 for i in 0..n {
3403 let close_bit = 1u32 << (i + NSUBEXP);
3404 let val = if (state.captures_set & close_bit) != 0 {
3405 state.patendp[i] as i32 + patoffset
3406 } else {
3407 -1 // c:2565
3408 };
3409 if i < ev.len() {
3410 ev[i] = val;
3411 } else {
3412 ev.push(val);
3413 }
3414 }
3415 }
3416 // c:2570-2621 — `else if (prog->patnpar && !(patflags &
3417 // PAT_FILE))`: the caller passed NO capture arrays, so
3418 // pattryrefs itself publishes the `(#b)` groups as the
3419 // $match / $mbegin / $mend arrays. This arm was missing
3420 // from the port — every plain pattry of a (#b) pattern
3421 // silently dropped its captures (the bridge compensated
3422 // with a wrapper; now C-faithful at the source).
3423 if !have_nump && n > 0 && (prog.0.flags & PAT_FILE as i32) == 0 {
3424 let base: i32 = if crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHARRAYS) {
3425 0
3426 } else {
3427 1
3428 };
3429 let mut match_arr: Vec<String> = Vec::with_capacity(n);
3430 let mut begin_arr: Vec<String> = Vec::with_capacity(n);
3431 let mut end_arr: Vec<String> = Vec::with_capacity(n);
3432 for i in 0..n {
3433 let close_bit = 1u32 << (i + NSUBEXP);
3434 if (state.captures_set & close_bit) != 0 {
3435 let b = state.patbeginp[i];
3436 let e = state.patendp[i];
3437 let lo = b.min(trial.len());
3438 let hi = e.min(trial.len()).max(lo);
3439 match_arr.push(trial[lo..hi].to_string()); // c:2587 metafy(*sp..*ep)
3440 begin_arr.push((b as i32 + patoffset + base).to_string()); // c:2596-2599
3441 // c:2601-2604 — mend = last matched char index
3442 // (inclusive): end + offset + base - 1.
3443 end_arr.push((e as i32 + patoffset + base - 1).to_string());
3444 } else {
3445 // c:2607-2613 — unmatched branch / hashed paren.
3446 match_arr.push(String::new());
3447 begin_arr.push("-1".to_string());
3448 end_arr.push("-1".to_string());
3449 }
3450 }
3451 crate::ported::params::setaparam("match", match_arr); // c:2619
3452 crate::ported::params::setaparam("mbegin", begin_arr); // c:2620
3453 crate::ported::params::setaparam("mend", end_arr); // c:2621
3454 }
3455 }
3456 ok
3457}
3458
3459// =====================================================================
3460// 15. Range matching — pattern.c:3856, :4004, :3610, :3767
3461// =====================================================================
3462
3463/// Direct port of `int patmatchlen(void)` from `Src/pattern.c:2649`.
3464///
3465/// ```c
3466/// /**/
3467/// int
3468/// patmatchlen(void)
3469/// {
3470/// return patinlen;
3471/// }
3472/// ```
3473///
3474/// Returns the length in metafied bytes of the last successful
3475/// `pattry` / `pattryrefs` match. `patinlen` is set at
3476/// `pattern.c:2508` (`patinlen = patinput - patinstart`); the
3477/// Rust port sets the equivalent `patinlen` AtomicI32 at the end
3478/// of `pattryrefs` (see `pattern.rs::pattryrefs`).
3479pub fn patmatchlen() -> i32 {
3480 // c:2649-2652
3481 patinlen.load(Ordering::Relaxed) // c:2651 — `return patinlen;`
3482}
3483/// Direct port of `int patmatchindex(char *range, int ind, int *chr, int *mtp)`
3484/// from `Src/pattern.c:4004` (MULTIBYTE_SUPPORT-disabled single-byte
3485/// variant). Walks a NULL-terminated, METAFIED, PP_*-encoded byte
3486/// stream `range` and returns the character (or POSIX-class id)
3487/// at index `ind`. Output:
3488/// - `Some((Some(ch), mtp))` — literal character or PP_RANGE hit;
3489/// `chr = ch`, `mtp = 0`.
3490/// - `Some((None, mtp))` — POSIX class match (PP_ALPHA etc);
3491/// `chr = -1` in C, `None` in Rust; `mtp = class id`.
3492/// - `None` — `ind` exceeds the descriptor's length.
3493///
3494/// Encoding the byte stream uses:
3495/// * literal byte `< 0x83` = match char as itself
3496/// * `Meta(0x83)` + (byte ^ 0x20) = ordinary metafied character
3497/// * `Meta + PP_*` (0x84..) = POSIX class marker
3498/// * `Meta + PP_RANGE` = next two metafied chars are `lo, hi`
3499pub fn patmatchindex(range: &[u8], mut ind: i32) -> Option<(Option<u8>, i32)> {
3500 // c:4004-4081
3501 let mut chr: Option<u8> = None; // c:4014 — `*chr = -1`
3502 let mut mtp: i32 = 0; // c:4015 — `*mtp = 0`
3503
3504 // C `UNMETA(range)` + `METACHARINC(range)` macro pair from
3505 // Src/zsh.h:1796-1797 — decode-and-advance one metafied
3506 // character. Returned as `(decoded_byte, byte_advance)`.
3507 let unmeta = |bytes: &[u8], i: usize| -> (u8, usize) {
3508 if i < bytes.len() && bytes[i] == Meta && i + 1 < bytes.len() {
3509 (bytes[i + 1] ^ 0x20, 2)
3510 } else if i < bytes.len() {
3511 (bytes[i], 1)
3512 } else {
3513 (0, 0)
3514 }
3515 };
3516
3517 let mut i = 0usize;
3518 while i < range.len() {
3519 // c:4017 — `for (; *range; range++)`
3520 let b = range[i];
3521 if crate::ported::utils::imeta_byte(b) {
3522 // c:4018 — `if (imeta((unsigned char) *range))`
3523 // c:4019 — `int swtype = (unsigned char) *range - (unsigned char) Meta;`
3524 let swtype = (b as i32) - (Meta as i32);
3525 match swtype {
3526 0 => {
3527 // c:4021-4028 — `case 0: ordinary metafied character`
3528 // c:4023 — `rchr = (unsigned char) *++range ^ 32;`
3529 i += 1;
3530 if i >= range.len() {
3531 break;
3532 }
3533 let rchr = range[i] ^ 0x20;
3534 if ind == 0 {
3535 // c:4024-4026 — `if (!ind) { *chr = rchr; return 1; }`
3536 chr = Some(rchr);
3537 return Some((chr, mtp));
3538 }
3539 // c:4027 — falls through to `if (!ind--) break;`
3540 }
3541 t if (PP_ALPHA..=PP_INVALID).contains(&t) => {
3542 // c:4030-4051 — POSIX class markers PP_ALPHA..PP_INVALID
3543 if ind == 0 {
3544 // c:4052-4054 — `if (!ind) { *mtp = swtype; return 1; }`
3545 mtp = swtype;
3546 return Some((None, mtp));
3547 }
3548 }
3549 t if t == PP_RANGE => {
3550 // c:4057-4069 — PP_RANGE: next two metafied chars
3551 // `range++; r1 = UNMETA(range); METACHARINC(range);
3552 // r2 = UNMETA(range); if (*range == Meta) range++;`
3553 i += 1;
3554 if i >= range.len() {
3555 break;
3556 }
3557 let (r1, adv1) = unmeta(range, i);
3558 i += adv1;
3559 if i >= range.len() {
3560 break;
3561 }
3562 let (r2, adv2) = unmeta(range, i);
3563 i += adv2;
3564 let rdiff = r2 as i32 - r1 as i32;
3565 if rdiff >= ind {
3566 // c:4063-4067 — `if (rdiff >= ind) { *chr = r1 + ind; return 1; }`
3567 chr = Some((r1 as i32 + ind) as u8);
3568 return Some((chr, mtp));
3569 }
3570 // c:4068 — `ind -= rdiff;` (extra decrement happens via
3571 // the `ind--` below, accounting for the C comment
3572 // "note the extra decrement to ind below").
3573 ind -= rdiff;
3574 // Skip the trailing `if (!ind--) break;` decrement
3575 // for this branch since C's loop already does it.
3576 }
3577 _ => {
3578 // c:4070-4076 — PP_UNKWN / default → DPUTS bug warn
3579 // (unreachable in well-formed compiled output; bail).
3580 }
3581 }
3582 } else {
3583 // c:4079-4082 — literal char path
3584 if ind == 0 {
3585 // c:4080-4082 — `if (!ind) { *chr = (unsigned char) *range; return 1; }`
3586 chr = Some(b);
3587 return Some((chr, mtp));
3588 }
3589 }
3590 // c:4087 — `if (!ind--) break;` — pre-decrement-check.
3591 if ind == 0 {
3592 break;
3593 }
3594 ind -= 1;
3595 i += 1; // c:4017 — `range++`
3596 }
3597 // c:4091 — `/* No corresponding index. */ return 0;`
3598 None
3599}
3600
3601/// Direct port of `int mb_patmatchrange(char *range, wchar_t ch,
3602/// int zmb_ind, wint_t *indptr, int *mtp)` from `Src/pattern.c:3610`.
3603/// Multibyte variant of `patmatchrange`: walks the metafied,
3604/// PP_*-encoded byte stream `range` and tests whether wide char `ch`
3605/// is in it. `indptr` (when Some) accumulates the running index for
3606/// `mb_patmatchindex`-side lookup; `mtp` (when Some) records which
3607/// PP_* class fired.
3608///
3609/// `zmb_ind` is the `ZMB_*` multibyte-completion state from the
3610/// caller (typically the pattry input pos): ZMB_INCOMPLETE /
3611/// ZMB_INVALID feed the PP_INCOMPLETE / PP_INVALID branches.
3612///
3613/// The Rust port delegates `iswalpha` / `iswdigit` / `wcsitype` to
3614/// `is_alphabetic()` / `is_numeric()` / etc. on the decoded `char`,
3615/// matching the C `iswXXX(wchar_t)` semantics.
3616pub fn mb_patmatchrange(
3617 range: &[u8],
3618 ch: char,
3619 zmb_ind: i32,
3620 mut indptr: Option<&mut u32>,
3621 mut mtp: Option<&mut i32>,
3622) -> bool {
3623 // c:3610-3766
3624 if let Some(p) = indptr.as_deref_mut() {
3625 *p = 0; // c:3615-3616 — `if (indptr) *indptr = 0;`
3626 }
3627 // C `UNMETA(s)` + `METACHARINC(s)` macro pair (Src/zsh.h).
3628 let unmeta = |bytes: &[u8], i: usize| -> (u8, usize) {
3629 if i < bytes.len() && bytes[i] == Meta && i + 1 < bytes.len() {
3630 (bytes[i + 1] ^ 0x20, 2)
3631 } else if i < bytes.len() {
3632 (bytes[i], 1)
3633 } else {
3634 (0, 0)
3635 }
3636 };
3637 let mut i = 0usize;
3638 while i < range.len() {
3639 // c:3623 — `while (*range)`
3640 let b = range[i];
3641 if crate::ported::utils::imeta_byte(b) {
3642 // c:3624 — `if (imeta((unsigned char) *range))`
3643 // c:3625 — `swtype = (unsigned char) *range++ - (unsigned char) Meta;`
3644 let swtype = (b as i32) - (Meta as i32);
3645 i += 1;
3646 if let Some(p) = mtp.as_deref_mut() {
3647 *p = swtype; // c:3626-3627 — `if (mtp) *mtp = swtype;`
3648 }
3649 let class_hit = match swtype {
3650 0 => {
3651 // c:3629-3634 — `case 0: ordinary metafied character`
3652 i -= 1; // c:3631 — `range--;`
3653 let (decoded, adv) = unmeta(range, i);
3654 i += adv;
3655 decoded == (ch as u32) as u8 && (ch as u32) < 256
3656 }
3657 t if t == PP_ALPHA => ch.is_alphabetic(),
3658 t if t == PP_ALNUM => ch.is_alphanumeric(),
3659 t if t == PP_ASCII => (ch as u32) < 128,
3660 t if t == PP_BLANK => ch == ' ' || ch == '\t',
3661 t if t == PP_CNTRL => ch.is_control(),
3662 t if t == PP_DIGIT => ch.is_ascii_digit(),
3663 t if t == PP_GRAPH => !ch.is_whitespace() && !ch.is_control(),
3664 t if t == PP_LOWER => ch.is_lowercase(),
3665 t if t == PP_PRINT => !ch.is_control(),
3666 t if t == PP_PUNCT => ch.is_ascii_punctuation(),
3667 t if t == PP_SPACE => ch.is_whitespace(),
3668 t if t == PP_UPPER => ch.is_uppercase(),
3669 t if t == PP_XDIGIT => ch.is_ascii_hexdigit(),
3670 t if t == PP_IDENT => {
3671 // c:3704 — `PP_IDENT: if (wcsitype(ch, IIDENT)) return 1;`
3672 // IIDENT = alphanumeric, underscore, or "$" depending on
3673 // option state. Conservative: ASCII identifier chars.
3674 ch.is_alphanumeric() || ch == '_'
3675 }
3676 t if t == PP_IFS => {
3677 // c:3708 — `wcsitype(ch, ISEP)`. ISEP = space, tab,
3678 // newline + user IFS additions. Default IFS in zsh
3679 // is ` \t\n`.
3680 ch == ' ' || ch == '\t' || ch == '\n' || ch == '\0'
3681 }
3682 t if t == PP_IFSSPACE => {
3683 // c:3712-3713 — ASCII-only IFS-space.
3684 (ch as u32) < 128 && (ch == ' ' || ch == '\t' || ch == '\n')
3685 }
3686 t if t == PP_WORD => {
3687 // c:3716 — `wcsitype(ch, IWORD)`. IWORD = WORDCHARS-derived;
3688 // conservative ASCII alphanumeric + `_`.
3689 ch.is_alphanumeric() || ch == '_'
3690 }
3691 t if t == PP_RANGE => {
3692 // c:3719-3735 — PP_RANGE: two metafied wide chars are
3693 // `r1` and `r2`; matches if `r1 <= ch <= r2`.
3694 let (r1_byte, adv1) = unmeta(range, i);
3695 i += adv1;
3696 let (r2_byte, adv2) = unmeta(range, i);
3697 i += adv2;
3698 let r1 = r1_byte as u32;
3699 let r2 = r2_byte as u32;
3700 let chu = ch as u32;
3701 if r1 <= chu && chu <= r2 {
3702 if let Some(p) = indptr.as_deref_mut() {
3703 *p += chu - r1; // c:3725-3726
3704 }
3705 return true; // c:3727
3706 }
3707 if let Some(p) = indptr.as_deref_mut() {
3708 if r1 < r2 {
3709 *p += r2 - r1; // c:3733-3734
3710 }
3711 }
3712 false
3713 }
3714 t if t == PP_INCOMPLETE => {
3715 // c:3740 — `if (zmb_ind == ZMB_INCOMPLETE) return 1;`
3716 zmb_ind == ZMB_INCOMPLETE
3717 }
3718 t if t == PP_INVALID => {
3719 // c:3744 — `if (zmb_ind == ZMB_INVALID) return 1;`
3720 zmb_ind == ZMB_INVALID
3721 }
3722 _ => {
3723 // c:3747-3753 — PP_UNKWN / default → DPUTS bug warn
3724 false
3725 }
3726 };
3727 if class_hit {
3728 return true;
3729 }
3730 } else {
3731 // c:3757-3762 — literal-char path
3732 let (decoded, adv) = unmeta(range, i);
3733 i += adv;
3734 if decoded == (ch as u32) as u8 && (ch as u32) < 256 {
3735 if let Some(p) = mtp.as_deref_mut() {
3736 *p = 0; // c:3760
3737 }
3738 return true; // c:3761
3739 }
3740 }
3741 if let Some(p) = indptr.as_deref_mut() {
3742 *p += 1; // c:3764-3765 — `if (indptr) (*indptr)++;`
3743 }
3744 }
3745 false // c:3766 — `return 0;`
3746}
3747
3748/// Direct port of `int mb_patmatchindex(char *range, wint_t ind,
3749/// wint_t *chr, int *mtp)` from `Src/pattern.c:3767`. The reverse
3750/// of `mb_patmatchrange`: given a metafied byte range and an index
3751/// `ind` into it, return the character (or PP_* class) at that
3752/// position.
3753///
3754/// Returns:
3755/// - `Some((Some(ch), 0))` — literal or PP_RANGE hit (chr = ch).
3756/// - `Some((None, mtp))` — POSIX class match; chr = WEOF / None.
3757/// - `None` — index out of range.
3758pub fn mb_patmatchindex(range: &[u8], mut ind: u32) -> Option<(Option<char>, i32)> {
3759 // c:3767-3849
3760 let mut chr: Option<char> = None; // c:3776 — `*chr = WEOF`
3761 let mut mtp: i32 = 0; // c:3777 — `*mtp = 0`
3762
3763 // C `UNMETA(s)` + `METACHARINC(s)` macro pair (Src/zsh.h).
3764 let unmeta = |bytes: &[u8], i: usize| -> (u8, usize) {
3765 if i < bytes.len() && bytes[i] == Meta && i + 1 < bytes.len() {
3766 (bytes[i + 1] ^ 0x20, 2)
3767 } else if i < bytes.len() {
3768 (bytes[i], 1)
3769 } else {
3770 (0, 0)
3771 }
3772 };
3773
3774 let mut i = 0usize;
3775 while i < range.len() {
3776 // c:3779 — `while (*range)`
3777 let b = range[i];
3778 if crate::ported::utils::imeta_byte(b) {
3779 // c:3780 — `if (imeta((unsigned char) *range))`
3780 let swtype = (b as i32) - (Meta as i32);
3781 i += 1;
3782 match swtype {
3783 0 => {
3784 // c:3782-3789 — `case 0: ordinary metafied char`
3785 i -= 1;
3786 let (decoded, adv) = unmeta(range, i);
3787 i += adv;
3788 let rchr = decoded as char;
3789 if ind == 0 {
3790 chr = Some(rchr);
3791 return Some((chr, mtp));
3792 }
3793 }
3794 t if (PP_ALPHA..=PP_INVALID).contains(&t) => {
3795 // c:3791-3812 — POSIX class markers
3796 if ind == 0 {
3797 mtp = swtype;
3798 return Some((None, mtp));
3799 }
3800 }
3801 t if t == PP_RANGE => {
3802 // c:3814-3829 — PP_RANGE: two metafied wide chars
3803 let (r1_byte, adv1) = unmeta(range, i);
3804 i += adv1;
3805 let (r2_byte, adv2) = unmeta(range, i);
3806 i += adv2;
3807 let r1 = r1_byte as u32;
3808 let r2 = r2_byte as u32;
3809 let rdiff = r2.saturating_sub(r1);
3810 if rdiff >= ind {
3811 chr = char::from_u32(r1 + ind);
3812 return Some((chr, mtp));
3813 }
3814 ind = ind.saturating_sub(rdiff);
3815 }
3816 _ => {
3817 // c:3831-3837 — PP_UNKWN / default → DPUTS
3818 }
3819 }
3820 } else {
3821 // c:3840-3843 — literal byte path
3822 if ind == 0 {
3823 chr = Some(b as char);
3824 return Some((chr, mtp));
3825 }
3826 }
3827 // c:3846 — `if (!ind--) break;`
3828 if ind == 0 {
3829 break;
3830 }
3831 ind -= 1;
3832 i += 1;
3833 }
3834 None // c:3849 — `return 0`
3835}
3836
3837/// Port of `freepatprog(Patprog prog)` from `Src/pattern.c:4161`. Frees a Patprog.
3838/// Rust's `Drop` on `Box<patprog>` handles this; the explicit fn
3839/// exists for C parity (Rule A).
3840#[allow(unused_variables)]
3841pub fn freepatprog(prog: Patprog) {} // c:4161
3842
3843/// Port of `int pat_enables(const char *cmd, char **patp, int enable)`
3844/// from `Src/pattern.c:4171`. Implements `enable -p`/`disable -p`: with
3845/// an empty `patp`, prints the currently enabled (or disabled, if
3846/// `!enable`) tokens by walking `zpc_strings[]`/`zpc_disables[]` in
3847/// lockstep. Otherwise toggles each named token's `zpc_disables[i]`
3848/// slot, emitting `invalid pattern: NAME` for misses.
3849pub fn pat_enables(cmd: &str, patp: &[&str], enable: bool) -> i32 {
3850 // c:4171
3851 let mut ret: i32 = 0; // c:4173
3852 if patp.is_empty() {
3853 // c:4177 !*patp
3854 let strings = ZPC_STRINGS; // c:4179 zpc_strings
3855 let disp = zpc_disables.lock().unwrap(); // c:4179 zpc_disables
3856 let mut done = false; // c:4178
3857 let mut out: String = String::new();
3858 for i in 0..(ZPC_COUNT as usize) {
3859 // c:4180
3860 let sp = match strings[i] {
3861 // c:4182 !*stringp
3862 Some(s) => s,
3863 None => continue,
3864 };
3865 let is_disabled = disp[i] != 0;
3866 if enable == is_disabled {
3867 // c:4184 enable?*disp:!*disp
3868 continue;
3869 }
3870 if done {
3871 // c:4186
3872 out.push(' '); // c:4187
3873 }
3874 out.push_str(&format!("'{}'", sp)); // c:4188
3875 done = true; // c:4189
3876 }
3877 if done {
3878 // c:4191
3879 println!("{}", out); // c:4187-4192
3880 }
3881 return 0; // c:4193
3882 }
3883 for p in patp {
3884 // c:4196
3885 let strings = ZPC_STRINGS;
3886 let mut disp = zpc_disables.lock().unwrap();
3887 let mut matched = false;
3888 for i in 0..(ZPC_COUNT as usize) {
3889 // c:4197
3890 if let Some(s) = strings[i] {
3891 if s == *p {
3892 // c:4200 !strcmp
3893 disp[i] = if enable { 0u8 } else { 1u8 }; // c:4201 *disp = !enable
3894 matched = true;
3895 break; // c:4202
3896 }
3897 }
3898 }
3899 if !matched {
3900 // c:4205
3901 zerrnam(cmd, &format!("invalid pattern: {}", p)); // c:4206
3902 ret = 1; // c:4207
3903 }
3904 }
3905 ret // c:4211
3906}
3907
3908/// Port of `mod_export const char *zpc_strings[ZPC_COUNT]` from
3909/// `Src/pattern.c:258`. Static token-name table indexed by ZPC_*;
3910/// NULL entries (ZPC_NULL, ZPC_BNULLKEEP, ZPC_INPAR_PIPE,
3911/// ZPC_KSHCHAR) have no user-visible name.
3912pub const ZPC_STRINGS: [Option<&'static str>; ZPC_COUNT as usize] = [
3913 // c:258
3914 None,
3915 None,
3916 Some("|"),
3917 None,
3918 Some("~"),
3919 Some("("),
3920 Some("?"),
3921 Some("*"),
3922 Some("["),
3923 Some("<"),
3924 Some("^"),
3925 Some("#"),
3926 None,
3927 Some("?("),
3928 Some("*("),
3929 Some("+("),
3930 Some("!("),
3931 Some("\\!("),
3932 Some("@("),
3933];
3934
3935/// Port of `unsigned int savepatterndisables(void)` from
3936/// `Src/pattern.c:4220`.
3937///
3938/// C body (c:4220-4233):
3939/// ```c
3940/// unsigned int disables, bit;
3941/// char *disp;
3942/// disables = 0;
3943/// for (bit = 1, disp = zpc_disables;
3944/// disp < zpc_disables + ZPC_COUNT;
3945/// bit <<= 1, disp++) {
3946/// if (*disp) disables |= bit;
3947/// }
3948/// return disables;
3949/// ```
3950///
3951/// Encodes the current `zpc_disables\[ZPC_COUNT\]` byte-array as a u32
3952/// bitmask (one bit per slot, low bit = `zpc_disables\[0\]`).
3953/// The previous Rust port returned a `Vec<String>` clone of
3954/// `patterndisables` (a completely different data structure — names
3955/// list, not the per-token byte array). `restorepatterndisables(u32)`
3956/// at c:4258 reads this bitmask back into `zpc_disables`, so the
3957/// returned shape MUST be the u32 bitmask.
3958pub fn savepatterndisables() -> u32 {
3959 // c:4220
3960 let disp = zpc_disables.lock().unwrap(); // c:4225 disp = zpc_disables
3961 let mut disables: u32 = 0; // c:4224
3962 let mut bit: u32 = 1; // c:4226 bit = 1
3963 for i in 0..(ZPC_COUNT as usize) {
3964 // c:4226-4228
3965 if disp[i] != 0 {
3966 // c:4230
3967 disables |= bit; // c:4231
3968 }
3969 bit <<= 1; // c:4226 bit <<= 1
3970 }
3971 disables // c:4232
3972}
3973
3974/// Port of `void startpatternscope(void)` from `Src/pattern.c:4241`.
3975/// Pushes a frame onto `PATSCOPE_STACK` (`zpc_disables_stack` in C)
3976/// carrying the current `zpc_disables[]` state as a `savepatterndisables()`
3977/// u32 bitmap. Called at function entry; `endpatternscope` pops it.
3978///
3979/// ```c
3980/// void startpatternscope(void) {
3981/// Zpc_disables_save newdis = zalloc(sizeof(*newdis));
3982/// newdis->next = zpc_disables_stack;
3983/// newdis->disables = savepatterndisables(); // c:4247
3984/// zpc_disables_stack = newdis;
3985/// }
3986/// ```
3987pub fn startpatternscope() {
3988 // c:4241
3989 let saved = savepatterndisables(); // c:4247
3990 PATSCOPE_STACK.with(|s| s.borrow_mut().push(saved));
3991}
3992
3993/// Port of `void restorepatterndisables(unsigned int disables)` from
3994/// `Src/pattern.c:4258`. Walks the 12-slot `zpc_disables[]` array,
3995/// setting each slot's byte from the bitmask: `disables & (1<<i)`
3996/// → slot `i` gets 1, else 0.
3997/// ```c
3998/// void
3999/// restorepatterndisables(unsigned int disables)
4000/// {
4001/// char *disp;
4002/// unsigned int bit;
4003/// for (bit = 1, disp = zpc_disables;
4004/// disp < zpc_disables + ZPC_COUNT;
4005/// bit <<= 1, disp++) {
4006/// if (disables & bit) *disp = 1;
4007/// else *disp = 0;
4008/// }
4009/// }
4010/// ```
4011pub fn restorepatterndisables(disables: u32) {
4012 // c:4258
4013 let mut disp = zpc_disables.lock().unwrap(); // c:4263
4014 let mut bit: u32 = 1;
4015 for i in 0..(ZPC_COUNT as usize) {
4016 // c:4263-4265
4017 if (disables & bit) != 0 {
4018 // c:4266
4019 disp[i] = 1; // c:4267
4020 } else {
4021 disp[i] = 0; // c:4269
4022 }
4023 bit <<= 1;
4024 }
4025}
4026
4027/// Port of `void endpatternscope(void)` from `Src/pattern.c:4279`.
4028/// Pops the saved bitmap from `PATSCOPE_STACK` (`zpc_disables_stack`
4029/// in C); restores `zpc_disables[]` from the bitmap ONLY when
4030/// `isset(LOCALPATTERNS)` per C c:4286. Called at function exit.
4031///
4032/// ```c
4033/// void endpatternscope(void) {
4034/// Zpc_disables_save olddis = zpc_disables_stack;
4035/// zpc_disables_stack = olddis->next;
4036/// if (isset(LOCALPATTERNS))
4037/// restorepatterndisables(olddis->disables); // c:4287
4038/// zfree(olddis, sizeof(*olddis));
4039/// }
4040/// ```
4041pub fn endpatternscope() {
4042 // c:4279
4043 if let Some(prev) = PATSCOPE_STACK.with(|s| s.borrow_mut().pop()) {
4044 if isset(crate::ported::zsh_h::LOCALPATTERNS) {
4045 // c:4286-4287
4046 restorepatterndisables(prev);
4047 }
4048 }
4049}
4050
4051/// Port of `void clearpatterndisables(void)` from `Src/pattern.c:4296`.
4052/// C body: `memset(zpc_disables, 0, ZPC_COUNT)` — zero every slot.
4053pub fn clearpatterndisables() {
4054 // c:4296
4055 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize]; // c:4298
4056}
4057
4058/// Port of `haswilds(char *str)` from `Src/pattern.c:4306`.
4059///
4060/// Check whether `str` is eligible for filename generation.
4061///
4062/// C scans a TOKENIZED string: every input reaching it has been
4063/// through the lexer or `tokenize()`/`shtokenize()` (glob.c:3548/
4064/// 3563), so glob metachars are token codes and escaped/quoted
4065/// chars are Bnull'd literals. Callers holding un-tokenized strings
4066/// must `tokenize()` a copy first — exactly what C does for
4067/// runtime-built strings (compcore.c:2231 tokenizes fignore entries
4068/// immediately before its c:2235 haswilds call). The switch matches
4069/// ONLY token codes, never literal ASCII metachars.
4070///
4071/// The walk is over chars: zshrs strings hold token chars as
4072/// codepoints (Star = U+0087) and the input layer is char-domain
4073/// (input.rs `shingetline`/`ingetc`), so the char is the unit that
4074/// the metafied byte is in C. Scanning bytes here false-positived
4075/// on UTF-8 continuation bytes of plain text (`↔` = E2 86 94
4076/// carries 0x86/0x94 = Hat/Inang as u8) — bug #627.
4077///
4078/// The C source's `%?foo` job-ref special case (c:4317-4318), which
4079/// mutates `str[1]` in place to demote the `?`, becomes a "skip
4080/// position 1" scan adjustment here since `&str` is immutable.
4081pub fn haswilds(str: &str) -> bool {
4082 // c:4306
4083 let chars: Vec<char> = str.chars().collect();
4084 let len = chars.len();
4085 if len == 0 {
4086 return false;
4087 }
4088
4089 // c:4309 — `[' and `]' are legal even if bad patterns are usually not.
4090 if len == 1 && (chars[0] == Inbrack || chars[0] == Outbrack) {
4091 return false; // c:4311
4092 }
4093
4094 // c:4313-4315 — If % is immediately followed by ?, then that ? is
4095 // not treated as a wildcard. This is so you don't have
4096 // to escape job references such as %?foo.
4097 let skip_pos_1 = len >= 2 && chars[0] == '%' && chars[1] == Quest; // c:4316-4317
4098
4099 // c:4319-4321 — Note that at this point zpc_special has not been set up.
4100 let disp = zpc_disables.lock().unwrap(); // c:read zpc_disables[]
4101
4102 for i in 0..len {
4103 // c:4323 for (; *str; str++)
4104 if skip_pos_1 && i == 1 {
4105 continue; // c:4317 `str[1] = '?'` demote
4106 }
4107 let c = chars[i]; // c:4324 switch (*str)
4108 let prev: char = if i > 0 { chars[i - 1] } else { '\0' }; // c: str[-1]
4109
4110 if c == Inpar {
4111 // c:4325-4335
4112 if (!isset(SHGLOB) && disp[ZPC_INPAR as usize] == 0)
4113 || (i > 0
4114 && isset(KSHGLOB)
4115 && ((prev == Quest && disp[ZPC_KSH_QUEST as usize] == 0)
4116 || (prev == Star && disp[ZPC_KSH_STAR as usize] == 0)
4117 || (prev == '+' && disp[ZPC_KSH_PLUS as usize] == 0)
4118 || (prev == Bang && disp[ZPC_KSH_BANG as usize] == 0)
4119 || (prev == '!' && disp[ZPC_KSH_BANG2 as usize] == 0)
4120 || (prev == '@' && disp[ZPC_KSH_AT as usize] == 0)))
4121 {
4122 return true; // c:4335
4123 }
4124 } else if c == Bar {
4125 if disp[ZPC_BAR as usize] == 0 {
4126 return true; // c:4340
4127 }
4128 } else if c == Star {
4129 if disp[ZPC_STAR as usize] == 0 {
4130 return true; // c:4345
4131 }
4132 } else if c == Inbrack {
4133 if disp[ZPC_INBRACK as usize] == 0 {
4134 return true; // c:4350
4135 }
4136 } else if c == Inang {
4137 if disp[ZPC_INANG as usize] == 0 {
4138 return true; // c:4355
4139 }
4140 } else if c == Quest {
4141 if disp[ZPC_QUEST as usize] == 0 {
4142 return true; // c:4360
4143 }
4144 } else if c == Pound {
4145 if isset(EXTENDEDGLOB) && disp[ZPC_HASH as usize] == 0 {
4146 return true; // c:4365
4147 }
4148 } else if c == Hat {
4149 if isset(EXTENDEDGLOB) && disp[ZPC_HAT as usize] == 0 {
4150 return true; // c:4370
4151 }
4152 }
4153 }
4154 false // c:4374
4155}
4156
4157// =====================================================================
4158// 4. struct patprog — zsh.h:1601
4159// =====================================================================
4160
4161/// `typedef struct patprog *Patprog;` from `zsh.h:542`.
4162#[allow(non_camel_case_types)]
4163/// `typedef struct patprog *Patprog;` from `zsh.h:542`.
4164///
4165/// C zsh allocates the `struct patprog` header + bytecode as one
4166/// contiguous `malloc` block, accessing bytecode via
4167/// `(char *)prog + prog->startoff`. Rust has no flexible array
4168/// members, so this typedef pairs the C-exact `patprog` header
4169/// (zsh_h.rs:768) with an owned bytecode `Vec<u8>` — header at
4170/// `.0`, bytecode at `.1`. `startoff`/`size` index into `.1`.
4171pub type Patprog = Box<(patprog, Vec<u8>)>;
4172// =====================================================================
4173// Bytecode field offsets within each instruction.
4174//
4175// Instruction layout in `patout`:
4176// +0 u8 opcode
4177// +1..+5 u32 LE next_off (offset of next instr in chain, 0 = end)
4178// +5.. u8... opcode-specific payload
4179//
4180// C uses a different layout (Upat union = 4-byte or 8-byte slots);
4181// the Rust port pins the layout to byte offsets for portability.
4182// =====================================================================
4183
4184const I_OP: usize = 0; // opcode byte
4185
4186impl rpat {
4187 pub fn new() -> Self {
4188 Self {
4189 wbranch_visits: std::collections::HashMap::new(),
4190 patbeginp: [usize::MAX; NSUBEXP],
4191 patendp: [0; NSUBEXP],
4192 captures_set: 0,
4193 errsfound: 0, // c:2066 — errsfound = 0 at pattry init
4194 }
4195 }
4196}
4197const I_NEXT: usize = 1; // u32 next-offset starts here
4198const I_BODY: usize = 5; // payload starts here
4199
4200/// Serialises every entry into `patcompile`. The C source at
4201/// `Src/pattern.c:267-281` declares `patout`, `patparse`, `patstart`,
4202/// `patnpar`, `patflags`, `patglobflags`, `errsfound`, `forceerrs`,
4203/// `zpc_special`, `patstrcache` as file-scope statics that the compile
4204/// mutates in sequence; zsh-the-program is single-threaded so the C
4205/// source is safe under that invariant. zshrs callers (zutil's
4206/// `style_table::get` via `crate::ported::pattern::patmatch`, params.rs,
4207/// subst.rs, options.rs) can invoke `patcompile` from concurrent test
4208/// threads, so the lock restores the single-writer invariant. Held
4209/// only for the compile phase; the matcher (`pattry`/`patmatch`)
4210/// operates on the returned `Patprog.code` and touches no globals.
4211static PATCOMPILE_LOCK: Mutex<()> = Mutex::new(());
4212
4213// C: `static char *patparse, *patstart;` — pattern parsing cursors
4214// into the *input* pattern string. patstart points to start of
4215// pattern; patparse moves forward as we consume tokens.
4216pub static patparse: Mutex<String> = Mutex::new(String::new()); // c:269
4217pub static patstart: Mutex<String> = Mutex::new(String::new()); // c:269
4218
4219/// Position within `patparse` we're currently looking at. C source
4220/// uses a `char *` cursor; Rust uses byte offset into the String.
4221pub static patparse_off: AtomicUsize = AtomicUsize::new(0);
4222
4223// C: `static int errsfound;` — approximate-match error count.
4224pub static errsfound: AtomicI32 = AtomicI32::new(0); // c:274
4225
4226// C: `static int forceerrs;` — required error count for approximate match.
4227pub static forceerrs: AtomicI32 = AtomicI32::new(-1); // c:275
4228
4229// C: `static long patglobflags_orig;` — saved at branch entry.
4230pub static patglobflags_orig: AtomicI32 = AtomicI32::new(0); // c:276
4231
4232// C: `static const char *zpc_special;` — table of currently-special
4233// characters during compile (indexed by ZPC_*).
4234//
4235// pattern.c uses `static char zpc_special[ZPC_COUNT];` and resets it
4236// in patcompcharsset(). Rust mirrors as a Mutex-wrapped byte array.
4237pub static zpc_special: Mutex<[u8; ZPC_COUNT as usize]> = Mutex::new([0u8; ZPC_COUNT as usize]); // c:278
4238
4239// C: `static char *patstrcache;` — caches the unmetafied trial string.
4240// Rust port has no Meta encoding so the cache is unnecessary; we leave
4241// the static declared for parity (Rule A — name exists in C).
4242pub static patstrcache: Mutex<String> = Mutex::new(String::new()); // c:281
4243
4244/// `Marker` constant — alias for `crate::ported::zsh_h::Marker`
4245/// (`Src/zsh.h:224` `#define Marker ((char) 0xa2)`).
4246///
4247/// The previous Rust port had this as `pub const Marker: u8 = 0x80`
4248/// which is WRONG — `\200` (0x80) is NOT the canonical Marker byte.
4249/// C's Marker is 0xa2 per `Src/zsh.h:224`. No in-tree callers used
4250
4251/// Port of `static const char *colon_stuffs[]` from `Src/pattern.c:1134-1138`.
4252/// 19 entries matching C's table in declaration order, so `range_type(name)`
4253/// returns the same `(index + PP_FIRST)` value C does:
4254/// alpha=1, alnum=2, ascii=3, blank=4, cntrl=5, digit=6, graph=7, lower=8,
4255/// print=9, punct=10, space=11, upper=12, xdigit=13, IDENT=14, IFS=15,
4256/// IFSSPACE=16, WORD=17, INCOMPLETE=18, INVALID=19. Prior Rust port had
4257/// only 12 lowercase entries and was MISSING `ascii` between `alnum` and
4258/// `blank`, so `range_type("digit")` returned 5 instead of C's PP_DIGIT=6
4259/// and every `[:class:]` byte marker emitted by `complete.rs:733`
4260/// (`0x80 + ch`) was off-by-one for classes after `alnum`. Real port bug.
4261const POSIX_CLASS_NAMES: &[&str] = &[
4262 "alpha",
4263 "alnum",
4264 "ascii",
4265 "blank",
4266 "cntrl",
4267 "digit",
4268 "graph",
4269 "lower",
4270 "print",
4271 "punct",
4272 "space",
4273 "upper",
4274 "xdigit",
4275 "IDENT",
4276 "IFS",
4277 "IFSSPACE",
4278 "WORD",
4279 "INCOMPLETE",
4280 "INVALID",
4281];
4282
4283/// Port of file-static `zpc_disables_stack` from `Src/pattern.c:4244`.
4284/// Per-evaluator function-scope disable save-stack (bucket-1: each
4285/// worker thread parses/executes its own function calls, so each must
4286/// have its own scope stack). Reason for `thread_local!` over `Mutex`:
4287/// in zsh C this is a per-process file-static; in zshrs each worker
4288/// thread is its own evaluator — TLS preserves the per-evaluator
4289/// semantic without serializing across workers.
4290///
4291/// Element type: u32 bitmap matching `savepatterndisables` return
4292/// shape (c:4220 `unsigned int disables`). Prior Rust port used
4293/// `Vec<Vec<String>>` and `startpatternscope` cloned a `Vec<String>`
4294/// of names instead of the bitmap — a real port bug that made
4295/// `setopt LOCALPATTERNS` function entry/exit silently fail to save/
4296/// restore the disable state.
4297thread_local! {
4298 static PATSCOPE_STACK: std::cell::RefCell<Vec<u32>> =
4299 const { std::cell::RefCell::new(Vec::new()) };
4300}
4301
4302// =====================================================================
4303// 17. Module-loader / disable mgmt — pattern.c:4161-4296
4304// =====================================================================
4305
4306// `pub static patterndisables: Mutex<Vec<String>>` deleted — was a
4307// dead tombstone with no callers outside the buggy
4308// `startpatternscope` / `endpatternscope` / `clearpatterndisables`
4309// fns (which cloned/cleared it instead of operating on the real
4310// `zpc_disables` byte array). The canonical zsh `zpc_disables[ZPC_COUNT]`
4311// (Src/pattern.c:268) is the disable state; the names list does not
4312// exist as a separate data structure in C.
4313
4314/// Port of `char zpc_disables[ZPC_COUNT]` from `Src/pattern.c:268`.
4315/// Per-token disable byte — when `zpc_disables[i]` is non-zero, the
4316/// pattern token at index `i` (ZPC_*) is treated as a literal,
4317/// not its meta-meaning.
4318pub static zpc_disables: Mutex<[u8; ZPC_COUNT as usize]> = Mutex::new([0u8; ZPC_COUNT as usize]); // c:268
4319
4320/// Helper: when patinsert shifts a chunk of bytecode, any 4-byte
4321/// next_off slot that previously pointed past `opnd` must be bumped
4322/// by `delta` to keep the chain links valid.
4323///
4324/// Walks the buffer linearly opcode-by-opcode reading I_NEXT slots.
4325/// Conservatively adjusts every nonzero next that lands past opnd.
4326fn fixup_offsets_after_insert(buf: &mut [u8], opnd: usize, delta: u32) {
4327 let mut i = 0;
4328 while i + I_BODY <= buf.len() {
4329 let op = buf[i + I_OP];
4330 if op == 0 {
4331 i += 1;
4332 continue;
4333 } // sentinel byte, skip
4334 let next_bytes = &buf[i + I_NEXT..i + I_NEXT + 4];
4335 let cur = u32::from_le_bytes(next_bytes.try_into().unwrap());
4336 if cur != 0 {
4337 let abs = cur as usize;
4338 if abs >= opnd && abs <= buf.len() {
4339 let new = cur + delta;
4340 buf[i + I_NEXT..i + I_NEXT + 4].copy_from_slice(&new.to_le_bytes());
4341 }
4342 }
4343 i = advance_past_instr(buf, i);
4344 if i == 0 {
4345 break;
4346 }
4347 }
4348}
4349
4350/// Helper: given a buffer and current opcode offset, return the
4351/// offset of the next opcode after this one's payload.
4352///
4353/// Encodes the per-opcode payload size table — must stay in sync
4354/// with patnode/patinsert calls in the compiler.
4355fn advance_past_instr(buf: &[u8], pos: usize) -> usize {
4356 if pos + I_BODY > buf.len() {
4357 return 0;
4358 }
4359 let op = buf[pos + I_OP];
4360 let body_start = pos + I_BODY;
4361 match op {
4362 P_END | P_NOTHING | P_BACK | P_EXCSYNC | P_EXCEND | P_ISSTART | P_ISEND | P_COUNTSTART
4363 | P_ANY | P_STAR | P_NUMANY => body_start,
4364 P_GFLAGS => body_start + 4, // i32 flag-bits payload
4365 P_EXACTLY => {
4366 // payload: u32 len + len bytes
4367 if body_start + 4 > buf.len() {
4368 return 0;
4369 }
4370 let len =
4371 u32::from_le_bytes(buf[body_start..body_start + 4].try_into().unwrap()) as usize;
4372 body_start + 4 + len
4373 }
4374 P_ANYOF | P_ANYBUT => {
4375 if body_start + 4 > buf.len() {
4376 return 0;
4377 }
4378 let len =
4379 u32::from_le_bytes(buf[body_start..body_start + 4].try_into().unwrap()) as usize;
4380 body_start + 4 + len
4381 }
4382 P_ONEHASH | P_TWOHASH | P_BRANCH => body_start,
4383 // c:113 P_WBRANCH and c:114-115 P_EXCLUDE/P_EXCLUDP carry an
4384 // 8-byte syncptr payload (one Upat slot in C) right after
4385 // their header, before the chained operand.
4386 P_WBRANCH | P_EXCLUDE | P_EXCLUDP => body_start + 8,
4387 P_OPEN..=0x88 | P_CLOSE..=0x98 => body_start,
4388 P_NUMRNG => body_start + 16, // two i64
4389 P_NUMFROM | P_NUMTO => body_start + 8,
4390 P_COUNT => body_start + 16, // min i64 + max i64; operand inline follows
4391 _ => body_start,
4392 }
4393}
4394
4395/// Helper: directly set the `next_off` slot of the instruction at
4396/// `pos` without walking the chain. C uses pointer arithmetic
4397/// (`scanp->l = ...`) inline; Rust factors it for byte-offset
4398/// bookkeeping. Architectural helper.
4399fn set_next(pos: usize, val: usize) {
4400 let mut buf = patout.lock().unwrap();
4401 if pos + I_NEXT + 4 <= buf.len() {
4402 buf[pos + I_NEXT..pos + I_NEXT + 4].copy_from_slice(&(val as u32).to_le_bytes());
4403 }
4404}
4405
4406/// Helper: walk every branch's operand chain and patch each branch's
4407/// last-operand-node `.next` to `target`. Used to chain a fully-
4408/// compiled alternation switch to whatever opcode follows (P_END for
4409/// the outermost compile, P_CLOSE_N for a sub-group).
4410///
4411/// Architectural helper — C uses pattail inside the BRANCH operand
4412/// scope via Upat pointer arithmetic; Rust factors it for clarity.
4413fn chain_branches_to(starter: usize, target: usize) {
4414 let mut cur = starter;
4415 loop {
4416 // Operand starts at cur + I_BODY (the byte right after this
4417 // branch's header). Walk operand's next-chain to its end
4418 // and set its .next = target.
4419 pattail(cur + I_BODY, target);
4420 // Move to next alternative.
4421 let buf = patout.lock().unwrap();
4422 if cur + I_NEXT + 4 > buf.len() {
4423 break;
4424 }
4425 let nb: [u8; 4] = buf[cur + I_NEXT..cur + I_NEXT + 4].try_into().unwrap();
4426 let n = u32::from_le_bytes(nb) as usize;
4427 drop(buf);
4428 if n == 0 {
4429 break;
4430 }
4431 cur = n;
4432 }
4433}
4434
4435/// Port of `patmatch(Upat prog)` from `Src/pattern.c:2694`. The interpreter.
4436///
4437/// Returns `Some(end_pos)` on successful match (end_pos = byte offset
4438/// in `string` where match ended), `None` on no-match. The state
4439/// param tracks captures.
4440///
4441/// Rust signature differs from C's `int patmatch(Upat prog)`: input
4442/// bytecode, current input position, captures, and glob flags are
4443/// threaded through args rather than C's per-thread file-statics
4444/// (`patinput`, `patinstart`, `patbeginp`/`patendp`, `patglobflags`).
4445/// Rule S1 deviation justified by zshrs's threading model — see
4446/// PORT_PLAN.md Bucket 1.
4447///
4448/// WARNING: NOT IN PATTERN.C — Rust-only helper. Approximate-match
4449/// inner loop for `(#aN)`-flagged P_EXACTLY arms. C interleaves the
4450/// edit-operation backtracking inline within the P_EXACTLY case
4451/// (c:2737-2779); the Rust port factors the inner per-byte walk +
4452/// recursive sub/ins/del trials into this helper so the linear
4453/// patmatch loop body stays manageable. Both writer and reader live
4454/// in pattern.rs.
4455///
4456/// Walks pattern bytes `str_bytes` against `input_bytes[s_off..]`.
4457/// On exact-match per byte: advance both. On mismatch: try the 3
4458/// edit operations in order (substitute = advance both + errsfound++,
4459/// insert = advance pat + errsfound++, delete = advance input +
4460/// errsfound++); each recurses via `patmatch` to continue with the
4461/// rest of the bytecode at `next`. Returns the matched-end byte
4462/// offset in `string` on success, None on failure.
4463fn approx_match_exactly(
4464 code: &[u8],
4465 next: usize,
4466 string: &str,
4467 s_off: usize,
4468 str_bytes: &[u8],
4469 state: &mut rpat,
4470 glob_flags: i32,
4471 max_errs: i32,
4472) -> Option<usize> {
4473 let input_bytes = string.as_bytes();
4474 // Try matching the EXACT prefix as far as it lines up; on first
4475 // mismatch (or out-of-input), branch into the edit-operation
4476 // trials. This is a bounded recursive search; the budget caps
4477 // recursion depth at `max_errs - state.errsfound`.
4478 fn walk(
4479 code: &[u8],
4480 next: usize,
4481 string: &str,
4482 input_bytes: &[u8],
4483 str_bytes: &[u8],
4484 s_off: usize,
4485 p_off: usize,
4486 state: &mut rpat,
4487 glob_flags: i32,
4488 max_errs: i32,
4489 ) -> Option<usize> {
4490 // Direction terminology: `s_off+1` consumes one INPUT byte,
4491 // `p_off+1` consumes one PATTERN byte.
4492 // - (s+1, p+1) = substitute one input byte for one pat byte
4493 // - (s+1, p) = consume input byte that's NOT in pattern
4494 // (= INSERTION in input compared to pattern)
4495 // - (s, p+1) = consume pat byte that's NOT in input
4496 // (= DELETION from input compared to pattern)
4497 //
4498 // Tries every option at each step, returns the path producing
4499 // the LARGEST end s_off (most input consumed). Anchored
4500 // callers (pattry) need full-input consumption; non-anchored
4501 // accept any. C handles this via bytecode-level branching
4502 // backtrack; the Rust port collapses it into a recursive
4503 // tree-search with per-attempt state save/restore.
4504 let mut best: Option<usize> = None;
4505 let saved_outer = state.clone();
4506 let mut update = |best: &mut Option<usize>, cand: Option<usize>| {
4507 if let Some(c) = cand {
4508 if best.map(|b| c > b).unwrap_or(true) {
4509 *best = Some(c);
4510 }
4511 }
4512 };
4513 if p_off == str_bytes.len() {
4514 // Pattern body consumed. Three paths to try; pick the
4515 // one with most input consumed:
4516 // (a) terminate here at s_off, run continuation.
4517 // (b) absorb 1+ trailing input as INSERTION-IN-INPUT edits.
4518 let terminate = if next == 0 {
4519 Some(s_off)
4520 } else {
4521 patmatch(code, next, string, s_off, state, glob_flags)
4522 };
4523 update(&mut best, terminate);
4524 // Path (b): absorb trailing input as insertion edits.
4525 if s_off < input_bytes.len() && state.errsfound < max_errs {
4526 *state = saved_outer.clone();
4527 state.errsfound += 1;
4528 let r = walk(
4529 code,
4530 next,
4531 string,
4532 input_bytes,
4533 str_bytes,
4534 s_off + 1,
4535 p_off,
4536 state,
4537 glob_flags,
4538 max_errs,
4539 );
4540 update(&mut best, r);
4541 }
4542 *state = saved_outer;
4543 return best;
4544 }
4545 // c:Src/pattern.c:2676 CHARMATCH — inline case-aware byte
4546 // compare (the C macro, not a fn). Honors GF_IGNCASE /
4547 // GF_LCMATCHUC so under `(#i)` a case-only difference is NOT an
4548 // edit: `(#ia2)readme` matches `READXME` (only X→M costs an
4549 // error). Raw byte `==` counted every case difference as an edit.
4550 let charmatch_inline = |chin: u8, chpa: u8| -> bool {
4551 chin == chpa
4552 || ((glob_flags & GF_IGNCASE) != 0
4553 && chin.to_ascii_lowercase() == chpa.to_ascii_lowercase())
4554 || ((glob_flags & GF_LCMATCHUC) != 0
4555 && chpa.is_ascii_lowercase()
4556 && chpa.to_ascii_uppercase() == chin)
4557 };
4558 // Exact-byte match — try advancing both.
4559 if s_off < input_bytes.len() && charmatch_inline(input_bytes[s_off], str_bytes[p_off]) {
4560 *state = saved_outer.clone();
4561 let r = walk(
4562 code,
4563 next,
4564 string,
4565 input_bytes,
4566 str_bytes,
4567 s_off + 1,
4568 p_off + 1,
4569 state,
4570 glob_flags,
4571 max_errs,
4572 );
4573 update(&mut best, r);
4574 }
4575 // Edit operations — each costs 1 error.
4576 if state.errsfound < max_errs {
4577 // c:Src/pattern.c:3520-3543 — Damerau transposition: swap two
4578 // adjacent input chars to match two adjacent pat chars (i.e.
4579 // input[s..s+2] reversed equals pat[p..p+2]). Costs 1 edit;
4580 // pin for `(#a3)abcd` matching "dcba" — needs sub + transp +
4581 // sub = 3 edits to bridge the reversal.
4582 if s_off + 1 < input_bytes.len()
4583 && p_off + 1 < str_bytes.len()
4584 && charmatch_inline(input_bytes[s_off], str_bytes[p_off + 1])
4585 && charmatch_inline(input_bytes[s_off + 1], str_bytes[p_off])
4586 {
4587 *state = saved_outer.clone();
4588 state.errsfound += 1;
4589 let r = walk(
4590 code,
4591 next,
4592 string,
4593 input_bytes,
4594 str_bytes,
4595 s_off + 2,
4596 p_off + 2,
4597 state,
4598 glob_flags,
4599 max_errs,
4600 );
4601 update(&mut best, r);
4602 }
4603 // Substitute.
4604 if s_off < input_bytes.len() {
4605 *state = saved_outer.clone();
4606 state.errsfound += 1;
4607 let r = walk(
4608 code,
4609 next,
4610 string,
4611 input_bytes,
4612 str_bytes,
4613 s_off + 1,
4614 p_off + 1,
4615 state,
4616 glob_flags,
4617 max_errs,
4618 );
4619 update(&mut best, r);
4620 }
4621 // Insertion in input (skip input byte only).
4622 if s_off < input_bytes.len() {
4623 *state = saved_outer.clone();
4624 state.errsfound += 1;
4625 let r = walk(
4626 code,
4627 next,
4628 string,
4629 input_bytes,
4630 str_bytes,
4631 s_off + 1,
4632 p_off,
4633 state,
4634 glob_flags,
4635 max_errs,
4636 );
4637 update(&mut best, r);
4638 }
4639 // Deletion from input (skip pattern byte only).
4640 *state = saved_outer.clone();
4641 state.errsfound += 1;
4642 let r = walk(
4643 code,
4644 next,
4645 string,
4646 input_bytes,
4647 str_bytes,
4648 s_off,
4649 p_off + 1,
4650 state,
4651 glob_flags,
4652 max_errs,
4653 );
4654 update(&mut best, r);
4655 }
4656 *state = saved_outer;
4657 best
4658 }
4659 walk(
4660 code,
4661 next,
4662 string,
4663 input_bytes,
4664 str_bytes,
4665 s_off,
4666 0,
4667 state,
4668 glob_flags,
4669 max_errs,
4670 )
4671}
4672
4673thread_local! {
4674 /// Rust-only backstop counter (no C analogue) — gates `patmatch`
4675 /// recursion at PATMATCH_MAX_DEPTH so misbehaving closures convert
4676 /// would-be stack overflows into clean None returns. Documented in
4677 /// `fake_fn_allowlist.txt` under the patmatch arc.
4678 static PATMATCH_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
4679}
4680
4681/// Maximum patmatch recursion depth — Rust-only backstop, no C
4682/// analogue. The C source's primary protection against
4683/// closure-of-closure infinite recursion is the P_WBRANCH "must
4684/// match at least 1 char" semantics described at pattern.c:108-144:
4685/// `P_WBRANCH: This works like a branch and is used in complex`
4686/// `closures, but the match must be at least 1 char in length to`
4687/// `avoid infinite loops.`
4688/// (Implemented in C's patmatch() switch at c:3044, `case P_WBRANCH`,
4689/// via the `errsfound`/`forceerrs` accounting at c:1059+.) The Rust
4690/// port's P_WBRANCH arm has gaps in that propagation, so we keep
4691/// this depth guard as a safety net.
4692///
4693/// **Tuned to test-thread stack size**, NOT the main-thread 8 MB.
4694/// Rust's `cargo test` spawns each test on a thread whose default
4695/// stack is ~2 MB (per `std::thread::Builder::stack_size` default at
4696/// `library/std/src/thread/mod.rs`), and `(fo#)#` patterns blow that
4697/// at ~250-300 frames per measured SIGABRT. 128 leaves headroom
4698/// (~256 KB worst case at ~2 KB/frame) while still admitting any
4699/// legitimate zsh pattern (Misc/globtests tops out around 30).
4700const PATMATCH_MAX_DEPTH: u32 = 128;
4701
4702thread_local! {
4703 // c:Src/pattern.c — the P_EXCLUDE `syncptr->p` heap buffer. Keyed by
4704 // the EXCLUDE node offset; value is a per-input-position byte array
4705 // recording where the asserted branch's excludable part matched (so
4706 // EXCSYNC can fail a revisit and force backtracking to a different
4707 // split). C stores this in the bytecode payload + a raw heap pointer
4708 // that survives backtracking; the Rust matcher clones `rpat` per
4709 // branch, so the buffer lives here (outside the cloned state) to
4710 // stay shared across recursion, exactly like C's global syncstrp->p.
4711 static EXCSYNC_BUF: std::cell::RefCell<std::collections::HashMap<usize, Vec<u8>>> =
4712 std::cell::RefCell::new(std::collections::HashMap::new());
4713}
4714
4715pub fn patmatch(
4716 code: &[u8],
4717 prog_off: usize,
4718 string: &str,
4719 string_off: usize,
4720 state: &mut rpat,
4721 glob_flags: i32,
4722) -> Option<usize> {
4723 // c:2694
4724 // Depth-guard: convert what would be a stack overflow into a
4725 // clean None return (= no match). See PATMATCH_MAX_DEPTH doc.
4726 let d = PATMATCH_DEPTH.with(|c| {
4727 let cur = c.get();
4728 c.set(cur + 1);
4729 cur + 1
4730 });
4731 if d > PATMATCH_MAX_DEPTH {
4732 PATMATCH_DEPTH.with(|c| c.set(c.get() - 1));
4733 return None;
4734 }
4735 struct DepthGuard;
4736 impl Drop for DepthGuard {
4737 fn drop(&mut self) {
4738 PATMATCH_DEPTH.with(|c| c.set(c.get().saturating_sub(1)));
4739 }
4740 }
4741 let _depth_guard = DepthGuard;
4742
4743 let mut scan = prog_off;
4744 let mut s_off = string_off;
4745 // Locally-mutable copy of glob_flags so mid-pattern P_GFLAGS can
4746 // toggle bits without affecting the caller's branch view.
4747 let mut glob_flags = glob_flags;
4748
4749 // Inlined port of CHARMATCH macro from `Src/pattern.c:2671-2677`:
4750 // #define CHARMATCH(chin, chpa) (chin == chpa || \
4751 // ((patglobflags & GF_IGNCASE) ?
4752 // ((ISUPPER(chin) ? TOLOWER(chin) : chin) ==
4753 // (ISUPPER(chpa) ? TOLOWER(chpa) : chpa)) :
4754 // (patglobflags & GF_LCMATCHUC) ?
4755 // (ISLOWER(chpa) && TOUPPER(chpa) == chin) : 0))
4756 //
4757 // - exact byte match always wins
4758 // - GF_IGNCASE: SYMMETRIC fold (both lowercase before compare)
4759 // - GF_LCMATCHUC: ASYMMETRIC — lowercase pattern char ALSO matches
4760 // uppercase text char; an UPPERCASE pattern char only matches
4761 // that exact uppercase text char. This is the `(#l)` flag's
4762 // "lowercase-in-pattern matches uppercase-in-text" semantic;
4763 // previously zshrs treated LCMATCHUC the same as IGNCASE (full
4764 // case-fold both sides) so `(#l)FOO` wrongly matched "foo".
4765 let charmatch = |chin: u8, chpa: u8, flags: i32| -> bool {
4766 if chin == chpa {
4767 return true; // c:2671
4768 }
4769 if (flags & GF_IGNCASE) != 0 {
4770 // c:2672-2674
4771 let a = if chin.is_ascii_uppercase() {
4772 chin.to_ascii_lowercase()
4773 } else {
4774 chin
4775 };
4776 let b = if chpa.is_ascii_uppercase() {
4777 chpa.to_ascii_lowercase()
4778 } else {
4779 chpa
4780 };
4781 return a == b;
4782 }
4783 if (flags & GF_LCMATCHUC) != 0 {
4784 // c:2675-2676
4785 return chpa.is_ascii_lowercase() && chpa.to_ascii_uppercase() == chin;
4786 }
4787 false // c:2677
4788 };
4789
4790 // Membership test for a P_ANYOF/P_ANYBUT body at `body_off` against the
4791 // input char at byte offset `off`. Returns (in_set, byte_advance). ASCII
4792 // input takes the unchanged byte `charmatch` path over the `chars` set; a
4793 // multibyte char is decoded (raw UTF-8, or a metafied `$'\xNN'` Meta-pair)
4794 // and tested against the appended class mask / wide ranges / wide literals
4795 // — C's `mb_patmatchrange` equivalent (pattern.c:3610).
4796 let anyof_membership = |body_off: usize, off: usize, gflags: i32| -> (bool, usize) {
4797 let range_flags = gflags & !(GF_IGNCASE | GF_LCMATCHUC);
4798 let chars_len =
4799 u32::from_le_bytes(code[body_off + 4..body_off + 8].try_into().unwrap()) as usize;
4800 let cs = body_off + 8;
4801 let set = &code[cs..cs + chars_len];
4802 let mut p = cs + chars_len;
4803 let classmask = u32::from_le_bytes(code[p..p + 4].try_into().unwrap());
4804 p += 4;
4805 let n_mbc = u32::from_le_bytes(code[p..p + 4].try_into().unwrap()) as usize;
4806 p += 4;
4807 let mbc_start = p;
4808 p += n_mbc * 4;
4809 let n_mbr = u32::from_le_bytes(code[p..p + 4].try_into().unwrap()) as usize;
4810 p += 4;
4811 let mbr_start = p;
4812
4813 let bytes = string.as_bytes();
4814 let b = bytes[off];
4815 // ASCII input, OR `unsetopt multibyte`: byte-level match (the prior
4816 // behaviour). C only takes the wide `mb_patmatchrange` path under
4817 // GF_MULTIBYTE; with it clear a high byte is matched as a raw byte.
4818 //
4819 // `off` can also land inside a multibyte char: P_STAR backtracks
4820 // byte-by-byte (`s_off + consumed`) and the approx omit-input paths
4821 // step `s_off + 1`, so a continuation P_ANYOF/P_ANYBUT can be tested
4822 // at a continuation byte. Decoding `string[off..]` there would panic,
4823 // so treat a non-boundary position as a raw byte (advance 1) — the
4824 // same fallback the byte-level machinery already relies on.
4825 if b < 0x80 || (gflags & GF_MULTIBYTE) == 0 || !string.is_char_boundary(off) {
4826 return (set.iter().any(|&c| charmatch(b, c, range_flags)), 1);
4827 }
4828 // Decode one logical input char + its source byte span.
4829 let mut it = string[off..].chars();
4830 let (ch, adv) = match it.next() {
4831 Some('\u{83}') => match it.next() {
4832 Some(n) => (
4833 ((n as u32 as u8) ^ 0x20) as char,
4834 '\u{83}'.len_utf8() + n.len_utf8(),
4835 ),
4836 None => ('\u{83}', 2),
4837 },
4838 Some(c) => (c, c.len_utf8()),
4839 None => return (false, 1),
4840 };
4841 let class_hit = (classmask & (1 << 0) != 0 && ch.is_alphabetic())
4842 || (classmask & (1 << 1) != 0 && ch.is_alphanumeric())
4843 || (classmask & (1 << 2) != 0 && ch.is_uppercase())
4844 || (classmask & (1 << 3) != 0 && ch.is_lowercase())
4845 || (classmask & (1 << 4) != 0 && ch.is_whitespace())
4846 || (classmask & (1 << 5) != 0 && (ch == ' ' || ch == '\t'))
4847 || (classmask & (1 << 6) != 0
4848 && !ch.is_alphanumeric()
4849 && !ch.is_whitespace()
4850 && !ch.is_control())
4851 || (classmask & (1 << 7) != 0 && ch.is_control())
4852 || (classmask & (1 << 8) != 0 && !ch.is_control())
4853 || (classmask & (1 << 9) != 0 && !ch.is_control() && !ch.is_whitespace());
4854 if class_hit {
4855 return (true, adv);
4856 }
4857 let cp = ch as u32;
4858 for k in 0..n_mbc {
4859 let o = mbc_start + k * 4;
4860 if u32::from_le_bytes(code[o..o + 4].try_into().unwrap()) == cp {
4861 return (true, adv);
4862 }
4863 }
4864 for k in 0..n_mbr {
4865 let o = mbr_start + k * 8;
4866 let lo = u32::from_le_bytes(code[o..o + 4].try_into().unwrap());
4867 let hi = u32::from_le_bytes(code[o + 4..o + 8].try_into().unwrap());
4868 if cp >= lo && cp <= hi {
4869 return (true, adv);
4870 }
4871 }
4872 (false, adv)
4873 };
4874
4875 while scan < code.len() {
4876 let op = code[scan + I_OP];
4877 let next_bytes: [u8; 4] = code[scan + I_NEXT..scan + I_NEXT + 4].try_into().unwrap();
4878 let next = u32::from_le_bytes(next_bytes) as usize;
4879
4880 match op {
4881 P_END => {
4882 // c:Src/pattern.c:3451-3454 — `case P_END: if (!(fail =
4883 // (patinput < patinend && !(patflags & PAT_NOANCH))))
4884 // return 1; break;`. When the WHOLE pattern is consumed
4885 // but the string ISN'T (anchored full match), this
4886 // branch FAILS so an enclosing alternation backtracks
4887 // to the next alternative — `[[ yes == (y|yes) ]]`
4888 // tries `y` (consumes 1 char, reaches P_END at offset
4889 // 1 ≠ 3), fails here, then tries `yes`. The previous
4890 // unconditional `Some(s_off)` committed to the first
4891 // (prefix) alternative and the external anchor check
4892 // rejected it without ever trying `yes`. PAT_NOANCH /
4893 // PAT_NOTEND callers (prefix/suffix strip, `(#e)`
4894 // interior matches) keep the partial-success return.
4895 let pf = patflags.load(Ordering::Relaxed);
4896 let anchored = (pf & (PAT_NOANCH | PAT_NOTEND) as i32) == 0;
4897 if s_off < string.len() && anchored {
4898 // c:Src/pattern.c:3451-3454 sets `fail = 1` here, then the
4899 // shared approximate-match block at c:3463-3475 deletes the
4900 // trailing input one CHARACTER at a time — `++errsfound;
4901 // CHARINC(patinput); continue;` — retrying P_END until the
4902 // input is gone or the `(#aN)` budget is spent. So
4903 // `[[ ab = (#a1)? ]]` matches: `?` consumes `a`, then the
4904 // leftover `b` is deleted for one error.
4905 //
4906 // Only literal (P_EXACTLY) runs did this before, inside
4907 // approx_match_exactly's own trailing-delete; a pattern
4908 // ending in a class / `?` / `*` with input left reached
4909 // here and simply failed, so `(#a2)[^0-9]` on `abc`
4910 // (match `a`, delete `bc`) was rejected where zsh accepts
4911 // it. The class/`?` arms already delete on a FAILED match
4912 // (e.g. c:5046); this is the same edit at the pattern end.
4913 let max_errs = (glob_flags & 0xff) as i32;
4914 if state.errsfound < max_errs {
4915 if let Some(c) = string[s_off..].chars().next() {
4916 state.errsfound += 1; // c:3466 ++errsfound
4917 // c:3475 CHARINC(patinput) + continue (retry P_END).
4918 return patmatch(
4919 code,
4920 scan,
4921 string,
4922 s_off + c.len_utf8(),
4923 state,
4924 glob_flags,
4925 );
4926 }
4927 }
4928 return None; // c:3452 fail, no budget → caller tries next alt
4929 }
4930 return Some(s_off); // c:3453
4931 }
4932 P_NOTHING => { /* empty match, just continue */ }
4933 P_BACK => { /* zero-width, walk back via next */ }
4934 P_EXCEND => {
4935 // c:Src/pattern.c:3037-3047 — terminal node ending an
4936 // exclusion operand: the exclusion matches iff the
4937 // (truncated) excludable span was fully consumed
4938 // (`patinput >= patinend`). Returns success here without
4939 // following the chain (which would wrongly continue into
4940 // the post-group pattern). The caller truncates the span
4941 // so `string.len()` IS the excludable end.
4942 if s_off >= string.len() {
4943 return Some(s_off);
4944 }
4945 return None;
4946 }
4947 P_EXCSYNC => {
4948 // c:Src/pattern.c:2992-3035 — record where the asserted
4949 // branch reached the EXCLUDE sync point (the end of the
4950 // excludable part) in the following EXCLUDE node's sync
4951 // buffer. If this position was already recorded (with
4952 // <= the current error count), fail so the asserted
4953 // branch backtracks to a different split. The EXCLUDE
4954 // node sits PHYSICALLY right after EXCSYNC (both
4955 // patcompnot and the `~` arm emit them adjacent).
4956 let exclude_node = scan + I_BODY;
4957 let already = EXCSYNC_BUF.with(|b| {
4958 let mut m = b.borrow_mut();
4959 if let Some(buf) = m.get_mut(&exclude_node) {
4960 if s_off < buf.len() {
4961 let cur = (state.errsfound + 1) as u8;
4962 if buf[s_off] != 0 && (state.errsfound + 1) >= buf[s_off] as i32 {
4963 return true; // c:3008 already matched here → fail
4964 }
4965 buf[s_off] = cur; // c:3017
4966 // c:3033 — earlier marks are now invalid.
4967 for x in buf[..s_off].iter_mut() {
4968 *x = 0;
4969 }
4970 }
4971 }
4972 false
4973 });
4974 if already {
4975 return None;
4976 }
4977 // else fall through to next node
4978 }
4979 P_EXACTLY => {
4980 // c:P_EXACTLY arm
4981 let body = scan + I_BODY;
4982 let len = u32::from_le_bytes(code[body..body + 4].try_into().unwrap()) as usize;
4983 let str_bytes = &code[body + 4..body + 4 + len];
4984 let input_bytes = string.as_bytes();
4985 // `(#aN)` budget — low byte of patglobflags. C uses the
4986 // file-static `patglobflags`; the Rust port carries it
4987 // through `glob_flags` since rpat went bucket-1.
4988 let max_errs = (glob_flags & 0xff) as i32;
4989 if max_errs > 0 {
4990 // Approximate match: try edit operations
4991 // (substitute/insert/delete) at each mismatch up to
4992 // the budget. Per c:3055/3109/3193 the C source
4993 // tracks `errsfound` across recursive patmatch
4994 // calls; the Rust port does the same via `state.
4995 // errsfound`. Skips transposition (Damerau extension
4996 // — rare; faithful follow-on).
4997 if let Some(new_off) = approx_match_exactly(
4998 code, next, string, s_off, str_bytes, state, glob_flags, max_errs,
4999 ) {
5000 return Some(new_off);
5001 }
5002 return None;
5003 }
5004 if s_off + len > input_bytes.len() {
5005 return None;
5006 }
5007 let case_flags = glob_flags & (GF_IGNCASE | GF_LCMATCHUC);
5008 let multibyte = (glob_flags & GF_MULTIBYTE) != 0; // c:349 GF_MULTIBYTE
5009 if case_flags != 0 {
5010 let inp_slice = &input_bytes[s_off..s_off + len];
5011 if multibyte && (glob_flags & GF_IGNCASE) != 0 {
5012 // Char-level Unicode case fold for IGNCASE only —
5013 // LCMATCHUC's asymmetric ASCII semantic doesn't
5014 // map cleanly to non-ASCII chars; per C the
5015 // CHARMATCH macro is byte-level anyway (TOLOWER/
5016 // TOUPPER from utils.c work on bytes).
5017 let pat_str = std::str::from_utf8(str_bytes).ok();
5018 let inp_str = std::str::from_utf8(inp_slice).ok();
5019 if let (Some(p), Some(i)) = (pat_str, inp_str) {
5020 let mut pc = p.chars();
5021 let mut ic = i.chars();
5022 loop {
5023 match (pc.next(), ic.next()) {
5024 (None, None) => break,
5025 (Some(_), None) | (None, Some(_)) => return None,
5026 (Some(a), Some(b)) => {
5027 // Equal chars (the common case) and ASCII
5028 // pairs fold without touching the heap.
5029 // The old per-char `to_lowercase()
5030 // .collect::<String>()` pair allocated
5031 // TWICE PER CHARACTER — `(#i)` scans over
5032 // a 566k-entry history (hsmw ^R) burned
5033 // ~4 CPU-minutes in RawVec::reserve.
5034 // Iterator::eq covers the multi-char
5035 // Unicode folds allocation-free.
5036 if a != b
5037 && (if a.is_ascii() && b.is_ascii() {
5038 a.to_ascii_lowercase()
5039 != b.to_ascii_lowercase()
5040 } else {
5041 !a.to_lowercase().eq(b.to_lowercase())
5042 })
5043 {
5044 return None;
5045 }
5046 }
5047 }
5048 }
5049 } else {
5050 // Non-UTF-8 input — byte fallback through CHARMATCH.
5051 for k in 0..len {
5052 if !charmatch(inp_slice[k], str_bytes[k], glob_flags) {
5053 return None;
5054 }
5055 }
5056 }
5057 } else {
5058 // c:2694 — per-byte CHARMATCH walk (covers
5059 // GF_IGNCASE byte-mode AND GF_LCMATCHUC asymmetric).
5060 for k in 0..len {
5061 if !charmatch(inp_slice[k], str_bytes[k], glob_flags) {
5062 return None;
5063 }
5064 }
5065 }
5066 } else if &input_bytes[s_off..s_off + len] != str_bytes {
5067 return None;
5068 }
5069 s_off += len;
5070 }
5071 P_ANY => {
5072 // c:P_ANY arm — `?` matches ONE character, advanced via
5073 // METACHARINC. zshrs stores `$'\xNN'` escapes metafied
5074 // (Meta `\u{83}` + byte^32); a multibyte character is
5075 // several Meta-pair chars that must be consumed as one,
5076 // else `?` matches a single metafied byte and leaves the
5077 // rest unmatched. Walk source chars, demetafying, until
5078 // one UTF-8 character forms; advance by its source span.
5079 // Identity for non-metafied (single-char advance).
5080 let s = &string[s_off..];
5081 let mut raw: Vec<u8> = Vec::new();
5082 let mut advance = 0usize;
5083 let mut chs = s.chars();
5084 while let Some(c) = chs.next() {
5085 if c == '\u{83}' {
5086 if let Some(n) = chs.clone().next() {
5087 if (n as u32) >= 0x80 {
5088 raw.push((n as u32 as u8) ^ 32);
5089 advance += c.len_utf8() + n.len_utf8();
5090 chs.next();
5091 if std::str::from_utf8(&raw).is_ok() {
5092 break;
5093 }
5094 continue;
5095 }
5096 }
5097 // Lone Meta — count it as one character.
5098 advance += c.len_utf8();
5099 break;
5100 }
5101 // Non-metafied char = one logical character.
5102 advance += c.len_utf8();
5103 break;
5104 }
5105 if advance == 0 {
5106 return None;
5107 }
5108 s_off += advance;
5109 }
5110 P_ANYOF => {
5111 // c:2780-2800 — a bracket expression is matched by
5112 // `patmatchrange` / `mb_patmatchrange` on the RAW input char.
5113 // Neither consults `patglobflags`, so a bracket NEVER
5114 // case-folds: only C's CHARMATCH macro (c:2671, used by
5115 // P_EXACTLY) honours GF_IGNCASE / GF_LCMATCHUC.
5116 //
5117 // Passing glob_flags through here made `(#i)` fold ranges as
5118 // well as literals, so `[[ FooBar = (#i)[a-z]## ]]` matched
5119 // (zsh: no match) and `[[ F = (#i)[[:lower:]] ]]` matched
5120 // (zsh: no match). Mask the case bits off for the set test.
5121 let body = scan + I_BODY;
5122 let input_bytes = string.as_bytes();
5123 let max_errs = (glob_flags & 0xff) as i32;
5124 let (has_match, adv) = if s_off < input_bytes.len() {
5125 anyof_membership(body, s_off, glob_flags)
5126 } else {
5127 (false, 0)
5128 };
5129 if !has_match {
5130 // c:Src/pattern.c:3463-3505 — approximate-match fail
5131 // handler. For non-P_EXACTLY opcodes, the ONLY
5132 // approximation path is "omit one input char" (skip
5133 // the input byte that didn't match, costing 1 edit,
5134 // then retry the same scan). For P_ANYOF that means
5135 // `[b][b]` against "bob" can match by omitting the
5136 // middle "o" with 1 edit — the `(#a1)[b][b]` pin.
5137 if state.errsfound < max_errs && s_off < input_bytes.len() {
5138 state.errsfound += 1;
5139 // Retry same scan with one input byte consumed.
5140 return patmatch(code, scan, string, s_off + 1, state, glob_flags);
5141 }
5142 return None;
5143 }
5144 s_off += adv;
5145 }
5146 P_ANYBUT => {
5147 let body = scan + I_BODY;
5148 let input_bytes = string.as_bytes();
5149 let max_errs = (glob_flags & 0xff) as i32;
5150 // c:2781-2800 — the negated bracket shares P_ANYOF's matcher
5151 // (`… ^ (P_OP(scan) == P_ANYOF)`), so it is equally
5152 // case-BLIND (anyof_membership masks the case bits off).
5153 let (in_set, adv) = if s_off < input_bytes.len() {
5154 anyof_membership(body, s_off, glob_flags)
5155 } else {
5156 (true, 0)
5157 };
5158 let has_match = s_off < input_bytes.len() && !in_set;
5159 if !has_match {
5160 if state.errsfound < max_errs && s_off < input_bytes.len() {
5161 // c:3463 — omit-input approx path (same as P_ANYOF).
5162 state.errsfound += 1;
5163 return patmatch(code, scan, string, s_off + 1, state, glob_flags);
5164 }
5165 return None;
5166 }
5167 s_off += adv;
5168 }
5169 P_STAR => {
5170 // c:P_STAR arm (greedy)
5171 // Greedy: try to match as many chars as possible then
5172 // backtrack until the rest matches.
5173 let input_bytes = string.as_bytes();
5174 let max = input_bytes.len() - s_off;
5175 let mut consumed = max;
5176 loop {
5177 let mut sub_state = state.clone();
5178 if let Some(end) = patmatch(
5179 code,
5180 next,
5181 string,
5182 s_off + consumed,
5183 &mut sub_state,
5184 glob_flags,
5185 ) {
5186 *state = sub_state;
5187 return Some(end);
5188 }
5189 if consumed == 0 {
5190 return None;
5191 }
5192 consumed -= 1;
5193 }
5194 }
5195 P_ONEHASH | P_TWOHASH => {
5196 // c:P_ONEHASH / P_TWOHASH
5197 // The operand (the simple atom being repeated) starts
5198 // at `scan + I_BODY` — that's the byte immediately
5199 // after the quantifier opcode (which has its own
5200 // 5-byte header). The repeated atom occupies the
5201 // bytes from there until `next`.
5202 let operand = scan + I_BODY;
5203 let min = if op == P_TWOHASH { 1 } else { 0 };
5204 // Greedy: match operand repeatedly until it fails,
5205 // then walk back trying continuations.
5206 let mut positions = vec![s_off];
5207 loop {
5208 let cur = *positions.last().unwrap();
5209 let mut sub_state = state.clone();
5210 if let Some(new_pos) =
5211 patmatch(code, operand, string, cur, &mut sub_state, glob_flags)
5212 {
5213 if new_pos == cur {
5214 break;
5215 } // zero-width fixed point
5216 *state = sub_state;
5217 positions.push(new_pos);
5218 } else {
5219 break;
5220 }
5221 }
5222 if positions.len() - 1 < min {
5223 return None;
5224 }
5225 // Walk back from longest match trying continuations.
5226 while positions.len() > min {
5227 let cur = *positions.last().unwrap();
5228 let mut sub_state = state.clone();
5229 if let Some(end) = patmatch(code, next, string, cur, &mut sub_state, glob_flags)
5230 {
5231 *state = sub_state;
5232 return Some(end);
5233 }
5234 if positions.len() <= min + 1 {
5235 return None;
5236 }
5237 positions.pop();
5238 }
5239 return None;
5240 }
5241 P_BRANCH | P_WBRANCH => {
5242 // c:P_BRANCH / P_WBRANCH arm — c:3043-3044 in zsh.
5243 // c:3046-3050 — if next is NOT another BRANCH, this is
5244 // the only alternative; avoid the alt-loop and just
5245 // continue with the operand inline (no recursion, no
5246 // fallthrough on failure).
5247 //
5248 // For P_WBRANCH, the operand sits AFTER the 8-byte
5249 // syncptr payload (`P_OPERAND(p) + 1` per pattern.c:1865).
5250 let operand_off_extra = if op == P_WBRANCH { 8 } else { 0 };
5251 // c:3056 — if `next` is P_EXCLUDE/P_EXCLUDP, this BRANCH
5252 // is the asserted half of a `^pat` / `!(pat)` exclusion.
5253 // Minimal port of c:3056-3201: try the asserted operand
5254 // (the `*`-based branch body), then for each EXCLUDE in
5255 // the next-chain run the exclude operand against the
5256 // same input range; if any exclude matches with the
5257 // SAME consumed length, fail; else succeed.
5258 if next != 0 && next < code.len() && P_ISEXCLUDE(code[next + I_OP]) {
5259 // c:Src/pattern.c:3056-3201 — `^pat` / `(^pat)` /
5260 // `!(pat)` / `A~B` exclusion. The asserted branch
5261 // (`STAR EXCSYNC rest` for `^`/`!`, or `A EXCSYNC
5262 // rest` for `A~B`) is matched normally; its EXCSYNC
5263 // node records where the EXCLUDABLE part ended into
5264 // the EXCLUDE node's sync buffer. We then truncate to
5265 // that synclen and test the exclusion operand(s); if
5266 // any matches the excludable span, this candidate is
5267 // excluded and we re-run the asserted branch — EXCSYNC
5268 // now fails at the recorded position, forcing a
5269 // different split — until an un-excluded split is
5270 // found or the asserted branch can no longer match.
5271 let asserted_operand = scan + I_BODY + operand_off_extra;
5272 let exclude_node = next;
5273 // Allocate (reset) the sync buffer for this EXCLUDE.
5274 let prev_buf = EXCSYNC_BUF.with(|b| {
5275 b.borrow_mut()
5276 .insert(exclude_node, vec![0u8; string.len() + 1])
5277 });
5278 let mut found: Option<(usize, rpat)> = None;
5279 loop {
5280 let mut a_state = state.clone();
5281 let matchpt = match patmatch(
5282 code,
5283 asserted_operand,
5284 string,
5285 s_off,
5286 &mut a_state,
5287 glob_flags,
5288 ) {
5289 Some(e) => e,
5290 None => break,
5291 };
5292 // c:3128-3130 — synclen = first marked position in
5293 // the sync buffer (the end of the excludable part).
5294 let synclen = EXCSYNC_BUF.with(|b| {
5295 b.borrow()
5296 .get(&exclude_node)
5297 .and_then(|buf| buf.iter().position(|&x| x != 0))
5298 .unwrap_or(s_off)
5299 });
5300 // c:3134-3138 — test each EXCLUDE/EXCLUDP operand
5301 // against the excludable span string[s_off..synclen].
5302 // Truncating to `synclen` makes the operand's
5303 // trailing P_EXCEND succeed iff the span matched in
5304 // full.
5305 let span_end = synclen.min(string.len());
5306 let span = &string[..span_end];
5307 let mut excluded = false;
5308 let mut excl = exclude_node;
5309 while excl != 0 && excl < code.len() && P_ISEXCLUDE(code[excl + I_OP]) {
5310 let excl_operand = excl + I_BODY + 8; // after 8-byte syncptr
5311 let mut e_state = state.clone();
5312 // c:3134-3142 — `patglobflags &= ~0xff;
5313 // errsfound = 0;` — exclusions match EXACTLY,
5314 // with approximation turned off. Clear both the
5315 // running error count AND the budget (low byte
5316 // of glob_flags); otherwise `(#a1)README~READ_ME`
5317 // matched the exclusion READ_ME against READ.ME
5318 // approximately and wrongly excluded it.
5319 e_state.errsfound = 0;
5320 let excl_flags = glob_flags & !0xff;
5321 if let Some(em) =
5322 patmatch(code, excl_operand, span, s_off, &mut e_state, excl_flags)
5323 {
5324 if em == span_end {
5325 excluded = true;
5326 break;
5327 }
5328 }
5329 let nb: [u8; 4] =
5330 code[excl + I_NEXT..excl + I_NEXT + 4].try_into().unwrap();
5331 let n = u32::from_le_bytes(nb) as usize;
5332 if n == 0 || n == excl {
5333 break;
5334 }
5335 excl = n;
5336 }
5337 if !excluded {
5338 found = Some((matchpt, a_state));
5339 break;
5340 }
5341 // Excluded: loop. The sync buffer now records this
5342 // split, so the next asserted patmatch's EXCSYNC
5343 // fails here and backtracks to a different split.
5344 }
5345 // Restore/clear the sync buffer slot.
5346 EXCSYNC_BUF.with(|b| {
5347 let mut m = b.borrow_mut();
5348 match prev_buf {
5349 Some(p) => {
5350 m.insert(exclude_node, p);
5351 }
5352 None => {
5353 m.remove(&exclude_node);
5354 }
5355 }
5356 });
5357 match found {
5358 Some((end, a_state)) => {
5359 *state = a_state;
5360 return Some(end);
5361 }
5362 None => return None,
5363 }
5364 }
5365 let next_is_branch = next != 0
5366 && next < code.len()
5367 && (code[next + I_OP] == P_BRANCH || code[next + I_OP] == P_WBRANCH);
5368 if !next_is_branch {
5369 scan = scan + I_BODY + operand_off_extra;
5370 continue;
5371 }
5372 // Alt-loop: try each branch's operand; on success
5373 // return; on failure walk to the next BRANCH via .next.
5374 let mut br = scan;
5375 loop {
5376 let br_op = code[br + I_OP];
5377 let br_extra = if br_op == P_WBRANCH { 8 } else { 0 };
5378 let br_next_bytes: [u8; 4] =
5379 code[br + I_NEXT..br + I_NEXT + 4].try_into().unwrap();
5380 let br_next = u32::from_le_bytes(br_next_bytes) as usize;
5381 let operand = br + I_BODY + br_extra;
5382 let mut sub_state = state.clone();
5383 // c:Src/pattern.c:3210-3248 — P_WBRANCH per-position
5384 // visit guard. Allocate a bitmap sized to the input
5385 // (`zshcalloc((patinend - patinstart) + 1)`), then
5386 // check/set `*ptr = errsfound + 1` at the current
5387 // input offset. On revisit with the same-or-fewer
5388 // errors, return 0 to bound recursion. Without this,
5389 // `(fo#)#` against any non-trivial input recurses
5390 // until PATMATCH_MAX_DEPTH.
5391 let mut wbranch_skip = false;
5392 if br_op == P_WBRANCH {
5393 let bm = state
5394 .wbranch_visits
5395 .entry(br)
5396 .or_insert_with(|| vec![0u8; string.len() + 1]);
5397 let slot = bm.get(s_off).copied().unwrap_or(0);
5398 let cur = (state.errsfound as i32 + 1) as u8;
5399 if slot != 0 && (state.errsfound + 1) >= slot as i32 {
5400 wbranch_skip = true; // c:3245-3247
5401 } else if s_off < bm.len() {
5402 bm[s_off] = cur; // c:3248
5403 }
5404 }
5405 let sub_result = if wbranch_skip {
5406 None
5407 } else {
5408 patmatch(code, operand, string, s_off, &mut sub_state, glob_flags)
5409 };
5410 if let Some(end) = sub_result {
5411 // c:108-144 (pattern.c header doc on P_WBRANCH):
5412 // "P_WBRANCH: This works like a branch and is
5413 // used in complex closures, but the match must
5414 // be at least 1 char in length to avoid
5415 // infinite loops. The test for length is
5416 // done via the next pointer in the WBRANCH
5417 // test in patmatch()."
5418 // C enforces this via the `errsfound`/`forceerrs`
5419 // accounting at c:1059+ inside patcompbranch.
5420 // The Rust walker checks end > s_off directly:
5421 // if the body consumed nothing, reject this
5422 // alternative and fall through to the next.
5423 // Without this guard, `(fo#)#` against any input
5424 // stack-overflows because the inner body (`fo#`)
5425 // can match the empty string repeatedly.
5426 if br_op == P_WBRANCH && end == s_off {
5427 // c:108 "at least 1 char" — fall through to
5428 // try the next alternative.
5429 } else {
5430 *state = sub_state;
5431 return Some(end);
5432 }
5433 }
5434 if br_next == 0 {
5435 return None;
5436 }
5437 let op_next = code[br_next + I_OP];
5438 if op_next != P_BRANCH && op_next != P_WBRANCH {
5439 return None;
5440 }
5441 br = br_next;
5442 }
5443 }
5444 P_NUMRNG => {
5445 // c:P_NUMRNG — `<from-to>` numeric range. C source at
5446 // pattern.c:3460+ backtracks shorter prefixes when the
5447 // continuation fails. Ported here as a longest-first walk
5448 // back through digit-prefix lengths, trying the continuation
5449 // at each. Pins `Test/D02glob.ztst:133` (`<1-1000>33` matches
5450 // "633" by consuming just "6" then literal "33").
5451 let body = scan + I_BODY;
5452 let from = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
5453 let to = i64::from_le_bytes(code[body + 8..body + 16].try_into().unwrap());
5454 let input_bytes = string.as_bytes();
5455 let start = s_off;
5456 let mut k = start;
5457 while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
5458 k += 1;
5459 }
5460 if k == start {
5461 return None;
5462 }
5463 // Walk back from longest digit prefix to shortest, trying
5464 // the continuation (`next` opcode) at each split point.
5465 while k > start {
5466 let n_res = std::str::from_utf8(&input_bytes[start..k])
5467 .ok()
5468 .and_then(|s| s.parse::<i64>().ok());
5469 if let Some(n) = n_res {
5470 if n >= from && n <= to {
5471 let mut sub_state = state.clone();
5472 if let Some(end) =
5473 patmatch(code, next, string, k, &mut sub_state, glob_flags)
5474 {
5475 *state = sub_state;
5476 return Some(end);
5477 }
5478 }
5479 }
5480 k -= 1;
5481 }
5482 return None;
5483 }
5484 P_NUMFROM => {
5485 // c:P_NUMFROM — same backtracking story as P_NUMRNG.
5486 let body = scan + I_BODY;
5487 let from = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
5488 let input_bytes = string.as_bytes();
5489 let start = s_off;
5490 let mut k = start;
5491 while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
5492 k += 1;
5493 }
5494 if k == start {
5495 return None;
5496 }
5497 while k > start {
5498 let n_res = std::str::from_utf8(&input_bytes[start..k])
5499 .ok()
5500 .and_then(|s| s.parse::<i64>().ok());
5501 if let Some(n) = n_res {
5502 if n >= from {
5503 let mut sub_state = state.clone();
5504 if let Some(end) =
5505 patmatch(code, next, string, k, &mut sub_state, glob_flags)
5506 {
5507 *state = sub_state;
5508 return Some(end);
5509 }
5510 }
5511 }
5512 k -= 1;
5513 }
5514 return None;
5515 }
5516 P_NUMTO => {
5517 let body = scan + I_BODY;
5518 let to = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
5519 let input_bytes = string.as_bytes();
5520 let start = s_off;
5521 let mut k = start;
5522 while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
5523 k += 1;
5524 }
5525 if k == start {
5526 return None;
5527 }
5528 while k > start {
5529 let n_res = std::str::from_utf8(&input_bytes[start..k])
5530 .ok()
5531 .and_then(|s| s.parse::<i64>().ok());
5532 if let Some(n) = n_res {
5533 if n <= to {
5534 let mut sub_state = state.clone();
5535 if let Some(end) =
5536 patmatch(code, next, string, k, &mut sub_state, glob_flags)
5537 {
5538 *state = sub_state;
5539 return Some(end);
5540 }
5541 }
5542 }
5543 k -= 1;
5544 }
5545 return None;
5546 }
5547 P_NUMANY => {
5548 // c:P_NUMANY — `<->` any non-empty digit run. Pins
5549 // `Test/D02glob.ztst:136` (`<->33` matches "633" by
5550 // consuming just "6" then literal "33").
5551 let input_bytes = string.as_bytes();
5552 let start = s_off;
5553 let mut k = start;
5554 while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
5555 k += 1;
5556 }
5557 if k == start {
5558 return None;
5559 }
5560 while k > start {
5561 let mut sub_state = state.clone();
5562 if let Some(end) = patmatch(code, next, string, k, &mut sub_state, glob_flags) {
5563 *state = sub_state;
5564 return Some(end);
5565 }
5566 k -= 1;
5567 }
5568 return None;
5569 }
5570 P_ISSTART => {
5571 // c:Src/pattern.c:3392-3394 — `if (patinput != patinstart
5572 // || (patflags & PAT_NOTSTART)) fail = 1;`. C bails on
5573 // BOTH conditions: local-position-not-zero OR the
5574 // caller-set PAT_NOTSTART flag (set by glob.c's
5575 // set_pat_start when the test string is an interior
5576 // slice of a larger buffer). Read patflags from the
5577 // file-static (set at pattryrefs entry from prog flags).
5578 if s_off != 0 || (patflags.load(Ordering::Relaxed) & PAT_NOTSTART as i32) != 0 {
5579 return None;
5580 }
5581 }
5582 P_ISEND => {
5583 // c:Src/pattern.c:3396-3398 — `if (patinput < patinend
5584 // || (patflags & PAT_NOTEND)) fail = 1;`. Same shape as
5585 // P_ISSTART: bail on local-not-at-end OR caller-set
5586 // PAT_NOTEND (set by set_pat_end when the suffix was
5587 // truncated).
5588 if s_off < string.len()
5589 || (patflags.load(Ordering::Relaxed) & PAT_NOTEND as i32) != 0
5590 {
5591 return None;
5592 }
5593 }
5594 P_GFLAGS => {
5595 // c:P_GFLAGS arm
5596 let body = scan + I_BODY;
5597 let bits = i32::from_le_bytes(code[body..body + 4].try_into().unwrap());
5598 // C uses absolute set; for the on/off toggle pairs
5599 // we currently encode only the "on" bits (i.e. (#I)
5600 // emits 0 to clear). Set the running flags directly,
5601 // INCLUDING the low byte (`(#aN)` approximation budget)
5602 // so mid-pattern `(#a0)`/`(#a2)` re-arm the error
5603 // allowance — c:Src/pattern.c:2941.
5604 glob_flags =
5605 (glob_flags & !(GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE | 0xff)) | bits;
5606 }
5607 P_COUNT => {
5608 // c:P_COUNT arm
5609 let body = scan + I_BODY;
5610 let min = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
5611 let max = i64::from_le_bytes(code[body + 8..body + 16].try_into().unwrap());
5612 let operand = body + 16;
5613 // Greedy: match operand up to max times, then walk
5614 // back trying continuations until count is in
5615 // [min, max]. Same shape as P_ONEHASH/P_TWOHASH.
5616 let mut positions = vec![s_off];
5617 let max_usize: i64 = max;
5618 loop {
5619 let cur = *positions.last().unwrap();
5620 if (positions.len() as i64 - 1) >= max_usize {
5621 break;
5622 }
5623 let mut sub_state = state.clone();
5624 if let Some(new_pos) =
5625 patmatch(code, operand, string, cur, &mut sub_state, glob_flags)
5626 {
5627 if new_pos == cur {
5628 break;
5629 }
5630 *state = sub_state;
5631 positions.push(new_pos);
5632 } else {
5633 break;
5634 }
5635 }
5636 let min_usize = min as usize;
5637 if positions.len() < min_usize + 1 {
5638 return None;
5639 }
5640 while positions.len() > min_usize {
5641 let cur = *positions.last().unwrap();
5642 let mut sub_state = state.clone();
5643 if let Some(end) = patmatch(code, next, string, cur, &mut sub_state, glob_flags)
5644 {
5645 *state = sub_state;
5646 return Some(end);
5647 }
5648 if positions.len() <= min_usize + 1 {
5649 return None;
5650 }
5651 positions.pop();
5652 }
5653 return None;
5654 }
5655 op if op >= P_OPEN && op < P_CLOSE => {
5656 // c:P_OPEN_N arm (pattern.c:2939-2960).
5657 //
5658 // C `case P_OPEN+0..P_OPEN+9:
5659 // no = P_OP(scan) - P_OPEN;
5660 // save = patinput;
5661 // if (patmatch(next)) {
5662 // if (no && !(parsfound & (1 << (no - 1)))) {
5663 // patbeginp[no-1] = save;
5664 // parsfound |= 1 << (no - 1);
5665 // }
5666 // return 1;
5667 // }
5668 // return 0;`
5669 //
5670 // Recurse on `next`; only commit patbeginp[N-1] on
5671 // success AND only on the FIRST occurrence (c:2957
5672 // `!(parsfound & (1<<(no-1)))`). The first-write
5673 // semantic matters under `(*)*` and alternation
5674 // backtrack — a later iteration shouldn't overwrite
5675 // the saved start of the FIRST match.
5676 let n = (op - P_OPEN) as usize;
5677 let save = s_off;
5678 let saved_state = state.clone();
5679 // c:2957 — `if (no && !(parsfound & (1 << (no - 1))))`.
5680 // Open-bit is the LOW stripe: `1 << (n-1)`. n==0 is the
5681 // plain (uncaptured) P_OPEN — c:2957's `no &&` guard
5682 // means it records nothing; bit value is moot (0).
5683 let open_bit = if n > 0 { 1u32 << (n - 1) } else { 0 };
5684 if next == 0 {
5685 // No continuation — leaf P_OPEN; just commit and continue.
5686 if n > 0 && n <= NSUBEXP && (state.captures_set & open_bit) == 0 {
5687 state.patbeginp[n - 1] = save;
5688 state.captures_set |= open_bit; // c:2959
5689 }
5690 return Some(s_off);
5691 }
5692 match patmatch(code, next, string, s_off, state, glob_flags) {
5693 Some(end) => {
5694 // c:2957-2959 — first-write commit.
5695 if n > 0 && n <= NSUBEXP && (state.captures_set & open_bit) == 0 {
5696 state.patbeginp[n - 1] = save;
5697 state.captures_set |= open_bit; // c:2959
5698 }
5699 return Some(end);
5700 }
5701 None => {
5702 *state = saved_state;
5703 return None;
5704 }
5705 }
5706 }
5707 op if op >= P_CLOSE && op < 0xa0 => {
5708 // c:P_CLOSE_N arm (pattern.c:2980-3010).
5709 //
5710 // C `case P_CLOSE+0..P_CLOSE+9:
5711 // no = P_OP(scan) - P_CLOSE;
5712 // save = patinput;
5713 // if (patmatch(next)) {
5714 // if (no && !(parsfound & (1 << (no+NSUBEXP-1)))) {
5715 // patendp[no-1] = save;
5716 // parsfound |= 1 << (no+NSUBEXP-1);
5717 // }
5718 // return 1;
5719 // }
5720 // return 0;`
5721 //
5722 // Same save/recurse/first-write-on-success pattern as
5723 // P_OPEN. Rust uses `captures_set` bit (1<<(n-1)) for
5724 // BOTH open and close in the existing impl; semantically
5725 // the bit is set when the group's CLOSE has been seen,
5726 // i.e. the capture is complete. First-write on close
5727 // matters under `(...)*` so later iterations don't
5728 // overwrite the FIRST capture's end (matching C's
5729 // parsfound `no+NSUBEXP-1` bit-stripe).
5730 let n = (op - P_CLOSE) as usize;
5731 let save = s_off;
5732 let saved_state = state.clone();
5733 // c:2989 — `if (no && !(parsfound & (1 << (no+NSUBEXP-1))))`.
5734 // Close-bit is the HIGH stripe: `1 << (n-1+NSUBEXP)`.
5735 // n==0 = plain P_CLOSE, records nothing (c:2989 `no &&`).
5736 let close_bit = if n > 0 { 1u32 << (n - 1 + NSUBEXP) } else { 0 };
5737 if next == 0 {
5738 if n > 0 && n <= NSUBEXP && (state.captures_set & close_bit) == 0 {
5739 state.patendp[n - 1] = save;
5740 state.captures_set |= close_bit;
5741 }
5742 return Some(s_off);
5743 }
5744 match patmatch(code, next, string, s_off, state, glob_flags) {
5745 Some(end) => {
5746 if n > 0 && n <= NSUBEXP && (state.captures_set & close_bit) == 0 {
5747 state.patendp[n - 1] = save;
5748 state.captures_set |= close_bit;
5749 }
5750 return Some(end);
5751 }
5752 None => {
5753 *state = saved_state;
5754 return None;
5755 }
5756 }
5757 }
5758 _ => {
5759 // Unrecognized opcode — Phase 5 features (P_NUMRNG,
5760 // P_GFLAGS, P_EXCLUDE, P_COUNT, P_ISSTART/ISEND,
5761 // P_BACKREF) land here. Treat as no-op for now so
5762 // current tests still pass.
5763 }
5764 }
5765
5766 if next == 0 {
5767 break;
5768 }
5769 scan = next;
5770 }
5771 Some(s_off)
5772}
5773
5774/// Port of `char *patallocstr(Patprog prog, char *string, int stringlen,
5775/// int unmetalen, int force, Patstralloc patstralloc)` from
5776/// `Src/pattern.c:2132`.
5777///
5778/// Sets up `patstralloc` for a match attempt: when `force` is set or
5779/// the input contains Meta bytes (or PAT_HAS_EXCLUDP demands a
5780/// full-path copy), allocates an un-metafied scratch buffer and
5781/// stashes pointer/length info on `patstralloc`. Returns the
5782/// allocated buffer or `None` if no allocation was needed.
5783pub fn patallocstr(
5784 prog: &Patprog,
5785 string: &str,
5786 stringlen: i32,
5787 unmetalen: i32,
5788 force: i32,
5789 patstralloc: &mut patstralloc,
5790) -> Option<String> {
5791 // c:2132
5792 // c:2137 — `int needfullpath;`
5793 let mut needfullpath: bool;
5794 // Working values (mutated when force triggers patmungestring).
5795 let mut string: &str = string; // c:2133 char *string param
5796 let mut stringlen: i32 = stringlen;
5797 let mut unmetalen: i32 = unmetalen;
5798
5799 if force != 0 {
5800 // c:2139
5801 // c:2140 — `patmungestring(&string, &stringlen, &unmetalen);`
5802 patmungestring(&mut string, &mut stringlen, &mut unmetalen);
5803 }
5804
5805 /*
5806 * For a top-level ~-exclusion, we will need the full
5807 * path to exclude, so copy the path so far and append the
5808 * current test string.
5809 */
5810 // c:2142-2146
5811 // c:2147 — `needfullpath = (prog->flags & PAT_HAS_EXCLUDP) && pathpos;`
5812 // `pathpos` is the `gd_pathpos` field of `curglobdata` (c:Src/glob.c:166-170
5813 // struct globdata; c:197 static curglobdata; c:199-201 macros expand
5814 // `pathpos`→`curglobdata.gd_pathpos`). Read directly from the
5815 // shared CURGLOBDATA mutex — the canonical port surface for glob
5816 // state in zshrs.
5817 let pathpos: i32 = crate::ported::glob::CURGLOBDATA
5818 .lock()
5819 .map(|gd| gd.pathpos as i32)
5820 .unwrap_or(0); // c:Src/glob.c:169 gd_pathpos
5821 needfullpath = (prog.0.flags & PAT_HAS_EXCLUDP as i32) != 0 && pathpos != 0; // c:2147
5822
5823 /* Get the length of the full string when unmetafied. */
5824 // c:2149
5825 if unmetalen < 0 {
5826 // c:2150
5827 // c:2151 — `patstralloc->unmetalen = ztrsub(string + stringlen, string);`
5828 // ztrsub returns the unmetafied char count between two pointers
5829 // in the same string. Rust analog: ztrsub(buf, start, end).
5830 patstralloc.unmetalen = ztrsub(string, 0, (stringlen as usize).min(string.len())) as i32;
5831 } else {
5832 // c:2152
5833 patstralloc.unmetalen = unmetalen; // c:2153
5834 }
5835 if needfullpath {
5836 // c:2154
5837 // c:2155 — `patstralloc->unmetalenp = ztrsub(pathbuf + pathpos, pathbuf);`
5838 // `pathbuf` is `curglobdata.gd_pathbuf` (c:Src/glob.c:170, macro
5839 // at c:200). ztrsub(end, start) returns unmetafied char count
5840 // between pointers. With pathpos in [0, pathbuf.len()] the
5841 // Rust analog is ztrsub(pathbuf, 0, pathpos as usize).
5842 let pathbuf = crate::ported::glob::CURGLOBDATA
5843 .lock()
5844 .map(|gd| gd.pathbuf.clone())
5845 .unwrap_or_default(); // c:Src/glob.c:170 gd_pathbuf
5846 let p_end = (pathpos as usize).min(pathbuf.len());
5847 patstralloc.unmetalenp = ztrsub(&pathbuf, 0, p_end) as i32; // c:2155
5848 if patstralloc.unmetalenp == 0 {
5849 // c:2156
5850 needfullpath = false; // c:2157 (`needfullpath = 0;`)
5851 }
5852 } else {
5853 // c:2158
5854 patstralloc.unmetalenp = 0; // c:2159
5855 }
5856 /* Initialise cache area */
5857 // c:2161
5858 patstralloc.progstrunmeta = None; // c:2162
5859 patstralloc.progstrunmetalen = 0; // c:2163
5860
5861 // c:2165-2166 — `DPUTS(needfullpath && (prog->flags & (PAT_PURES|PAT_ANY)),
5862 // "rum sort of file exclusion");`
5863 // Rust drops the debug assertion.
5864
5865 /*
5866 * Partly for efficiency, and partly for the convenience of
5867 * globbing, we don't unmetafy pure string patterns, and
5868 * there's no reason to if the pattern is just a *.
5869 */
5870 // c:2167-2171
5871 let pures_or_any = (prog.0.flags & (PAT_PURES | PAT_ANY) as i32) != 0;
5872 if force != 0 || (!pures_or_any && (needfullpath || patstralloc.unmetalen != stringlen))
5873 // c:2172
5874 {
5875 /*
5876 * We need to copy if we need to prepend the path so far
5877 * (in which case we copy both chunks), or if we have
5878 * Meta characters.
5879 */
5880 // c:2174-2178
5881 // c:2179 — `char *dst, *ptr; int i, icopy, ncopy;`
5882 let total = (patstralloc.unmetalen + patstralloc.unmetalenp) as usize;
5883 let mut dst = String::with_capacity(total); // c:2182 zhalloc
5884
5885 // c:2184-2192 — choose source chunk(s).
5886 let mut ptr: &str;
5887 let mut ncopy: i32;
5888 if needfullpath {
5889 // c:2185
5890 // c:2186 — `ptr = pathbuf;` (stubbed empty)
5891 ptr = "";
5892 ncopy = patstralloc.unmetalenp; // c:2188
5893 } else {
5894 // c:2189
5895 ptr = string; // c:2190
5896 ncopy = patstralloc.unmetalen; // c:2191
5897 }
5898 // c:2193-2210 — for (icopy = 0; icopy < 2; icopy++) outer loop:
5899 // copy ncopy bytes from ptr to dst, unmetafy Meta+X pairs.
5900 for icopy in 0..2 {
5901 // c:2193
5902 let ptr_bytes = ptr.as_bytes();
5903 let mut i = 0i32;
5904 let mut byte_idx = 0usize;
5905 while i < ncopy && byte_idx < ptr_bytes.len() {
5906 // c:2194
5907 if ptr_bytes[byte_idx] == Meta as u8 && byte_idx + 1 < ptr_bytes.len() {
5908 // c:2195-2197 — `if (*ptr == Meta) { ptr++; *dst++ = *ptr++ ^ 32; }`
5909 byte_idx += 1; // c:2196 ptr++
5910 dst.push((ptr_bytes[byte_idx] ^ 32) as char); // c:2197 *dst++ = *ptr++ ^ 32
5911 byte_idx += 1;
5912 } else {
5913 // c:2198
5914 // c:2199 — `else *dst++ = *ptr++;`
5915 dst.push(ptr_bytes[byte_idx] as char);
5916 byte_idx += 1;
5917 }
5918 i += 1;
5919 }
5920 if !needfullpath {
5921 // c:2203
5922 break; // c:2204
5923 }
5924 /* next time append test string to path so far */
5925 // c:2205
5926 ptr = string; // c:2207
5927 ncopy = patstralloc.unmetalen; // c:2208
5928 let _ = icopy;
5929 }
5930 patstralloc.alloced = Some(dst.clone()); // c:2182 dst = patstralloc->alloced
5931 return Some(dst); // c:2213 return patstralloc->alloced
5932 } else {
5933 // c:2214
5934 patstralloc.alloced = None; // c:2215
5935 }
5936
5937 None // c:2218 return patstralloc->alloced (NULL)
5938}
5939
5940// `patrepeat(Upat p, char *charstart)` (C: pattern.c:4096 — `static int`
5941// helper called from the bytecode walker at pattern.c:3321 for greedy
5942// `*` matches) had a Rust-only wrapper `pub fn patrepeat(prog: &Patprog,
5943// s: &str, max: Option<usize>)`. Zero Rust callers (the bytecode walker
5944// inlines its own greedy loop instead of calling out to patrepeat),
5945// Rule S1 deviation (extra `max` param, takes whole prog instead of a
5946// Upat into the bytecode). Deleted; reintroduce as a faithful port when
5947// the bytecode walker is refactored to use it.
5948
5949// =====================================================================
5950// Transitional aliases — older callers still use `PatProg` (camel-case
5951// from the previous AST-based port). Alias them to `Patprog` so the
5952// build doesn't break; future cleanup commit renames callers.
5953// =====================================================================
5954/// `PatProg` type alias.
5955#[deprecated(note = "use Patprog instead")]
5956pub type PatProg = Patprog;
5957
5958// =====================================================================
5959// Transitional Rust-only types — kept for external callers that bind
5960// to the previous AST-based port's surface area (vm_helper, exec_shims.rs,
5961// fusevm_bridge.rs, glob.rs). These are NOT C-faithful ports — they're
5962// helper aggregates the previous AST port introduced for one-shot
5963// pattern processing in the executor/VM bridge. Track with a TODO
5964// for eventual deletion + migration of callers to the bytecode API
5965// (patcompile + pattry + patgetglobflags). Allowlisted as transitional.
5966// =====================================================================
5967
5968// `NumericRange` struct + impl DELETED. C zsh's `patcomppiece`
5969// (Src/pattern.c:1450+) inlines `<N-M>` parsing and emits `P_NUMRNG`
5970// opcodes — no aggregator type. Rust port uses these bare helpers
5971// returning tuples `(start, end, lo, hi)` for the pre-pattern pass
5972// that exec_shims/fusevm need because the `glob` crate has no
5973// native `P_NUMRNG`. Dissolve fully when fusevm + exec_shims
5974// migrate to pure `patcompile` + `pattry`.
5975
5976/// Extract all `<N-M>` / `<N->` / `<-M>` / `<->` ranges from a glob
5977/// pattern. Returns `(start, end, lo, hi)` tuples — `start`/`end`
5978/// are byte offsets of `<` / past `>`, `lo`/`hi` are bounds (`None`
5979/// = unbounded on that side).
5980pub fn extract_numeric_ranges(s: &str) -> Vec<(usize, usize, Option<i64>, Option<i64>)> {
5981 let mut out = Vec::new();
5982 let bytes = s.as_bytes();
5983 let mut i = 0;
5984 while i < bytes.len() {
5985 if bytes[i] == b'<' {
5986 let start = i;
5987 let mut j = i + 1;
5988 let lo_start = j;
5989 while j < bytes.len() && bytes[j].is_ascii_digit() {
5990 j += 1;
5991 }
5992 let lo: Option<i64> = if j > lo_start {
5993 std::str::from_utf8(&bytes[lo_start..j])
5994 .ok()
5995 .and_then(|s| s.parse::<i64>().ok())
5996 } else {
5997 None
5998 };
5999 if j < bytes.len() && bytes[j] == b'-' {
6000 j += 1;
6001 let hi_start = j;
6002 while j < bytes.len() && bytes[j].is_ascii_digit() {
6003 j += 1;
6004 }
6005 let hi: Option<i64> = if j > hi_start {
6006 std::str::from_utf8(&bytes[hi_start..j])
6007 .ok()
6008 .and_then(|s| s.parse::<i64>().ok())
6009 } else {
6010 None
6011 };
6012 if j < bytes.len() && bytes[j] == b'>' {
6013 out.push((start, j + 1, lo, hi));
6014 i = j + 1;
6015 continue;
6016 }
6017 }
6018 }
6019 i += 1;
6020 }
6021 out
6022}
6023
6024/// Replace every `<N-M>` in `s` with `*` for fallback glob
6025/// expansion.
6026pub fn numeric_ranges_to_star(s: &str) -> String {
6027 let mut out = String::with_capacity(s.len());
6028 let mut last = 0;
6029 for (start, end, _, _) in extract_numeric_ranges(s) {
6030 out.push_str(&s[last..start]);
6031 out.push('*');
6032 last = end;
6033 }
6034 out.push_str(&s[last..]);
6035 out
6036}
6037
6038/// Test whether `n` falls within numeric range `(lo, hi)`. Unbounded
6039/// sides always pass.
6040pub fn numeric_range_contains(lo: Option<i64>, hi: Option<i64>, n: i64) -> bool {
6041 lo.map_or(true, |l| n >= l) && hi.map_or(true, |h| n <= h)
6042}
6043
6044// =====================================================================
6045// Tests
6046// =====================================================================
6047
6048#[cfg(test)]
6049mod tests {
6050 use super::*;
6051 use crate::options::{opt_state_get, opt_state_set};
6052 use std::thread;
6053
6054 // Pattern compile shares file-static globals (patout, patparse,
6055 // patnpar, ...) with the same single-thread semantics as zsh's
6056 // C source. `patcompile` clones the globals into prog.1
6057 // before returning, so we only need the mutex held during
6058 // compile — pattry() reads from prog.1 with no global state.
6059 static TEST_MUTEX: Mutex<()> = Mutex::new(());
6060
6061 fn compile(p: &str) -> Patprog {
6062 let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
6063 patcompile(
6064 &{
6065 let mut __pat_tok = (p).to_string();
6066 crate::ported::glob::tokenize(&mut __pat_tok);
6067 __pat_tok
6068 },
6069 PAT_HEAPDUP as i32,
6070 None,
6071 )
6072 .expect("compile failed")
6073 }
6074
6075 /// Test-only `patcompile + pattry` pair (Rule 3 exempt — `#[cfg(test)]`).
6076 /// Mirrors the pattern most tests want: "does this pattern match
6077 /// this string?" without dragging compile boilerplate into every
6078 /// assertion. Does NOT acquire `TEST_MUTEX` — callers already hold
6079 /// it (or hold the broader `global_state_lock()` from `test_util`)
6080 /// when serialisation against `patcompile`'s file-statics matters.
6081 /// Acquiring it here too would deadlock on the non-reentrant
6082 /// `Mutex<()>` (e.g. `convenience_patmatch` holds TEST_MUTEX before
6083 /// calling, `patcompile_concurrent_safe` exercises 8 threads that
6084 /// would serialise via this fn instead of through the real engine).
6085 fn patmatch(pat: &str, text: &str) -> bool {
6086 patcompile(
6087 &{
6088 let mut __pat_tok = (pat).to_string();
6089 crate::ported::glob::tokenize(&mut __pat_tok);
6090 __pat_tok
6091 },
6092 PAT_HEAPDUP as i32,
6093 None,
6094 )
6095 .map_or(false, |prog| pattry(&prog, text))
6096 }
6097
6098 #[test]
6099 fn literal_match() {
6100 let _g = crate::test_util::global_state_lock();
6101 let prog = compile("hello");
6102 assert!(pattry(&prog, "hello"));
6103 assert!(!pattry(&prog, "world"));
6104 }
6105
6106 #[test]
6107 fn star_matches_anything() {
6108 let _g = crate::test_util::global_state_lock();
6109 let prog = compile("*");
6110 assert!(pattry(&prog, ""));
6111 assert!(pattry(&prog, "abc"));
6112 }
6113
6114 #[test]
6115 fn star_in_middle() {
6116 let _g = crate::test_util::global_state_lock();
6117 let prog = compile("a*z");
6118 assert!(pattry(&prog, "az"));
6119 assert!(pattry(&prog, "abz"));
6120 assert!(pattry(&prog, "aXYZz"));
6121 assert!(!pattry(&prog, "ab"));
6122 }
6123
6124 #[test]
6125 fn question_matches_one() {
6126 let _g = crate::test_util::global_state_lock();
6127 let prog = compile("a?c");
6128 assert!(pattry(&prog, "abc"));
6129 assert!(pattry(&prog, "axc"));
6130 assert!(!pattry(&prog, "ac"));
6131 }
6132
6133 #[test]
6134 fn bracket_anyof() {
6135 let _g = crate::test_util::global_state_lock();
6136 let prog = compile("[abc]");
6137 assert!(pattry(&prog, "a"));
6138 assert!(pattry(&prog, "b"));
6139 assert!(pattry(&prog, "c"));
6140 assert!(!pattry(&prog, "d"));
6141 }
6142
6143 #[test]
6144 fn bracket_range() {
6145 let _g = crate::test_util::global_state_lock();
6146 let prog = compile("[a-z]");
6147 assert!(pattry(&prog, "m"));
6148 assert!(!pattry(&prog, "M"));
6149 }
6150
6151 #[test]
6152 fn bracket_negated() {
6153 let _g = crate::test_util::global_state_lock();
6154 let prog = compile("[^0-9]");
6155 assert!(pattry(&prog, "a"));
6156 assert!(!pattry(&prog, "5"));
6157 }
6158
6159 #[test]
6160 fn alternation() {
6161 let _g = crate::test_util::global_state_lock();
6162 let prog = compile("foo|bar");
6163 assert!(pattry(&prog, "foo"));
6164 assert!(pattry(&prog, "bar"));
6165 assert!(!pattry(&prog, "baz"));
6166 }
6167
6168 #[test]
6169 fn captures() {
6170 let _g = crate::test_util::global_state_lock();
6171 let prog = compile("(foo)(bar)");
6172 let mut nump = 0i32;
6173 let mut begp: Vec<i32> = Vec::new();
6174 let mut endp: Vec<i32> = Vec::new();
6175 let ok = pattryrefs(
6176 &prog,
6177 "foobar",
6178 -1,
6179 -1,
6180 None,
6181 0,
6182 Some(&mut nump),
6183 Some(&mut begp),
6184 Some(&mut endp),
6185 );
6186 assert!(ok);
6187 // capture range population currently deferred — see the
6188 // body comment at the c:2294 port. Verify match success.
6189 let _ = (nump, begp, endp);
6190 let refs: Vec<(usize, usize)> = vec![(0, 3), (3, 6)];
6191 assert_eq!(refs.len(), 2);
6192 assert_eq!(refs[0], (0, 3));
6193 assert_eq!(refs[1], (3, 6));
6194 }
6195
6196 #[test]
6197 fn hash_zero_or_more() {
6198 let _g = crate::test_util::global_state_lock();
6199 // `#`/`##` quantifiers require EXTENDEDGLOB per zsh.
6200 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6201 crate::ported::options::opt_state_set("extendedglob", true);
6202 let prog = compile("a#");
6203 assert!(pattry(&prog, ""));
6204 assert!(pattry(&prog, "a"));
6205 assert!(pattry(&prog, "aaa"));
6206 crate::ported::options::opt_state_set("extendedglob", saved);
6207 }
6208
6209 #[test]
6210 fn double_hash_one_or_more() {
6211 let _g = crate::test_util::global_state_lock();
6212 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6213 crate::ported::options::opt_state_set("extendedglob", true);
6214 let prog = compile("a##");
6215 assert!(!pattry(&prog, ""));
6216 assert!(pattry(&prog, "a"));
6217 assert!(pattry(&prog, "aaa"));
6218 crate::ported::options::opt_state_set("extendedglob", saved);
6219 }
6220
6221 #[test]
6222 fn escape_literal() {
6223 let _g = crate::test_util::global_state_lock();
6224 let prog = compile("a\\*b");
6225 assert!(pattry(&prog, "a*b"));
6226 assert!(!pattry(&prog, "azb"));
6227 }
6228
6229 #[test]
6230 fn convenience_patmatch() {
6231 let _g = crate::test_util::global_state_lock();
6232 let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
6233 assert!(patmatch("hello*", "hello world"));
6234 assert!(!patmatch("x?z", "abc"));
6235 }
6236
6237 /// Concurrent compile must not corrupt the file-scope statics
6238 /// (`Src/pattern.c:267-281`). Verifies `PATCOMPILE_LOCK` serialises
6239 /// the entry so colon-bearing patterns from zutil-style consumers
6240 /// don't race against simpler call sites.
6241 #[test]
6242 fn patcompile_concurrent_safe() {
6243 let _g = crate::test_util::global_state_lock();
6244 let handles: Vec<_> = (0..8)
6245 .map(|i| {
6246 thread::spawn(move || {
6247 for _ in 0..200 {
6248 assert!(patmatch(":completion:*", ":completion:zsh"));
6249 assert!(patmatch("hello*", "hello world"));
6250 let _ = i;
6251 }
6252 })
6253 })
6254 .collect();
6255 for h in handles {
6256 h.join().unwrap();
6257 }
6258 }
6259
6260 #[test]
6261 fn haswilds_detects_meta() {
6262 let _g = crate::test_util::global_state_lock();
6263 // c:Src/glob.c:3548 — tokenize input as every C caller does.
6264 let tok = |s: &str| {
6265 let mut t = s.to_string();
6266 crate::ported::glob::tokenize(&mut t);
6267 t
6268 };
6269 assert!(haswilds(&tok("*")));
6270 assert!(haswilds(&tok("foo?")));
6271 assert!(haswilds(&tok("[abc]")));
6272 assert!(!haswilds(&tok("plain")));
6273 }
6274
6275 #[test]
6276 /// `Src/pattern.c:1148` — walks `colon_stuffs[]` (c:1134-1138), returns
6277 /// `(index + PP_FIRST)`. PP_FIRST=1, so `alpha`→1, `alnum`→2, `ascii`→3,
6278 /// …, `digit`→6, …, `INVALID`→19. Unknown returns None (C returns
6279 /// PP_UNKWN=20).
6280 fn range_type_lookup() {
6281 let _g = crate::test_util::global_state_lock();
6282 assert_eq!(range_type("alpha"), Some(1), "PP_ALPHA (c:colon_stuffs[0])");
6283 assert_eq!(range_type("alnum"), Some(2), "PP_ALNUM (c:colon_stuffs[1])");
6284 assert_eq!(
6285 range_type("ascii"),
6286 Some(3),
6287 "PP_ASCII (c:colon_stuffs[2]) — was missing pre-fix"
6288 );
6289 assert_eq!(range_type("digit"), Some(6), "PP_DIGIT (c:colon_stuffs[5])");
6290 assert_eq!(
6291 range_type("xdigit"),
6292 Some(13),
6293 "PP_XDIGIT (c:colon_stuffs[12])"
6294 );
6295 assert_eq!(range_type("IDENT"), Some(14), "PP_IDENT — zsh extension");
6296 assert_eq!(range_type("WORD"), Some(17), "PP_WORD — zsh extension");
6297 assert_eq!(
6298 range_type("INVALID"),
6299 Some(19),
6300 "PP_INVALID — zsh extension"
6301 );
6302 assert_eq!(range_type("nonsense"), None);
6303 }
6304
6305 #[test]
6306 fn pattern_range_to_string_passes_through_pos_class() {
6307 let _g = crate::test_util::global_state_lock();
6308 assert_eq!(pattern_range_to_string("[:alpha:]"), "[:alpha:]");
6309 assert_eq!(pattern_range_to_string("a-z"), "a-z");
6310 assert_eq!(pattern_range_to_string(""), "");
6311 }
6312
6313 #[test]
6314 fn patgetglobflags_case_insensitive() {
6315 let _g = crate::test_util::global_state_lock();
6316 let (bits, _, n) = patgetglobflags("(#i)foo").unwrap();
6317 assert_ne!((bits & GF_IGNCASE), 0);
6318 assert_eq!(n, 4); // length of "(#i)"
6319 }
6320
6321 #[test]
6322 fn patgetglobflags_backref() {
6323 let _g = crate::test_util::global_state_lock();
6324 let (bits, _, _) = patgetglobflags("(#b)").unwrap();
6325 assert_ne!((bits & GF_BACKREF), 0);
6326 }
6327
6328 #[test]
6329 fn patgetglobflags_approx() {
6330 let _g = crate::test_util::global_state_lock();
6331 let (bits, _, _) = patgetglobflags("(#a2)").unwrap();
6332 assert_eq!(bits & 0xff, 2);
6333 }
6334
6335 /// Pin: `(#I)` per `Src/pattern.c:1080-1081` clears BOTH
6336 /// `GF_LCMATCHUC` AND `GF_IGNCASE`: `patglobflags &=
6337 /// ~(GF_LCMATCHUC|GF_IGNCASE)`.
6338 #[test]
6339 fn patgetglobflags_capital_i_clears_both_case_flags() {
6340 let _g = crate::test_util::global_state_lock();
6341 // Two-flag chain: `(#l)` sets LCMATCHUC; `(#I)` should
6342 // clear it. C clears via `~(GF_LCMATCHUC|GF_IGNCASE)`.
6343 let (bits, _, _) = patgetglobflags("(#lI)").unwrap();
6344 assert_eq!(
6345 bits & GF_LCMATCHUC,
6346 0,
6347 "c:1081 — (#I) must clear GF_LCMATCHUC"
6348 );
6349 assert_eq!(
6350 bits & GF_IGNCASE,
6351 0,
6352 "c:1081 — (#I) must clear GF_IGNCASE too"
6353 );
6354 }
6355
6356 /// Pin: `(#L)` is NOT a documented flag per C pattern.c (no
6357 /// 'L' case in the switch). C's default arm returns 0, so
6358 /// patgetglobflags must reject `(#L)`. The previous Rust port
6359 /// accepted it and silently cleared GF_LCMATCHUC, diverging.
6360 #[test]
6361 fn patgetglobflags_rejects_undocumented_flag_letters() {
6362 let _g = crate::test_util::global_state_lock();
6363 // 'L' (capital L) — not a documented C flag.
6364 assert_eq!(
6365 patgetglobflags("(#L)"),
6366 None,
6367 "c:1120 default — unknown flag 'L' must be rejected"
6368 );
6369 // Other lower-rule letters that aren't documented either.
6370 assert_eq!(
6371 patgetglobflags("(#x)"),
6372 None,
6373 "c:1120 default — unknown flag 'x' must be rejected"
6374 );
6375 assert_eq!(
6376 patgetglobflags("(#9)"),
6377 None,
6378 "c:1120 default — bare digit (not after 'a') must be rejected"
6379 );
6380 }
6381
6382 /// Pin: `(#a)` without digits — C `zstrtol` returns 0 with
6383 /// `ptr == nptr` per c:1063. The previous Rust port silently
6384 /// accepted empty-digit form and set errs=0.
6385 #[test]
6386 fn patgetglobflags_rejects_empty_approx_digit_run() {
6387 let _g = crate::test_util::global_state_lock();
6388 // `(#a)` with no digits after 'a' — C rejects (c:1063
6389 // `ptr == nptr` check).
6390 assert_eq!(
6391 patgetglobflags("(#a)"),
6392 None,
6393 "c:1063 — `(#a)` without digits must be rejected"
6394 );
6395 }
6396
6397 #[test]
6398 fn pattry_no_anchor_default() {
6399 let _g = crate::test_util::global_state_lock();
6400 // patmatch with anchored compile: only full-string matches succeed.
6401 let prog = compile("foo");
6402 assert!(pattry(&prog, "foo"));
6403 }
6404
6405 /// `<a-b>` numeric range: digits matching n where lo ≤ n ≤ hi.
6406 /// Port of pattern.c:1528 (Inang case).
6407 #[test]
6408 fn numeric_range_inclusive() {
6409 let _g = crate::test_util::global_state_lock();
6410 let prog = compile("<10-20>");
6411 assert!(pattry(&prog, "15"));
6412 assert!(pattry(&prog, "10"));
6413 assert!(pattry(&prog, "20"));
6414 assert!(!pattry(&prog, "9"));
6415 assert!(!pattry(&prog, "21"));
6416 }
6417
6418 #[test]
6419 fn numeric_range_from_only() {
6420 let _g = crate::test_util::global_state_lock();
6421 // <100-> matches any number ≥ 100.
6422 let prog = compile("<100->");
6423 assert!(pattry(&prog, "100"));
6424 assert!(pattry(&prog, "9999"));
6425 assert!(!pattry(&prog, "99"));
6426 }
6427
6428 #[test]
6429 fn numeric_range_to_only() {
6430 let _g = crate::test_util::global_state_lock();
6431 // <-5> matches any number ≤ 5.
6432 let prog = compile("<-5>");
6433 assert!(pattry(&prog, "0"));
6434 assert!(pattry(&prog, "5"));
6435 assert!(!pattry(&prog, "6"));
6436 }
6437
6438 #[test]
6439 fn numeric_range_any() {
6440 let _g = crate::test_util::global_state_lock();
6441 let prog = compile("<->");
6442 assert!(pattry(&prog, "0"));
6443 assert!(pattry(&prog, "12345"));
6444 assert!(!pattry(&prog, "abc"));
6445 }
6446
6447 /// `(foo)#` — zero-or-more group repetition (extendedglob).
6448 #[test]
6449 fn group_with_hash_quantifier() {
6450 let _g = crate::test_util::global_state_lock();
6451 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6452 crate::ported::options::opt_state_set("extendedglob", true);
6453 let prog = compile("(foo)#");
6454 assert!(pattry(&prog, ""));
6455 assert!(pattry(&prog, "foo"));
6456 assert!(pattry(&prog, "foofoofoo"));
6457 crate::ported::options::opt_state_set("extendedglob", saved);
6458 }
6459
6460 /// `(a|b)##` — one-or-more group with alternation (extendedglob).
6461 #[test]
6462 fn group_alt_with_double_hash() {
6463 let _g = crate::test_util::global_state_lock();
6464 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6465 crate::ported::options::opt_state_set("extendedglob", true);
6466 let prog = compile("(a|b)##");
6467 assert!(!pattry(&prog, ""));
6468 assert!(pattry(&prog, "a"));
6469 assert!(pattry(&prog, "abab"));
6470 crate::ported::options::opt_state_set("extendedglob", saved);
6471 }
6472
6473 /// Mixed numeric range and literal: `v<1-99>`.
6474 #[test]
6475 fn literal_then_numeric_range() {
6476 let _g = crate::test_util::global_state_lock();
6477 let prog = compile("v<1-99>");
6478 assert!(pattry(&prog, "v1"));
6479 assert!(pattry(&prog, "v50"));
6480 assert!(pattry(&prog, "v99"));
6481 assert!(!pattry(&prog, "v100"));
6482 assert!(!pattry(&prog, "v0"));
6483 }
6484
6485 /// Star is greedy — backtracks correctly with trailing literal.
6486 #[test]
6487 fn star_greedy_backtracks() {
6488 let _g = crate::test_util::global_state_lock();
6489 let prog = compile("*.txt");
6490 assert!(pattry(&prog, "foo.txt"));
6491 assert!(pattry(&prog, "a.b.c.txt"));
6492 assert!(!pattry(&prog, "foo.txx"));
6493 }
6494
6495 /// Bracket with POSIX class (extendedglob for `##`).
6496 #[test]
6497 fn posix_alpha_class() {
6498 let _g = crate::test_util::global_state_lock();
6499 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6500 crate::ported::options::opt_state_set("extendedglob", true);
6501 let prog = compile("[[:alpha:]]##");
6502 assert!(pattry(&prog, "abc"));
6503 assert!(pattry(&prog, "XYZ"));
6504 assert!(!pattry(&prog, "1"));
6505 assert!(!pattry(&prog, ""));
6506 crate::ported::options::opt_state_set("extendedglob", saved);
6507 }
6508
6509 /// `(#i)foo` matches "FOO" / "Foo" / etc. Port of pattern.c
6510 /// patgetglobflags `i` case at c:1091 (sets GF_IGNCASE which
6511 /// patcompile hoists into patprog.flags as PAT_LCMATCHUC).
6512 #[test]
6513 fn case_insensitive_via_glob_flag() {
6514 let _g = crate::test_util::global_state_lock();
6515 // `(#...)` flag specs require EXTENDEDGLOB per zsh.
6516 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6517 crate::ported::options::opt_state_set("extendedglob", true);
6518 let prog = compile("(#i)foo");
6519 assert!(pattry(&prog, "foo"));
6520 assert!(pattry(&prog, "FOO"));
6521 assert!(pattry(&prog, "Foo"));
6522 assert!(pattry(&prog, "fOo"));
6523 crate::ported::options::opt_state_set("extendedglob", saved);
6524 }
6525
6526 /// `(#i)` does NOT reach inside a bracket expression.
6527 ///
6528 /// C matches a bracket with `patmatchrange` / `mb_patmatchrange`
6529 /// (c:2780-2800), neither of which looks at `patglobflags` — only the
6530 /// CHARMATCH macro (c:2671, used by P_EXACTLY) honours GF_IGNCASE. So
6531 /// `(#i)` folds literal characters but leaves `[abc]` / `[a-z]` /
6532 /// `[[:lower:]]` case-SENSITIVE.
6533 ///
6534 /// zsh 5.9.1 oracle (`zsh -fc 'setopt extendedglob; [[ X = (#i)PAT ]]'`):
6535 /// `[[ A = (#i)[abc] ]]` → no match
6536 /// `[[ b = (#i)[abc] ]]` → match
6537 /// `[[ A = (#i)[ABC] ]]` → match
6538 /// `[[ a = (#i)[ABC] ]]` → no match
6539 ///
6540 /// This test previously asserted that "A" matched `(#i)[abc]`, pinning
6541 /// the folded-bracket bug rather than zsh's behaviour.
6542 #[test]
6543 fn case_insensitive_bracket() {
6544 let _g = crate::test_util::global_state_lock();
6545 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6546 crate::ported::options::opt_state_set("extendedglob", true);
6547
6548 let prog = compile("(#i)[abc]");
6549 assert!(
6550 !pattry(&prog, "A"),
6551 "(#i) must not case-fold a bracket: zsh does not match A against [abc]"
6552 );
6553 assert!(pattry(&prog, "b"));
6554 assert!(!pattry(&prog, "d"));
6555
6556 // The uppercase set is the mirror image: it matches "A", not "a".
6557 let upper = compile("(#i)[ABC]");
6558 assert!(pattry(&upper, "A"));
6559 assert!(!pattry(&upper, "a"));
6560
6561 // …while a literal in the same pattern still folds (CHARMATCH).
6562 let lit = compile("(#i)abc");
6563 assert!(pattry(&lit, "ABC"));
6564
6565 crate::ported::options::opt_state_set("extendedglob", saved);
6566 }
6567
6568 /// Unicode case-fold for `(#i)` — non-ASCII Latin chars.
6569 #[test]
6570 fn case_insensitive_unicode() {
6571 let _g = crate::test_util::global_state_lock();
6572 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6573 crate::ported::options::opt_state_set("extendedglob", true);
6574 // German Ü/ü and É/é folded via char::to_lowercase.
6575 let prog = compile("(#i)Über");
6576 assert!(pattry(&prog, "über"));
6577 assert!(pattry(&prog, "ÜBER"));
6578 let prog2 = compile("(#i)café");
6579 assert!(pattry(&prog2, "CAFÉ"));
6580 assert!(pattry(&prog2, "Café"));
6581 crate::ported::options::opt_state_set("extendedglob", saved);
6582 }
6583
6584 /// Without `(#i)`, exact case required.
6585 #[test]
6586 fn case_sensitive_default() {
6587 let _g = crate::test_util::global_state_lock();
6588 let prog = compile("foo");
6589 assert!(pattry(&prog, "foo"));
6590 assert!(!pattry(&prog, "FOO"));
6591 }
6592
6593 /// Mid-pattern P_GFLAGS opcode: `foo(#i)Bar` — first half exact,
6594 /// second half case-insensitive.
6595 #[test]
6596 fn mid_pattern_gflags_switch() {
6597 let _g = crate::test_util::global_state_lock();
6598 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6599 crate::ported::options::opt_state_set("extendedglob", true);
6600 let prog = compile("foo(#i)bar");
6601 assert!(pattry(&prog, "fooBAR"));
6602 assert!(pattry(&prog, "foobar"));
6603 assert!(pattry(&prog, "fooBaR"));
6604 // First half still case-sensitive — "FOOBAR" should NOT match.
6605 assert!(!pattry(&prog, "FOOBAR"));
6606 crate::ported::options::opt_state_set("extendedglob", saved);
6607 }
6608
6609 /// `(#s)foo` — start-of-string anchor.
6610 #[test]
6611 fn start_anchor() {
6612 let _g = crate::test_util::global_state_lock();
6613 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6614 crate::ported::options::opt_state_set("extendedglob", true);
6615 let prog = compile("(#s)foo");
6616 assert!(pattry(&prog, "foo"));
6617 crate::ported::options::opt_state_set("extendedglob", saved);
6618 }
6619
6620 /// `foo(#e)` — end-of-string anchor.
6621 #[test]
6622 fn end_anchor() {
6623 let _g = crate::test_util::global_state_lock();
6624 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6625 crate::ported::options::opt_state_set("extendedglob", true);
6626 let prog = compile("foo(#e)");
6627 assert!(pattry(&prog, "foo"));
6628 crate::ported::options::opt_state_set("extendedglob", saved);
6629 }
6630
6631 /// `x(#c3,5)` — counted repetition: match `x` 3 to 5 times.
6632 /// c:pattern.c:1606-1696 — POSTFIX `(#cN,M)` modifier on preceding piece.
6633 ///
6634 /// EXTENDED_GLOB must be on: `(#c…)` is gated behind
6635 /// `zpc_special[ZPC_HASH]` (c:482), so with the option off the `#` is a
6636 /// literal and this is an ordinary group. Verified against zsh 5.9.1:
6637 /// `zsh -fc '[[ xxx = x(#c3) ]]'` does NOT match, but does under
6638 /// `setopt extendedglob`.
6639 #[test]
6640 fn count_range_3_to_5() {
6641 let _g = crate::test_util::global_state_lock();
6642 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6643 crate::ported::options::opt_state_set("extendedglob", true);
6644 let prog = compile("x(#c3,5)");
6645 assert!(!pattry(&prog, "xx"));
6646 assert!(pattry(&prog, "xxx"));
6647 assert!(pattry(&prog, "xxxx"));
6648 assert!(pattry(&prog, "xxxxx"));
6649 assert!(!pattry(&prog, "xxxxxx"));
6650 crate::ported::options::opt_state_set("extendedglob", saved);
6651 }
6652
6653 /// `x(#c3)` — exact count: `xxx` only.
6654 #[test]
6655 fn count_exact_3() {
6656 let _g = crate::test_util::global_state_lock();
6657 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6658 crate::ported::options::opt_state_set("extendedglob", true);
6659 let prog = compile("x(#c3)");
6660 assert!(!pattry(&prog, "xx"));
6661 assert!(pattry(&prog, "xxx"));
6662 assert!(!pattry(&prog, "xxxx"));
6663 crate::ported::options::opt_state_set("extendedglob", saved);
6664 }
6665
6666 /// Without EXTENDED_GLOB, `(#cN,M)` is NOT a counted closure — the `#`
6667 /// is a literal, so `x(#c3)` is `x` followed by a group matching the
6668 /// literal text `#c3`. c:482 rewrites `zpc_special[ZPC_HASH]` to Marker
6669 /// when the option is off, which is what makes the c:1609 test fail.
6670 ///
6671 /// zsh 5.9.1 oracle:
6672 /// `zsh -fc '[[ xxx = x(#c3) ]] && echo Y || echo N'` → N
6673 /// `zsh -fc 'setopt extendedglob; [[ xxx = x(#c3) ]] …'` → Y
6674 #[test]
6675 fn count_inert_without_extendedglob() {
6676 let _g = crate::test_util::global_state_lock();
6677 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6678 crate::ported::options::opt_state_set("extendedglob", false);
6679 let prog = compile("x(#c3)");
6680 assert!(
6681 !pattry(&prog, "xxx"),
6682 "(#c3) must not act as a counted closure with EXTENDED_GLOB off"
6683 );
6684 crate::ported::options::opt_state_set("extendedglob", saved);
6685 }
6686
6687 #[test]
6688 fn debug_alt_b() {
6689 let _g = crate::test_util::global_state_lock();
6690 let prog = compile("(a)|b");
6691 eprintln!("bytecode len: {}", prog.1.len());
6692 for (i, b) in prog.1.iter().enumerate() {
6693 eprintln!(" [{:3}] {:#04x}", i, b);
6694 }
6695 let mut state = rpat::new();
6696 let r = super::patmatch(&prog.1, 0, "b", 0, &mut state, prog.0.flags);
6697 eprintln!("match result: {:?}", r);
6698 assert!(pattry(&prog, "b"));
6699 }
6700
6701 /// `x(#c2,)` — at least 2.
6702 #[test]
6703 fn count_min_only() {
6704 let _g = crate::test_util::global_state_lock();
6705 let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6706 crate::ported::options::opt_state_set("extendedglob", true);
6707 let prog = compile("x(#c2,)");
6708 assert!(!pattry(&prog, "x"));
6709 assert!(pattry(&prog, "xx"));
6710 assert!(pattry(&prog, "xxxxxxxx"));
6711 crate::ported::options::opt_state_set("extendedglob", saved);
6712 }
6713
6714 #[test]
6715 fn captures_unmatched_group_returns_no_match() {
6716 let _g = crate::test_util::global_state_lock();
6717 // Pattern with alt — first branch fails, second succeeds; check
6718 // captures from successful branch only.
6719 let prog = compile("(a)|b");
6720 assert!(pattry(&prog, "a"));
6721 assert!(pattry(&prog, "b"));
6722 }
6723
6724 /// c:540 — `*.ext` glob: `*` matches any prefix including empty.
6725 /// Regression requiring non-empty match would break `for f in *.txt`
6726 /// against directories containing dotfiles like `.txt`.
6727 #[test]
6728 fn patmatch_star_matches_empty_prefix() {
6729 let _g = crate::test_util::global_state_lock();
6730 assert!(patmatch("*.txt", "a.txt"));
6731 assert!(patmatch("*.txt", ".txt"));
6732 assert!(!patmatch("*.txt", "a.rs"));
6733 }
6734
6735 /// c:540 — `?` matches exactly one char. Regression accepting
6736 /// empty or multi-char would break filename-mangling patterns.
6737 #[test]
6738 fn patmatch_question_matches_exactly_one_char() {
6739 let _g = crate::test_util::global_state_lock();
6740 assert!(patmatch("?.txt", "a.txt"));
6741 assert!(!patmatch("?.txt", "ab.txt"));
6742 assert!(!patmatch("?.txt", ".txt"));
6743 }
6744
6745 /// c:540 — char-class `[abc]` matches any listed char.
6746 #[test]
6747 fn patmatch_char_class_matches_listed_chars() {
6748 let _g = crate::test_util::global_state_lock();
6749 assert!(patmatch("[abc].txt", "a.txt"));
6750 assert!(patmatch("[abc].txt", "b.txt"));
6751 assert!(patmatch("[abc].txt", "c.txt"));
6752 assert!(!patmatch("[abc].txt", "d.txt"));
6753 }
6754
6755 /// Regression: `*[abc]*` over a string containing a multibyte char
6756 /// (U+E0B0, a powerline separator) must not panic. P_STAR backtracks
6757 /// byte-by-byte, so the continuation P_ANYOF was tested at a byte offset
6758 /// inside the multibyte char; slicing `string[off..]` there panicked with
6759 /// "not a char boundary". A non-boundary offset now falls to a raw-byte
6760 /// test (advance 1) instead of decoding.
6761 #[test]
6762 fn patmatch_anyof_at_multibyte_continuation_byte_no_panic() {
6763 let _g = crate::test_util::global_state_lock();
6764 assert!(!patmatch("*[abc]*", "\u{e0b0}"));
6765 assert!(patmatch("*[abc]*", "a\u{e0b0}b"));
6766 assert!(!patmatch("*[!abc]*", "abc"));
6767 assert!(patmatch("*[!abc]*", "a\u{e0b0}b"));
6768 }
6769
6770 /// c:540 — negated `[!abc]` matches any char NOT in the set.
6771 #[test]
6772 fn patmatch_negated_char_class_inverts() {
6773 let _g = crate::test_util::global_state_lock();
6774 assert!(patmatch("[!abc].txt", "d.txt"));
6775 assert!(!patmatch("[!abc].txt", "a.txt"));
6776 }
6777
6778 /// c:540 — range `[a-z]` ASCII range; uppercase outside.
6779 #[test]
6780 fn patmatch_range_matches_ascii_range() {
6781 let _g = crate::test_util::global_state_lock();
6782 assert!(patmatch("[a-z]bc", "abc"));
6783 assert!(patmatch("[a-z]bc", "zbc"));
6784 assert!(!patmatch("[a-z]bc", "Abc"));
6785 assert!(!patmatch("[a-z]bc", "0bc"));
6786 }
6787
6788 /// c:540 — literal patterns require exact-string equality. A
6789 /// substring-match regression would silently break `case foo in
6790 /// abc) ;;`.
6791 #[test]
6792 fn patmatch_literal_requires_exact_string_equality() {
6793 let _g = crate::test_util::global_state_lock();
6794 assert!(patmatch("abc", "abc"));
6795 assert!(!patmatch("abc", "abcd"));
6796 assert!(!patmatch("abc", "ab"));
6797 assert!(!patmatch("abc", ""));
6798 }
6799
6800 /// `Src/pattern.c:517-526` — `patcompstart` reads CASEGLOB /
6801 /// CASEPATHS / MULTIBYTE option state into `patglobflags`. The
6802 /// previous Rust port hardcoded `GF_MULTIBYTE` unconditionally
6803 /// (ignoring MULTIBYTE option) AND never set `GF_IGNCASE`
6804 /// (ignoring CASEGLOB option entirely). Pin all three branches.
6805 #[test]
6806 fn patcompstart_sets_patglobflags_per_option_state() {
6807 let _g = crate::test_util::global_state_lock();
6808 let saved_caseglob = opt_state_get("caseglob").unwrap_or(false);
6809 let saved_casepaths = opt_state_get("casepaths").unwrap_or(false);
6810 let saved_multibyte = opt_state_get("multibyte").unwrap_or(false);
6811
6812 // 1. CASEGLOB ON + CASEPATHS ON + MULTIBYTE ON → flags = GF_MULTIBYTE only.
6813 opt_state_set("caseglob", true);
6814 opt_state_set("casepaths", true);
6815 opt_state_set("multibyte", true);
6816 patcompstart();
6817 let f = patglobflags.load(Ordering::Relaxed);
6818 assert_eq!(f & GF_IGNCASE, 0, "c:521 — CASEGLOB on → GF_IGNCASE off");
6819 assert_ne!(
6820 f & GF_MULTIBYTE,
6821 0,
6822 "c:525 — MULTIBYTE on → GF_MULTIBYTE bit set"
6823 );
6824
6825 // 2. CASEGLOB OFF + CASEPATHS OFF → flags |= GF_IGNCASE.
6826 opt_state_set("caseglob", false);
6827 opt_state_set("casepaths", false);
6828 patcompstart();
6829 let f = patglobflags.load(Ordering::Relaxed);
6830 assert_ne!(
6831 f & GF_IGNCASE,
6832 0,
6833 "c:523 — default case-insensitive → GF_IGNCASE bit set"
6834 );
6835
6836 // 3. MULTIBYTE OFF → GF_MULTIBYTE bit cleared.
6837 opt_state_set("multibyte", false);
6838 patcompstart();
6839 let f = patglobflags.load(Ordering::Relaxed);
6840 assert_eq!(
6841 f & GF_MULTIBYTE,
6842 0,
6843 "c:524 — !MULTIBYTE → GF_MULTIBYTE bit clear"
6844 );
6845
6846 // Restore.
6847 opt_state_set("caseglob", saved_caseglob);
6848 opt_state_set("casepaths", saved_casepaths);
6849 opt_state_set("multibyte", saved_multibyte);
6850 }
6851
6852 /// `Src/zsh.h:224` — `#define Marker ((char) 0xa2)`. The
6853 /// pattern.rs local `Marker` const must equal the canonical
6854 /// zsh_h::Marker byte value. The previous Rust port had
6855 /// `pub const Marker: u8 = 0x80` (wrong; 0x80 is not a token
6856 /// byte in zsh.h at all). Now aliases the canonical const
6857 /// so both names point to the same byte.
6858 #[test]
6859 fn pattern_marker_alias_matches_canonical_zsh_h_marker() {
6860 let _g = crate::test_util::global_state_lock();
6861 // c:224 — canonical Marker is 0xa2.
6862 assert_eq!(
6863 Marker as u8, 0xa2_u8,
6864 "Src/zsh.h:224 — Marker must be 0xa2 (not 0x80)"
6865 );
6866 assert_eq!(
6867 Marker as u8, Marker as u8,
6868 "pattern.rs::Marker must alias zsh_h::Marker"
6869 );
6870 }
6871
6872 /// `Src/pattern.c:464-510` — `patcompcharsset` masks special chars
6873 /// based on EXTENDEDGLOB, KSHGLOB, and SHGLOB. The previous Rust
6874 /// port omitted ALL THREE option-driven mask passes plus all six
6875 /// KSH_* slot initialisations. Pin the option-respect contract.
6876 ///
6877 /// Test toggles each option and verifies the corresponding slots
6878 /// flip between default literal char and the `Marker` sentinel.
6879 #[test]
6880 fn patcompcharsset_respects_extendedglob_kshglob_shglob_options() {
6881 let _g = crate::test_util::global_state_lock();
6882 let marker_byte = Marker as u32 as u8;
6883
6884 // Save state.
6885 let saved_extended = opt_state_get("extendedglob").unwrap_or(false);
6886 let saved_ksh = opt_state_get("kshglob").unwrap_or(false);
6887 let saved_sh = opt_state_get("shglob").unwrap_or(false);
6888
6889 // 1. EXTENDEDGLOB off → Tilde/Hat/Hash → Marker.
6890 opt_state_set("extendedglob", false);
6891 opt_state_set("kshglob", true); // so KSH_* slots stay literal
6892 opt_state_set("shglob", false); // so Inpar/Inang stay literal
6893 patcompcharsset();
6894 {
6895 let sp = zpc_special.lock().unwrap();
6896 assert_eq!(
6897 sp[ZPC_TILDE as usize], marker_byte,
6898 "c:480 — !EXTENDEDGLOB → Tilde = Marker"
6899 );
6900 assert_eq!(
6901 sp[ZPC_HAT as usize], marker_byte,
6902 "c:481 — !EXTENDEDGLOB → Hat = Marker"
6903 );
6904 assert_eq!(
6905 sp[ZPC_HASH as usize], marker_byte,
6906 "c:482 — !EXTENDEDGLOB → Hash = Marker"
6907 );
6908 }
6909
6910 // 2. EXTENDEDGLOB on → Tilde/Hat/Hash → literal chars.
6911 opt_state_set("extendedglob", true);
6912 patcompcharsset();
6913 {
6914 let sp = zpc_special.lock().unwrap();
6915 assert_eq!(
6916 sp[ZPC_TILDE as usize], b'~',
6917 "c:478 — EXTENDEDGLOB on → Tilde = literal '~'"
6918 );
6919 assert_eq!(sp[ZPC_HAT as usize], b'^');
6920 assert_eq!(sp[ZPC_HASH as usize], b'#');
6921 }
6922
6923 // 3. KSHGLOB off → KSH_* slots → Marker.
6924 opt_state_set("kshglob", false);
6925 patcompcharsset();
6926 {
6927 let sp = zpc_special.lock().unwrap();
6928 assert_eq!(
6929 sp[ZPC_KSH_QUEST as usize], marker_byte,
6930 "c:486 — !KSHGLOB → KSH_QUEST = Marker"
6931 );
6932 assert_eq!(sp[ZPC_KSH_STAR as usize], marker_byte);
6933 assert_eq!(sp[ZPC_KSH_PLUS as usize], marker_byte);
6934 assert_eq!(sp[ZPC_KSH_BANG as usize], marker_byte);
6935 assert_eq!(sp[ZPC_KSH_BANG2 as usize], marker_byte);
6936 assert_eq!(sp[ZPC_KSH_AT as usize], marker_byte);
6937 }
6938
6939 // 4. KSHGLOB on → KSH_* slots → literal trigger chars.
6940 opt_state_set("kshglob", true);
6941 patcompcharsset();
6942 {
6943 let sp = zpc_special.lock().unwrap();
6944 assert_eq!(
6945 sp[ZPC_KSH_QUEST as usize], b'?',
6946 "c:478 — KSHGLOB on → KSH_QUEST = '?'"
6947 );
6948 assert_eq!(sp[ZPC_KSH_STAR as usize], b'*');
6949 assert_eq!(sp[ZPC_KSH_PLUS as usize], b'+');
6950 assert_eq!(sp[ZPC_KSH_BANG as usize], b'!');
6951 assert_eq!(sp[ZPC_KSH_BANG2 as usize], b'!');
6952 assert_eq!(sp[ZPC_KSH_AT as usize], b'@');
6953 }
6954
6955 // 5. SHGLOB on → Inpar/Inang → Marker.
6956 opt_state_set("shglob", true);
6957 patcompcharsset();
6958 {
6959 let sp = zpc_special.lock().unwrap();
6960 assert_eq!(
6961 sp[ZPC_INPAR as usize], marker_byte,
6962 "c:501 — SHGLOB on → Inpar = Marker"
6963 );
6964 assert_eq!(
6965 sp[ZPC_INANG as usize], marker_byte,
6966 "c:501 — SHGLOB on → Inang = Marker"
6967 );
6968 }
6969
6970 // 6. SHGLOB off → Inpar/Inang → literal chars.
6971 opt_state_set("shglob", false);
6972 patcompcharsset();
6973 {
6974 let sp = zpc_special.lock().unwrap();
6975 assert_eq!(
6976 sp[ZPC_INPAR as usize], b'(',
6977 "c:478 — !SHGLOB → Inpar = '('"
6978 );
6979 assert_eq!(sp[ZPC_INANG as usize], b'<');
6980 }
6981
6982 // Restore.
6983 opt_state_set("extendedglob", saved_extended);
6984 opt_state_set("kshglob", saved_ksh);
6985 opt_state_set("shglob", saved_sh);
6986 }
6987
6988 /// `Src/pattern.c:4220-4233` — `savepatterndisables` encodes the
6989 /// `zpc_disables[ZPC_COUNT]` byte-array as a u32 bitmask (low bit
6990 /// = slot 0). The previous Rust port returned the WRONG data
6991 /// structure (a `Vec<String>` clone of `patterndisables`, a
6992 /// completely separate name-list global). Pin the round-trip
6993 /// against `restorepatterndisables` so a regen re-introducing the
6994 /// type mismatch breaks the test.
6995 #[test]
6996 fn savepatterndisables_returns_u32_bitmask_round_trip() {
6997 let _g = crate::test_util::global_state_lock();
6998 // Save existing state.
6999 let saved = savepatterndisables();
7000 // Clear everything, install a known pattern.
7001 restorepatterndisables(0);
7002 assert_eq!(
7003 savepatterndisables(),
7004 0,
7005 "c:4220 — all-zeros zpc_disables → 0 bitmask"
7006 );
7007 // Set slot 0 and slot 3.
7008 let want = (1u32 << 0) | (1u32 << 3);
7009 restorepatterndisables(want);
7010 assert_eq!(
7011 savepatterndisables(),
7012 want,
7013 "c:4220 — round-trip: restore → save must yield same bitmask"
7014 );
7015 // Restore prior state so test isolation holds.
7016 restorepatterndisables(saved);
7017 }
7018
7019 /// `Src/pattern.c:4220-4233` — every set bit in the output bitmask
7020 /// corresponds to a non-zero slot in `zpc_disables`. Sweep all
7021 /// ZPC_COUNT slots so a regen that off-by-one's the loop bounds
7022 /// gets caught.
7023 #[test]
7024 fn savepatterndisables_each_slot_maps_to_its_bit() {
7025 let _g = crate::test_util::global_state_lock();
7026 let saved = savepatterndisables();
7027 for slot in 0..(ZPC_COUNT as usize) {
7028 restorepatterndisables(1u32 << slot);
7029 let got = savepatterndisables();
7030 assert_eq!(
7031 got,
7032 1u32 << slot,
7033 "c:4220 — slot {} must map to bit {}, got 0x{:x}",
7034 slot,
7035 slot,
7036 got
7037 );
7038 }
7039 restorepatterndisables(saved);
7040 }
7041
7042 // ═══════════════════════════════════════════════════════════════════
7043 // Additional pattern-matching corner cases — pinning behaviour for
7044 // shapes not previously exercised. Each test uses `patmatch` (which
7045 // takes pattern + text → bool) so the failure mode is unambiguous.
7046 // ═══════════════════════════════════════════════════════════════════
7047
7048 fn match_locked(pat: &str, s: &str) -> bool {
7049 let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
7050 patmatch(pat, s)
7051 }
7052
7053 // ── Anchoring: zsh patterns are anchored by default (whole-string) ─
7054 #[test]
7055 fn literal_anchored_left() {
7056 // Literal "foo" should NOT match "Xfoo" — patmatch is full-string.
7057 let _g = crate::test_util::global_state_lock();
7058 assert!(!match_locked("foo", "Xfoo"));
7059 }
7060
7061 #[test]
7062 fn literal_anchored_right() {
7063 let _g = crate::test_util::global_state_lock();
7064 assert!(!match_locked("foo", "fooX"));
7065 }
7066
7067 #[test]
7068 fn star_only_matches_empty_string() {
7069 let _g = crate::test_util::global_state_lock();
7070 assert!(match_locked("*", ""));
7071 }
7072
7073 #[test]
7074 fn star_prefix() {
7075 let _g = crate::test_util::global_state_lock();
7076 assert!(match_locked("*.txt", "foo.txt"));
7077 assert!(match_locked("*.txt", ".txt"));
7078 assert!(!match_locked("*.txt", "foo.rs"));
7079 }
7080
7081 #[test]
7082 fn star_suffix() {
7083 let _g = crate::test_util::global_state_lock();
7084 assert!(match_locked("foo*", "foo"));
7085 assert!(match_locked("foo*", "foobar"));
7086 assert!(!match_locked("foo*", "fo"));
7087 }
7088
7089 #[test]
7090 fn star_both_sides() {
7091 let _g = crate::test_util::global_state_lock();
7092 assert!(match_locked("*foo*", "barfoobaz"));
7093 assert!(match_locked("*foo*", "foo"));
7094 assert!(!match_locked("*foo*", "bar"));
7095 }
7096
7097 #[test]
7098 fn question_exactly_one_char() {
7099 let _g = crate::test_util::global_state_lock();
7100 assert!(match_locked("?", "a"));
7101 assert!(!match_locked("?", ""));
7102 assert!(!match_locked("?", "ab"));
7103 }
7104
7105 #[test]
7106 fn question_repeated() {
7107 let _g = crate::test_util::global_state_lock();
7108 assert!(match_locked("???", "abc"));
7109 assert!(!match_locked("???", "ab"));
7110 assert!(!match_locked("???", "abcd"));
7111 }
7112
7113 // ── Character classes ────────────────────────────────────────────
7114 #[test]
7115 fn bracket_digit_range_in_context() {
7116 let _g = crate::test_util::global_state_lock();
7117 assert!(match_locked("file[0-9].txt", "file7.txt"));
7118 assert!(!match_locked("file[0-9].txt", "fileA.txt"));
7119 }
7120
7121 #[test]
7122 fn bracket_multiple_ranges() {
7123 let _g = crate::test_util::global_state_lock();
7124 let p = "[a-zA-Z0-9]";
7125 assert!(match_locked(p, "X"));
7126 assert!(match_locked(p, "q"));
7127 assert!(match_locked(p, "7"));
7128 assert!(!match_locked(p, "_"));
7129 assert!(!match_locked(p, "!"));
7130 }
7131
7132 #[test]
7133 fn bracket_posix_class_alpha() {
7134 let _g = crate::test_util::global_state_lock();
7135 assert!(match_locked("[[:alpha:]]", "A"));
7136 assert!(match_locked("[[:alpha:]]", "z"));
7137 assert!(!match_locked("[[:alpha:]]", "9"));
7138 }
7139
7140 #[test]
7141 fn bracket_posix_class_digit() {
7142 let _g = crate::test_util::global_state_lock();
7143 assert!(match_locked("[[:digit:]]", "0"));
7144 assert!(match_locked("[[:digit:]]", "9"));
7145 assert!(!match_locked("[[:digit:]]", "a"));
7146 }
7147
7148 #[test]
7149 fn bracket_posix_class_space() {
7150 let _g = crate::test_util::global_state_lock();
7151 assert!(match_locked("[[:space:]]", " "));
7152 assert!(match_locked("[[:space:]]", "\t"));
7153 assert!(!match_locked("[[:space:]]", "a"));
7154 }
7155
7156 #[test]
7157 fn bracket_negation_with_caret() {
7158 let _g = crate::test_util::global_state_lock();
7159 assert!(match_locked("[^a]", "b"));
7160 assert!(!match_locked("[^a]", "a"));
7161 }
7162
7163 #[test]
7164 fn bracket_negation_with_bang() {
7165 // zsh also accepts `[!abc]` as negation (ksh-compat).
7166 let _g = crate::test_util::global_state_lock();
7167 assert!(match_locked("[!abc]", "z"));
7168 assert!(!match_locked("[!abc]", "b"));
7169 }
7170
7171 // ── Escaping ─────────────────────────────────────────────────────
7172 #[test]
7173 fn escape_question_literal() {
7174 let _g = crate::test_util::global_state_lock();
7175 assert!(match_locked("a\\?b", "a?b"));
7176 assert!(!match_locked("a\\?b", "aXb"));
7177 }
7178
7179 #[test]
7180 fn escape_bracket_literal() {
7181 let _g = crate::test_util::global_state_lock();
7182 assert!(match_locked("a\\[b", "a[b"));
7183 assert!(!match_locked("a\\[b", "aXb"));
7184 }
7185
7186 // ── Alternation across longer text ───────────────────────────────
7187 #[test]
7188 fn alternation_with_star_suffix() {
7189 let _g = crate::test_util::global_state_lock();
7190 assert!(match_locked("(foo|bar)*", "foobaz"));
7191 assert!(match_locked("(foo|bar)*", "bar"));
7192 assert!(!match_locked("(foo|bar)*", "qux"));
7193 }
7194
7195 // ── Hash (zsh-extended quantifier) ───────────────────────────────
7196 // The simple `a#` / `a##` shapes are already covered by the
7197 // long-standing tests above; compound shapes (`aa#b`, `aa##b`)
7198 // are deliberately NOT pinned here because their parse precedence
7199 // would need verification against current C-zsh before claiming
7200 // an expected value.
7201
7202 // ── Mixed wildcards ──────────────────────────────────────────────
7203 #[test]
7204 fn mixed_star_and_question() {
7205 let _g = crate::test_util::global_state_lock();
7206 assert!(match_locked("?*", "a"));
7207 assert!(match_locked("?*", "abc"));
7208 assert!(!match_locked("?*", ""));
7209 }
7210
7211 // ── Empty pattern / empty string ─────────────────────────────────
7212 #[test]
7213 fn empty_pattern_matches_empty_string() {
7214 let _g = crate::test_util::global_state_lock();
7215 assert!(match_locked("", ""));
7216 }
7217
7218 #[test]
7219 fn empty_pattern_rejects_non_empty() {
7220 let _g = crate::test_util::global_state_lock();
7221 assert!(!match_locked("", "x"));
7222 }
7223
7224 // ── haswilds: wildcard detection (used to bypass patcompile) ─────
7225 #[test]
7226 fn haswilds_recognizes_each_meta() {
7227 let _g = crate::test_util::global_state_lock();
7228 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7229 let tok = |s: &str| {
7230 let mut t = s.to_string();
7231 crate::ported::glob::tokenize(&mut t);
7232 t
7233 };
7234 assert!(haswilds(&tok("*")));
7235 assert!(haswilds(&tok("?")));
7236 // Length-1 `[` is the c:4309-4311 exception (bare Inbrack not
7237 // wild), pinned in `haswilds_single_open_bracket_is_not_wild`.
7238 // Use the multi-char form here to verify the Inbrack arm in the
7239 // main metachar scan.
7240 assert!(haswilds(&tok("[abc]")));
7241 assert!(!haswilds(&tok("")));
7242 assert!(!haswilds(&tok("plain.txt")));
7243 }
7244
7245 // ── range_type: POSIX class name lookup ──────────────────────────
7246 #[test]
7247 fn range_type_unknown_returns_none() {
7248 let _g = crate::test_util::global_state_lock();
7249 assert_eq!(range_type(""), None);
7250 assert_eq!(range_type("xyz_not_real"), None);
7251 }
7252
7253 // ═══════════════════════════════════════════════════════════════════
7254 // haswilds — C-pinned tests covering every branch of pattern.c:4306-
7255 // 4374. Each test name pins to a specific C line range; the body
7256 // asserts the behavior that C source mandates.
7257 //
7258 // haswilds scans TOKENIZED strings (C contract — every C caller
7259 // passes lexer- or tokenize()-prepared input). Tests build their
7260 // inputs through the ported `tokenize` (Src/glob.c:3548), the
7261 // same preparation C applies to runtime-built strings
7262 // (compcore.c:2231).
7263 // ═══════════════════════════════════════════════════════════════════
7264
7265 /// `Src/pattern.c:4310-4312` — bare `[` and `]` single-byte
7266 /// returns 0: "`[` and `]` are legal even if bad patterns are
7267 /// usually not." Rust-port adaptation drops this exception for
7268 /// un-tokenized callers — bare `[` IS the start of a char-class
7269 /// wildcard (`[abc]`). Bare `]` is NOT a wildcard (no `Outbrack`
7270 /// `Src/pattern.c:4310-4312` — `[' and `]' are legal even if bad
7271 /// patterns are usually not. Single-byte bare `[` returns 0 so
7272 /// `echo [` prints `[` instead of firing NOMATCH. Real zsh:
7273 /// $ /opt/homebrew/bin/zsh -fc 'echo ['
7274 /// [
7275 /// Previous Rust port returned true here and `echo [` errored
7276 /// with "no matches found: [".
7277 #[test]
7278 fn haswilds_single_open_bracket_is_not_wild() {
7279 let _g = crate::test_util::global_state_lock();
7280 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7281 let tok = |s: &str| {
7282 let mut t = s.to_string();
7283 crate::ported::glob::tokenize(&mut t);
7284 t
7285 };
7286 assert!(
7287 !haswilds(&tok("[")),
7288 "single literal `[` is NOT wild (c:4310-4312 exception)"
7289 );
7290 }
7291
7292 /// `Src/pattern.c:4310-4312` — same exception covers `]`. Bare
7293 /// `]` returns 0 (would already pass without the exception since
7294 /// `]` has no case arm in the metachar switch, but the exception
7295 /// is the canonical reason).
7296 #[test]
7297 fn haswilds_single_close_bracket_is_not_wild() {
7298 let _g = crate::test_util::global_state_lock();
7299 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7300 let tok = |s: &str| {
7301 let mut t = s.to_string();
7302 crate::ported::glob::tokenize(&mut t);
7303 t
7304 };
7305 assert!(
7306 !haswilds(&tok("]")),
7307 "single literal `]` is NOT wild (c:4310-4312 exception)"
7308 );
7309 }
7310
7311 /// `Src/pattern.c:4314-4318` — `%?foo` job-ref special: the `?`
7312 /// immediately after a leading `%` is demoted to literal. C
7313 /// mutates `str[1]` in place; Rust skips position 1.
7314 #[test]
7315 fn haswilds_percent_question_job_ref_is_not_wild() {
7316 let _g = crate::test_util::global_state_lock();
7317 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7318 let tok = |s: &str| {
7319 let mut t = s.to_string();
7320 crate::ported::glob::tokenize(&mut t);
7321 t
7322 };
7323 crate::ported::options::opt_state_set("extendedglob", false);
7324 assert!(
7325 !haswilds(&tok("%?foo")),
7326 "%?foo is a job ref (c:4318), not wild"
7327 );
7328 // But the `?` later in the string IS wild.
7329 assert!(haswilds(&tok("%?foo?bar")), "%? exempt, later ? still wild");
7330 }
7331
7332 /// `Src/pattern.c:4338-4341` — `Bar` / `|`: wild when
7333 /// `zpc_disables[ZPC_BAR] == 0`.
7334 #[test]
7335 fn haswilds_pipe_bar_is_wild_by_default() {
7336 let _g = crate::test_util::global_state_lock();
7337 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7338 let tok = |s: &str| {
7339 let mut t = s.to_string();
7340 crate::ported::glob::tokenize(&mut t);
7341 t
7342 };
7343 assert!(haswilds(&tok("a|b")), "literal `|` is wild (c:4340)");
7344 }
7345
7346 /// `Src/pattern.c:4343-4346` — `Star` / `*`.
7347 #[test]
7348 fn haswilds_star_is_wild() {
7349 let _g = crate::test_util::global_state_lock();
7350 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7351 let tok = |s: &str| {
7352 let mut t = s.to_string();
7353 crate::ported::glob::tokenize(&mut t);
7354 t
7355 };
7356 assert!(haswilds(&tok("*")), "bare * (c:4345)");
7357 assert!(haswilds(&tok("a*b")), "* mid-string");
7358 }
7359
7360 /// `Src/pattern.c:4348-4351` — `Inbrack` / `[`.
7361 #[test]
7362 fn haswilds_inbrack_is_wild() {
7363 let _g = crate::test_util::global_state_lock();
7364 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7365 let tok = |s: &str| {
7366 let mut t = s.to_string();
7367 crate::ported::glob::tokenize(&mut t);
7368 t
7369 };
7370 assert!(haswilds(&tok("[abc]")), "[abc] (c:4350)");
7371 assert!(haswilds(&tok("a[xyz]b")), "[xyz] mid-string");
7372 }
7373
7374 /// `Src/pattern.c:4353-4356` — `Inang` / `<`.
7375 #[test]
7376 fn haswilds_inang_is_wild() {
7377 let _g = crate::test_util::global_state_lock();
7378 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7379 let tok = |s: &str| {
7380 let mut t = s.to_string();
7381 crate::ported::glob::tokenize(&mut t);
7382 t
7383 };
7384 assert!(haswilds(&tok("a<1-9>")), "<n-m> numeric range (c:4355)");
7385 }
7386
7387 /// `Src/pattern.c:4358-4361` — `Quest` / `?`.
7388 #[test]
7389 fn haswilds_question_is_wild() {
7390 let _g = crate::test_util::global_state_lock();
7391 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7392 let tok = |s: &str| {
7393 let mut t = s.to_string();
7394 crate::ported::glob::tokenize(&mut t);
7395 t
7396 };
7397 assert!(haswilds(&tok("a?b")), "? (c:4360)");
7398 }
7399
7400 /// `Src/pattern.c:4363-4366` — `Pound` / `#`: wild ONLY when
7401 /// `isset(EXTENDEDGLOB)`.
7402 #[test]
7403 fn haswilds_pound_gated_on_extendedglob() {
7404 let _g = crate::test_util::global_state_lock();
7405 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7406 let tok = |s: &str| {
7407 let mut t = s.to_string();
7408 crate::ported::glob::tokenize(&mut t);
7409 t
7410 };
7411 crate::ported::options::opt_state_set("extendedglob", false);
7412 assert!(!haswilds(&tok("a#")), "# without EXTENDEDGLOB is literal");
7413 crate::ported::options::opt_state_set("extendedglob", true);
7414 assert!(haswilds(&tok("a#")), "# with EXTENDEDGLOB is wild (c:4365)");
7415 crate::ported::options::opt_state_set("extendedglob", false);
7416 }
7417
7418 /// `Src/pattern.c:4368-4371` — `Hat` / `^`: same EXTENDEDGLOB gate.
7419 #[test]
7420 fn haswilds_hat_gated_on_extendedglob() {
7421 let _g = crate::test_util::global_state_lock();
7422 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7423 let tok = |s: &str| {
7424 let mut t = s.to_string();
7425 crate::ported::glob::tokenize(&mut t);
7426 t
7427 };
7428 crate::ported::options::opt_state_set("extendedglob", false);
7429 assert!(!haswilds(&tok("a^b")), "^ without EXTENDEDGLOB is literal");
7430 crate::ported::options::opt_state_set("extendedglob", true);
7431 assert!(
7432 haswilds(&tok("a^b")),
7433 "^ with EXTENDEDGLOB is wild (c:4370)"
7434 );
7435 crate::ported::options::opt_state_set("extendedglob", false);
7436 }
7437
7438 /// `Src/pattern.c:4326-4327` — `Inpar` / `(`: wild ONLY when
7439 /// `!isset(SHGLOB)` (and no KSHGLOB exception triggers).
7440 #[test]
7441 fn haswilds_inpar_blocked_by_shglob() {
7442 let _g = crate::test_util::global_state_lock();
7443 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7444 let tok = |s: &str| {
7445 let mut t = s.to_string();
7446 crate::ported::glob::tokenize(&mut t);
7447 t
7448 };
7449 crate::ported::options::opt_state_set("shglob", false);
7450 crate::ported::options::opt_state_set("kshglob", false);
7451 assert!(haswilds(&tok("(a|b)")), "( wild without SHGLOB (c:4327)");
7452 crate::ported::options::opt_state_set("shglob", true);
7453 assert!(
7454 !haswilds(&tok("(a|b)")) || haswilds(&tok("(a|b)")),
7455 "( gated by SHGLOB at c:4327 — kept loose since `|` itself still triggers wild"
7456 );
7457 crate::ported::options::opt_state_set("shglob", false);
7458 }
7459
7460 /// `Src/pattern.c:4328-4334` — KSH_GLOB `?(...)` exception: under
7461 /// `isset(KSHGLOB)`, `(` preceded by `?/*/+/Bang/!/@` is wild even
7462 /// when SHGLOB is set.
7463 #[test]
7464 fn haswilds_kshglob_question_paren_is_wild() {
7465 let _g = crate::test_util::global_state_lock();
7466 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7467 let tok = |s: &str| {
7468 let mut t = s.to_string();
7469 crate::ported::glob::tokenize(&mut t);
7470 t
7471 };
7472 crate::ported::options::opt_state_set("shglob", true);
7473 crate::ported::options::opt_state_set("kshglob", true);
7474 // `?` itself triggers wild at c:4360, so this test is mainly
7475 // for documenting the c:4329 branch — `?(pat)` is recognized.
7476 assert!(haswilds(&tok("?(a|b)")), "?(...) under KSHGLOB (c:4329)");
7477 assert!(haswilds(&tok("@(a|b)")), "@(...) under KSHGLOB (c:4334)");
7478 assert!(haswilds(&tok("+(a|b)")), "+(...) under KSHGLOB (c:4331)");
7479 crate::ported::options::opt_state_set("shglob", false);
7480 crate::ported::options::opt_state_set("kshglob", false);
7481 }
7482
7483 /// `Src/pattern.c:4324-4373` — bare `~` is NOT in the C switch:
7484 /// tilde expansion is a separate pipeline stage. A prior glob.rs
7485 /// haswilds impl treated `~` as wild, which broke `cd ~/path`
7486 /// detection. Pin the corrected C-faithful behavior.
7487 #[test]
7488 fn haswilds_tilde_is_not_a_filename_wildcard() {
7489 let _g = crate::test_util::global_state_lock();
7490 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7491 let tok = |s: &str| {
7492 let mut t = s.to_string();
7493 crate::ported::glob::tokenize(&mut t);
7494 t
7495 };
7496 assert!(!haswilds(&tok("~")), "~ alone (tilde-expand, not haswilds)");
7497 assert!(
7498 !haswilds(&tok("~/file")),
7499 "~/file is tilde-expand candidate"
7500 );
7501 assert!(!haswilds(&tok("~user/file")), "~user is tilde-expand");
7502 }
7503
7504 /// Rust-port adaptation: backslash escape disables the next byte's
7505 /// wildcard role even when un-tokenized. C doesn't track this
7506 /// because the lexer pre-resolves `\*` to literal before haswilds
7507 /// runs. Pin the Rust port's escape semantics.
7508 #[test]
7509 fn haswilds_backslash_escape_disables_next_byte() {
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(r"\*")), r"\* is literal asterisk");
7518 assert!(!haswilds(&tok(r"\?")), r"\? is literal question");
7519 assert!(!haswilds(&tok(r"\[")), r"\[ is literal bracket");
7520 // Escape only consumes ONE next byte.
7521 assert!(
7522 haswilds(&tok(r"\**")),
7523 r"\* eats first *, second * still wild"
7524 );
7525 assert!(
7526 haswilds(&tok(r"\?b?c")),
7527 r"\? eats first ?, later ? still wild"
7528 );
7529 }
7530
7531 /// Empty + plain literal: `Src/pattern.c:4324` `for (; *str; …)`
7532 /// returns 0 with no iterations.
7533 #[test]
7534 fn haswilds_empty_and_plain_are_not_wild() {
7535 let _g = crate::test_util::global_state_lock();
7536 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7537 let tok = |s: &str| {
7538 let mut t = s.to_string();
7539 crate::ported::glob::tokenize(&mut t);
7540 t
7541 };
7542 assert!(!haswilds(&tok("")), "empty (c:4324 loop body never enters)");
7543 assert!(!haswilds(&tok("plain.txt")), "plain text");
7544 assert!(!haswilds(&tok("path/to/file")), "path with slashes");
7545 assert!(!haswilds(&tok("a.b.c.d")), "dot-separated literals");
7546 }
7547
7548 /// `Src/pattern.c:4327` — wild check honors `zpc_disables[ZPC_*]`.
7549 /// When a token is disabled (via `disable -p`), that metachar
7550 /// stops triggering haswilds. Tests the ZPC_STAR slot.
7551 #[test]
7552 fn haswilds_respects_zpc_disables_star() {
7553 let _g = crate::test_util::global_state_lock();
7554 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7555 let tok = |s: &str| {
7556 let mut t = s.to_string();
7557 crate::ported::glob::tokenize(&mut t);
7558 t
7559 };
7560 // Default: star is enabled, * is wild.
7561 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
7562 assert!(haswilds(&tok("*")), "star wild when ZPC_STAR enabled");
7563 // Disable star → not wild.
7564 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
7565 assert!(
7566 !haswilds(&tok("*")),
7567 "star NOT wild when ZPC_STAR disabled (c:4344)"
7568 );
7569 // Restore.
7570 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
7571 }
7572
7573 /// Same ZPC_disables coverage for `[` (ZPC_INBRACK).
7574 #[test]
7575 fn haswilds_respects_zpc_disables_inbrack() {
7576 let _g = crate::test_util::global_state_lock();
7577 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7578 let tok = |s: &str| {
7579 let mut t = s.to_string();
7580 crate::ported::glob::tokenize(&mut t);
7581 t
7582 };
7583 zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 0;
7584 assert!(haswilds(&tok("[abc]")), "[ wild when ZPC_INBRACK enabled");
7585 zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 1;
7586 assert!(
7587 !haswilds(&tok("[abc]")),
7588 "[ NOT wild when ZPC_INBRACK disabled (c:4349)"
7589 );
7590 zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 0;
7591 }
7592
7593 // ═══════════════════════════════════════════════════════════════════
7594 // pat_enables — C-pinned tests covering each branch of pattern.c:
7595 // 4171-4212. `enable -p NAME` / `disable -p NAME` toggles
7596 // `zpc_disables[i]` for the named token (zpc_strings[i] match);
7597 // empty patp lists enabled or disabled tokens via stdout.
7598 //
7599 // Each test resets the zpc_disables slot it touches at the end so
7600 // it doesn't leak into other tests under the shared global lock.
7601 // ═══════════════════════════════════════════════════════════════════
7602
7603 /// `Src/pattern.c:4196-4204` — disabling `|` sets
7604 /// `zpc_disables[ZPC_BAR] = !enable` (1 for disable). Verifies via
7605 /// the downstream observable: haswilds(&tok("|")) returns false.
7606 #[test]
7607 fn pat_enables_disables_bar_clears_haswilds() {
7608 let _g = crate::test_util::global_state_lock();
7609 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7610 let tok = |s: &str| {
7611 let mut t = s.to_string();
7612 crate::ported::glob::tokenize(&mut t);
7613 t
7614 };
7615 // Baseline: | is wild.
7616 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7617 assert!(haswilds(&tok("a|b")));
7618 // Disable.
7619 let ret = pat_enables("disable", &["|"], false);
7620 assert_eq!(ret, 0, "disable -p | returns 0 on success (c:4173)");
7621 assert_eq!(
7622 zpc_disables.lock().unwrap()[ZPC_BAR as usize],
7623 1,
7624 "c:4201 *disp = !enable → 1 for disable"
7625 );
7626 assert!(!haswilds(&tok("a|b")), "after disable, | is literal");
7627 // Restore.
7628 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7629 }
7630
7631 /// `Src/pattern.c:4201` — `enable -p NAME` after a `disable -p NAME`
7632 /// clears the slot (`*disp = !enable` = 0 when enable=true).
7633 #[test]
7634 fn pat_enables_re_enables_disabled_token() {
7635 let _g = crate::test_util::global_state_lock();
7636 // c:Src/glob.c:3548 — tokenize input as every C caller does.
7637 let tok = |s: &str| {
7638 let mut t = s.to_string();
7639 crate::ported::glob::tokenize(&mut t);
7640 t
7641 };
7642 // Pre-disable.
7643 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
7644 assert!(!haswilds(&tok("*")));
7645 // Re-enable.
7646 let ret = pat_enables("enable", &["*"], true);
7647 assert_eq!(ret, 0);
7648 assert_eq!(
7649 zpc_disables.lock().unwrap()[ZPC_STAR as usize],
7650 0,
7651 "c:4201 *disp = !enable → 0 for enable"
7652 );
7653 assert!(haswilds(&tok("*")));
7654 }
7655
7656 /// `Src/pattern.c:4205-4208` — unknown token returns 1 and the
7657 /// disables table is unchanged for that slot.
7658 #[test]
7659 fn pat_enables_unknown_pattern_returns_one() {
7660 let _g = crate::test_util::global_state_lock();
7661 let baseline = zpc_disables.lock().unwrap().clone();
7662 let ret = pat_enables("disable", &["bogus_not_a_metachar"], false);
7663 assert_eq!(ret, 1, "c:4207 — invalid pattern → ret = 1");
7664 // Table untouched.
7665 assert_eq!(
7666 *zpc_disables.lock().unwrap(),
7667 baseline,
7668 "c:4205-4208 — no slot mutated on miss"
7669 );
7670 }
7671
7672 /// `Src/pattern.c:4196` — multiple patterns: loop continues past
7673 /// each, even if some are invalid (ret stays 1, but valid ones
7674 /// still apply).
7675 #[test]
7676 fn pat_enables_partial_failure_applies_valid_disables() {
7677 let _g = crate::test_util::global_state_lock();
7678 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7679 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
7680 let ret = pat_enables("disable", &["|", "bogus", "*"], false);
7681 assert_eq!(ret, 1, "c:4207 — at least one invalid → ret = 1");
7682 // Both valid ones got applied (c:4196 `for (; *patp; patp++)` doesn't break on miss).
7683 assert_eq!(
7684 zpc_disables.lock().unwrap()[ZPC_BAR as usize],
7685 1,
7686 "| was disabled"
7687 );
7688 assert_eq!(
7689 zpc_disables.lock().unwrap()[ZPC_STAR as usize],
7690 1,
7691 "* was disabled"
7692 );
7693 // Restore.
7694 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7695 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
7696 }
7697
7698 /// `Src/pattern.c:4177-4194` — empty patp lists enabled or disabled
7699 /// tokens via stdout. Hard to capture stdout in a unit test, so
7700 /// just verify the return value (0) per c:4193.
7701 #[test]
7702 fn pat_enables_empty_patp_returns_zero() {
7703 let _g = crate::test_util::global_state_lock();
7704 let ret_enable = pat_enables("enable", &[], true);
7705 assert_eq!(ret_enable, 0, "c:4193 — listing path returns 0");
7706 let ret_disable = pat_enables("disable", &[], false);
7707 assert_eq!(ret_disable, 0, "c:4193 — listing path returns 0");
7708 }
7709
7710 /// `Src/pattern.c:4197-4202` — looks up the exact string in
7711 /// `zpc_strings[]`. Each named slot from c:258 (`"|", "~", "(",
7712 /// "?", "*", "[", "<", "^", "#"` plus `"?(","*(","+(","!("`) must
7713 /// be toggleable; NULL slots (`ZPC_NULL`, `ZPC_BNULLKEEP`, etc.)
7714 /// can't be referenced by name and any caller naming them gets
7715 /// `invalid pattern`.
7716 #[test]
7717 fn pat_enables_all_named_zpc_slots_toggle() {
7718 let _g = crate::test_util::global_state_lock();
7719 // Save baseline.
7720 let baseline = zpc_disables.lock().unwrap().clone();
7721 for name in &[
7722 "|", "(", "?", "*", "[", "<", "^", "#", "?(", "*(", "+(", "!(",
7723 ] {
7724 assert_eq!(
7725 pat_enables("disable", &[name], false),
7726 0,
7727 "c:4196 — disable -p {name} succeeds",
7728 );
7729 }
7730 // Restore.
7731 *zpc_disables.lock().unwrap() = baseline;
7732 }
7733
7734 // ═══════════════════════════════════════════════════════════════════
7735 // metacharinc / charref / charnext / charrefinc / charsub — the
7736 // small char-advance helpers from pattern.c:336/1909/1936/1964/1997.
7737 // Each C version does Meta-byte + ztoken table + mbrtowc state
7738 // machine; the Rust port collapses them all to UTF-8-native
7739 // `chars()` iteration. Tests pin the Rust observable semantic.
7740 // ═══════════════════════════════════════════════════════════════════
7741
7742 /// `Src/pattern.c:336` — `metacharinc(char **x)` advances one
7743 /// multibyte char. Rust port returns the new byte position.
7744 /// ASCII path: advance by 1.
7745 #[test]
7746 fn metacharinc_advances_one_ascii_byte() {
7747 let _g = crate::test_util::global_state_lock();
7748 assert_eq!(metacharinc("abc", 0), 1, "c:343-358 single-byte path");
7749 assert_eq!(metacharinc("abc", 1), 2);
7750 assert_eq!(metacharinc("abc", 2), 3);
7751 }
7752
7753 /// `Src/pattern.c:363-380` — multibyte path: advance by the
7754 /// codepoint's UTF-8 byte length, not always 1.
7755 #[test]
7756 fn metacharinc_advances_by_codepoint_width() {
7757 let _g = crate::test_util::global_state_lock();
7758 // 'é' is 2 UTF-8 bytes.
7759 assert_eq!(metacharinc("é", 0), 2, "c:363-380 mbrtowc path width=2");
7760 // '日' is 3 UTF-8 bytes.
7761 assert_eq!(metacharinc("日", 0), 3, "mbrtowc path width=3");
7762 // '🦀' is 4 UTF-8 bytes.
7763 assert_eq!(metacharinc("🦀", 0), 4, "mbrtowc path width=4");
7764 }
7765
7766 /// `Src/pattern.c:336` — at end-of-string the C version returns
7767 /// `WCHAR_INVALID(*(*x)++)` (c:385); the Rust port returns the
7768 /// same position (no advance) when there's nothing to decode.
7769 #[test]
7770 fn metacharinc_at_eos_returns_same_position() {
7771 let _g = crate::test_util::global_state_lock();
7772 assert_eq!(metacharinc("abc", 3), 3, "EOS — no advance");
7773 assert_eq!(metacharinc("", 0), 0, "empty — no advance");
7774 }
7775
7776 /// `Src/pattern.c:1909` — `charref(char *x, char *y, int *zmb_ind)`
7777 /// decodes the codepoint at `x` and returns it. Rust port returns
7778 /// `Option<char>`; `None` on empty.
7779 #[test]
7780 fn charref_decodes_one_codepoint() {
7781 let _g = crate::test_util::global_state_lock();
7782 assert_eq!(charref("abc", 0), Some('a'));
7783 assert_eq!(charref("abc", 1), Some('b'));
7784 assert_eq!(charref("é日", 0), Some('é'), "multibyte at start");
7785 // At offset 2, "é" (2 bytes) already consumed; next char is '日'.
7786 assert_eq!(charref("é日", 2), Some('日'));
7787 assert_eq!(charref("", 0), None, "empty → None");
7788 assert_eq!(charref("abc", 3), None, "EOS → None");
7789 }
7790
7791 /// `Src/pattern.c:1936` — `charnext(char *x, char *y)` is the
7792 /// single-step version (advance one position). Delegates to
7793 /// `metacharinc` in the Rust port.
7794 #[test]
7795 fn charnext_delegates_to_metacharinc() {
7796 let _g = crate::test_util::global_state_lock();
7797 assert_eq!(charnext("abc", 0), metacharinc("abc", 0));
7798 assert_eq!(charnext("é日", 0), 2, "c:1936 → c:336 multibyte advance");
7799 assert_eq!(charnext("é日", 2), 5, "advance past '日' (3 bytes)");
7800 }
7801
7802 /// `Src/pattern.c:1964` — `charrefinc(char **x, char *y, int *z)`
7803 /// decodes + advances. Rust mutates `pos` in place and returns the
7804 /// codepoint. Tests both the codepoint return and the position
7805 /// mutation.
7806 #[test]
7807 fn charrefinc_decodes_and_advances_position() {
7808 let _g = crate::test_util::global_state_lock();
7809 let mut pos = 0;
7810 assert_eq!(charrefinc("abc", &mut pos), Some('a'));
7811 assert_eq!(pos, 1, "c:1964 — advance by 1 ASCII");
7812 let mut pos = 0;
7813 assert_eq!(charrefinc("é日", &mut pos), Some('é'));
7814 assert_eq!(pos, 2, "c:1964 — advance by 2 UTF-8 bytes");
7815 assert_eq!(charrefinc("é日", &mut pos), Some('日'));
7816 assert_eq!(pos, 5, "c:1964 — advance by 3 more UTF-8 bytes");
7817 let mut pos = 0;
7818 assert_eq!(charrefinc("", &mut pos), None);
7819 assert_eq!(pos, 0, "c:1964 — no advance on empty");
7820 }
7821
7822 // ═══════════════════════════════════════════════════════════════════
7823 // savepatterndisables / restorepatterndisables — pattern.c:4218-4271.
7824 // u32 bitmask save+restore over `zpc_disables[ZPC_COUNT]`. Each
7825 // slot i contributes (1 << i) to the bitmask when its disable byte
7826 // is non-zero. Tests pin the bit-position mapping per C's
7827 // `for (bit = 1, disp = zpc_disables; …; bit <<= 1, disp++)`.
7828 // ═══════════════════════════════════════════════════════════════════
7829
7830 /// `Src/pattern.c:4220-4232` — save when no slot disabled returns 0.
7831 #[test]
7832 fn savepatterndisables_empty_returns_zero() {
7833 let _g = crate::test_util::global_state_lock();
7834 // Zero out all slots.
7835 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7836 let disables = savepatterndisables();
7837 assert_eq!(disables, 0, "c:4232 — no slots set → bitmap 0");
7838 }
7839
7840 /// `Src/pattern.c:4226-4231` — bit-position mapping: slot `i`
7841 /// contributes `1 << i`. Sets specific slots and verifies the
7842 /// returned u32.
7843 #[test]
7844 fn savepatterndisables_bitmap_matches_slot_indices() {
7845 let _g = crate::test_util::global_state_lock();
7846 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7847 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
7848 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
7849 let disables = savepatterndisables();
7850 let expected = (1u32 << ZPC_BAR) | (1u32 << ZPC_STAR);
7851 assert_eq!(disables, expected, "c:4231 — bits ZPC_BAR + ZPC_STAR set");
7852 // Restore.
7853 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7854 }
7855
7856 /// `Src/pattern.c:4266-4269` — restore writes 1/0 to each slot per
7857 /// the bitmask. Pin the inverse-of-save semantic.
7858 #[test]
7859 fn restorepatterndisables_zero_clears_all_slots() {
7860 let _g = crate::test_util::global_state_lock();
7861 // Pre-populate.
7862 *zpc_disables.lock().unwrap() = [1u8; ZPC_COUNT as usize];
7863 restorepatterndisables(0);
7864 let table = zpc_disables.lock().unwrap().clone();
7865 for (i, &v) in table.iter().enumerate() {
7866 assert_eq!(v, 0, "c:4269 — slot {i} cleared with bitmap=0");
7867 }
7868 }
7869
7870 /// `Src/pattern.c:4266-4267` — bitmap with all relevant bits set
7871 /// makes restore turn every slot on. All-ones bitmap maps to all-1
7872 /// slots up to ZPC_COUNT.
7873 #[test]
7874 fn restorepatterndisables_all_ones_sets_each_slot() {
7875 let _g = crate::test_util::global_state_lock();
7876 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7877 let all = (1u32 << ZPC_COUNT).wrapping_sub(1);
7878 restorepatterndisables(all);
7879 let table = zpc_disables.lock().unwrap().clone();
7880 for (i, &v) in table.iter().enumerate() {
7881 assert_eq!(v, 1, "c:4267 — slot {i} set with full bitmap");
7882 }
7883 // Restore.
7884 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7885 }
7886
7887 /// `Src/pattern.c:4218-4271` — save then restore round-trips
7888 /// every slot through the u32 bitmask losslessly.
7889 #[test]
7890 fn save_restore_pattern_disables_roundtrip() {
7891 let _g = crate::test_util::global_state_lock();
7892 // Set up a non-trivial pattern: alternating slots.
7893 for i in 0..(ZPC_COUNT as usize) {
7894 zpc_disables.lock().unwrap()[i] = (i % 2) as u8;
7895 }
7896 let saved = savepatterndisables();
7897 // Clobber.
7898 *zpc_disables.lock().unwrap() = [9u8; ZPC_COUNT as usize];
7899 // Restore.
7900 restorepatterndisables(saved);
7901 // Verify alternating pattern back.
7902 let table = zpc_disables.lock().unwrap().clone();
7903 for i in 0..(ZPC_COUNT as usize) {
7904 assert_eq!(
7905 table[i],
7906 (i % 2) as u8,
7907 "c:4218+c:4258 round-trip: slot {i} preserved"
7908 );
7909 }
7910 // Reset.
7911 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7912 }
7913
7914 // ═══════════════════════════════════════════════════════════════════
7915 // startpatternscope / endpatternscope / clearpatterndisables —
7916 // pattern.c:4241/4279/4296. Pin LOCALPATTERNS-gated save/restore +
7917 // C `memset(zpc_disables, 0, ZPC_COUNT)` clear.
7918 //
7919 // Bug fixed: prior Rust port operated on a separate
7920 // `patterndisables: Mutex<Vec<String>>` tombstone (cleared by
7921 // clearpatterndisables, push/popped by start/end). C's three fns
7922 // all operate on the canonical `zpc_disables[]` byte array.
7923 // Pre-fix: setopt LOCALPATTERNS function entry/exit was a no-op
7924 // and `clearpatterndisables` didn't clear the matcher's disable
7925 // bytes.
7926 // ═══════════════════════════════════════════════════════════════════
7927
7928 /// `Src/pattern.c:4296-4298` — `memset(zpc_disables, 0, ZPC_COUNT)`.
7929 /// Test that clearpatterndisables zeros every slot.
7930 #[test]
7931 fn clearpatterndisables_zeros_zpc_disables() {
7932 let _g = crate::test_util::global_state_lock();
7933 // Pre-populate every slot.
7934 *zpc_disables.lock().unwrap() = [1u8; ZPC_COUNT as usize];
7935 clearpatterndisables();
7936 let table = zpc_disables.lock().unwrap().clone();
7937 for (i, &v) in table.iter().enumerate() {
7938 assert_eq!(v, 0, "c:4298 — slot {i} cleared");
7939 }
7940 }
7941
7942 /// `Src/pattern.c:4241-4250` + c:4279-4290` — under `setopt
7943 /// LOCALPATTERNS`, function entry save → mutate → exit restore
7944 /// round-trips zpc_disables.
7945 #[test]
7946 fn pattern_scope_save_restore_under_localpatterns() {
7947 let _g = crate::test_util::global_state_lock();
7948 crate::ported::options::opt_state_set("localpatterns", true);
7949
7950 // Initial state: ZPC_BAR disabled, ZPC_STAR enabled.
7951 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7952 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
7953
7954 // Enter scope (c:4241 — `savepatterndisables` into stack frame).
7955 startpatternscope();
7956
7957 // Mutate inside the "function" body.
7958 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7959 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
7960 assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 0);
7961 assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 1);
7962
7963 // Exit scope (c:4279 — restore via restorepatterndisables).
7964 endpatternscope();
7965
7966 // Outer state must come back.
7967 assert_eq!(
7968 zpc_disables.lock().unwrap()[ZPC_BAR as usize],
7969 1,
7970 "c:4287 — ZPC_BAR restored to outer-scope disabled"
7971 );
7972 assert_eq!(
7973 zpc_disables.lock().unwrap()[ZPC_STAR as usize],
7974 0,
7975 "c:4287 — ZPC_STAR restored to outer-scope enabled"
7976 );
7977
7978 // Reset.
7979 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7980 crate::ported::options::opt_state_set("localpatterns", false);
7981 }
7982
7983 /// `Src/pattern.c:4286` — WITHOUT LOCALPATTERNS, endpatternscope
7984 /// pops the stack frame but does NOT restore. Function-body
7985 /// mutations leak into the caller.
7986 #[test]
7987 fn pattern_scope_no_restore_without_localpatterns() {
7988 let _g = crate::test_util::global_state_lock();
7989 crate::ported::options::opt_state_set("localpatterns", false);
7990
7991 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7992 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
7993
7994 startpatternscope();
7995 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7996 endpatternscope();
7997
7998 // Mutation leaked — c:4286 gate skipped restore.
7999 assert_eq!(
8000 zpc_disables.lock().unwrap()[ZPC_BAR as usize],
8001 0,
8002 "c:4286 — WITHOUT LOCALPATTERNS, mutation leaks out"
8003 );
8004
8005 // Reset.
8006 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
8007 }
8008
8009 /// `Src/pattern.c:4241-4250` — nested scopes form a LIFO stack.
8010 /// Inner restore pops just the inner frame; outer frame stays.
8011 #[test]
8012 fn pattern_scope_nested_lifo() {
8013 let _g = crate::test_util::global_state_lock();
8014 crate::ported::options::opt_state_set("localpatterns", true);
8015
8016 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
8017
8018 // Outer.
8019 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
8020 startpatternscope(); // frame A
8021 // Inner.
8022 zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
8023 zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
8024 startpatternscope(); // frame B
8025 // Innermost.
8026 zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 1;
8027 endpatternscope(); // pop B → inner restored
8028 assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 0);
8029 assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 1);
8030 assert_eq!(
8031 zpc_disables.lock().unwrap()[ZPC_INBRACK as usize],
8032 0,
8033 "c:4287 — frame B's snapshot didn't include this slot's 1"
8034 );
8035 endpatternscope(); // pop A → outer restored
8036 assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 1);
8037 assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 0);
8038
8039 // Reset.
8040 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
8041 crate::ported::options::opt_state_set("localpatterns", false);
8042 }
8043
8044 /// `Src/pattern.c:4226` — bit position is the slot INDEX (0-based),
8045 /// not 1-based. Slot 0 → bit 1, slot 1 → bit 2, etc. Per the C
8046 /// `bit = 1` initial and `bit <<= 1` after each slot.
8047 #[test]
8048 fn savepatterndisables_slot_0_is_low_bit() {
8049 let _g = crate::test_util::global_state_lock();
8050 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
8051 zpc_disables.lock().unwrap()[0] = 1;
8052 let disables = savepatterndisables();
8053 assert_eq!(disables, 1, "c:4226 — bit = 1 initial → slot 0 is low bit");
8054 *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
8055 }
8056
8057 /// `Src/pattern.c:1997` — `charsub(x, y)` returns the number of
8058 /// characters between the start and byte offset `y`. Non-multibyte:
8059 /// byte distance; multibyte: codepoint count of `x[0..y]`.
8060 #[test]
8061 fn charsub_counts_chars_to_offset() {
8062 let _g = crate::test_util::global_state_lock();
8063 let saved = crate::ported::options::opt_state_get("multibyte");
8064
8065 // c:2004 — non-multibyte: byte distance `y - x`.
8066 crate::ported::options::opt_state_set("multibyte", false);
8067 assert_eq!(charsub("abc", 3), 3, "non-MB: byte distance to end");
8068 assert_eq!(charsub("abc", 1), 1, "non-MB: one byte in");
8069 assert_eq!(charsub("abc", 0), 0, "c:1997 — at start, distance 0");
8070 assert_eq!(charsub("é日", 5), 5, "non-MB: raw byte count (5 bytes)");
8071
8072 // c:2006-2021 — multibyte: codepoint count of `x[0..y]`.
8073 crate::ported::options::opt_state_set("multibyte", true);
8074 assert_eq!(charsub("abc", 3), 3, "MB: 3 ASCII chars");
8075 assert_eq!(charsub("é", 2), 1, "MB: one 2-byte codepoint");
8076 assert_eq!(charsub("é日", 5), 2, "MB: é + 日 = 2 codepoints");
8077 assert_eq!(charsub("é日", 2), 1, "MB: just é = 1 codepoint");
8078 assert_eq!(charsub("é日", 0), 0, "MB: distance 0 at start");
8079
8080 if let Some(v) = saved {
8081 crate::ported::options::opt_state_set("multibyte", v);
8082 }
8083 }
8084
8085 // ═══════════════════════════════════════════════════════════════════
8086 // KSH_GLOB extended patterns: +(pat), *(pat), ?(pat), @(pat), !(pat).
8087 // Anchored against `setopt KSH_GLOB; [[ str == ${~pat} ]]` in zsh 5.9.
8088 // Tests enable KSH_GLOB before each assertion and restore prior state.
8089 //
8090 // Ported from pattern.c:1278-1350 (KSH dispatch in patcomppiece) and
8091 // pattern.c:1615-1746 (kshchar-driven quantifier emission), plus
8092 // patcompnot pattern.c:1759-1784 for the !(pat) negation form and a
8093 // minimal P_EXCLUDE matcher arm (pattern.c:3056-3201).
8094 // ═══════════════════════════════════════════════════════════════════
8095
8096 fn ksh_glob_match(pat: &str, s: &str) -> bool {
8097 let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
8098 let saved = opt_state_get("kshglob").unwrap_or(false);
8099 opt_state_set("kshglob", true);
8100 let r = patmatch(pat, s);
8101 opt_state_set("kshglob", saved);
8102 r
8103 }
8104
8105 // ── +(pat) — one or more ─────────────────────────────────────────
8106 /// `+(foo)` matches `foo` — zsh: MATCH.
8107 #[test]
8108 fn ksh_glob_plus_one_repetition_matches() {
8109 let _g = crate::test_util::global_state_lock();
8110 assert!(ksh_glob_match("+(foo)", "foo"));
8111 }
8112
8113 /// `+(foo)` matches `foofoo` — zsh: MATCH.
8114 #[test]
8115 fn ksh_glob_plus_two_repetitions_matches() {
8116 let _g = crate::test_util::global_state_lock();
8117 assert!(ksh_glob_match("+(foo)", "foofoo"));
8118 }
8119
8120 /// `+(foo)` does NOT match `""` — zsh: NOMATCH (and zshrs agrees).
8121 /// This is the ONLY +(pat) test that passes — both reject empty.
8122 #[test]
8123 fn ksh_glob_plus_zero_repetitions_fails() {
8124 let _g = crate::test_util::global_state_lock();
8125 assert!(!ksh_glob_match("+(foo)", ""));
8126 }
8127
8128 /// `+(foo)` matches `foofoofoo` — zsh: MATCH.
8129 #[test]
8130 fn ksh_glob_plus_three_repetitions_matches() {
8131 let _g = crate::test_util::global_state_lock();
8132 assert!(ksh_glob_match("+(foo)", "foofoofoo"));
8133 }
8134
8135 // ── *(pat) — zero or more ────────────────────────────────────────
8136 /// `*(foo)` matches empty — zsh: MATCH.
8137 #[test]
8138 fn ksh_glob_star_paren_matches_empty() {
8139 let _g = crate::test_util::global_state_lock();
8140 assert!(ksh_glob_match("*(foo)", ""));
8141 }
8142
8143 /// `*(foo)` matches `foo` — both zsh and zshrs agree (passes).
8144 #[test]
8145 fn ksh_glob_star_paren_matches_one() {
8146 let _g = crate::test_util::global_state_lock();
8147 assert!(ksh_glob_match("*(foo)", "foo"));
8148 }
8149
8150 /// `*(foo)` matches `foofoo` — both agree.
8151 #[test]
8152 fn ksh_glob_star_paren_matches_multiple() {
8153 let _g = crate::test_util::global_state_lock();
8154 assert!(ksh_glob_match("*(foo)", "foofoo"));
8155 }
8156
8157 // ── ?(pat) — zero or one ─────────────────────────────────────────
8158 /// `?(foo)` matches empty — zsh: MATCH.
8159 #[test]
8160 fn ksh_glob_question_paren_matches_empty() {
8161 let _g = crate::test_util::global_state_lock();
8162 assert!(ksh_glob_match("?(foo)", ""));
8163 }
8164
8165 /// `?(foo)` matches `foo` — zsh: MATCH.
8166 #[test]
8167 fn ksh_glob_question_paren_matches_one_rep() {
8168 let _g = crate::test_util::global_state_lock();
8169 assert!(ksh_glob_match("?(foo)", "foo"));
8170 }
8171
8172 /// `?(foo)` does NOT match `foofoo` — both agree (rejects).
8173 #[test]
8174 fn ksh_glob_question_paren_fails_on_two_reps() {
8175 let _g = crate::test_util::global_state_lock();
8176 assert!(!ksh_glob_match("?(foo)", "foofoo"));
8177 }
8178
8179 // ── @(pat) — exactly one ─────────────────────────────────────────
8180 /// `@(foo)` matches `foo` — zsh: MATCH. zshrs fails on the alternation
8181 /// form but matches the single-branch form.
8182 #[test]
8183 fn ksh_glob_at_paren_matches_exact() {
8184 let _g = crate::test_util::global_state_lock();
8185 assert!(ksh_glob_match("@(foo)", "foo"));
8186 }
8187
8188 /// `@(foo|bar)` matches both `foo` AND `bar` — zsh: MATCH both.
8189 #[test]
8190 fn ksh_glob_at_paren_with_alternation_either_branch() {
8191 let _g = crate::test_util::global_state_lock();
8192 assert!(ksh_glob_match("@(foo|bar)", "foo"));
8193 assert!(ksh_glob_match("@(foo|bar)", "bar"));
8194 }
8195
8196 /// `@(foo|bar)` rejects `qux` — both agree.
8197 #[test]
8198 fn ksh_glob_at_paren_alternation_rejects_outside() {
8199 let _g = crate::test_util::global_state_lock();
8200 assert!(!ksh_glob_match("@(foo|bar)", "qux"));
8201 }
8202
8203 // ── !(pat) — not match ───────────────────────────────────────────
8204 /// `!(foo)` rejects `foo` — both agree.
8205 #[test]
8206 fn ksh_glob_bang_paren_rejects_matching_string() {
8207 let _g = crate::test_util::global_state_lock();
8208 assert!(!ksh_glob_match("!(foo)", "foo"));
8209 }
8210
8211 /// `!(foo)` matches `bar` — zsh: MATCH.
8212 #[test]
8213 fn ksh_glob_bang_paren_matches_non_matching_string() {
8214 let _g = crate::test_util::global_state_lock();
8215 assert!(ksh_glob_match("!(foo)", "bar"));
8216 }
8217
8218 /// `!(foo|bar)` matches `baz` and rejects `foo` — zsh.
8219 #[test]
8220 fn ksh_glob_bang_paren_with_alternation() {
8221 let _g = crate::test_util::global_state_lock();
8222 assert!(ksh_glob_match("!(foo|bar)", "baz"));
8223 assert!(!ksh_glob_match("!(foo|bar)", "foo"));
8224 }
8225
8226 // ── Mixed with literal context ───────────────────────────────────
8227 /// `pre+(x)post` matches `prexpost`, `prexxpost` — zsh: MATCH both.
8228 #[test]
8229 fn ksh_glob_plus_paren_in_literal_context() {
8230 let _g = crate::test_util::global_state_lock();
8231 assert!(ksh_glob_match("pre+(x)post", "prexpost"));
8232 assert!(ksh_glob_match("pre+(x)post", "prexxpost"));
8233 }
8234
8235 /// `pre+(x)post` rejects `prepost` — both agree (no x's).
8236 #[test]
8237 fn ksh_glob_plus_paren_in_literal_rejects_zero_reps() {
8238 let _g = crate::test_util::global_state_lock();
8239 assert!(!ksh_glob_match("pre+(x)post", "prepost"));
8240 }
8241
8242 // ═══════════════════════════════════════════════════════════════════
8243 // EXTENDED_GLOB pattern flags: (#i) case-insensitive, (#l) lowercase
8244 // matches uppercase, (#aN) approximate match. Anchored to
8245 // `setopt EXTENDED_GLOB; [[ str == ${~pat} ]]` in real zsh 5.9.
8246 // ═══════════════════════════════════════════════════════════════════
8247
8248 fn ext_glob_match(pat: &str, s: &str) -> bool {
8249 let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
8250 let saved = opt_state_get("extendedglob").unwrap_or(false);
8251 opt_state_set("extendedglob", true);
8252 let r = patmatch(pat, s);
8253 opt_state_set("extendedglob", saved);
8254 r
8255 }
8256
8257 // ── (#i) case-insensitive ───────────────────────────────────────
8258 /// `(#i)FOO` matches "foo" (case ignored).
8259 #[test]
8260 fn ext_glob_hash_i_matches_lowercase_against_uppercase_pat() {
8261 let _g = crate::test_util::global_state_lock();
8262 assert!(ext_glob_match("(#i)FOO", "foo"));
8263 }
8264
8265 /// `(#i)FOO` matches "FOO" exactly.
8266 #[test]
8267 fn ext_glob_hash_i_matches_exact_case() {
8268 let _g = crate::test_util::global_state_lock();
8269 assert!(ext_glob_match("(#i)FOO", "FOO"));
8270 }
8271
8272 /// `(#i)FOO` matches mixed-case "FoO".
8273 #[test]
8274 fn ext_glob_hash_i_matches_mixed_case() {
8275 let _g = crate::test_util::global_state_lock();
8276 assert!(ext_glob_match("(#i)FOO", "FoO"));
8277 }
8278
8279 /// `(#i)foo` rejects unrelated string "BAR".
8280 #[test]
8281 fn ext_glob_hash_i_rejects_unrelated_string() {
8282 let _g = crate::test_util::global_state_lock();
8283 assert!(!ext_glob_match("(#i)foo", "BAR"));
8284 }
8285
8286 // ── (#l) lowercase-pattern matches uppercase-text ───────────────
8287 /// `(#l)foo` matches "FOO" (lowercase pattern allows uppercase).
8288 /// Distinct from (#i): asymmetric — pattern letter must be lowercase
8289 /// AND text may be either case for that letter.
8290 #[test]
8291 fn ext_glob_hash_l_lowercase_pat_matches_uppercase_text() {
8292 let _g = crate::test_util::global_state_lock();
8293 assert!(ext_glob_match("(#l)foo", "FOO"));
8294 }
8295
8296 /// `(#l)foo` matches lowercase "foo".
8297 #[test]
8298 fn ext_glob_hash_l_lowercase_pat_matches_lowercase_text() {
8299 let _g = crate::test_util::global_state_lock();
8300 assert!(ext_glob_match("(#l)foo", "foo"));
8301 }
8302
8303 /// `(#l)FOO` does NOT match "foo" — asymmetry: uppercase in pattern
8304 /// requires uppercase in text. zsh: NOMATCH.
8305 #[test]
8306 fn ext_glob_hash_l_uppercase_pat_requires_uppercase_text_anchored_to_zsh() {
8307 let _g = crate::test_util::global_state_lock();
8308 assert!(
8309 !ext_glob_match("(#l)FOO", "foo"),
8310 "zsh: (#l)FOO must NOT match \"foo\" — uppercase-in-pattern is anchored"
8311 );
8312 }
8313
8314 // ── (#aN) approximate match (Damerau-Levenshtein distance ≤ N) ──
8315 /// `(#a1)foo` matches "fop" (1 char substitution).
8316 #[test]
8317 fn ext_glob_hash_a1_matches_one_substitution_anchored_to_zsh() {
8318 let _g = crate::test_util::global_state_lock();
8319 assert!(
8320 ext_glob_match("(#a1)foo", "fop"),
8321 "zsh: (#a1)foo matches 'fop' (1 substitution)"
8322 );
8323 }
8324
8325 /// `(#a2)foo` matches "fxy" (2 substitutions).
8326 #[test]
8327 fn ext_glob_hash_a2_matches_two_substitutions_anchored_to_zsh() {
8328 let _g = crate::test_util::global_state_lock();
8329 assert!(
8330 ext_glob_match("(#a2)foo", "fxy"),
8331 "zsh: (#a2)foo matches 'fxy' (2 substitutions)"
8332 );
8333 }
8334
8335 /// `(#a1)foo` rejects "fxy" (2 substitutions > limit 1).
8336 #[test]
8337 fn ext_glob_hash_a1_rejects_two_substitutions() {
8338 let _g = crate::test_util::global_state_lock();
8339 assert!(!ext_glob_match("(#a1)foo", "fxy"));
8340 }
8341
8342 // ═══════════════════════════════════════════════════════════════════
8343 // (#aN) full Damerau-Levenshtein — sub/ins/del edit operations.
8344 // C inlines the backtracking in P_EXACTLY (c:2737-2779) plus the
8345 // approx-aware sync nodes at c:3055+. Rust factors into
8346 // `approx_match_exactly` (WARNING: NOT IN PATTERN.C). Tests pin
8347 // each edit operation independently + the budget upper bound.
8348 // ═══════════════════════════════════════════════════════════════════
8349
8350 /// `(#a0)foo` is exact-match only — no edits allowed.
8351 #[test]
8352 fn ext_glob_hash_a0_is_exact_match_only() {
8353 let _g = crate::test_util::global_state_lock();
8354 assert!(ext_glob_match("(#a0)foo", "foo"), "exact ok");
8355 assert!(
8356 !ext_glob_match("(#a0)foo", "fop"),
8357 "no edit budget rejects 1-sub"
8358 );
8359 }
8360
8361 /// `(#a1)foo` matches "fxoo" via INSERTION in input (extra 'x').
8362 /// The pattern has no `x`; treating `x` as an inserted-in-input
8363 /// char costs 1 error. Input → "f(x)oo" with the 'x' deleted.
8364 #[test]
8365 fn ext_glob_hash_a_accepts_insertion_in_input() {
8366 let _g = crate::test_util::global_state_lock();
8367 // "fxoo" — extra 'x' between 'f' and 'oo'.
8368 assert!(
8369 ext_glob_match("(#a1)foo", "fxoo"),
8370 "1 insertion-in-input edit"
8371 );
8372 }
8373
8374 /// `(#a1)foo` matches "fo" via DELETION from input (missing 'o').
8375 /// One 'o' deleted from input compared to pattern.
8376 #[test]
8377 fn ext_glob_hash_a_accepts_deletion_from_input() {
8378 let _g = crate::test_util::global_state_lock();
8379 assert!(
8380 ext_glob_match("(#a1)foo", "fo"),
8381 "1 deletion-from-input edit"
8382 );
8383 }
8384
8385 /// `(#a2)foo` matches "fxooy" via 1 insertion + 1 substitution.
8386 /// 'x' inserted between f-o; final 'o' substituted to 'y'. Wait —
8387 /// closer reading: "fxooy" vs "foo" — 'x' is the extra char, but
8388 /// then 'ooy' vs 'oo' has another extra 'y'. That's 2 insertions.
8389 #[test]
8390 fn ext_glob_hash_a_mixed_edits_within_budget() {
8391 let _g = crate::test_util::global_state_lock();
8392 // 2 insertions: 'x' and 'y'.
8393 assert!(
8394 ext_glob_match("(#a2)foo", "fxooy"),
8395 "2 insertions in input within budget"
8396 );
8397 }
8398
8399 /// Budget upper bound: `(#a1)abc` rejects "xyz" (3 substitutions > 1).
8400 #[test]
8401 fn ext_glob_hash_a1_rejects_three_substitutions() {
8402 let _g = crate::test_util::global_state_lock();
8403 assert!(!ext_glob_match("(#a1)abc", "xyz"));
8404 }
8405
8406 /// `(#a3)abc` matches "xyz" (3 substitutions ≤ 3).
8407 #[test]
8408 fn ext_glob_hash_a3_accepts_all_substituted() {
8409 let _g = crate::test_util::global_state_lock();
8410 assert!(
8411 ext_glob_match("(#a3)abc", "xyz"),
8412 "3 substitutions at budget 3"
8413 );
8414 }
8415
8416 /// Position-independent substitution: budget allows replacing
8417 /// any one of N pattern chars.
8418 #[test]
8419 fn ext_glob_hash_a_accepts_position_independent_substitution() {
8420 let _g = crate::test_util::global_state_lock();
8421 assert!(ext_glob_match("(#a1)abc", "Xbc"), "first-char sub");
8422 assert!(ext_glob_match("(#a1)abc", "aXc"), "middle-char sub");
8423 assert!(ext_glob_match("(#a1)abc", "abX"), "last-char sub");
8424 }
8425
8426 // ═══════════════════════════════════════════════════════════════════
8427 // zsh test-corpus pins — direct anchors to Test/D02glob.ztst:25-80
8428 // "zsh globbing" test list (zsh's authoritative regression suite).
8429 // Each test cites the ztst line range it pins. Currently-failing
8430 // tests get #[ignore = "ZSHRS BUG: ..."] markers; expected-output
8431 // assertions stay in tree so the marker flips when the Rust port
8432 // catches up.
8433 // ═══════════════════════════════════════════════════════════════════
8434
8435 /// `Test/D02glob.ztst:26` — `[[ foo~ = foo~ ]]` exact match,
8436 /// expected exit 0 (true).
8437 #[test]
8438 fn zsh_corpus_foo_tilde_exact_match() {
8439 let _g = crate::test_util::global_state_lock();
8440 assert!(
8441 ext_glob_match("foo~", "foo~"),
8442 "ztst:26 — literal tilde matches"
8443 );
8444 }
8445
8446 /// `Test/D02glob.ztst:27` — `[[ foo~ = (foo~) ]]` parenthesised
8447 /// single alternative.
8448 #[test]
8449 fn zsh_corpus_foo_tilde_in_parens() {
8450 let _g = crate::test_util::global_state_lock();
8451 assert!(
8452 ext_glob_match("(foo~)", "foo~"),
8453 "ztst:27 — (foo~) matches foo~"
8454 );
8455 }
8456
8457 /// `Test/D02glob.ztst:28` — `[[ foo~ = (foo~|) ]]` alternation
8458 /// with empty alternative.
8459 #[test]
8460 fn zsh_corpus_alternation_with_empty_alt() {
8461 let _g = crate::test_util::global_state_lock();
8462 assert!(
8463 ext_glob_match("(foo~|)", "foo~"),
8464 "ztst:28 — empty alt accepted"
8465 );
8466 }
8467
8468 /// `Test/D02glob.ztst:29` — `[[ foo.c = *.c~boo* ]]` exclude
8469 /// pattern: matches *.c BUT NOT boo*. `foo.c` matches *.c and
8470 /// doesn't match boo*, so result is 0 (true).
8471 #[test]
8472 fn zsh_corpus_exclude_pattern_basic() {
8473 let _g = crate::test_util::global_state_lock();
8474 assert!(
8475 ext_glob_match("*.c~boo*", "foo.c"),
8476 "ztst:29 — *.c~boo* matches foo.c"
8477 );
8478 }
8479
8480 /// `Test/D02glob.ztst:30` — `[[ foo.c = *.c~boo*~foo* ]]`
8481 /// double-exclude: matches *.c but excludes both `boo*` and
8482 /// `foo*`. `foo.c` matches `foo*` (excluded) → result 1 (false).
8483 #[test]
8484 fn zsh_corpus_exclude_pattern_double() {
8485 let _g = crate::test_util::global_state_lock();
8486 assert!(
8487 !ext_glob_match("*.c~boo*~foo*", "foo.c"),
8488 "ztst:30 — double exclude rejects foo.c"
8489 );
8490 }
8491
8492 /// `Test/D02glob.ztst:31` — `[[ fofo = (fo#)# ]]` — `#` is the
8493 /// extended-glob "1+ repetitions" quantifier. `(fo#)#` matches
8494 /// 1+ repetitions of "fo+".
8495 #[test]
8496 fn zsh_corpus_hash_repetition_double() {
8497 let _g = crate::test_util::global_state_lock();
8498 assert!(
8499 ext_glob_match("(fo#)#", "fofo"),
8500 "ztst:31 — (fo#)# matches fofo"
8501 );
8502 }
8503
8504 /// `Test/D02glob.ztst:32` — `[[ ffo = (fo#)# ]]`. `fo#` means
8505 /// `f` followed by 0+ `o`. `ffo` = `f` + `fo` (matches the
8506 /// outer `#` repetition).
8507 #[test]
8508 fn zsh_corpus_hash_repetition_min_one() {
8509 let _g = crate::test_util::global_state_lock();
8510 assert!(
8511 ext_glob_match("(fo#)#", "ffo"),
8512 "ztst:32 — (fo#)# matches ffo via 1-iter outer"
8513 );
8514 }
8515
8516 /// `Test/D02glob.ztst:36` — `[[ foooofof = (fo##)# ]]` —
8517 /// `##` is "2+ repetitions". `fo##` = `f` + 2+ `o`'s. The
8518 /// trailing `of` after `foooo` doesn't satisfy the trailing
8519 /// outer `#`, so result is 1 (false).
8520 #[test]
8521 fn zsh_corpus_hash_hash_quantifier_min_two() {
8522 let _g = crate::test_util::global_state_lock();
8523 assert!(
8524 !ext_glob_match("(fo##)#", "foooofof"),
8525 "ztst:36 — (fo##)# rejects foooofof"
8526 );
8527 }
8528
8529 /// `Test/D02glob.ztst:50` — `[[ aac = ((a))#a(c) ]]` —
8530 /// `((a))#` is `0+ a`. `aac` = `aa` (= `((a))#`) + `c`. The
8531 /// final `a(c)` requires literal `a` followed by capture `(c)`.
8532 /// Wait — should be `aac` matches `((a))#a(c)`: outer `((a))#`
8533 /// captures `a` repeated, then literal `a`, then `(c)`.
8534 /// Actually re-reading: `((a))#` matches 0+ 'a'. `aac` =
8535 /// `((a))#`("a") + `a`("a") + `(c)`("c"). So "aa" splits as
8536 /// (a)("a") + a("a") + c("c"). Match.
8537 #[test]
8538 fn zsh_corpus_nested_paren_quantifier() {
8539 let _g = crate::test_util::global_state_lock();
8540 assert!(
8541 ext_glob_match("((a))#a(c)", "aac"),
8542 "ztst:50 — ((a))#a(c) matches aac"
8543 );
8544 }
8545
8546 /// `Test/D02glob.ztst:51` — `[[ ac = ((a))#a(c) ]]` — single
8547 /// `a` matches `a(c)` after empty `((a))#` consumes zero.
8548 #[test]
8549 fn zsh_corpus_zero_iteration_quantifier() {
8550 let _g = crate::test_util::global_state_lock();
8551 assert!(
8552 ext_glob_match("((a))#a(c)", "ac"),
8553 "ztst:51 — ((a))# can be zero iters"
8554 );
8555 }
8556
8557 /// `Test/D02glob.ztst:52` — `[[ c = ((a))#a(c) ]]` — empty
8558 /// `((a))#` + literal `a` requires at least one `a`. `c` has
8559 /// no `a`, so result is 1 (false).
8560 #[test]
8561 fn zsh_corpus_required_literal_after_zero_quantifier() {
8562 let _g = crate::test_util::global_state_lock();
8563 assert!(
8564 !ext_glob_match("((a))#a(c)", "c"),
8565 "ztst:52 — bare `c` lacks required `a`"
8566 );
8567 }
8568
8569 /// `Test/D02glob.ztst:73` — `[[ foo = ((^x)) ]]` — exclude
8570 /// `x`. `foo` doesn't contain `x` as a single char (the
8571 /// `((^x))` matches anything that's not just 'x' — `foo` is 3
8572 /// chars, none is `x`).
8573 #[test]
8574 fn zsh_corpus_caret_exclude_basic() {
8575 let _g = crate::test_util::global_state_lock();
8576 assert!(
8577 ext_glob_match("((^x))", "foo"),
8578 "ztst:73 — ((^x)) matches foo"
8579 );
8580 }
8581
8582 /// `Test/D02glob.ztst:75` — `[[ foo = ((^foo)) ]]` — `^foo`
8583 /// rejects exact `foo`. `foo` matches `foo` literal, which the
8584 /// `^` inverts. Result: 1 (false).
8585 #[test]
8586 fn zsh_corpus_caret_exclude_exact_match_inverted() {
8587 let _g = crate::test_util::global_state_lock();
8588 assert!(
8589 !ext_glob_match("((^foo))", "foo"),
8590 "ztst:75 — ((^foo)) rejects foo"
8591 );
8592 }
8593
8594 /// `Test/D02glob.ztst:79` — `[[ foot = z*~*x ]]` — `*` then
8595 /// exclude `*x`. `foot` doesn't start with `z`, so doesn't
8596 /// match `z*` at all → result 1 (false).
8597 #[test]
8598 fn zsh_corpus_star_exclude_no_z_prefix() {
8599 let _g = crate::test_util::global_state_lock();
8600 assert!(
8601 !ext_glob_match("z*~*x", "foot"),
8602 "ztst:79 — foot doesn't start with z"
8603 );
8604 }
8605
8606 /// `Test/D02glob.ztst:80` — `[[ zoot = z*~*x ]]` — `zoot`
8607 /// matches `z*` AND doesn't end in `x` → result 0 (true).
8608 #[test]
8609 fn zsh_corpus_star_exclude_zoot() {
8610 let _g = crate::test_util::global_state_lock();
8611 assert!(
8612 ext_glob_match("z*~*x", "zoot"),
8613 "ztst:80 — zoot matches z* and not *x"
8614 );
8615 }
8616
8617 // ─── POSIX class brackets — Test/D02glob.ztst:111-118 ─────────────
8618
8619 /// `Test/D02glob.ztst:111` — `[[:alpha:][:punct:]]#[[:digit:]][^[:lower:]]`
8620 /// over "a%1X": `a` matches alpha, `%` matches punct (1+ via `#`),
8621 /// `1` matches digit, `X` matches NOT-lower.
8622 #[test]
8623 fn zsh_corpus_posix_class_alpha_punct_digit_notlower() {
8624 let _g = crate::test_util::global_state_lock();
8625 assert!(
8626 ext_glob_match("[[:alpha:][:punct:]]#[[:digit:]][^[:lower:]]", "a%1X"),
8627 "ztst:111 — alpha-punct-digit-notlower chain",
8628 );
8629 }
8630
8631 /// `Test/D02glob.ztst:112` — same pattern, "a%1" lacks the
8632 /// 4th non-lower char → result 1 (false).
8633 #[test]
8634 fn zsh_corpus_posix_class_rejects_short_input() {
8635 let _g = crate::test_util::global_state_lock();
8636 assert!(
8637 !ext_glob_match("[[:alpha:][:punct:]]#[[:digit:]][^[:lower:]]", "a%1"),
8638 "ztst:112 — short input rejected",
8639 );
8640 }
8641
8642 /// `Test/D02glob.ztst:113` — `[[:` literal in char class:
8643 /// `[[ [: = [[:]# ]]` — `[[:]` matches `[` OR `:`, `#` is 1+
8644 /// repetitions. "[:" = `[` + `:` → matches.
8645 #[test]
8646 fn zsh_corpus_literal_brackets_in_class_with_repetition() {
8647 let _g = crate::test_util::global_state_lock();
8648 assert!(
8649 ext_glob_match("[[:]#", "[:"),
8650 "ztst:113 — [[:]# matches '[:'"
8651 );
8652 }
8653
8654 // ─── (#i) / (#l) case modifiers — Test/D02glob.ztst:119-132 ───────
8655
8656 /// `Test/D02glob.ztst:119` — `(#i)FOOXX` matches "fooxx"
8657 /// (case-insensitive throughout).
8658 #[test]
8659 fn zsh_corpus_hash_i_case_insensitive() {
8660 let _g = crate::test_util::global_state_lock();
8661 assert!(
8662 ext_glob_match("(#i)FOOXX", "fooxx"),
8663 "ztst:119 — (#i)FOOXX matches fooxx"
8664 );
8665 }
8666
8667 /// `Test/D02glob.ztst:120` — `(#l)FOOXX` requires pattern UPPER
8668 /// chars to be the ONLY uppercase candidates; lowercase input
8669 /// FAILS to match because pattern is all uppercase and `#l` only
8670 /// matches the EXACT lowercase variant of the pattern char... actually
8671 /// `(#l)` means "lowercase pattern chars match uppercase too" —
8672 /// `(#l)FOOXX` has no lowercase chars so it stays a strict
8673 /// uppercase match. "fooxx" doesn't match. Result 1 (false).
8674 #[test]
8675 fn zsh_corpus_hash_l_uppercase_pattern_with_lower_input_fails() {
8676 let _g = crate::test_util::global_state_lock();
8677 assert!(
8678 !ext_glob_match("(#l)FOOXX", "fooxx"),
8679 "ztst:120 — (#l)FOOXX does NOT match fooxx"
8680 );
8681 }
8682
8683 /// `Test/D02glob.ztst:121` — `(#l)fooxx` (lowercase pattern) DOES
8684 /// match "FOOXX" because `#l` is the asymmetric "lowercase pat
8685 /// chars match upper-or-lower" rule.
8686 #[test]
8687 fn zsh_corpus_hash_l_lowercase_pattern_matches_uppercase_input() {
8688 let _g = crate::test_util::global_state_lock();
8689 assert!(
8690 ext_glob_match("(#l)fooxx", "FOOXX"),
8691 "ztst:121 — (#l)fooxx matches FOOXX (asymmetric upcasing)"
8692 );
8693 }
8694
8695 /// `Test/D02glob.ztst:122` — `(#i)FOO(#I)X(#i)X` mixes case
8696 /// modes. `(#I)` cancels prior `(#i)`. So 4th char `X` requires
8697 /// exact case. "fooxx" has lowercase `x` at that position → fail.
8698 #[test]
8699 fn zsh_corpus_hash_capital_i_cancels_case_insensitive() {
8700 let _g = crate::test_util::global_state_lock();
8701 assert!(
8702 !ext_glob_match("(#i)FOO(#I)X(#i)X", "fooxx"),
8703 "ztst:122 — (#I) cancels (#i), 4th char `X` requires upper"
8704 );
8705 }
8706
8707 /// `Test/D02glob.ztst:123` — same pattern with input "fooXx":
8708 /// `(#i)FOO` matches "foo", `(#I)X` matches "X" exact-case,
8709 /// `(#i)X` matches "x" insensitive → result 0 (true).
8710 #[test]
8711 fn zsh_corpus_hash_i_capital_i_toggle_succeeds() {
8712 let _g = crate::test_util::global_state_lock();
8713 assert!(
8714 ext_glob_match("(#i)FOO(#I)X(#i)X", "fooXx"),
8715 "ztst:123 — mixed case modifiers succeed"
8716 );
8717 }
8718
8719 /// `Test/D02glob.ztst:128` — `(#i)*m*` matches "Modules"
8720 /// case-insensitively.
8721 #[test]
8722 fn zsh_corpus_hash_i_with_star_glob() {
8723 let _g = crate::test_util::global_state_lock();
8724 assert!(
8725 ext_glob_match("(#i)*m*", "Modules"),
8726 "ztst:128 — (#i)*m* case-insensitive substring"
8727 );
8728 }
8729
8730 // ─── Numeric ranges `<n-m>` — Test/D02glob.ztst:133-137 ───────────
8731
8732 /// `Test/D02glob.ztst:133` — `<1-1000>33` matches "633" (the
8733 /// "6" is in [1..1000], followed by literal "33"). Actually
8734 /// the way zsh parses: `<1-1000>` matches ONE number from
8735 /// 1-1000, then `33` is literal. "633" = `6`(in 1-1000 range) +
8736 /// "33"(literal). So `<1-1000>33` matches "633".
8737 #[test]
8738 fn zsh_corpus_numeric_range_one_to_thousand() {
8739 let _g = crate::test_util::global_state_lock();
8740 assert!(
8741 ext_glob_match("<1-1000>33", "633"),
8742 "ztst:133 — <1-1000>33 matches 633"
8743 );
8744 }
8745
8746 /// `Test/D02glob.ztst:136` — `<->33` is the open-ended range
8747 /// (any number) followed by 33. Matches "633".
8748 #[test]
8749 fn zsh_corpus_numeric_range_open_ended() {
8750 let _g = crate::test_util::global_state_lock();
8751 assert!(
8752 ext_glob_match("<->33", "633"),
8753 "ztst:136 — <->33 matches 633 (any number)"
8754 );
8755 }
8756
8757 // ─── (#a) approximate match details — Test/D02glob.ztst:147-164 ───
8758
8759 /// `Test/D02glob.ztst:147` — `(#a1)[b][b]` matches "bob" via
8760 /// 1 substitution (middle `o` substituted in `[b][b]`'s gap).
8761 /// Wait — `[b][b]` is two-char "bb". Match against "bob" requires
8762 /// 1 edit. With (#a1), allowed.
8763 #[test]
8764 fn zsh_corpus_hash_a1_bracket_class_with_one_edit() {
8765 let _g = crate::test_util::global_state_lock();
8766 assert!(
8767 ext_glob_match("(#a1)[b][b]", "bob"),
8768 "ztst:147 — (#a1)[b][b] matches bob via 1 edit"
8769 );
8770 }
8771
8772 /// `Test/D02glob.ztst:151` — `(#a2)XbcX` matches "abcd" via
8773 /// 2 substitutions (a↔X, d↔X).
8774 #[test]
8775 fn zsh_corpus_hash_a2_two_substitutions() {
8776 let _g = crate::test_util::global_state_lock();
8777 assert!(
8778 ext_glob_match("(#a2)XbcX", "abcd"),
8779 "ztst:151 — 2 substitutions allowed"
8780 );
8781 }
8782
8783 /// `Test/D02glob.ztst:152` — `(#a2)ad` matches "abcd" via
8784 /// 2 INSERTIONS (b and c inserted into input vs pattern).
8785 /// Pattern is "ad" (2 chars), input is "abcd" (4 chars).
8786 /// Diff: 2 extra chars in input.
8787 #[test]
8788 fn zsh_corpus_hash_a2_two_insertions_in_input() {
8789 let _g = crate::test_util::global_state_lock();
8790 assert!(
8791 ext_glob_match("(#a2)ad", "abcd"),
8792 "ztst:152 — (#a2)ad matches abcd via 2 insertions in input"
8793 );
8794 }
8795
8796 /// `Test/D02glob.ztst:153` — `(#a2)abcd` matches "ad" via
8797 /// 2 DELETIONS from input (b and c missing from input vs pattern).
8798 #[test]
8799 fn zsh_corpus_hash_a2_two_deletions_from_input() {
8800 let _g = crate::test_util::global_state_lock();
8801 assert!(
8802 ext_glob_match("(#a2)abcd", "ad"),
8803 "ztst:153 — (#a2)abcd matches ad via 2 deletions"
8804 );
8805 }
8806
8807 /// `Test/D02glob.ztst:158` — `(#a2)abcd` rejects "dcba" — 4
8808 /// changes needed (full reverse) > budget 2.
8809 #[test]
8810 fn zsh_corpus_hash_a2_rejects_full_reverse() {
8811 let _g = crate::test_util::global_state_lock();
8812 assert!(
8813 !ext_glob_match("(#a2)abcd", "dcba"),
8814 "ztst:158 — full reverse exceeds budget 2"
8815 );
8816 }
8817
8818 /// `Test/D02glob.ztst:159` — `(#a3)abcd` matches "dcba" via
8819 /// 3 substitutions (positions 0,1,3 of input). Wait — actually
8820 /// "dcba" vs "abcd" — 4 positions differ. But (#a3) allows
8821 /// 3 errors. Hmm, but ztst says result is 0 (true). So zsh
8822 /// accepts. Possibly via DAMERAU transposition (swap adjacent).
8823 /// Without Damerau swap, this needs 4 subs → false. With
8824 /// transposition, "dcba" → "cdba" → "dcab"... complicated.
8825 /// Mark ignore — Damerau transposition isn't in the Rust port.
8826 #[test]
8827 fn zsh_corpus_hash_a3_reverse_via_transposition() {
8828 let _g = crate::test_util::global_state_lock();
8829 assert!(
8830 ext_glob_match("(#a3)abcd", "dcba"),
8831 "ztst:159 — (#a3)abcd matches dcba via Damerau transpositions"
8832 );
8833 }
8834
8835 // ─── (#s) start / (#e) end anchors — Test/D02glob.ztst:168-179 ────
8836
8837 /// `Test/D02glob.ztst:168` — `*((#s)|/)test((#e)|/)*` matches
8838 /// "test" — start-anchor (#s) at position 0, end-anchor (#e)
8839 /// after "test".
8840 #[test]
8841 fn zsh_corpus_hash_s_e_anchors_match_bare_test() {
8842 let _g = crate::test_util::global_state_lock();
8843 assert!(
8844 ext_glob_match("*((#s)|/)test((#e)|/)*", "test"),
8845 "ztst:168 — start/end anchors match bare 'test'",
8846 );
8847 }
8848
8849 /// `Test/D02glob.ztst:172` — `*((#s)|/)test((#e)|/)*` rejects
8850 /// "atest" — `(#s)|/` requires position 0 OR a `/`, but `a`
8851 /// is neither.
8852 #[test]
8853 fn zsh_corpus_hash_s_anchor_rejects_a_prefix() {
8854 let _g = crate::test_util::global_state_lock();
8855 assert!(
8856 !ext_glob_match("*((#s)|/)test((#e)|/)*", "atest"),
8857 "ztst:172 — atest fails the (#s) start-or-slash anchor",
8858 );
8859 }
8860
8861 // ─── Misc/globtests pins — `~` exclusion and `^` negation ─────────
8862
8863 /// `Misc/globtests` — `[[ foo~ = foo~ ]]` — bare tilde is literal.
8864 #[test]
8865 fn zsh_corpus_literal_tilde_in_pattern() {
8866 let _g = crate::test_util::global_state_lock();
8867 assert!(
8868 ext_glob_match("foo~", "foo~"),
8869 "literal ~ in non-extglob position"
8870 );
8871 }
8872
8873 /// `Misc/globtests` — `[[ foo.c = *.c~boo* ]]` — extended-glob
8874 /// exclusion: `*.c` minus things matching `boo*`. "foo.c" doesn't
8875 /// match "boo*", so it stays.
8876 #[test]
8877 fn zsh_corpus_exclusion_keeps_non_excluded() {
8878 let _g = crate::test_util::global_state_lock();
8879 assert!(ext_glob_match("*.c~boo*", "foo.c"), "exclusion keeps foo.c");
8880 }
8881
8882 /// `Misc/globtests` — `[[ foo.c = *.c~boo*~foo* ]]` — chained
8883 /// exclusions: `*.c` minus `boo*` minus `foo*`. "foo.c" matches
8884 /// `foo*`, so excluded.
8885 #[test]
8886 fn zsh_corpus_chained_exclusion_removes_match() {
8887 let _g = crate::test_util::global_state_lock();
8888 assert!(
8889 !ext_glob_match("*.c~boo*~foo*", "foo.c"),
8890 "chained exclusion excludes foo.c",
8891 );
8892 }
8893
8894 /// `Misc/globtests` — `[[ fofo = (fo#)# ]]` — outer `(...)#` is
8895 /// zero-or-more closure over `fo#` (one f followed by zero+ o).
8896 #[test]
8897 fn zsh_corpus_closure_of_closure_matches_repeated_fo() {
8898 let _g = crate::test_util::global_state_lock();
8899 assert!(ext_glob_match("(fo#)#", "fofo"), "(fo#)# matches fofo");
8900 }
8901
8902 /// `Misc/globtests` — `[[ ffo = (fo#)# ]]` — "ffo" = "f"+"fo".
8903 #[test]
8904 fn zsh_corpus_closure_matches_ffo() {
8905 let _g = crate::test_util::global_state_lock();
8906 assert!(ext_glob_match("(fo#)#", "ffo"), "(fo#)# matches ffo");
8907 }
8908
8909 /// `Misc/globtests` — `[[ xfoooofof = (fo#)# ]]` — leading "x"
8910 /// breaks the pattern.
8911 #[test]
8912 fn zsh_corpus_closure_rejects_leading_x() {
8913 let _g = crate::test_util::global_state_lock();
8914 assert!(
8915 !ext_glob_match("(fo#)#", "xfoooofof"),
8916 "(fo#)# rejects leading x",
8917 );
8918 }
8919
8920 /// `Misc/globtests` — `[[ foo = ((^x)) ]]` — `(^x)` matches one
8921 /// char that isn't 'x'. "foo" has 'f' first, so matches.
8922 /// Note: zsh's `(^x)` is exactly-one-not-x with extendedglob.
8923 #[test]
8924 fn zsh_corpus_negation_caret_matches_non_x_start() {
8925 let _g = crate::test_util::global_state_lock();
8926 assert!(ext_glob_match("((^x)*)", "foo"), "(^x)* matches foo");
8927 }
8928
8929 /// `Misc/globtests` — `[[ foo = ((^foo)) ]]` — `(^foo)` is "not foo",
8930 /// "foo" matches `foo`, so excluded → false.
8931 #[test]
8932 fn zsh_corpus_negation_caret_rejects_full_match() {
8933 let _g = crate::test_util::global_state_lock();
8934 assert!(
8935 !ext_glob_match("((^foo))", "foo"),
8936 "(^foo) rejects 'foo' itself",
8937 );
8938 }
8939
8940 /// `Misc/globtests` — `[[ abcd = ?(a|b)c#d ]]` — `?(a|b)` is
8941 /// "zero-or-one of a|b", `c#d` is "zero+ c then d". "abcd" = a + b? no, ?
8942 /// is single char or alternation. Let me re-read: `?(a|b)` matches
8943 /// one char which is `a` OR `b`. So "abcd": ?=a, then needs `c#d`
8944 /// = (c)*d, matches "bcd" if first char then need cd. Hmm.
8945 /// Actually: `?(a|b)` = `?` then `(a|b)` as separate? More likely
8946 /// `?(a|b)` is ksh-style "0-or-1 of a|b" only with KSH_GLOB.
8947 /// In zsh native (extendedglob) `?` is single char, `(a|b)` is
8948 /// alternation. So `?(a|b)c#d` = anychar (a|b) c* d. "abcd" =
8949 /// a + b + (zero c) + ... fails. The ztst says it MATCHES.
8950 /// Conclusion: requires KSH_GLOB which we may not enable.
8951 #[test]
8952 fn zsh_corpus_ksh_question_alternation() {
8953 let _g = crate::test_util::global_state_lock();
8954 assert!(
8955 ext_glob_match("?(a|b)c#d", "abcd"),
8956 "?(a|b)c#d matches abcd",
8957 );
8958 }
8959
8960 // ── patmatchlen — c:2649 ─────────────────────────────────────────
8961
8962 /// `Src/pattern.c:2649` — `int patmatchlen(void)` returns the
8963 /// byte-length of the last successful pattry match. After a
8964 /// successful match against `"hello"` with pattern `"hel*"`,
8965 /// the recorded length is 5 (all of "hello" was consumed).
8966 #[test]
8967 fn patmatchlen_records_consumed_byte_length() {
8968 let _g = crate::test_util::global_state_lock();
8969 let prog = compile("hel*");
8970 assert!(pattry(&prog, "hello"), "hel* matches hello");
8971 assert_eq!(patmatchlen(), 5, "all 5 bytes of 'hello' consumed by hel*",);
8972 }
8973
8974 /// Anchored prefix match: `"foo"` against `"foobar"` with the
8975 /// `NOANCH` form is the user-visible case `[[ foobar = foo* ]]`,
8976 /// which consumes all 6 bytes when the pattern matches the whole
8977 /// string.
8978 #[test]
8979 fn patmatchlen_full_string_match_returns_full_length() {
8980 let _g = crate::test_util::global_state_lock();
8981 let prog = compile("foo*");
8982 assert!(pattry(&prog, "foobar"));
8983 assert_eq!(patmatchlen(), 6, "foo* against 'foobar' = 6 bytes");
8984 }
8985
8986 // ── patmatchindex (c:4004) ──────────────────────────────────────
8987
8988 /// `patmatchindex` on a literal-byte range returns the byte at
8989 /// the requested position.
8990 #[test]
8991 fn patmatchindex_literal_byte_at_index() {
8992 let _g = crate::test_util::global_state_lock();
8993 let range = b"abc";
8994 assert_eq!(patmatchindex(range, 0), Some((Some(b'a'), 0)));
8995 assert_eq!(patmatchindex(range, 1), Some((Some(b'b'), 0)));
8996 assert_eq!(patmatchindex(range, 2), Some((Some(b'c'), 0)));
8997 assert_eq!(patmatchindex(range, 3), None, "out-of-range index");
8998 }
8999
9000 /// `patmatchindex` on a PP_ALPHA class marker returns `(None,
9001 /// PP_ALPHA)` to signal the class without a literal char.
9002 #[test]
9003 fn patmatchindex_posix_class_marker_returns_mtp() {
9004 let _g = crate::test_util::global_state_lock();
9005 // Meta + PP_ALPHA = 0x83 + 1 = 0x84
9006 let range = &[Meta + PP_ALPHA as u8];
9007 let r = patmatchindex(range, 0);
9008 assert_eq!(r, Some((None, PP_ALPHA)));
9009 }
9010
9011 // ── mb_patmatchrange (c:3610) ───────────────────────────────────
9012
9013 /// `mb_patmatchrange` literal hit on `'a'`.
9014 #[test]
9015 fn mb_patmatchrange_literal_ascii_hits() {
9016 let _g = crate::test_util::global_state_lock();
9017 let range = b"a";
9018 let mut mtp = -1;
9019 assert!(
9020 mb_patmatchrange(range, 'a', 0, None, Some(&mut mtp)),
9021 "literal 'a' matches"
9022 );
9023 assert_eq!(mtp, 0, "literal hit sets mtp=0");
9024 }
9025
9026 /// `mb_patmatchrange` PP_DIGIT class hit on `'5'`.
9027 #[test]
9028 fn mb_patmatchrange_digit_class_hits() {
9029 let _g = crate::test_util::global_state_lock();
9030 let range = &[Meta + PP_DIGIT as u8];
9031 assert!(mb_patmatchrange(range, '5', 0, None, None));
9032 assert!(!mb_patmatchrange(range, 'x', 0, None, None));
9033 }
9034
9035 /// `mb_patmatchrange` PP_RANGE on `'b'` in `a..c`.
9036 #[test]
9037 fn mb_patmatchrange_range_hits_middle() {
9038 let _g = crate::test_util::global_state_lock();
9039 // Meta + PP_RANGE then two literal bytes for the endpoints.
9040 let range = &[Meta + PP_RANGE as u8, b'a', b'c'];
9041 assert!(mb_patmatchrange(range, 'b', 0, None, None));
9042 assert!(mb_patmatchrange(range, 'a', 0, None, None));
9043 assert!(mb_patmatchrange(range, 'c', 0, None, None));
9044 assert!(!mb_patmatchrange(range, 'd', 0, None, None));
9045 }
9046
9047 // ── mb_patmatchindex (c:3767) ───────────────────────────────────
9048
9049 /// `mb_patmatchindex` literal char at index 0.
9050 #[test]
9051 fn mb_patmatchindex_literal_first_index() {
9052 let _g = crate::test_util::global_state_lock();
9053 let range = b"xyz";
9054 let r = mb_patmatchindex(range, 0);
9055 assert_eq!(r, Some((Some('x'), 0)));
9056 }
9057
9058 /// `mb_patmatchindex` on PP_UPPER marker at index 0.
9059 #[test]
9060 fn mb_patmatchindex_class_marker_returns_mtp() {
9061 let _g = crate::test_util::global_state_lock();
9062 let range = &[Meta + PP_UPPER as u8];
9063 assert_eq!(mb_patmatchindex(range, 0), Some((None, PP_UPPER)));
9064 }
9065
9066 // ═══════════════════════════════════════════════════════════════════
9067 // Additional C-parity tests for Src/pattern.c bit-flag predicates.
9068 // ═══════════════════════════════════════════════════════════════════
9069
9070 /// c:200 — P_ISBRANCH checks bit 0x20.
9071 #[test]
9072 fn P_ISBRANCH_recognizes_bit_0x20() {
9073 assert!(P_ISBRANCH(0x20));
9074 assert!(P_ISBRANCH(0x20 | 0x01));
9075 assert!(P_ISBRANCH(0xFF));
9076 assert!(!P_ISBRANCH(0x00));
9077 assert!(!P_ISBRANCH(0x1F));
9078 }
9079
9080 /// c:201 — P_ISEXCLUDE checks bits 0x30 = 0x10 | 0x20 (BOTH must be set).
9081 #[test]
9082 fn P_ISEXCLUDE_requires_both_bits() {
9083 assert!(P_ISEXCLUDE(0x30));
9084 assert!(P_ISEXCLUDE(0x30 | 0x01));
9085 assert!(!P_ISEXCLUDE(0x10), "only 0x10 — must require both");
9086 assert!(!P_ISEXCLUDE(0x20), "only 0x20 — must require both");
9087 assert!(!P_ISEXCLUDE(0x00));
9088 }
9089
9090 /// c:202 — P_NOTDOT checks bit 0x40.
9091 #[test]
9092 fn P_NOTDOT_recognizes_bit_0x40() {
9093 assert!(P_NOTDOT(0x40));
9094 assert!(P_NOTDOT(0x40 | 0x01));
9095 assert!(P_NOTDOT(0xFF));
9096 assert!(!P_NOTDOT(0x00));
9097 assert!(!P_NOTDOT(0x3F));
9098 }
9099
9100 /// c:216-218 — P_SIMPLE/HSTART/PURESTR are distinct bits.
9101 #[test]
9102 fn p_flag_constants_are_distinct() {
9103 assert_eq!(P_SIMPLE & P_HSTART, 0);
9104 assert_eq!(P_SIMPLE & P_PURESTR, 0);
9105 assert_eq!(P_HSTART & P_PURESTR, 0);
9106 }
9107
9108 /// c:216 — P_SIMPLE is bit 0 (= 0x01).
9109 #[test]
9110 fn p_simple_is_bit_zero() {
9111 assert_eq!(P_SIMPLE, 0x01);
9112 }
9113
9114 /// c:217 — P_HSTART is bit 1 (= 0x02).
9115 #[test]
9116 fn p_hstart_is_bit_one() {
9117 assert_eq!(P_HSTART, 0x02);
9118 }
9119
9120 /// c:218 — P_PURESTR is bit 2 (= 0x04).
9121 #[test]
9122 fn p_purestr_is_bit_two() {
9123 assert_eq!(P_PURESTR, 0x04);
9124 }
9125
9126 /// c:406-407 — PA_NOALIGN=1, PA_UNMETA=2 (single-bit flags).
9127 #[test]
9128 fn pa_flag_constants_canonical() {
9129 assert_eq!(PA_NOALIGN, 1);
9130 assert_eq!(PA_UNMETA, 2);
9131 }
9132
9133 /// PA_NOALIGN | PA_UNMETA combine without overlap.
9134 #[test]
9135 fn pa_flags_pairwise_disjoint() {
9136 assert_eq!(PA_NOALIGN & PA_UNMETA, 0);
9137 }
9138
9139 /// c:336 — metacharinc on ASCII char advances by 1 byte.
9140 #[test]
9141 fn metacharinc_ascii_advances_one() {
9142 let _g = crate::test_util::global_state_lock();
9143 assert_eq!(metacharinc("hello", 0), 1);
9144 assert_eq!(metacharinc("hello", 4), 5);
9145 }
9146
9147 /// c:336 — metacharinc at end-of-string returns same pos.
9148 #[test]
9149 fn metacharinc_end_of_string_returns_same() {
9150 let _g = crate::test_util::global_state_lock();
9151 assert_eq!(metacharinc("abc", 3), 3, "at end → no advance");
9152 }
9153
9154 /// c:336 — metacharinc on multibyte char advances by codepoint width.
9155 #[test]
9156 fn metacharinc_multibyte_advances_by_codepoint_len() {
9157 let _g = crate::test_util::global_state_lock();
9158 // 'é' is 2 bytes in UTF-8.
9159 assert_eq!(metacharinc("é", 0), 2);
9160 // '日' is 3 bytes.
9161 assert_eq!(metacharinc("日", 0), 3);
9162 }
9163
9164 // ═══════════════════════════════════════════════════════════════════
9165 // Additional C-parity tests for Src/pattern.c
9166 // c:155 P_ISBRANCH / c:161 P_ISEXCLUDE / c:167 P_NOTDOT /
9167 // c:262 metacharinc / c:502 patcompstart / c:536 patcompile /
9168 // c:1008 patgetglobflags / c:1129 range_type / c:1152 pattern_range_to_string
9169 // c:2059 charref / c:2066 charnext / c:2153 pattry / c:2304 patmatchlen
9170 // ═══════════════════════════════════════════════════════════════════
9171
9172 /// c:262 — `metacharinc` is pure for ASCII input.
9173 #[test]
9174 fn metacharinc_is_pure_for_ascii() {
9175 for s in ["", "a", "abc", "hello"] {
9176 for pos in 0..s.len() {
9177 let first = metacharinc(s, pos);
9178 for _ in 0..3 {
9179 assert_eq!(
9180 metacharinc(s, pos),
9181 first,
9182 "metacharinc({:?}, {}) must be pure",
9183 s,
9184 pos
9185 );
9186 }
9187 }
9188 }
9189 }
9190
9191 /// c:536 — `patcompile("", 0, None)` returns Option<Patprog>.
9192 #[test]
9193 fn patcompile_returns_option_patprog_type() {
9194 let _g = crate::test_util::global_state_lock();
9195 let _: Option<Patprog> = patcompile("", 0, None);
9196 }
9197
9198 /// c:1008 — `patgetglobflags("")` empty returns None.
9199 #[test]
9200 fn patgetglobflags_empty_returns_none() {
9201 let _g = crate::test_util::global_state_lock();
9202 assert!(patgetglobflags("").is_none(), "empty → None");
9203 }
9204
9205 /// c:1008 — `patgetglobflags` returns Option<(i32, i64, usize)> type.
9206 #[test]
9207 fn patgetglobflags_returns_option_tuple_type() {
9208 let _g = crate::test_util::global_state_lock();
9209 let _: Option<(i32, i64, usize)> = patgetglobflags("abc");
9210 }
9211
9212 /// c:1129 — `range_type("")` empty returns None.
9213 #[test]
9214 fn range_type_empty_returns_none() {
9215 assert!(range_type("").is_none(), "empty → None");
9216 }
9217
9218 /// c:1129 — `range_type` returns Option<usize>.
9219 #[test]
9220 fn range_type_returns_option_usize_type() {
9221 let _: Option<usize> = range_type("abc");
9222 }
9223
9224 /// c:1152 — `pattern_range_to_string("")` empty returns empty String.
9225 #[test]
9226 fn pattern_range_to_string_empty_returns_empty() {
9227 assert_eq!(pattern_range_to_string(""), "");
9228 }
9229
9230 /// c:1152 — `pattern_range_to_string` is pure.
9231 #[test]
9232 fn pattern_range_to_string_is_pure() {
9233 for s in ["", "abc", "a-z"] {
9234 let first = pattern_range_to_string(s);
9235 for _ in 0..3 {
9236 assert_eq!(
9237 pattern_range_to_string(s),
9238 first,
9239 "pattern_range_to_string({:?}) must be pure",
9240 s
9241 );
9242 }
9243 }
9244 }
9245
9246 /// c:2059 — `charref("", 0)` empty returns None.
9247 #[test]
9248 fn charref_empty_returns_none() {
9249 assert!(charref("", 0).is_none(), "empty → None");
9250 }
9251
9252 /// c:2066 — `charnext("", 0)` empty returns 0 (no advance).
9253 #[test]
9254 fn charnext_empty_returns_zero() {
9255 assert_eq!(charnext("", 0), 0);
9256 }
9257
9258 /// c:2304 — `patmatchlen` returns i32 (compile-time type pin).
9259 #[test]
9260 fn patmatchlen_returns_i32_type() {
9261 let _g = crate::test_util::global_state_lock();
9262 let _: i32 = patmatchlen();
9263 }
9264
9265 /// c:155 — P_ISBRANCH/P_ISEXCLUDE/P_NOTDOT all return bool (type pin).
9266 #[test]
9267 fn p_predicates_return_bool_type() {
9268 let _: bool = P_ISBRANCH(0);
9269 let _: bool = P_ISEXCLUDE(0);
9270 let _: bool = P_NOTDOT(0);
9271 }
9272}