zsh/ported/glob.rs
1//! Filename generation (globbing) for zshrs
2//!
3//! Direct port from zsh/Src/glob.c
4//!
5//! Supports:
6//! - Basic glob patterns (*, ?, [...])
7//! - Extended glob patterns (#, ##, ~, ^)
8//! - Recursive globbing (**/*)
9//! - Glob qualifiers (., /, @, etc.)
10//! - Brace expansion ({a,b,c}, {1..10})
11//! - Sorting and filtering matches
12
13use crate::ported::builtin::LASTVAL;
14use crate::ported::options::opt_state_get;
15use crate::ported::pattern::{haswilds, Patprog};
16use crate::ported::signals::unqueue_signals;
17use crate::ported::sort::zstrcmp;
18use crate::ported::string::dyncat;
19use crate::ported::utils::{errflag, init_dirsav, lchdir, restoredir, zerr};
20// `vm_helper` import removed — ShellExecutor reach-in routed through
21// the `crate::ported::exec` accessor wrappers (see memory
22// feedback_no_exec_script_from_ported).
23use crate::ported::lex::untokenize;
24use crate::ported::mem::dupstring;
25use crate::ported::subst::singsub;
26use crate::ported::subst::LinkList;
27use crate::ported::zsh_h::{imatchdata, repldata};
28use crate::ported::zsh_h::{
29 isset, redir, Bnull, Bnullkeep, Dnull, Inang, Meta, Nularg, Outang, Pound, Snull, BAREGLOBQUAL,
30 BRACECCL, CASEGLOB, ERRFLAG_INT, EXTENDEDGLOB, GLOBDOTS, GLOBSTARSHORT, IS_DASH, LISTTYPES,
31 MARKDIRS, MB_METASTRLEN2END, MULTIOS, NULLGLOB, NUMERICGLOBSORT, PAT_NOTEND, PAT_NOTSTART,
32 PP_UNKWN, PREFORK_SINGLE, REDIR_CLOSE, REDIR_ERRWRITE, REDIR_MERGEIN, REDIR_MERGEOUT, SHGLOB,
33 SUB_ALL, SUB_BIND, SUB_DOSUBST, SUB_EIND, SUB_END, SUB_GLOBAL, SUB_LEN, SUB_LIST, SUB_LONG,
34 SUB_MATCH, SUB_REST, SUB_START, SUB_SUBSTR, ZSHTOK_SHGLOB, ZSHTOK_SUBST,
35};
36use crate::ported::ztype_h::imeta;
37use crate::subst::prefork;
38use std::collections::HashSet;
39use std::fs::{self, Metadata};
40use std::os::unix::fs::{MetadataExt, PermissionsExt};
41use std::path::{Component, Path, PathBuf};
42use std::sync::atomic::Ordering;
43use std::time::{SystemTime, UNIX_EPOCH};
44
45/// A glob match with metadata for sorting
46#[derive(Debug, Clone)]
47/// One glob match result.
48/// Port of `struct gmatch` from Src/glob.c — `gmatchcmp()`
49/// (line 936) sorts arrays of these for the `o`/`O` qualifier.
50pub struct gmatch {
51 /// `name` field.
52 pub name: String,
53 /// `path` field.
54 pub path: PathBuf,
55 /// `size` field.
56 pub size: u64,
57 /// `atime` field.
58 pub atime: i64,
59 /// `mtime` field.
60 pub mtime: i64,
61 /// `ctime` field.
62 pub ctime: i64,
63 /// `links` field.
64 pub links: u64,
65 /// `mode` field.
66 pub mode: u32,
67 /// `uid` field.
68 pub uid: u32,
69 /// `gid` field.
70 pub gid: u32,
71 /// `dev` field.
72 pub dev: u64,
73 /// `ino` field.
74 pub ino: u64,
75 // For symlink targets (when following)
76 /// `target_size` field.
77 pub target_size: u64,
78 /// `target_atime` field.
79 pub target_atime: i64,
80 /// `target_mtime` field.
81 pub target_mtime: i64,
82 /// `target_ctime` field.
83 pub target_ctime: i64,
84 /// `target_links` field.
85 pub target_links: u64,
86 // Sub-second timestamp components — port of `long ansec/mnsec/cnsec` +
87 // `long _ansec/_mnsec/_cnsec` in `struct gmatch` (Src/glob.c:63-74).
88 // gmatchcmp tie-breaks equal-second mtimes/atimes/ctimes by these
89 // nanosecond fields (c:992/999/1006/1019/1026/1033) — without them,
90 // files touched <1s apart sort by name rather than mtime.
91 /// `ansec` field. c:64
92 pub ansec: i64,
93 /// `mnsec` field. c:68
94 pub mnsec: i64,
95 /// `cnsec` field. c:72
96 pub cnsec: i64,
97 /// `_ansec` field. c:65
98 pub target_ansec: i64,
99 /// `_mnsec` field. c:69
100 pub target_mnsec: i64,
101 /// `_cnsec` field. c:73
102 pub target_cnsec: i64,
103 // For exec sort strings
104 /// `sort_strings` field.
105 pub sort_strings: Vec<String>,
106}
107
108impl GlobOptSnapshot {
109 /// Read every glob-relevant option from the canonical live store
110 /// (`opt_state_get`) once. Returns a self-contained snapshot.
111 pub fn capture() -> Self {
112 Self {
113 bareglobqual: isset(BAREGLOBQUAL),
114 braceccl: isset(BRACECCL),
115 caseglob: isset(CASEGLOB),
116 extendedglob: isset(EXTENDEDGLOB),
117 globdots: isset(GLOBDOTS),
118 globstarshort: isset(GLOBSTARSHORT),
119 listtypes: isset(LISTTYPES),
120 markdirs: isset(MARKDIRS),
121 nullglob: isset(NULLGLOB),
122 numericglobsort: isset(NUMERICGLOBSORT),
123 }
124 }
125}
126
127thread_local! {
128 /// Thread-local glob option cache. `Some(snap)` while a glob()
129 /// call is in flight on this thread; `None` otherwise (reads
130 /// fall back to the live `isset()`).
131 static GLOB_OPTS_TLS: std::cell::RefCell<Option<GlobOptSnapshot>> =
132 const { std::cell::RefCell::new(None) };
133}
134
135// =====================================================================
136// GS_* — sort-specifier flag bits — `Src/glob.c:77-94`. The `glob -O`
137// + `glob -o` sort-spec parser stuffs these bits into the per-glob
138// sortspec struct so the qsort comparator picks the right key.
139// =====================================================================
140
141/// Port of `GS_NAME` from `Src/glob.c:77`. Sort by filename.
142pub const GS_NAME: i32 = 1; // c:77
143
144impl Drop for GlobOptsGuard {
145 fn drop(&mut self) {
146 if self.populated {
147 GLOB_OPTS_TLS.with_borrow_mut(|g| *g = None);
148 }
149 }
150}
151/// Port of `GS_DEPTH` from `Src/glob.c:78`. Sort by directory depth.
152pub const GS_DEPTH: i32 = 2; // c:78
153/// Port of `GS_EXEC` from `Src/glob.c:79`. Sort via external function.
154pub const GS_EXEC: i32 = 4; // c:79
155
156/// Port of `GS_SHIFT_BASE` from `Src/glob.c:81`. Bit position where
157/// the size/mtime/atime/ctime/links sort keys live.
158pub const GS_SHIFT_BASE: i32 = 8; // c:81
159
160/// Port of `GS_SIZE` from `Src/glob.c:83`. Sort by file size.
161pub const GS_SIZE: i32 = GS_SHIFT_BASE; // c:83
162/// Port of `GS_ATIME` from `Src/glob.c:84`. Sort by access time.
163pub const GS_ATIME: i32 = GS_SHIFT_BASE << 1; // c:84
164/// Port of `GS_MTIME` from `Src/glob.c:85`. Sort by modification time.
165pub const GS_MTIME: i32 = GS_SHIFT_BASE << 2; // c:85
166/// Port of `GS_CTIME` from `Src/glob.c:86`. Sort by inode-change time.
167pub const GS_CTIME: i32 = GS_SHIFT_BASE << 3; // c:86
168/// Port of `GS_LINKS` from `Src/glob.c:87`. Sort by hard-link count.
169pub const GS_LINKS: i32 = GS_SHIFT_BASE << 4; // c:87
170
171/// Port of `GS_SHIFT` from `Src/glob.c:89`. Bit-shift offset where the
172/// reverse-direction variants of the size/atime/mtime/ctime/links
173/// sort flags live.
174pub const GS_SHIFT: i32 = 5; // c:89
175
176/// Port of `GS__SIZE` from `Src/glob.c:90` (reverse-sort variant).
177pub const GS__SIZE: i32 = GS_SIZE << GS_SHIFT; // c:90
178/// Port of `GS__ATIME` from `Src/glob.c:91`.
179pub const GS__ATIME: i32 = GS_ATIME << GS_SHIFT; // c:91
180/// Port of `GS__MTIME` from `Src/glob.c:92`.
181pub const GS__MTIME: i32 = GS_MTIME << GS_SHIFT; // c:92
182/// Port of `GS__CTIME` from `Src/glob.c:93`.
183pub const GS__CTIME: i32 = GS_CTIME << GS_SHIFT; // c:93
184/// Port of `GS__LINKS` from `Src/glob.c:94`.
185pub const GS__LINKS: i32 = GS_LINKS << GS_SHIFT; // c:94
186
187/// Port of `GS_DESC` from `Src/glob.c:96`. Descending-order toggle.
188pub const GS_DESC: i32 = GS_SHIFT_BASE << (2 * GS_SHIFT); // c:96
189/// Port of `GS_NONE` from `Src/glob.c:97`. Marker for no-sort spec.
190pub const GS_NONE: i32 = GS_SHIFT_BASE << (2 * GS_SHIFT + 1); // c:97
191
192/// Port of `GS_NORMAL` from `Src/glob.c:99`. Forward-direction sort
193/// keys (excluding NAME/DEPTH/EXEC and the reverse variants).
194pub const GS_NORMAL: i32 = GS_SIZE | GS_ATIME | GS_MTIME | GS_CTIME | GS_LINKS; // c:99
195/// Port of `GS_LINKED` from `Src/glob.c:100`. Reverse-direction
196/// (linked) sort keys.
197pub const GS_LINKED: i32 = GS_NORMAL << GS_SHIFT; // c:100
198
199// =====================================================================
200// TT_* — time + size unit selectors. Two parallel namespaces using
201// the same numeric values.
202// =====================================================================
203
204/// Port of `TT_DAYS` from `Src/glob.c:121`. Time qualifier in days.
205pub const TT_DAYS: i32 = 0; // c:121
206/// Port of `TT_HOURS` from `Src/glob.c:122`. Time qualifier in hours.
207pub const TT_HOURS: i32 = 1; // c:122
208/// Port of `TT_MINS` from `Src/glob.c:123`. Time qualifier in minutes.
209pub const TT_MINS: i32 = 2; // c:123
210/// Port of `TT_WEEKS` from `Src/glob.c:124`. Time qualifier in weeks.
211pub const TT_WEEKS: i32 = 3; // c:124
212/// Port of `TT_MONTHS` from `Src/glob.c:125`. Time qualifier in months.
213pub const TT_MONTHS: i32 = 4; // c:125
214/// Port of `TT_SECONDS` from `Src/glob.c:126`. Time qualifier in seconds.
215pub const TT_SECONDS: i32 = 5; // c:126
216
217/// Port of `TT_BYTES` from `Src/glob.c:128`. Size qualifier in bytes.
218pub const TT_BYTES: i32 = 0; // c:128
219/// Port of `TT_POSIX_BLOCKS` from `Src/glob.c:129`. Size qualifier
220/// in POSIX 512-byte blocks (the `b` glob qualifier suffix).
221pub const TT_POSIX_BLOCKS: i32 = 1; // c:129
222/// Port of `TT_KILOBYTES` from `Src/glob.c:130`.
223pub const TT_KILOBYTES: i32 = 2; // c:130
224/// Port of `TT_MEGABYTES` from `Src/glob.c:131`.
225pub const TT_MEGABYTES: i32 = 3; // c:131
226/// Port of `TT_GIGABYTES` from `Src/glob.c:132`.
227pub const TT_GIGABYTES: i32 = 4; // c:132
228/// Port of `TT_TERABYTES` from `Src/glob.c:133`.
229pub const TT_TERABYTES: i32 = 5; // c:133
230
231/// Port of `MAX_SORTS` from `Src/glob.c:164`. Maximum sort-spec keys
232/// per glob (`glob -O 'reverse(name).size'` style).
233pub const MAX_SORTS: usize = 12; // c:164
234
235/// Main glob state — port of `struct globdata` from Src/glob.c:168.
236/// C zsh has a single file-static `static struct globdata curglobdata;`
237/// (glob.c:196) and accesses fields through a wall of #define macros
238/// (`matchsz`/`matchct`/`pathbuf`/`pathpos`/`quals`/...) that all
239/// resolve to `curglobdata.gd_*`. The Rust port collapses the
240/// `gd_matchsz`/`gd_matchct`/`gd_matchbuf`/`gd_matchptr` quartet into
241/// `matches: Vec<GlobMatch>` (the natural Rust shape) and folds the
242/// `gd_gf_*` glob-flag bag into `options: GlobOptions`, but the
243/// 1:1 correspondence to `struct globdata` is otherwise faithful.
244// struct to easily save/restore current state // c:166
245/// `globdata` — see fields for layout.
246#[allow(non_camel_case_types)]
247pub struct globdata {
248 // c:168
249 /// `matches` field.
250 pub matches: Vec<gmatch>,
251 /// `qualifiers` field.
252 pub qualifiers: Option<qualifier_set>,
253 /// c:206 — `quals` (`curglobdata.gd_quals`): the `struct qual` list
254 /// that `insert()` walks per candidate file. Arena-backed port of
255 /// the C pointer list; `None` when the glob has no qualifiers.
256 pub quals: Option<QualArena>,
257 pub pathbuf: String, // c:170 gd_pathbuf
258 pub pathpos: usize, // c:169 gd_pathpos
259 pub matchct: i32, // c:173 gd_matchct
260 pub pathbufsz: usize, // c:174 gd_pathbufsz
261 pub pathbufcwd: i32, // c:175 gd_pathbufcwd
262 pub gf_nullglob: i32, // c:186 gd_gf_nullglob
263 pub gf_markdirs: i32, // c:186 gd_gf_markdirs
264 pub gf_noglobdots: i32, // c:186 gd_gf_noglobdots
265 pub gf_listtypes: i32, // c:186 gd_gf_listtypes
266 pub gf_pre_words: Option<Vec<String>>, // c:190 gd_gf_pre_words
267 pub gf_post_words: Option<Vec<String>>, // c:190 gd_gf_post_words
268}
269
270// c:197 — `static struct globdata curglobdata;`
271// C's single file-static glob state; macros at c:199-222 redirect
272// `pathbufsz`, `gf_noglobdots`, etc. to `curglobdata.gd_*`. Rust
273// mirror — accessed via lock for thread safety; C is single-threaded.
274/// `CURGLOBDATA` static.
275pub static CURGLOBDATA: std::sync::Mutex<globdata> = std::sync::Mutex::new(globdata {
276 matches: Vec::new(),
277 qualifiers: None,
278 quals: None,
279 pathbuf: String::new(),
280 pathpos: 0,
281 matchct: 0,
282 pathbufsz: 0,
283 pathbufcwd: 0,
284 gf_nullglob: 0,
285 gf_markdirs: 0,
286 gf_noglobdots: 0,
287 gf_listtypes: 0,
288 gf_pre_words: None,
289 gf_post_words: None,
290});
291
292/// Port of `int badcshglob` from `Src/glob.c:103`. Tracks csh-glob
293/// diagnostic state across a single command line: bit 1 = "at
294/// least one expansion failed" (CSHNULLGLOB and no matches),
295/// bit 2 = "at least one expansion produced output". `globlist`
296/// resets to 0 at entry, ORs the bits as each `zglob` runs, then
297/// emits "no match" iff the final value is 1 (some failed, none
298/// succeeded). Used to make `*.nope *.ok` succeed without
299/// diagnostic, but `*.nope alone` error out under CSHNULLGLOB.
300pub static BADCSHGLOB: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:103
301
302/// Port of `static char **inserts;` from `Src/glob.c:340`. Set by
303/// `qualsheval` (c:3927-3937) from the `$reply` array / `$REPLY` scalar
304/// after an `(e:…:)` / `(+…)` qualifier eval; `insert()` then emits one
305/// match per element instead of the original name (c:428). `None` ==
306/// C's `inserts == NULL` (emit the original name once). Reset to `None`
307/// before each per-file qualifier walk (C: `inserts = NULL` at insert()
308/// entry, c:353).
309pub static INSERTS: std::sync::Mutex<Option<Vec<String>>> = std::sync::Mutex::new(None);
310
311/// Port of `struct complist` from `Src/glob.c:252`.
312/// C body:
313/// ```c
314/// struct complist {
315/// Complist next;
316/// Patprog pat;
317/// int closure; /* 1 if this is a (foo/)# */
318/// int follow; /* 1 to go thru symlinks */
319/// };
320/// ```
321#[allow(non_camel_case_types)]
322pub struct complist {
323 // c:252
324 pub next: Option<Box<complist>>, // c:253
325 pub pat: crate::ported::pattern::Patprog, // c:254
326 pub closure: i32, // c:255
327 pub follow: i32, // c:256
328}
329
330/// Add path component (from glob.c addpath lines 263-274)
331/// Append a path component to a glob path buffer.
332/// Port of `addpath(char *s, int l)` from Src/glob.c:265.
333pub fn addpath(s: &mut String, l: &str) {
334 // c:265
335 s.push_str(l);
336 if !s.ends_with('/') {
337 s.push('/');
338 }
339}
340
341/// PARTIAL port of `statfullpath(const char *s, struct stat *st, int l)` from
342/// `Src/glob.c:283` — NOT yet faithful, and currently has NO callers (the live
343/// qualifier matcher inlines `fs::metadata`/`symlink_metadata` on the already-
344/// full path). Two gaps remain, both blocked on the glob path-state flow:
345/// 1. C prepends the global `pathbuf[pathbufcwd..pathpos]` (the accumulated
346/// glob directory) to `s`; this port omits it. A faithful version must
347/// take the pathbuf as a parameter — it cannot read the `CURGLOBDATA`
348/// singleton because the matcher already holds that lock (deadlock).
349/// 2. C's `st` is the OUTPUT `struct stat *`; the second arg here is a string
350/// suffix, which is not the C contract.
351/// The `l` flag now matches C: `l ? lstat : stat` (l → `symlink_metadata`).
352/// Do not wire this in until gap #1 is resolved.
353pub fn statfullpath(s: &str, st: &str, l: bool) -> Option<Metadata> {
354 // c:283
355 let full = if st.is_empty() {
356 if s.is_empty() {
357 ".".to_string()
358 } else {
359 s.to_string()
360 }
361 } else {
362 format!("{}{}", s, st)
363 };
364
365 // c:308 — `return l ? lstat(buf, st) : stat(buf, st);`
366 if l {
367 fs::symlink_metadata(&full).ok() // l → lstat
368 } else {
369 fs::metadata(&full).ok() // !l → stat (follows symlinks)
370 }
371}
372// END moved-from-exec-rs (free ported)
373
374// ===========================================================
375// Direct ports of static helpers from Src/glob.c not yet covered
376// above. The Rust glob engine (`crate::glob`) re-implements the
377// lexer + matcher; these free-fn entries satisfy ABI/name parity
378// for the drift gate.
379// ===========================================================
380
381/// Port of `scanner(Complist q, int shortcircuit)` from `Src/glob.c:500`.
382///
383/// Walks the `complist` path-component chain built by `parsecomplist`,
384/// descending the directory tree one component per node. `q.closure`
385/// (set for `**` and `(dir/)#` / `(dir/)##`) drives the any-depth match:
386/// the zero-directory case scans `q.next` directly, then each matching
387/// subdirectory is re-scanned against `(q.closure) ? q : q.next` — i.e.
388/// the same node `q` repeats for `**`, matching at every depth.
389///
390/// Deviations from C, all pre-existing in this engine — NOT introduced
391/// by this port:
392/// * directory entries come from `fs::read_dir` + `zreaddir` (the
393/// ported `zreaddir`, glob.c:5217) instead of C `opendir`; same
394/// skip-`.`/`..` behaviour, and names are real (non-metafied) so no
395/// `unmeta` round-trip is needed;
396/// * the approximate-match error-reduction block (`forceerrs`/
397/// `errsfound`, glob.c:619-639) is omitted — the Rust matcher keeps
398/// no per-match error count;
399/// * `glob_pre`/`glob_suf` (ZLE prefix/suffix) filtering is omitted.
400/// Leading-dot files are filtered by `pattry`'s `PAT_NOGLD` check
401/// (pattern.rs:2890); `globdata_glob` sets `gf_noglobdots` so
402/// `parsecomplist` compiles the flag in (faithful to C's `glob()`).
403///
404/// The `in_closure_repeat` parameter is Rust-only: it carries C's
405/// `q->closure = 1` mutation across the recursion. A `(dir/)##` node
406/// (closure==2) requires at least one directory, so the zero-directory
407/// match is skipped only the FIRST time the node is entered; once we have
408/// descended one closure level it behaves like `(dir/)#` (zero-or-more).
409/// `q` is shared (`&complist`), so C's in-place field mutation is modelled
410/// by this argument instead. Callers pass `false`.
411fn scanner(state: &mut globdata, q: Option<&complist>, shortcircuit: i32, in_closure_repeat: bool) {
412 use std::sync::atomic::Ordering;
413 // c:506 — `if (!q || errflag) return;`
414 let Some(q) = q else { return };
415 if errflag.load(Ordering::SeqCst) & crate::ported::zsh_h::ERRFLAG_ERROR != 0 {
416 return;
417 }
418 let pbcwdsav = state.pathbufcwd; // c:503
419 let mut ds = init_dirsav(); // c:508
420 let path_max = crate::ported::zsh_system_h::PATH_MAX;
421
422 // c:510-518 — closure preamble: try zero directories via q.next first
423 // (skipped for `(dir/)##` until at least one directory is consumed).
424 let closure = q.closure;
425 if closure != 0 {
426 let skip_zero = q.closure == 2 && !in_closure_repeat;
427 if !skip_zero {
428 scanner(state, q.next.as_deref(), shortcircuit, false); // c:515
429 if shortcircuit != 0 && shortcircuit == state.matchct {
430 return;
431 }
432 }
433 }
434
435 let p = &q.pat; // c:519
436 if (p.0.flags & crate::ported::zsh_h::PAT_PURES as i32) != 0 {
437 // c:521-565 — pure literal section: append to the path (intermediate)
438 // or emit (final); no directory scan.
439 let start = p.0.startoff as usize;
440 let l = p.0.patmlen as usize;
441 let str_lit = String::from_utf8_lossy(&p.1[start..start + l]).into_owned();
442
443 // c:524-536 — PATH_MAX guard.
444 if l + (l == 0) as usize + state.pathpos - state.pathbufcwd as usize >= path_max {
445 if l >= path_max {
446 return;
447 }
448 let anchor = state
449 .pathbuf
450 .get(state.pathbufcwd as usize..)
451 .unwrap_or("")
452 .to_string();
453 let err = lchdir(&anchor, Some(&mut ds), 0);
454 if err == -1 {
455 return;
456 }
457 if err != 0 {
458 zerr("current directory lost during glob");
459 return;
460 }
461 state.pathbufcwd = state.pathpos as i32;
462 }
463
464 if q.next.is_some() {
465 // c:539-560 — not the last section: add to path, recurse.
466 let oppos = state.pathpos;
467 if errflag.load(Ordering::SeqCst) & crate::ported::zsh_h::ERRFLAG_ERROR == 0 {
468 // c:543-552 — `.`/`..` handling inside a closure walk.
469 let mut add = true;
470 if closure != 0 && !state.pathbuf.is_empty() {
471 if str_lit == "." {
472 add = false; // c:546
473 } else if str_lit == ".." {
474 // c:547-551 — drop `..` that would escape the root.
475 use std::os::unix::fs::MetadataExt;
476 let cur: &str = &state.pathbuf;
477 add = match (fs::metadata("/"), fs::metadata(cur)) {
478 (Ok(r), Ok(c)) => r.ino() != c.ino() || r.dev() != c.dev(),
479 _ => true,
480 };
481 }
482 }
483 if add {
484 addpath(&mut state.pathbuf, &str_lit); // c:553
485 state.pathpos = state.pathbuf.len();
486 // c:554 — closure: only recurse when the new path is a
487 // real directory (`!statfullpath("", NULL, 1)`).
488 let recurse = closure == 0
489 || fs::metadata(state.pathbuf.as_str())
490 .map(|m| m.is_dir())
491 .unwrap_or(false);
492 if recurse {
493 scanner(
494 state,
495 if closure != 0 {
496 Some(q)
497 } else {
498 q.next.as_deref()
499 },
500 shortcircuit,
501 closure != 0,
502 ); // c:555
503 if shortcircuit != 0 && shortcircuit == state.matchct {
504 return;
505 }
506 }
507 state.pathbuf.truncate(oppos); // c:558
508 state.pathpos = oppos;
509 }
510 }
511 } else {
512 // c:561-564 — last section: emit the literal.
513 let full = std::path::Path::new(&state.pathbuf).join(&str_lit);
514 insert(state, &full, 0);
515 if shortcircuit != 0 && shortcircuit == state.matchct {
516 return;
517 }
518 }
519 } else {
520 // c:567-685 — pattern-matched section: scan the directory.
521 let base = {
522 let from_cwd = state.pathbuf.get(state.pathbufcwd as usize..).unwrap_or("");
523 if from_cwd.is_empty() {
524 ".".to_string()
525 } else {
526 from_cwd.to_string()
527 }
528 };
529 let dirs = q.next.is_some(); // c:570
530 let mut rd = match fs::read_dir(&base) {
531 Ok(d) => d,
532 Err(_) => return, // c:573 — opendir == NULL
533 };
534 // c:572-573 — collect matching subdirs, descend AFTER the dir
535 // handle is dropped (C buffers them in `subdirs`). `zreaddir(lock, 1)`
536 // ALWAYS skips `.`/`..` (c:5217); leading-dot files are admitted and
537 // then accepted/rejected by `pattry`'s PAT_NOGLD rule, so dotglob /
538 // `(D)` is handled by the compiled flag, never by re-including
539 // `.`/`..` here (that previously leaked `.`/`..` under dotglob).
540 let mut subdirs: Vec<String> = Vec::new();
541 while let Some(name) = crate::ported::utils::zreaddir(&mut rd, 1) {
542 if errflag.load(Ordering::SeqCst) & crate::ported::zsh_h::ERRFLAG_ERROR != 0 {
543 break;
544 }
545 if !crate::ported::pattern::pattry(p, &name) {
546 continue; // c:583
547 }
548 // c:585-597 — PATH_MAX lchdir bookkeeping.
549 if pbcwdsav == state.pathbufcwd
550 && name.len() + state.pathpos - state.pathbufcwd as usize >= path_max
551 {
552 let anchor = state
553 .pathbuf
554 .get(state.pathbufcwd as usize..)
555 .unwrap_or("")
556 .to_string();
557 let err = lchdir(&anchor, Some(&mut ds), 0);
558 if err == -1 {
559 break;
560 }
561 if err != 0 {
562 zerr("current directory lost during glob");
563 break;
564 }
565 state.pathbufcwd = state.pathpos as i32;
566 }
567 if dirs {
568 // c:642-655 — closure: only descend real directories.
569 if closure != 0 {
570 let probe = std::path::Path::new(&base).join(&name);
571 let md = if q.follow != 0 {
572 fs::metadata(&probe)
573 } else {
574 fs::symlink_metadata(&probe)
575 };
576 match md {
577 Ok(m) if m.is_dir() => {}
578 _ => continue,
579 }
580 }
581 subdirs.push(name); // c:657-663
582 } else {
583 // c:665-670 — last component: emit.
584 let full = std::path::Path::new(&base).join(&name);
585 insert(state, &full, 1);
586 if shortcircuit != 0 && shortcircuit == state.matchct {
587 return;
588 }
589 }
590 }
591 drop(rd); // c:672 — closedir
592 // c:674-684 — descend into each collected subdir.
593 if !subdirs.is_empty() {
594 let oppos = state.pathpos;
595 for name in subdirs {
596 addpath(&mut state.pathbuf, &name);
597 state.pathpos = state.pathbuf.len();
598 scanner(
599 state,
600 if closure != 0 {
601 Some(q)
602 } else {
603 q.next.as_deref()
604 },
605 shortcircuit,
606 closure != 0,
607 ); // c:681
608 if shortcircuit != 0 && shortcircuit == state.matchct {
609 return;
610 }
611 state.pathbuf.truncate(oppos);
612 state.pathpos = oppos;
613 }
614 }
615 }
616
617 // c:687-694 — restore cwd if we lchdir'd partway through.
618 if pbcwdsav < state.pathbufcwd {
619 if restoredir(&mut ds) != 0 {
620 zerr("current directory lost during glob"); // c:689
621 }
622 state.pathbufcwd = pbcwdsav;
623 }
624}
625
626/* This function tokenizes a zsh glob pattern */
627// c:706
628/// Port of `parsecomplist(char *instr)` from Src/glob.c:710.
629/// Tokenize a zsh glob path pattern into a `Complist` of path
630/// components, recursively. Returns `None` and sets `errflag |=
631/// ERRFLAG_ERROR` on parse failure. Reads `gf_noglobdots` from
632/// `CURGLOBDATA` (the curglobdata singleton at c:197).
633pub fn parsecomplist(instr: &str) -> Option<Box<complist>> {
634 // c:710
635 let p1: crate::ported::pattern::Patprog; // c:712
636 let l1: Box<complist>; // c:713
637 // c:714 `char *str;` — used as the skipparens cursor (parens branch).
638 // c:715 — compflags depend on gf_noglobdots from curglobdata.
639 let gf_noglobdots: i32 = CURGLOBDATA
640 .lock()
641 .unwrap_or_else(|e| e.into_inner())
642 .gf_noglobdots; // c:214 macro / c:186 field
643 let compflags: i32 = if gf_noglobdots != 0 {
644 crate::ported::zsh_h::PAT_FILE | crate::ported::zsh_h::PAT_NOGLD
645 } else {
646 crate::ported::zsh_h::PAT_FILE
647 }; // c:715
648
649 let chars: Vec<char> = instr.chars().collect();
650 if chars.len() >= 2
651 && chars[0] == crate::ported::zsh_h::Star
652 && chars[1] == crate::ported::zsh_h::Star
653 {
654 // c:717
655 let mut shortglob: i32 = 0; // c:718
656 let cond_a = chars.get(2) == Some(&'/'); // c:719
657 let cond_b =
658 chars.get(2) == Some(&crate::ported::zsh_h::Star) && chars.get(3) == Some(&'/'); // c:719
659 // c:719-720 — `instr[2] == '/' || (instr[2] == Star && instr[3] == '/')
660 // || (shortglob = isset(GLOBSTARSHORT))`. C's `||` SHORT-CIRCUITS:
661 // the `shortglob = isset(GLOBSTARSHORT)` assignment runs ONLY when
662 // `**` is not explicitly followed by `/`. Mirror that with a lazy
663 // `||` so cond_a/cond_b keep `shortglob == 0` — an eager
664 // `let cond_c = { shortglob = … }` set it unconditionally, making
665 // `**/x` advance by 1 (`shortglob ? 1 : 3`) instead of 3, leaving a
666 // stray `*` that collapsed `**/` to a single directory level.
667 let enter = cond_a || cond_b || {
668 shortglob = if crate::ported::zsh_h::isset(crate::ported::zsh_h::GLOBSTARSHORT) {
669 1
670 } else {
671 0
672 };
673 shortglob != 0
674 }; // c:720
675 if enter {
676 /* Match any number of directories. */
677 // c:721
678 /* with three stars, follow symbolic links */ // c:724
679 let follow: i32 = if chars.get(2) == Some(&crate::ported::zsh_h::Star) {
680 1
681 } else {
682 0
683 }; // c:725
684 /*
685 * With GLOBSTARSHORT, leave a star in place for the
686 * pattern inside the directory.
687 */ // c:726-729
688 let advance: usize = (if shortglob != 0 { 1 } else { 3 }) + follow as usize; // c:730
689
690 /* Now get the next path component if there is one. */
691 // c:732
692 let next_instr: String = chars[advance..].iter().collect();
693 // c:733 — `l1 = (Complist) zhalloc(sizeof *l1);`
694 let next_l = parsecomplist(&next_instr); // c:734
695 if next_l.is_none() {
696 crate::ported::utils::errflag.fetch_or(
697 crate::ported::zsh_h::ERRFLAG_ERROR,
698 std::sync::atomic::Ordering::Relaxed,
699 ); // c:735
700 return None; // c:736
701 }
702 let pat = crate::ported::pattern::patcompile(
703 "",
704 compflags | crate::ported::zsh_h::PAT_ANY,
705 None,
706 )?; // c:738
707 l1 = Box::new(complist {
708 next: next_l,
709 pat,
710 closure: 1, // c:739
711 follow, // c:740
712 });
713 return Some(l1); // c:741
714 }
715 }
716
717 /* Parse repeated directories such as (dir/)# and (dir/)## */
718 // c:745
719 let zpc = crate::ported::pattern::zpc_special
720 .lock()
721 .unwrap_or_else(|e| e.into_inner());
722 let inpar_c = zpc[crate::ported::zsh_h::ZPC_INPAR as usize] as char;
723 let hash_c = zpc[crate::ported::zsh_h::ZPC_HASH as usize] as char;
724 drop(zpc);
725
726 // c:746-748 — `if (*(str = instr) == zpc_special[ZPC_INPAR] &&
727 // !skipparens(Inpar, Outpar, (char **)&str) &&
728 // *str == zpc_special[ZPC_HASH] && str[-2] == '/')`.
729 // Routed through the canonical `skipparens` port for caller-coverage
730 // parity with C — was previously inlined as a divergent depth-walk
731 // because the old Rust signature returned usize. The C-faithful
732 // `skipparens(open, close, &mut &str)` now matches `int skipparens
733 // (char inpar, char outpar, char **s)` at utils.c:2409.
734 let instr_chars: String = chars.iter().collect();
735 let inpar_byte = crate::ported::zsh_h::Inpar as u32;
736 let outpar_byte = crate::ported::zsh_h::Outpar as u32;
737 let mut cursor: &str = &instr_chars;
738 let skip_level: i32 = crate::ported::utils::skipparens(
739 char::from_u32(inpar_byte).unwrap_or('('),
740 char::from_u32(outpar_byte).unwrap_or(')'),
741 &mut cursor,
742 );
743 let str_after_parens: Option<usize> = if chars.first() == Some(&inpar_c) {
744 Some(instr_chars.chars().count() - cursor.chars().count())
745 } else {
746 None
747 };
748 let parens_balanced = chars.first() == Some(&inpar_c) && skip_level == 0; // c:746-747 `!skipparens(...)`
749 let after_paren_idx = str_after_parens.unwrap_or(0);
750 let str_at_hash = parens_balanced && chars.get(after_paren_idx) == Some(&hash_c); // c:748 `*str == Pound`
751 // c:748 `str[-2] == '/'` — `str` is past `)`, so `str[-2]` is char
752 // before `)`. In chars, that's `chars[after_paren_idx - 2]`.
753 let preceded_by_slash =
754 parens_balanced && after_paren_idx >= 2 && chars.get(after_paren_idx - 2) == Some(&'/');
755
756 if parens_balanced && str_at_hash && preceded_by_slash {
757 // c:749 — `instr++;`
758 let mut cursor: String = chars[1..].iter().collect();
759 let mut endexp = String::new();
760 let p1_opt = crate::ported::pattern::patcompile(&cursor, compflags, Some(&mut endexp)); // c:750
761 if p1_opt.is_none() {
762 return None; // c:751
763 }
764 let p1_real = p1_opt.unwrap();
765 cursor = endexp; // C: `instr` advanced past compiled pattern
766 let c2: Vec<char> = cursor.chars().collect();
767 if c2.first() == Some(&'/')
768 && c2.get(1) == Some(&crate::ported::zsh_h::Outpar)
769 && c2.get(2) == Some(&crate::ported::zsh_h::Pound)
770 {
771 // c:752
772 let mut pdflag: i32 = 0; // c:753
773 let mut adv = 3; // c:755
774 if c2.get(3) == Some(&crate::ported::zsh_h::Pound) {
775 pdflag = 1; // c:757
776 adv = 4; // c:758
777 }
778 let next_instr: String = c2[adv..].iter().collect();
779 // c:760-761 — `l1 = (Complist) zhalloc(...); l1->pat = p1;`
780 /* special case (/)# to avoid infinite recursion */ // c:762
781 // c:763 — `(*((char *)p1 + p1->startoff)) ? 1 + pdflag : 0`
782 let pat_nonempty = !p1_real.1.is_empty()
783 && p1_real
784 .1
785 .get(p1_real.0.startoff as usize)
786 .copied()
787 .unwrap_or(0)
788 != 0;
789 let closure = if pat_nonempty { 1 + pdflag } else { 0 };
790 let next_l = parsecomplist(&next_instr); // c:765
791 // c:766 — `return (l1->pat) ? l1 : NULL;`
792 l1 = Box::new(complist {
793 next: next_l,
794 pat: p1_real,
795 closure,
796 follow: 0, // c:764
797 });
798 return Some(l1);
799 }
800 } else {
801 /* parse single path component */
802 // c:769
803 let mut endexp = String::new();
804 let p1_opt = crate::ported::pattern::patcompile(
805 instr,
806 compflags | crate::ported::zsh_h::PAT_FILET,
807 Some(&mut endexp),
808 ); // c:770
809 if p1_opt.is_none() {
810 return None; // c:771
811 }
812 p1 = p1_opt.unwrap();
813 let cursor: Vec<char> = endexp.chars().collect();
814 /* then do the remaining path components */
815 // c:772
816 let head = cursor.first().copied();
817 if head == Some('/') || head.is_none() {
818 // c:773
819 let ef: i32 = if head == Some('/') { 1 } else { 0 }; // c:774
820 let next_l: Option<Box<complist>> = if ef != 0 {
821 let rest: String = cursor[1..].iter().collect();
822 parsecomplist(&rest) // c:779
823 } else {
824 None
825 };
826 // c:780 — `return (ef && !l1->next) ? NULL : l1;`
827 if ef != 0 && next_l.is_none() {
828 return None;
829 }
830 l1 = Box::new(complist {
831 next: next_l,
832 pat: p1,
833 closure: 0, // c:778
834 follow: 0,
835 });
836 return Some(l1);
837 }
838 }
839 crate::ported::utils::errflag.fetch_or(
840 crate::ported::zsh_h::ERRFLAG_ERROR,
841 std::sync::atomic::Ordering::Relaxed,
842 ); // c:783
843 None // c:784
844}
845
846/* turn a string into a Complist struct: this has path components */
847// c:787
848/// Port of `parsepat(char *str)` from Src/glob.c:791.
849/// Top-level entry: strip leading `(#...)` flag block via
850/// `patgetglobflags`, then initialise `pathbuf`/`pathpos` per the
851/// pattern's absolute-vs-relative head, then dispatch to
852/// `parsecomplist`. Mutates `CURGLOBDATA.{pathbuf,pathpos,pathbufsz}`.
853pub fn parsepat(s: &str) -> Option<Box<complist>> {
854 // c:791
855 // c:793 `long assert; int ignore;` — captured into patgetglobflags result
856 crate::ported::pattern::patcompstart(); // c:796
857 let chars: Vec<char> = s.chars().collect();
858 let zpc = crate::ported::pattern::zpc_special
859 .lock()
860 .unwrap_or_else(|e| e.into_inner());
861 let inpar_c = zpc[crate::ported::zsh_h::ZPC_INPAR as usize] as char;
862 let hash_c = zpc[crate::ported::zsh_h::ZPC_HASH as usize] as char;
863 let ksh_at_c = zpc[crate::ported::zsh_h::ZPC_KSH_AT as usize] as char;
864 drop(zpc);
865
866 /*
867 * Check for initial globbing flags, so that they don't form
868 * a bogus path component.
869 */
870 // c:797-800
871 let mut cursor: String = s.to_string();
872 let first_is_inpar_hash = chars.first() == Some(&inpar_c) && chars.get(1) == Some(&hash_c); // c:801
873 let first_is_ksh_at_inpar_hash = chars.first() == Some(&ksh_at_c)
874 && chars.get(1) == Some(&crate::ported::zsh_h::Inpar)
875 && chars.get(2) == Some(&hash_c); // c:802-803
876 if first_is_inpar_hash || first_is_ksh_at_inpar_hash {
877 let skip = if chars.first() == Some(&crate::ported::zsh_h::Inpar) {
878 2
879 } else {
880 3
881 }; // c:804
882 cursor = chars[skip..].iter().collect();
883 let flag_result = crate::ported::pattern::patgetglobflags(&cursor); // c:805
884 if flag_result.is_none() {
885 return None; // c:806
886 }
887 let (_bits, _assertp, consumed) = flag_result.unwrap();
888 cursor = cursor[consumed..].to_string();
889 }
890
891 /* Now there is no (#X) in front, we can check the path. */
892 // c:809
893 {
894 let mut gd = CURGLOBDATA.lock().unwrap_or_else(|e| e.into_inner());
895 // c:810-811 — `if (!pathbuf) pathbuf = zalloc(pathbufsz = PATH_MAX+1);`
896 if gd.pathbuf.capacity() == 0 {
897 gd.pathbufsz = libc::PATH_MAX as usize + 1;
898 gd.pathbuf = String::with_capacity(gd.pathbufsz);
899 }
900 // c:812 — `DPUTS(pathbufcwd, "BUG: glob changed directory");`
901 debug_assert!(gd.pathbufcwd == 0, "BUG: glob changed directory");
902 if cursor.starts_with('/') {
903 // c:813
904 /* pattern has absolute path */
905 cursor = cursor[1..].to_string(); // c:814 `str++;`
906 gd.pathbuf.clear();
907 gd.pathbuf.push('/'); // c:815 `pathbuf[0] = '/';`
908 gd.pathpos = 1; // c:816 `pathbuf[pathpos = 1] = '\0';`
909 } else {
910 /* pattern is relative to pwd */
911 // c:817
912 gd.pathbuf.clear(); // c:818 `pathbuf[pathpos = 0] = '\0';`
913 gd.pathpos = 0;
914 }
915 }
916
917 parsecomplist(&cursor) // c:820
918}
919
920/// Parse qualifier (from glob.c qgetnum)
921/// Parse a numeric glob-qualifier argument.
922/// Port of `qgetnum(char **s)` from Src/glob.c:827.
923pub fn qgetnum(s: &str) -> Option<(i64, &str)> {
924 // c:827
925 let end = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
926 if end == 0 {
927 return None;
928 }
929 let num = s[..end].parse::<i64>().ok()?;
930 Some((num, &s[end..]))
931}
932
933impl globdata {
934 /// `new` — see implementation.
935 pub fn new() -> Self {
936 globdata {
937 matches: Vec::new(),
938 qualifiers: None,
939 quals: None,
940 pathbuf: String::with_capacity(4096),
941 pathpos: 0,
942 matchct: 0,
943 pathbufsz: 4096,
944 pathbufcwd: 0,
945 gf_nullglob: 0,
946 gf_markdirs: 0,
947 gf_noglobdots: 0,
948 gf_listtypes: 0,
949 gf_pre_words: None,
950 gf_post_words: None,
951 }
952 }
953}
954
955// ============================================================================
956// Mode specification parsing (from glob.c qgetmodespec)
957// ============================================================================
958
959// `ModeSpec` struct deleted — Rust-only helper. C `qgetmodespec`
960// (`Src/glob.c:844`) parses one clause and returns the combined
961// `long` mode-bits directly, mutating the parse cursor via `char**`.
962// The Rust port now returns `(who, op, perm, rest)` as a flat tuple
963// so the canonical "no intermediate struct" pattern is preserved.
964
965/// Parse mode specification like chmod (from glob.c qgetmodespec lines 790-920)
966/// Examples: u+x, go-w, a=r, 755 — returns `(who, op, perm, rest)`.
967/// Port of `qgetmodespec(char **s)` from `Src/glob.c:844`.
968pub fn qgetmodespec(s: &str) -> Option<(u32, char, u32, &str)> {
969 let mut chars = s.chars().peekable();
970 let mut spec_who: u32 = 0;
971 let mut spec_op: char = '\0';
972 let mut spec_perm: u32 = 0;
973
974 // Check for octal mode
975 if chars.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
976 let mut mode_str = String::new();
977 while let Some(&c) = chars.peek() {
978 if c.is_ascii_digit() && c < '8' {
979 mode_str.push(c);
980 chars.next();
981 } else {
982 break;
983 }
984 }
985 if let Ok(mode) = u32::from_str_radix(&mode_str, 8) {
986 spec_perm = mode;
987 spec_op = '=';
988 spec_who = 0o7777;
989 let rest_pos = s.len() - chars.collect::<String>().len();
990 return Some((spec_who, spec_op, spec_perm, &s[rest_pos..]));
991 }
992 return None;
993 }
994
995 // Parse symbolic mode
996 // Who: u, g, o, a
997 let mut who = 0u32;
998 while let Some(&c) = chars.peek() {
999 match c {
1000 'u' => {
1001 who |= 0o4700;
1002 chars.next();
1003 }
1004 'g' => {
1005 who |= 0o2070;
1006 chars.next();
1007 }
1008 'o' => {
1009 who |= 0o1007;
1010 chars.next();
1011 }
1012 'a' => {
1013 who |= 0o7777;
1014 chars.next();
1015 }
1016 _ => break,
1017 }
1018 }
1019 if who == 0 {
1020 who = 0o7777; // Default to all
1021 }
1022 spec_who = who;
1023
1024 // Op: +, -, =
1025 spec_op = match chars.next() {
1026 Some('+') => '+',
1027 Some('-') => '-',
1028 Some('=') => '=',
1029 _ => return None,
1030 };
1031
1032 // Perm: r, w, x, X, s, t
1033 let mut perm = 0u32;
1034 while let Some(&c) = chars.peek() {
1035 match c {
1036 'r' => {
1037 perm |= 0o444;
1038 chars.next();
1039 }
1040 'w' => {
1041 perm |= 0o222;
1042 chars.next();
1043 }
1044 'x' => {
1045 perm |= 0o111;
1046 chars.next();
1047 }
1048 'X' => {
1049 perm |= 0o111;
1050 chars.next();
1051 } // Conditional execute
1052 's' => {
1053 perm |= 0o6000;
1054 chars.next();
1055 }
1056 't' => {
1057 perm |= 0o1000;
1058 chars.next();
1059 }
1060 _ => break,
1061 }
1062 }
1063 // c:Src/glob.c:903-913 — `while ((c = *p) == '?' || (c >= '0'
1064 // && c <= '7')) { ... val = (val << 3) | (c - '0'); }`.
1065 // In the C code path, perm letters (`r`/`w`/`x`/`s`/`t`) only
1066 // get parsed when an explicit `who` mask was set (the `if
1067 // (mask)` arm at c:877). When there's NO who (the
1068 // bare-spec form like `f=644`), C falls through to the
1069 // numeric digit loop in the `else if` arm at c:901. The
1070 // Rust port previously accepted letters in either case but
1071 // rejected digits — `f=644` would parse op='=' then break
1072 // on digit '6' and return spec_perm = 0, which the caller
1073 // computed as yes=0, no=0o7777 (essentially "match
1074 // nothing"). After also accepting digits here, `f=644`
1075 // builds spec_perm = 0o644 like C, and the matcher
1076 // resolves `*(.f=644)` correctly. Bug #105 in
1077 // docs/BUGS.md.
1078 if perm == 0 {
1079 let mut val = 0u32;
1080 let mut any_digit = false;
1081 while let Some(&c) = chars.peek() {
1082 if c == '?' {
1083 val <<= 3;
1084 chars.next();
1085 any_digit = true;
1086 } else if c.is_ascii_digit() && c < '8' {
1087 val = (val << 3) | (c as u32 - '0' as u32);
1088 chars.next();
1089 any_digit = true;
1090 } else {
1091 break;
1092 }
1093 }
1094 if any_digit {
1095 perm = val;
1096 }
1097 }
1098 spec_perm = perm & who;
1099
1100 let rest_pos = s.len() - chars.collect::<String>().len();
1101 Some((spec_who, spec_op, spec_perm, &s[rest_pos..]))
1102}
1103
1104// `impl GlobMatch` block deleted — C builds Gmatch entries inline
1105// in `insert()` (glob.c:346) and `gmatchcmp` (glob.c:936) is a
1106// free function taking two Gmatch pointers. The Rust port mirrors
1107// that shape: no methods on the struct; construction inlined at
1108// the scanner call site, comparator as a free fn below.
1109
1110/// Port of `gmatchcmp(Gmatch a, Gmatch b)` from Src/glob.c:936 —
1111/// the qsort comparator the `o`/`O` glob qualifier drives.
1112///
1113/// `specs` is the equivalent of the C `gf_sortlist` array, each
1114/// entry a packed i32 (the C `struct globsort.tp` field):
1115/// bits 0..=4 — primary key (GS_NAME / GS_DEPTH / GS_EXEC /
1116/// GS_SIZE / GS_ATIME / GS_MTIME / GS_CTIME /
1117/// GS_LINKS, plus GS_NONE marker)
1118/// bits << GS_SHIFT — same keys, follow-link variant
1119/// (GS__SIZE / GS__ATIME / …)
1120/// GS_DESC bit — reverse direction (`O` qualifier instead of `o`)
1121/// WARNING: param names don't match C — Rust=(b, specs, numeric_sort) vs C=(a, b)
1122pub fn gmatchcmp(
1123 // c:936
1124 a: &gmatch,
1125 b: &gmatch,
1126 specs: &[i32],
1127 numeric_sort: bool,
1128) -> std::cmp::Ordering {
1129 for &tp in specs {
1130 // c:943
1131 let key = tp & !GS_DESC; // c:944 s->tp & ~GS_DESC
1132 let follow = (key & GS_LINKED) != 0;
1133 let key_unshifted = if follow { key >> GS_SHIFT } else { key };
1134 let cmp = if key_unshifted == GS_NAME {
1135 // c:945 — `gmatch->name` in C zsh is the FULL match string
1136 // (Src/glob.c sets it from the accumulated path during the
1137 // scanner walk), so the qsort comparator inherently sorts by
1138 // full path. The Rust port stores the basename in `name`;
1139 // use `path` instead so `**/*` matches the full-path
1140 // lexicographic order zsh produces. Verified vs
1141 // /opt/homebrew/bin/zsh: `echo /tmp/rg/**/*` → `f sub sub/g`
1142 // (sorted by full path, not basename).
1143 let a_cow = a.path.to_string_lossy();
1144 let b_cow = b.path.to_string_lossy();
1145 // c:Src/glob.c:424 — C stores bare-relative match names
1146 // (`dyncat(pathbuf, news)`, no "./"), so its single qsort at
1147 // c:1977 sorts the exact strings it emits. The Rust scanner joins
1148 // depth-0 matches against base "." (glob.rs:527 + :584), giving a
1149 // leading "./" that deeper matches lack and that `glob_emit_path`
1150 // strips at emit. Since '.' (0x2E) sorts before any letter, that
1151 // stray prefix clustered every top-level entry ahead of any nested
1152 // path (`**/*` → `d e m d/z` instead of `d d/z e m`). Strip it here
1153 // so the sort key matches the emit key.
1154 let a_full = a_cow.strip_prefix("./").unwrap_or(&a_cow);
1155 let b_full = b_cow.strip_prefix("./").unwrap_or(&b_cow);
1156 zstrcmp(
1157 a_full,
1158 b_full,
1159 if numeric_sort {
1160 crate::zsh_h::SORTIT_NUMERICALLY as u32
1161 } else {
1162 0
1163 },
1164 )
1165 } else if key_unshifted == GS_DEPTH {
1166 // c:949-972 — pairwise: skip the common prefix of the two
1167 // full paths, then `slasha`/`slashb` = does the remainder of
1168 // each (excluding its final char) still contain a `/`, i.e.
1169 // is this path deeper past the divergence point. `r = slasha
1170 // - slashb`; ascending puts the deeper path first.
1171 let an = a.path.to_string_lossy();
1172 let bn = b.path.to_string_lossy();
1173 let ab = an.as_bytes();
1174 let bb = bn.as_bytes();
1175 // c:951 — `while (*aptr && *aptr == *bptr) aptr++,bptr++;`
1176 let mut i = 0;
1177 while i < ab.len() && i < bb.len() && ab[i] == bb[i] {
1178 i += 1;
1179 }
1180 // c:953-954 — if at the end of one and the prev char is `/`,
1181 // back up one so the trailing component is counted.
1182 let (mut ai, mut bi) = (i, i);
1183 if (ai >= ab.len() || bi >= bb.len()) && ai > 0 && ab[ai - 1] == b'/' {
1184 ai -= 1;
1185 bi -= 1;
1186 }
1187 // c:956-964 — slash present in the remainder, excluding last char.
1188 let has_slash = |s: &[u8]| s.len() > 1 && s[..s.len() - 1].contains(&b'/');
1189 let slasha = has_slash(&ab[ai.min(ab.len())..]) as i32;
1190 let slashb = has_slash(&bb[bi.min(bb.len())..]) as i32;
1191 // c:971 — `r = slasha - slashb`; deeper (slash=1) sorts first.
1192 slashb.cmp(&slasha)
1193 } else if key_unshifted == GS_SIZE {
1194 // c:985
1195 if follow {
1196 a.target_size.cmp(&b.target_size)
1197 } else {
1198 a.size.cmp(&b.size)
1199 }
1200 } else if key_unshifted == GS_ATIME {
1201 // c:988-994 — `r = a->atime - b->atime;` then tie-break by
1202 // `a->ansec - b->ansec` (under GET_ST_ATIME_NSEC).
1203 let primary = if follow {
1204 b.target_atime.cmp(&a.target_atime)
1205 } else {
1206 b.atime.cmp(&a.atime)
1207 };
1208 if primary != std::cmp::Ordering::Equal {
1209 primary
1210 } else if follow {
1211 b.target_ansec.cmp(&a.target_ansec) // c:1019
1212 } else {
1213 b.ansec.cmp(&a.ansec) // c:992
1214 }
1215 } else if key_unshifted == GS_MTIME {
1216 // c:995-1001 — `r = a->mtime - b->mtime;` then tie-break by
1217 // `a->mnsec - b->mnsec` (under GET_ST_MTIME_NSEC). Without
1218 // the nsec tie-break, files touched <1s apart sort by name.
1219 let primary = if follow {
1220 b.target_mtime.cmp(&a.target_mtime)
1221 } else {
1222 b.mtime.cmp(&a.mtime)
1223 };
1224 if primary != std::cmp::Ordering::Equal {
1225 primary
1226 } else if follow {
1227 b.target_mnsec.cmp(&a.target_mnsec) // c:1026
1228 } else {
1229 b.mnsec.cmp(&a.mnsec) // c:999
1230 }
1231 } else if key_unshifted == GS_CTIME {
1232 // c:1002-1008 — `r = a->ctime - b->ctime;` then tie-break by
1233 // `a->cnsec - b->cnsec` (under GET_ST_CTIME_NSEC).
1234 let primary = if follow {
1235 b.target_ctime.cmp(&a.target_ctime)
1236 } else {
1237 b.ctime.cmp(&a.ctime)
1238 };
1239 if primary != std::cmp::Ordering::Equal {
1240 primary
1241 } else if follow {
1242 b.target_cnsec.cmp(&a.target_cnsec) // c:1033
1243 } else {
1244 b.cnsec.cmp(&a.cnsec) // c:1006
1245 }
1246 } else if key_unshifted == GS_LINKS {
1247 // c:1010 — `r = b->links - a->links;` — SAME sign convention
1248 // as GS_SIZE (c:985), so ascending `ol` is fewest-links-first.
1249 // (Was reversed vs GS_SIZE, putting most-linked first.)
1250 if follow {
1251 a.target_links.cmp(&b.target_links)
1252 } else {
1253 a.links.cmp(&b.links)
1254 }
1255 } else if key_unshifted == GS_EXEC {
1256 // c:974
1257 let idx = ((key as u32) >> 16) as usize;
1258 let asx = a.sort_strings.get(idx).map(|s| s.as_str()).unwrap_or("");
1259 let bsx = b.sort_strings.get(idx).map(|s| s.as_str()).unwrap_or("");
1260 zstrcmp(
1261 asx,
1262 bsx,
1263 if numeric_sort {
1264 crate::zsh_h::SORTIT_NUMERICALLY as u32
1265 } else {
1266 0
1267 },
1268 )
1269 } else {
1270 std::cmp::Ordering::Equal // GS_NONE / unknown
1271 };
1272 if cmp != std::cmp::Ordering::Equal {
1273 return if (tp & GS_DESC) != 0 {
1274 cmp.reverse()
1275 } else {
1276 cmp
1277 };
1278 }
1279 }
1280 std::cmp::Ordering::Equal
1281}
1282
1283// `Redirect` struct + `RedirectType` enum + `xpandredir` fn
1284// DELETED. Both types were Rust-only duplicates of `parse::Redirect`
1285// / `parse::RedirectOp` with no callers. The `xpandredir` impl took
1286// the wrong signature anyway — C's `xpandredir(struct redir *fn,
1287// LinkList redirtab)` at `Src/glob.c:2150` mutates a linked-list
1288// in place and returns int; this Rust version returned `Vec<Redirect>`
1289// and operated on the duplicate Redirect type. Port `xpandredir`
1290// freshly against `parse::Redirect` when an actual caller appears.
1291
1292// ============================================================================
1293// Exec string for sorting (from glob.c glob_exec_string)
1294// ============================================================================
1295
1296/// Port of `static char *glob_exec_string(char **sp)` from
1297/// `Src/glob.c:1085`.
1298///
1299/// **C is a PARSER, not an executor.** It extracts the qualifier
1300/// argument string from `*sp` (advancing the pointer past the
1301/// delimiters) and returns the duplicated text. The actual eval
1302/// happens at the call sites (c:1680/1713/1747) which feed the
1303/// returned string into `qualsheval` (for `e:`/`+:`) or
1304/// `gf_sortlist[].exec` (for sort).
1305///
1306/// Previous Rust port was COMPLETELY MIS-IMPLEMENTED:
1307/// 1. **Signature wrong:** Rust took `(cmd, filename)` and returned
1308/// command output. C takes `(**sp)` and returns the parsed
1309/// qualifier string (the `cmd` itself, NOT its output).
1310/// 2. **Forked `/bin/sh`:** spawned a separate POSIX shell process
1311/// to run the cmd. That's even more broken than `qualsheval`
1312/// was, because the function isn't supposed to execute anything
1313/// at all at this layer.
1314///
1315/// Now mirrors C's parser body (c:1090-1117): handle the `+` prefix
1316/// (identifier form) vs the `e:` delimited form (get_strarg), set
1317/// `*sp` past the closing delimiter, return the dup'd inner text.
1318///
1319/// Returns `(parsed_string, advance_offset)` so the caller can
1320/// emulate C's `*sp = tt + plus;` pointer advance via slice
1321/// indexing.
1322/// WARNING: param names don't match C — Rust=(s, plus) vs C=(sp)
1323pub fn glob_exec_string(s: &str, plus_form: bool) -> Option<(String, usize)> {
1324 // c:1085
1325 if plus_form {
1326 // c:1090
1327 // c:1092 — `tt = itype_end(s, IIDENT, 0);`
1328 // c:1093-1097 — `if (tt == s) { zerr("missing identifier after `+'"); return NULL; }`
1329 let tt = crate::ported::utils::itype_end(s, crate::ported::ztype_h::IIDENT as u32, false);
1330 if tt == 0 {
1331 // c:1093
1332 zerr("missing identifier after `+'"); // c:1095
1333 return None; // c:1096
1334 }
1335 // c:1109 — `sdata = dupstring(s + plus);` (plus=0 here).
1336 // c:1113 — `*sp = tt + plus;` → advance offset is `tt`.
1337 Some((s[..tt].to_string(), tt))
1338 } else {
1339 // c:1099 — `tt = get_strarg(s, &plus);` — find matching delimiter.
1340 // get_strarg returns delimiter-balanced span; for `e:foo:` it
1341 // walks `s` past the inner expr and returns position of the
1342 // closing `:`.
1343 match crate::ported::subst::get_strarg(s) {
1344 Some((_del, content, rest)) => {
1345 // c:1100-1104 — `if (!*tt) { zerr("missing end of string"); return NULL; }`
1346 if rest.is_empty() && content.is_empty() {
1347 zerr("missing end of string"); // c:1102
1348 return None; // c:1103
1349 }
1350 // c:1109-1115 — `sdata = dupstring(s + plus); ... *sp = tt + plus;`.
1351 // Advance offset: bytes consumed of `s` = s.len() - rest.len().
1352 let advance = s.len() - rest.len();
1353 Some((content, advance))
1354 }
1355 None => {
1356 zerr("missing end of string"); // c:1102
1357 None
1358 }
1359 }
1360 }
1361}
1362
1363/*
1364 * Insert a glob match.
1365 * If there were words to prepend given by the P glob qualifier, do so.
1366 */
1367// c:1120-1123
1368/// Port of `insert_glob_match(LinkList list, LinkNode next, char *data)`
1369/// from Src/glob.c:1125. Inserts `data` at `next`, with optional
1370/// `gf_pre_words` prefix and `gf_post_words` suffix injection from
1371/// CURGLOBDATA (the `P:before:after:` glob qualifier).
1372pub fn insert_glob_match(list: &mut Vec<String>, next: usize, data: &str) {
1373 // c:1125
1374 let (pre, post) = {
1375 let gd = CURGLOBDATA.lock().unwrap_or_else(|e| e.into_inner());
1376 (gd.gf_pre_words.clone(), gd.gf_post_words.clone()) // c:1127, c:1136
1377 };
1378 // C `insertlinknode(list, next, data)` inserts AFTER node `next`,
1379 // returning the newly added node (so subsequent inserts append in
1380 // order). Our Vec<String> analog inserts at index `next+1` and
1381 // bumps `next` to point at the just-inserted slot.
1382 let mut cur = next;
1383 let n = list.len();
1384 let mut clamp = |i: usize| -> usize {
1385 if i > n {
1386 n
1387 } else {
1388 i
1389 }
1390 };
1391 if let Some(pre_words) = pre {
1392 // c:1127 `if (gf_pre_words)`
1393 for w in pre_words.iter() {
1394 // c:1129
1395 let pos = clamp(cur + 1);
1396 list.insert(pos, w.clone()); // c:1130 `dupstring(getdata(added))`
1397 cur = pos; // c:1130 — return-value advances `next`
1398 }
1399 }
1400 let pos = clamp(cur + 1);
1401 list.insert(pos, data.to_string()); // c:1134
1402 cur = pos;
1403 if let Some(post_words) = post {
1404 // c:1136 `if (gf_post_words)`
1405 for w in post_words.iter() {
1406 // c:1138
1407 let pos = clamp(cur + 1);
1408 list.insert(pos, w.clone()); // c:1139
1409 cur = pos;
1410 }
1411 }
1412}
1413
1414/// Port of `checkglobqual(char *str, int sl, int nobareglob, char **sp)` from Src/glob.c:1158.
1415/// C: `int checkglobqual(char *str, int sl, int nobareglob, char **sp)` —
1416/// confirm the trailing `(...)` is a glob qualifier (not literal).
1417/// Sets `*sp` to the qualifier start position. Returns 0 if not a
1418/// qualifier, non-zero if it is.
1419/// WARNING: param names don't match C — Rust=(str, sl, _nobareglob) vs C=(str, sl, nobareglob, sp)
1420pub fn checkglobqual(
1421 str: &str,
1422 sl: i32,
1423 _nobareglob: i32, // c:1158
1424 sp: &mut Option<usize>,
1425) -> i32 {
1426 // c:1163-1164 — `if (str[sl-1] != Outpar) return 0;`
1427 let bytes = str.as_bytes();
1428 let sl = sl as usize;
1429 if sl == 0 || bytes[sl - 1] != b')' {
1430 // c:1164
1431 return 0;
1432 }
1433 // c:1167-1212 — walk backwards counting parens to find matching `(`.
1434 let mut paren = 1i32;
1435 let mut i = sl - 1;
1436 while i > 0 {
1437 i -= 1;
1438 match bytes[i] {
1439 b')' => paren += 1,
1440 b'(' => {
1441 paren -= 1;
1442 if paren == 0 {
1443 *sp = Some(i);
1444 return 1; // c:1209
1445 }
1446 }
1447 _ => {}
1448 }
1449 }
1450 0 // c:1212
1451}
1452
1453/// Port of `zglob(LinkList list, LinkNode np, int nountok)` from
1454/// Src/glob.c:1214. Top-level glob expansion: gate on GLOBOPT/
1455/// EXECOPT/haswilds (c:1230-1234), remove the placeholder node,
1456/// expand the pattern via `glob_path` (which covers the c:1240-2012
1457/// qualifier+scanner+sort body), then splice the resulting matches
1458/// back at `node` via `insert_glob_match` (c:1995-2007).
1459pub fn zglob(list: &mut Vec<String>, np: usize, nountok: i32) {
1460 // c:1214
1461 if np >= list.len() {
1462 return;
1463 }
1464 // c:1217 — `LinkNode node = prevnode(np);` — the insertion point
1465 // after the placeholder is uremnode'd. Vec analog: insert at np.
1466 let node: usize = np; // c:1217
1467 let ostr = list[np].clone(); // c:1221 `ostr = getdata(np)`
1468 // c:1226 — `nobareglob = !isset(BAREGLOBQUAL);` (consumed by qualifier
1469 // parser inside glob_path).
1470
1471 // c:1230 — `if (unset(GLOBOPT) || !haswilds(ostr) || unset(EXECOPT))`
1472 if !crate::ported::zsh_h::isset(crate::ported::zsh_h::GLOBOPT)
1473 || !haswilds(&ostr)
1474 || !crate::ported::zsh_h::isset(crate::ported::zsh_h::EXECOPT)
1475 {
1476 if nountok == 0 {
1477 // c:1231
1478 // c:1232 — untokenize in-place; replace list[np] with the
1479 // untokenized form.
1480 list[np] = crate::ported::lex::untokenize(&ostr);
1481 }
1482 return; // c:1233
1483 }
1484
1485 // c:1235 — `save_globstate(saved);`. zshrs snapshots the
1486 // glob-relevant options into TLS (`enter_glob_scope`, required for
1487 // thread-safety) — the RAII guard restores them on return.
1488 let _glob_scope = enter_glob_scope();
1489
1490 // c:1237-1238 — `str = dupstring(ostr); uremnode(list, np);`
1491 list.remove(np); // c:1238
1492
1493 // c:1240-1995 — the scanner walk + match collection. zshrs drives
1494 // it through `globdata_glob`, the RUST-ONLY read_dir adaptation of
1495 // C's `scanner()` (c:500; C's `lchdir` descent is process-global and
1496 // unsafe under zshrs's threading). globdata_glob owns its own
1497 // globdata; call it directly rather than via the glob_path Vec
1498 // convenience.
1499 let matches = {
1500 let mut st = globdata::new();
1501 globdata_glob(&mut st, &ostr)
1502 };
1503
1504 // c:1871-1875 — badcshglob accounting. Each zglob run updates
1505 // the per-command-line counter so globlist's terminal diagnostic
1506 // can distinguish "some failures, no successes" (emit error)
1507 // from "some failures, some successes" (silent).
1508 if !matches.is_empty() {
1509 // c:1872 — `badcshglob |= 2;` (at least one expansion OK).
1510 BADCSHGLOB.fetch_or(2, std::sync::atomic::Ordering::Relaxed);
1511 } else if crate::ported::zsh_h::isset(crate::ported::zsh_h::CSHNULLGLOB) {
1512 // c:1874-1875 — `badcshglob |= 1;` (at least one expansion
1513 // failed) under CSHNULLGLOB.
1514 BADCSHGLOB.fetch_or(1, std::sync::atomic::Ordering::Relaxed);
1515 }
1516
1517 // c:1872-1888 — `Deal with failures to match depending on options`.
1518 // C body verbatim (c:1872-1888):
1519 // if (matchct)
1520 // badcshglob |= 2;
1521 // else if (!gf_nullglob) {
1522 // if (isset(CSHNULLGLOB)) {
1523 // badcshglob |= 1;
1524 // } else if (isset(NOMATCH)) {
1525 // zerr("no matches found: %s", ostr);
1526 // zfree(matchbuf, 0);
1527 // restore_globstate(saved);
1528 // return;
1529 // } else {
1530 // /* treat as an ordinary string */
1531 // untokenize(matchptr->name = dupstring(ostr));
1532 // matchptr++;
1533 // matchct = 1;
1534 // }
1535 // }
1536 // gf_nullglob (c:212) is the per-glob nullglob bit toggled by
1537 // qualifier `N`; here we approximate with the global option.
1538 // Parity bug #13: previously this Rust arm fell through to the
1539 // ordinary-literal path (c:1882-1887) unconditionally, making
1540 // `echo /never/*` print the literal glob instead of erroring.
1541 if matches.is_empty() {
1542 let nullglob = isset(crate::ported::zsh_h::NULLGLOB); // c:1873 !gf_nullglob
1543 let csh_nullglob = isset(crate::ported::zsh_h::CSHNULLGLOB); // c:1874
1544 // c:Src/glob.c:1843-1854 — `if (!q || errflag) { ... zerr(
1545 // "bad pattern", ostr); return; }`. When the qualifier
1546 // parser already emitted a diagnostic (e.g. "number expected"
1547 // from qgetnum at c:832) and set errflag, the no-matches /
1548 // bad-pattern terminal block runs but the prior zerr is what
1549 // the user sees first. Skipping the redundant "no matches
1550 // found" here matches zsh which has already aborted glob
1551 // expansion via the errflag-gated return at c:1787 / c:1843.
1552 if errflag.load(Ordering::SeqCst) != 0 {
1553 return;
1554 }
1555 // c:1872 — `else if (!gf_nullglob) { ... }`. The ENTIRE no-match
1556 // handling (CSHNULLGLOB, NOMATCH, literal fallback) is gated on
1557 // nullglob being OFF. When nullglob is set the failed word is
1558 // simply dropped: C never enters the block, matchbuf stays empty,
1559 // and glob() removes the word from the list. Hoisting the
1560 // `!nullglob` test to a single outer gate (matching the C
1561 // `else if`) ensures the literal-insert fallback below is skipped
1562 // under nullglob too — previously it fell through unconditionally
1563 // and echoed the literal, diverging from c:1872.
1564 if !nullglob {
1565 if csh_nullglob {
1566 // c:1874-1875 — `if (isset(CSHNULLGLOB)) { badcshglob |= 1; }`
1567 // (already recorded above). The else-if chain means the
1568 // ordinary-string arm is NOT reached: the failed word is
1569 // DROPPED like nullglob, and globlist's terminal check
1570 // (subst.c:505-507, ported at subst.rs:1419) emits the
1571 // csh-style `no match` when NO word on the line matched.
1572 // Falling through to the literal insert here produced the
1573 // NOMATCH-style "no matches found: PAT" instead.
1574 return;
1575 }
1576 if isset(crate::ported::zsh_h::NOMATCH) {
1577 // c:1876-1880 — `else if (isset(NOMATCH)) { zerr; return; }`
1578 crate::ported::utils::zerr(&format!("no matches found: {}", ostr));
1579 // c:1878 — `zfree(matchbuf, 0);` (Rust drop handles it)
1580 // c:1879 — `restore_globstate(saved);` (handled by guard)
1581 return; // c:1880
1582 }
1583 // c:1882-1887 — `treat as an ordinary string`. The matchptr++
1584 // bookkeeping in C maps to our `list.insert`.
1585 let restored = if nountok == 0 {
1586 crate::ported::lex::untokenize(&ostr) // c:1884 untokenize(matchptr->name = dupstring(ostr))
1587 } else {
1588 ostr.clone()
1589 };
1590 let pos = if node > list.len() { list.len() } else { node };
1591 list.insert(pos, restored); // c:1885 matchptr++, c:1886 matchct = 1
1592 }
1593 // nullglob set → fall through with matchbuf empty: the word is
1594 // dropped (c:1872 skips the block entirely). c:1889's
1595 // `else if (in_expandredir)` redirect-failure arm is handled by
1596 // the caller (vm_helper.rs redirect-glob path), not here.
1597 return;
1598 }
1599
1600 // c:1995-2007 — splice matches back via insert_glob_match (honors
1601 // gf_pre_words / gf_post_words).
1602 let mut cur = if node == 0 { 0 } else { node - 1 }; // node is `prevnode(np)` semantics
1603 for m in matches.iter() {
1604 insert_glob_match(list, cur, m); // c:1995
1605 cur += 1;
1606 if let Some(g) = CURGLOBDATA.lock().ok() {
1607 // Advance past any pre_words that insert_glob_match added.
1608 if let Some(p) = &g.gf_pre_words {
1609 cur += p.len();
1610 }
1611 if let Some(p) = &g.gf_post_words {
1612 cur += p.len();
1613 }
1614 }
1615 }
1616}
1617
1618/// File type character for -F style listing
1619/// Render a mode bitmap as the `*` qualifier letter (`d`/`b`/
1620/// `c`/`l`/`s`/`p`/etc.).
1621/// Port of `file_type(mode_t filemode)` from Src/glob.c:2018.
1622pub fn file_type(filemode: u32) -> char {
1623 // c:2018
1624 let fmt = filemode & libc::S_IFMT as u32;
1625 if fmt == libc::S_IFBLK as u32 {
1626 '#'
1627 } else if fmt == libc::S_IFCHR as u32 {
1628 '%'
1629 } else if fmt == libc::S_IFDIR as u32 {
1630 '/'
1631 } else if fmt == libc::S_IFIFO as u32 {
1632 '|'
1633 } else if fmt == libc::S_IFLNK as u32 {
1634 '@'
1635 } else if fmt == libc::S_IFREG as u32 {
1636 if filemode & 0o111 != 0 {
1637 '*'
1638 } else {
1639 ' '
1640 }
1641 } else if fmt == libc::S_IFSOCK as u32 {
1642 '='
1643 } else {
1644 '?'
1645 }
1646}
1647
1648// ============================================================================
1649// Brace expansion
1650// ============================================================================
1651
1652/// Check if string has brace expansion
1653/// Check whether a string has brace-expansion `{a,b}` content.
1654/// Port of `hasbraces(char *str)` from Src/glob.c:2042.
1655/// WARNING: param names don't match C — Rust=(s, brace_ccl) vs C=(str)
1656pub fn hasbraces(s: &str, brace_ccl: bool) -> bool {
1657 // c:2042
1658 let mut depth = 0;
1659 let mut has_comma = false;
1660 let mut has_dotdot = false;
1661 let mut brace_open: Option<usize> = None;
1662
1663 let chars: Vec<char> = s.chars().collect();
1664 let len = chars.len();
1665
1666 let mut i = 0;
1667 while i < len {
1668 // c:Src/lex.c:3587-3600 — backslash escape converts the next
1669 // char into a tokenized literal (Bnull/Bnullkeep). xpandbraces
1670 // sees the tokenized form so `\{` never enters the brace
1671 // walk. Accept both the canonical Bnull (`\u{9f}`,
1672 // Src/zsh.h:195) and ASCII `\` so direct callers (tests,
1673 // utility paths) and pipeline callers (bridge → multsub →
1674 // xpandbraces with Bnull markers from gettokstr) both behave
1675 // the same: skip the escape marker plus the next char.
1676 if (chars[i] == '\\' || chars[i] == '\u{9f}') && i + 1 < len {
1677 i += 2; // c:3591 — skip Bnull/Bnullkeep + escaped char
1678 continue;
1679 }
1680 match chars[i] {
1681 // c:Src/glob.c:hasbraces — Inbrace/Outbrace/Comma TOKEN
1682 // strictly (\u{8f} / \u{90} / \u{9a}). The lexer emits
1683 // TOKEN form for unescaped `{`/`,`/`}`; `\X` produces
1684 // Bnull + ASCII X. After remnulargs strips Bnull, the
1685 // ASCII X doesn't match the TOKEN check so escaped
1686 // braces correctly bypass expansion.
1687 '\u{8f}' => {
1688 if depth == 0 {
1689 brace_open = Some(i);
1690 }
1691 depth += 1;
1692 }
1693 '\u{90}' if depth > 0 => {
1694 depth -= 1;
1695 if depth == 0 {
1696 // c:2050-2061 — a comma group always expands.
1697 if has_comma {
1698 return true;
1699 }
1700 if has_dotdot {
1701 // c:2071-2096 — a `..` group expands ONLY when it
1702 // is a valid char range (bracechardots, c:2071) OR
1703 // a numeric range whose endpoints are immediately
1704 // bounded by the closing brace: C walks `[-]digits
1705 // .. [-]digits` and requires the next char to be
1706 // Outbrace (c:2084) or `..incr..` + Outbrace
1707 // (c:2087-2095). Trailing garbage (`{0..5%2}`,
1708 // `{1..3x}`, `{1.2..3}`, `{1..5.}`) fails this, so
1709 // the group stays LITERAL with braces. Nested
1710 // groups stay permissive — recursive xpandbraces
1711 // handles the inner range.
1712 let content: String =
1713 chars[brace_open.unwrap_or(0) + 1..i].iter().collect();
1714 let nested = content.contains('\u{8f}');
1715 // c:2074-2096 — structural walk: optional `-`,
1716 // digits (0+), `..`, optional `-`, digits (0+),
1717 // then MUST end at the brace (Outbrace), or take a
1718 // `..incr` step then end. C allows EMPTY endpoints
1719 // (`{1..}`, `{..5}` pass hasbraces, then fail the
1720 // range parse and get brace-stripped) — what it
1721 // forbids is trailing non-range chars (`{0..5%2}`,
1722 // `{1.2..3}`). Closes with c:2085 `idigit(lbr[1])
1723 // || idigit(str[-1])` — a digit adjacent to `..`.
1724 // Dash TOKEN (\u{9b}) reads as ASCII `-`.
1725 let numeric_ok = {
1726 let norm: String = content
1727 .chars()
1728 .map(|c| if c == '\u{9b}' { '-' } else { c })
1729 .collect();
1730 let b = norm.as_bytes();
1731 let n = b.len();
1732 let mut j = 0;
1733 let mut shape_ok = false;
1734 if j < n && b[j] == b'-' {
1735 j += 1; // c:2074 leading `-`
1736 }
1737 while j < n && b[j].is_ascii_digit() {
1738 j += 1; // c:2076 first number
1739 }
1740 if j + 1 < n && b[j] == b'.' && b[j + 1] == b'.' {
1741 j += 2; // c:2078 `..`
1742 if j < n && b[j] == b'-' {
1743 j += 1; // c:2080
1744 }
1745 while j < n && b[j].is_ascii_digit() {
1746 j += 1; // c:2082 second number
1747 }
1748 if j == n {
1749 shape_ok = true; // c:2084 Outbrace
1750 } else if j + 1 < n && b[j] == b'.' && b[j + 1] == b'.' {
1751 j += 2; // c:2087 `..incr`
1752 if j < n && b[j] == b'-' {
1753 j += 1;
1754 }
1755 while j < n && b[j].is_ascii_digit() {
1756 j += 1; // c:2091
1757 }
1758 if j == n {
1759 shape_ok = true; // c:2093
1760 }
1761 }
1762 }
1763 // c:2085/2094 — digit adjacent to `..`.
1764 shape_ok
1765 && (b.first().is_some_and(|c| c.is_ascii_digit())
1766 || b.last().is_some_and(|c| c.is_ascii_digit()))
1767 };
1768 if nested || bracechardots(&content).is_some() || numeric_ok {
1769 return true;
1770 }
1771 }
1772 if brace_ccl {
1773 if let Some(open) = brace_open {
1774 if i > open + 1 {
1775 return true;
1776 }
1777 }
1778 }
1779 has_comma = false;
1780 has_dotdot = false;
1781 brace_open = None;
1782 }
1783 }
1784 // c:Src/glob.c:hasbraces — accept comma / `..` at ANY depth
1785 // (was `depth == 1` only). Nested groups like `{{1,2}}`
1786 // have the comma at depth 2; without this, hasbraces
1787 // returned false and the outer xpandbraces never ran,
1788 // leaving `{{1,2}}` literal instead of expanding to
1789 // `{1} {2}` per zsh's nested-brace pass.
1790 '\u{9a}' if depth > 0 => has_comma = true,
1791 '.' if depth > 0 && i + 1 < len && chars[i + 1] == '.' => has_dotdot = true,
1792 _ => {}
1793 }
1794 i += 1;
1795 }
1796
1797 false
1798}
1799
1800// ============================================================================
1801// Brace char range parsing (from glob.c bracechardots)
1802// ============================================================================
1803
1804/// Parse character range in braces like {a..z} (from glob.c bracechardots line 2222)
1805/// Port of `bracechardots(char *str, convchar_t *c1p, convchar_t *c2p)` from `Src/glob.c:2222`.
1806/// WARNING: param names don't match C — Rust=(s) vs C=(str, c1p, c2p)
1807pub fn bracechardots(s: &str) -> Option<(char, char, i32)> {
1808 let chars: Vec<char> = s.chars().collect();
1809
1810 // Must be at least "a..b"
1811 if chars.len() < 4 {
1812 return None;
1813 }
1814
1815 // Find ..
1816 let dotdot_pos = s.find("..")?;
1817 if dotdot_pos == 0 {
1818 return None;
1819 }
1820
1821 let left = &s[..dotdot_pos];
1822 let right = &s[dotdot_pos + 2..];
1823
1824 // Check for increment
1825 let (end_str, incr) = if let Some(pos) = right.find("..") {
1826 let end = &right[..pos];
1827 let inc: i32 = right[pos + 2..].parse().unwrap_or(1);
1828 (end, inc)
1829 } else {
1830 (right, 1)
1831 };
1832
1833 // Single character range
1834 if left.chars().count() == 1 && end_str.chars().count() == 1 {
1835 let c1 = left.chars().next()?;
1836 let c2 = end_str.chars().next()?;
1837 return Some((c1, c2, incr));
1838 }
1839
1840 None
1841}
1842
1843/// Expand braces in a string
1844/// Brace-expand a string into a flat list.
1845/// Port of `xpandbraces(LinkList list, LinkNode *np)` from Src/glob.c:2276 — same
1846/// `{a,b}` / `{1..10}` / `{a-z}` handling.
1847/// WARNING: param names don't match C — Rust=(s, brace_ccl) vs C=(list, np)
1848pub fn xpandbraces(s: &str, brace_ccl: bool) -> Vec<String> {
1849 // c:2276
1850 if !hasbraces(s, brace_ccl) {
1851 return vec![s.to_string()];
1852 }
1853
1854 // Inline single-brace expansion — direct port of the per-iteration
1855 // brace-scan inside zsh's xpandbraces (Src/glob.c:2276). Walks the
1856 // string, finds the first `{`...`}` group, classifies as range
1857 // (`a..b`) / comma (`a,b`) / ccl (`[abc]`-style char-class), and
1858 // dispatches to the matching expander. Returns Some(parts) on
1859 // expansion, None if no brace group or unmatched.
1860 // c:Src/glob.c:2276 xpandbraces — advances `lbr` through every
1861 // candidate `{` and tries each. Bug #575: zshrs only tried the
1862 // FIRST `{` and returned None for the whole string when that
1863 // group was not expandable, so `{a-c}{1..3}` left the `{1..3}`
1864 // unexpanded. Mirror C by carrying a `from` offset and walking
1865 // to the next `{` on failure.
1866 //
1867 // Returns (Some(parts), _) on successful expansion. Returns
1868 // (None, Some(next_from)) when the current `{...}` was found but
1869 // didn't expand — outer loop should retry from next_from to scan
1870 // for a later expandable group. Returns (None, None) when no
1871 // `{...}` remains.
1872 let try_expand_from = |s: &str, from: usize| -> (Option<Vec<String>>, Option<usize>) {
1873 let chars: Vec<char> = s.chars().collect();
1874 let len = chars.len();
1875 // c:Src/glob.c:xpandbraces — Inbrace TOKEN strict.
1876 let start = match chars[from..].iter().position(|&c| c == '\u{8f}') {
1877 Some(p) => from + p,
1878 None => return (None, None),
1879 };
1880 let mut depth = 1;
1881 let mut comma_positions = Vec::new();
1882 let mut dotdot_pos = None;
1883 for i in (start + 1)..len {
1884 match chars[i] {
1885 '\u{8f}' => depth += 1,
1886 '\u{90}' => {
1887 depth -= 1;
1888 if depth == 0 {
1889 let next_from = i + 1;
1890 let prefix: String = chars[..start].iter().collect();
1891 let suffix: String = chars[i + 1..].iter().collect();
1892 let content: String = chars[start + 1..i].iter().collect();
1893 if let Some(dp) = dotdot_pos {
1894 if comma_positions.is_empty() {
1895 if let Some(parts) = expand_range(&prefix, &content, dp, &suffix) {
1896 return (Some(parts), None);
1897 }
1898 // c:Src/glob.c:2476-2506 — when range
1899 // parsing fails INSIDE C's xpandbraces
1900 // (err != 0 set at c:2355/2360/2369/2371),
1901 // the function falls through to the
1902 // normal-comma-expansion path. With no
1903 // commas, that loop produces a single
1904 // `prefix + content + suffix` — i.e.
1905 // strips the outermost braces only.
1906 // BUT: C only reaches xpandbraces when
1907 // `hasbraces()` (c:2042) returned 1,
1908 // and hasbraces requires DIGITS in the
1909 // range endpoints (c:2076-2096) for the
1910 // numeric form. Patterns like
1911 // `{hello..world}` fail hasbraces and
1912 // never enter xpandbraces — they stay
1913 // as literal `{hello..world}`.
1914 //
1915 // Rust's hasbraces is more permissive
1916 // (accepts any `..` inside `{}`), so we
1917 // gate the strip-braces fallback on the
1918 // SAME shape C uses: at least one digit
1919 // must appear in the left or right
1920 // endpoint, mirroring c:2085 `idigit
1921 // (lbr[1]) || idigit(str[-1])`.
1922 // `dp` is a CHAR index; byte-slice via a
1923 // converted offset so multibyte/metafied
1924 // content (`{$'\x80'..$'\x81'}`) can't panic.
1925 let dp_byte = content
1926 .char_indices()
1927 .nth(dp)
1928 .map(|(b, _)| b)
1929 .unwrap_or(content.len());
1930 let left = &content[..dp_byte];
1931 let right = &content[dp_byte + 2..];
1932 let strip_end =
1933 right.find("..").map(|p| &right[..p]).unwrap_or(right);
1934 let has_digit = left.chars().any(|c| c.is_ascii_digit())
1935 || strip_end.chars().any(|c| c.is_ascii_digit());
1936 if has_digit {
1937 // c:2495-2498 — strip braces.
1938 return (
1939 Some(vec![format!("{}{}{}", prefix, content, suffix)]),
1940 None,
1941 );
1942 }
1943 // Non-digit `..` content (e.g.
1944 // `{hello..world}`) — C wouldn't have
1945 // entered xpandbraces. Preserve the
1946 // literal pattern intact. Allow outer
1947 // loop to retry from next_from in case
1948 // a later brace group is expandable.
1949 return (None, Some(next_from));
1950 }
1951 }
1952 if !comma_positions.is_empty() {
1953 return (
1954 expand_comma(&prefix, &content, &comma_positions, &suffix),
1955 None,
1956 );
1957 }
1958 if brace_ccl && !content.is_empty() {
1959 return (expand_ccl(&prefix, &content, &suffix), None);
1960 }
1961 // Outer brace has no comma/dotdot at depth 1,
1962 // but content may contain nested braces (e.g.
1963 // `{{1,2}}` → `{1} {2}` — outer braces become
1964 // literal, inner expands).
1965 if content.contains('\u{8f}') {
1966 let inner_expanded = xpandbraces(&content, brace_ccl);
1967 let mut out: Vec<String> = Vec::with_capacity(inner_expanded.len());
1968 for piece in inner_expanded {
1969 out.push(format!("{}{{{}}}{}", prefix, piece, suffix));
1970 }
1971 return (Some(out), None);
1972 }
1973 // Literal-only group: allow outer to retry
1974 // from next_from for a later expandable group.
1975 return (None, Some(next_from));
1976 }
1977 }
1978 '\u{9a}' if depth == 1 => comma_positions.push(i - start - 1),
1979 '.' if depth == 1 && i + 1 < len && chars[i + 1] == '.' && dotdot_pos.is_none() => {
1980 dotdot_pos = Some(i - start - 1);
1981 }
1982 _ => {}
1983 }
1984 }
1985 (None, None)
1986 };
1987
1988 // Outer loop: try expanding starting from each `{` in turn.
1989 // Returns first successful expansion; None if none found.
1990 let try_expand_one = |s: &str| -> Option<Vec<String>> {
1991 let mut from = 0;
1992 loop {
1993 let (result, next) = try_expand_from(s, from);
1994 if let Some(parts) = result {
1995 return Some(parts);
1996 }
1997 match next {
1998 Some(nf) if nf > from => from = nf,
1999 _ => return None,
2000 }
2001 }
2002 };
2003
2004 let mut results = vec![s.to_string()];
2005 let mut changed = true;
2006 while changed {
2007 changed = false;
2008 let mut new_results = Vec::new();
2009 for item in &results {
2010 if let Some(expanded) = try_expand_one(item) {
2011 new_results.extend(expanded);
2012 changed = true;
2013 } else {
2014 new_results.push(item.clone());
2015 }
2016 }
2017 results = new_results;
2018 }
2019 results
2020}
2021
2022/// Simple glob pattern matching
2023/* check to see if a matches b (b is not a filename pattern) */ // c:2510
2024// !!! WARNING: RUST-ONLY HELPER — extra args + flipped arg order vs C !!!
2025// C signature: `int matchpat(char *a, char *b)` — `a` = text to match,
2026// `b` = pattern. Rust callers (cond.rs, watch.rs, glob.rs internal users)
2027// pass `(pattern, text, extended, case_sensitive)` — pattern-FIRST,
2028// flipped from C, plus two per-call option overrides. The BODY below
2029// is C-faithful for the matching path (patcompile + pattry, no Unicode
2030// case-folding); `extended`/`case_sensitive` drive transient
2031// `opt_state_set` overrides around the patcompile call. The transient
2032// swap is the WRONG mechanism — patcompile is not reentrant under
2033// concurrent option mutation. SOLE acceptable use: this same-process,
2034// single-threaded matchpat call path. Replace with: all callers
2035// migrate to (a) global setopt and (b) canonical C arg order
2036// `matchpat(text, pattern)`.
2037// !!! WARNING: RUST-ONLY HELPER — extra args + flipped arg order vs C !!!
2038/// Port of `matchpat(char *a, char *b)` from `Src/glob.c:2514`.
2039/// CALLER CONVENTION: Rust=(pattern, text, ext, cs) — pattern FIRST
2040/// (FLIPPED from C `matchpat(text=a, pattern=b)`). Body remaps to
2041/// C order internally.
2042pub fn matchpat(pattern_in: &str, text_in: &str, extended: bool, case_sensitive: bool) -> bool {
2043 // Remap to C names (a=text, b=pattern) at the function boundary so
2044 // the body below reads identically to Src/glob.c:2514-2530.
2045 let a = text_in;
2046 let b = pattern_in;
2047 // c:2514
2048 crate::ported::signals_h::queue_signals(); // c:2519
2049 // GF_IGNCASE handling: zshrs's pattern matcher DOES honor the
2050 // flag in `patmatch_internal` (pattern.rs:2245/2312/2333 — P_EXACTLY,
2051 // P_ANYOF, P_ANYBUT all check `glob_flags & (GF_IGNCASE|GF_LCMATCHUC)`
2052 // and case-fold appropriately). The pre-fold below is a defensive
2053 // fast path for the simple case-insensitive `:#`/`:#:` filter — it
2054 // bypasses the matcher's per-arm fold and avoids any stale-cache
2055 // edge case in the compile→match handoff. Costs one to_lowercase
2056 // call per side; correct under all matcher implementations.
2057 let (a_eff, b_eff) = if case_sensitive {
2058 (a.to_string(), b.to_string())
2059 } else {
2060 (a.to_lowercase(), b.to_lowercase())
2061 };
2062 // Per-call option override (Rust-only): snapshot, set, restore.
2063 let prev_extended = crate::ported::options::opt_state_get("extendedglob");
2064 let prev_caseglob = crate::ported::options::opt_state_get("caseglob");
2065 crate::ported::options::opt_state_set("extendedglob", extended);
2066 crate::ported::options::opt_state_set("caseglob", case_sensitive);
2067 // c:2521 — `if (!(p = patcompile(b, PAT_STATIC, NULL)))`. Rust uses
2068 // PAT_HEAPDUP (=0) — pattern::patmatch's canonical compile path;
2069 // PAT_STATIC's static-buffer path is incomplete in zshrs.
2070 let p_opt = crate::ported::pattern::patcompile(
2071 &{
2072 let mut __pat_tok = (&b_eff).to_string();
2073 crate::ported::glob::tokenize(&mut __pat_tok);
2074 __pat_tok
2075 },
2076 crate::ported::zsh_h::PAT_HEAPDUP,
2077 None,
2078 );
2079 if let Some(v) = prev_extended {
2080 crate::ported::options::opt_state_set("extendedglob", v);
2081 }
2082 if let Some(v) = prev_caseglob {
2083 crate::ported::options::opt_state_set("caseglob", v);
2084 }
2085 let ret = match p_opt {
2086 Some(p) => crate::ported::pattern::pattry(&p, &a_eff), // c:2525
2087 None => {
2088 crate::ported::utils::zerr(&format!("bad pattern: {}", b)); // c:2522
2089 false // c:2523
2090 }
2091 };
2092 crate::ported::signals_h::unqueue_signals(); // c:2527
2093 ret // c:2529
2094}
2095
2096/// Port of `get_match_ret(Imatchdata imd, int b, int e)` from `Src/glob.c:2550`.
2097///
2098/// C returns `char *`: `NULL` when there is nothing to emit, `imd->mstr`
2099/// when the match was recorded into `repllist` (SUB_GLOBAL / SUB_LIST),
2100/// else a freshly built buffer. Rust returns `Option<String>` (`None` ==
2101/// C `NULL`). `b`/`e` arrive as *unmetafied* byte offsets and are first
2102/// re-based to metafied byte offsets, exactly as C does. Takes `&mut`
2103/// because the SUB_GLOBAL/SUB_LIST branch pushes onto `imd->repllist`.
2104pub fn get_match_ret(imd: &mut imatchdata, b: usize, e: usize) -> Option<String> {
2105 let mut buf = String::new(); // c:2552 char buf[80]
2106 let mut ll: i64 = 0; // c:2553 ll = 0
2107 let mut bl: usize = 0; // c:2553 bl = 0
2108 let mut t = false; // c:2553 t = 0
2109 let mut add: usize = 0; // c:2553 add = 0
2110 let fl = imd.flags; // c:2553 fl = imd->flags
2111 let mut replstr: Option<String> = imd.replstr.clone(); // c:2552 *replstr = imd->replstr
2112
2113 let ustr_owned = imd.ustr.clone().unwrap_or_default();
2114 let ustr = ustr_owned.as_bytes();
2115 let mstr_owned = imd.mstr.clone().unwrap_or_default();
2116 let mstr = mstr_owned.as_bytes();
2117 let mlen = imd.mlen as usize; // c:2544 imd->mlen
2118
2119 // c:2555-2564 — account for b and e referring to unmetafied string.
2120 let mut p = 0usize; // c:2556 p = imd->ustr
2121 while p < b && p < ustr.len() {
2122 // c:2556 for (; p < imd->ustr + b; p++)
2123 if imeta(ustr[p]) {
2124 add += 1; // c:2558 add++
2125 }
2126 p += 1;
2127 }
2128 let b = b + add; // c:2559 b += add
2129 while p < e && p < ustr.len() {
2130 // c:2560 for (; p < imd->ustr + e; p++)
2131 if imeta(ustr[p]) {
2132 add += 1; // c:2562 add++
2133 }
2134 p += 1;
2135 }
2136 let e = e + add; // c:2563 e += add
2137
2138 // c:2566 — Everything now refers to metafied lengths.
2139 if replstr.is_some() || (fl & SUB_LIST) != 0 {
2140 // c:2567
2141 if (fl & SUB_DOSUBST) != 0 {
2142 // c:2568
2143 let mut rs = dupstring(replstr.as_deref().unwrap_or("")); // c:2569 dupstring(replstr)
2144 rs = singsub(&rs); // c:2570 singsub(&replstr)
2145 rs = untokenize(&rs); // c:2571 untokenize(replstr)
2146 replstr = Some(rs);
2147 }
2148 if (fl & (SUB_GLOBAL | SUB_LIST)) != 0 && imd.repllist.is_some() {
2149 // c:2573 — replacing the chunk, just add this to the list.
2150 let rd = repldata {
2151 b: b as i32, // c:2578 rd->b = b
2152 e: e as i32, // c:2579 rd->e = e
2153 replstr: replstr.clone(), // c:2580 rd->replstr = replstr
2154 };
2155 imd.repllist.as_mut().unwrap().push(rd); // c:2581-2584 z/addlinknode(repllist, rd)
2156 return imd.mstr.clone(); // c:2585 return imd->mstr
2157 }
2158 if let Some(ref r) = replstr {
2159 ll += r.len() as i64; // c:2588 ll += strlen(replstr)
2160 }
2161 }
2162 if (fl & SUB_MATCH) != 0 {
2163 // c:2590 matched portion
2164 ll += 1 + (e as i64 - b as i64); // c:2591 ll += 1 + (e - b)
2165 }
2166 if (fl & SUB_REST) != 0 {
2167 // c:2592 unmatched portion
2168 ll += 1 + (mlen as i64 - (e as i64 - b as i64)); // c:2593 ll += 1 + (mlen - (e - b))
2169 }
2170 if (fl & SUB_BIND) != 0 {
2171 // c:2594 position of start of matched portion
2172 buf = format!("{} ", MB_METASTRLEN2END(mstr_owned.as_str(), false, b) + 1); // c:2596
2173 bl = buf.len(); // c:2597 bl = strlen(buf)
2174 ll += bl as i64; // c:2597 ll += bl
2175 }
2176 if (fl & SUB_EIND) != 0 {
2177 // c:2599 position of end of matched portion
2178 buf.push_str(&format!(
2179 "{} ",
2180 MB_METASTRLEN2END(mstr_owned.as_str(), false, e) + 1
2181 )); // c:2601
2182 bl = buf.len(); // c:2602 bl = strlen(buf)
2183 ll += bl as i64; // c:2602 ll += bl
2184 }
2185 if (fl & SUB_LEN) != 0 {
2186 // c:2603 length of matched portion — MB_METASTRLEN2END(mstr+b, 0, mstr+e)
2187 let sub = if b <= mstr.len() {
2188 &mstr_owned[b..]
2189 } else {
2190 ""
2191 };
2192 buf.push_str(&format!(
2193 "{} ",
2194 MB_METASTRLEN2END(sub, false, e.saturating_sub(b))
2195 )); // c:2605
2196 bl = buf.len(); // c:2606 bl = strlen(buf)
2197 ll += bl as i64; // c:2606 ll += bl
2198 }
2199 if bl != 0 {
2200 buf.pop(); // c:2609 buf[bl - 1] = '\0' — drop the trailing space
2201 }
2202
2203 if ll == 0 {
2204 return None; // c:2614 return NULL
2205 }
2206
2207 // c:2617 — rr = r = hcalloc(ll); build the result buffer.
2208 let mut r = String::new();
2209
2210 if (fl & SUB_MATCH) != 0 {
2211 // c:2619-2623 — copy matched portion to new buffer.
2212 let end = e.min(mstr.len());
2213 let start = b.min(end);
2214 r.push_str(&mstr_owned[start..end]); // c:2621
2215 t = true; // c:2622
2216 }
2217 if (fl & SUB_REST) != 0 {
2218 // c:2624 — copy unmatched portion. If both portions requested,
2219 // put a space in between (why?).
2220 if t {
2221 r.push(' '); // c:2627
2222 }
2223 // c:2629-2630 — unmatched bits before the match.
2224 let pre = b.min(mstr.len());
2225 r.push_str(&mstr_owned[..pre]); // c:2630
2226 if let Some(ref rs) = replstr {
2227 r.push_str(rs); // c:2632-2633 copy replstr
2228 }
2229 // c:2634-2635 — unmatched bits after the match.
2230 let post = e.min(mstr.len());
2231 r.push_str(&mstr_owned[post..]); // c:2635
2232 t = true; // c:2636
2233 }
2234 if bl != 0 {
2235 // c:2639-2643 — append the numeric buffer; space first if needed.
2236 if t {
2237 r.push(' '); // c:2642
2238 }
2239 r.push_str(&buf); // c:2643 strcpy(rr, buf)
2240 }
2241 Some(r) // c:2645 return r
2242}
2243
2244/// Compile pattern and get match info (from glob.c compgetmatch line 2650)
2245/// Port of `compgetmatch(char *pat, int *flp, char **replstrp)` from `Src/glob.c:2650`.
2246/// WARNING: param names don't match C — Rust=(pat) vs C=(pat, flp, replstrp)
2247pub fn compgetmatch(pat: &str) -> Option<(String, i32)> {
2248 // c:1993 — `SUB_START` (anchor at head) / `SUB_END` (anchor at
2249 // tail) / `SUB_LONG` (`##`/`%%` doubled = longest). All three
2250 // imported from the canonical zsh_h.rs port; the previous local
2251 // redeclaration risked the same drift hazard as the HIST_*
2252 // bit-value bug caught earlier.
2253 let mut flags: i32 = 0;
2254 let mut pattern = pat.to_string();
2255
2256 if pattern.starts_with('#') {
2257 flags |= SUB_START;
2258 pattern = pattern[1..].to_string();
2259 }
2260 if pattern.starts_with("##") {
2261 flags |= SUB_START | SUB_LONG;
2262 pattern = pattern[2..].to_string();
2263 }
2264 if pattern.ends_with('%') {
2265 flags |= SUB_END;
2266 pattern.pop();
2267 }
2268 if pattern.ends_with("%%") {
2269 flags |= SUB_END | SUB_LONG;
2270 pattern.truncate(pattern.len().saturating_sub(2));
2271 }
2272
2273 Some((pattern, flags))
2274}
2275
2276/// Port of `getmatch(char **sp, char *pat, int fl, int n, char *replstr)`
2277/// from `Src/glob.c:2710`. C body (4 lines):
2278/// `Patprog p;
2279/// if (!(p = compgetmatch(pat, &fl, &replstr))) return 1;
2280/// return igetmatch(sp, p, fl, n, replstr, NULL);`
2281/// Rust returns the resulting string (callers don't take a `**sp`
2282/// out-pointer); compgetmatch/igetmatch hold the real prepare +
2283/// match-and-replace logic.
2284pub fn getmatch(sp: &str, pat: &str, fl: i32, n: i32, replstr: Option<&str>) -> String {
2285 let (prep_pat, prep_fl) = match compgetmatch(pat) {
2286 // c:2713
2287 Some(t) => t,
2288 None => return sp.to_string(), // c:2713 return 1
2289 };
2290 let mut buf = sp.to_string();
2291 igetmatch(&mut buf, &prep_pat, prep_fl | fl, n, replstr); // c:2715
2292 buf
2293}
2294
2295/// Get match for array elements (from glob.c getmatcharr lines 2690-2750)
2296/// Port of `getmatcharr(char ***ap, char *pat, int fl, int n, char *replstr)` from `Src/glob.c:2727`.
2297pub fn getmatcharr(
2298 ap: &[String],
2299 pat: &str,
2300 fl: i32,
2301 n: i32,
2302 replstr: Option<&str>,
2303) -> Vec<String> {
2304 ap.iter()
2305 .map(|s| getmatch(s, pat, fl, n, replstr))
2306 .collect()
2307}
2308
2309/// Port of `int getmatchlist(char *str, Patprog p, LinkList *repllistp)`
2310/// from `Src/glob.c:2749`.
2311/// ```c
2312/// int
2313/// getmatchlist(char *str, Patprog p, LinkList *repllistp)
2314/// {
2315/// char **sp = &str;
2316/// return igetmatch(sp, p, SUB_LONG|SUB_GLOBAL|SUB_SUBSTR|SUB_LIST,
2317/// 0, NULL, repllistp);
2318/// }
2319/// ```
2320/// 3-line delegation to `igetmatch` with the canonical flag set. The
2321/// `repllistp` out-param is the LinkList that receives the match
2322/// position pairs; the Rust port currently lacks a repllistp out-
2323/// channel on `igetmatch`, so this entry mirrors the C structure
2324/// and returns the igetmatch status.
2325pub fn getmatchlist(sp: &mut String, p: &str) -> i32 {
2326 // c:2749
2327 igetmatch(
2328 sp,
2329 p,
2330 SUB_LONG | SUB_GLOBAL | SUB_SUBSTR | SUB_LIST, // c:2761
2331 0,
2332 None,
2333 ) // c:2762
2334}
2335
2336/// File-static `static int in_expandredir = 0;` from `Src/glob.c:1206`
2337/// — set during the `globlist` call inside `xpandredir` so the glob
2338/// pipeline knows the result feeds a redirection (suppresses the
2339/// "no matches" warning in the caller's normal flow).
2340pub static IN_EXPANDREDIR: std::sync::atomic::AtomicI32 = // c:1206
2341 std::sync::atomic::AtomicI32::new(0);
2342
2343/// Direct port of `int xpandredir(struct redir *fn, LinkList redirtab)`
2344/// from `Src/glob.c:2150`. Expands `>>*.c`-style redirections by
2345/// running `prefork` + (when MULTIOS is set) `globlist` over the redir
2346/// name. Single match: rewrite `fn->name` in place and decode the
2347/// MERGEIN/MERGEOUT special syntax (`-` close, `p` pipe-fd, digits =
2348/// fd number). Multi match: clone the redir for each name and append
2349/// to `redirtab`. Returns 1 if multios produced multiple entries, 0
2350/// otherwise.
2351/// WARNING: param names don't match C — Rust=(fn_, redirtab) vs C=(fn, redirtab)
2352pub fn xpandredir(
2353 fn_: &mut redir, // c:2150
2354 redirtab: &mut Vec<redir>,
2355) -> i32 {
2356 use std::sync::atomic::Ordering::SeqCst;
2357 let mut ret = 0; // c:2156
2358 let name = match fn_.name.as_deref() {
2359 // c:2160 init_list1(fake, fn->name)
2360 Some(n) => n.to_string(),
2361 None => return 0,
2362 };
2363 let mut fake: LinkList = LinkList::new();
2364 fake.push_back(name); // c:2160
2365 let mut rf = 0i32;
2366 prefork(
2367 &mut fake, // c:2162 prefork
2368 if isset(MULTIOS) { 0 } else { PREFORK_SINGLE },
2369 &mut rf,
2370 );
2371 // c:2164 — `if (!errflag && isset(MULTIOS))`. C uses LOGICAL NOT
2372 // on `errflag` (an `int`), so the condition is "errflag is zero".
2373 // The previous Rust port wrote `!errflag.load(...) != 0` which is
2374 // BITWISE NOT in Rust (`!i32` is `^-1`), making the condition
2375 // equivalent to `errflag != -1` — true for every common errflag
2376 // value (0 / ERRFLAG_ERROR=1 / ERRFLAG_INT=2). Result: the
2377 // globlist call ran even when errflag was set, papering over
2378 // the abort path that C uses to bail early on prior errors.
2379 if errflag.load(SeqCst) == 0 && isset(MULTIOS) {
2380 // c:2164
2381 IN_EXPANDREDIR.store(1, SeqCst); // c:2165
2382 crate::ported::subst::globlist(&mut fake, 0); // c:2166
2383 IN_EXPANDREDIR.store(0, SeqCst); // c:2167
2384 }
2385 if errflag.load(SeqCst) != 0 {
2386 return 0;
2387 } // c:2169
2388 let names: Vec<String> = fake.iter().cloned().collect();
2389 if names.len() == 1 {
2390 // c:2171 nonempty(&fake) && !nextnode(firstnode)
2391 let s = crate::lex::untokenize(&names[0]); // c:2174 untokenize(s)
2392 fn_.name = Some(s.clone()); // c:2173 fn->name = s
2393 if fn_.typ == REDIR_MERGEIN || fn_.typ == REDIR_MERGEOUT {
2394 // c:2175
2395 let bytes = s.as_bytes();
2396 if bytes.len() == 1 && IS_DASH(bytes[0] as char) {
2397 // c:2176-2177 IS_DASH(s[0]) && !s[1]
2398 fn_.typ = REDIR_CLOSE;
2399 } else if bytes.len() == 1 && bytes[0] == b'p' {
2400 // c:2178 s[0]=='p' && !s[1]
2401 fn_.fd2 = -2;
2402 } else {
2403 let mut i = 0;
2404 while i < bytes.len() && bytes[i].is_ascii_digit() {
2405 i += 1;
2406 } // c:2181 idigit
2407 if i == bytes.len() && i > 0 {
2408 // c:2183 !*s && s > fn->name
2409 fn_.fd2 = crate::ported::utils::zstrtol(&s, 10).0 as i32; // c:2184 zstrtol(.., 10)
2410 } else if fn_.typ == REDIR_MERGEIN {
2411 // c:2185
2412 zerr("file number expected"); // c:2186
2413 } else {
2414 fn_.typ = REDIR_ERRWRITE; // c:2188
2415 }
2416 }
2417 }
2418 } else if fn_.typ == REDIR_MERGEIN {
2419 // c:2192
2420 zerr("file number expected"); // c:2193
2421 } else {
2422 if fn_.typ == REDIR_MERGEOUT {
2423 fn_.typ = REDIR_ERRWRITE;
2424 } // c:2195
2425 for nam in names {
2426 // c:2196 while ((nam = ugetnode(&fake)))
2427 let mut ff = fn_.clone(); // c:2199-2200 zhalloc + *ff = *fn
2428 ff.name = Some(nam); // c:2201 ff->name = nam
2429 redirtab.push(ff); // c:2202 addlinknode(redirtab, ff)
2430 ret = 1; // c:2203
2431 }
2432 }
2433 ret // c:2206
2434}
2435
2436/// Port of `freerepldata(void *ptr)` from Src/glob.c:2766.
2437/// C: `static void freerepldata(void *ptr)` →
2438/// `zfree(ptr, sizeof(struct repldata));`
2439#[allow(unused_variables)]
2440pub fn freerepldata(ptr: *mut std::ffi::c_void) { // c:2766
2441 // Rust drop covers the equivalent.
2442}
2443
2444/// Port of `freematchlist(LinkList repllist)` from Src/glob.c:2773.
2445/// C: `void freematchlist(LinkList repllist)` →
2446/// `freelinklist(repllist, freerepldata);`
2447///
2448/// The C `repllist` is a `LinkList` of `struct repldata` (the same
2449/// node `get_match_ret` records for SUB_GLOBAL/SUB_LIST). The Rust
2450/// port operates on `Vec<repldata>` — matching the canonical
2451/// `imatchdata.repllist` type — not the prior ad-hoc `Vec<(usize,usize)>`.
2452/// Clearing the Vec drops each `repldata` (Rust's `freerepldata` +
2453/// `freelinklist` equivalent).
2454pub fn freematchlist(repllist: Option<&mut Vec<repldata>>) {
2455 // c:2773
2456 if let Some(l) = repllist {
2457 l.clear(); // c:2776 freelinklist(repllist, freerepldata)
2458 }
2459}
2460
2461/// Port of `set_pat_start(Patprog p, int offs)` from `Src/glob.c:2780`.
2462///
2463/// When we advance up the test string from its start, tell the pattern
2464/// matcher that a start-of-string assertion `(#s)` should fail: set
2465/// `PAT_NOTSTART` when `offs` is nonzero (the real start is past the
2466/// actual start), clear it when `offs == 0`. Mutates `p->flags`.
2467/// Replaces the prior fake that sliced the pattern string and returned
2468/// a substring — unrelated to the C behaviour (the matcher reads
2469/// `PAT_NOTSTART` off `prog.flags`, see pattern.rs:4792).
2470pub fn set_pat_start(p: &mut Patprog, offs: i32) {
2471 // c:2780
2472 if offs != 0 {
2473 p.0.flags |= PAT_NOTSTART; // c:2790 p->flags |= PAT_NOTSTART
2474 } else {
2475 p.0.flags &= !PAT_NOTSTART; // c:2792 p->flags &= ~PAT_NOTSTART
2476 }
2477}
2478
2479/// Port of `set_pat_end(Patprog p, char null_me)` from `Src/glob.c:2796`.
2480///
2481/// When we shorten the string at the tail, tell the pattern matcher
2482/// that an end-of-string assertion `(#e)` should fail: set `PAT_NOTEND`
2483/// when the char `null_me` about to be zapped is non-NUL, clear it when
2484/// it is already NUL. Mutates `p->flags`. Replaces the prior fake that
2485/// sliced the pattern string and returned a prefix (the matcher reads
2486/// `PAT_NOTEND` off `prog.flags`, see pattern.rs:2803).
2487pub fn set_pat_end(p: &mut Patprog, null_me: u8) {
2488 // c:2796
2489 if null_me != 0 {
2490 p.0.flags |= PAT_NOTEND; // c:2806 p->flags |= PAT_NOTEND
2491 } else {
2492 p.0.flags &= !PAT_NOTEND; // c:2808 p->flags &= ~PAT_NOTEND
2493 }
2494}
2495
2496/// Port of `igetmatch(char **sp, Patprog p, int fl, int n, char *replstr, LinkList *repllistp)` from Src/glob.c:2832.
2497/// C: `static int igetmatch(char **sp, Patprog p, int fl, int n,
2498/// char *replstr, LinkList *repllistp)` — pattern-replace inner
2499/// matcher; modifies `*sp` in place, optionally collects match
2500/// positions into `*repllistp`.
2501/// WARNING: param names don't match C — Rust=(sp, p, fl, n, replstr) vs C=(sp, p, fl, n, replstr, repllistp)
2502pub fn igetmatch(
2503 sp: &mut String,
2504 p: &str,
2505 fl: i32,
2506 _n: i32, // c:2832
2507 replstr: Option<&str>,
2508) -> i32 {
2509 // c:2840-3100+ — full SUB_* dispatch: longest/shortest/global/end-
2510 // anchor replacement loop with multibyte tracking. Rust port walks
2511 // chars + `matchpat`; full Patprog substrate (with chunked DFA
2512 // execution) lives in src/ported/pattern.rs. SUB_START imported
2513 // from zsh_h.rs at top-of-file rather than redeclared locally.
2514 let anchored_start = (fl & SUB_START) != 0;
2515 let anchored_end = (fl & SUB_END) != 0;
2516 let substr_mode = (fl & SUB_SUBSTR) != 0;
2517 let shortest = (fl & SUB_LONG) == 0;
2518 let chars: Vec<char> = sp.chars().collect();
2519 let len = chars.len();
2520
2521 // c:2887-2898 — SUB_ALL: entire-string match flag.
2522 if (fl & SUB_ALL) != 0 {
2523 // c:2887
2524 let i = matchpat(p, sp, true, true); // c:2888 pattrylen
2525 if !i {
2526 // c:2889
2527 // c:2890-2893 — no match: clear replstr.
2528 if (fl & SUB_MATCH) != 0 {
2529 // c:2895
2530 *sp = String::new();
2531 return 0; // c:2896
2532 }
2533 return 1; // c:2897
2534 }
2535 if let Some(r) = replstr {
2536 // c:2894 get_match_ret
2537 *sp = r.to_string();
2538 }
2539 if sp.is_empty() && (fl & SUB_REST) != 0 && i {
2540 // c:2895
2541 return 0; // c:2896
2542 }
2543 return 1; // c:2897
2544 }
2545
2546 if len == 0 {
2547 return 1;
2548 }
2549 // c:2998-3041 — SUB_LIST: collect all match offset pairs.
2550 if (fl & SUB_LIST) != 0 {
2551 // c:2998
2552 // Walk all substrings; collect (start, end) pairs the caller
2553 // can read via subsequent `getmatchlist` invocations. Without
2554 // a `repllistp` out-channel through the Rust signature, we
2555 // only walk to validate at-least-one-match; the canonical
2556 // collection point is the caller-supplied LinkList that the
2557 // C body populates at c:3000+. Defer until the out-vec lands.
2558 let mut found = false;
2559 for start in 0..len {
2560 // c:3008
2561 for end in (start + 1)..=len {
2562 // c:3009
2563 let s2: String = chars[start..end].iter().collect();
2564 if matchpat(p, &s2, true, true) {
2565 // c:3010
2566 found = true;
2567 if shortest {
2568 break;
2569 } // c:3011 SUB_LONG vs short
2570 }
2571 }
2572 if !substr_mode {
2573 break;
2574 }
2575 }
2576 return if found { 0 } else { 1 };
2577 }
2578
2579 // c:Src/glob.c:2900+ — SUB_MATCH inverts the disposition of the
2580 // anchored-strip operators. Default (no SUB_MATCH): the matched
2581 // portion is REMOVED and the rest is returned (`${var#pat}` →
2582 // tail after the matched prefix). With SUB_MATCH: the matched
2583 // portion is KEPT and the rest is removed (`${(M)var#pat}` → the
2584 // matched prefix only). Previously honoured only in the SUB_ALL
2585 // arm at c:2895; the anchored-prefix / anchored-suffix / substr
2586 // arms below always returned prefix+suffix, so `(M)` was a no-op
2587 // for `#pat` / `%pat`. Mirror the C SUB_MATCH branch by emitting
2588 // just the matched slice.
2589 let match_only = (fl & SUB_MATCH) != 0;
2590 let (match_start, match_end) = if anchored_start && anchored_end {
2591 if matchpat(p, sp, true, true) {
2592 (0, len)
2593 } else {
2594 if match_only {
2595 *sp = String::new();
2596 return 0;
2597 }
2598 return 1;
2599 }
2600 } else if anchored_start {
2601 let mut best_end = 0;
2602 for end in 1..=len {
2603 let substr: String = chars[..end].iter().collect();
2604 if matchpat(p, &substr, true, true) {
2605 if shortest {
2606 *sp = if match_only {
2607 chars[..end].iter().collect()
2608 } else {
2609 match replstr {
2610 Some(r) => format!("{}{}", r, chars[end..].iter().collect::<String>()),
2611 None => chars[end..].iter().collect(),
2612 }
2613 };
2614 return 0;
2615 }
2616 best_end = end;
2617 }
2618 }
2619 if best_end > 0 {
2620 (0, best_end)
2621 } else {
2622 if match_only {
2623 *sp = String::new();
2624 return 0;
2625 }
2626 return 1;
2627 }
2628 } else if anchored_end {
2629 let mut best_start = len;
2630 for start in (0..len).rev() {
2631 let substr: String = chars[start..].iter().collect();
2632 if matchpat(p, &substr, true, true) {
2633 if shortest {
2634 *sp = if match_only {
2635 chars[start..].iter().collect()
2636 } else {
2637 match replstr {
2638 Some(r) => {
2639 format!("{}{}", chars[..start].iter().collect::<String>(), r)
2640 }
2641 None => chars[..start].iter().collect(),
2642 }
2643 };
2644 return 0;
2645 }
2646 best_start = start;
2647 }
2648 }
2649 if best_start < len {
2650 (best_start, len)
2651 } else {
2652 if match_only {
2653 *sp = String::new();
2654 return 0;
2655 }
2656 return 1;
2657 }
2658 } else {
2659 for start in 0..len {
2660 for end in (start + 1)..=len {
2661 let substr: String = chars[start..end].iter().collect();
2662 if matchpat(p, &substr, true, true) {
2663 if match_only {
2664 *sp = chars[start..end].iter().collect();
2665 return 0;
2666 }
2667 let prefix: String = chars[..start].iter().collect();
2668 let suffix: String = chars[end..].iter().collect();
2669 *sp = match replstr {
2670 Some(r) => format!("{}{}{}", prefix, r, suffix),
2671 None => format!("{}{}", prefix, suffix),
2672 };
2673 return 0;
2674 }
2675 }
2676 }
2677 if match_only {
2678 *sp = String::new();
2679 return 0;
2680 }
2681 return 1;
2682 };
2683 *sp = if match_only {
2684 chars[match_start..match_end].iter().collect()
2685 } else {
2686 let prefix: String = chars[..match_start].iter().collect();
2687 let suffix: String = chars[match_end..].iter().collect();
2688 match replstr {
2689 Some(r) => format!("{}{}{}", prefix, r, suffix),
2690 None => format!("{}{}", prefix, suffix),
2691 }
2692 };
2693 0
2694}
2695
2696// ============================================================================
2697// Tokenization (from glob.c tokenize family)
2698// ============================================================================
2699
2700// `enum GlobToken` deleted — C uses the byte-token constants
2701// (`Star`/`Quest`/`Inpar`/...) from `Src/zsh.h:159-200`, mirrored in
2702// the Rust port at `zsh_h.rs:128-160`. `tokenize()` (`Src/glob.c:3548`
2703// → `zshtokenize`) mutates the input string in place, replacing each
2704// glob-metacharacter with its high-bit byte token; the Rust port now
2705// matches.
2706
2707// `ztokens[]` from `Src/lex.c:38` — the source-char ↔ token-byte
2708// table the C tokenizer indexes with `(t - ztokens) + Pound`. Each
2709// position N in the string maps to high-bit byte `Pound + N`.
2710pub const ZTOKENS: &str = "#$^*(())$=|{}[]`<>>?~`,-!'\"\\\\";
2711
2712/// Tokenize a glob pattern in place — port of `tokenize(char *s)` from
2713/// `Src/glob.c:3548`. One-line C delegation: `zshtokenize(s, 0)`.
2714pub fn tokenize(s: &mut String) {
2715 // c:3548
2716 zshtokenize(s, 0); // c:3552
2717}
2718
2719/// Tokenize for shell — port of `shtokenize(char *s)` from
2720/// `Src/glob.c:3563`. Builds flags from SHGLOB option then delegates
2721/// to `zshtokenize`.
2722pub fn shtokenize(s: &mut String) {
2723 // c:3563
2724 let mut flags = ZSHTOK_SUBST; // c:3567
2725 if isset(SHGLOB) {
2726 // c:3568
2727 flags |= ZSHTOK_SHGLOB; // c:3569
2728 }
2729 zshtokenize(s, flags); // c:3570
2730}
2731
2732/// Port of `zshtokenize(char *s, int flags)` from `Src/glob.c:3575`.
2733/// Walks `s` in place, replacing each glob-metachar with its high-bit
2734/// byte token from the `ZTOKENS` table; respects ZSHTOK_SUBST (use
2735/// Bnullkeep for escaped tokens) and ZSHTOK_SHGLOB (don't tokenize
2736/// `<` / `(` / `|` / `)`).
2737pub fn zshtokenize(s: &mut String, flags: i32) {
2738 // c:3575
2739 let ztokens: Vec<char> = ZTOKENS.chars().collect();
2740 let mut chars: Vec<char> = s.chars().collect();
2741 let mut bslash = false; // c:3578
2742 let mut i = 0;
2743 while i < chars.len() {
2744 // c:3580 for (; *s; s++)
2745 let c = chars[i];
2746 match c {
2747 x if x == Meta as char => { // c:3583 case Meta
2748 i += 2; // c:3585 skip both Meta and next
2749 bslash = false;
2750 continue;
2751 }
2752 x if x == Bnull || x == Bnullkeep || x == '\\' => { // c:3587-3589
2753 if bslash { // c:3590
2754 chars[i - 1] = if (flags & ZSHTOK_SUBST) != 0 { // c:3591
2755 Bnullkeep
2756 } else {
2757 Bnull
2758 };
2759 } else {
2760 bslash = true; // c:3595
2761 i += 1;
2762 continue; // c:3596 (skip bslash=0 reset)
2763 }
2764 }
2765 '<' => { // c:3598
2766 if (flags & ZSHTOK_SHGLOB) != 0 { // c:3599
2767 // break — no tokenization
2768 } else if bslash { // c:3601
2769 chars[i - 1] = if (flags & ZSHTOK_SUBST) != 0 {
2770 Bnullkeep
2771 } else {
2772 Bnull
2773 };
2774 } else {
2775 // c:3605-3614 — try to parse `<N-N>` redirection.
2776 let t = i; // c:3605
2777 let mut j = i + 1;
2778 while j < chars.len() && chars[j].is_ascii_digit() { // c:3606 idigit
2779 j += 1;
2780 }
2781 if j < chars.len() && (chars[j] == '-') { // c:3607 IS_DASH
2782 let mut k = j + 1;
2783 while k < chars.len() && chars[k].is_ascii_digit() { // c:3609
2784 k += 1;
2785 }
2786 if k < chars.len() && chars[k] == '>' { // c:3611
2787 chars[t] = Inang; // c:3613
2788 chars[k] = Outang; // c:3614
2789 i = k + 1;
2790 bslash = false;
2791 continue;
2792 }
2793 }
2794 // c:3608/3611 `goto cont` — re-switch on current *s;
2795 // since none of the conditions matched, fall through.
2796 }
2797 }
2798 '(' | '|' | ')' if (flags & ZSHTOK_SHGLOB) != 0 => { // c:3617-3620
2799 // no tokenization under SHGLOB
2800 }
2801 '>' | '^' | '#' | '~' | '[' | ']' | '*' | '?' // c:3621-3631
2802 | '=' | '-' | '!' | '(' | '|' | ')' => {
2803 for (n, &t) in ztokens.iter().enumerate() { // c:3633
2804 if t == c { // c:3634
2805 if bslash { // c:3635
2806 chars[i - 1] = if (flags & ZSHTOK_SUBST) != 0 {
2807 Bnullkeep
2808 } else {
2809 Bnull
2810 };
2811 } else {
2812 chars[i] = char::from_u32(Pound as u32 + n as u32)
2813 .unwrap_or(c); // c:3638
2814 }
2815 break; // c:3639
2816 }
2817 }
2818 }
2819 _ => {}
2820 }
2821 bslash = false; // c:3646
2822 i += 1;
2823 }
2824 *s = chars.into_iter().collect();
2825}
2826
2827/// Port of `void remnulargs(char *s)` from `Src/glob.c:3649`.
2828///
2829/// C body (c:3651-3676): walks `s` looking for INULL bytes.
2830/// - `Bnullkeep` in SCAN phase: skip (the `continue` at c:3658)
2831/// — don't treat as a null marker.
2832/// - Any other INULL: switch to COPY phase. In copy phase:
2833/// - `Bnullkeep` becomes literal `\` (the "active backslash"
2834/// is re-materialized).
2835/// - Other INULLs: stripped.
2836/// - Non-INULL: kept.
2837/// - If the post-copy string is empty, replace with `Nularg`
2838/// (single-char empty-arg marker).
2839///
2840/// The previous Rust port collapsed the body to
2841/// `s.retain(|c| c != '\0' && c != Bnullkeep)` — a simple
2842/// strip of NUL and Bnullkeep. Three divergences:
2843/// - Stripped Bnullkeep entirely instead of preserving it in
2844/// the scan-only phase (when it appears BEFORE any other
2845/// inull) or converting to `\` in the copy phase.
2846/// - Didn't strip Snull/Dnull/Bnull/Nularg — those inulls
2847/// stayed in the output, polluting downstream processing.
2848/// - Didn't emit Nularg sentinel for empty post-strip strings.
2849pub fn remnulargs(s: &mut String) {
2850 // c:3649
2851 if s.is_empty() {
2852 // c:3654
2853 return;
2854 }
2855 // c:3656 `inull(c)` predicate: Snull / Dnull / Bnull / Bnullkeep / Nularg.
2856 let is_inull =
2857 |c: char| c == Snull || c == Dnull || c == Bnull || c == Bnullkeep || c == Nularg;
2858 let src: Vec<char> = s.chars().collect();
2859 let mut out: Vec<char> = Vec::with_capacity(src.len());
2860 let mut i = 0usize;
2861 // rs-only adaptation of C's byte walk: C strings are byte arrays and
2862 // store an eight-bit char (e.g. `$'\M-\C-a'` = 0x81) RAW, so its byte
2863 // never aliases an inull sentinel. A Rust `String` is UTF-8, so zshrs
2864 // METAFIES that byte — Meta (0x83) + (byte ^ 0x20) — and 0x81 ^ 0x20 =
2865 // 0xA1, which IS the Nularg sentinel (0xBD/0xBE/0xBF/0xBC likewise
2866 // alias Snull/Dnull/Bnull/Bnullkeep). A metafied byte is DATA, not a
2867 // sentinel, so treat `Meta` + next char as an opaque pair on BOTH the
2868 // scan and copy walks — otherwise `${(qq)v}` for v=$'\M-\C-a' dropped
2869 // the 0xA1 and left the lone Meta (rs emitted `'\xc2\x83'` where zsh
2870 // emits `'\x81'`). C's byte walk needs no such guard.
2871 // Meta is a byte constant (0x83); a metafied string stores it as the
2872 // char U+0083, so compare against the char form.
2873 let meta = char::from(crate::ported::zsh_h::Meta);
2874 // c:3656 — SCAN phase: copy chars, skip standalone Bnullkeep.
2875 while i < src.len() {
2876 let c = src[i];
2877 if c == meta && i + 1 < src.len() {
2878 out.push(c);
2879 out.push(src[i + 1]);
2880 i += 2;
2881 continue;
2882 }
2883 if c == Bnullkeep {
2884 // c:3657 continue
2885 i += 1;
2886 continue;
2887 }
2888 if is_inull(c) {
2889 // c:3663 inull(c)
2890 // c:3664+ — COPY phase: walk the rest, fold Bnullkeep
2891 // to `\\`, strip other inulls.
2892 i += 1;
2893 while i < src.len() {
2894 let d = src[i];
2895 if d == meta && i + 1 < src.len() {
2896 // Metafied data pair — keep verbatim.
2897 out.push(d);
2898 out.push(src[i + 1]);
2899 i += 2;
2900 continue;
2901 }
2902 if d == Bnullkeep {
2903 // c:3666
2904 out.push('\\'); // c:3667
2905 } else if !is_inull(d) {
2906 // c:3668
2907 out.push(d); // c:3669
2908 }
2909 i += 1;
2910 }
2911 break;
2912 }
2913 // SCAN phase non-inull: keep verbatim.
2914 out.push(c);
2915 i += 1;
2916 }
2917 // c:3673-3675 — empty result → Nularg sentinel.
2918 if out.is_empty() {
2919 out.push(Nularg);
2920 }
2921 *s = out.into_iter().collect();
2922}
2923
2924/// Port of `qualdev(UNUSED(char *name), struct stat *buf, off_t dv, UNUSED(char *dummy))` from Src/glob.c:3688.
2925/// C: `static int qualdev(UNUSED(char *name), struct stat *buf, off_t dv,
2926/// UNUSED(char *dummy))` → `return (off_t)buf->st_dev == dv;`
2927#[allow(unused_variables)]
2928pub fn qualdev(name: &str, buf: &libc::stat, dv: i64, dummy: &str) -> i32 {
2929 // c:3688
2930 (buf.st_dev as i64 == dv) as i32 // c:3697
2931}
2932
2933/// Port of `qualnlink(UNUSED(char *name), struct stat *buf, off_t ct, UNUSED(char *dummy))` from Src/glob.c:3697.
2934/// C: ternary on `g_range`: < / > / == against `st_nlink`.
2935#[allow(unused_variables)]
2936pub fn qualnlink(name: &str, buf: &libc::stat, ct: i64, dummy: &str) -> i32 {
2937 // c:3697
2938 // c:3699-3701 — `g_range` selects `<` / `>` / `==`. This MUST read the same
2939 // `g_range` static the qual-eval sets (glob.rs ~5020) and that qualsize /
2940 // qualtime read — not the stale Rust-only `G_RANGE` duplicate, which was
2941 // never stored, leaving every `l[+-]N` comparison stuck on `==`.
2942 let g = g_range.load(Ordering::Relaxed);
2943 let nl = buf.st_nlink as i64; // c:3708
2944 if g < 0 {
2945 (nl < ct) as i32
2946 } else if g > 0 {
2947 (nl > ct) as i32
2948 } else {
2949 (nl == ct) as i32
2950 }
2951}
2952
2953/// Port of `qualuid(UNUSED(char *name), struct stat *buf, off_t uid, UNUSED(char *dummy))` from Src/glob.c:3708.
2954/// C: `return buf->st_uid == uid;`
2955#[allow(unused_variables)]
2956pub fn qualuid(name: &str, buf: &libc::stat, uid: i64, dummy: &str) -> i32 {
2957 // c:3708
2958 (buf.st_uid as i64 == uid) as i32 // c:3717
2959}
2960
2961/// Port of `qualgid(UNUSED(char *name), struct stat *buf, off_t gid, UNUSED(char *dummy))` from Src/glob.c:3717.
2962/// C: `return buf->st_gid == gid;`
2963#[allow(unused_variables)]
2964pub fn qualgid(name: &str, buf: &libc::stat, gid: i64, dummy: &str) -> i32 {
2965 // c:3717
2966 (buf.st_gid as i64 == gid) as i32 // c:3726
2967}
2968
2969/// Port of `qualisdev(UNUSED(char *name), struct stat *buf, UNUSED(off_t junk), UNUSED(char *dummy))` from Src/glob.c:3726.
2970/// C: `return S_ISBLK(buf->st_mode) || S_ISCHR(buf->st_mode);`
2971#[allow(unused_variables)]
2972pub fn qualisdev(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
2973 // c:3726
2974 let m = buf.st_mode as u32 & libc::S_IFMT as u32;
2975 ((m == libc::S_IFBLK as u32) || (m == libc::S_IFCHR as u32)) as i32 // c:3735
2976}
2977
2978/// Port of `qualisblk(UNUSED(char *name), struct stat *buf, UNUSED(off_t junk), UNUSED(char *dummy))` from Src/glob.c:3735.
2979/// C: `return S_ISBLK(buf->st_mode);`
2980#[allow(unused_variables)]
2981pub fn qualisblk(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
2982 // c:3735
2983 ((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFBLK as u32) as i32 // c:3744
2984}
2985
2986/// Port of `qualischr(UNUSED(char *name), struct stat *buf, UNUSED(off_t junk), UNUSED(char *dummy))` from Src/glob.c:3744.
2987/// C: `return S_ISCHR(buf->st_mode);`
2988#[allow(unused_variables)]
2989pub fn qualischr(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
2990 // c:3744
2991 ((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFCHR as u32) as i32 // c:3753
2992}
2993
2994/// Port of `qualisdir(UNUSED(char *name), struct stat *buf, UNUSED(off_t junk), UNUSED(char *dummy))` from Src/glob.c:3753.
2995/// C: `return S_ISDIR(buf->st_mode);`
2996#[allow(unused_variables)]
2997pub fn qualisdir(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
2998 // c:3753
2999 ((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFDIR as u32) as i32 // c:3762
3000}
3001
3002/// Port of `qualisfifo(UNUSED(char *name), struct stat *buf, UNUSED(off_t junk), UNUSED(char *dummy))` from Src/glob.c:3762.
3003/// C: `return S_ISFIFO(buf->st_mode);`
3004#[allow(unused_variables)]
3005pub fn qualisfifo(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
3006 // c:3762
3007 ((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFIFO as u32) as i32 // c:3771
3008}
3009
3010/// Port of `qualislnk(UNUSED(char *name), struct stat *buf, UNUSED(off_t junk), UNUSED(char *dummy))` from Src/glob.c:3771.
3011/// C: `return S_ISLNK(buf->st_mode);`
3012#[allow(unused_variables)]
3013pub fn qualislnk(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
3014 // c:3771
3015 ((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFLNK as u32) as i32 // c:3780
3016}
3017
3018/// Port of `qualisreg(UNUSED(char *name), struct stat *buf, UNUSED(off_t junk), UNUSED(char *dummy))` from Src/glob.c:3780.
3019/// C: `return S_ISREG(buf->st_mode);`
3020#[allow(unused_variables)]
3021pub fn qualisreg(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
3022 // c:3780
3023 ((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFREG as u32) as i32 // c:3789
3024}
3025
3026/// Port of `qualissock(UNUSED(char *name), struct stat *buf, UNUSED(off_t junk), UNUSED(char *dummy))` from Src/glob.c:3789.
3027/// C: `return S_ISSOCK(buf->st_mode);`
3028#[allow(unused_variables)]
3029pub fn qualissock(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
3030 // c:3789
3031 ((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFSOCK as u32) as i32
3032 // c:3798
3033}
3034
3035/// Port of `qualflags(UNUSED(char *name), struct stat *buf, off_t mod, UNUSED(char *dummy))` from Src/glob.c:3798.
3036/// C: `return mode_to_octal(buf->st_mode) & mod;`
3037#[allow(unused_variables)]
3038/// WARNING: param names don't match C — Rust=(name, buf, dummy) vs C=(name, buf, mod, dummy)
3039pub fn qualflags(name: &str, buf: &libc::stat, r#mod: i64, dummy: &str) -> i32 {
3040 // c:3798
3041 (mode_to_octal(buf.st_mode as u32) as i64 & r#mod) as i32 // c:3807
3042}
3043
3044/// Port of `qualmodeflags(UNUSED(char *name), struct stat *buf, off_t mod, UNUSED(char *dummy))` from Src/glob.c:3807.
3045/// C: `((v & y) == y && !(v & n))` where `y = mod & 07777`, `n = mod >> 12`.
3046#[allow(unused_variables)]
3047/// WARNING: param names don't match C — Rust=(name, buf, dummy) vs C=(name, buf, mod, dummy)
3048pub fn qualmodeflags(name: &str, buf: &libc::stat, r#mod: i64, dummy: &str) -> i32 {
3049 // c:3807
3050 let v = mode_to_octal(buf.st_mode as u32) as i64; // c:3818
3051 let y = r#mod & 0o7777;
3052 let n = r#mod >> 12;
3053 (((v & y) == y) && (v & n) == 0) as i32 // c:3818
3054}
3055
3056/// Port of `qualiscom(UNUSED(char *name), struct stat *buf, UNUSED(off_t mod), UNUSED(char *dummy))` from Src/glob.c:3818.
3057/// C: `return S_ISREG(buf->st_mode) && (buf->st_mode & S_IXUGO);`
3058#[allow(unused_variables)]
3059pub fn qualiscom(name: &str, buf: &libc::stat, r#mod: i64, dummy: &str) -> i32 {
3060 // c:3818
3061 let is_reg = (buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFREG as u32;
3062 let s_ixugo: u32 = (libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH) as u32;
3063 (is_reg && (buf.st_mode as u32 & s_ixugo) != 0) as i32 // c:3820
3064}
3065
3066/// Port of `static int g_units;` from `Src/glob.c`. Time/size unit
3067/// selector for the qualtime / qualsize qualifier predicates. Values
3068/// come from the TT_* enum (TT_DAYS=0, TT_HOURS=1, TT_MINS=2,
3069/// TT_WEEKS=3, TT_MONTHS=4 for time; TT_BYTES=0, TT_POSIX_BLOCKS=1,
3070/// TT_KILOBYTES=2, etc. for size).
3071pub static g_units: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
3072
3073/// Port of `static int g_range;` from `Src/glob.c`. Comparison
3074/// direction for qualtime / qualsize / qualnlink: -1 = `<`, 0 = `==`,
3075/// +1 = `>`. Set by the glob qualifier parser.
3076pub static g_range: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
3077
3078/// Port of `static int g_amc;` from `Src/glob.c`. Which mtime to read
3079/// for qualtime: 0 = atime, 1 = mtime, 2 = ctime. Set by the `am`,
3080/// `mm`, `cm` qualifier letters.
3081pub static g_amc: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
3082
3083/// Direct port of `static int qualsize(UNUSED(char *name),
3084/// struct stat *buf, off_t size, UNUSED(char *dummy))` from
3085/// `Src/glob.c:3825-3866`. Glob-qualifier predicate: returns true
3086/// when `buf->st_size` (scaled by `g_units`) compares to `size`
3087/// via `g_range`.
3088///
3089/// **Previous fakery:** the file held a same-named helper that was
3090/// actually a unit-conversion *parser* `(s, units) -> Option<(i64,
3091/// &str)>` — wrong shape, wrong semantics, zero callers. The
3092/// canonical C signature is restored here; the qualifier-eval site
3093/// at glob.rs:3914 already inlines the equivalent (uses
3094/// qualifier::Size struct fields) and is the live path.
3095#[allow(non_snake_case)]
3096pub fn qualsize(_name: &str, buf: &libc::stat, size: i64, _dummy: &str) -> i32 {
3097 use std::sync::atomic::Ordering;
3098 // c:3831 `zlong scaled = buf->st_size;`
3099 let mut scaled: i64 = buf.st_size as i64;
3100 // c:3837-3860 — switch (g_units) ceiling-divide by the unit.
3101 match g_units.load(Ordering::SeqCst) {
3102 x if x == TT_POSIX_BLOCKS => {
3103 scaled = (scaled + 511) / 512; // c:3838-3841
3104 }
3105 x if x == TT_KILOBYTES => {
3106 scaled = (scaled + 1023) / 1024; // c:3842-3845
3107 }
3108 x if x == TT_MEGABYTES => {
3109 scaled = (scaled + 1_048_575) / 1_048_576; // c:3846-3849
3110 }
3111 x if x == TT_GIGABYTES => {
3112 scaled = (scaled + 1_073_741_823) / 1_073_741_824; // c:3851-3854
3113 }
3114 x if x == TT_TERABYTES => {
3115 scaled = (scaled + 1_099_511_627_775) / 1_099_511_627_776; // c:3855-3858
3116 }
3117 _ => {} // c:3860 default: bytes — no scaling.
3118 }
3119 // c:3862-3864 — `g_range < 0 ? < : g_range > 0 ? > : ==`.
3120 let r = g_range.load(Ordering::SeqCst);
3121 i32::from(if r < 0 {
3122 scaled < size
3123 } else if r > 0 {
3124 scaled > size
3125 } else {
3126 scaled == size
3127 })
3128}
3129
3130/// Direct port of `static int qualtime(UNUSED(char *name),
3131/// struct stat *buf, off_t days, UNUSED(char *dummy))` from
3132/// `Src/glob.c:3870-3901`. Glob-qualifier predicate: returns true
3133/// when `now - buf->{a,m,c}time` (selected by `g_amc`, scaled by
3134/// `g_units`) compares to `days` via `g_range`.
3135///
3136/// **Previous fakery:** same misshapen parser as the qualsize one
3137/// above. Restored to C signature; the qualifier-eval site at
3138/// glob.rs:3940 already inlines the equivalent (uses qualifier::
3139/// Atime struct fields with the live atime/mtime/ctime read).
3140#[allow(non_snake_case)]
3141pub fn qualtime(_name: &str, buf: &libc::stat, days: i64, _dummy: &str) -> i32 {
3142 use std::sync::atomic::Ordering;
3143 // c:3876-3878 — `time(&now); diff = now - (g_amc == 0 ? st_atime
3144 // : g_amc == 1 ? st_mtime : st_ctime);`
3145 let now: i64 = std::time::SystemTime::now()
3146 .duration_since(std::time::UNIX_EPOCH)
3147 .map(|d| d.as_secs() as i64)
3148 .unwrap_or(0);
3149 let amc = g_amc.load(Ordering::SeqCst);
3150 let stamp: i64 = match amc {
3151 0 => buf.st_atime as i64, // c:3877
3152 1 => buf.st_mtime as i64, // c:3877
3153 _ => buf.st_ctime as i64, // c:3878
3154 };
3155 let mut diff: i64 = now - stamp;
3156 // c:3880-3896 — scale by g_units.
3157 match g_units.load(Ordering::SeqCst) {
3158 x if x == TT_DAYS => diff /= 86400, // c:3881-3883
3159 x if x == TT_HOURS => diff /= 3600, // c:3884-3886
3160 x if x == TT_MINS => diff /= 60, // c:3887-3889
3161 x if x == TT_WEEKS => diff /= 604800, // c:3890-3892
3162 x if x == TT_MONTHS => diff /= 2592000, // c:3893-3895
3163 _ => {} // c:3896 default: seconds.
3164 }
3165 // c:3898-3900 — same range comparison as qualsize.
3166 let r = g_range.load(Ordering::SeqCst);
3167 i32::from(if r < 0 {
3168 diff < days
3169 } else if r > 0 {
3170 diff > days
3171 } else {
3172 diff == days
3173 })
3174}
3175
3176/// Port of `static int qualsheval(char *name, UNUSED(struct stat *buf),
3177/// UNUSED(off_t days), char *str)` from `Src/glob.c:3907`.
3178///
3179/// C body (c:3907-3943):
3180/// ```c
3181/// if ((prog = parse_string(str, 0))) {
3182/// int ef = errflag, lv = lastval; // c:3912 save
3183/// unsetparam("reply"); // c:3915
3184/// setsparam("REPLY", ztrdup(name)); // c:3916
3185/// execode(prog, 1, 0, "globqual"); // c:3919
3186/// ret = lastval; // c:3921
3187/// errflag = ef | (errflag & ERRFLAG_INT); // c:3924 restore
3188/// lastval = lv; // c:3925
3189/// return !ret;
3190/// }
3191/// return 0;
3192/// ```
3193///
3194/// The `e:EXPR:` glob qualifier runs `EXPR` against each match
3195/// candidate with `$REPLY` pre-set to the filename. The expr is
3196/// evaluated IN THE CURRENT SHELL — it can read shell locals,
3197/// reference shell functions, set $reply etc.
3198///
3199/// The previous Rust port spawned `/bin/sh -c <expr>` via
3200/// `Command::new("sh")`, which:
3201/// 1. Loses access to current zsh function/alias/local-var scope.
3202/// 2. Runs `sh` (POSIX), not `zsh` — entire `(e:zsh-feature:)`
3203/// class of qualifiers silently failed.
3204/// 3. Skipped errflag/lastval save+restore.
3205///
3206/// Fixed: route through the canonical `ShellExecutor` for in-shell
3207/// evaluation. Set `REPLY` via paramtab (C `setsparam`), restore
3208/// `errflag`/`lastval` per c:3924-3925, mask any parse-time
3209/// ERRFLAG_ERROR while preserving ERRFLAG_INT (user interrupt).
3210/// WARNING: param names don't match C — Rust=(filename, expr) vs C=(name, buf, days, str)
3211pub fn qualsheval(filename: &str, _buf: &libc::stat, _data: i64, expr: &str) -> i32 {
3212 // c:3907
3213 // c:3912 — save errflag + lastval.
3214 let saved_errflag = errflag.load(Ordering::Relaxed); // c:3912
3215 let saved_lastval = LASTVAL.load(Ordering::Relaxed); // c:3912
3216 // c:3915 — `unsetparam("reply");`
3217 crate::ported::params::unsetparam("reply"); // c:3915
3218 // c:3916 — `setsparam("REPLY", ztrdup(name));`. Set in the
3219 // canonical paramtab so the evaluated expression sees `$REPLY`.
3220 crate::ported::params::setsparam("REPLY", filename); // c:3916
3221 // c:3919 — `execode(prog, 1, 0, "globqual");`. Route through the
3222 // executor via the `crate::ported::exec` accessor wrappers
3223 // (resolve the live executor on demand). Direct ShellExecutor
3224 // reach-in from src/ported/ is forbidden — see memory
3225 // feedback_no_exec_script_from_ported.
3226 let rc = crate::ported::exec::execute_script(expr).unwrap_or(1); // c:3919
3227 let ret = LASTVAL.load(Ordering::Relaxed); // c:3921 ret = lastval
3228 let _ = rc;
3229 // c:3924 — `errflag = ef | (errflag & ERRFLAG_INT);`. Restore
3230 // pre-call errflag plus any interrupt bit set during eval.
3231 let post_errflag = errflag.load(Ordering::Relaxed);
3232 errflag.store(
3233 saved_errflag | (post_errflag & ERRFLAG_INT),
3234 Ordering::Relaxed,
3235 ); // c:3924
3236 // c:3925 — `lastval = lv;`. Restore pre-call lastval.
3237 LASTVAL.store(saved_lastval, Ordering::Relaxed); // c:3925
3238 // c:3927-3937 — `inserts = getaparam("reply") || gethparam("reply")`,
3239 // else the `reply`/`REPLY` SCALAR as a one-element list. The eval can
3240 // thus REPLACE the matched name with zero-or-more names (`reply=(…)`)
3241 // or rename it (`REPLY=…`). REPLY was seeded to `filename` at c:3916,
3242 // so this is always Some — at minimum the (possibly modified) REPLY.
3243 let inserts: Vec<String> = match crate::ported::params::getaparam("reply") {
3244 Some(arr) => arr, // c:3927
3245 None => match crate::ported::params::getsparam("reply") // c:3931
3246 .or_else(|| crate::ported::params::getsparam("REPLY"))
3247 {
3248 Some(scalar) => vec![scalar], // c:3934-3936
3249 None => Vec::new(),
3250 },
3251 };
3252 *INSERTS.lock().unwrap() = Some(inserts);
3253 // c:3941 — `return !ret;` — qualifier passes iff lastval is 0.
3254 i32::from(ret == 0)
3255}
3256
3257/// Port of `qualnonemptydir(char *name, struct stat *buf, UNUSED(off_t days), UNUSED(char *str))` from Src/glob.c:3948.
3258/// C: opendir(name) and check if any non-`.`/`..` entries exist.
3259pub fn qualnonemptydir(name: &str, buf: &libc::stat, days: i64, str: &str) -> i32 {
3260 // c:3948
3261 // c:3948 — `if (!S_ISDIR(buf->st_mode)) return 0;`
3262 if (buf.st_mode as u32 & libc::S_IFMT as u32) != libc::S_IFDIR as u32 {
3263 // c:3950
3264 return 0;
3265 }
3266 // c:3953-3964 — opendir + readdir loop, skip "." and ".." entries.
3267 match fs::read_dir(name) {
3268 Ok(entries) => entries.filter_map(|e| e.ok()).any(|e| {
3269 let n = e.file_name();
3270 let s = n.to_string_lossy();
3271 s != "." && s != ".."
3272 }) as i32,
3273 Err(_) => 0,
3274 }
3275}
3276
3277// =====================================================================
3278// Thread-local glob option snapshot — race fix.
3279//
3280// `Src/glob.c` reads `isset(NULLGLOB)` / `isset(BAREGLOBQUAL)` / etc.
3281// inline at each callsite during a glob. In zsh those reads hit the
3282// thread-local C `opts[]` array (zsh is single-threaded). In zshrs
3283// they hit `OPTS_LIVE` — a process-wide `RwLock<HashMap>`.
3284//
3285// Embedders that snapshot/restore options around individual glob
3286// invocations on multiple threads (e.g. stryke's `StrykeGlobOptsGuard`
3287// + `glob_par` rayon parallelism) race: thread A sets bareglobqual=1,
3288// calls glob; while A is mid-parse, thread B's matching guard restores
3289// bareglobqual=0; A's qualifier-parse step sees bareglobqual=0 and
3290// drops the trailing `(N)` as a literal substring, producing false
3291// matches.
3292//
3293// Fix: snapshot every glob-relevant option ONCE at glob() entry into
3294// a thread-local cache, then read all isset() inside glob from the
3295// snapshot. The snapshot is dropped on glob exit via an RAII guard.
3296// Each thread's in-flight glob() sees a coherent set of options
3297// regardless of concurrent setopt/unsetopt on other threads.
3298// =====================================================================
3299
3300/// Snapshot of glob-relevant zsh options taken at glob entry. Holds
3301/// the live `isset(...)` value for each option that the glob engine
3302/// or its helpers (`parse_qualifiers`, `scanner`,
3303/// `sort_matches`, `glob_emit_path`) consult.
3304#[derive(Debug, Clone, Copy, Default)]
3305pub struct GlobOptSnapshot {
3306 pub bareglobqual: bool,
3307 pub braceccl: bool,
3308 pub caseglob: bool,
3309 pub extendedglob: bool,
3310 pub globdots: bool,
3311 pub globstarshort: bool,
3312 pub listtypes: bool,
3313 pub markdirs: bool,
3314 pub nullglob: bool,
3315 pub numericglobsort: bool,
3316}
3317
3318/// RAII guard that drops the thread-local snapshot on scope exit.
3319/// Constructed by `enter_glob_scope()`. Nested glob calls on the
3320/// same thread reuse the outer snapshot (the guard's `populated`
3321/// field tracks whether we installed or just observed).
3322pub struct GlobOptsGuard {
3323 populated: bool,
3324}
3325
3326/// Sort specifier flags
3327// `GlobSort` / `SortOrder` / `SortSpec` deleted — C uses bit-flag
3328// `int tp` in `struct globsort { int tp; char *exec; }` (Src/glob.c:155):
3329// tp & ~GS_DESC selects the sort key (GS_NAME/GS_DEPTH/GS_EXEC/…),
3330// tp & GS_DESC reverses direction (`O` vs `o` qualifier),
3331// tp << GS_SHIFT carries the follow-link variants.
3332// All those bits already exist as i32 constants at glob.rs:33+
3333// (GS_NAME / GS_DEPTH / GS_SIZE / … / GS_DESC / GS_NONE / GS__SIZE /
3334// …). The enum + Ascending/Descending + struct triple-wrapper was a
3335// Rust-only convenience with no C counterpart; callers now operate
3336// on the raw i32 the same way `gmatchcmp()` at glob.c:936 does.
3337
3338// `TimeUnit` / `SizeUnit` / `RangeOp` enums deleted — Rust-only
3339// wrappers around constants/chars that exist as raw values in C:
3340// `TimeUnit::Seconds` → TT_SECONDS i32 (glob.c:126 → glob.rs:99)
3341// `TimeUnit::Minutes` → TT_MINS (c:123)
3342// `TimeUnit::Hours` → TT_HOURS (c:122)
3343// `TimeUnit::Days` → TT_DAYS (c:121)
3344// `TimeUnit::Weeks` → TT_WEEKS (c:124)
3345// `TimeUnit::Months` → TT_MONTHS (c:125)
3346// `SizeUnit::Bytes` → TT_BYTES (c:128)
3347// `SizeUnit::PosixBlocks` → TT_POSIX_BLOCKS (c:129)
3348// `SizeUnit::Kilobytes` → TT_KILOBYTES (c:130)
3349// `SizeUnit::Megabytes` → TT_MEGABYTES (c:131)
3350// `SizeUnit::Gigabytes` → TT_GIGABYTES (c:132)
3351// `SizeUnit::Terabytes` → TT_TERABYTES (c:133)
3352// `RangeOp::Less`/`Equal`/`Greater` → raw chars `<` `=` `>`
3353// (zsh's qgetnum at glob.c:827 returns the raw operator char
3354// and the qualifier handlers switch on it inline).
3355
3356/// Port of `TestMatchFunc` — the `int (*)(char *, struct stat *, off_t,
3357/// char *)` function-pointer type held by `struct qual.func` (Src/glob.c).
3358/// Every glob-qualifier test (qualuid, qualisreg, qualiscom, qualsize, …)
3359/// has this signature so `insert()` can call them uniformly through the
3360/// `func` slot. `name` is the metafied path, `buf` the stat, `data` the
3361/// qualifier argument (uid, mode bits, day count, …), `sdata` the
3362/// expression text (only `qualsheval` uses it; others ignore it).
3363pub type TestMatchFunc = fn(name: &str, buf: &libc::stat, data: i64, sdata: &str) -> i32;
3364
3365/// Port of `struct qual` from `Src/glob.c:138`. A node in the linked list
3366/// of glob qualifiers parsed from a `(…)` suffix. `insert()` (glob.c:346)
3367/// walks this list per candidate file, calling `func` on each node and
3368/// accepting/rejecting the file by `sense`. The terminating node carries
3369/// `func == None` (C tests `qn && qn->func` to stop).
3370///
3371/// REPRESENTATION: C's `next`/`or` are `struct qual *` pointers into a
3372/// graph that `parsepat` builds and relinks through aliased cursors (the
3373/// OR-distribution at glob.c:1797-1820 aliases one node from several
3374/// chains at once). Rust single-ownership `Box` cannot express that, so
3375/// the port uses an arena ([`QualArena`]): nodes live in a `Vec<qual>`
3376/// and `next`/`or` are `Option<usize>` indices. C pointer assignments map
3377/// to index assignments, so the aliased-cursor algorithm translates
3378/// almost verbatim.
3379#[allow(non_camel_case_types)]
3380#[derive(Clone, Debug)]
3381pub struct qual {
3382 /// c:139 — `struct qual *next;` next qualifier in the AND-chain.
3383 pub next: Option<usize>,
3384 /// c:140 — `struct qual *or;` head of an alternative OR-chain.
3385 pub or: Option<usize>,
3386 /// c:141 — `TestMatchFunc func;` the test to call; `None` terminates.
3387 pub func: Option<TestMatchFunc>,
3388 /// c:142 — `off_t data;` argument passed to `func`.
3389 pub data: i64,
3390 /// c:143 — `int sense;` bit0: 0=assert / 1=negate; bit1: follow links.
3391 pub sense: i32,
3392 /// c:144 — `int amc;` which timestamp to test (0=a, 1=m, 2=c).
3393 pub amc: i32,
3394 /// c:145 — `int range;` test `<` / `>` / `=` (signum of comparison).
3395 pub range: i32,
3396 /// c:146 — `int units;` multiplier for time (TT_DAYS…) or size.
3397 pub units: i32,
3398 /// c:147 — `char *sdata;` expression to eval (currently qualsheval only).
3399 pub sdata: Option<String>,
3400}
3401
3402impl qual {
3403 /// A fresh node with the C zero-initialised defaults (`hcalloc`).
3404 pub fn new() -> qual {
3405 qual {
3406 next: None,
3407 or: None,
3408 func: None,
3409 data: 0,
3410 sense: 0,
3411 amc: 0,
3412 range: 0,
3413 units: 0,
3414 sdata: None,
3415 }
3416 }
3417}
3418
3419impl Default for qual {
3420 fn default() -> qual {
3421 qual::new()
3422 }
3423}
3424
3425/// Arena backing the [`qual`] graph. C allocates each `struct qual` with
3426/// `hcalloc` and links them by pointer; the Rust port allocates them in
3427/// this `Vec` and links them by index. `alloc()` mirrors a single
3428/// `hcalloc(sizeof *qn)` and returns the new node's index; `head` holds
3429/// the index of `quals` (the list `insert()` walks), `None` when empty.
3430#[derive(Clone, Default, Debug)]
3431pub struct QualArena {
3432 pub nodes: Vec<qual>,
3433 pub head: Option<usize>,
3434}
3435
3436impl QualArena {
3437 pub fn new() -> QualArena {
3438 QualArena {
3439 nodes: Vec::new(),
3440 head: None,
3441 }
3442 }
3443
3444 /// c:`qn = (struct qual *)hcalloc(sizeof *qn);` — allocate a fresh
3445 /// zeroed node and return its arena index.
3446 pub fn alloc(&mut self) -> usize {
3447 self.nodes.push(qual::new());
3448 self.nodes.len() - 1
3449 }
3450}
3451
3452/// A glob qualifier function
3453#[derive(Debug, Clone)]
3454/// One glob qualifier — Rust-extension sum type. C uses a linked
3455/// list of `struct qual` (`Src/zsh.h:140-152`) with function-pointer
3456/// `func` per node; each variant here maps to one of C's `q*` test
3457/// ported (`qisreg`, `qisdir`, `qowner`, `qtime`, ...) at
3458/// `Src/glob.c:1080-1340`. The full `struct qual` port + per-test fn
3459/// dispatch lives in a later phase; this enum keeps the parsed-form
3460/// the per-match filter inside `scanner()` (line 500) needs.
3461#[allow(non_camel_case_types)]
3462pub enum qualifier {
3463 /// File type qualifiers
3464 IsRegular,
3465 IsDirectory,
3466 IsSymlink,
3467 IsSocket,
3468 IsFifo,
3469 IsBlockDev,
3470 IsCharDev,
3471 IsDevice,
3472 IsExecutable,
3473
3474 /// Permission qualifiers
3475 Readable,
3476 Writable,
3477 Executable,
3478 WorldReadable,
3479 WorldWritable,
3480 WorldExecutable,
3481 GroupReadable,
3482 GroupWritable,
3483 GroupExecutable,
3484 Setuid,
3485 Setgid,
3486 Sticky,
3487
3488 /// Ownership qualifiers
3489 OwnedByEuid,
3490 OwnedByEgid,
3491 OwnedByUid(u32),
3492 OwnedByGid(u32),
3493
3494 /// Numeric qualifiers with range. `unit` is a `TT_*` i32
3495 /// (glob.rs:99-113 / glob.c:121-133); `op` is the raw range
3496 /// operator char (`<`, `=`, `>`) — mirrors C's qgetnum which
3497 /// returns the operator char and the handler switches inline.
3498 Size {
3499 value: u64,
3500 unit: i32,
3501 op: char,
3502 },
3503 Links {
3504 value: u64,
3505 op: char,
3506 },
3507 Atime {
3508 value: i64,
3509 unit: i32,
3510 op: char,
3511 },
3512 Mtime {
3513 value: i64,
3514 unit: i32,
3515 op: char,
3516 },
3517 Ctime {
3518 value: i64,
3519 unit: i32,
3520 op: char,
3521 },
3522
3523 /// Mode specification
3524 Mode {
3525 yes: u32,
3526 no: u32,
3527 },
3528
3529 /// Device number
3530 Device(u64),
3531
3532 /// Non-empty directory
3533 NonEmptyDir,
3534
3535 /// Shell evaluation
3536 Eval(String),
3537
3538 /// `^` — toggle the running match sense for SUBSEQUENT qualifiers in
3539 /// this list. C uses a running `sense ^= 1` (Src/glob.c:1346) stored
3540 /// per `struct qual.sense`; the enum can't carry per-node sense, so a
3541 /// marker in the list reproduces it: `check_qualifier_list` flips a
3542 /// running sense bit on each marker and XORs it into later tests.
3543 /// Fixes `*(/^@)` (dir AND not-symlink), `*(.^x)`, etc.
3544 ToggleSense,
3545}
3546
3547// Misnamed `gmatchcmp(&str, &str)` deleted — was a Rust-only
3548// locale-aware string compare claiming to be the C qsort
3549// comparator. The real C `gmatchcmp(Gmatch, Gmatch)` at glob.c:936
3550// is now ported as `gmatchcmp(&GlobMatch, &GlobMatch, &[i32], bool)`
3551// below. The string-compare case the old name claimed routes
3552// through canonical `zstrcmp` (sort.c:191).
3553
3554// `GlobOptions` struct deleted — Rust-only Bag-of-options with no
3555// C counterpart. C reads each option directly from the global
3556// `opts[]` array via `isset(NULL_GLOB)` / `isset(EXTENDED_GLOB)`
3557// / etc. (Src/options.c). The Rust port uses the canonical
3558// `opt_state_get(name) -> Option<bool>`
3559// which reads from the same global store. Inlined at each
3560// callsite as `opt_state_get("name").unwrap_or(default)`.
3561
3562/// Parsed glob qualifier set
3563#[derive(Debug, Clone, Default)]
3564/// Compiled qualifier list for one glob.
3565/// Mirrors the `struct qual *` linked list `parsepat()`
3566/// (Src/glob.c:791) builds — every `(qual)` after a glob pattern
3567/// adds to it.
3568#[allow(non_camel_case_types)]
3569pub struct qualifier_set {
3570 // c:138
3571 pub qualifiers: Vec<qualifier>,
3572 pub alternatives: Vec<Vec<qualifier>>,
3573 pub follow_links: bool,
3574 /// Packed sort-spec flags, one per `o`/`O` qualifier in the pattern.
3575 /// Each entry is the C `struct globsort.tp` field — `GS_NAME` /
3576 /// `GS_DEPTH` / `GS_EXEC` / `GS_SIZE` / `GS_ATIME` / `GS_MTIME` /
3577 /// `GS_CTIME` / `GS_LINKS` (or their `<< GS_SHIFT` follow-link
3578 /// variants), OR'd with `GS_DESC` for reverse direction.
3579 pub sorts: Vec<i32>, // c:155 struct globsort.tp[]
3580 /// c:74 `struct globsort.exec` — the eval code for each `GS_EXEC`
3581 /// (`oe:…:` / `o+…`) sort spec, indexed by the `idx` packed into the
3582 /// sort entry's high bits (`GS_EXEC | (idx << 16)`). Evaluated per
3583 /// match with `$REPLY` = the match name; the resulting `$REPLY` is
3584 /// the sort key (the original name is still emitted).
3585 pub sort_exec: Vec<String>, // c:74 globsort.exec
3586 /// The faithful glob.c `struct qual` representation of `alternatives`,
3587 /// built at parse end (additive — the enum is kept for now). AND-chain
3588 /// via `next`, alternatives via `or`, per-node `sense`/`range`/`amc`/
3589 /// `units`/`sdata`. The convergence target: `check_qualifiers` walks
3590 /// THIS via `insert()` semantics (glob.c:392-419), and the enum is
3591 /// eventually deleted.
3592 pub quals: QualArena, // c:206 curglobdata.gd_quals
3593 pub first: Option<i32>,
3594 pub last: Option<i32>,
3595 pub colon_mods: Option<String>,
3596 pub pre_words: Vec<String>,
3597 pub post_words: Vec<String>,
3598 /// `(M)` qualifier — append `/` to directory entries in output.
3599 /// Direct port of zsh/Src/glob.c:1557-1561 (`case 'M'`):
3600 /// `gf_markdirs = !(sense & 1)` — set when the qualifier appears
3601 /// without a `^` toggle. Stored per-qualifier-set rather than per
3602 /// GlobOptions so a single glob call's qualifier picks it up.
3603 pub mark_dirs: bool,
3604 /// `(T)` qualifier — append type-char (ls -F style) to every entry.
3605 /// Direct port of zsh/Src/glob.c:1562-1566 (`case 'T'`).
3606 pub list_types: bool,
3607 /// `(N)` qualifier — per-glob nullglob: empty result on no-match,
3608 /// no error. Direct port of zsh/Src/glob.c:1567-1569 (`case 'N'`):
3609 /// `gf_nullglob = !(sense & 1)` — set when `(N)` appears without
3610 /// the `^` toggle.
3611 pub nullglob: bool,
3612 /// `(D)` qualifier — include dotfiles in the glob result (override
3613 /// the global `globdots`/`dotglob` option for this glob alone).
3614 /// Direct port of zsh/Src/glob.c case 'D' which sets
3615 /// `gf_glob.dots = 1` for the duration of this glob expansion.
3616 pub globdots: bool,
3617 /// `(YN)` qualifier — short-circuit: limit number of matches to
3618 /// at most N. Direct port of zsh/Src/glob.c:1579-1595 (`case 'Y'`)
3619 /// which sets the C `shortcircuit` int. The matcher should stop
3620 /// after collecting N entries; the Rust port truncates after the
3621 /// match walk completes (the optimization gain is the same in
3622 /// the typical short-glob case). Bug #41 in docs/BUGS.md.
3623 pub short_circuit: Option<i32>,
3624 /// `(n)` qualifier — per-glob numeric sort. Direct port of
3625 /// zsh/Src/glob.c:1575-1577 (`case 'n': gf_numsort = !(sense & 1)`).
3626 /// Distinct from the `o`/`O` sort KEY `n` (= GS_NAME, lexical):
3627 /// standalone `(n)` makes the name comparison NUMERIC (so `*(n)`
3628 /// orders `f2` before `f10`), OR'd with the global NUMERIC_GLOB_SORT
3629 /// option at sort time (glob.c:947/983 `gf_numsort ?
3630 /// SORTIT_NUMERICALLY : 0`). `None` = not specified (fall back to
3631 /// the global option); `Some(true)` = `(n)`; `Some(false)` = `(^n)`
3632 /// which OVERRIDES the global option to force lexical.
3633 pub numsort: Option<bool>,
3634}
3635
3636/// Enter a glob scope: snapshot options into TLS if no outer scope
3637/// is active, returning a guard that clears the snapshot on Drop.
3638/// Re-entrant calls (nested globdata_glob via brace expansion etc.)
3639/// return a no-op guard that observes the existing snapshot.
3640pub fn enter_glob_scope() -> GlobOptsGuard {
3641 let already = GLOB_OPTS_TLS.with_borrow(|g| g.is_some());
3642 if already {
3643 return GlobOptsGuard { populated: false };
3644 }
3645 let snap = GlobOptSnapshot::capture();
3646 GLOB_OPTS_TLS.with_borrow_mut(|g| *g = Some(snap));
3647 GlobOptsGuard { populated: true }
3648}
3649
3650/// Read a glob-relevant option through the TLS snapshot when one is
3651/// active; fall back to the live `isset()` otherwise. Use this in
3652/// any glob-engine helper that previously read `isset(OPT)` for one
3653/// of the snapshotted options.
3654#[inline]
3655pub fn glob_isset(opt: i32) -> bool {
3656 GLOB_OPTS_TLS.with_borrow(|g| match g {
3657 Some(snap) => match opt {
3658 x if x == BAREGLOBQUAL => snap.bareglobqual,
3659 x if x == BRACECCL => snap.braceccl,
3660 x if x == CASEGLOB => snap.caseglob,
3661 x if x == EXTENDEDGLOB => snap.extendedglob,
3662 x if x == GLOBDOTS => snap.globdots,
3663 x if x == GLOBSTARSHORT => snap.globstarshort,
3664 x if x == LISTTYPES => snap.listtypes,
3665 x if x == MARKDIRS => snap.markdirs,
3666 x if x == NULLGLOB => snap.nullglob,
3667 x if x == NUMERICGLOBSORT => snap.numericglobsort,
3668 _ => isset(opt),
3669 },
3670 None => isset(opt),
3671 })
3672}
3673
3674// ===========================================================
3675// `impl globdata` block above kept only for the `new()`
3676// constructor (Default-style). All scanner / parser / qualifier
3677// ported below are top-level — C glob.c has them as top-level
3678// statics that mutate the file-static `curglobdata`. Each is
3679// flagged with `// RUST-ONLY` and a comment naming the closest
3680// C equivalent + the proper-port target (typically `scanner`
3681// at glob.c:500 driving `insert` at c:346).
3682// ===========================================================
3683
3684/// Main entry point: expand a glob pattern.
3685///
3686/// Closest C equivalent: `zglob` driver at glob.c:1214 calls
3687/// `parsepat` (c:791) and then `scanner` (c:500). The Rust port
3688/// here orchestrates qualifier parsing, brace expansion (which C
3689/// does separately in `xpandbraces`), then drives the faithful
3690/// `parsecomplist` (c:710) → `complist` → `scanner` (c:500) path.
3691pub fn globdata_glob(state: &mut globdata, pattern: &str) -> Vec<String> {
3692 // RUST-ONLY
3693 // Brace pre-expansion. In zsh, `xpandbraces` (zsh/Src/glob.c:2275)
3694 // runs during substitution before glob — patterns reaching glob()
3695 // are already brace-free in the production path (vm_helper handles
3696 // it). For direct programmatic callers of glob_with_options, run
3697 // the brace pass here so `GlobOptions.brace_ccl` is actually
3698 // consulted: with brace_ccl set, `{a-mnop}` expands to a..m,n,o,p
3699 // per glob.c:2424 BRACECCL block; without, only `{a,b}` lists and
3700 // `{1..5}`/`{a..e}` ranges expand. Recurse on each variant and
3701 // concatenate matches.
3702 let brace_ccl = glob_isset(BRACECCL);
3703 if hasbraces(pattern, brace_ccl) {
3704 let mut all = Vec::new();
3705 for variant in xpandbraces(pattern, brace_ccl) {
3706 all.extend(globdata_glob(state, &variant));
3707 }
3708 return all;
3709 }
3710
3711 state.matches.clear();
3712 state.pathbuf.clear();
3713 state.pathpos = 0;
3714
3715 // Parse qualifiers first so a bare-qualifier pattern like `dir(/)`
3716 // (no wildcard, just a stat-based filter) still enters the expansion
3717 // path. Without this, `haswilds("dir(/)")` returns false and the
3718 // pattern echoes back unfiltered, which defeats the whole point of
3719 // qualifiers.
3720 let (pat, quals) = parse_qualifiers(pattern);
3721 state.qualifiers = quals;
3722
3723 // c:Src/glob.c — `A~B` exclusion. zsh compiles the ENTIRE glob
3724 // (path components + `~B` exclusion + `**`) into ONE Patprog and
3725 // matches each candidate PATH against it, so the `~B` applies to the
3726 // whole path. zshrs's component scanner can't express a path-level
3727 // exclusion, so split a TOP-LEVEL `~` (extendedglob; depth 0, not in
3728 // `[...]`/`(...)`, not Bnull-escaped) off here into the main pattern
3729 // plus full-path exclusion patterns, glob the main pattern, then drop
3730 // matched paths matching any exclusion below. Without this the `~B`
3731 // (which may contain `/`) was split across path components and either
3732 // excluded everything (`~*/.git/*` → `*.txt~*` matched all) or
3733 // nothing. `~` inside one component still works the same (main globs,
3734 // the post-filter applies the exclusion).
3735 let (pat, glob_exclusions): (String, Vec<String>) =
3736 if glob_isset(EXTENDEDGLOB) && (pat.contains('~') || pat.contains('\u{98}')) {
3737 let cv: Vec<char> = pat.chars().collect();
3738 let mut parts: Vec<String> = Vec::new();
3739 let mut cur = String::new();
3740 let mut bd = 0i32; // `[...]` depth
3741 let mut pd = 0i32; // `(...)` depth
3742 let mut k = 0usize;
3743 while k < cv.len() {
3744 let c = cv[k];
3745 match c {
3746 '\u{9f}' => {
3747 // Bnull escape — next char is literal.
3748 cur.push(c);
3749 if k + 1 < cv.len() {
3750 cur.push(cv[k + 1]);
3751 k += 2;
3752 continue;
3753 }
3754 }
3755 '[' => {
3756 bd += 1;
3757 cur.push(c);
3758 }
3759 ']' => {
3760 if bd > 0 {
3761 bd -= 1;
3762 }
3763 cur.push(c);
3764 }
3765 '(' | '\u{88}' => {
3766 pd += 1;
3767 cur.push(c);
3768 }
3769 ')' | '\u{8a}' => {
3770 if pd > 0 {
3771 pd -= 1;
3772 }
3773 cur.push(c);
3774 }
3775 '~' | '\u{98}' if bd == 0 && pd == 0 => {
3776 parts.push(std::mem::take(&mut cur));
3777 }
3778 _ => cur.push(c),
3779 }
3780 k += 1;
3781 }
3782 parts.push(cur);
3783 // Only split when there's a non-empty main pattern AND at
3784 // least one exclusion (a leading `~` is home-dir, handled by
3785 // filesub before glob — never reaches here as an exclusion).
3786 if parts.len() > 1 && !parts[0].is_empty() {
3787 let main = parts.remove(0);
3788 (main, parts)
3789 } else {
3790 // No usable split — restore the original pattern verbatim
3791 // (the `~` separators dropped during the scan).
3792 (cv.iter().collect(), Vec::new())
3793 }
3794 } else {
3795 (pat, Vec::new())
3796 };
3797
3798 // c:Src/glob.c:1843-1854 — `if (!q || errflag) { ... return; }`. When
3799 // the qualifier parser already emitted a diagnostic (e.g. "number
3800 // expected" from qgetnum at c:832) and set errflag, abort glob
3801 // expansion entirely rather than continuing to scan with a partial
3802 // qualifier set. Without this gate, `?(a)` (where `(a)` is an
3803 // invalid qualifier) error-prints "number expected" then continues
3804 // to expand `?` against the dir, emitting matches alongside the
3805 // error — bug #549.
3806 if crate::ported::utils::errflag.load(std::sync::atomic::Ordering::SeqCst)
3807 & crate::ported::zsh_h::ERRFLAG_ERROR
3808 != 0
3809 {
3810 return Vec::new();
3811 }
3812
3813 // Now check wildcards on the qualifier-stripped pattern. A pure
3814 // literal with a qualifier (`name(.)`) still needs to enter the
3815 // scanner so the qualifier filter can run against the literal name.
3816 // haswilds scans TOKENIZED strings (c:Src/glob.c:1230 runs on the
3817 // lexer-tokenized word); glob_path receives both tokenized (zglob)
3818 // and untokenized (expand_glob fast paths) patterns, so tokenize a
3819 // local copy for the check — existing token chars pass through
3820 // zshtokenize untouched, untokenized metachars gain their tokens.
3821 let mut pat_tok = pat.clone();
3822 tokenize(&mut pat_tok); // c:Src/glob.c:3548
3823 if !haswilds(&pat_tok) && state.qualifiers.is_none() {
3824 return vec![pattern.to_string()];
3825 }
3826
3827 // c:glob() — `gd.gf_noglobdots = !isset(GLOBDOTS)` (plus the `(D)`
3828 // qualifier). parsecomplist reads this from CURGLOBDATA to compile
3829 // PAT_NOGLD into each component, which is what makes pattry reject
3830 // leading-dot files (pattern.rs:2890). Set it before parsing so the
3831 // faithful flag-driven dot filtering replaces the old manual skip.
3832 {
3833 let globdots = glob_isset(GLOBDOTS)
3834 || state
3835 .qualifiers
3836 .as_ref()
3837 .map(|qf| qf.globdots)
3838 .unwrap_or(false);
3839 let mut gd = CURGLOBDATA.lock().unwrap_or_else(|e| e.into_inner());
3840 gd.gf_noglobdots = if globdots { 0 } else { 1 };
3841 }
3842
3843 // c:parsepat (glob.c:809-820) — init pathbuf for absolute vs relative,
3844 // strip the leading `/`, then parse into a `complist` and run the
3845 // faithful `scanner`. parsecomplist consumes TOKENIZED input (it tests
3846 // for the `Star` token and hands segments to patcompile), so feed it
3847 // `pat_tok` (already tokenized above; tokenize is idempotent).
3848 state.pathbufcwd = 0; // c:812 — `DPUTS(pathbufcwd, ...)` invariant
3849 let parse_src = if let Some(rest) = pat_tok.strip_prefix('/') {
3850 // c:813-816 — absolute path.
3851 state.pathbuf.push('/');
3852 state.pathpos = 1;
3853 rest.to_string()
3854 } else {
3855 // c:817-818 — relative to pwd.
3856 pat_tok.clone()
3857 };
3858 if let Some(complist) = parsecomplist(&parse_src) {
3859 scanner(state, Some(&complist), 0, false);
3860 } else {
3861 // c:Src/glob.c:1842-1854 — `q = parsepat(str); if (!q || errflag)`.
3862 // This is the `!q` half: the pattern failed to COMPILE (e.g. an
3863 // unclosed `[` character class — patcomppiece returns -1 at
3864 // pattern.rs:1687). C then either treats the word as a literal
3865 // string (when `unset(BADPATTERN)`) or aborts with
3866 // `zerr("bad pattern: %s", ostr)`. zshrs previously dropped the
3867 // `None` silently, so `echo abc[def` fell through to the
3868 // no-matches / literal-under-NO_NOMATCH path instead of the
3869 // compile diagnostic — and BADPATTERN was ignored entirely.
3870 // (The `errflag` half — a diagnostic already emitted by the
3871 // qualifier parser — is handled by the gate at glob.rs:3770.)
3872 if !isset(crate::ported::zsh_h::BADPATTERN) {
3873 // c:1846-1848 — treat as an ordinary literal string.
3874 return vec![pattern.to_string()];
3875 }
3876 // c:1851-1852 — clear any stale error bit so zerr fires, then
3877 // emit. utils::zerr re-sets ERRFLAG_ERROR, which zglob's gate at
3878 // glob.rs:1541 reads to suppress the redundant "no matches found".
3879 errflag.fetch_and(
3880 !crate::ported::zsh_h::ERRFLAG_ERROR,
3881 std::sync::atomic::Ordering::SeqCst,
3882 );
3883 zerr(&format!("bad pattern: {}", pattern));
3884 return Vec::new();
3885 }
3886
3887 // c:Src/glob.c — apply top-level `~B` exclusions to the full matched
3888 // paths (split off above). A match is dropped if its emitted path
3889 // matches ANY exclusion pattern (`*` matches `/` here, matching zsh's
3890 // flat-string exclusion semantics).
3891 if !glob_exclusions.is_empty() {
3892 let eg = glob_isset(EXTENDEDGLOB);
3893 let cg = glob_isset(CASEGLOB);
3894 state.matches.retain(|m| {
3895 let p = glob_emit_path(&m.path);
3896 !glob_exclusions.iter().any(|ex| matchpat(ex, &p, eg, cg))
3897 });
3898 }
3899
3900 // c:1680 — populate GS_EXEC sort keys before sorting. Evaluate each
3901 // `oe:…:` / `o+…` code per match with $REPLY = the match name, and
3902 // capture the resulting $REPLY as that match's sort string (gmatchcmp
3903 // reads gmatch.sort_strings[idx]). Exec sort changes ORDER only — the
3904 // original name is still what's emitted.
3905 let sort_codes: Vec<String> = state
3906 .qualifiers
3907 .as_ref()
3908 .map(|q| q.sort_exec.clone())
3909 .unwrap_or_default();
3910 if !sort_codes.is_empty() {
3911 for m in state.matches.iter_mut() {
3912 let name = glob_emit_path(&m.path);
3913 for code in &sort_codes {
3914 crate::ported::params::setsparam("REPLY", &name);
3915 let _ = crate::ported::exec::execute_script(code);
3916 let key = crate::ported::params::getsparam("REPLY").unwrap_or_else(|| name.clone());
3917 m.sort_strings.push(key);
3918 }
3919 }
3920 }
3921
3922 // c:Src/glob.c:518 — the `Y` limit belongs to the SCAN, not to the result:
3923 // scanner(q->next, shortcircuit);
3924 // if (shortcircuit && shortcircuit == matchct)
3925 // return;
3926 // The scanner simply stops once N matches exist, so the survivors are the
3927 // first N *found*, and only then does c:1868's sort run over them. Applying
3928 // the limit after sorting (as apply_selection used to) keeps the first N by
3929 // SORT ORDER instead — a different set: `*(.Y2on)` must be the two files
3930 // the scan reached, re-ordered by name, not the two alphabetically-first.
3931 //
3932 // This port collects every match before sorting, so the faithful place for
3933 // the truncation is here — after collection, before sort_matches. The
3934 // `[first,last]` subscript stays in apply_selection: c:1868's sort precedes
3935 // it, so THAT one really is a post-sort selection.
3936 //
3937 // Zero means "no limit" (c:518's leading `shortcircuit &&`), hence `> 0`.
3938 if let Some(n) = state.qualifiers.as_ref().and_then(|q| q.short_circuit) {
3939 if n > 0 {
3940 let n = n as usize;
3941 if state.matches.len() > n {
3942 state.matches.truncate(n); // c:518
3943 }
3944 }
3945 }
3946
3947 // Sort results
3948 sort_matches(state);
3949
3950 // Apply subscript selection
3951 apply_selection(state);
3952
3953 // Extract filenames. Mark-dirs / list-types come from EITHER
3954 // the canonical global option store OR the parsed `(M)`/`(T)`
3955 // qualifier on this glob. Direct port of zsh/Src/glob.c:355,372
3956 // — output marker emission consults the per-glob `gf_markdirs`
3957 // / `gf_listtypes` flags which the qualifier parser at
3958 // glob.c:1557-1566 sets.
3959 let mark_dirs = glob_isset(MARKDIRS)
3960 || state
3961 .qualifiers
3962 .as_ref()
3963 .map(|q| q.mark_dirs)
3964 .unwrap_or(false);
3965 // c:1562-1566 — `gf_listtypes` is set ONLY by the `T` glob
3966 // qualifier (`*(T)`), NOT by the global LISTTYPES option.
3967 // LISTTYPES is the completion-listing option (see man zshoptions
3968 // "LIST_TYPES") and doesn't affect glob output. Including the
3969 // option here mis-decorated every executable-file glob with a
3970 // trailing `*` and every directory with `/` for users with the
3971 // default `setopt listtypes` ON.
3972 let list_types = state
3973 .qualifiers
3974 .as_ref()
3975 .map(|q| q.list_types)
3976 .unwrap_or(false);
3977 let colon_mods = state.qualifiers.as_ref().and_then(|q| q.colon_mods.clone());
3978 // c:Src/glob.c — a pattern ending in `/` ("trailing slash" syntax)
3979 // forces matches to be directories AND preserves the slash in the
3980 // output. zsh's parsepat treats `pat/` as `pat` + an empty trailing
3981 // component; the scanner restricts to directories and the emitter
3982 // appends `/`. The Rust port's parser short-circuits the empty
3983 // trailing component, so we filter + re-add the suffix here at emit
3984 // time. `/` is never a glob metachar so a literal trailing-slash
3985 // check on the raw pattern is sufficient.
3986 // Use the qualifier-stripped `pat` for the trailing-slash check.
3987 // For `*/(N)` the original `pattern` ends with `)`, so the check
3988 // on `pattern` returned false and the trailing-slash directory-
3989 // filter was skipped — `*/(N)` then matched every file, not just
3990 // directories. The qualifier-stripped `pat` is `*/` which still
3991 // carries the trailing `/`.
3992 let trailing_slash = pat.ends_with('/') && pat.len() > 1;
3993 // c:Src/glob.c — preserve user-typed `./` / `../` prefix in
3994 // output. glob_emit_path strips CurDir uniformly so the
3995 // leading `./` would be lost. Detect the source prefix here
3996 // and re-prepend after emit. Same `pat` (post-qualifier) — the
3997 // prefix is on the path part, not the qualifier.
3998 let leading_dot_prefix: &str = if pat.starts_with("../") {
3999 "../"
4000 } else if pat.starts_with("./") {
4001 "./"
4002 } else {
4003 ""
4004 };
4005 let mut results: Vec<String> = state
4006 .matches
4007 .iter()
4008 .filter(|m| !trailing_slash || fs::metadata(&m.path).map(|md| md.is_dir()).unwrap_or(false))
4009 .map(|m| {
4010 let mut s = glob_emit_path(&m.path);
4011 if !leading_dot_prefix.is_empty()
4012 && !s.starts_with(leading_dot_prefix)
4013 && !s.starts_with('/')
4014 {
4015 s = format!("{}{}", leading_dot_prefix, s);
4016 }
4017 if trailing_slash && !s.ends_with('/') {
4018 s.push('/');
4019 }
4020 if mark_dirs || list_types {
4021 if let Ok(meta) = fs::symlink_metadata(&m.path) {
4022 let ch = file_type(meta.mode());
4023 if list_types || (mark_dirs && ch == '/') {
4024 // Don't double-stamp if trailing_slash already added one.
4025 if !s.ends_with(ch) {
4026 s.push(ch);
4027 }
4028 }
4029 }
4030 }
4031 // Apply colon modifiers AFTER mark/list-type appendage —
4032 // zsh applies them last in glob.c:432 modify() per emitted
4033 // node, so `(M:t)` would mark THEN tail (effectively just
4034 // tail since the slash is gone). Faithful order.
4035 if let Some(ref m) = colon_mods {
4036 // Delegate to canonical `modify()` port in subst.rs
4037 // (Src/subst.c:4531). Local colon-modifier impl was
4038 // an invented duplicate — removed.
4039 s = crate::ported::subst::modify(&s, m);
4040 }
4041 s
4042 })
4043 .collect();
4044
4045 // c:421-426 — zsh applies the colon modifier in insert() (during
4046 // collection) BEFORE the match sort, so the default (name) sort
4047 // orders the MODIFIED names: `*(.:e)` on (apple.z,banana.a,cherry.m)
4048 // sorts the extensions → (a,m,z), not name order (z,a,m). The Rust
4049 // pipeline modifies at emit (after sort_matches), so re-sort the
4050 // modified results here — but ONLY for the default name sort; an
4051 // explicit `o`/`O` keeps its stat-keyed order with modified output.
4052 let explicit_sort = state
4053 .qualifiers
4054 .as_ref()
4055 .map(|q| !q.sorts.is_empty())
4056 .unwrap_or(false);
4057 if colon_mods.is_some() && !explicit_sort {
4058 results.sort();
4059 }
4060
4061 // c:1744-1756 / insert_glob_match c:1430 — `P:word:` (gf_pre_words)
4062 // emits `word` as a separate element BEFORE each match; `^P:word:`
4063 // (gf_post_words) emits it AFTER. Applied last, after sort/modifiers.
4064 let (pre_words, post_words) = state
4065 .qualifiers
4066 .as_ref()
4067 .map(|q| (q.pre_words.clone(), q.post_words.clone()))
4068 .unwrap_or_default();
4069 if !pre_words.is_empty() || !post_words.is_empty() {
4070 let mut expanded =
4071 Vec::with_capacity(results.len() * (1 + pre_words.len() + post_words.len()));
4072 for s in results {
4073 expanded.extend(pre_words.iter().cloned());
4074 expanded.push(s);
4075 expanded.extend(post_words.iter().cloned());
4076 }
4077 results = expanded;
4078 }
4079
4080 // c:Src/glob.c:1872-1888 — no-match path. Return Vec::new() so
4081 // the caller (vm_helper.rs::expand_glob) drives the
4082 // NULLGLOB / NOMATCH / literal-fallback policy. Previously
4083 // this pushed `pattern.to_string()` on no-match, which made
4084 // `expand_glob` see a non-empty result and skip the NOMATCH
4085 // check entirely. zsh's `setopt nomatch` is on by default, so
4086 // `echo /never_real/*` should emit "no matches found" and
4087 // exit 1 — not silently print the literal. Parity bug #13
4088 // recurrence. Per-glob `(N)` qualifier (nullglob_per_qual)
4089 // and global NULLGLOB are now consulted in expand_glob too.
4090 let _ = state.qualifiers.as_ref().map(|q| q.nullglob);
4091 results
4092}
4093
4094// `sort_matches_by_type` deleted — dead code (no callers anywhere
4095// in the tree). The sort path lives in `GlobMatch::compare` which
4096// dispatches off the canonical `GS_*` tp bits exactly like C's
4097// `gmatchcmp()` at glob.c:936.
4098
4099// ============================================================================
4100// Pattern matching with replacement (from glob.c getmatch family)
4101// ============================================================================
4102
4103// `MatchFlags` deleted — Rust-only bool-bag wrapper. C uses bare
4104// `int fl` with `SUB_*` bits from `Src/zsh.h:1981+` (mirrored at
4105// `zsh_h.rs:2463+`). Callers now pass an `i32` and test bits.
4106
4107// Trimmed local `struct imatchdata` deleted — it was a fake 5-field
4108// duplicate (str/pattern/match_start/match_end/replacement) of the
4109// canonical `Src/zsh.h:1740` port that already lives in
4110// `zsh_h.rs::imatchdata` (mstr/mlen/ustr/ulen/flags/replstr/repllist).
4111// `get_match_ret` now uses the canonical struct; see its port above.
4112
4113/// Strip a trailing `(qual)` block from a glob pattern.
4114/// **RUST-ONLY** — C glob.c handles qualifier parsing inline in
4115/// `parsepat` (c:791) as it builds the Complist. Move to that
4116/// shape when porting parsepat for real.
4117pub fn parse_qualifiers(pattern: &str) -> (String, Option<qualifier_set>) {
4118 // RUST-ONLY
4119 if !pattern.ends_with(')') {
4120 return (pattern.to_string(), None);
4121 }
4122
4123 let bytes = pattern.as_bytes();
4124 // Backslash-escaped trailing `)` is literal — no qualifier block
4125 // present. C's glob path handles this via Bnull preservation in
4126 // the lexer; zshrs's path arrives here with `\)` for quoted
4127 // close-parens (e.g. assoc-value `'\)'`). Without the gate, the
4128 // qualifier-extraction walk below treats the literal `)` as the
4129 // qualifier terminator.
4130 let last_paren_escaped = {
4131 let mut bs = 0usize;
4132 let mut j = bytes.len() - 1;
4133 while j > 0 && bytes[j - 1] == b'\\' {
4134 bs += 1;
4135 j -= 1;
4136 }
4137 bs % 2 == 1
4138 };
4139 if last_paren_escaped {
4140 return (pattern.to_string(), None);
4141 }
4142
4143 // Find matching open paren
4144 let mut depth = 0;
4145 let mut qual_start = None;
4146
4147 // c:Src/glob.c haswilds backslash handling — a `\(` / `\)` is a
4148 // literal paren, not a qualifier delimiter. Track which paren
4149 // bytes are escaped by a preceding `\` (counting consecutive
4150 // backslashes — even count means the paren is unescaped, odd
4151 // count means escaped). Without this gate, `\(*\)` is mis-parsed
4152 // as `(*\)` qualifier + empty pattern prefix, sending `*\` to
4153 // the qualifier-letter switch which emits "unknown file
4154 // attribute: \".
4155 let is_escaped = |idx: usize| -> bool {
4156 let mut bs = 0usize;
4157 let mut j = idx;
4158 while j > 0 && bytes[j - 1] == b'\\' {
4159 bs += 1;
4160 j -= 1;
4161 }
4162 bs % 2 == 1
4163 };
4164
4165 for i in (0..bytes.len()).rev() {
4166 match bytes[i] {
4167 b')' => {
4168 if !is_escaped(i) {
4169 depth += 1;
4170 }
4171 }
4172 b'(' => {
4173 if !is_escaped(i) {
4174 depth -= 1;
4175 if depth == 0 {
4176 qual_start = Some(i);
4177 break;
4178 }
4179 }
4180 }
4181 _ => {}
4182 }
4183 }
4184
4185 let start = match qual_start {
4186 Some(s) => s,
4187 None => return (pattern.to_string(), None),
4188 };
4189
4190 // Check for (#q...) explicit qualifier syntax.
4191 let qual_str = &pattern[start + 1..pattern.len() - 1];
4192 // c:Src/glob.c:1192-1197 — `if (isset(EXTENDEDGLOB) &&
4193 // !zpc_disables[ZPC_HASH] && s[1] == Pound) { if (s[2] != 'q')
4194 // return 0; ret = 2; }`. The leading `#` inside `(...)` is ONLY
4195 // special under EXTENDEDGLOB: `(#q...)` is the explicit glob
4196 // qualifier, and `(#X...)` for any other X is an inline pattern flag
4197 // (passed through). Without EXTENDEDGLOB the `#` is not special at
4198 // all — the `(...)` is a bare qualifier group and a leading `#` is an
4199 // (unknown) attribute char, so `*(#q.)` errors `unknown file
4200 // attribute: #` rather than silently applying the `.` qualifier.
4201 let (is_explicit, qual_content) = if glob_isset(EXTENDEDGLOB) && qual_str.starts_with('#') {
4202 if let Some(after) = qual_str.strip_prefix("#q") {
4203 (true, after) // c:1195 ret = 2 — explicit glob qualifier
4204 } else {
4205 // c:1194 `if (s[2] != 'q') return 0;` — inline pattern flag
4206 // (`(#i)`, `(#c1,2)`, `(#a)`, `(#l)`, `(#s)`, `(#e)`, `(#m)`).
4207 return (pattern.to_string(), None);
4208 }
4209 } else if glob_isset(BAREGLOBQUAL) {
4210 (false, qual_str)
4211 } else {
4212 return (pattern.to_string(), None);
4213 };
4214
4215 // Don't parse as qualifiers if it contains | or ~ (alternatives/exclusions)
4216 if !is_explicit && (qual_content.contains('|') || qual_content.contains('~')) {
4217 return (pattern.to_string(), None);
4218 }
4219
4220 // Parse the qualifiers
4221 let qs = parse_qualifier_string(qual_content);
4222 (pattern[..start].to_string(), Some(qs))
4223}
4224
4225/// Parse the body of a `(...)` qualifier block into a qualifier_set.
4226/// **RUST-ONLY** — see header on `parse_qualifiers`.
4227fn parse_qualifier_string(s: &str) -> qualifier_set {
4228 // RUST-ONLY
4229 let mut qs = qualifier_set::default();
4230 let mut chars = s.chars().peekable();
4231 let mut negated = false;
4232 let mut follow = false;
4233
4234 while let Some(c) = chars.next() {
4235 match c {
4236 '^' => {
4237 // c:1346 — `sense ^= 1`. Keep the running `negated` for
4238 // the flag qualifiers (N/D/M/T) that read it, AND emit a
4239 // marker so the per-qualifier sense reaches the list walk.
4240 negated = !negated;
4241 qs.qualifiers.push(qualifier::ToggleSense);
4242 }
4243 '-' => follow = !follow,
4244 ',' => {
4245 // Start new alternative
4246 if !qs.qualifiers.is_empty() {
4247 qs.alternatives.push(std::mem::take(&mut qs.qualifiers));
4248 }
4249 negated = false;
4250 follow = false;
4251 }
4252 ':' => {
4253 // Colon modifiers - rest of string
4254 let rest: String = chars.collect();
4255 qs.colon_mods = Some(format!(":{}", rest));
4256 break;
4257 }
4258 // File type qualifiers
4259 '/' => qs.qualifiers.push(qualifier::IsDirectory),
4260 '.' => qs.qualifiers.push(qualifier::IsRegular),
4261 '@' => qs.qualifiers.push(qualifier::IsSymlink),
4262 '=' => qs.qualifiers.push(qualifier::IsSocket),
4263 'p' => qs.qualifiers.push(qualifier::IsFifo),
4264 '%' => match chars.peek() {
4265 Some('b') => {
4266 chars.next();
4267 qs.qualifiers.push(qualifier::IsBlockDev);
4268 }
4269 Some('c') => {
4270 chars.next();
4271 qs.qualifiers.push(qualifier::IsCharDev);
4272 }
4273 _ => qs.qualifiers.push(qualifier::IsDevice),
4274 },
4275 '*' => qs.qualifiers.push(qualifier::IsExecutable),
4276 // Permission qualifiers
4277 'r' => qs.qualifiers.push(qualifier::Readable),
4278 'w' => qs.qualifiers.push(qualifier::Writable),
4279 'x' => qs.qualifiers.push(qualifier::Executable),
4280 'R' => qs.qualifiers.push(qualifier::WorldReadable),
4281 'W' => qs.qualifiers.push(qualifier::WorldWritable),
4282 'X' => qs.qualifiers.push(qualifier::WorldExecutable),
4283 'A' => qs.qualifiers.push(qualifier::GroupReadable),
4284 'I' => qs.qualifiers.push(qualifier::GroupWritable),
4285 'E' => qs.qualifiers.push(qualifier::GroupExecutable),
4286 's' => qs.qualifiers.push(qualifier::Setuid),
4287 'S' => qs.qualifiers.push(qualifier::Setgid),
4288 't' => qs.qualifiers.push(qualifier::Sticky),
4289 // c:Src/glob.c case 'f' — file-mode qualifier accepts
4290 // an octal mode (`f755`) or chmod-style symbolic spec
4291 // (`fu+x`/`fg-w`/`fa=r`) and matches files whose mode
4292 // bits agree. Multiple delimited specs can be chained
4293 // by separator character. Direct port of qgetmodespec
4294 // dispatch + mode-bit accumulator in qgetmodespec.
4295 'f' => {
4296 // c:Src/glob.c:849 — qgetmodespec dispatches on the
4297 // first char: if it's `=`/`+`/`-`/`?` or a digit
4298 // (`(c >= '0' && c <= '7')`) the spec is bare (no
4299 // surrounding delimiter, no who) — `f+x` / `f-w` /
4300 // `f=755` / `f0644`. Otherwise the char is the
4301 // OPENING DELIMITER paired with a matching closer
4302 // (`<` → `>`, `[` → `]`, `{` → `}`, anything else
4303 // → itself, e.g. `f:u+x:` uses `:` on both sides).
4304 // The previous port used `!d.is_alphanumeric()`
4305 // which incorrectly classified `+`, `-`, `=`, `?`
4306 // as delimiters, sending `f+x` down the
4307 // delimiter-string path that then called
4308 // qgetmodespec on body "x" — qgetmodespec rejects
4309 // `x` (no op char) and silently dropped the
4310 // qualifier, so `*(.f+x)` matched ALL files. Bug
4311 // #105 in docs/BUGS.md.
4312 let rest: String = chars.clone().collect();
4313 let trimmed: &str = match rest.chars().next() {
4314 // c:849-861 — bare spec ONLY when the first char is
4315 // `=`/`+`/`-`/`?` or an octal digit; ANY other char is
4316 // the OPENING DELIMITER (`<`→`>`, `[`→`]`, `{`→`}`, else
4317 // itself). Who-clauses (u/g/o/a) are valid only INSIDE a
4318 // delimited spec, so bare `fu+x` lands here with `u` as
4319 // the delimiter — and with no closing `u` it is an
4320 // unterminated (invalid) spec, exactly as in zsh.
4321 Some(d) if !matches!(d, '+' | '-' | '=' | '?' | '0'..='7') => {
4322 chars.next(); // consume opening delim
4323 let close = match d {
4324 '<' => '>',
4325 '[' => ']',
4326 '{' => '}',
4327 other => other,
4328 };
4329 let mut body = String::new();
4330 let mut closed = false;
4331 while let Some(pc) = chars.next() {
4332 if pc == close {
4333 closed = true;
4334 break;
4335 }
4336 body.push(pc);
4337 }
4338 if !closed {
4339 // c:884/930 — `zerr("invalid mode specification")`
4340 // on an unterminated spec; the glob then fails.
4341 crate::ported::utils::errflag.fetch_or(
4342 crate::ported::utils::ERRFLAG_ERROR,
4343 std::sync::atomic::Ordering::Relaxed,
4344 );
4345 return qs;
4346 }
4347 if let Some((_who, op, perm, _r)) = qgetmodespec(&body) {
4348 let (yes, no) = match op {
4349 '=' => (perm, 0o7777 & !perm),
4350 '+' => (perm, 0),
4351 '-' => (0, perm),
4352 _ => (perm, 0),
4353 };
4354 qs.qualifiers.push(qualifier::Mode { yes, no });
4355 }
4356 ""
4357 }
4358 _ => {
4359 let parsed = qgetmodespec(&rest);
4360 if let Some((who, op, perm, r)) = parsed {
4361 let consumed = rest.len() - r.len();
4362 for _ in 0..consumed {
4363 chars.next();
4364 }
4365 let _ = who;
4366 let (yes, no) = match op {
4367 '=' => (perm, 0o7777 & !perm),
4368 '+' => (perm, 0),
4369 '-' => (0, perm),
4370 _ => (perm, 0),
4371 };
4372 qs.qualifiers.push(qualifier::Mode { yes, no });
4373 }
4374 ""
4375 }
4376 };
4377 let _ = trimmed;
4378 }
4379 // Ownership
4380 'U' => qs.qualifiers.push(qualifier::OwnedByEuid),
4381 'G' => qs.qualifiers.push(qualifier::OwnedByEgid),
4382 'u' => {
4383 let uid = parse_uid_gid(&mut chars, false);
4384 qs.qualifiers.push(qualifier::OwnedByUid(uid));
4385 }
4386 'g' => {
4387 let gid = parse_uid_gid(&mut chars, true);
4388 qs.qualifiers.push(qualifier::OwnedByGid(gid));
4389 }
4390 // Size
4391 'L' => {
4392 let (unit, op, val) = parse_size_spec(&mut chars);
4393 qs.qualifiers.push(qualifier::Size {
4394 value: val,
4395 unit,
4396 op,
4397 });
4398 }
4399 // Link count
4400 'l' => {
4401 let (op, val) = parse_range_spec(&mut chars);
4402 qs.qualifiers.push(qualifier::Links { value: val, op });
4403 }
4404 // Times
4405 'a' => {
4406 let (unit, op, val) = schedgetfn(&mut chars);
4407 qs.qualifiers.push(qualifier::Atime {
4408 value: val as i64,
4409 unit,
4410 op,
4411 });
4412 }
4413 'm' => {
4414 let (unit, op, val) = schedgetfn(&mut chars);
4415 qs.qualifiers.push(qualifier::Mtime {
4416 value: val as i64,
4417 unit,
4418 op,
4419 });
4420 }
4421 'c' => {
4422 let (unit, op, val) = schedgetfn(&mut chars);
4423 qs.qualifiers.push(qualifier::Ctime {
4424 value: val as i64,
4425 unit,
4426 op,
4427 });
4428 }
4429 // Sort qualifier — `o<key>` ascending / `O<key>`
4430 // descending. Mirrors the parser arm in zsh's glob.c
4431 // that builds `struct globsort` entries (tp = GS_* OR
4432 // GS_DESC, optionally shifted by GS_SHIFT for the
4433 // follow-links variant).
4434 'o' | 'O' => {
4435 let desc = c == 'O';
4436 if let Some(&sc) = chars.peek() {
4437 let key: i32 = match sc {
4438 'n' => {
4439 chars.next();
4440 GS_NAME
4441 }
4442 'L' => {
4443 chars.next();
4444 GS_SIZE
4445 }
4446 'l' => {
4447 chars.next();
4448 GS_LINKS
4449 }
4450 'a' => {
4451 chars.next();
4452 GS_ATIME
4453 }
4454 'm' => {
4455 chars.next();
4456 GS_MTIME
4457 }
4458 'c' => {
4459 chars.next();
4460 GS_CTIME
4461 }
4462 'd' => {
4463 chars.next();
4464 GS_DEPTH
4465 }
4466 'N' => {
4467 chars.next();
4468 GS_NONE
4469 }
4470 'e' | '+' => {
4471 // c:1672-1687 — GS_EXEC sort: `glob_exec_string`
4472 // parses the eval code (delimited `e:code:` or
4473 // identifier `+name`); store it indexed and pack
4474 // the index into the sort entry. The code is
4475 // consumed HERE (not left for the main loop), so
4476 // `oe:…:` is a SORT (eval = key, original name
4477 // emitted), not a separate eval qualifier.
4478 let is_plus = sc == '+';
4479 chars.next(); // consume 'e' / '+'
4480 let rest: String = chars.clone().collect();
4481 match glob_exec_string(&rest, is_plus) {
4482 Some((code, consumed)) => {
4483 for _ in 0..consumed {
4484 chars.next();
4485 }
4486 let idx = qs.sort_exec.len();
4487 qs.sort_exec.push(code);
4488 GS_EXEC | ((idx as i32) << 16)
4489 }
4490 None => {
4491 // glob_exec_string already emitted the
4492 // diagnostic + set errflag; bail.
4493 return qs;
4494 }
4495 }
4496 }
4497 _ => {
4498 // c:1689 — `default: zerr("unknown sort
4499 // specifier"); restore_globstate(saved);
4500 // return;`. Was `_ => GS_NAME`, which silently
4501 // accepted invalid keys (and reparsed them as
4502 // separate qualifiers, e.g. `oD` → o + D-dotglob
4503 // instead of erroring like zsh).
4504 crate::ported::utils::zerr("unknown sort specifier");
4505 crate::ported::utils::errflag.fetch_or(
4506 crate::ported::utils::ERRFLAG_ERROR,
4507 std::sync::atomic::Ordering::Relaxed,
4508 );
4509 return qs;
4510 }
4511 };
4512 let shifted = if follow && (key & GS_NORMAL) != 0 {
4513 key << GS_SHIFT // c:1692-1694
4514 } else {
4515 key
4516 };
4517 // c:1695-1702 — a sort key may not be repeated:
4518 // if (t != GS_EXEC) {
4519 // if (gf_sorts & t) {
4520 // zerr("doubled sort specifier");
4521 // restore_globstate(saved);
4522 // return;
4523 // }
4524 // }
4525 // gf_sorts |= t;
4526 // GS_EXEC is exempt — `oe:…:` may appear repeatedly, each
4527 // with its own code. This check was missing entirely, so
4528 // `*(.onon)` and `*(.onOn)` silently sorted where zsh
4529 // fails. Note the test is on the SHIFTED key, so `oL` and
4530 // `OL` collide (same key, different direction) while `oL`
4531 // and `oLm` do not (the follow variant shifts).
4532 if (shifted & GS_EXEC) == 0 && qs.sorts.iter().any(|s| (s & !GS_DESC) == shifted)
4533 {
4534 crate::ported::utils::zerr("doubled sort specifier"); // c:1697
4535 crate::ported::utils::errflag.fetch_or(
4536 crate::ported::utils::ERRFLAG_ERROR,
4537 std::sync::atomic::Ordering::Relaxed,
4538 );
4539 return qs; // c:1699
4540 }
4541 // c:1658-1662 — `if (gf_nsorts == MAX_SORTS) { zerr("too
4542 // many glob sort specifiers"); ... return; }`. MAX_SORTS is
4543 // 12 (c:164). Unreachable in practice now that the doubled
4544 // check above rejects repeats, but it is C's bound and the
4545 // list is otherwise unbounded.
4546 if qs.sorts.len() == MAX_SORTS {
4547 crate::ported::utils::zerr("too many glob sort specifiers"); // c:1659
4548 crate::ported::utils::errflag.fetch_or(
4549 crate::ported::utils::ERRFLAG_ERROR,
4550 std::sync::atomic::Ordering::Relaxed,
4551 );
4552 return qs; // c:1661
4553 }
4554 let tp = shifted | (if desc { GS_DESC } else { 0 });
4555 qs.sorts.push(tp);
4556 } else {
4557 // c:1666-1690 — C does not guard on "is there a next
4558 // character": it reads `switch (*s)` unconditionally, so an
4559 // `o`/`O` at the very end of the qualifier list sees the
4560 // terminating `)` and lands on `default: zerr("unknown sort
4561 // specifier")`. This port skipped the whole block when the
4562 // iterator was exhausted, so a trailing `o` was silently
4563 // ignored and `*(No)` listed every match where zsh fails.
4564 // A NON-trailing bad spec (`*(ozN)`) already errored — it
4565 // reaches the `_` arm above — which is why only the
4566 // end-of-list form survived.
4567 crate::ported::utils::zerr("unknown sort specifier"); // c:1688
4568 crate::ported::utils::errflag.fetch_or(
4569 crate::ported::utils::ERRFLAG_ERROR,
4570 std::sync::atomic::Ordering::Relaxed,
4571 );
4572 return qs; // c:1690
4573 }
4574 }
4575 // Flags
4576 // c:1567-1569 — `case 'N': gf_nullglob = !(sense & 1);`.
4577 // Per-glob nullglob: empty result on no-match, no error.
4578 'N' => qs.nullglob = !negated,
4579 'D' => qs.globdots = !negated,
4580 // c:1575-1577 — `case 'n': gf_numsort = !(sense & 1)`.
4581 // Standalone `(n)` enables numeric sort for this glob (the
4582 // `^n` toggle disables it). Consumed in sort_matches.
4583 'n' => qs.numsort = Some(!negated),
4584 // (M) / (T) — set per-qualifier-set flags. glob.c:1557-1566:
4585 // case 'M': gf_markdirs = !(sense & 1); break;
4586 // case 'T': gf_listtypes = !(sense & 1); break;
4587 // `sense & 1` = the `^`-toggle bit. zshrs's parser tracks
4588 // `negated`; mirror by `!negated`. Read at output-emit
4589 // time to mark dirs / list types like coreutils ls -F.
4590 'M' => qs.mark_dirs = !negated,
4591 'T' => qs.list_types = !negated,
4592 'F' => qs.qualifiers.push(qualifier::NonEmptyDir),
4593 // c:1744-1756 — `P:word:` adds `word` to gf_pre_words (or
4594 // gf_post_words under `^`), emitted as a separate element
4595 // before/after each match. `glob_exec_string` parses the
4596 // delimited word.
4597 'P' => {
4598 let rest: String = chars.clone().collect();
4599 if let Some((word, consumed)) = glob_exec_string(&rest, false) {
4600 for _ in 0..consumed {
4601 chars.next();
4602 }
4603 if negated {
4604 qs.post_words.push(word); // c:1750 gf_post_words
4605 } else {
4606 qs.pre_words.push(word); // c:1750 gf_pre_words
4607 }
4608 }
4609 }
4610 // Subscript
4611 '[' => {
4612 let (first, last) = parse_subscript(&mut chars);
4613 qs.first = first;
4614 qs.last = last;
4615 }
4616 // c:Src/glob.c:1579-1595 `case 'Y'` — short-circuit:
4617 // limit matches to at most N. Reads a numeric argument
4618 // immediately following the `Y`. Bug #41 in docs/BUGS.md.
4619 'Y' => {
4620 // c:1579-1594:
4621 // const char *s_saved = s;
4622 // shortcircuit = !(sense & 1);
4623 // if (shortcircuit) {
4624 // data = qgetnum(&s);
4625 // if ((shortcircuit = data) != data) {
4626 // /* Integer overflow */
4627 // zerr("value too big: Y%s", s_saved);
4628 // restore_globstate(saved);
4629 // return;
4630 // }
4631 // }
4632 // `data` is a zlong and `shortcircuit` an int, so the
4633 // assignment-then-compare is a 64→32 TRUNCATION test: a limit
4634 // that does not survive the narrowing is fatal. This parsed
4635 // straight into i32 and silently dropped the qualifier when
4636 // that failed, so `*(Y2147483648)` listed every match where
4637 // zsh errors — the limit was simply ignored.
4638 //
4639 // s_saved is the text AFTER the `Y`, running to the end of the
4640 // qualifier list, which is why the message carries any trailing
4641 // qualifiers too: `*(.NY99…)` reports `value too big: Y99…`
4642 // exactly as spelled.
4643 let s_saved: String = chars.clone().collect(); // c:1582
4644 let mut num_str = String::new();
4645 while let Some(&pc) = chars.peek() {
4646 if pc.is_ascii_digit() {
4647 num_str.push(pc);
4648 chars.next();
4649 } else {
4650 break;
4651 }
4652 }
4653 // c:1586 — qgetnum is `while (idigit(**s)) v = v * 10 + …`,
4654 // with no overflow check of its own; it simply wraps. Mirror
4655 // that rather than failing the parse, so the truncation test
4656 // below is what decides, exactly as in C.
4657 let mut data: i64 = 0;
4658 for ch in num_str.chars() {
4659 data = data
4660 .wrapping_mul(10)
4661 .wrapping_add((ch as i64) - ('0' as i64));
4662 }
4663 let sc = data as i32; // c:1587 `shortcircuit = data`
4664 if sc as i64 != data {
4665 // c:1587 `!= data`
4666 crate::ported::utils::zerr(&format!("value too big: Y{s_saved}")); // c:1589
4667 crate::ported::utils::errflag.fetch_or(
4668 crate::ported::utils::ERRFLAG_ERROR,
4669 std::sync::atomic::Ordering::Relaxed,
4670 );
4671 return qs; // c:1591
4672 }
4673 qs.short_circuit = Some(sc);
4674 }
4675 // c:Src/glob.c:1599-1620 `case 'e'` — `(e:CODE:)` shell-
4676 // eval qualifier. The body is delimited by the char
4677 // immediately following 'e' (matched at both ends).
4678 // Common forms in zsh scripts: `(e:'[[ -L $REPLY ]]':)`
4679 // for symlink filter, etc. The body runs against each
4680 // candidate path with $REPLY set; the file is kept iff
4681 // the body returns 0. zshrs's qualifier::Eval variant
4682 // was defined but had no parser arm — was rejected as
4683 // "unknown file attribute: e". Bug #469.
4684 'e' => {
4685 let delim = match chars.next() {
4686 Some(d) => d,
4687 None => break,
4688 };
4689 let mut body = String::new();
4690 while let Some(&pc) = chars.peek() {
4691 if pc == delim {
4692 chars.next();
4693 break;
4694 }
4695 body.push(pc);
4696 chars.next();
4697 }
4698 if negated {
4699 // (^e:CODE:) inverts; we route through Eval and
4700 // flip by wrapping the body. Simpler: just store
4701 // and let the negated flag at the qualifier_set
4702 // level handle inversion.
4703 }
4704 qs.qualifiers.push(qualifier::Eval(body));
4705 }
4706 // c:Src/glob.c:1708-1722 `case '+':` — `(+FUNC)` invokes
4707 // shell function FUNC on each candidate; keep file iff
4708 // function returns 0. C's glob_exec_string (c:1085) reads
4709 // the identifier name via `itype_end(s, IIDENT, 0)` when
4710 // the qualifier letter was '+'. The body wraps the call as
4711 // a shell expression `FUNC` that qualsheval (c:4769 zshrs
4712 // qualifier::Eval) runs as a one-shot. Bug #N — this arm
4713 // was missing entirely, so `*(+func)` and `*(s+0)`-style
4714 // mixed-qualifier strings errored "unknown file attribute:
4715 // +" instead of routing through the Eval path that would
4716 // either match files or fall through to "no matches found".
4717 '+' => {
4718 // c:1090-1097 — `tt = itype_end(s, IIDENT, 0); if
4719 // (tt == s) zerr("missing identifier after `+'")`.
4720 // Read identifier chars greedily.
4721 let mut ident = String::new();
4722 while let Some(&pc) = chars.peek() {
4723 if pc.is_ascii_alphanumeric() || pc == '_' {
4724 ident.push(pc);
4725 chars.next();
4726 } else {
4727 break;
4728 }
4729 }
4730 if ident.is_empty() {
4731 crate::ported::utils::zerr("missing identifier after `+'"); // c:1095
4732 crate::ported::utils::errflag.fetch_or(
4733 crate::ported::utils::ERRFLAG_ERROR,
4734 std::sync::atomic::Ordering::Relaxed,
4735 );
4736 return qs;
4737 }
4738 // c:1109-1117 — the identifier IS the body that
4739 // qualsheval will run. Push as Eval so the same
4740 // per-file evaluator at c:4769 fires.
4741 qs.qualifiers.push(qualifier::Eval(ident));
4742 }
4743 // c:Src/glob.c:1758-1762 — `default: zerr("unknown file
4744 // attribute: %c", *s); restore_globstate(saved); return;`.
4745 // Bug #583: zshrs's `_ => {}` arm silently accepted any
4746 // unknown qualifier letter, so `*(z)` returned all files
4747 // rc=0 instead of erroring "unknown file attribute: z".
4748 // Mirror C's strict behavior — but skip ASCII whitespace
4749 // / `\0` (parser leftovers) and chars below 0x21 since
4750 // C's qualifier loop stops on those naturally via its
4751 // `case ')'` arm before reaching default.
4752 ch if !ch.is_whitespace() && ch != '\0' && ch != ')' => {
4753 crate::ported::utils::zerr(&format!("unknown file attribute: {}", ch));
4754 crate::ported::utils::errflag.fetch_or(
4755 crate::ported::utils::ERRFLAG_ERROR,
4756 std::sync::atomic::Ordering::Relaxed,
4757 );
4758 return qs;
4759 }
4760 _ => {}
4761 }
4762 }
4763
4764 if !qs.qualifiers.is_empty() {
4765 qs.alternatives.push(std::mem::take(&mut qs.qualifiers));
4766 }
4767
4768 qs.follow_links = follow;
4769
4770 // Build the faithful glob.c `struct qual` arena from `alternatives`
4771 // (additive: matching still uses the enum). Each alternative is an
4772 // AND-chain (`next` links); alternatives chain via `or`. ToggleSense
4773 // markers toggle a running per-node `sense` (c:1346). Each variant
4774 // maps to its leaf TestMatchFunc + data, mirroring check_single_
4775 // qualifier's dispatch (Src/glob.c:1343-1620 case → func/data).
4776 {
4777 let op_range = |o: char| -> i32 {
4778 match o {
4779 '<' => -1,
4780 '>' => 1,
4781 _ => 0,
4782 }
4783 };
4784 let mut arena = QualArena::new();
4785 let mut prev_alt_head: Option<usize> = None;
4786 for alt in &qs.alternatives {
4787 let mut sense = 0i32;
4788 let mut chain_tail: Option<usize> = None;
4789 let mut chain_head: Option<usize> = None;
4790 for q in alt {
4791 // (func, data, range, amc, units, sdata) — None = no node.
4792 let fields: Option<(TestMatchFunc, i64, i32, i32, i32, Option<String>)> = match q {
4793 qualifier::ToggleSense => {
4794 sense ^= 1; // c:1346
4795 None
4796 }
4797 qualifier::IsRegular => Some((qualisreg, 0, 0, 0, 0, None)),
4798 qualifier::IsDirectory => Some((qualisdir, 0, 0, 0, 0, None)),
4799 qualifier::IsSymlink => Some((qualislnk, 0, 0, 0, 0, None)),
4800 qualifier::IsSocket => Some((qualissock, 0, 0, 0, 0, None)),
4801 qualifier::IsFifo => Some((qualisfifo, 0, 0, 0, 0, None)),
4802 qualifier::IsBlockDev => Some((qualisblk, 0, 0, 0, 0, None)),
4803 qualifier::IsCharDev => Some((qualischr, 0, 0, 0, 0, None)),
4804 qualifier::IsDevice => Some((qualisdev, 0, 0, 0, 0, None)),
4805 qualifier::IsExecutable => Some((qualiscom, 0, 0, 0, 0, None)),
4806 qualifier::Readable => Some((qualflags, 0o400, 0, 0, 0, None)),
4807 qualifier::Writable => Some((qualflags, 0o200, 0, 0, 0, None)),
4808 qualifier::Executable => Some((qualflags, 0o100, 0, 0, 0, None)),
4809 qualifier::WorldReadable => Some((qualflags, 0o004, 0, 0, 0, None)),
4810 qualifier::WorldWritable => Some((qualflags, 0o002, 0, 0, 0, None)),
4811 qualifier::WorldExecutable => Some((qualflags, 0o001, 0, 0, 0, None)),
4812 qualifier::GroupReadable => Some((qualflags, 0o040, 0, 0, 0, None)),
4813 qualifier::GroupWritable => Some((qualflags, 0o020, 0, 0, 0, None)),
4814 qualifier::GroupExecutable => Some((qualflags, 0o010, 0, 0, 0, None)),
4815 qualifier::Setuid => Some((qualflags, 0o4000, 0, 0, 0, None)),
4816 qualifier::Setgid => Some((qualflags, 0o2000, 0, 0, 0, None)),
4817 qualifier::Sticky => Some((qualflags, 0o1000, 0, 0, 0, None)),
4818 qualifier::OwnedByEuid => {
4819 Some((qualuid, unsafe { libc::geteuid() } as i64, 0, 0, 0, None))
4820 }
4821 qualifier::OwnedByEgid => {
4822 Some((qualgid, unsafe { libc::getegid() } as i64, 0, 0, 0, None))
4823 }
4824 qualifier::OwnedByUid(u) => Some((qualuid, *u as i64, 0, 0, 0, None)),
4825 qualifier::OwnedByGid(g) => Some((qualgid, *g as i64, 0, 0, 0, None)),
4826 qualifier::Size { value, unit, op } => {
4827 Some((qualsize, *value as i64, op_range(*op), -1, *unit, None))
4828 }
4829 qualifier::Links { value, op } => {
4830 Some((qualnlink, *value as i64, op_range(*op), 0, 0, None))
4831 }
4832 qualifier::Atime { value, unit, op } => {
4833 Some((qualtime, *value, op_range(*op), 0, *unit, None))
4834 }
4835 qualifier::Mtime { value, unit, op } => {
4836 Some((qualtime, *value, op_range(*op), 1, *unit, None))
4837 }
4838 qualifier::Ctime { value, unit, op } => {
4839 Some((qualtime, *value, op_range(*op), 2, *unit, None))
4840 }
4841 qualifier::Mode { yes, no } => Some((
4842 qualmodeflags,
4843 (*yes as i64) | ((*no as i64) << 12),
4844 0,
4845 0,
4846 0,
4847 None,
4848 )),
4849 qualifier::Device(d) => Some((qualdev, *d as i64, 0, 0, 0, None)),
4850 qualifier::NonEmptyDir => Some((qualnonemptydir, 0, 0, 0, 0, None)),
4851 qualifier::Eval(code) => Some((qualsheval, 0, 0, 0, 0, Some(code.clone()))),
4852 };
4853 let Some((func, data, range, amc, units, sdata)) = fields else {
4854 continue;
4855 };
4856 let idx = arena.alloc(); // c:1768 hcalloc
4857 {
4858 let node = &mut arena.nodes[idx];
4859 node.func = Some(func); // c:1774
4860 node.data = data; // c:1776
4861 node.sense = sense; // c:1775
4862 node.range = range; // c:1778
4863 node.units = units; // c:1779
4864 node.amc = amc; // c:1780
4865 node.sdata = sdata; // c:1777
4866 }
4867 if let Some(t) = chain_tail {
4868 arena.nodes[t].next = Some(idx); // c:1770
4869 }
4870 chain_tail = Some(idx);
4871 if chain_head.is_none() {
4872 chain_head = Some(idx);
4873 }
4874 }
4875 // Link this alternative's head into the or-chain (c:1324).
4876 if let Some(h) = chain_head {
4877 if let Some(p) = prev_alt_head {
4878 arena.nodes[p].or = Some(h);
4879 }
4880 prev_alt_head = Some(h);
4881 if arena.head.is_none() {
4882 arena.head = Some(h);
4883 }
4884 }
4885 }
4886 qs.quals = arena;
4887 }
4888
4889 qs
4890}
4891
4892/// Parse a numeric uid/gid or a delimited user/group name from a `u`/`g`
4893/// qualifier char stream. Port of the name-resolution arms of the `u`/`g`
4894/// qualifier cases at `Src/glob.c:1468-1535`:
4895/// `if (idigit(*s)) data = qgetnum(&s);` else `get_strarg`-delimited name
4896/// resolved through `getpwnam(3)` / `getgrnam(3)`. On an unknown name it
4897/// emits the C diagnostic (`zerr` sets errflag) and returns 0, matching
4898/// C's `data = 0;` arm. `is_group` selects getgrnam over getpwnam.
4899fn parse_uid_gid(chars: &mut std::iter::Peekable<std::str::Chars>, is_group: bool) -> u32 {
4900 // c:1468/1511 — `if (idigit(*s)) data = qgetnum(&s);`
4901 if chars.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
4902 let mut num = String::new();
4903 while let Some(&c) = chars.peek() {
4904 if c.is_ascii_digit() {
4905 num.push(c);
4906 chars.next();
4907 } else {
4908 break;
4909 }
4910 }
4911 return num.parse().unwrap_or(0);
4912 }
4913 // c:1471-1505 — `tt = get_strarg(s, &arglen);` the char right after the
4914 // qualifier is the delimiter; the name runs to the next delimiter.
4915 let delim = match chars.next() {
4916 Some(d) => d,
4917 None => {
4918 // c:1481/1524 — `zerr("missing delimiter for 'u'/'g' glob qualifier")`.
4919 zerr(if is_group {
4920 "missing delimiter for 'g' glob qualifier"
4921 } else {
4922 "missing delimiter for 'u' glob qualifier"
4923 });
4924 return 0;
4925 }
4926 };
4927 let mut name = String::new();
4928 for c in chars.by_ref() {
4929 if c == delim {
4930 break;
4931 }
4932 name.push(c);
4933 }
4934 // c:1488/1530 — resolve via getpwnam(3) / getgrnam(3).
4935 let cstr = match std::ffi::CString::new(name.as_bytes()) {
4936 Ok(c) => c,
4937 Err(_) => return 0,
4938 };
4939 unsafe {
4940 if is_group {
4941 let gr = libc::getgrnam(cstr.as_ptr()); // c:1530
4942 if !gr.is_null() {
4943 return (*gr).gr_gid; // c:1531
4944 }
4945 zerr("unknown group"); // c:1533
4946 } else {
4947 let pw = libc::getpwnam(cstr.as_ptr()); // c:1488
4948 if !pw.is_null() {
4949 return (*pw).pw_uid; // c:1489
4950 }
4951 // c:1491 — `zerr("unknown username '%s'", s + arglen);`
4952 zerr(&format!("unknown username '{}'", name));
4953 }
4954 }
4955 // c:1493/1535 — `data = 0;` after the unknown-name diagnostic.
4956 0
4957}
4958
4959/// Parse the unit/op/value tail of an `(L...)` size qualifier.
4960/// **RUST-ONLY** — C parses inline in parsepat; the size-unit
4961/// constants are `TT_BYTES`/`TT_KILOBYTES`/… at glob.c:128-133.
4962fn parse_size_spec(
4963 // RUST-ONLY
4964 chars: &mut std::iter::Peekable<std::str::Chars>,
4965) -> (i32, char, u64) {
4966 let unit: i32 = match chars.peek() {
4967 Some('p') | Some('P') => {
4968 chars.next();
4969 TT_POSIX_BLOCKS
4970 }
4971 Some('k') | Some('K') => {
4972 chars.next();
4973 TT_KILOBYTES
4974 }
4975 Some('m') | Some('M') => {
4976 chars.next();
4977 TT_MEGABYTES
4978 }
4979 Some('g') | Some('G') => {
4980 chars.next();
4981 TT_GIGABYTES
4982 }
4983 Some('t') | Some('T') => {
4984 chars.next();
4985 TT_TERABYTES
4986 }
4987 _ => TT_BYTES,
4988 };
4989 let (op, val) = parse_range_spec(chars);
4990 (unit, op, val)
4991}
4992
4993/// Parse the unit/op/value tail of an `(a/m/c...)` time qualifier.
4994/// **RUST-ONLY** — C parses inline in parsepat; time-unit constants
4995/// are `TT_SECONDS`/`TT_MINS`/… at glob.c:121-126.
4996fn schedgetfn(
4997 // RUST-ONLY (clashes with sched.c:341 name, unrelated fn)
4998 chars: &mut std::iter::Peekable<std::str::Chars>,
4999) -> (i32, char, u64) {
5000 let unit: i32 = match chars.peek() {
5001 Some('s') => {
5002 chars.next();
5003 TT_SECONDS
5004 }
5005 Some('m') => {
5006 chars.next();
5007 TT_MINS
5008 }
5009 Some('h') => {
5010 chars.next();
5011 TT_HOURS
5012 }
5013 Some('d') => {
5014 chars.next();
5015 TT_DAYS
5016 }
5017 Some('w') => {
5018 chars.next();
5019 TT_WEEKS
5020 }
5021 Some('M') => {
5022 chars.next();
5023 TT_MONTHS
5024 }
5025 _ => TT_DAYS,
5026 };
5027 let (op, val) = parse_range_spec(chars);
5028 (unit, op, val)
5029}
5030
5031/// Parse `[+-]?NUMBER` operator+value tail. Mirrors C qgetnum
5032/// (glob.c:827) which returns the int value; here we return the
5033/// operator char along with the value since callers need both.
5034fn parse_range_spec(chars: &mut std::iter::Peekable<std::str::Chars>) -> (char, u64) {
5035 // RUST-ONLY
5036 // C's qgetnum at glob.c:827 returns the operator char inline.
5037 // `+N` = greater, `-N` = less, bare digits = equal.
5038 let op: char = match chars.peek() {
5039 Some('+') => {
5040 chars.next();
5041 '>'
5042 }
5043 Some('-') => {
5044 chars.next();
5045 '<'
5046 }
5047 _ => '=',
5048 };
5049 let mut num = String::new();
5050 while let Some(&c) = chars.peek() {
5051 if c.is_ascii_digit() {
5052 num.push(c);
5053 chars.next();
5054 } else {
5055 break;
5056 }
5057 }
5058 // c:Src/glob.c:826-834 — `qgetnum` errors with "number expected"
5059 // when not followed by `idigit(**s)`. Previously the Rust parser
5060 // silently treated the missing number as 0 and let the outer
5061 // qualifier loop continue consuming bytes as fresh qualifier
5062 // letters — `*(Lr)` parsed as `L0 + r` (readable, size=0) and
5063 // dropped to the no-matches path. Setting errflag here propagates
5064 // the canonical error message and gates the "no matches found"
5065 // emission in zglob (c:1843-1854 `if (errflag) ... return`).
5066 if num.is_empty() {
5067 crate::ported::utils::zerr("number expected"); // c:832
5068 crate::ported::utils::errflag.fetch_or(
5069 crate::ported::zsh_h::ERRFLAG_ERROR,
5070 std::sync::atomic::Ordering::SeqCst,
5071 );
5072 return (op, 0); // c:833
5073 }
5074 let val = num.parse().unwrap_or(0);
5075 (op, val)
5076}
5077
5078/// Parse `[FIRST,LAST]` subscript range. **RUST-ONLY**.
5079fn parse_subscript(
5080 // RUST-ONLY
5081 chars: &mut std::iter::Peekable<std::str::Chars>,
5082) -> (Option<i32>, Option<i32>) {
5083 let mut first_str = String::new();
5084 let mut last_str = String::new();
5085 let mut in_last = false;
5086
5087 while let Some(&c) = chars.peek() {
5088 chars.next();
5089 if c == ']' {
5090 break;
5091 } else if c == ',' {
5092 in_last = true;
5093 } else if in_last {
5094 last_str.push(c);
5095 } else {
5096 first_str.push(c);
5097 }
5098 }
5099
5100 let first = first_str.parse().ok();
5101 let last = if in_last {
5102 last_str.parse().ok()
5103 } else {
5104 first
5105 };
5106 (first, last)
5107}
5108
5109/// Port of `static void insert(char *s, int checked)` from `Src/glob.c:346`.
5110/// Records one matched path into the glob result list: runs the qualifier
5111/// walk (`check_qualifiers` = the c:381-419 `struct qual` walk over the
5112/// arena) and, if accepted, builds the `gmatch` entry from the file's
5113/// stat and appends it (c:421-490) — honoring any `$reply`/`$REPLY`
5114/// replacement (INSERTS, c:428) set by an `(e:…:)` eval qualifier.
5115/// Called by the scanner per matched name, mirroring glob.c:576/668.
5116pub fn insert(state: &mut globdata, s: &Path, checked: i32) {
5117 use std::os::unix::fs::MetadataExt;
5118 let _ = checked; // c:347 already-stat'd hint; check_qualifiers re-stats.
5119 // c:381-419 — qualifier walk; reject the file if it fails.
5120 if !check_qualifiers(state, s) {
5121 return;
5122 }
5123 // c:421-490 — build + append the match entry. Symlink targets get
5124 // their own stat for the GS_LINKED (follow-link) sort variants.
5125 let Ok(meta) = fs::symlink_metadata(s) else {
5126 return;
5127 };
5128 let name = s
5129 .file_name()
5130 .map(|n| n.to_string_lossy().to_string())
5131 .unwrap_or_default();
5132 let (tsize, tatime, tmtime, tctime, tlinks, tansec, tmnsec, tcnsec) =
5133 if meta.file_type().is_symlink() {
5134 if let Ok(tm) = fs::metadata(s) {
5135 (
5136 tm.size(),
5137 tm.atime(),
5138 tm.mtime(),
5139 tm.ctime(),
5140 tm.nlink(),
5141 tm.atime_nsec(),
5142 tm.mtime_nsec(),
5143 tm.ctime_nsec(),
5144 )
5145 } else {
5146 (
5147 meta.size(),
5148 meta.atime(),
5149 meta.mtime(),
5150 meta.ctime(),
5151 meta.nlink(),
5152 meta.atime_nsec(),
5153 meta.mtime_nsec(),
5154 meta.ctime_nsec(),
5155 )
5156 }
5157 } else {
5158 (
5159 meta.size(),
5160 meta.atime(),
5161 meta.mtime(),
5162 meta.ctime(),
5163 meta.nlink(),
5164 meta.atime_nsec(),
5165 meta.mtime_nsec(),
5166 meta.ctime_nsec(),
5167 )
5168 };
5169 // c:428 — `while (!inserts || (news = *inserts++))`: an (e:…:) eval
5170 // may have set $reply/$REPLY (INSERTS), replacing this match with
5171 // zero-or-more names; otherwise emit the original name once. The
5172 // synthetic path makes glob_emit_path yield the replacement string;
5173 // the stat fields stay those of the matched file (C keeps
5174 // matchptr->size etc. from buf).
5175 let emit: Vec<(String, PathBuf)> = match INSERTS.lock().unwrap().take() {
5176 Some(list) => list
5177 .into_iter()
5178 .map(|n| (n.clone(), PathBuf::from(&n)))
5179 .collect(),
5180 None => vec![(name.clone(), s.to_path_buf())],
5181 };
5182 for (nm, pth) in emit {
5183 state.matches.push(gmatch {
5184 name: nm,
5185 path: pth,
5186 size: meta.size(),
5187 atime: meta.atime(),
5188 mtime: meta.mtime(),
5189 ctime: meta.ctime(),
5190 links: meta.nlink(),
5191 mode: meta.mode(),
5192 uid: meta.uid(),
5193 gid: meta.gid(),
5194 dev: meta.dev(),
5195 ino: meta.ino(),
5196 target_size: tsize,
5197 target_atime: tatime,
5198 target_mtime: tmtime,
5199 target_ctime: tctime,
5200 target_links: tlinks,
5201 ansec: meta.atime_nsec(),
5202 mnsec: meta.mtime_nsec(),
5203 cnsec: meta.ctime_nsec(),
5204 target_ansec: tansec,
5205 target_mnsec: tmnsec,
5206 target_cnsec: tcnsec,
5207 sort_strings: Vec::new(),
5208 });
5209 state.matchct += 1; // c:479 matchptr++
5210 }
5211}
5212
5213/// Drive the OR-of-AND qualifier filter against `path`. **RUST-ONLY**
5214/// — C glob.c does qualifier eval inline inside `insert()` (c:381+).
5215fn check_qualifiers(state: &globdata, path: &Path) -> bool {
5216 use std::os::unix::fs::MetadataExt;
5217 use std::sync::atomic::Ordering;
5218 // c:353 — `inserts = NULL;` at insert() entry: clear any reply/REPLY
5219 // replacement left by a previous file's (e:…:) eval before this walk.
5220 *INSERTS.lock().unwrap() = None;
5221 let qs = match &state.qualifiers {
5222 Some(q) => q,
5223 None => return true,
5224 };
5225 // No test qualifiers (only flags/sort/words) → no filtering.
5226 let arena = &qs.quals;
5227 if arena.head.is_none() {
5228 return true;
5229 }
5230
5231 let meta = match if qs.follow_links {
5232 // c:399-402 — `if (!S_ISLNK(buf.st_mode) || statfullpath(s,&buf2,0))
5233 // memcpy(&buf2, &buf, sizeof(buf));`
5234 // Follow the link, but fall back to the lstat buffer when the
5235 // target stat fails (a broken symlink) so `*(-@)` / `*(-^/)`
5236 // still see the dangling link rather than dropping it.
5237 fs::metadata(path).or_else(|_| fs::symlink_metadata(path))
5238 } else {
5239 fs::symlink_metadata(path)
5240 } {
5241 Ok(m) => m,
5242 Err(_) => return false,
5243 };
5244 // Bridge meta → libc::stat for the leaf test fns (no extra syscall).
5245 let mut st: libc::stat = unsafe { std::mem::zeroed() };
5246 st.st_mode = meta.mode() as _;
5247 st.st_uid = meta.uid() as _;
5248 st.st_gid = meta.gid() as _;
5249 st.st_dev = meta.dev() as _;
5250 st.st_nlink = meta.nlink() as _;
5251 st.st_size = meta.size() as _;
5252 st.st_atime = meta.atime() as _;
5253 st.st_mtime = meta.mtime() as _;
5254 st.st_ctime = meta.ctime() as _;
5255 // qualsheval / qualnonemptydir need the name (REPLY / opendir).
5256 let name = glob_emit_path(path);
5257
5258 // c:387-419 — insert()'s qual-walk over the `struct qual` arena: AND
5259 // via `next`, OR (alternatives) via `or`, per-node `sense`. `qo` is
5260 // the current alternative head; a rejected node advances to `qo->or`
5261 // (next alternative) or, if none, rejects the file. Falling off the
5262 // `next` chain (or hitting a func-less terminator) accepts.
5263 let mut qo = arena.head;
5264 let mut qn = qo;
5265 loop {
5266 let Some(qn_i) = qn else { return true }; // c:419 — chain matched
5267 let (func, data, sense, range, amc, units, sdata, next) = {
5268 let n = &arena.nodes[qn_i];
5269 (
5270 n.func,
5271 n.data,
5272 n.sense,
5273 n.range,
5274 n.amc,
5275 n.units,
5276 n.sdata.clone(),
5277 n.next,
5278 )
5279 };
5280 let func = match func {
5281 Some(f) => f,
5282 None => return true, // c:392 — `qn && qn->func` terminator
5283 };
5284 // c:393-395 — set the scratch the range/time/size leaf fns read.
5285 g_range.store(range, Ordering::SeqCst);
5286 g_amc.store(amc, Ordering::SeqCst);
5287 g_units.store(units, Ordering::SeqCst);
5288 let r = func(&name, &st, data, sdata.as_deref().unwrap_or(""));
5289 // c:407-409 — reject if `(!(func()) ^ sense) & 1`.
5290 let reject = ((if r == 0 { 1 } else { 0 }) ^ (sense & 1)) & 1 == 1;
5291 if reject {
5292 // c:411-415 — try the next alternative, else reject the file.
5293 match arena.nodes[qo.unwrap()].or {
5294 Some(or_i) => {
5295 qo = Some(or_i);
5296 qn = Some(or_i);
5297 }
5298 None => return false,
5299 }
5300 } else {
5301 qn = next; // c:417
5302 }
5303 }
5304}
5305
5306/// Sort the per-state matches per the qualifier `o`/`O` keys.
5307/// **RUST-ONLY** — C does this inline at the end of `scanner()`
5308/// (glob.c:1700ish, qsort with `gmatchcmp` comparator).
5309fn sort_matches(state: &mut globdata) {
5310 // RUST-ONLY
5311 // Default sort is GS_NAME ascending (c:204 gf_sortlist
5312 // initial setup). Per-qualifier `o<key>` / `O<key>` overrides.
5313 // c:1855-1857 — when gf_nsorts == 0 (no explicit o/O qualifier),
5314 // glob.c falls back to GS_NAME (or GS_NONE under shortcircuit).
5315 // Mirror that here so a qualifier set carrying ONLY non-sort
5316 // qualifiers (`Lk+0`, etc.) still sorts alphabetically.
5317 // c:1855-1857 is a CONDITIONAL default, not a fixed one:
5318 // if (!gf_nsorts) {
5319 // gf_sortlist[0].tp = gf_sorts = (shortcircuit ? GS_NONE : GS_NAME);
5320 // gf_nsorts = 1;
5321 // }
5322 // A `Y` limit means "give me the first N the scanner finds", so with no
5323 // explicit o/O spec the default becomes GS_NONE and the results stay in
5324 // SCAN order — sorting them would defeat the short-circuit, since the first
5325 // N by name are not the first N found. This always chose GS_NAME, so a
5326 // limited glob returned the alphabetically-first N instead of the
5327 // scan-order-first N, and even `*(Y99)` (a limit larger than the match
5328 // count, which discards nothing) came back sorted where zsh leaves it in
5329 // readdir order.
5330 //
5331 // `Y0` is the counter-case and must still sort: c:1583 parses the argument
5332 // over `shortcircuit`, so 0 lands there legitimately and c:518's
5333 // `if (shortcircuit && …)` reads it as "no limit" — hence the `> 0` test
5334 // rather than `.is_some()`.
5335 let limited = state
5336 .qualifiers
5337 .as_ref()
5338 .and_then(|q| q.short_circuit)
5339 .is_some_and(|n| n > 0); // c:1856 `shortcircuit ?`
5340 let default_spec = if limited { GS_NONE } else { GS_NAME }; // c:1856
5341 let mut specs: Vec<i32> = state
5342 .qualifiers
5343 .as_ref()
5344 .map(|q| {
5345 if q.sorts.is_empty() {
5346 vec![default_spec] // c:1856
5347 } else {
5348 q.sorts.clone()
5349 }
5350 })
5351 .unwrap_or_else(|| vec![GS_NAME]);
5352
5353 // GS_NONE marker — caller wants no sort at all.
5354 if specs.iter().any(|&tp| (tp & GS_NONE) != 0) {
5355 return;
5356 }
5357
5358 // c:936 gmatchcmp returns 0 when every sort key ties. In C the final
5359 // order of equal-key matches is then whatever the libc `qsort` does with
5360 // all-equal elements (glob.c:1976) — NON-stable on macOS/BSD (arbitrary,
5361 // input-dependent: `*(oL)` on three 0-byte files a/b/d prints `d a b`),
5362 // and on glibc it depends on readdir order. So zsh has NO portable,
5363 // defined tie-break here. zshrs is a cross-architecture shell and must be
5364 // DETERMINISTIC, so we impose an explicit GS_NAME final tie-breaker:
5365 // equal-key matches order by name ascending, identical on every platform.
5366 // This is an intentional determinism guarantee, NOT a bug-for-bug match of
5367 // zsh's qsort-defined order (which can't be reproduced portably). Skip
5368 // when the last key is already GS_NAME (the no-qualifier default, or an
5369 // explicit trailing `on`) so we don't double-compare.
5370 if specs.last().map_or(true, |&last| (last & !GS_DESC) != GS_NAME) {
5371 specs.push(GS_NAME); // deterministic name-ascending tie-break
5372 }
5373
5374 // c:1258/1575 — gf_numsort starts at the global NUMERIC_GLOB_SORT
5375 // option, then a per-glob `(n)`/`(^n)` qualifier OVERRIDES it
5376 // absolutely (so `(^n)` forces lexical even when the option is on).
5377 let numeric = state
5378 .qualifiers
5379 .as_ref()
5380 .and_then(|q| q.numsort)
5381 .unwrap_or_else(|| glob_isset(NUMERICGLOBSORT));
5382 state
5383 .matches
5384 .sort_by(|a, b| gmatchcmp(a, b, &specs, numeric));
5385}
5386
5387/// Apply `[FIRST,LAST]` qualifier subscript on the match list.
5388/// **RUST-ONLY** — C uses `gd_pre_first` / `gd_first` index tracking
5389/// during scanner emit; here we slice after the full walk.
5390fn apply_selection(state: &mut globdata) {
5391 // RUST-ONLY
5392 let (first, last, short_circuit) = match &state.qualifiers {
5393 Some(q) => (q.first, q.last, q.short_circuit),
5394 None => return,
5395 };
5396
5397 let len = state.matches.len() as i32;
5398 if len == 0 {
5399 return;
5400 }
5401
5402 let start = match first {
5403 Some(f) if f < 0 => (len + f).max(0) as usize,
5404 Some(f) => (f - 1).max(0) as usize,
5405 None => 0,
5406 };
5407
5408 let end = match last {
5409 Some(l) if l < 0 => (len + l + 1).max(0) as usize,
5410 Some(l) => l.min(len) as usize,
5411 None => len as usize,
5412 };
5413
5414 if start < end && start < state.matches.len() {
5415 state.matches = state.matches[start..end.min(state.matches.len())].to_vec();
5416 } else {
5417 state.matches.clear();
5418 }
5419
5420 // c:Src/glob.c:1579-1595 `case 'Y'` — apply the short-circuit
5421 // limit after the [first,last] slice (so `(Y3[2,4])` would yield
5422 // up to 3 from the [2,4] subscript range). C truncates DURING
5423 // the scanner walk via `if (shortcircuit == matchct) break;` —
5424 // here we slice post-walk which produces the same set on the
5425 // common short-glob case where the scanner doesn't recurse
5426 // unboundedly. Bug #41 in docs/BUGS.md.
5427 // NOTE: the `Y` short-circuit is deliberately NOT applied here. It bounds
5428 // the SCAN (c:Src/glob.c:518), so it must run BEFORE the sort — see the
5429 // truncation at the sort_matches call site. Only the `[first,last]`
5430 // subscript belongs in this post-sort selection.
5431 let _ = short_circuit;
5432}
5433
5434// `haswilds()` is defined in `Src/pattern.c:4306`, not glob.c — the
5435// canonical Rust port lives at `crate::ported::pattern::haswilds`.
5436// This file previously carried a divergent re-implementation tracking
5437// bracket/escape state instead of the token-aware `zpc_disables[]` /
5438// SHGLOB / KSHGLOB / EXTENDEDGLOB checks the C source actually does.
5439// Drift deleted; glob.rs callers route through the canonical port.
5440
5441#[cfg(test)]
5442mod gs_tt_tests {
5443 use super::*;
5444
5445 #[test]
5446 fn gs_size_offset_matches_c() {
5447 let _g = crate::test_util::global_state_lock();
5448 // c:83 — GS_SIZE = GS_SHIFT_BASE = 8.
5449 assert_eq!(GS_SIZE, 8);
5450 // c:84 — GS_ATIME = GS_SHIFT_BASE << 1 = 16.
5451 assert_eq!(GS_ATIME, 16);
5452 // c:87 — GS_LINKS = GS_SHIFT_BASE << 4 = 128.
5453 assert_eq!(GS_LINKS, 128);
5454 }
5455
5456 #[test]
5457 fn gs_normal_covers_all_size_keys() {
5458 let _g = crate::test_util::global_state_lock();
5459 // c:99 — GS_NORMAL = SIZE | ATIME | MTIME | CTIME | LINKS.
5460 assert_ne!(GS_NORMAL & GS_SIZE, 0);
5461 assert_ne!(GS_NORMAL & GS_ATIME, 0);
5462 assert_ne!(GS_NORMAL & GS_MTIME, 0);
5463 assert_ne!(GS_NORMAL & GS_CTIME, 0);
5464 assert_ne!(GS_NORMAL & GS_LINKS, 0);
5465 }
5466
5467 #[test]
5468 fn tt_namespaces_share_indices() {
5469 let _g = crate::test_util::global_state_lock();
5470 // c:121-126 vs c:128-133 — TT_DAYS == TT_BYTES == 0, etc.
5471 assert_eq!(TT_DAYS, TT_BYTES);
5472 assert_eq!(TT_HOURS, TT_POSIX_BLOCKS);
5473 assert_eq!(TT_MINS, TT_KILOBYTES);
5474 assert_eq!(TT_WEEKS, TT_MEGABYTES);
5475 assert_eq!(TT_MONTHS, TT_GIGABYTES);
5476 assert_eq!(TT_SECONDS, TT_TERABYTES);
5477 }
5478
5479 #[test]
5480 fn max_sorts_is_12() {
5481 let _g = crate::test_util::global_state_lock();
5482 assert_eq!(MAX_SORTS, 12);
5483 }
5484}
5485
5486fn expand_range(
5487 prefix: &str,
5488 content: &str,
5489 dotdot_pos: usize,
5490 suffix: &str,
5491) -> Option<Vec<String>> {
5492 // c:Src/glob.c::xpandbraces parses brace-range endpoints via
5493 // zstrtol, which is TOKEN-aware and treats Dash TOKEN (\u{9b})
5494 // as ASCII `-`. Rust's i64::parse only accepts ASCII, so
5495 // untokenize the content here before splitting/parsing — matches
5496 // C semantics without weakening the strict-TOKEN gates above.
5497 let owned: String;
5498 let content: &str = if content.chars().any(|c| {
5499 let cu = c as u32;
5500 (0x84..=0xa1).contains(&cu) && c != '\u{8f}' && c != '\u{90}' && c != '\u{9a}'
5501 }) {
5502 owned = crate::lex::untokenize(content);
5503 &owned
5504 } else {
5505 content
5506 };
5507 // `dotdot_pos` is a CHAR index (xpandbraces counts it over a char vector,
5508 // `i - start - 1`), but the slicing below is byte-based. For ASCII content
5509 // they coincide; for content holding multibyte/metafied chars (e.g.
5510 // `{$'\x80'..$'\x81'}`, where the bytes are Meta-escaped) a raw byte slice
5511 // at the char index lands mid-char and panics. Convert to a byte offset.
5512 let dotdot_byte = content
5513 .char_indices()
5514 .nth(dotdot_pos)
5515 .map(|(b, _)| b)
5516 .unwrap_or(content.len());
5517 let left = &content[..dotdot_byte];
5518 let right_start = dotdot_byte + 2; // ".." is two ASCII bytes
5519
5520 // Check for second `..` for `{N..M..S}` step form. Step may be
5521 // signed: negative-step REVERSES the natural direction sequence
5522 // per zsh's brace expansion (Src/glob.c::xpandbraces recursive
5523 // iteration with sign tracking). Examples:
5524 // {1..32..3} → 1,4,7,…,31 (natural ascending)
5525 // {1..32..-3} → 31,28,…,1 (same set, reversed)
5526 // {32..1..3} → 32,29,…,2 (natural descending)
5527 // {32..1..-3} → 2,5,…,32 (same set, reversed)
5528 //
5529 // c:Src/glob.c:2365-2369 — `if (dotdot == 2 && *p == '.' &&`
5530 // ` p[1] == '.') {`
5531 // ` rincr = zstrtol(p+2, &p, 10);`
5532 // ` wid3 = p - dots2 - 2;`
5533 // ` if (p != str2 || !rincr)`
5534 // ` err++;`
5535 // The `!rincr` test (c:2368) rejects a zero step: when err > 0,
5536 // the C code falls past the c:2374 `if (!err)` block, so the
5537 // brace expansion is abandoned and the literal pattern is
5538 // preserved. Parity bug #15: previously the Rust port silently
5539 // clamped step to 1 via `.max(1)`, producing `1 2 3 4 5` for
5540 // `{1..5..0}` instead of zsh's literal `1..5..0`.
5541 let (right, incr_abs, incr_sign_negative, step_text) =
5542 if let Some(pos) = content[right_start..].find("..") {
5543 let r = &content[right_start..right_start + pos];
5544 let s_text = &content[right_start + pos + 2..];
5545 let raw: i64 = s_text.parse().unwrap_or(1);
5546 if raw == 0 {
5547 // c:2368 — `!rincr` → err++ → no expansion. Return
5548 // None so xpandbraces falls back to literal.
5549 return None;
5550 }
5551 (r, raw.unsigned_abs(), raw < 0, s_text)
5552 } else {
5553 (&content[right_start..], 1u64, false, "")
5554 };
5555
5556 // Try numeric range
5557 if let (Ok(start), Ok(end)) = (left.parse::<i64>(), right.parse::<i64>()) {
5558 let mut results = Vec::new();
5559
5560 // Iterate from `start` toward `end` with abs(step). Sign of
5561 // start/end relative to each other determines natural
5562 // direction; step is always |step|.
5563 let step = incr_abs.max(1) as i64;
5564 let mut vals: Vec<i64> = Vec::new();
5565 if start <= end {
5566 let mut v = start;
5567 while v <= end {
5568 vals.push(v);
5569 v += step;
5570 }
5571 } else {
5572 let mut v = start;
5573 while v >= end {
5574 vals.push(v);
5575 v -= step;
5576 }
5577 }
5578 if incr_sign_negative {
5579 vals.reverse();
5580 }
5581
5582 // Padding: zsh pads with leading zeros when ANY of the three
5583 // textual fields (left endpoint, right endpoint, step) has a
5584 // leading zero after stripping the optional sign. Width is
5585 // the max textual width across left/right/step. For negative
5586 // values, the sign prefix counts toward width — we emit `-`
5587 // then zero-pad the remaining digits (`-02`, not `0-2`).
5588 // Direct port of Src/lex.c::dobrace_pad logic which detects
5589 // pad mode per the textual form of all three fields.
5590 // zsh's pad-detection: pad fires when any of the three
5591 // textual fields (left / right / step) is a multi-digit
5592 // form with a leading zero (e.g. "01", "00", "003"), with
5593 // ONE exception: when the LEFT endpoint is exactly "0"
5594 // (bare single zero, possibly signed), pad is suppressed
5595 // regardless of right's form. Empirical zsh:
5596 // {0..-5} → 0 -1 -2 -3 -4 -5 (left=0 → no pad)
5597 // {0..03} → 0 1 2 3 (left=0 → no pad)
5598 // {00..-5} → 00 -1 -2 -3 -4 -5 (left=00 → pad w=2)
5599 // {1..03} → 01 02 03 (right=03 → pad w=2)
5600 // {1..-03} → 001 000 -01 -02 -03 (right=-03 → pad w=3)
5601 let lstrip = left.trim_start_matches(['+', '-']);
5602 let rstrip = right.trim_start_matches(['+', '-']);
5603 let sstrip = step_text.trim_start_matches(['+', '-']);
5604 let is_padded_field = |stripped: &str| stripped.len() >= 2 && stripped.starts_with('0');
5605 // Suppress pad ONLY when LEFT is exactly the single-char "0"
5606 // (no sign, no extra digits). "-0" or "00" both pad.
5607 let left_is_bare_zero = left == "0";
5608 let pad = !left_is_bare_zero
5609 && (is_padded_field(lstrip)
5610 || is_padded_field(rstrip)
5611 || (!step_text.is_empty() && is_padded_field(sstrip)));
5612 let width = left.len().max(right.len()).max(step_text.len());
5613
5614 for v in vals {
5615 let formatted = if pad {
5616 if v < 0 {
5617 let abs = (-v).to_string();
5618 let inner_w = width.saturating_sub(1);
5619 format!("-{:0>w$}", abs, w = inner_w)
5620 } else {
5621 format!("{:0>w$}", v, w = width)
5622 }
5623 } else {
5624 v.to_string()
5625 };
5626 results.push(format!("{}{}{}", prefix, formatted, suffix));
5627 }
5628 return Some(results);
5629 }
5630
5631 // Try character range
5632 // c:Src/lex.c::dobrace alpha-range path — only handles bare a..z,
5633 // not a..z..N. zsh emits literal `{a..z..3}` because the step
5634 // form is numeric-only. Detect step_text presence on the char-
5635 // range arm and refuse to expand so the literal survives.
5636 if left.len() == 1 && right.len() == 1 && step_text.is_empty() {
5637 let start = left.chars().next()?;
5638 let end = right.chars().next()?;
5639 let (start, end, reverse) = if start <= end {
5640 (start, end, false)
5641 } else {
5642 (end, start, true)
5643 };
5644
5645 let mut results = Vec::new();
5646 let mut chars: Vec<char> = (start..=end).collect();
5647 if reverse {
5648 chars.reverse();
5649 }
5650
5651 for c in chars {
5652 results.push(format!("{}{}{}", prefix, c, suffix));
5653 }
5654 return Some(results);
5655 }
5656
5657 None
5658}
5659
5660fn expand_comma(
5661 prefix: &str,
5662 content: &str,
5663 positions: &[usize],
5664 suffix: &str,
5665) -> Option<Vec<String>> {
5666 // positions are CHAR indices into `content` (per the xpandbraces
5667 // walker above that builds them from `chars: Vec<char>`). Slice
5668 // by char-index, not byte-index — `&content[last..pos]` would
5669 // panic on multi-byte token chars like Comma TOKEN (`\u{9a}`,
5670 // 2 UTF-8 bytes) and Inbrace (`\u{8f}`, 2 bytes).
5671 let chars: Vec<char> = content.chars().collect();
5672 let mut results = Vec::new();
5673 let mut last: usize = 0;
5674 for &pos in positions {
5675 let part: String = chars[last..pos].iter().collect();
5676 results.push(format!("{}{}{}", prefix, part, suffix));
5677 last = pos + 1;
5678 }
5679 let tail: String = chars[last..].iter().collect();
5680 results.push(format!("{}{}{}", prefix, tail, suffix));
5681 Some(results)
5682}
5683
5684fn expand_ccl(prefix: &str, content: &str, suffix: &str) -> Option<Vec<String>> {
5685 // c:Src/glob.c:expand_ccl — char-class range expansion for the
5686 // `setopt braceccl` brace shape `{m-o}` → m,n,o. Accept both
5687 // ASCII `-` and Dash TOKEN (\u{9b}) as the range separator —
5688 // the bridge passthru path delivers `{m-o}` with Dash TOKEN
5689 // since the lexer tokenizes `-` to Dash inside word context.
5690 let mut chars_set = HashSet::new();
5691 let chars: Vec<char> = content.chars().collect();
5692 let mut i = 0;
5693
5694 while i < chars.len() {
5695 let is_range = i + 2 < chars.len() && (chars[i + 1] == '-' || chars[i + 1] == '\u{9b}');
5696 if is_range {
5697 let start = chars[i];
5698 let end = chars[i + 2];
5699 for c in start..=end {
5700 chars_set.insert(c);
5701 }
5702 i += 3;
5703 } else {
5704 chars_set.insert(chars[i]);
5705 i += 1;
5706 }
5707 }
5708
5709 let mut results: Vec<String> = chars_set
5710 .iter()
5711 .map(|c| format!("{}{}{}", prefix, c, suffix))
5712 .collect();
5713 results.sort();
5714 Some(results)
5715}
5716
5717// ============================================================================
5718// Convenience functions
5719// ============================================================================
5720
5721/// Split a pattern at its trailing zsh-style qualifier suffix. Returns
5722/// `(pattern_without_qualifier, qualifier_inner)` — the inner is the bytes
5723/// between the matching parens, without the surrounding `()` and without
5724/// any leading `#q`. Returns `(pattern, None)` when there is no qualifier
5725/// suffix. Useful for callers that want to use the pattern half with the
5726/// runtime [`matchpat`] (which has no qualifier semantics) while
5727/// reporting or applying the qualifier separately.
5728/// Split a glob pattern into (path-pattern, qualifier-string).
5729/// Port of the qualifier-detection step in `parsepat()`
5730/// (Src/glob.c:791).
5731pub fn split_qualifier(pattern: &str) -> (&str, Option<&str>) {
5732 if !pattern.ends_with(')') {
5733 return (pattern, None);
5734 }
5735 let bytes = pattern.as_bytes();
5736 let mut depth = 0;
5737 for i in (0..bytes.len()).rev() {
5738 match bytes[i] {
5739 b')' => depth += 1,
5740 b'(' => {
5741 depth -= 1;
5742 if depth == 0 {
5743 let inner = &pattern[i + 1..pattern.len() - 1];
5744 let inner = inner.strip_prefix("#q").unwrap_or(inner);
5745 return (&pattern[..i], Some(inner));
5746 }
5747 }
5748 _ => {}
5749 }
5750 }
5751 (pattern, None)
5752}
5753
5754/// Strip redundant `.` / `CurDir` segments from relative match paths for
5755/// output. Rust's `read_dir(".")` yields `entry.path()` like `./foo` while
5756/// `read_dir("foo")` yields `foo/bar` — zsh prints the latter shape for both.
5757fn glob_emit_path(path: &Path) -> String {
5758 match path.components().next() {
5759 Some(Component::Prefix(_) | Component::RootDir) => path.to_string_lossy().to_string(),
5760 None => ".".to_string(),
5761 _ => {
5762 let mut out = PathBuf::new();
5763 for c in path.components() {
5764 match c {
5765 Component::CurDir => {}
5766 Component::ParentDir => out.push(".."),
5767 Component::Normal(s) => out.push(s),
5768 Component::Prefix(_) | Component::RootDir => {}
5769 }
5770 }
5771 if out.as_os_str().is_empty() {
5772 ".".to_string()
5773 } else {
5774 out.to_string_lossy().to_string()
5775 }
5776 }
5777 }
5778}
5779
5780/// Check if path is a symlink
5781/// Check whether a glob match is a symlink.
5782/// Port of the `S_ISLNK(lstat.st_mode)` test in Src/glob.c.
5783pub fn is_symlink(path: &str) -> bool {
5784 fs::symlink_metadata(path)
5785 .map(|m| m.file_type().is_symlink())
5786 .unwrap_or(false)
5787}
5788
5789// ===========================================================
5790// Methods moved verbatim from src/ported/vm_helper because their
5791// C counterpart's source file maps 1:1 to this Rust module.
5792// Phase: glob
5793// ===========================================================
5794
5795// BEGIN moved-from-exec-rs
5796// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
5797
5798// END moved-from-exec-rs
5799
5800// ===========================================================
5801// Methods moved verbatim from src/ported/vm_helper because their
5802// C counterpart's source file maps 1:1 to this Rust module.
5803// Phase: drift
5804// ===========================================================
5805
5806// BEGIN moved-from-exec-rs
5807// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
5808
5809// END moved-from-exec-rs
5810
5811/// !!! RUST-ONLY adapter — NO DIRECT C COUNTERPART !!!
5812///
5813/// Thin "pattern string in, match list out" entry over the glob engine
5814/// for call sites holding a single pattern that want its matches as a
5815/// `Vec<String>`, rather than going through C's in-place
5816/// `zglob(LinkList, LinkNode, int)` list mutation. Snapshots the
5817/// glob-relevant options into TLS (`enter_glob_scope` — required because
5818/// zshrs runs glob on multiple threads; see that fn's doc) and drives
5819/// `globdata_glob`, the RUST-ONLY read_dir-based adaptation of C's
5820/// `scanner()` (Src/glob.c:500 — C descends with `lchdir`, which is
5821/// process-global and unsafe under zshrs's threading, so the port walks
5822/// absolute paths instead). ALL real glob semantics — brace expansion,
5823/// `(a|b)` alternation, `^`/`~` exclusion, qualifiers, `**`, sorting —
5824/// live in `globdata_glob`/`scanner`/`patcompile`, not here. (The 130
5825/// lines of ad-hoc `^`/`~`/alternation read_dir+matchpat code that used
5826/// to live here were verified redundant with `globdata_glob` and
5827/// removed.) The faithful C entry is `zglob` (c:1214).
5828pub fn glob_path(pattern: &str) -> Vec<String> {
5829 let _glob_scope = enter_glob_scope();
5830 let mut state = globdata::new();
5831 globdata_glob(&mut state, pattern)
5832}
5833
5834// The qualifier-comparison direction static is `g_range` (Src/glob.c, ported at
5835// glob.rs ~2799). The duplicate `G_RANGE` that used to live here was a Rust-only
5836// erroneous second copy — never written by the qual-eval, so qualnlink (its only
5837// reader) was stuck on `==`. Removed; qualnlink now reads `g_range`.
5838
5839// `mode_to_octal` lives at `crate::ported::utils::mode_to_octal`
5840// (port of `Src/utils.c:7634` — 12 bit-by-bit POSIX permission
5841// mappings). Local alias kept for the call sites at c:1610/1618
5842// which used the masked identity before the canonical port landed.
5843fn mode_to_octal(mode: u32) -> u32 {
5844 crate::ported::utils::mode_to_octal(mode) as u32
5845}
5846
5847#[cfg(test)]
5848mod tests {
5849 use super::*;
5850 use std::fs::{self, File};
5851 use tempfile::TempDir;
5852
5853 use crate::ported::options::{opt_state_set, opt_state_unset};
5854 use crate::ported::zsh_h::{redir, ERRFLAG_ERROR, REDIR_WRITE};
5855
5856 /// Convert ASCII brace-expansion source to the lexer-tokenized
5857 /// form `hasbraces` / `xpandbraces` consume per c:Src/glob.c —
5858 /// ASCII `{` → Inbrace (\u{8f}), `}` → Outbrace (\u{90}), `,` →
5859 /// Comma (\u{9a}). Backslash-escaped variants (`\{`, `\}`, `\,`)
5860 /// emit Bnull + literal so the canonical "escape via Bnull"
5861 /// distinction reaches the brace walker. Used by every test in
5862 /// this module that wants to drive the canonical TOKEN-strict
5863 /// path with readable ASCII source.
5864 fn tok(s: &str) -> String {
5865 let mut out = String::with_capacity(s.len());
5866 let mut chars = s.chars().peekable();
5867 while let Some(c) = chars.next() {
5868 match c {
5869 '\\' => match chars.peek() {
5870 Some('{') | Some('}') | Some(',') => {
5871 out.push('\u{9f}');
5872 out.push(chars.next().unwrap());
5873 }
5874 _ => out.push(c),
5875 },
5876 '{' => out.push('\u{8f}'),
5877 '}' => out.push('\u{90}'),
5878 ',' => out.push('\u{9a}'),
5879 _ => out.push(c),
5880 }
5881 }
5882 out
5883 }
5884
5885 fn setup_test_dir() -> TempDir {
5886 let dir = TempDir::new().unwrap();
5887 let base = dir.path();
5888
5889 // Create test files
5890 File::create(base.join("file1.txt")).unwrap();
5891 File::create(base.join("file2.txt")).unwrap();
5892 File::create(base.join("file3.rs")).unwrap();
5893 File::create(base.join(".hidden")).unwrap();
5894
5895 // Create subdirectory
5896 fs::create_dir(base.join("subdir")).unwrap();
5897 File::create(base.join("subdir/nested.txt")).unwrap();
5898
5899 dir
5900 }
5901
5902 #[test]
5903 fn test_haswilds() {
5904 let _g = crate::test_util::global_state_lock();
5905 // c:Src/glob.c:3548 — full glob tokenize (shadows the
5906 // brace-only `fn tok` test helper above on purpose:
5907 // haswilds consumes glob tokens, not just braces).
5908 let tok = |s: &str| {
5909 let mut t = s.to_string();
5910 tokenize(&mut t);
5911 t
5912 };
5913 assert!(haswilds(&tok("*.txt")));
5914 assert!(haswilds(&tok("file?.txt")));
5915 assert!(haswilds(&tok("file[12].txt")));
5916 assert!(!haswilds(&tok("file.txt")));
5917 assert!(!haswilds(&tok("path/to/file.txt")));
5918 }
5919
5920 #[test]
5921 fn test_pattern_match() {
5922 let _g = crate::test_util::global_state_lock();
5923 assert!(matchpat("*.txt", "file.txt", false, true));
5924 assert!(matchpat("file?.txt", "file1.txt", false, true));
5925 assert!(!matchpat("*.txt", "file.rs", false, true));
5926 assert!(matchpat("file[12].txt", "file1.txt", false, true));
5927 assert!(!matchpat("file[12].txt", "file3.txt", false, true));
5928 }
5929
5930 #[test]
5931 fn test_brace_expansion() {
5932 let _g = crate::test_util::global_state_lock();
5933 let result = xpandbraces(&tok("{a,b,c}"), false);
5934 assert_eq!(result, vec!["a", "b", "c"]);
5935
5936 let result = xpandbraces(&tok("file{1,2,3}.txt"), false);
5937 assert_eq!(result, vec!["file1.txt", "file2.txt", "file3.txt"]);
5938
5939 let result = xpandbraces(&tok("{1..5}"), false);
5940 assert_eq!(result, vec!["1", "2", "3", "4", "5"]);
5941
5942 let result = xpandbraces(&tok("{a..e}"), false);
5943 assert_eq!(result, vec!["a", "b", "c", "d", "e"]);
5944 }
5945
5946 #[test]
5947 fn test_glob_simple() {
5948 let _g = crate::test_util::global_state_lock();
5949 let dir = setup_test_dir();
5950 let pattern = format!("{}/*.txt", dir.path().display());
5951
5952 let mut state = globdata::new();
5953 let results = globdata_glob(&mut state, &pattern);
5954
5955 assert_eq!(results.len(), 2);
5956 assert!(results.iter().any(|s| s.ends_with("file1.txt")));
5957 assert!(results.iter().any(|s| s.ends_with("file2.txt")));
5958 }
5959
5960 #[test]
5961 fn test_glob_hidden() {
5962 let _g = crate::test_util::global_state_lock();
5963 let dir = setup_test_dir();
5964 let pattern = format!("{}/*", dir.path().display());
5965
5966 // Default (globdots off) → hidden files skipped. `dotglob`
5967 // is the bash alias for zsh's canonical `globdots` option;
5968 // the C-faithful read goes through `isset(GLOBDOTS)` which
5969 // resolves the canonical name.
5970 opt_state_set("globdots", false);
5971 let mut state = globdata::new();
5972 let results = globdata_glob(&mut state, &pattern);
5973 assert!(!results.iter().any(|s| s.contains(".hidden")));
5974
5975 // setopt globdots → hidden files included.
5976 opt_state_set("globdots", true);
5977 let mut state = globdata::new();
5978 let results = globdata_glob(&mut state, &pattern);
5979 assert!(results.iter().any(|s| s.contains(".hidden")));
5980 opt_state_set("globdots", false); // reset for other tests
5981 }
5982
5983 #[test]
5984 fn test_glob_emit_path_strips_read_dir_dot_slash() {
5985 let _g = crate::test_util::global_state_lock();
5986 assert_eq!(glob_emit_path(Path::new("./sub")), "sub");
5987 assert_eq!(glob_emit_path(Path::new("sub/deeper")), "sub/deeper");
5988 assert_eq!(glob_emit_path(Path::new("././x")), "x");
5989 assert_eq!(glob_emit_path(Path::new("../up")), "../up");
5990 }
5991
5992 #[test]
5993 fn test_file_type_char() {
5994 let _g = crate::test_util::global_state_lock();
5995 assert_eq!(file_type(libc::S_IFDIR as u32), '/');
5996 assert_eq!(file_type(libc::S_IFREG as u32), ' ');
5997 assert_eq!(file_type(libc::S_IFREG as u32 | 0o111), '*');
5998 assert_eq!(file_type(libc::S_IFLNK as u32), '@');
5999 }
6000
6001 /// `Src/glob.c:2018-2036` — `file_type(mode_t filemode)` returns a
6002 /// single-char marker keyed off S_ISBLK/CHR/DIR/FIFO/LNK/REG/SOCK,
6003 /// with `S_IXUGO` (== 0o111 = S_IXUSR|S_IXGRP|S_IXOTH) distinguishing
6004 /// executable regular files (`*`) from non-executable (` `). The
6005 /// catch-all (e.g. door, port, unknown types) returns `?`.
6006 /// Pin every branch by position.
6007 #[test]
6008 fn file_type_every_branch_matches_c_dispatch() {
6009 let _g = crate::test_util::global_state_lock();
6010 assert_eq!(
6011 file_type(libc::S_IFBLK as u32),
6012 '#',
6013 "c:2020 — S_ISBLK → '#'"
6014 );
6015 assert_eq!(
6016 file_type(libc::S_IFCHR as u32),
6017 '%',
6018 "c:2022 — S_ISCHR → '%'"
6019 );
6020 assert_eq!(
6021 file_type(libc::S_IFDIR as u32),
6022 '/',
6023 "c:2024 — S_ISDIR → '/'"
6024 );
6025 assert_eq!(
6026 file_type(libc::S_IFIFO as u32),
6027 '|',
6028 "c:2026 — S_ISFIFO → '|'"
6029 );
6030 assert_eq!(
6031 file_type(libc::S_IFLNK as u32),
6032 '@',
6033 "c:2028 — S_ISLNK → '@'"
6034 );
6035 // Regular file: ' ' if not executable, '*' if executable (any bit in S_IXUGO).
6036 assert_eq!(
6037 file_type(libc::S_IFREG as u32),
6038 ' ',
6039 "c:2030 — non-executable regular file → ' '"
6040 );
6041 // Each individual exec bit triggers '*'.
6042 assert_eq!(
6043 file_type(libc::S_IFREG as u32 | 0o100),
6044 '*',
6045 "c:2030 — S_IXUSR alone is enough"
6046 );
6047 assert_eq!(
6048 file_type(libc::S_IFREG as u32 | 0o010),
6049 '*',
6050 "c:2030 — S_IXGRP alone is enough"
6051 );
6052 assert_eq!(
6053 file_type(libc::S_IFREG as u32 | 0o001),
6054 '*',
6055 "c:2030 — S_IXOTH alone is enough"
6056 );
6057 assert_eq!(
6058 file_type(libc::S_IFSOCK as u32),
6059 '=',
6060 "c:2033 — S_ISSOCK → '='"
6061 );
6062 // Catch-all for unknown S_IFMT — bare 0 returns '?'.
6063 assert_eq!(file_type(0), '?', "c:2035 — unknown st_mode → '?'");
6064 }
6065
6066 #[test]
6067 fn test_zstrcmp_numeric() {
6068 let _g = crate::test_util::global_state_lock();
6069 let n = crate::zsh_h::SORTIT_NUMERICALLY as u32;
6070 assert_eq!(zstrcmp("file1", "file2", n), std::cmp::Ordering::Less);
6071 assert_eq!(zstrcmp("file10", "file2", n), std::cmp::Ordering::Greater);
6072 assert_eq!(zstrcmp("file10", "file10", n), std::cmp::Ordering::Equal);
6073 }
6074
6075 /// Race-fix verification: snapshot pins bareglobqual for the
6076 /// duration of a glob scope even when the live store flips
6077 /// underneath. Mimics the stryke pattern in
6078 /// MenkeTechnologies/strykelang#3 — one thread is mid-glob with
6079 /// bareglobqual=1 snapshot when another flips it off.
6080 #[test]
6081 fn glob_opts_snapshot_isolates_concurrent_setopt() {
6082 let _g = crate::test_util::global_state_lock();
6083
6084 // Preserve the existing live state so the test is hermetic.
6085 let saved = opt_state_get("bareglobqual");
6086
6087 // Live store says bareglobqual=true; enter scope captures that.
6088 opt_state_set("bareglobqual", true);
6089 let _scope = enter_glob_scope();
6090 assert!(
6091 glob_isset(BAREGLOBQUAL),
6092 "TLS snapshot reads bareglobqual=true at scope entry"
6093 );
6094
6095 // Simulate a concurrent thread flipping the live store.
6096 opt_state_set("bareglobqual", false);
6097 assert!(
6098 glob_isset(BAREGLOBQUAL),
6099 "TLS snapshot must still report bareglobqual=true \
6100 even though live store now reads false"
6101 );
6102
6103 // Restore.
6104 match saved {
6105 Some(v) => opt_state_set("bareglobqual", v),
6106 None => opt_state_unset("bareglobqual"),
6107 }
6108 }
6109
6110 /// After the scope guard drops, reads fall back to the live store.
6111 #[test]
6112 fn glob_opts_snapshot_clears_on_drop() {
6113 let _g = crate::test_util::global_state_lock();
6114
6115 let saved = opt_state_get("nullglob");
6116 opt_state_set("nullglob", true);
6117 {
6118 let _scope = enter_glob_scope();
6119 assert!(glob_isset(NULLGLOB), "snapshot live=true → true inside");
6120 }
6121 // Outside scope: flip live store, read should follow live.
6122 opt_state_set("nullglob", false);
6123 assert!(
6124 !glob_isset(NULLGLOB),
6125 "post-scope: glob_isset falls back to live store"
6126 );
6127
6128 match saved {
6129 Some(v) => opt_state_set("nullglob", v),
6130 None => opt_state_unset("nullglob"),
6131 }
6132 }
6133
6134 /// Nested glob scopes share the outer snapshot — inner doesn't
6135 /// re-capture or clear on its own Drop.
6136 #[test]
6137 fn glob_opts_snapshot_nested_is_noop() {
6138 let _g = crate::test_util::global_state_lock();
6139
6140 let saved = opt_state_get("extendedglob");
6141 opt_state_set("extendedglob", true);
6142 let _outer = enter_glob_scope();
6143 assert!(glob_isset(EXTENDEDGLOB));
6144 {
6145 let _inner = enter_glob_scope();
6146 // Live store flip while nested.
6147 opt_state_set("extendedglob", false);
6148 assert!(glob_isset(EXTENDEDGLOB), "inner observes outer snapshot");
6149 } // inner drops — outer snapshot still active.
6150 assert!(
6151 glob_isset(EXTENDEDGLOB),
6152 "outer snapshot survives inner drop"
6153 );
6154
6155 match saved {
6156 Some(v) => opt_state_set("extendedglob", v),
6157 None => opt_state_unset("extendedglob"),
6158 }
6159 }
6160
6161 /// c:2150 (xpandredir port) — when the redir name has no wildcard
6162 /// AND no `$var`/`!hist`/`{a,b}` to expand, prefork+globlist leave
6163 /// the name unchanged. The single-match path at c:2171 must
6164 /// rewrite `fn.name` to the same string and return 0 (no multi-fan).
6165 /// Regression that returns 1 would trigger MULTIOS dispatch on a
6166 /// single literal filename — wrong shell semantics.
6167 #[test]
6168 fn xpandredir_single_literal_filename_returns_zero() {
6169 let _g = crate::test_util::global_state_lock();
6170 let mut fn_ = redir {
6171 typ: REDIR_WRITE,
6172 flags: 0,
6173 fd1: 1,
6174 fd2: -1,
6175 name: Some("/tmp/zshrs_test_out".to_string()),
6176 varid: None,
6177 here_terminator: None,
6178 munged_here_terminator: None,
6179 };
6180 let mut tab: Vec<redir> = Vec::new();
6181 let r = xpandredir(&mut fn_, &mut tab);
6182 assert_eq!(r, 0, "literal filename → single match → ret=0");
6183 assert_eq!(
6184 fn_.name.as_deref(),
6185 Some("/tmp/zshrs_test_out"),
6186 "literal name must round-trip through prefork unchanged"
6187 );
6188 assert!(tab.is_empty(), "no multi-match → redirtab not appended");
6189 }
6190
6191 /// c:2176-2177 — `>&-` (REDIR_MERGEOUT with name "-") collapses to
6192 /// REDIR_CLOSE per the `IS_DASH(s[0]) && !s[1]` branch. Regression
6193 /// where this fails leaves `>&-` as a literal merge-fd attempt,
6194 /// which the executor would interpret as "merge with fd -1".
6195 #[test]
6196 fn xpandredir_dash_merge_collapses_to_close() {
6197 let _g = crate::test_util::global_state_lock();
6198 let mut fn_ = redir {
6199 typ: REDIR_MERGEOUT,
6200 flags: 0,
6201 fd1: 1,
6202 fd2: -1,
6203 name: Some("-".to_string()),
6204 varid: None,
6205 here_terminator: None,
6206 munged_here_terminator: None,
6207 };
6208 let mut tab: Vec<redir> = Vec::new();
6209 let _ = xpandredir(&mut fn_, &mut tab);
6210 assert_eq!(
6211 fn_.typ, REDIR_CLOSE,
6212 "`>&-` must rewrite typ to REDIR_CLOSE"
6213 );
6214 }
6215
6216 /// c:2150 — empty `fn.name` should return 0 cleanly. Catches a
6217 /// regression that panics on `.as_deref().unwrap()` for absent name.
6218 #[test]
6219 fn xpandredir_with_no_name_returns_zero_no_panic() {
6220 let _g = crate::test_util::global_state_lock();
6221 let mut fn_ = redir {
6222 typ: REDIR_WRITE,
6223 flags: 0,
6224 fd1: 1,
6225 fd2: -1,
6226 name: None,
6227 varid: None,
6228 here_terminator: None,
6229 munged_here_terminator: None,
6230 };
6231 let mut tab: Vec<redir> = Vec::new();
6232 assert_eq!(xpandredir(&mut fn_, &mut tab), 0);
6233 }
6234
6235 /// `IN_EXPANDREDIR` static defaults to 0 — set transiently inside
6236 /// xpandredir per c:2165/2167. After a normal call it must restore
6237 /// to 0. Regression that leaks the flag would skew unrelated glob
6238 /// expansions outside redirections.
6239 #[test]
6240 fn in_expandredir_flag_is_zero_at_rest() {
6241 let _g = crate::test_util::global_state_lock();
6242 assert_eq!(IN_EXPANDREDIR.load(Ordering::SeqCst), 0);
6243 }
6244
6245 /// c:4306 — `haswilds` honours backslash-escapes: `\*` is a literal
6246 /// star, NOT a wildcard. Regression that ignores the escape would
6247 /// trigger globbing on `printf '%s\n' \*`, breaking shell scripts
6248 /// that quote literal stars.
6249 #[test]
6250 fn haswilds_respects_backslash_escape() {
6251 let _g = crate::test_util::global_state_lock();
6252 // c:Src/glob.c:3548 — full glob tokenize (shadows the
6253 // brace-only `fn tok` test helper above on purpose:
6254 // haswilds consumes glob tokens, not just braces).
6255 let tok = |s: &str| {
6256 let mut t = s.to_string();
6257 tokenize(&mut t);
6258 t
6259 };
6260 assert!(haswilds(&tok("*.txt")), "bare * is wild");
6261 assert!(
6262 !haswilds(&tok(r"\*.txt")),
6263 "escaped \\* is literal — NOT wild"
6264 );
6265 assert!(
6266 !haswilds(&tok(r"\?.txt")),
6267 "escaped \\? is literal — NOT wild"
6268 );
6269 }
6270
6271 /// c:4306 — `[` immediately enters bracket mode AND counts as a
6272 /// wildcard. The early return on `[` is critical — even
6273 /// unterminated brackets must be flagged so `cd [` doesn't try
6274 /// to chdir to a literal `[`.
6275 #[test]
6276 fn haswilds_open_bracket_alone_is_a_wildcard() {
6277 let _g = crate::test_util::global_state_lock();
6278 // c:Src/glob.c:3548 — full glob tokenize (shadows the
6279 // brace-only `fn tok` test helper above on purpose:
6280 // haswilds consumes glob tokens, not just braces).
6281 let tok = |s: &str| {
6282 let mut t = s.to_string();
6283 tokenize(&mut t);
6284 t
6285 };
6286 assert!(haswilds(&tok("[abc]")), "char-class is wild");
6287 assert!(haswilds(&tok("foo[")), "even unterminated [ is wild");
6288 }
6289
6290 /// c:4306 — wildcard chars `*` `?` inside an OPEN bracket-context
6291 /// are NOT additional wildcards (the bracket already is one).
6292 /// Regression that double-counts would make haswilds report `[*]`
6293 /// as wildcard-twice — cosmetic, but confuses any caller using
6294 /// the bool as a "should I glob?" gate that AND-tests with another
6295 /// flag.
6296 #[test]
6297 fn haswilds_extglob_chars_inside_bracket_dont_double_count() {
6298 let _g = crate::test_util::global_state_lock();
6299 // c:Src/glob.c:3548 — full glob tokenize (shadows the
6300 // brace-only `fn tok` test helper above on purpose:
6301 // haswilds consumes glob tokens, not just braces).
6302 let tok = |s: &str| {
6303 let mut t = s.to_string();
6304 tokenize(&mut t);
6305 t
6306 };
6307 // Once `[` is seen, function returns true immediately, so the
6308 // post-bracket chars don't matter. But this docs the contract.
6309 assert!(haswilds(&tok("[*]")));
6310 }
6311
6312 /// c:4306 — plain text returns false. Catches a regression where
6313 /// any non-empty input is flagged as wild (would break `cd /tmp`
6314 /// by triggering glob expansion on a literal path).
6315 #[test]
6316 fn haswilds_plain_text_not_wild() {
6317 let _g = crate::test_util::global_state_lock();
6318 // c:Src/glob.c:3548 — full glob tokenize (shadows the
6319 // brace-only `fn tok` test helper above on purpose:
6320 // haswilds consumes glob tokens, not just braces).
6321 let tok = |s: &str| {
6322 let mut t = s.to_string();
6323 tokenize(&mut t);
6324 t
6325 };
6326 assert!(!haswilds(&tok("plain")));
6327 assert!(!haswilds(&tok("")));
6328 assert!(!haswilds(&tok("/usr/local/bin")));
6329 assert!(!haswilds(&tok("file.txt")));
6330 }
6331
6332 /// c:4363-4371 — `#` and `^` are recognised as wildcards by
6333 /// `haswilds` **only when `EXTENDEDGLOB` is set**, matching the C
6334 /// source's `isset(EXTENDEDGLOB)` gate at pattern.c:4364/4369. `~`
6335 /// is **not** a filename-generation wildcard (zsh handles it as
6336 /// tilde expansion in a separate pipeline stage); a prior
6337 /// glob.rs-local `haswilds` impl returned true for `~`, which
6338 /// caused zglob to mis-classify tilde-prefixed args as needing
6339 /// filename generation. Pin the corrected behavior.
6340 #[test]
6341 fn haswilds_extended_glob_chars_recognised() {
6342 let _g = crate::test_util::global_state_lock();
6343 // c:Src/glob.c:3548 — full glob tokenize (shadows the
6344 // brace-only `fn tok` test helper above on purpose:
6345 // haswilds consumes glob tokens, not just braces).
6346 let tok = |s: &str| {
6347 let mut t = s.to_string();
6348 tokenize(&mut t);
6349 t
6350 };
6351 // EXTENDEDGLOB off → `#` and `^` are not wild.
6352 crate::ported::options::opt_state_set("extendedglob", false);
6353 assert!(
6354 !haswilds(&tok("foo#bar")),
6355 "# not wild without EXTENDEDGLOB"
6356 );
6357 assert!(
6358 !haswilds(&tok("foo^bar")),
6359 "^ not wild without EXTENDEDGLOB"
6360 );
6361 // EXTENDEDGLOB on → `#` and `^` are wild (c:4364, c:4369).
6362 crate::ported::options::opt_state_set("extendedglob", true);
6363 assert!(haswilds(&tok("foo#bar")), "# is extglob wild");
6364 assert!(haswilds(&tok("foo^bar")), "^ is extglob wild");
6365 crate::ported::options::opt_state_set("extendedglob", false);
6366 // `~` is NOT in haswilds' switch (c:4324-4373) — tilde expansion
6367 // is a separate pipeline stage.
6368 assert!(
6369 !haswilds(&tok("~/file")),
6370 "~ is NOT a filename-generation wildcard"
6371 );
6372 }
6373
6374 /// c:2514 — `matchpat` returns true for exact match. Sanity check
6375 /// the simplest dispatch path (the matcher recipe builds a
6376 /// trivial pattern, runs it).
6377 #[test]
6378 fn matchpat_exact_literal_matches() {
6379 let _g = crate::test_util::global_state_lock();
6380 assert!(matchpat("hello", "hello", false, true));
6381 assert!(!matchpat("hello", "world", false, true));
6382 }
6383
6384 /// c:2514 — `matchpat` with case_sensitive=false MUST treat upper
6385 /// and lower as equal (`HELLO` matches `hello`). Regression keeping
6386 /// case-sensitive when flag is false would silently break every
6387 /// `[[ "$x" = (#i)foo ]]`-style match.
6388 #[test]
6389 fn matchpat_case_insensitive_when_flag_clear() {
6390 let _g = crate::test_util::global_state_lock();
6391 assert!(
6392 matchpat("hello", "HELLO", false, false),
6393 "case-insensitive match must succeed across cases"
6394 );
6395 assert!(matchpat("FoO", "foo", false, false));
6396 }
6397
6398 /// c:2514 — case-sensitive (default) MUST reject case-different
6399 /// inputs. Pinning the contract.
6400 #[test]
6401 fn matchpat_case_sensitive_rejects_case_different() {
6402 let _g = crate::test_util::global_state_lock();
6403 assert!(
6404 !matchpat("hello", "HELLO", false, true),
6405 "case-sensitive default must reject HELLO != hello"
6406 );
6407 }
6408
6409 /// c:2780 — `set_pat_start(p, offs)` sets `PAT_NOTSTART` when offs is
6410 /// nonzero (matched substring starts past the real start, so `(#s)`
6411 /// must fail) and clears it when offs is 0. A regression here would
6412 /// let `(#s)` anchors fire mid-string during substring globbing.
6413 #[test]
6414 fn set_pat_start_toggles_pat_notstart_flag() {
6415 let _g = crate::test_util::global_state_lock();
6416 let mut p = mk_test_patprog();
6417 set_pat_start(&mut p, 2);
6418 assert!(
6419 p.0.flags & PAT_NOTSTART != 0,
6420 "offs!=0 must set PAT_NOTSTART"
6421 );
6422 set_pat_start(&mut p, 0);
6423 assert!(
6424 p.0.flags & PAT_NOTSTART == 0,
6425 "offs==0 must clear PAT_NOTSTART"
6426 );
6427 }
6428
6429 /// c:2796 — `set_pat_end(p, null_me)` sets `PAT_NOTEND` when the char
6430 /// being zapped is non-NUL (string shortened at tail, so `(#e)` must
6431 /// fail) and clears it when that char is already NUL. A regression
6432 /// would let `(#e)` anchors fire before the real end.
6433 #[test]
6434 fn set_pat_end_toggles_pat_notend_flag() {
6435 let _g = crate::test_util::global_state_lock();
6436 let mut p = mk_test_patprog();
6437 set_pat_end(&mut p, b'x');
6438 assert!(
6439 p.0.flags & PAT_NOTEND != 0,
6440 "non-NUL null_me must set PAT_NOTEND"
6441 );
6442 set_pat_end(&mut p, 0);
6443 assert!(
6444 p.0.flags & PAT_NOTEND == 0,
6445 "NUL null_me must clear PAT_NOTEND"
6446 );
6447 }
6448
6449 /// Build a zeroed `Patprog` for flag-toggle tests.
6450 fn mk_test_patprog() -> Patprog {
6451 Box::new((
6452 crate::ported::zsh_h::patprog {
6453 startoff: 0,
6454 size: 0,
6455 mustoff: 0,
6456 patmlen: 0,
6457 globflags: 0,
6458 globend: 0,
6459 flags: 0,
6460 patnpar: 0,
6461 patstartch: 0,
6462 },
6463 Vec::new(),
6464 ))
6465 }
6466
6467 /// c:2773 — `freematchlist(None)` is a no-op (matches C's
6468 /// `if (repllist) freelinklist(...)`). Regression panicking on
6469 /// None would crash every glob-replace path with no matches.
6470 #[test]
6471 fn freematchlist_handles_none_safely() {
6472 let _g = crate::test_util::global_state_lock();
6473 freematchlist(None);
6474 // No assertion — survival is the test.
6475 }
6476
6477 /// c:2773 — `freematchlist(Some(&mut Vec))` clears the list.
6478 /// Regression that drops a stale entry would leak match positions
6479 /// across calls.
6480 #[test]
6481 fn freematchlist_clears_provided_vec() {
6482 let _g = crate::test_util::global_state_lock();
6483 let mut v = vec![
6484 repldata {
6485 b: 0,
6486 e: 5,
6487 replstr: None,
6488 },
6489 repldata {
6490 b: 10,
6491 e: 15,
6492 replstr: Some("x".to_string()),
6493 },
6494 ];
6495 freematchlist(Some(&mut v));
6496 assert!(v.is_empty(), "freematchlist must clear the input vec");
6497 }
6498
6499 /// `Src/glob.c:2042-2142` — `hasbraces(str)` returns true when
6500 /// `str` contains a brace-expansion candidate. A bare lbrace/rbrace
6501 /// is NOT a brace expansion (it's a literal). Detection requires a
6502 /// matched pair containing either a comma or a dotdot range. Pin
6503 /// the canonical contract.
6504 #[test]
6505 fn hasbraces_matched_pair_with_comma_or_dotdot() {
6506 let _g = crate::test_util::global_state_lock();
6507 // Matched + comma → true.
6508 assert!(
6509 hasbraces(&tok("a{b,c}d"), false),
6510 "c:2127 — lbrace + comma + rbrace is a brace expansion"
6511 );
6512 // Matched + dotdot → true.
6513 assert!(
6514 hasbraces(&tok("file{1..3}.txt"), false),
6515 "c:2082 — N..M range is a brace expansion"
6516 );
6517 // Matched WITHOUT comma/dotdot → false (it's a literal).
6518 assert!(
6519 !hasbraces(&tok("{abc}"), false),
6520 "literal braces are NOT a brace expansion without comma or dotdot"
6521 );
6522 // Unmatched → false.
6523 assert!(
6524 !hasbraces(&tok("{abc"), false),
6525 "lone lbrace without matching rbrace is not brace expansion"
6526 );
6527 assert!(
6528 !hasbraces(&tok("abc}"), false),
6529 "lone rbrace is not brace expansion"
6530 );
6531 // No braces → false.
6532 assert!(!hasbraces(&tok("plain"), false));
6533 assert!(!hasbraces(&tok(""), false));
6534 }
6535
6536 /// `Src/glob.c:2049-2063` — BRACECCL branch: when `isset(BRACECCL)`
6537 /// (the `brace_ccl` parameter), any non-empty matched braces
6538 /// become a character-class set, regardless of whether the body
6539 /// contains a comma. Empty pair is still not.
6540 #[test]
6541 fn hasbraces_brace_ccl_makes_any_pair_match() {
6542 let _g = crate::test_util::global_state_lock();
6543 // c:2049 — BRACECCL: non-empty pair is enough.
6544 assert!(
6545 hasbraces(&tok("{abc}"), true),
6546 "c:2049 — BRACECCL: non-empty pair is char-class set"
6547 );
6548 assert!(
6549 hasbraces(&tok("x{q}y"), true),
6550 "c:2049 — single-char pair counts under BRACECCL"
6551 );
6552 // Empty pair shouldn't trigger.
6553 assert!(
6554 !hasbraces(&tok("{}"), true),
6555 "empty pair still not a brace expansion even under BRACECCL"
6556 );
6557 // Without BRACECCL, plain literal-letter pair is NOT a brace expansion.
6558 assert!(
6559 !hasbraces(&tok("{abc}"), false),
6560 "c:2049 — BRACECCL off → plain literal pair stays literal"
6561 );
6562 }
6563
6564 /// `Src/glob.c:2042-2142` — depth tracking: comma/dotdot count
6565 /// only at depth==1 in our port. So nested inner comma doesn't
6566 /// qualify the outer pair, but TWO independent top-level pairs
6567 /// (each with comma) DO trigger detection.
6568 #[test]
6569 fn hasbraces_depth_1_check_for_comma_dotdot() {
6570 let _g = crate::test_util::global_state_lock();
6571 assert!(
6572 hasbraces(&tok("a{1,2}b{3,4}c"), false),
6573 "two independent top-level pairs, first one matches at depth 1"
6574 );
6575 }
6576
6577 /// Pin: `remnulargs` per `Src/glob.c:3649-3679`:
6578 /// - Strings with NO inull bytes are unchanged.
6579 /// - Strings with ONLY Bnullkeep: keep as-is in scan phase
6580 /// (Bnullkeep is "active backslash" that hasn't triggered
6581 /// copy phase yet).
6582 /// - After encountering any other inull: switch to copy phase:
6583 /// * Bnullkeep → '\\' (literal backslash).
6584 /// * Other inulls (Snull/Dnull/Bnull/Nularg) → stripped.
6585 /// * Non-inull → kept.
6586 /// - Empty post-strip → replaced with single Nularg.
6587 #[test]
6588 fn remnulargs_matches_c_inull_handling() {
6589 let _g = crate::test_util::global_state_lock();
6590
6591 // Plain ASCII unchanged (no inulls).
6592 let mut s = "hello".to_string();
6593 remnulargs(&mut s);
6594 assert_eq!(
6595 s, "hello",
6596 "c:3654 — no inull bytes leaves string unchanged"
6597 );
6598
6599 // Snull-triggered copy: Snull stripped, rest kept.
6600 let mut s = format!("ab{}cd", Snull);
6601 remnulargs(&mut s);
6602 assert_eq!(
6603 s, "abcd",
6604 "c:3663 — Snull triggers copy; itself stripped, rest kept"
6605 );
6606
6607 // Bnullkeep AFTER Snull trigger → '\\' (active backslash).
6608 let mut s = format!("ab{}c{}d", Snull, Bnullkeep);
6609 remnulargs(&mut s);
6610 assert_eq!(
6611 s, "abc\\d",
6612 "c:3666 — Bnullkeep in copy phase becomes literal '\\\\'"
6613 );
6614
6615 // Other inulls (Bnull, Dnull) stripped in copy phase.
6616 let mut s = format!("a{}b{}c{}d", Snull, Bnull, Dnull);
6617 remnulargs(&mut s);
6618 assert_eq!(s, "abcd", "c:3668 — Bnull/Dnull stripped in copy phase");
6619
6620 // Empty post-strip → Nularg.
6621 let mut s = format!("{}", Snull);
6622 remnulargs(&mut s);
6623 assert_eq!(
6624 s,
6625 format!("{}", Nularg),
6626 "c:3674 — empty result replaced by Nularg sentinel"
6627 );
6628 }
6629
6630 /// `Src/glob.c:1085-1117` — `glob_exec_string` is a PARSER, not
6631 /// an executor. Extracts the qualifier inner text from `*sp`
6632 /// (advancing past delimiters). Previous Rust port forked
6633 /// `/bin/sh` to execute the cmd — that's entirely the wrong
6634 /// layer; execution happens at the call sites via qualsheval.
6635 /// Pin: plus-form extracts identifier; delimited form extracts
6636 /// content + advances.
6637 #[test]
6638 fn glob_exec_string_parses_qualifier_text() {
6639 let _g = crate::test_util::global_state_lock();
6640 // Plus form: identifier ends at first non-ident char.
6641 // C: `(+myfunc:rest)` — `s` points past `+`, plus_form=true.
6642 let r = glob_exec_string("myfunc rest", true);
6643 assert!(r.is_some(), "c:1092 — identifier parse should succeed");
6644 let (ident, _adv) = r.unwrap();
6645 assert_eq!(
6646 ident, "myfunc",
6647 "c:1092 — itype_end stops at first non-IIDENT char"
6648 );
6649
6650 // Plus form with no identifier (immediate non-ident) — error.
6651 let r = glob_exec_string(" leading-space", true);
6652 assert!(
6653 r.is_none(),
6654 "c:1093-1096 — empty identifier emits zerr + returns None"
6655 );
6656 }
6657
6658 /// `Src/glob.c:3907-3943` — `qualsheval(name, _, _, expr)` sets
6659 /// `$REPLY` via paramtab, evaluates `expr` IN THE CURRENT SHELL,
6660 /// then restores errflag+lastval. Previous Rust port spawned
6661 /// `/bin/sh -c expr` which:
6662 /// 1. Couldn't access shell locals / functions / aliases.
6663 /// 2. Ran `sh` (POSIX), not `zsh` — every zsh-feature qualifier
6664 /// silently failed.
6665 ///
6666 /// Pin: after qualsheval, errflag is restored (mod ERRFLAG_INT),
6667 /// lastval is restored, and `$REPLY` is set to the filename.
6668 /// Easiest direct pin: errflag/lastval restore around an `expr`
6669 /// that mutates them.
6670 #[test]
6671 fn qualsheval_restores_errflag_and_lastval() {
6672 let _g = crate::test_util::global_state_lock();
6673 // Seed errflag and lastval with distinctive values.
6674 errflag.store(0, Ordering::Relaxed);
6675 LASTVAL.store(42, Ordering::Relaxed);
6676 // Even if the expr mutates these via the executor, c:3924-3925
6677 // restore them after.
6678 let st0: libc::stat = unsafe { std::mem::zeroed() };
6679 let _ = qualsheval("/tmp/file", &st0, 0, ":"); // no-op expr
6680 // c:3924 — errflag restored.
6681 assert_eq!(
6682 errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR,
6683 0,
6684 "c:3924 — qualsheval must restore errflag (no ERRFLAG_ERROR leak)"
6685 );
6686 // c:3925 — lastval restored.
6687 assert_eq!(
6688 LASTVAL.load(Ordering::Relaxed),
6689 42,
6690 "c:3925 — qualsheval must restore lastval to pre-call value"
6691 );
6692 // c:3916 — $REPLY visible in paramtab.
6693 assert_eq!(
6694 crate::ported::params::getsparam("REPLY"),
6695 Some("/tmp/file".to_string()),
6696 "c:3916 — qualsheval must set $REPLY to filename"
6697 );
6698 }
6699
6700 /// `Src/glob.c:2164` — `if (!errflag && isset(MULTIOS))` is the
6701 /// logical zero-check guarding globlist recursion. Previous Rust
6702 /// port used `!errflag.load(...) != 0` which is BITWISE NOT
6703 /// (`^-1`), evaluating to `errflag != -1` — almost-always-true.
6704 ///
6705 /// Pin: a `matchpat` smoke that DOES NOT mutate errflag, then a
6706 /// direct check that the fix sense is correct (errflag==0 enters
6707 /// the branch, errflag!=0 skips). Can't easily exercise the full
6708 /// xpandredir path without a redir struct + IN_EXPANDREDIR
6709 /// global, so this is a sense-of-the-condition pin.
6710 #[test]
6711 fn xpandredir_errflag_check_uses_logical_zero() {
6712 let _g = crate::test_util::global_state_lock();
6713 // Mirror the in-port logic with the canonical zero-check.
6714 let saved = errflag.load(Ordering::Relaxed);
6715 // c:2164 — errflag==0 path: branch should fire.
6716 errflag.store(0, Ordering::Relaxed);
6717 let zero_enters = errflag.load(Ordering::Relaxed) == 0;
6718 assert!(
6719 zero_enters,
6720 "c:2164 — errflag==0 enters the branch (the canonical sense)"
6721 );
6722 // c:2164 — errflag!=0 path: branch should skip.
6723 errflag.store(1, Ordering::Relaxed);
6724 let nonzero_skips = errflag.load(Ordering::Relaxed) == 0;
6725 assert!(
6726 !nonzero_skips,
6727 "c:2164 — errflag!=0 skips the branch (regression of bitwise-NOT bug fix)"
6728 );
6729 errflag.store(saved, Ordering::Relaxed);
6730 }
6731
6732 // ═══════════════════════════════════════════════════════════════════
6733 // Additional unit coverage — pure pattern/brace/glob behaviour that
6734 // doesn't need a real filesystem. Uses `matchpat` for high-level
6735 // pattern checks (extended=false, case_sensitive=true is the most
6736 // common path) and `xpandbraces` for brace expansion.
6737 // ═══════════════════════════════════════════════════════════════════
6738
6739 // ── matchpat: full-string pattern matching ───────────────────────
6740 #[test]
6741 fn matchpat_literal_no_wildcards() {
6742 let _g = crate::test_util::global_state_lock();
6743 assert!(matchpat("foo", "foo", false, true));
6744 assert!(!matchpat("foo", "bar", false, true));
6745 assert!(!matchpat("foo", "foobar", false, true));
6746 }
6747
6748 #[test]
6749 fn matchpat_star_consumes_substring() {
6750 let _g = crate::test_util::global_state_lock();
6751 assert!(matchpat("a*b", "ab", false, true));
6752 assert!(matchpat("a*b", "ahellob", false, true));
6753 assert!(!matchpat("a*b", "abc", false, true));
6754 }
6755
6756 #[test]
6757 fn matchpat_question_is_single_char() {
6758 let _g = crate::test_util::global_state_lock();
6759 assert!(matchpat("a?c", "abc", false, true));
6760 assert!(matchpat("a?c", "axc", false, true));
6761 assert!(!matchpat("a?c", "ac", false, true));
6762 assert!(!matchpat("a?c", "abbc", false, true));
6763 }
6764
6765 #[test]
6766 fn matchpat_bracket_class_inline() {
6767 let _g = crate::test_util::global_state_lock();
6768 assert!(matchpat("file[abc].txt", "filea.txt", false, true));
6769 assert!(matchpat("file[abc].txt", "fileb.txt", false, true));
6770 assert!(!matchpat("file[abc].txt", "filed.txt", false, true));
6771 }
6772
6773 #[test]
6774 fn matchpat_case_sensitive_strict() {
6775 let _g = crate::test_util::global_state_lock();
6776 assert!(matchpat("Foo", "Foo", false, true));
6777 assert!(!matchpat("Foo", "foo", false, true));
6778 assert!(!matchpat("Foo", "FOO", false, true));
6779 }
6780
6781 #[test]
6782 fn matchpat_empty_pattern_matches_only_empty() {
6783 let _g = crate::test_util::global_state_lock();
6784 assert!(matchpat("", "", false, true));
6785 assert!(!matchpat("", "x", false, true));
6786 }
6787
6788 #[test]
6789 fn matchpat_star_alone_matches_anything() {
6790 let _g = crate::test_util::global_state_lock();
6791 assert!(matchpat("*", "", false, true));
6792 assert!(matchpat("*", "a", false, true));
6793 assert!(matchpat("*", "abcdef", false, true));
6794 }
6795
6796 #[test]
6797 fn matchpat_question_alone_one_char() {
6798 let _g = crate::test_util::global_state_lock();
6799 assert!(!matchpat("?", "", false, true));
6800 assert!(matchpat("?", "a", false, true));
6801 assert!(!matchpat("?", "ab", false, true));
6802 }
6803
6804 // ── haswilds: meta-char detection ───────────────────────────────
6805 #[test]
6806 fn haswilds_each_glob_meta() {
6807 let _g = crate::test_util::global_state_lock();
6808 // c:Src/glob.c:3548 — full glob tokenize (shadows the
6809 // brace-only `fn tok` test helper above on purpose:
6810 // haswilds consumes glob tokens, not just braces).
6811 let tok = |s: &str| {
6812 let mut t = s.to_string();
6813 tokenize(&mut t);
6814 t
6815 };
6816 assert!(haswilds(&tok("*")));
6817 assert!(haswilds(&tok("?")));
6818 assert!(haswilds(&tok("[abc]")));
6819 assert!(haswilds(&tok("a*b")));
6820 assert!(haswilds(&tok("a?b")));
6821 }
6822
6823 #[test]
6824 fn haswilds_plain_strings_have_no_wildcards() {
6825 let _g = crate::test_util::global_state_lock();
6826 // c:Src/glob.c:3548 — full glob tokenize (shadows the
6827 // brace-only `fn tok` test helper above on purpose:
6828 // haswilds consumes glob tokens, not just braces).
6829 let tok = |s: &str| {
6830 let mut t = s.to_string();
6831 tokenize(&mut t);
6832 t
6833 };
6834 assert!(!haswilds(&tok("")));
6835 assert!(!haswilds(&tok("plain.txt")));
6836 assert!(!haswilds(&tok("/abs/path/file.rs")));
6837 assert!(!haswilds(&tok("./rel/file")));
6838 }
6839
6840 // ── xpandbraces: brace expansion (zsh extension, not POSIX) ─────
6841 #[test]
6842 fn xpandbraces_three_alternatives() {
6843 let _g = crate::test_util::global_state_lock();
6844 assert_eq!(xpandbraces(&tok("{a,b,c}"), false), vec!["a", "b", "c"]);
6845 }
6846
6847 #[test]
6848 fn xpandbraces_with_prefix_and_suffix() {
6849 let _g = crate::test_util::global_state_lock();
6850 assert_eq!(
6851 xpandbraces(&tok("pre{x,y,z}post"), false),
6852 vec!["prexpost", "preypost", "prezpost"]
6853 );
6854 }
6855
6856 #[test]
6857 fn xpandbraces_numeric_range_ascending() {
6858 let _g = crate::test_util::global_state_lock();
6859 assert_eq!(
6860 xpandbraces(&tok("{1..5}"), false),
6861 vec!["1", "2", "3", "4", "5"]
6862 );
6863 }
6864
6865 #[test]
6866 fn xpandbraces_alpha_range_ascending() {
6867 let _g = crate::test_util::global_state_lock();
6868 assert_eq!(
6869 xpandbraces(&tok("{a..e}"), false),
6870 vec!["a", "b", "c", "d", "e"]
6871 );
6872 }
6873
6874 #[test]
6875 fn xpandbraces_single_alternative_passes_through_literal() {
6876 // Real zsh 5.9 (`print -l {a}`) outputs `{a}` verbatim — no
6877 // expansion because there is no comma or range inside the
6878 // braces. xpandbraces operates on TOKEN form throughout
6879 // (c:Src/glob.c::xpandbraces); the eventual ASCII conversion
6880 // happens later in untokenize. So unit-level pass-through
6881 // preserves TOKEN bytes, not ASCII braces.
6882 let _g = crate::test_util::global_state_lock();
6883 let out = xpandbraces(&tok("{a}"), false);
6884 assert_eq!(
6885 out,
6886 vec![tok("{a}")],
6887 "zsh 5.9 returns the input verbatim (TOKEN form preserved at xpandbraces layer)"
6888 );
6889 }
6890
6891 #[test]
6892 fn xpandbraces_no_braces_returns_input() {
6893 let _g = crate::test_util::global_state_lock();
6894 // Pure literal has nothing to expand — should yield the input
6895 // as a single element (or empty for empty input).
6896 let out = xpandbraces(&tok("plain"), false);
6897 assert_eq!(out, vec!["plain"]);
6898 }
6899
6900 // ── file_type: mode → marker char (used by `ls -F`-style output) ─
6901 #[test]
6902 fn file_type_dir_marker_is_slash() {
6903 let _g = crate::test_util::global_state_lock();
6904 assert_eq!(file_type(libc::S_IFDIR as u32), '/');
6905 }
6906
6907 #[test]
6908 fn file_type_regular_plain_is_space() {
6909 let _g = crate::test_util::global_state_lock();
6910 assert_eq!(file_type(libc::S_IFREG as u32), ' ');
6911 }
6912
6913 #[test]
6914 fn file_type_regular_executable_is_star() {
6915 let _g = crate::test_util::global_state_lock();
6916 // Any of the three executable bits should switch the marker.
6917 for x in [0o100, 0o010, 0o001] {
6918 assert_eq!(
6919 file_type(libc::S_IFREG as u32 | x),
6920 '*',
6921 "exec bit 0o{x:o} should produce '*'"
6922 );
6923 }
6924 }
6925
6926 #[test]
6927 fn file_type_symlink_is_at() {
6928 let _g = crate::test_util::global_state_lock();
6929 assert_eq!(file_type(libc::S_IFLNK as u32), '@');
6930 }
6931
6932 #[test]
6933 fn file_type_fifo_is_pipe() {
6934 let _g = crate::test_util::global_state_lock();
6935 assert_eq!(file_type(libc::S_IFIFO as u32), '|');
6936 }
6937
6938 #[test]
6939 fn file_type_socket_is_equal() {
6940 let _g = crate::test_util::global_state_lock();
6941 assert_eq!(file_type(libc::S_IFSOCK as u32), '=');
6942 }
6943
6944 // ═══════════════════════════════════════════════════════════════════
6945 // xpandbraces edge cases — anchored to `print -l <pattern>` in zsh 5.9.
6946 // Where zshrs diverges, the test FAILS to expose the bug.
6947 // ═══════════════════════════════════════════════════════════════════
6948
6949 /// `{1..10..2}` → 1 3 5 7 9 (step range). zsh: 5 elements.
6950 #[test]
6951 fn xpandbraces_numeric_step_two() {
6952 let _g = crate::test_util::global_state_lock();
6953 assert_eq!(
6954 xpandbraces(&tok("{1..10..2}"), false),
6955 vec!["1", "3", "5", "7", "9"]
6956 );
6957 }
6958
6959 /// `{10..1..2}` → 10 8 6 4 2 (descending step).
6960 #[test]
6961 fn xpandbraces_numeric_descending_step_two() {
6962 let _g = crate::test_util::global_state_lock();
6963 assert_eq!(
6964 xpandbraces(&tok("{10..1..2}"), false),
6965 vec!["10", "8", "6", "4", "2"]
6966 );
6967 }
6968
6969 /// `{5..1}` → 5 4 3 2 1 (descending range, no step).
6970 #[test]
6971 fn xpandbraces_numeric_descending_range() {
6972 let _g = crate::test_util::global_state_lock();
6973 assert_eq!(
6974 xpandbraces(&tok("{5..1}"), false),
6975 vec!["5", "4", "3", "2", "1"]
6976 );
6977 }
6978
6979 /// `{01..10}` → 01 02 03 ... 10 (zero-padded; pad preserved).
6980 #[test]
6981 fn xpandbraces_zero_padded_numeric_range() {
6982 let _g = crate::test_util::global_state_lock();
6983 assert_eq!(
6984 xpandbraces(&tok("{01..10}"), false),
6985 vec!["01", "02", "03", "04", "05", "06", "07", "08", "09", "10"]
6986 );
6987 }
6988
6989 /// `{001..010}` → 001 002 ... 010 (3-digit pad preserved).
6990 #[test]
6991 fn xpandbraces_three_digit_pad_preserved() {
6992 let _g = crate::test_util::global_state_lock();
6993 assert_eq!(
6994 xpandbraces(&tok("{001..010}"), false),
6995 vec!["001", "002", "003", "004", "005", "006", "007", "008", "009", "010"]
6996 );
6997 }
6998
6999 /// `\{a,b,c\}` → no expansion (escaped braces aren't brace
7000 /// delimiters). xpandbraces returns input unchanged; the lexer
7001 /// upstream strips the backslashes before this layer sees them.
7002 /// Anchored test: zsh `print -r -- \{a,b,c\}` → `{a,b,c}` because
7003 /// the lexer tokenizes `\{`/`\}` to `Bnull{`/`Bnull}` before
7004 /// xpandbraces runs, then untokenize drops the Bnulls. At the
7005 /// xpandbraces-unit level (raw backslashes), the only invariant
7006 /// to verify is "no expansion fires" — the brace literal survives.
7007 #[test]
7008 fn xpandbraces_escaped_braces_remain_literal_anchored_to_zsh() {
7009 let _g = crate::test_util::global_state_lock();
7010 // tok() emits Bnull+ASCII `{` for `\{` and Bnull+ASCII `}` for
7011 // `\}`, with Comma TOKEN for the unescaped commas inside.
7012 // xpandbraces scans for Inbrace TOKEN only (per
7013 // c:Src/glob.c::xpandbraces) — finds none here, so the
7014 // string passes through unchanged at this layer. The Bnulls
7015 // are stripped later by remnulargs in prefork.
7016 assert_eq!(
7017 xpandbraces(&tok("\\{a,b,c\\}"), false),
7018 vec![tok("\\{a,b,c\\}")],
7019 "xpandbraces unit: escaped braces survive without expansion (no per-element splat)"
7020 );
7021 }
7022
7023 /// `{a,b}{c,d}` → ac ad bc bd (Cartesian product).
7024 /// 2 * 2 = 4 elements, in row-major order.
7025 #[test]
7026 fn xpandbraces_cartesian_product_two_by_two() {
7027 let _g = crate::test_util::global_state_lock();
7028 assert_eq!(
7029 xpandbraces(&tok("{a,b}{c,d}"), false),
7030 vec!["ac", "ad", "bc", "bd"]
7031 );
7032 }
7033
7034 /// `{a,{b,c}}` → a b c (nested flattened).
7035 #[test]
7036 fn xpandbraces_nested_braces_flatten() {
7037 let _g = crate::test_util::global_state_lock();
7038 assert_eq!(xpandbraces(&tok("{a,{b,c}}"), false), vec!["a", "b", "c"]);
7039 }
7040
7041 /// `{}` → `{}` literal (empty brace doesn't expand). xpandbraces
7042 /// preserves TOKEN form at this layer (c:Src/glob.c::xpandbraces);
7043 /// ASCII conversion happens later in untokenize.
7044 #[test]
7045 fn xpandbraces_empty_braces_remain_literal() {
7046 let _g = crate::test_util::global_state_lock();
7047 assert_eq!(xpandbraces(&tok("{}"), false), vec![tok("{}")]);
7048 }
7049
7050 /// `a{b,c}d{e,f}` → abde abdf acde acdf (cartesian with surrounding text).
7051 #[test]
7052 fn xpandbraces_cartesian_with_surrounding_text() {
7053 let _g = crate::test_util::global_state_lock();
7054 assert_eq!(
7055 xpandbraces(&tok("a{b,c}d{e,f}"), false),
7056 vec!["abde", "abdf", "acde", "acdf"]
7057 );
7058 }
7059
7060 /// `{a..z..3}` → `{a..z..3}` literal (alpha range with step NOT expanded
7061 /// by zsh — surprising but verified). xpandbraces preserves TOKEN form
7062 /// at this layer; ASCII conversion happens later in untokenize.
7063 #[test]
7064 fn xpandbraces_alpha_step_unsupported_anchored_to_zsh() {
7065 let _g = crate::test_util::global_state_lock();
7066 assert_eq!(
7067 xpandbraces(&tok("{a..z..3}"), false),
7068 vec![tok("{a..z..3}")],
7069 "zsh: alpha range with step → literal (TOKEN form preserved at xpandbraces layer)"
7070 );
7071 }
7072
7073 // ═══════════════════════════════════════════════════════════════════
7074 // zsh test-corpus pins — direct anchors to Test/D09brace.ztst.
7075 // Tests verify brace expansion matches zsh's documented output.
7076 // ═══════════════════════════════════════════════════════════════════
7077
7078 /// `Test/D09brace.ztst:10-12` — basic brace expansion with
7079 /// nested range: `X{1,2,{3..6},7,8}Y` expands all 8 values.
7080 #[test]
7081 fn zsh_corpus_basic_brace_with_nested_range() {
7082 let _g = crate::test_util::global_state_lock();
7083 assert_eq!(
7084 xpandbraces(&tok("X{1,2,{3..6},7,8}Y"), false),
7085 vec!["X1Y", "X2Y", "X3Y", "X4Y", "X5Y", "X6Y", "X7Y", "X8Y"],
7086 "ztst:11 — basic brace expansion with nested range",
7087 );
7088 }
7089
7090 /// `Test/D09brace.ztst:30-32` — numeric range with leading zero
7091 /// padding: `X{01..4}Y` → all 4 values 0-padded to 2 digits.
7092 #[test]
7093 fn zsh_corpus_numeric_range_zero_padding() {
7094 let _g = crate::test_util::global_state_lock();
7095 assert_eq!(
7096 xpandbraces(&tok("X{01..4}Y"), false),
7097 vec!["X01Y", "X02Y", "X03Y", "X04Y"],
7098 "ztst:32 — leading-zero padding propagates to all values",
7099 );
7100 }
7101
7102 /// `Test/D09brace.ztst:34-36` — padding comes from RHS too:
7103 /// `X{1..04}Y` → 2-digit zero-padded.
7104 #[test]
7105 fn zsh_corpus_numeric_range_padding_from_rhs() {
7106 let _g = crate::test_util::global_state_lock();
7107 assert_eq!(
7108 xpandbraces(&tok("X{1..04}Y"), false),
7109 vec!["X01Y", "X02Y", "X03Y", "X04Y"],
7110 "ztst:36 — RHS padding `04` propagates",
7111 );
7112 }
7113
7114 /// `Test/D09brace.ztst:38-40` — unpadded range >9: `X{7..12}Y`
7115 /// numbers stay unpadded.
7116 #[test]
7117 fn zsh_corpus_numeric_range_no_padding_when_unspecified() {
7118 let _g = crate::test_util::global_state_lock();
7119 assert_eq!(
7120 xpandbraces(&tok("X{7..12}Y"), false),
7121 vec!["X7Y", "X8Y", "X9Y", "X10Y", "X11Y", "X12Y"],
7122 "ztst:40 — no padding when neither end has leading zero",
7123 );
7124 }
7125
7126 /// `Test/D09brace.ztst:42-44` — `X{07..12}Y` → 2-digit padded.
7127 #[test]
7128 fn zsh_corpus_numeric_range_lhs_padding_propagates() {
7129 let _g = crate::test_util::global_state_lock();
7130 assert_eq!(
7131 xpandbraces(&tok("X{07..12}Y"), false),
7132 vec!["X07Y", "X08Y", "X09Y", "X10Y", "X11Y", "X12Y"],
7133 "ztst:44 — LHS padding `07` propagates",
7134 );
7135 }
7136
7137 /// `Test/D09brace.ztst:46-48` — wider RHS padding overrides:
7138 /// `X{7..012}Y` → 3-digit padded.
7139 #[test]
7140 fn zsh_corpus_numeric_range_max_padding_width_wins() {
7141 let _g = crate::test_util::global_state_lock();
7142 assert_eq!(
7143 xpandbraces(&tok("X{7..012}Y"), false),
7144 vec!["X007Y", "X008Y", "X009Y", "X010Y", "X011Y", "X012Y"],
7145 "ztst:48 — widest padding width wins",
7146 );
7147 }
7148
7149 /// `Test/D09brace.ztst:50-52` — decreasing range: `X{4..1}Y` →
7150 /// 4,3,2,1 (reversed).
7151 #[test]
7152 fn zsh_corpus_numeric_range_decreasing() {
7153 let _g = crate::test_util::global_state_lock();
7154 assert_eq!(
7155 xpandbraces(&tok("X{4..1}Y"), false),
7156 vec!["X4Y", "X3Y", "X2Y", "X1Y"],
7157 "ztst:52 — decreasing range emits in reverse",
7158 );
7159 }
7160
7161 /// `Test/D09brace.ztst:54-56` — combined braces: `X{1..4}{1..4}Y`
7162 /// → 16-element cross product.
7163 #[test]
7164 fn zsh_corpus_combined_braces_cross_product() {
7165 let _g = crate::test_util::global_state_lock();
7166 assert_eq!(
7167 xpandbraces(&tok("X{1..4}{1..4}Y"), false),
7168 vec![
7169 "X11Y", "X12Y", "X13Y", "X14Y", "X21Y", "X22Y", "X23Y", "X24Y", "X31Y", "X32Y",
7170 "X33Y", "X34Y", "X41Y", "X42Y", "X43Y", "X44Y",
7171 ],
7172 "ztst:56 — combined-brace cross product",
7173 );
7174 }
7175
7176 /// `Test/D09brace.ztst:58-60` — negative numbers in range:
7177 /// `X{-4..4}Y` → `-4`..`4` (9 values including 0).
7178 #[test]
7179 fn zsh_corpus_negative_numbers_in_range() {
7180 let _g = crate::test_util::global_state_lock();
7181 assert_eq!(
7182 xpandbraces(&tok("X{-4..4}Y"), false),
7183 vec!["X-4Y", "X-3Y", "X-2Y", "X-1Y", "X0Y", "X1Y", "X2Y", "X3Y", "X4Y",],
7184 "ztst:60 — negative-to-positive range",
7185 );
7186 }
7187
7188 /// `Test/D09brace.ztst:62-64` — reverse-direction numeric range:
7189 /// `X{4..-4}Y` walks from 4 to -4 descending.
7190 #[test]
7191 fn zsh_corpus_brace_descending_range_from_positive_to_negative() {
7192 let _g = crate::test_util::global_state_lock();
7193 assert_eq!(
7194 xpandbraces(&tok("X{4..-4}Y"), false),
7195 vec!["X4Y", "X3Y", "X2Y", "X1Y", "X0Y", "X-1Y", "X-2Y", "X-3Y", "X-4Y",],
7196 "ztst:64 — descending 4..-4 produces 9 values",
7197 );
7198 }
7199
7200 /// `Test/D09brace.ztst:66-68` — stepped padded range:
7201 /// `X{004..-4..2}Y` = X004Y X002Y X000Y X-02Y X-04Y.
7202 /// Padding width 3 from LHS; step is +2 (sign of step is ignored,
7203 /// direction is from start to end).
7204 #[test]
7205 fn zsh_corpus_brace_stepped_padded_descending() {
7206 let _g = crate::test_util::global_state_lock();
7207 assert_eq!(
7208 xpandbraces(&tok("X{004..-4..2}Y"), false),
7209 vec!["X004Y", "X002Y", "X000Y", "X-02Y", "X-04Y"],
7210 "ztst:68 — stepped+padded descending",
7211 );
7212 }
7213
7214 /// `Test/D09brace.ztst:74-76` — step alignment 1: `X{1..32..3}Y`
7215 /// = X1Y X4Y X7Y X10Y X13Y X16Y X19Y X22Y X25Y X28Y X31Y.
7216 /// Step 3 starting from 1, capped at 32.
7217 #[test]
7218 fn zsh_corpus_brace_step_alignment_1_to_32_step_3() {
7219 let _g = crate::test_util::global_state_lock();
7220 assert_eq!(
7221 xpandbraces(&tok("X{1..32..3}Y"), false),
7222 vec![
7223 "X1Y", "X4Y", "X7Y", "X10Y", "X13Y", "X16Y", "X19Y", "X22Y", "X25Y", "X28Y",
7224 "X31Y",
7225 ],
7226 "ztst:76 — {{1..32..3}} step alignment",
7227 );
7228 }
7229
7230 /// `Test/D09brace.ztst:100-102` — `hey{a..j}there` — char range.
7231 #[test]
7232 fn zsh_corpus_brace_char_range_simple() {
7233 let _g = crate::test_util::global_state_lock();
7234 assert_eq!(
7235 xpandbraces(&tok("hey{a..j}there"), false),
7236 vec![
7237 "heyathere",
7238 "heybthere",
7239 "heycthere",
7240 "heydthere",
7241 "heyethere",
7242 "heyfthere",
7243 "heygthere",
7244 "heyhthere",
7245 "heyithere",
7246 "heyjthere",
7247 ],
7248 "ztst:102 — char range a..j",
7249 );
7250 }
7251
7252 /// `Test/D09brace.ztst:108-110` — reverse char range:
7253 /// `crumbs{y..p}ooh` walks down y → p (10 values).
7254 #[test]
7255 fn zsh_corpus_brace_char_range_reverse() {
7256 let _g = crate::test_util::global_state_lock();
7257 assert_eq!(
7258 xpandbraces(&tok("crumbs{y..p}ooh"), false),
7259 vec![
7260 "crumbsyooh",
7261 "crumbsxooh",
7262 "crumbswooh",
7263 "crumbsvooh",
7264 "crumbsuooh",
7265 "crumbstooh",
7266 "crumbssooh",
7267 "crumbsrooh",
7268 "crumbsqooh",
7269 "crumbspooh",
7270 ],
7271 "ztst:110 — char range y..p reverse",
7272 );
7273 }
7274
7275 /// `Test/D09brace.ztst:104-106` — nested brace: `gosh{1,{Z..a},2}cripes`
7276 /// — inner `{Z..a}` is char range Z to a in ASCII order (10 chars).
7277 /// Full output: gosh1cripes goshZcripes gosh[cripes gosh\cripes
7278 /// gosh]cripes gosh^cripes gosh_cripes gosh`cripes goshacripes
7279 /// gosh2cripes.
7280 #[test]
7281 fn zsh_corpus_brace_nested_with_char_range_ascii() {
7282 let _g = crate::test_util::global_state_lock();
7283 assert_eq!(
7284 xpandbraces(&tok("gosh{1,{Z..a},2}cripes"), false),
7285 vec![
7286 "gosh1cripes",
7287 "goshZcripes",
7288 "gosh[cripes",
7289 "gosh\\cripes",
7290 "gosh]cripes",
7291 "gosh^cripes",
7292 "gosh_cripes",
7293 "gosh`cripes",
7294 "goshacripes",
7295 "gosh2cripes",
7296 ],
7297 "ztst:106 — nested brace with ASCII char range",
7298 );
7299 }
7300
7301 /// `Test/D09brace.ztst:116-118` — unmatched closing brace after
7302 /// matched braces stays literal: `{1..10}{..` → `1{.. 2{.. ...`.
7303 /// `xpandbraces` itself preserves TOKEN form (Inbrace=`\u{8f}`,
7304 /// Outbrace=`\u{90}`) per the convention pinned by
7305 /// `xpandbraces_alpha_step_unsupported_anchored_to_zsh` — the
7306 /// later untokenize pass in the user-output pipeline turns
7307 /// surviving brace tokens into literal `{` / `}`. This test
7308 /// asserts the tokenized intermediate form.
7309 #[test]
7310 fn zsh_corpus_brace_unmatched_after_matched_left_literal() {
7311 let _g = crate::test_util::global_state_lock();
7312 let tokb = '\u{8f}'; // Inbrace
7313 let expected: Vec<String> = (1..=10).map(|n| format!("{}{}..", n, tokb)).collect();
7314 assert_eq!(
7315 xpandbraces(&tok("{1..10}{.."), false),
7316 expected,
7317 "ztst:118 — unmatched trailing {{.. preserved as tokenized Inbrace at xpandbraces layer",
7318 );
7319 }
7320
7321 // ── split_qualifier: parse trailing (qual) block ─────────────────
7322 /// `*.txt` (no trailing parens) → (input, None).
7323 #[test]
7324 fn split_qualifier_no_parens_returns_input_and_none() {
7325 let _g = crate::test_util::global_state_lock();
7326 let (head, qual) = split_qualifier("*.txt");
7327 assert_eq!(head, "*.txt");
7328 assert_eq!(qual, None);
7329 }
7330
7331 /// `*(N)` → ("*", Some("N")) — null-glob qualifier.
7332 #[test]
7333 fn split_qualifier_star_paren_N_extracts_null_glob_qual() {
7334 let _g = crate::test_util::global_state_lock();
7335 let (head, qual) = split_qualifier("*(N)");
7336 assert_eq!(head, "*");
7337 assert_eq!(qual, Some("N"));
7338 }
7339
7340 /// `*(.)` → ("*", Some(".")) — regular-file qualifier.
7341 #[test]
7342 fn split_qualifier_star_paren_dot_extracts_regular_file_qual() {
7343 let _g = crate::test_util::global_state_lock();
7344 let (head, qual) = split_qualifier("*(.)");
7345 assert_eq!(head, "*");
7346 assert_eq!(qual, Some("."));
7347 }
7348
7349 /// `*.rs(.)` → ("*.rs", Some(".")) — qualifier on glob pattern.
7350 #[test]
7351 fn split_qualifier_pattern_with_qual_extracts_correctly() {
7352 let _g = crate::test_util::global_state_lock();
7353 let (head, qual) = split_qualifier("*.rs(.)");
7354 assert_eq!(head, "*.rs");
7355 assert_eq!(qual, Some("."));
7356 }
7357
7358 /// `*(#qN)` → ("*", Some("N")) — `#q` prefix stripped per zsh syntax.
7359 #[test]
7360 fn split_qualifier_hash_q_prefix_stripped() {
7361 let _g = crate::test_util::global_state_lock();
7362 let (head, qual) = split_qualifier("*(#qN)");
7363 assert_eq!(head, "*");
7364 assert_eq!(qual, Some("N"));
7365 }
7366
7367 /// `*(om[1])` → ("*", Some("om[1]")) — sort-by-mtime keep first.
7368 #[test]
7369 fn split_qualifier_multichar_qual_with_brackets() {
7370 let _g = crate::test_util::global_state_lock();
7371 let (head, qual) = split_qualifier("*(om[1])");
7372 assert_eq!(head, "*");
7373 assert_eq!(qual, Some("om[1]"));
7374 }
7375
7376 /// `(a)(b)` — multiple paren groups: split takes the OUTERMOST trailing.
7377 #[test]
7378 fn split_qualifier_multiple_groups_takes_outermost_trailing() {
7379 let _g = crate::test_util::global_state_lock();
7380 let (head, qual) = split_qualifier("(a)(b)");
7381 assert_eq!(head, "(a)");
7382 assert_eq!(qual, Some("b"));
7383 }
7384
7385 // ═══════════════════════════════════════════════════════════════════
7386 // parse_qualifier_string — file-type / permission / sort qualifiers.
7387 // Tests call the private fn directly (same crate, same file).
7388 // Each qualifier letter maps to a `qualifier::*` variant per the
7389 // zsh glob.c arm at c:1495+. Pin the parse, not the match — the
7390 // match path needs a real filesystem.
7391 //
7392 // IMPORTANT: at end of parse_qualifier_string (c:3433-3435 in this
7393 // file), if `qualifiers` is non-empty it gets moved into
7394 // `alternatives[]`. So tests read the qualifier from `alternatives[0]`
7395 // — qualifiers itself is empty after the move.
7396 // ═══════════════════════════════════════════════════════════════════
7397
7398 /// Helper: read the only qualifier from a single-letter parse result.
7399 fn first_qual(qs: &qualifier_set) -> &qualifier {
7400 // After the post-loop move, single-letter input lands in
7401 // alternatives[0][0]. Fall back to qualifiers[0] for safety in
7402 // case the move ever changes.
7403 if !qs.alternatives.is_empty() && !qs.alternatives[0].is_empty() {
7404 &qs.alternatives[0][0]
7405 } else {
7406 &qs.qualifiers[0]
7407 }
7408 }
7409
7410 /// Empty qualifier body → no alternatives, no qualifiers.
7411 #[test]
7412 fn parse_qualifier_string_empty_returns_empty_set() {
7413 let _g = crate::test_util::global_state_lock();
7414 let qs = parse_qualifier_string("");
7415 assert!(qs.qualifiers.is_empty());
7416 assert!(qs.alternatives.is_empty());
7417 assert!(!qs.nullglob);
7418 }
7419
7420 /// `/` → IsDirectory (in alternatives[0]).
7421 #[test]
7422 fn parse_qualifier_string_slash_is_directory() {
7423 let _g = crate::test_util::global_state_lock();
7424 let qs = parse_qualifier_string("/");
7425 assert!(matches!(first_qual(&qs), qualifier::IsDirectory));
7426 }
7427
7428 /// `.` → IsRegular.
7429 #[test]
7430 fn parse_qualifier_string_dot_is_regular() {
7431 let _g = crate::test_util::global_state_lock();
7432 let qs = parse_qualifier_string(".");
7433 assert!(matches!(first_qual(&qs), qualifier::IsRegular));
7434 }
7435
7436 /// `parsecomplist` splits a path into per-component `complist` nodes,
7437 /// each literal section a PAT_PURES Patprog (relies on patcompile's
7438 /// PAT_FILE PURES segment-stop at `/`, pattern.c:584-610).
7439 #[test]
7440 fn parsecomplist_splits_path_components() {
7441 let _g = crate::test_util::global_state_lock();
7442 fn pure(p: &crate::ported::pattern::Patprog) -> Option<String> {
7443 if (p.0.flags & crate::ported::zsh_h::PAT_PURES as i32) != 0 {
7444 let off = p.0.startoff as usize;
7445 let l = p.0.patmlen as usize;
7446 p.1.get(off..off + l)
7447 .map(|b| String::from_utf8_lossy(b).into_owned())
7448 } else {
7449 None
7450 }
7451 }
7452 fn sections(mut q: &complist) -> Vec<String> {
7453 let mut out = Vec::new();
7454 loop {
7455 out.push(pure(&q.pat).unwrap_or_else(|| "<pat>".into()));
7456 match &q.next {
7457 Some(n) => q = n,
7458 None => break,
7459 }
7460 }
7461 out
7462 }
7463 // Literal path → split into per-component PAT_PURES sections.
7464 let mut t = "a/b/c.txt".to_string();
7465 tokenize(&mut t);
7466 let cl = parsecomplist(&t).expect("parsecomplist None");
7467 assert_eq!(sections(&cl), vec!["a", "b", "c.txt"]);
7468
7469 // Literal prefix before a pattern still splits the literal dirs;
7470 // the trailing glob section is a real pattern (not PAT_PURES).
7471 let mut t2 = "a/b/*.txt".to_string();
7472 tokenize(&mut t2);
7473 let cl2 = parsecomplist(&t2).expect("parsecomplist None");
7474 let s2 = sections(&cl2);
7475 assert_eq!(s2.len(), 3, "a/b/*.txt → 3 sections, got {s2:?}");
7476 assert_eq!(&s2[..2], &["a".to_string(), "b".to_string()]);
7477 assert_eq!(s2[2], "<pat>", "trailing *.txt must be a pattern section");
7478
7479 // Pattern-FIRST then literal dir: */foo → [<pat>, foo].
7480 let mut t3 = "*/foo".to_string();
7481 tokenize(&mut t3);
7482 let cl3 = parsecomplist(&t3).expect("parsecomplist None for */foo");
7483 let s3 = sections(&cl3);
7484 assert_eq!(s3.len(), 2, "*/foo → 2 sections, got {s3:?}");
7485 assert_eq!(s3[0], "<pat>", "leading * must be a pattern section");
7486 assert_eq!(s3[1], "foo");
7487 }
7488
7489 /// The `struct qual` arena is built from the parse alongside the enum:
7490 /// AND via `next`, alternatives via `or`, per-node `sense` from `^`.
7491 #[test]
7492 fn parse_qualifier_string_builds_qual_arena() {
7493 let _g = crate::test_util::global_state_lock();
7494 // single qualifier → one node, sense 0, head set, func present.
7495 let q = parse_qualifier_string(".");
7496 assert_eq!(q.quals.nodes.len(), 1);
7497 assert_eq!(q.quals.head, Some(0));
7498 assert_eq!(q.quals.nodes[0].sense, 0);
7499 assert!(q.quals.nodes[0].func.is_some());
7500 assert_eq!(q.quals.nodes[0].next, None);
7501
7502 // leading `^` → per-node sense flipped (c:1346).
7503 let q = parse_qualifier_string("^.");
7504 assert_eq!(q.quals.nodes.len(), 1);
7505 assert_eq!(q.quals.nodes[0].sense, 1);
7506
7507 // `/^@` → AND-chain of 2: dir(sense 0) -next-> symlink(sense 1).
7508 let q = parse_qualifier_string("/^@");
7509 assert_eq!(q.quals.nodes.len(), 2);
7510 assert_eq!(q.quals.nodes[0].sense, 0);
7511 assert_eq!(q.quals.nodes[0].next, Some(1));
7512 assert_eq!(q.quals.nodes[1].sense, 1);
7513 assert_eq!(q.quals.nodes[1].next, None);
7514
7515 // `.,/` → two alternatives: node0 -or-> node1, neither chained.
7516 let q = parse_qualifier_string(".,/");
7517 assert_eq!(q.quals.nodes.len(), 2);
7518 assert_eq!(q.quals.nodes[0].or, Some(1));
7519 assert_eq!(q.quals.nodes[0].next, None);
7520 assert_eq!(q.quals.head, Some(0));
7521 }
7522
7523 /// `@` → IsSymlink.
7524 #[test]
7525 fn parse_qualifier_string_at_is_symlink() {
7526 let _g = crate::test_util::global_state_lock();
7527 let qs = parse_qualifier_string("@");
7528 assert!(matches!(first_qual(&qs), qualifier::IsSymlink));
7529 }
7530
7531 /// `=` → IsSocket.
7532 #[test]
7533 fn parse_qualifier_string_equals_is_socket() {
7534 let _g = crate::test_util::global_state_lock();
7535 let qs = parse_qualifier_string("=");
7536 assert!(matches!(first_qual(&qs), qualifier::IsSocket));
7537 }
7538
7539 /// `p` → IsFifo.
7540 #[test]
7541 fn parse_qualifier_string_p_is_fifo() {
7542 let _g = crate::test_util::global_state_lock();
7543 let qs = parse_qualifier_string("p");
7544 assert!(matches!(first_qual(&qs), qualifier::IsFifo));
7545 }
7546
7547 /// `*` → IsExecutable.
7548 #[test]
7549 fn parse_qualifier_string_star_is_executable() {
7550 let _g = crate::test_util::global_state_lock();
7551 let qs = parse_qualifier_string("*");
7552 assert!(matches!(first_qual(&qs), qualifier::IsExecutable));
7553 }
7554
7555 /// `%b` → IsBlockDev.
7556 #[test]
7557 fn parse_qualifier_string_pct_b_is_block_device() {
7558 let _g = crate::test_util::global_state_lock();
7559 let qs = parse_qualifier_string("%b");
7560 assert!(matches!(first_qual(&qs), qualifier::IsBlockDev));
7561 }
7562
7563 /// `%c` → IsCharDev.
7564 #[test]
7565 fn parse_qualifier_string_pct_c_is_char_device() {
7566 let _g = crate::test_util::global_state_lock();
7567 let qs = parse_qualifier_string("%c");
7568 assert!(matches!(first_qual(&qs), qualifier::IsCharDev));
7569 }
7570
7571 /// `r` / `w` / `x` → Readable / Writable / Executable.
7572 #[test]
7573 fn parse_qualifier_string_perm_letters_map_to_perm_qualifiers() {
7574 let _g = crate::test_util::global_state_lock();
7575 for (letter, expected) in [
7576 ("r", qualifier::Readable),
7577 ("w", qualifier::Writable),
7578 ("x", qualifier::Executable),
7579 ] {
7580 let qs = parse_qualifier_string(letter);
7581 assert_eq!(
7582 std::mem::discriminant(first_qual(&qs)),
7583 std::mem::discriminant(&expected),
7584 "letter {letter:?} should map to {expected:?}"
7585 );
7586 }
7587 }
7588
7589 /// `U` → OwnedByEuid (process EUID).
7590 #[test]
7591 fn parse_qualifier_string_capital_U_is_owned_by_euid() {
7592 let _g = crate::test_util::global_state_lock();
7593 let qs = parse_qualifier_string("U");
7594 assert!(matches!(first_qual(&qs), qualifier::OwnedByEuid));
7595 }
7596
7597 /// `G` → OwnedByEgid.
7598 #[test]
7599 fn parse_qualifier_string_capital_G_is_owned_by_egid() {
7600 let _g = crate::test_util::global_state_lock();
7601 let qs = parse_qualifier_string("G");
7602 assert!(matches!(first_qual(&qs), qualifier::OwnedByEgid));
7603 }
7604
7605 /// Multiple file-type qualifiers stack — `./` → IsRegular then IsDirectory
7606 /// both end up in alternatives[0].
7607 #[test]
7608 fn parse_qualifier_string_multiple_letters_stack_in_one_alternative() {
7609 let _g = crate::test_util::global_state_lock();
7610 let qs = parse_qualifier_string("./");
7611 assert_eq!(qs.alternatives.len(), 1);
7612 assert_eq!(qs.alternatives[0].len(), 2);
7613 assert!(matches!(qs.alternatives[0][0], qualifier::IsRegular));
7614 assert!(matches!(qs.alternatives[0][1], qualifier::IsDirectory));
7615 }
7616
7617 /// `,` separates alternatives — `/,.` → two alternatives, each with one
7618 /// qualifier (IsDirectory and IsRegular respectively).
7619 #[test]
7620 fn parse_qualifier_string_comma_creates_two_alternatives() {
7621 let _g = crate::test_util::global_state_lock();
7622 let qs = parse_qualifier_string("/,.");
7623 assert_eq!(qs.alternatives.len(), 2, "two alternatives expected");
7624 assert!(matches!(qs.alternatives[0][0], qualifier::IsDirectory));
7625 assert!(matches!(qs.alternatives[1][0], qualifier::IsRegular));
7626 }
7627
7628 /// `:` introduces colon-modifiers; rest captured into colon_mods.
7629 #[test]
7630 fn parse_qualifier_string_colon_captures_modifiers_in_field() {
7631 let _g = crate::test_util::global_state_lock();
7632 let qs = parse_qualifier_string(":h");
7633 assert_eq!(qs.colon_mods.as_deref(), Some(":h"));
7634 }
7635
7636 /// `:` after a qualifier captures BOTH.
7637 #[test]
7638 fn parse_qualifier_string_qualifier_then_colon_mod() {
7639 let _g = crate::test_util::global_state_lock();
7640 let qs = parse_qualifier_string("/:h");
7641 assert!(matches!(first_qual(&qs), qualifier::IsDirectory));
7642 assert_eq!(qs.colon_mods.as_deref(), Some(":h"));
7643 }
7644
7645 /// `N` flag → nullglob set, no qualifier pushed.
7646 #[test]
7647 fn parse_qualifier_string_capital_N_sets_nullglob_flag() {
7648 let _g = crate::test_util::global_state_lock();
7649 let qs = parse_qualifier_string("N");
7650 assert!(qs.nullglob, "(N) must set nullglob");
7651 assert!(qs.alternatives.is_empty(), "N doesn't push to qualifiers");
7652 }
7653
7654 /// `M` flag → mark_dirs.
7655 #[test]
7656 fn parse_qualifier_string_capital_M_sets_mark_dirs_flag() {
7657 let _g = crate::test_util::global_state_lock();
7658 let qs = parse_qualifier_string("M");
7659 assert!(qs.mark_dirs, "(M) must set mark_dirs");
7660 }
7661
7662 /// `T` flag → list_types.
7663 #[test]
7664 fn parse_qualifier_string_capital_T_sets_list_types_flag() {
7665 let _g = crate::test_util::global_state_lock();
7666 let qs = parse_qualifier_string("T");
7667 assert!(qs.list_types, "(T) must set list_types");
7668 }
7669
7670 // ── split_qualifier integration with parse_qualifier_string ──────
7671 /// `*(.)` — split yields ("*", Some(".")); parse yields IsRegular.
7672 #[test]
7673 fn split_then_parse_dot_qualifier_yields_is_regular() {
7674 let _g = crate::test_util::global_state_lock();
7675 let (head, qual) = split_qualifier("*(.)");
7676 assert_eq!(head, "*");
7677 let qs = parse_qualifier_string(qual.unwrap());
7678 assert!(matches!(first_qual(&qs), qualifier::IsRegular));
7679 }
7680
7681 /// `*(/)` — directory-only glob.
7682 #[test]
7683 fn split_then_parse_slash_qualifier_yields_is_directory() {
7684 let _g = crate::test_util::global_state_lock();
7685 let (_, qual) = split_qualifier("*(/)");
7686 let qs = parse_qualifier_string(qual.unwrap());
7687 assert!(matches!(first_qual(&qs), qualifier::IsDirectory));
7688 }
7689
7690 // ═══════════════════════════════════════════════════════════════════
7691 // Sort qualifiers — `o<key>` ascending, `O<key>` descending.
7692 // ═══════════════════════════════════════════════════════════════════
7693
7694 /// `oN` — sort by NONE (no sorting key requested).
7695 #[test]
7696 fn parse_qualifier_string_oN_pushes_gs_none() {
7697 let _g = crate::test_util::global_state_lock();
7698 let qs = parse_qualifier_string("oN");
7699 assert_eq!(qs.sorts.len(), 1);
7700 assert_ne!(qs.sorts[0] & GS_NONE, 0, "oN must set GS_NONE bit");
7701 }
7702
7703 /// `on` — sort by name ascending.
7704 #[test]
7705 fn parse_qualifier_string_on_pushes_gs_name_ascending() {
7706 let _g = crate::test_util::global_state_lock();
7707 let qs = parse_qualifier_string("on");
7708 assert_eq!(qs.sorts.len(), 1);
7709 assert_eq!(qs.sorts[0] & GS_NAME, GS_NAME);
7710 assert_eq!(qs.sorts[0] & GS_DESC, 0);
7711 }
7712
7713 /// `On` — sort by name descending.
7714 #[test]
7715 fn parse_qualifier_string_On_pushes_gs_name_descending() {
7716 let _g = crate::test_util::global_state_lock();
7717 let qs = parse_qualifier_string("On");
7718 assert_eq!(qs.sorts.len(), 1);
7719 assert_eq!(qs.sorts[0] & GS_NAME, GS_NAME);
7720 assert_eq!(qs.sorts[0] & GS_DESC, GS_DESC);
7721 }
7722
7723 /// Multiple sort keys stack.
7724 #[test]
7725 fn parse_qualifier_string_chained_sort_keys_stack() {
7726 let _g = crate::test_util::global_state_lock();
7727 let qs = parse_qualifier_string("onOL");
7728 assert_eq!(qs.sorts.len(), 2);
7729 }
7730
7731 // ── Size qualifier — `L<unit><op><value>` ───────────────────────
7732 // NOTE: zshrs's parse_range_spec normalises `+` (greater) → '>' and
7733 // `-` (less) → '<' for the internal op char, matching C's qgetnum
7734 // convention (Src/glob.c c:1620-ish).
7735
7736 /// `Lk+1` — size > 1 kilobyte. zsh syntax: `L<unit><op><value>`
7737 /// (unit char BEFORE op).
7738 #[test]
7739 fn parse_qualifier_string_Lk_plus_one_size_kilobyte() {
7740 let _g = crate::test_util::global_state_lock();
7741 let qs = parse_qualifier_string("Lk+1");
7742 match first_qual(&qs) {
7743 qualifier::Size { value, unit, op } => {
7744 assert_eq!(*value, 1);
7745 assert_eq!(*unit, TT_KILOBYTES);
7746 assert_eq!(*op, '>', "+ is stored as '>' (greater than)");
7747 }
7748 other => panic!("expected Size, got {other:?}"),
7749 }
7750 }
7751
7752 /// `L-100` — size < 100 (default unit bytes). Stored op is '<'.
7753 #[test]
7754 fn parse_qualifier_string_L_minus_100_size_bytes() {
7755 let _g = crate::test_util::global_state_lock();
7756 let qs = parse_qualifier_string("L-100");
7757 match first_qual(&qs) {
7758 qualifier::Size { value, unit, op } => {
7759 assert_eq!(*value, 100);
7760 assert_eq!(*unit, TT_BYTES);
7761 assert_eq!(*op, '<', "- is stored as '<' (less than)");
7762 }
7763 other => panic!("expected Size, got {other:?}"),
7764 }
7765 }
7766
7767 /// `Lm+1` — size > 1 megabyte; unit recognised.
7768 #[test]
7769 fn parse_qualifier_string_Lm_megabyte_unit() {
7770 let _g = crate::test_util::global_state_lock();
7771 let qs = parse_qualifier_string("Lm+1");
7772 match first_qual(&qs) {
7773 qualifier::Size { unit, .. } => assert_eq!(*unit, TT_MEGABYTES),
7774 other => panic!("expected Size, got {other:?}"),
7775 }
7776 }
7777
7778 // ── Time qualifier — same op normalisation ───────────────────────
7779 /// `m-1` — modified less than 1 day ago. Stored op '<'.
7780 #[test]
7781 fn parse_qualifier_string_m_minus_1_day_default_unit() {
7782 let _g = crate::test_util::global_state_lock();
7783 let qs = parse_qualifier_string("m-1");
7784 match first_qual(&qs) {
7785 qualifier::Mtime { value, unit, op } => {
7786 assert_eq!(*value, 1);
7787 assert_eq!(*unit, TT_DAYS);
7788 assert_eq!(*op, '<');
7789 }
7790 other => panic!("expected Mtime, got {other:?}"),
7791 }
7792 }
7793
7794 /// `mh+24` — modified more than 24 hours ago. Stored op '>'.
7795 #[test]
7796 fn parse_qualifier_string_mh_plus_24_hours() {
7797 let _g = crate::test_util::global_state_lock();
7798 let qs = parse_qualifier_string("mh+24");
7799 match first_qual(&qs) {
7800 qualifier::Mtime { value, unit, op } => {
7801 assert_eq!(*value, 24);
7802 assert_eq!(*unit, TT_HOURS);
7803 assert_eq!(*op, '>');
7804 }
7805 other => panic!("expected Mtime, got {other:?}"),
7806 }
7807 }
7808
7809 /// `a-7` — accessed less than 7 days ago. Stored op '<'.
7810 #[test]
7811 fn parse_qualifier_string_a_minus_7_days() {
7812 let _g = crate::test_util::global_state_lock();
7813 let qs = parse_qualifier_string("a-7");
7814 match first_qual(&qs) {
7815 qualifier::Atime { value, unit, op } => {
7816 assert_eq!(*value, 7);
7817 assert_eq!(*unit, TT_DAYS);
7818 assert_eq!(*op, '<');
7819 }
7820 other => panic!("expected Atime, got {other:?}"),
7821 }
7822 }
7823
7824 // ── Subscript qualifier — `[N]` keep first ───────────────────────
7825 /// `[1]` — first entry only.
7826 #[test]
7827 fn parse_qualifier_string_bracket_one_sets_first_to_one() {
7828 let _g = crate::test_util::global_state_lock();
7829 let qs = parse_qualifier_string("[1]");
7830 assert_eq!(qs.first, Some(1));
7831 }
7832
7833 /// `om[1]` — sort by mtime, keep first.
7834 #[test]
7835 fn parse_qualifier_string_om_bracket_one_sort_and_subscript() {
7836 let _g = crate::test_util::global_state_lock();
7837 let qs = parse_qualifier_string("om[1]");
7838 assert_eq!(qs.sorts.len(), 1);
7839 assert_eq!(qs.sorts[0] & GS_MTIME, GS_MTIME);
7840 assert_eq!(qs.first, Some(1));
7841 }
7842
7843 // ═══════════════════════════════════════════════════════════════════
7844 // tokenize / remnulargs C-parity tests — pin Src/glob.c:3551 (tokenize)
7845 // and Src/glob.c:3649 (remnulargs). These two functions form the
7846 // input → glob-token → output sandwich every glob pattern walks
7847 // through; regressions silently corrupt every pattern match.
7848 // Tests that capture KNOWN ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"].
7849 // ═══════════════════════════════════════════════════════════════════
7850
7851 /// `tokenize("abc")` is a no-op for plain ASCII — no glob metachars.
7852 /// C glob.c:3551 — `for (; (c = *s); s++) zstrtok(...)` — only
7853 /// recognized glob bytes get replaced with their token form.
7854 #[test]
7855 fn tokenize_pure_ascii_unchanged() {
7856 let _g = crate::test_util::global_state_lock();
7857 let mut s = String::from("abc");
7858 tokenize(&mut s);
7859 assert_eq!(s, "abc");
7860 }
7861
7862 /// `tokenize("")` on empty input returns empty unchanged.
7863 #[test]
7864 fn tokenize_empty_unchanged() {
7865 let _g = crate::test_util::global_state_lock();
7866 let mut s = String::new();
7867 tokenize(&mut s);
7868 assert_eq!(s, "");
7869 }
7870
7871 /// `remnulargs("abc")` is a no-op for pure ASCII (no inull bytes).
7872 /// C glob.c:3649-3680 — only Snull/Dnull/Bnull/Bnullkeep/Nularg
7873 /// are stripped or transformed.
7874 #[test]
7875 fn remnulargs_pure_ascii_unchanged() {
7876 let _g = crate::test_util::global_state_lock();
7877 let mut s = String::from("hello");
7878 remnulargs(&mut s);
7879 assert_eq!(s, "hello");
7880 }
7881
7882 /// `remnulargs("")` returns empty unchanged (c:3653 early exit).
7883 #[test]
7884 fn remnulargs_empty_returns_empty() {
7885 let _g = crate::test_util::global_state_lock();
7886 let mut s = String::new();
7887 remnulargs(&mut s);
7888 assert_eq!(s, "");
7889 }
7890
7891 /// `remnulargs` strips Snull (single-quote scope marker).
7892 /// C glob.c:3656 — inull predicate includes Snull (0x84).
7893 /// Input `"a\u{84}b"` → `"ab"` (strip the Snull byte).
7894 #[test]
7895 fn remnulargs_strips_snull_marker() {
7896 let _g = crate::test_util::global_state_lock();
7897 let mut s = format!("a{}b", crate::ported::zsh_h::Snull);
7898 remnulargs(&mut s);
7899 assert_eq!(s, "ab", "Snull byte should be stripped");
7900 }
7901
7902 /// `remnulargs` strips Dnull (double-quote scope marker).
7903 #[test]
7904 fn remnulargs_strips_dnull_marker() {
7905 let _g = crate::test_util::global_state_lock();
7906 let mut s = format!("a{}b", crate::ported::zsh_h::Dnull);
7907 remnulargs(&mut s);
7908 assert_eq!(s, "ab", "Dnull byte should be stripped");
7909 }
7910
7911 /// `remnulargs` strips Bnull (backslash-quoted marker — c:3658
7912 /// `inull(c)` includes Bnull). The non-Bnullkeep variant of the
7913 /// active-backslash marker.
7914 #[test]
7915 fn remnulargs_strips_bnull_marker() {
7916 let _g = crate::test_util::global_state_lock();
7917 let mut s = format!("a{}b", crate::ported::zsh_h::Bnull);
7918 remnulargs(&mut s);
7919 assert_eq!(s, "ab", "Bnull byte should be stripped");
7920 }
7921
7922 /// `remnulargs` on a single Nularg returns the Nularg sentinel
7923 /// preserved. C glob.c:3690-3692 — if post-strip output is empty
7924 /// AND original had any inull, emit Nularg as the empty-arg marker.
7925 #[test]
7926 fn remnulargs_empty_post_strip_emits_nularg_sentinel() {
7927 let _g = crate::test_util::global_state_lock();
7928 let mut s = format!("{}", crate::ported::zsh_h::Snull);
7929 remnulargs(&mut s);
7930 assert_eq!(
7931 s,
7932 format!("{}", crate::ported::zsh_h::Nularg),
7933 "all-inull input should collapse to single Nularg marker"
7934 );
7935 }
7936
7937 /// `remnulargs` on the Bnullkeep marker: c:3669 — Bnullkeep
7938 /// either (a) preserved as-is when it appears BEFORE any other
7939 /// inull (scan phase), or (b) transformed to literal `\` (copy
7940 /// phase). Pin the simpler case: a lone Bnullkeep is stripped.
7941 #[test]
7942 fn remnulargs_lone_bnullkeep_stripped() {
7943 let _g = crate::test_util::global_state_lock();
7944 let mut s = format!("a{}b", crate::ported::zsh_h::Bnullkeep);
7945 remnulargs(&mut s);
7946 // C behavior depends on whether the byte is in scan or copy
7947 // phase. Faithful port should give either "ab" (scan strip)
7948 // or "a\\b" (copy phase materialize). Either is acceptable;
7949 // pin that it's NOT the raw input.
7950 assert!(
7951 s == "ab" || s == "a\\b",
7952 "Bnullkeep should be stripped or materialized; got {s:?}"
7953 );
7954 }
7955
7956 // ═══════════════════════════════════════════════════════════════════
7957 // Additional C-parity tests for Src/glob.c file_type + hasbraces.
7958 // ═══════════════════════════════════════════════════════════════════
7959
7960 /// c:2018 — file_type for regular file with no exec bits → ' '.
7961 #[test]
7962 #[cfg(unix)]
7963 fn file_type_regular_non_exec_returns_space() {
7964 let r = file_type(libc::S_IFREG as u32 | 0o644);
7965 assert_eq!(r, ' ');
7966 }
7967
7968 /// c:2018 — file_type for regular file WITH exec bit → '*'.
7969 #[test]
7970 #[cfg(unix)]
7971 fn file_type_regular_exec_returns_star() {
7972 let r = file_type(libc::S_IFREG as u32 | 0o755);
7973 assert_eq!(r, '*');
7974 }
7975
7976 /// c:2018 — file_type for directory → '/'.
7977 #[test]
7978 #[cfg(unix)]
7979 fn file_type_directory_returns_slash() {
7980 let r = file_type(libc::S_IFDIR as u32 | 0o755);
7981 assert_eq!(r, '/');
7982 }
7983
7984 /// c:2018 — file_type for symlink → '@'.
7985 #[test]
7986 #[cfg(unix)]
7987 fn file_type_symlink_returns_at() {
7988 let r = file_type(libc::S_IFLNK as u32 | 0o777);
7989 assert_eq!(r, '@');
7990 }
7991
7992 /// c:2018 — file_type for FIFO → '|'.
7993 #[test]
7994 #[cfg(unix)]
7995 fn file_type_fifo_returns_pipe() {
7996 let r = file_type(libc::S_IFIFO as u32 | 0o644);
7997 assert_eq!(r, '|');
7998 }
7999
8000 /// c:2018 — file_type for socket → '='.
8001 #[test]
8002 #[cfg(unix)]
8003 fn file_type_socket_returns_equals() {
8004 let r = file_type(libc::S_IFSOCK as u32 | 0o777);
8005 assert_eq!(r, '=');
8006 }
8007
8008 /// c:2018 — file_type for char device → '%'.
8009 #[test]
8010 #[cfg(unix)]
8011 fn file_type_char_device_returns_percent() {
8012 let r = file_type(libc::S_IFCHR as u32 | 0o644);
8013 assert_eq!(r, '%');
8014 }
8015
8016 /// c:2018 — file_type for block device → '#'.
8017 #[test]
8018 #[cfg(unix)]
8019 fn file_type_block_device_returns_hash() {
8020 let r = file_type(libc::S_IFBLK as u32 | 0o644);
8021 assert_eq!(r, '#');
8022 }
8023
8024 /// c:2018 — file_type for unknown mode → '?'.
8025 #[test]
8026 fn file_type_unknown_mode_returns_question() {
8027 // Bare 0 mode (none of the S_IF* bits set) → '?'.
8028 assert_eq!(file_type(0), '?');
8029 }
8030
8031 /// c:2018 — file_type is deterministic.
8032 #[test]
8033 #[cfg(unix)]
8034 fn file_type_is_deterministic() {
8035 for mode in [
8036 libc::S_IFREG as u32 | 0o644,
8037 libc::S_IFDIR as u32,
8038 libc::S_IFLNK as u32,
8039 0u32,
8040 ] {
8041 let first = file_type(mode);
8042 for _ in 0..5 {
8043 assert_eq!(file_type(mode), first);
8044 }
8045 }
8046 }
8047
8048 /// c:2042 — hasbraces on plain string returns false.
8049 #[test]
8050 fn hasbraces_no_braces_returns_false() {
8051 let _g = crate::test_util::global_state_lock();
8052 assert!(!hasbraces("hello", false));
8053 assert!(!hasbraces("", false));
8054 }
8055
8056 /// c:2042 — hasbraces on plain ASCII `{a,b}` returns FALSE because
8057 /// the C port checks TOKEN bytes (Inbrace=0x8f / Outbrace=0x90 /
8058 /// Comma=0x9a) not literal ASCII. The lexer pre-tokenizes input
8059 /// before hasbraces runs in the real pipeline. Pin the actual
8060 /// behavior so a regen that adds ASCII fast-path is caught.
8061 #[test]
8062 fn hasbraces_plain_ascii_braces_returns_false_uses_tokens() {
8063 let _g = crate::test_util::global_state_lock();
8064 assert!(
8065 !hasbraces("{a,b}", false),
8066 "plain ASCII not tokenized → false; lexer pre-tokenizes in real pipeline"
8067 );
8068 }
8069
8070 /// c:2042 — hasbraces on tokenized brace expansion returns true.
8071 /// Construct the TOKEN form: Inbrace + 'a' + Comma + 'b' + Outbrace.
8072 #[test]
8073 fn hasbraces_tokenized_brace_expansion_returns_true() {
8074 let _g = crate::test_util::global_state_lock();
8075 let tokenized = format!(
8076 "{}a{}b{}",
8077 '\u{8f}', // Inbrace token
8078 '\u{9a}', // Comma token
8079 '\u{90}' // Outbrace token
8080 );
8081 assert!(
8082 hasbraces(&tokenized, false),
8083 "Inbrace+a+Comma+b+Outbrace must trigger brace expansion"
8084 );
8085 }
8086
8087 /// c:2042 — hasbraces on unclosed `{` returns false (no matching pair).
8088 #[test]
8089 fn hasbraces_unclosed_returns_false() {
8090 let _g = crate::test_util::global_state_lock();
8091 assert!(!hasbraces("{", false));
8092 assert!(!hasbraces("{abc", false));
8093 }
8094
8095 // ═══════════════════════════════════════════════════════════════════
8096 // Additional C-parity tests for Src/glob.c
8097 // c:950 qgetnum / c:994 qgetmodespec / c:1250 glob_exec_string /
8098 // c:1351 checkglobqual / c:1539 hasbraces / c:1617 bracechardots /
8099 // c:1802 matchpat / c:1899 getmatch / c:1505 file_type
8100 // ═══════════════════════════════════════════════════════════════════
8101
8102 /// c:950 — `qgetnum("")` empty returns None.
8103 #[test]
8104 fn qgetnum_empty_returns_none() {
8105 assert!(qgetnum("").is_none(), "empty → None");
8106 }
8107
8108 /// c:950 — `qgetnum` returns Option<(i64, &str)> (type pin).
8109 #[test]
8110 fn qgetnum_returns_option_i64_str_tuple_type() {
8111 let _: Option<(i64, &str)> = qgetnum("42");
8112 }
8113
8114 /// c:950 — `qgetnum("123")` returns Some((123, "")) (canonical decimal).
8115 #[test]
8116 fn qgetnum_canonical_decimal_parses() {
8117 let r = qgetnum("123");
8118 assert!(r.is_some(), "valid digits → Some");
8119 let (n, rest) = r.unwrap();
8120 assert_eq!(n, 123);
8121 assert_eq!(rest, "");
8122 }
8123
8124 /// c:994 — `qgetmodespec("")` empty returns None.
8125 #[test]
8126 fn qgetmodespec_empty_returns_none() {
8127 assert!(qgetmodespec("").is_none());
8128 }
8129
8130 /// c:1539 — `hasbraces("")` empty returns false.
8131 #[test]
8132 fn hasbraces_empty_returns_false() {
8133 let _g = crate::test_util::global_state_lock();
8134 assert!(!hasbraces("", false));
8135 }
8136
8137 /// c:1539 — `hasbraces` is pure (no side effects).
8138 #[test]
8139 fn hasbraces_is_pure() {
8140 let _g = crate::test_util::global_state_lock();
8141 for s in ["", "abc", "{", "{a,b}", "no braces"] {
8142 let first = hasbraces(s, false);
8143 for _ in 0..3 {
8144 assert_eq!(
8145 hasbraces(s, false),
8146 first,
8147 "hasbraces({:?}, false) must be pure",
8148 s
8149 );
8150 }
8151 }
8152 }
8153
8154 /// c:1505 — `file_type(0)` returns char (compile-time type pin).
8155 #[test]
8156 fn file_type_returns_char_type() {
8157 let _: char = file_type(0);
8158 }
8159
8160 /// c:1505 — `file_type` is pure for arbitrary mode.
8161 #[test]
8162 fn file_type_is_pure() {
8163 for m in [0u32, 0o100000, 0o040000, 0o120000, 0o140000, 0o160000] {
8164 let first = file_type(m);
8165 for _ in 0..3 {
8166 assert_eq!(file_type(m), first, "file_type({:o}) must be pure", m);
8167 }
8168 }
8169 }
8170
8171 /// c:1802 — `matchpat("", "", false, false)` returns bool (type pin).
8172 #[test]
8173 fn matchpat_returns_bool_type() {
8174 let _g = crate::test_util::global_state_lock();
8175 let _: bool = matchpat("", "", false, false);
8176 }
8177
8178 /// c:1899 — `getmatch("", "", 0, 0, None)` returns String (type pin).
8179 #[test]
8180 fn getmatch_returns_string_type() {
8181 let _g = crate::test_util::global_state_lock();
8182 let _: String = getmatch("", "", 0, 0, None);
8183 }
8184
8185 /// c:1862 — `compgetmatch("")` empty returns Option<(String, i32)>.
8186 #[test]
8187 fn compgetmatch_returns_option_tuple_type() {
8188 let _g = crate::test_util::global_state_lock();
8189 let _: Option<(String, i32)> = compgetmatch("");
8190 }
8191}