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 = {
3227 // c:Src/glob.c:3919 — `execode(prog, 1, 0, "globqual");`. execode
3228 // (c:Src/exec.c:1245-1266) APPENDS its context for the duration, so
3229 // code inside an `(e:'…':)` / `(+cmd)` glob qualifier sees
3230 // `cmdarg:globqual`. Popped on every return path. Bug #1069.
3231 let sync_eval_ctx = |stack: &[String]| {
3232 let joined = stack.join(":");
3233 if let Ok(mut tab) = crate::ported::params::paramtab().write() {
3234 if let Some(pm) = tab.get_mut("zsh_eval_context") {
3235 pm.u_arr = Some(stack.to_vec());
3236 pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
3237 }
3238 if let Some(pm) = tab.get_mut("ZSH_EVAL_CONTEXT") {
3239 pm.u_str = Some(joined);
3240 pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
3241 }
3242 }
3243 };
3244 if let Ok(mut ctx) = crate::ported::exec::zsh_eval_context.lock() {
3245 ctx.push("globqual".to_string());
3246 sync_eval_ctx(&ctx);
3247 }
3248 struct GlobQualCtxGuard<F: Fn(&[String])>(F);
3249 impl<F: Fn(&[String])> Drop for GlobQualCtxGuard<F> {
3250 fn drop(&mut self) {
3251 if let Ok(mut ctx) = crate::ported::exec::zsh_eval_context.lock() {
3252 ctx.pop();
3253 (self.0)(&ctx);
3254 }
3255 }
3256 }
3257 let _ctx_guard = GlobQualCtxGuard(sync_eval_ctx);
3258 crate::ported::exec::execute_script(expr).unwrap_or(1) // c:3919
3259 };
3260 let ret = LASTVAL.load(Ordering::Relaxed); // c:3921 ret = lastval
3261 let _ = rc;
3262 // c:3924 — `errflag = ef | (errflag & ERRFLAG_INT);`. Restore
3263 // pre-call errflag plus any interrupt bit set during eval.
3264 let post_errflag = errflag.load(Ordering::Relaxed);
3265 errflag.store(
3266 saved_errflag | (post_errflag & ERRFLAG_INT),
3267 Ordering::Relaxed,
3268 ); // c:3924
3269 // c:3925 — `lastval = lv;`. Restore pre-call lastval.
3270 LASTVAL.store(saved_lastval, Ordering::Relaxed); // c:3925
3271 // c:3927-3937 — `inserts = getaparam("reply") || gethparam("reply")`,
3272 // else the `reply`/`REPLY` SCALAR as a one-element list. The eval can
3273 // thus REPLACE the matched name with zero-or-more names (`reply=(…)`)
3274 // or rename it (`REPLY=…`). REPLY was seeded to `filename` at c:3916,
3275 // so this is always Some — at minimum the (possibly modified) REPLY.
3276 let inserts: Vec<String> = match crate::ported::params::getaparam("reply") {
3277 Some(arr) => arr, // c:3927
3278 None => match crate::ported::params::getsparam("reply") // c:3931
3279 .or_else(|| crate::ported::params::getsparam("REPLY"))
3280 {
3281 Some(scalar) => vec![scalar], // c:3934-3936
3282 None => Vec::new(),
3283 },
3284 };
3285 *INSERTS.lock().unwrap() = Some(inserts);
3286 // c:3941 — `return !ret;` — qualifier passes iff lastval is 0.
3287 i32::from(ret == 0)
3288}
3289
3290/// Port of `qualnonemptydir(char *name, struct stat *buf, UNUSED(off_t days), UNUSED(char *str))` from Src/glob.c:3948.
3291/// C: opendir(name) and check if any non-`.`/`..` entries exist.
3292pub fn qualnonemptydir(name: &str, buf: &libc::stat, days: i64, str: &str) -> i32 {
3293 // c:3948
3294 // c:3948 — `if (!S_ISDIR(buf->st_mode)) return 0;`
3295 if (buf.st_mode as u32 & libc::S_IFMT as u32) != libc::S_IFDIR as u32 {
3296 // c:3950
3297 return 0;
3298 }
3299 // c:3953-3964 — opendir + readdir loop, skip "." and ".." entries.
3300 match fs::read_dir(name) {
3301 Ok(entries) => entries.filter_map(|e| e.ok()).any(|e| {
3302 let n = e.file_name();
3303 let s = n.to_string_lossy();
3304 s != "." && s != ".."
3305 }) as i32,
3306 Err(_) => 0,
3307 }
3308}
3309
3310// =====================================================================
3311// Thread-local glob option snapshot — race fix.
3312//
3313// `Src/glob.c` reads `isset(NULLGLOB)` / `isset(BAREGLOBQUAL)` / etc.
3314// inline at each callsite during a glob. In zsh those reads hit the
3315// thread-local C `opts[]` array (zsh is single-threaded). In zshrs
3316// they hit `OPTS_LIVE` — a process-wide `RwLock<HashMap>`.
3317//
3318// Embedders that snapshot/restore options around individual glob
3319// invocations on multiple threads (e.g. stryke's `StrykeGlobOptsGuard`
3320// + `glob_par` rayon parallelism) race: thread A sets bareglobqual=1,
3321// calls glob; while A is mid-parse, thread B's matching guard restores
3322// bareglobqual=0; A's qualifier-parse step sees bareglobqual=0 and
3323// drops the trailing `(N)` as a literal substring, producing false
3324// matches.
3325//
3326// Fix: snapshot every glob-relevant option ONCE at glob() entry into
3327// a thread-local cache, then read all isset() inside glob from the
3328// snapshot. The snapshot is dropped on glob exit via an RAII guard.
3329// Each thread's in-flight glob() sees a coherent set of options
3330// regardless of concurrent setopt/unsetopt on other threads.
3331// =====================================================================
3332
3333/// Snapshot of glob-relevant zsh options taken at glob entry. Holds
3334/// the live `isset(...)` value for each option that the glob engine
3335/// or its helpers (`parse_qualifiers`, `scanner`,
3336/// `sort_matches`, `glob_emit_path`) consult.
3337#[derive(Debug, Clone, Copy, Default)]
3338pub struct GlobOptSnapshot {
3339 pub bareglobqual: bool,
3340 pub braceccl: bool,
3341 pub caseglob: bool,
3342 pub extendedglob: bool,
3343 pub globdots: bool,
3344 pub globstarshort: bool,
3345 pub listtypes: bool,
3346 pub markdirs: bool,
3347 pub nullglob: bool,
3348 pub numericglobsort: bool,
3349}
3350
3351/// RAII guard that drops the thread-local snapshot on scope exit.
3352/// Constructed by `enter_glob_scope()`. Nested glob calls on the
3353/// same thread reuse the outer snapshot (the guard's `populated`
3354/// field tracks whether we installed or just observed).
3355pub struct GlobOptsGuard {
3356 populated: bool,
3357}
3358
3359/// Sort specifier flags
3360// `GlobSort` / `SortOrder` / `SortSpec` deleted — C uses bit-flag
3361// `int tp` in `struct globsort { int tp; char *exec; }` (Src/glob.c:155):
3362// tp & ~GS_DESC selects the sort key (GS_NAME/GS_DEPTH/GS_EXEC/…),
3363// tp & GS_DESC reverses direction (`O` vs `o` qualifier),
3364// tp << GS_SHIFT carries the follow-link variants.
3365// All those bits already exist as i32 constants at glob.rs:33+
3366// (GS_NAME / GS_DEPTH / GS_SIZE / … / GS_DESC / GS_NONE / GS__SIZE /
3367// …). The enum + Ascending/Descending + struct triple-wrapper was a
3368// Rust-only convenience with no C counterpart; callers now operate
3369// on the raw i32 the same way `gmatchcmp()` at glob.c:936 does.
3370
3371// `TimeUnit` / `SizeUnit` / `RangeOp` enums deleted — Rust-only
3372// wrappers around constants/chars that exist as raw values in C:
3373// `TimeUnit::Seconds` → TT_SECONDS i32 (glob.c:126 → glob.rs:99)
3374// `TimeUnit::Minutes` → TT_MINS (c:123)
3375// `TimeUnit::Hours` → TT_HOURS (c:122)
3376// `TimeUnit::Days` → TT_DAYS (c:121)
3377// `TimeUnit::Weeks` → TT_WEEKS (c:124)
3378// `TimeUnit::Months` → TT_MONTHS (c:125)
3379// `SizeUnit::Bytes` → TT_BYTES (c:128)
3380// `SizeUnit::PosixBlocks` → TT_POSIX_BLOCKS (c:129)
3381// `SizeUnit::Kilobytes` → TT_KILOBYTES (c:130)
3382// `SizeUnit::Megabytes` → TT_MEGABYTES (c:131)
3383// `SizeUnit::Gigabytes` → TT_GIGABYTES (c:132)
3384// `SizeUnit::Terabytes` → TT_TERABYTES (c:133)
3385// `RangeOp::Less`/`Equal`/`Greater` → raw chars `<` `=` `>`
3386// (zsh's qgetnum at glob.c:827 returns the raw operator char
3387// and the qualifier handlers switch on it inline).
3388
3389/// Port of `TestMatchFunc` — the `int (*)(char *, struct stat *, off_t,
3390/// char *)` function-pointer type held by `struct qual.func` (Src/glob.c).
3391/// Every glob-qualifier test (qualuid, qualisreg, qualiscom, qualsize, …)
3392/// has this signature so `insert()` can call them uniformly through the
3393/// `func` slot. `name` is the metafied path, `buf` the stat, `data` the
3394/// qualifier argument (uid, mode bits, day count, …), `sdata` the
3395/// expression text (only `qualsheval` uses it; others ignore it).
3396pub type TestMatchFunc = fn(name: &str, buf: &libc::stat, data: i64, sdata: &str) -> i32;
3397
3398/// Port of `struct qual` from `Src/glob.c:138`. A node in the linked list
3399/// of glob qualifiers parsed from a `(…)` suffix. `insert()` (glob.c:346)
3400/// walks this list per candidate file, calling `func` on each node and
3401/// accepting/rejecting the file by `sense`. The terminating node carries
3402/// `func == None` (C tests `qn && qn->func` to stop).
3403///
3404/// REPRESENTATION: C's `next`/`or` are `struct qual *` pointers into a
3405/// graph that `parsepat` builds and relinks through aliased cursors (the
3406/// OR-distribution at glob.c:1797-1820 aliases one node from several
3407/// chains at once). Rust single-ownership `Box` cannot express that, so
3408/// the port uses an arena ([`QualArena`]): nodes live in a `Vec<qual>`
3409/// and `next`/`or` are `Option<usize>` indices. C pointer assignments map
3410/// to index assignments, so the aliased-cursor algorithm translates
3411/// almost verbatim.
3412#[allow(non_camel_case_types)]
3413#[derive(Clone, Debug)]
3414pub struct qual {
3415 /// c:139 — `struct qual *next;` next qualifier in the AND-chain.
3416 pub next: Option<usize>,
3417 /// c:140 — `struct qual *or;` head of an alternative OR-chain.
3418 pub or: Option<usize>,
3419 /// c:141 — `TestMatchFunc func;` the test to call; `None` terminates.
3420 pub func: Option<TestMatchFunc>,
3421 /// c:142 — `off_t data;` argument passed to `func`.
3422 pub data: i64,
3423 /// c:143 — `int sense;` bit0: 0=assert / 1=negate; bit1: follow links.
3424 pub sense: i32,
3425 /// c:144 — `int amc;` which timestamp to test (0=a, 1=m, 2=c).
3426 pub amc: i32,
3427 /// c:145 — `int range;` test `<` / `>` / `=` (signum of comparison).
3428 pub range: i32,
3429 /// c:146 — `int units;` multiplier for time (TT_DAYS…) or size.
3430 pub units: i32,
3431 /// c:147 — `char *sdata;` expression to eval (currently qualsheval only).
3432 pub sdata: Option<String>,
3433}
3434
3435impl qual {
3436 /// A fresh node with the C zero-initialised defaults (`hcalloc`).
3437 pub fn new() -> qual {
3438 qual {
3439 next: None,
3440 or: None,
3441 func: None,
3442 data: 0,
3443 sense: 0,
3444 amc: 0,
3445 range: 0,
3446 units: 0,
3447 sdata: None,
3448 }
3449 }
3450}
3451
3452impl Default for qual {
3453 fn default() -> qual {
3454 qual::new()
3455 }
3456}
3457
3458/// Arena backing the [`qual`] graph. C allocates each `struct qual` with
3459/// `hcalloc` and links them by pointer; the Rust port allocates them in
3460/// this `Vec` and links them by index. `alloc()` mirrors a single
3461/// `hcalloc(sizeof *qn)` and returns the new node's index; `head` holds
3462/// the index of `quals` (the list `insert()` walks), `None` when empty.
3463#[derive(Clone, Default, Debug)]
3464pub struct QualArena {
3465 pub nodes: Vec<qual>,
3466 pub head: Option<usize>,
3467}
3468
3469impl QualArena {
3470 pub fn new() -> QualArena {
3471 QualArena {
3472 nodes: Vec::new(),
3473 head: None,
3474 }
3475 }
3476
3477 /// c:`qn = (struct qual *)hcalloc(sizeof *qn);` — allocate a fresh
3478 /// zeroed node and return its arena index.
3479 pub fn alloc(&mut self) -> usize {
3480 self.nodes.push(qual::new());
3481 self.nodes.len() - 1
3482 }
3483}
3484
3485/// A glob qualifier function
3486#[derive(Debug, Clone)]
3487/// One glob qualifier — Rust-extension sum type. C uses a linked
3488/// list of `struct qual` (`Src/zsh.h:140-152`) with function-pointer
3489/// `func` per node; each variant here maps to one of C's `q*` test
3490/// ported (`qisreg`, `qisdir`, `qowner`, `qtime`, ...) at
3491/// `Src/glob.c:1080-1340`. The full `struct qual` port + per-test fn
3492/// dispatch lives in a later phase; this enum keeps the parsed-form
3493/// the per-match filter inside `scanner()` (line 500) needs.
3494#[allow(non_camel_case_types)]
3495pub enum qualifier {
3496 /// File type qualifiers
3497 IsRegular,
3498 IsDirectory,
3499 IsSymlink,
3500 IsSocket,
3501 IsFifo,
3502 IsBlockDev,
3503 IsCharDev,
3504 IsDevice,
3505 IsExecutable,
3506
3507 /// Permission qualifiers
3508 Readable,
3509 Writable,
3510 Executable,
3511 WorldReadable,
3512 WorldWritable,
3513 WorldExecutable,
3514 GroupReadable,
3515 GroupWritable,
3516 GroupExecutable,
3517 Setuid,
3518 Setgid,
3519 Sticky,
3520
3521 /// Ownership qualifiers
3522 OwnedByEuid,
3523 OwnedByEgid,
3524 OwnedByUid(u32),
3525 OwnedByGid(u32),
3526
3527 /// Numeric qualifiers with range. `unit` is a `TT_*` i32
3528 /// (glob.rs:99-113 / glob.c:121-133); `op` is the raw range
3529 /// operator char (`<`, `=`, `>`) — mirrors C's qgetnum which
3530 /// returns the operator char and the handler switches inline.
3531 Size {
3532 value: u64,
3533 unit: i32,
3534 op: char,
3535 },
3536 Links {
3537 value: u64,
3538 op: char,
3539 },
3540 Atime {
3541 value: i64,
3542 unit: i32,
3543 op: char,
3544 },
3545 Mtime {
3546 value: i64,
3547 unit: i32,
3548 op: char,
3549 },
3550 Ctime {
3551 value: i64,
3552 unit: i32,
3553 op: char,
3554 },
3555
3556 /// Mode specification
3557 Mode {
3558 yes: u32,
3559 no: u32,
3560 },
3561
3562 /// Device number
3563 Device(u64),
3564
3565 /// Non-empty directory
3566 NonEmptyDir,
3567
3568 /// Shell evaluation
3569 Eval(String),
3570
3571 /// `^` — toggle the running match sense for SUBSEQUENT qualifiers in
3572 /// this list. C uses a running `sense ^= 1` (Src/glob.c:1346) stored
3573 /// per `struct qual.sense`; the enum can't carry per-node sense, so a
3574 /// marker in the list reproduces it: `check_qualifier_list` flips a
3575 /// running sense bit on each marker and XORs it into later tests.
3576 /// Fixes `*(/^@)` (dir AND not-symlink), `*(.^x)`, etc.
3577 ToggleSense,
3578}
3579
3580// Misnamed `gmatchcmp(&str, &str)` deleted — was a Rust-only
3581// locale-aware string compare claiming to be the C qsort
3582// comparator. The real C `gmatchcmp(Gmatch, Gmatch)` at glob.c:936
3583// is now ported as `gmatchcmp(&GlobMatch, &GlobMatch, &[i32], bool)`
3584// below. The string-compare case the old name claimed routes
3585// through canonical `zstrcmp` (sort.c:191).
3586
3587// `GlobOptions` struct deleted — Rust-only Bag-of-options with no
3588// C counterpart. C reads each option directly from the global
3589// `opts[]` array via `isset(NULL_GLOB)` / `isset(EXTENDED_GLOB)`
3590// / etc. (Src/options.c). The Rust port uses the canonical
3591// `opt_state_get(name) -> Option<bool>`
3592// which reads from the same global store. Inlined at each
3593// callsite as `opt_state_get("name").unwrap_or(default)`.
3594
3595/// Parsed glob qualifier set
3596#[derive(Debug, Clone, Default)]
3597/// Compiled qualifier list for one glob.
3598/// Mirrors the `struct qual *` linked list `parsepat()`
3599/// (Src/glob.c:791) builds — every `(qual)` after a glob pattern
3600/// adds to it.
3601#[allow(non_camel_case_types)]
3602pub struct qualifier_set {
3603 // c:138
3604 pub qualifiers: Vec<qualifier>,
3605 pub alternatives: Vec<Vec<qualifier>>,
3606 pub follow_links: bool,
3607 /// Packed sort-spec flags, one per `o`/`O` qualifier in the pattern.
3608 /// Each entry is the C `struct globsort.tp` field — `GS_NAME` /
3609 /// `GS_DEPTH` / `GS_EXEC` / `GS_SIZE` / `GS_ATIME` / `GS_MTIME` /
3610 /// `GS_CTIME` / `GS_LINKS` (or their `<< GS_SHIFT` follow-link
3611 /// variants), OR'd with `GS_DESC` for reverse direction.
3612 pub sorts: Vec<i32>, // c:155 struct globsort.tp[]
3613 /// c:74 `struct globsort.exec` — the eval code for each `GS_EXEC`
3614 /// (`oe:…:` / `o+…`) sort spec, indexed by the `idx` packed into the
3615 /// sort entry's high bits (`GS_EXEC | (idx << 16)`). Evaluated per
3616 /// match with `$REPLY` = the match name; the resulting `$REPLY` is
3617 /// the sort key (the original name is still emitted).
3618 pub sort_exec: Vec<String>, // c:74 globsort.exec
3619 /// The faithful glob.c `struct qual` representation of `alternatives`,
3620 /// built at parse end (additive — the enum is kept for now). AND-chain
3621 /// via `next`, alternatives via `or`, per-node `sense`/`range`/`amc`/
3622 /// `units`/`sdata`. The convergence target: `check_qualifiers` walks
3623 /// THIS via `insert()` semantics (glob.c:392-419), and the enum is
3624 /// eventually deleted.
3625 pub quals: QualArena, // c:206 curglobdata.gd_quals
3626 pub first: Option<i32>,
3627 pub last: Option<i32>,
3628 pub colon_mods: Option<String>,
3629 pub pre_words: Vec<String>,
3630 pub post_words: Vec<String>,
3631 /// `(M)` qualifier — append `/` to directory entries in output.
3632 /// Direct port of zsh/Src/glob.c:1557-1561 (`case 'M'`):
3633 /// `gf_markdirs = !(sense & 1)` — set when the qualifier appears
3634 /// without a `^` toggle. Stored per-qualifier-set rather than per
3635 /// GlobOptions so a single glob call's qualifier picks it up.
3636 pub mark_dirs: bool,
3637 /// `(T)` qualifier — append type-char (ls -F style) to every entry.
3638 /// Direct port of zsh/Src/glob.c:1562-1566 (`case 'T'`).
3639 pub list_types: bool,
3640 /// `(N)` qualifier — per-glob nullglob: empty result on no-match,
3641 /// no error. Direct port of zsh/Src/glob.c:1567-1569 (`case 'N'`):
3642 /// `gf_nullglob = !(sense & 1)` — set when `(N)` appears without
3643 /// the `^` toggle.
3644 pub nullglob: bool,
3645 /// `(D)` qualifier — include dotfiles in the glob result (override
3646 /// the global `globdots`/`dotglob` option for this glob alone).
3647 /// Direct port of zsh/Src/glob.c case 'D' which sets
3648 /// `gf_glob.dots = 1` for the duration of this glob expansion.
3649 pub globdots: bool,
3650 /// `(YN)` qualifier — short-circuit: limit number of matches to
3651 /// at most N. Direct port of zsh/Src/glob.c:1579-1595 (`case 'Y'`)
3652 /// which sets the C `shortcircuit` int. The matcher should stop
3653 /// after collecting N entries; the Rust port truncates after the
3654 /// match walk completes (the optimization gain is the same in
3655 /// the typical short-glob case). Bug #41 in docs/BUGS.md.
3656 pub short_circuit: Option<i32>,
3657 /// `(n)` qualifier — per-glob numeric sort. Direct port of
3658 /// zsh/Src/glob.c:1575-1577 (`case 'n': gf_numsort = !(sense & 1)`).
3659 /// Distinct from the `o`/`O` sort KEY `n` (= GS_NAME, lexical):
3660 /// standalone `(n)` makes the name comparison NUMERIC (so `*(n)`
3661 /// orders `f2` before `f10`), OR'd with the global NUMERIC_GLOB_SORT
3662 /// option at sort time (glob.c:947/983 `gf_numsort ?
3663 /// SORTIT_NUMERICALLY : 0`). `None` = not specified (fall back to
3664 /// the global option); `Some(true)` = `(n)`; `Some(false)` = `(^n)`
3665 /// which OVERRIDES the global option to force lexical.
3666 pub numsort: Option<bool>,
3667}
3668
3669/// Enter a glob scope: snapshot options into TLS if no outer scope
3670/// is active, returning a guard that clears the snapshot on Drop.
3671/// Re-entrant calls (nested globdata_glob via brace expansion etc.)
3672/// return a no-op guard that observes the existing snapshot.
3673pub fn enter_glob_scope() -> GlobOptsGuard {
3674 let already = GLOB_OPTS_TLS.with_borrow(|g| g.is_some());
3675 if already {
3676 return GlobOptsGuard { populated: false };
3677 }
3678 let snap = GlobOptSnapshot::capture();
3679 GLOB_OPTS_TLS.with_borrow_mut(|g| *g = Some(snap));
3680 GlobOptsGuard { populated: true }
3681}
3682
3683/// Read a glob-relevant option through the TLS snapshot when one is
3684/// active; fall back to the live `isset()` otherwise. Use this in
3685/// any glob-engine helper that previously read `isset(OPT)` for one
3686/// of the snapshotted options.
3687#[inline]
3688pub fn glob_isset(opt: i32) -> bool {
3689 GLOB_OPTS_TLS.with_borrow(|g| match g {
3690 Some(snap) => match opt {
3691 x if x == BAREGLOBQUAL => snap.bareglobqual,
3692 x if x == BRACECCL => snap.braceccl,
3693 x if x == CASEGLOB => snap.caseglob,
3694 x if x == EXTENDEDGLOB => snap.extendedglob,
3695 x if x == GLOBDOTS => snap.globdots,
3696 x if x == GLOBSTARSHORT => snap.globstarshort,
3697 x if x == LISTTYPES => snap.listtypes,
3698 x if x == MARKDIRS => snap.markdirs,
3699 x if x == NULLGLOB => snap.nullglob,
3700 x if x == NUMERICGLOBSORT => snap.numericglobsort,
3701 _ => isset(opt),
3702 },
3703 None => isset(opt),
3704 })
3705}
3706
3707// ===========================================================
3708// `impl globdata` block above kept only for the `new()`
3709// constructor (Default-style). All scanner / parser / qualifier
3710// ported below are top-level — C glob.c has them as top-level
3711// statics that mutate the file-static `curglobdata`. Each is
3712// flagged with `// RUST-ONLY` and a comment naming the closest
3713// C equivalent + the proper-port target (typically `scanner`
3714// at glob.c:500 driving `insert` at c:346).
3715// ===========================================================
3716
3717/// Main entry point: expand a glob pattern.
3718///
3719/// Closest C equivalent: `zglob` driver at glob.c:1214 calls
3720/// `parsepat` (c:791) and then `scanner` (c:500). The Rust port
3721/// here orchestrates qualifier parsing, brace expansion (which C
3722/// does separately in `xpandbraces`), then drives the faithful
3723/// `parsecomplist` (c:710) → `complist` → `scanner` (c:500) path.
3724pub fn globdata_glob(state: &mut globdata, pattern: &str) -> Vec<String> {
3725 // RUST-ONLY
3726 // Brace pre-expansion. In zsh, `xpandbraces` (zsh/Src/glob.c:2275)
3727 // runs during substitution before glob — patterns reaching glob()
3728 // are already brace-free in the production path (vm_helper handles
3729 // it). For direct programmatic callers of glob_with_options, run
3730 // the brace pass here so `GlobOptions.brace_ccl` is actually
3731 // consulted: with brace_ccl set, `{a-mnop}` expands to a..m,n,o,p
3732 // per glob.c:2424 BRACECCL block; without, only `{a,b}` lists and
3733 // `{1..5}`/`{a..e}` ranges expand. Recurse on each variant and
3734 // concatenate matches.
3735 let brace_ccl = glob_isset(BRACECCL);
3736 if hasbraces(pattern, brace_ccl) {
3737 let mut all = Vec::new();
3738 for variant in xpandbraces(pattern, brace_ccl) {
3739 all.extend(globdata_glob(state, &variant));
3740 }
3741 return all;
3742 }
3743
3744 state.matches.clear();
3745 state.pathbuf.clear();
3746 state.pathpos = 0;
3747
3748 // Parse qualifiers first so a bare-qualifier pattern like `dir(/)`
3749 // (no wildcard, just a stat-based filter) still enters the expansion
3750 // path. Without this, `haswilds("dir(/)")` returns false and the
3751 // pattern echoes back unfiltered, which defeats the whole point of
3752 // qualifiers.
3753 let (pat, quals) = parse_qualifiers(pattern);
3754 state.qualifiers = quals;
3755
3756 // c:Src/glob.c — `A~B` exclusion. zsh compiles the ENTIRE glob
3757 // (path components + `~B` exclusion + `**`) into ONE Patprog and
3758 // matches each candidate PATH against it, so the `~B` applies to the
3759 // whole path. zshrs's component scanner can't express a path-level
3760 // exclusion, so split a TOP-LEVEL `~` (extendedglob; depth 0, not in
3761 // `[...]`/`(...)`, not Bnull-escaped) off here into the main pattern
3762 // plus full-path exclusion patterns, glob the main pattern, then drop
3763 // matched paths matching any exclusion below. Without this the `~B`
3764 // (which may contain `/`) was split across path components and either
3765 // excluded everything (`~*/.git/*` → `*.txt~*` matched all) or
3766 // nothing. `~` inside one component still works the same (main globs,
3767 // the post-filter applies the exclusion).
3768 let (pat, glob_exclusions): (String, Vec<String>) =
3769 if glob_isset(EXTENDEDGLOB) && (pat.contains('~') || pat.contains('\u{98}')) {
3770 let cv: Vec<char> = pat.chars().collect();
3771 let mut parts: Vec<String> = Vec::new();
3772 let mut cur = String::new();
3773 let mut bd = 0i32; // `[...]` depth
3774 let mut pd = 0i32; // `(...)` depth
3775 let mut k = 0usize;
3776 while k < cv.len() {
3777 let c = cv[k];
3778 match c {
3779 '\u{9f}' => {
3780 // Bnull escape — next char is literal.
3781 cur.push(c);
3782 if k + 1 < cv.len() {
3783 cur.push(cv[k + 1]);
3784 k += 2;
3785 continue;
3786 }
3787 }
3788 '[' => {
3789 bd += 1;
3790 cur.push(c);
3791 }
3792 ']' => {
3793 if bd > 0 {
3794 bd -= 1;
3795 }
3796 cur.push(c);
3797 }
3798 '(' | '\u{88}' => {
3799 pd += 1;
3800 cur.push(c);
3801 }
3802 ')' | '\u{8a}' => {
3803 if pd > 0 {
3804 pd -= 1;
3805 }
3806 cur.push(c);
3807 }
3808 '~' | '\u{98}' if bd == 0 && pd == 0 => {
3809 parts.push(std::mem::take(&mut cur));
3810 }
3811 _ => cur.push(c),
3812 }
3813 k += 1;
3814 }
3815 parts.push(cur);
3816 // Only split when there's a non-empty main pattern AND at
3817 // least one exclusion (a leading `~` is home-dir, handled by
3818 // filesub before glob — never reaches here as an exclusion).
3819 if parts.len() > 1 && !parts[0].is_empty() {
3820 let main = parts.remove(0);
3821 (main, parts)
3822 } else {
3823 // No usable split — restore the original pattern verbatim
3824 // (the `~` separators dropped during the scan).
3825 (cv.iter().collect(), Vec::new())
3826 }
3827 } else {
3828 (pat, Vec::new())
3829 };
3830
3831 // c:Src/glob.c:1843-1854 — `if (!q || errflag) { ... return; }`. When
3832 // the qualifier parser already emitted a diagnostic (e.g. "number
3833 // expected" from qgetnum at c:832) and set errflag, abort glob
3834 // expansion entirely rather than continuing to scan with a partial
3835 // qualifier set. Without this gate, `?(a)` (where `(a)` is an
3836 // invalid qualifier) error-prints "number expected" then continues
3837 // to expand `?` against the dir, emitting matches alongside the
3838 // error — bug #549.
3839 if crate::ported::utils::errflag.load(std::sync::atomic::Ordering::SeqCst)
3840 & crate::ported::zsh_h::ERRFLAG_ERROR
3841 != 0
3842 {
3843 return Vec::new();
3844 }
3845
3846 // Now check wildcards on the qualifier-stripped pattern. A pure
3847 // literal with a qualifier (`name(.)`) still needs to enter the
3848 // scanner so the qualifier filter can run against the literal name.
3849 // haswilds scans TOKENIZED strings (c:Src/glob.c:1230 runs on the
3850 // lexer-tokenized word); glob_path receives both tokenized (zglob)
3851 // and untokenized (expand_glob fast paths) patterns, so tokenize a
3852 // local copy for the check — existing token chars pass through
3853 // zshtokenize untouched, untokenized metachars gain their tokens.
3854 let mut pat_tok = pat.clone();
3855 tokenize(&mut pat_tok); // c:Src/glob.c:3548
3856 if !haswilds(&pat_tok) && state.qualifiers.is_none() {
3857 return vec![pattern.to_string()];
3858 }
3859
3860 // c:glob() — `gd.gf_noglobdots = !isset(GLOBDOTS)` (plus the `(D)`
3861 // qualifier). parsecomplist reads this from CURGLOBDATA to compile
3862 // PAT_NOGLD into each component, which is what makes pattry reject
3863 // leading-dot files (pattern.rs:2890). Set it before parsing so the
3864 // faithful flag-driven dot filtering replaces the old manual skip.
3865 {
3866 let globdots = glob_isset(GLOBDOTS)
3867 || state
3868 .qualifiers
3869 .as_ref()
3870 .map(|qf| qf.globdots)
3871 .unwrap_or(false);
3872 let mut gd = CURGLOBDATA.lock().unwrap_or_else(|e| e.into_inner());
3873 gd.gf_noglobdots = if globdots { 0 } else { 1 };
3874 }
3875
3876 // c:parsepat (glob.c:809-820) — init pathbuf for absolute vs relative,
3877 // strip the leading `/`, then parse into a `complist` and run the
3878 // faithful `scanner`. parsecomplist consumes TOKENIZED input (it tests
3879 // for the `Star` token and hands segments to patcompile), so feed it
3880 // `pat_tok` (already tokenized above; tokenize is idempotent).
3881 state.pathbufcwd = 0; // c:812 — `DPUTS(pathbufcwd, ...)` invariant
3882 let parse_src = if let Some(rest) = pat_tok.strip_prefix('/') {
3883 // c:813-816 — absolute path.
3884 state.pathbuf.push('/');
3885 state.pathpos = 1;
3886 rest.to_string()
3887 } else {
3888 // c:817-818 — relative to pwd.
3889 pat_tok.clone()
3890 };
3891 if let Some(complist) = parsecomplist(&parse_src) {
3892 scanner(state, Some(&complist), 0, false);
3893 } else {
3894 // c:Src/glob.c:1842-1854 — `q = parsepat(str); if (!q || errflag)`.
3895 // This is the `!q` half: the pattern failed to COMPILE (e.g. an
3896 // unclosed `[` character class — patcomppiece returns -1 at
3897 // pattern.rs:1687). C then either treats the word as a literal
3898 // string (when `unset(BADPATTERN)`) or aborts with
3899 // `zerr("bad pattern: %s", ostr)`. zshrs previously dropped the
3900 // `None` silently, so `echo abc[def` fell through to the
3901 // no-matches / literal-under-NO_NOMATCH path instead of the
3902 // compile diagnostic — and BADPATTERN was ignored entirely.
3903 // (The `errflag` half — a diagnostic already emitted by the
3904 // qualifier parser — is handled by the gate at glob.rs:3770.)
3905 if !isset(crate::ported::zsh_h::BADPATTERN) {
3906 // c:1846-1848 — treat as an ordinary literal string.
3907 return vec![pattern.to_string()];
3908 }
3909 // c:1851-1852 — clear any stale error bit so zerr fires, then
3910 // emit. utils::zerr re-sets ERRFLAG_ERROR, which zglob's gate at
3911 // glob.rs:1541 reads to suppress the redundant "no matches found".
3912 errflag.fetch_and(
3913 !crate::ported::zsh_h::ERRFLAG_ERROR,
3914 std::sync::atomic::Ordering::SeqCst,
3915 );
3916 zerr(&format!("bad pattern: {}", pattern));
3917 return Vec::new();
3918 }
3919
3920 // c:Src/glob.c — apply top-level `~B` exclusions to the full matched
3921 // paths (split off above). A match is dropped if its emitted path
3922 // matches ANY exclusion pattern (`*` matches `/` here, matching zsh's
3923 // flat-string exclusion semantics).
3924 if !glob_exclusions.is_empty() {
3925 let eg = glob_isset(EXTENDEDGLOB);
3926 let cg = glob_isset(CASEGLOB);
3927 state.matches.retain(|m| {
3928 let p = glob_emit_path(&m.path);
3929 !glob_exclusions.iter().any(|ex| matchpat(ex, &p, eg, cg))
3930 });
3931 }
3932
3933 // c:1680 — populate GS_EXEC sort keys before sorting. Evaluate each
3934 // `oe:…:` / `o+…` code per match with $REPLY = the match name, and
3935 // capture the resulting $REPLY as that match's sort string (gmatchcmp
3936 // reads gmatch.sort_strings[idx]). Exec sort changes ORDER only — the
3937 // original name is still what's emitted.
3938 let sort_codes: Vec<String> = state
3939 .qualifiers
3940 .as_ref()
3941 .map(|q| q.sort_exec.clone())
3942 .unwrap_or_default();
3943 if !sort_codes.is_empty() {
3944 for m in state.matches.iter_mut() {
3945 let name = glob_emit_path(&m.path);
3946 for code in &sort_codes {
3947 crate::ported::params::setsparam("REPLY", &name);
3948 let _ = crate::ported::exec::execute_script(code);
3949 let key = crate::ported::params::getsparam("REPLY").unwrap_or_else(|| name.clone());
3950 m.sort_strings.push(key);
3951 }
3952 }
3953 }
3954
3955 // c:Src/glob.c:518 — the `Y` limit belongs to the SCAN, not to the result:
3956 // scanner(q->next, shortcircuit);
3957 // if (shortcircuit && shortcircuit == matchct)
3958 // return;
3959 // The scanner simply stops once N matches exist, so the survivors are the
3960 // first N *found*, and only then does c:1868's sort run over them. Applying
3961 // the limit after sorting (as apply_selection used to) keeps the first N by
3962 // SORT ORDER instead — a different set: `*(.Y2on)` must be the two files
3963 // the scan reached, re-ordered by name, not the two alphabetically-first.
3964 //
3965 // This port collects every match before sorting, so the faithful place for
3966 // the truncation is here — after collection, before sort_matches. The
3967 // `[first,last]` subscript stays in apply_selection: c:1868's sort precedes
3968 // it, so THAT one really is a post-sort selection.
3969 //
3970 // Zero means "no limit" (c:518's leading `shortcircuit &&`), hence `> 0`.
3971 if let Some(n) = state.qualifiers.as_ref().and_then(|q| q.short_circuit) {
3972 if n > 0 {
3973 let n = n as usize;
3974 if state.matches.len() > n {
3975 state.matches.truncate(n); // c:518
3976 }
3977 }
3978 }
3979
3980 // Sort results
3981 sort_matches(state);
3982
3983 // Apply subscript selection
3984 apply_selection(state);
3985
3986 // Extract filenames. Mark-dirs / list-types come from EITHER
3987 // the canonical global option store OR the parsed `(M)`/`(T)`
3988 // qualifier on this glob. Direct port of zsh/Src/glob.c:355,372
3989 // — output marker emission consults the per-glob `gf_markdirs`
3990 // / `gf_listtypes` flags which the qualifier parser at
3991 // glob.c:1557-1566 sets.
3992 let mark_dirs = glob_isset(MARKDIRS)
3993 || state
3994 .qualifiers
3995 .as_ref()
3996 .map(|q| q.mark_dirs)
3997 .unwrap_or(false);
3998 // c:1562-1566 — `gf_listtypes` is set ONLY by the `T` glob
3999 // qualifier (`*(T)`), NOT by the global LISTTYPES option.
4000 // LISTTYPES is the completion-listing option (see man zshoptions
4001 // "LIST_TYPES") and doesn't affect glob output. Including the
4002 // option here mis-decorated every executable-file glob with a
4003 // trailing `*` and every directory with `/` for users with the
4004 // default `setopt listtypes` ON.
4005 let list_types = state
4006 .qualifiers
4007 .as_ref()
4008 .map(|q| q.list_types)
4009 .unwrap_or(false);
4010 let colon_mods = state.qualifiers.as_ref().and_then(|q| q.colon_mods.clone());
4011 // c:Src/glob.c — a pattern ending in `/` ("trailing slash" syntax)
4012 // forces matches to be directories AND preserves the slash in the
4013 // output. zsh's parsepat treats `pat/` as `pat` + an empty trailing
4014 // component; the scanner restricts to directories and the emitter
4015 // appends `/`. The Rust port's parser short-circuits the empty
4016 // trailing component, so we filter + re-add the suffix here at emit
4017 // time. `/` is never a glob metachar so a literal trailing-slash
4018 // check on the raw pattern is sufficient.
4019 // Use the qualifier-stripped `pat` for the trailing-slash check.
4020 // For `*/(N)` the original `pattern` ends with `)`, so the check
4021 // on `pattern` returned false and the trailing-slash directory-
4022 // filter was skipped — `*/(N)` then matched every file, not just
4023 // directories. The qualifier-stripped `pat` is `*/` which still
4024 // carries the trailing `/`.
4025 let trailing_slash = pat.ends_with('/') && pat.len() > 1;
4026 // c:Src/glob.c — preserve user-typed `./` / `../` prefix in
4027 // output. glob_emit_path strips CurDir uniformly so the
4028 // leading `./` would be lost. Detect the source prefix here
4029 // and re-prepend after emit. Same `pat` (post-qualifier) — the
4030 // prefix is on the path part, not the qualifier.
4031 let leading_dot_prefix: &str = if pat.starts_with("../") {
4032 "../"
4033 } else if pat.starts_with("./") {
4034 "./"
4035 } else {
4036 ""
4037 };
4038 let mut results: Vec<String> = state
4039 .matches
4040 .iter()
4041 .filter(|m| !trailing_slash || fs::metadata(&m.path).map(|md| md.is_dir()).unwrap_or(false))
4042 .map(|m| {
4043 let mut s = glob_emit_path(&m.path);
4044 if !leading_dot_prefix.is_empty()
4045 && !s.starts_with(leading_dot_prefix)
4046 && !s.starts_with('/')
4047 {
4048 s = format!("{}{}", leading_dot_prefix, s);
4049 }
4050 if trailing_slash && !s.ends_with('/') {
4051 s.push('/');
4052 }
4053 if mark_dirs || list_types {
4054 if let Ok(meta) = fs::symlink_metadata(&m.path) {
4055 let ch = file_type(meta.mode());
4056 if list_types || (mark_dirs && ch == '/') {
4057 // Don't double-stamp if trailing_slash already added one.
4058 if !s.ends_with(ch) {
4059 s.push(ch);
4060 }
4061 }
4062 }
4063 }
4064 // Apply colon modifiers AFTER mark/list-type appendage —
4065 // zsh applies them last in glob.c:432 modify() per emitted
4066 // node, so `(M:t)` would mark THEN tail (effectively just
4067 // tail since the slash is gone). Faithful order.
4068 if let Some(ref m) = colon_mods {
4069 // Delegate to canonical `modify()` port in subst.rs
4070 // (Src/subst.c:4531). Local colon-modifier impl was
4071 // an invented duplicate — removed.
4072 s = crate::ported::subst::modify(&s, m);
4073 }
4074 s
4075 })
4076 .collect();
4077
4078 // c:421-426 — zsh applies the colon modifier in insert() (during
4079 // collection) BEFORE the match sort, so the default (name) sort
4080 // orders the MODIFIED names: `*(.:e)` on (apple.z,banana.a,cherry.m)
4081 // sorts the extensions → (a,m,z), not name order (z,a,m). The Rust
4082 // pipeline modifies at emit (after sort_matches), so re-sort the
4083 // modified results here — but ONLY for the default name sort; an
4084 // explicit `o`/`O` keeps its stat-keyed order with modified output.
4085 let explicit_sort = state
4086 .qualifiers
4087 .as_ref()
4088 .map(|q| !q.sorts.is_empty())
4089 .unwrap_or(false);
4090 if colon_mods.is_some() && !explicit_sort {
4091 results.sort();
4092 }
4093
4094 // c:1744-1756 / insert_glob_match c:1430 — `P:word:` (gf_pre_words)
4095 // emits `word` as a separate element BEFORE each match; `^P:word:`
4096 // (gf_post_words) emits it AFTER. Applied last, after sort/modifiers.
4097 let (pre_words, post_words) = state
4098 .qualifiers
4099 .as_ref()
4100 .map(|q| (q.pre_words.clone(), q.post_words.clone()))
4101 .unwrap_or_default();
4102 if !pre_words.is_empty() || !post_words.is_empty() {
4103 let mut expanded =
4104 Vec::with_capacity(results.len() * (1 + pre_words.len() + post_words.len()));
4105 for s in results {
4106 expanded.extend(pre_words.iter().cloned());
4107 expanded.push(s);
4108 expanded.extend(post_words.iter().cloned());
4109 }
4110 results = expanded;
4111 }
4112
4113 // c:Src/glob.c:1872-1888 — no-match path. Return Vec::new() so
4114 // the caller (vm_helper.rs::expand_glob) drives the
4115 // NULLGLOB / NOMATCH / literal-fallback policy. Previously
4116 // this pushed `pattern.to_string()` on no-match, which made
4117 // `expand_glob` see a non-empty result and skip the NOMATCH
4118 // check entirely. zsh's `setopt nomatch` is on by default, so
4119 // `echo /never_real/*` should emit "no matches found" and
4120 // exit 1 — not silently print the literal. Parity bug #13
4121 // recurrence. Per-glob `(N)` qualifier (nullglob_per_qual)
4122 // and global NULLGLOB are now consulted in expand_glob too.
4123 let _ = state.qualifiers.as_ref().map(|q| q.nullglob);
4124 results
4125}
4126
4127// `sort_matches_by_type` deleted — dead code (no callers anywhere
4128// in the tree). The sort path lives in `GlobMatch::compare` which
4129// dispatches off the canonical `GS_*` tp bits exactly like C's
4130// `gmatchcmp()` at glob.c:936.
4131
4132// ============================================================================
4133// Pattern matching with replacement (from glob.c getmatch family)
4134// ============================================================================
4135
4136// `MatchFlags` deleted — Rust-only bool-bag wrapper. C uses bare
4137// `int fl` with `SUB_*` bits from `Src/zsh.h:1981+` (mirrored at
4138// `zsh_h.rs:2463+`). Callers now pass an `i32` and test bits.
4139
4140// Trimmed local `struct imatchdata` deleted — it was a fake 5-field
4141// duplicate (str/pattern/match_start/match_end/replacement) of the
4142// canonical `Src/zsh.h:1740` port that already lives in
4143// `zsh_h.rs::imatchdata` (mstr/mlen/ustr/ulen/flags/replstr/repllist).
4144// `get_match_ret` now uses the canonical struct; see its port above.
4145
4146/// Strip a trailing `(qual)` block from a glob pattern.
4147/// **RUST-ONLY** — C glob.c handles qualifier parsing inline in
4148/// `parsepat` (c:791) as it builds the Complist. Move to that
4149/// shape when porting parsepat for real.
4150pub fn parse_qualifiers(pattern: &str) -> (String, Option<qualifier_set>) {
4151 // RUST-ONLY
4152 if !pattern.ends_with(')') {
4153 return (pattern.to_string(), None);
4154 }
4155
4156 let bytes = pattern.as_bytes();
4157 // Backslash-escaped trailing `)` is literal — no qualifier block
4158 // present. C's glob path handles this via Bnull preservation in
4159 // the lexer; zshrs's path arrives here with `\)` for quoted
4160 // close-parens (e.g. assoc-value `'\)'`). Without the gate, the
4161 // qualifier-extraction walk below treats the literal `)` as the
4162 // qualifier terminator.
4163 let last_paren_escaped = {
4164 let mut bs = 0usize;
4165 let mut j = bytes.len() - 1;
4166 while j > 0 && bytes[j - 1] == b'\\' {
4167 bs += 1;
4168 j -= 1;
4169 }
4170 bs % 2 == 1
4171 };
4172 if last_paren_escaped {
4173 return (pattern.to_string(), None);
4174 }
4175
4176 // Find matching open paren
4177 let mut depth = 0;
4178 let mut qual_start = None;
4179
4180 // c:Src/glob.c haswilds backslash handling — a `\(` / `\)` is a
4181 // literal paren, not a qualifier delimiter. Track which paren
4182 // bytes are escaped by a preceding `\` (counting consecutive
4183 // backslashes — even count means the paren is unescaped, odd
4184 // count means escaped). Without this gate, `\(*\)` is mis-parsed
4185 // as `(*\)` qualifier + empty pattern prefix, sending `*\` to
4186 // the qualifier-letter switch which emits "unknown file
4187 // attribute: \".
4188 let is_escaped = |idx: usize| -> bool {
4189 let mut bs = 0usize;
4190 let mut j = idx;
4191 while j > 0 && bytes[j - 1] == b'\\' {
4192 bs += 1;
4193 j -= 1;
4194 }
4195 bs % 2 == 1
4196 };
4197
4198 for i in (0..bytes.len()).rev() {
4199 match bytes[i] {
4200 b')' => {
4201 if !is_escaped(i) {
4202 depth += 1;
4203 }
4204 }
4205 b'(' => {
4206 if !is_escaped(i) {
4207 depth -= 1;
4208 if depth == 0 {
4209 qual_start = Some(i);
4210 break;
4211 }
4212 }
4213 }
4214 _ => {}
4215 }
4216 }
4217
4218 let start = match qual_start {
4219 Some(s) => s,
4220 None => return (pattern.to_string(), None),
4221 };
4222
4223 // Check for (#q...) explicit qualifier syntax.
4224 let qual_str = &pattern[start + 1..pattern.len() - 1];
4225 // c:Src/glob.c:1192-1197 — `if (isset(EXTENDEDGLOB) &&
4226 // !zpc_disables[ZPC_HASH] && s[1] == Pound) { if (s[2] != 'q')
4227 // return 0; ret = 2; }`. The leading `#` inside `(...)` is ONLY
4228 // special under EXTENDEDGLOB: `(#q...)` is the explicit glob
4229 // qualifier, and `(#X...)` for any other X is an inline pattern flag
4230 // (passed through). Without EXTENDEDGLOB the `#` is not special at
4231 // all — the `(...)` is a bare qualifier group and a leading `#` is an
4232 // (unknown) attribute char, so `*(#q.)` errors `unknown file
4233 // attribute: #` rather than silently applying the `.` qualifier.
4234 let (is_explicit, qual_content) = if glob_isset(EXTENDEDGLOB) && qual_str.starts_with('#') {
4235 if let Some(after) = qual_str.strip_prefix("#q") {
4236 (true, after) // c:1195 ret = 2 — explicit glob qualifier
4237 } else {
4238 // c:1194 `if (s[2] != 'q') return 0;` — inline pattern flag
4239 // (`(#i)`, `(#c1,2)`, `(#a)`, `(#l)`, `(#s)`, `(#e)`, `(#m)`).
4240 return (pattern.to_string(), None);
4241 }
4242 } else if glob_isset(BAREGLOBQUAL) {
4243 (false, qual_str)
4244 } else {
4245 return (pattern.to_string(), None);
4246 };
4247
4248 // Don't parse as qualifiers if it contains | or ~ (alternatives/exclusions)
4249 if !is_explicit && (qual_content.contains('|') || qual_content.contains('~')) {
4250 return (pattern.to_string(), None);
4251 }
4252
4253 // Parse the qualifiers
4254 let qs = parse_qualifier_string(qual_content);
4255 (pattern[..start].to_string(), Some(qs))
4256}
4257
4258/// Parse the body of a `(...)` qualifier block into a qualifier_set.
4259/// **RUST-ONLY** — see header on `parse_qualifiers`.
4260fn parse_qualifier_string(s: &str) -> qualifier_set {
4261 // RUST-ONLY
4262 let mut qs = qualifier_set::default();
4263 let mut chars = s.chars().peekable();
4264 let mut negated = false;
4265 let mut follow = false;
4266
4267 while let Some(c) = chars.next() {
4268 match c {
4269 '^' => {
4270 // c:1346 — `sense ^= 1`. Keep the running `negated` for
4271 // the flag qualifiers (N/D/M/T) that read it, AND emit a
4272 // marker so the per-qualifier sense reaches the list walk.
4273 negated = !negated;
4274 qs.qualifiers.push(qualifier::ToggleSense);
4275 }
4276 '-' => follow = !follow,
4277 ',' => {
4278 // Start new alternative
4279 if !qs.qualifiers.is_empty() {
4280 qs.alternatives.push(std::mem::take(&mut qs.qualifiers));
4281 }
4282 negated = false;
4283 follow = false;
4284 }
4285 ':' => {
4286 // Colon modifiers - rest of string
4287 let rest: String = chars.collect();
4288 qs.colon_mods = Some(format!(":{}", rest));
4289 break;
4290 }
4291 // File type qualifiers
4292 '/' => qs.qualifiers.push(qualifier::IsDirectory),
4293 '.' => qs.qualifiers.push(qualifier::IsRegular),
4294 '@' => qs.qualifiers.push(qualifier::IsSymlink),
4295 '=' => qs.qualifiers.push(qualifier::IsSocket),
4296 'p' => qs.qualifiers.push(qualifier::IsFifo),
4297 '%' => match chars.peek() {
4298 Some('b') => {
4299 chars.next();
4300 qs.qualifiers.push(qualifier::IsBlockDev);
4301 }
4302 Some('c') => {
4303 chars.next();
4304 qs.qualifiers.push(qualifier::IsCharDev);
4305 }
4306 _ => qs.qualifiers.push(qualifier::IsDevice),
4307 },
4308 '*' => qs.qualifiers.push(qualifier::IsExecutable),
4309 // Permission qualifiers
4310 'r' => qs.qualifiers.push(qualifier::Readable),
4311 'w' => qs.qualifiers.push(qualifier::Writable),
4312 'x' => qs.qualifiers.push(qualifier::Executable),
4313 'R' => qs.qualifiers.push(qualifier::WorldReadable),
4314 'W' => qs.qualifiers.push(qualifier::WorldWritable),
4315 'X' => qs.qualifiers.push(qualifier::WorldExecutable),
4316 'A' => qs.qualifiers.push(qualifier::GroupReadable),
4317 'I' => qs.qualifiers.push(qualifier::GroupWritable),
4318 'E' => qs.qualifiers.push(qualifier::GroupExecutable),
4319 's' => qs.qualifiers.push(qualifier::Setuid),
4320 'S' => qs.qualifiers.push(qualifier::Setgid),
4321 't' => qs.qualifiers.push(qualifier::Sticky),
4322 // c:Src/glob.c case 'f' — file-mode qualifier accepts
4323 // an octal mode (`f755`) or chmod-style symbolic spec
4324 // (`fu+x`/`fg-w`/`fa=r`) and matches files whose mode
4325 // bits agree. Multiple delimited specs can be chained
4326 // by separator character. Direct port of qgetmodespec
4327 // dispatch + mode-bit accumulator in qgetmodespec.
4328 'f' => {
4329 // c:Src/glob.c:849 — qgetmodespec dispatches on the
4330 // first char: if it's `=`/`+`/`-`/`?` or a digit
4331 // (`(c >= '0' && c <= '7')`) the spec is bare (no
4332 // surrounding delimiter, no who) — `f+x` / `f-w` /
4333 // `f=755` / `f0644`. Otherwise the char is the
4334 // OPENING DELIMITER paired with a matching closer
4335 // (`<` → `>`, `[` → `]`, `{` → `}`, anything else
4336 // → itself, e.g. `f:u+x:` uses `:` on both sides).
4337 // The previous port used `!d.is_alphanumeric()`
4338 // which incorrectly classified `+`, `-`, `=`, `?`
4339 // as delimiters, sending `f+x` down the
4340 // delimiter-string path that then called
4341 // qgetmodespec on body "x" — qgetmodespec rejects
4342 // `x` (no op char) and silently dropped the
4343 // qualifier, so `*(.f+x)` matched ALL files. Bug
4344 // #105 in docs/BUGS.md.
4345 let rest: String = chars.clone().collect();
4346 let trimmed: &str = match rest.chars().next() {
4347 // c:849-861 — bare spec ONLY when the first char is
4348 // `=`/`+`/`-`/`?` or an octal digit; ANY other char is
4349 // the OPENING DELIMITER (`<`→`>`, `[`→`]`, `{`→`}`, else
4350 // itself). Who-clauses (u/g/o/a) are valid only INSIDE a
4351 // delimited spec, so bare `fu+x` lands here with `u` as
4352 // the delimiter — and with no closing `u` it is an
4353 // unterminated (invalid) spec, exactly as in zsh.
4354 Some(d) if !matches!(d, '+' | '-' | '=' | '?' | '0'..='7') => {
4355 chars.next(); // consume opening delim
4356 let close = match d {
4357 '<' => '>',
4358 '[' => ']',
4359 '{' => '}',
4360 other => other,
4361 };
4362 let mut body = String::new();
4363 let mut closed = false;
4364 while let Some(pc) = chars.next() {
4365 if pc == close {
4366 closed = true;
4367 break;
4368 }
4369 body.push(pc);
4370 }
4371 if !closed {
4372 // c:884/930 — `zerr("invalid mode specification")`
4373 // on an unterminated spec; the glob then fails.
4374 crate::ported::utils::zerr("invalid mode specification");
4375 crate::ported::utils::errflag.fetch_or(
4376 crate::ported::utils::ERRFLAG_ERROR,
4377 std::sync::atomic::Ordering::Relaxed,
4378 );
4379 return qs;
4380 }
4381 if let Some((_who, op, perm, _r)) = qgetmodespec(&body) {
4382 let (yes, no) = match op {
4383 '=' => (perm, 0o7777 & !perm),
4384 '+' => (perm, 0),
4385 '-' => (0, perm),
4386 _ => (perm, 0),
4387 };
4388 qs.qualifiers.push(qualifier::Mode { yes, no });
4389 } else {
4390 // c:884/930 — qgetmodespec's every failure path is
4391 // `zerr("invalid mode specification"); return 0;`.
4392 // rs's qgetmodespec returns None only on those, so
4393 // None must fail the glob, not silently drop `f`.
4394 crate::ported::utils::zerr("invalid mode specification");
4395 crate::ported::utils::errflag.fetch_or(
4396 crate::ported::utils::ERRFLAG_ERROR,
4397 std::sync::atomic::Ordering::Relaxed,
4398 );
4399 return qs;
4400 }
4401 ""
4402 }
4403 _ => {
4404 let parsed = qgetmodespec(&rest);
4405 if let Some((who, op, perm, r)) = parsed {
4406 let consumed = rest.len() - r.len();
4407 for _ in 0..consumed {
4408 chars.next();
4409 }
4410 let _ = who;
4411 let (yes, no) = match op {
4412 '=' => (perm, 0o7777 & !perm),
4413 '+' => (perm, 0),
4414 '-' => (0, perm),
4415 _ => (perm, 0),
4416 };
4417 qs.qualifiers.push(qualifier::Mode { yes, no });
4418 } else {
4419 // c:930 — a bare `f` (empty spec) or otherwise
4420 // unparseable mode is `zerr("invalid mode
4421 // specification"); return 0;` in qgetmodespec, which
4422 // aborts the glob rather than dropping the qualifier.
4423 crate::ported::utils::zerr("invalid mode specification");
4424 crate::ported::utils::errflag.fetch_or(
4425 crate::ported::utils::ERRFLAG_ERROR,
4426 std::sync::atomic::Ordering::Relaxed,
4427 );
4428 return qs;
4429 }
4430 ""
4431 }
4432 };
4433 let _ = trimmed;
4434 }
4435 // Ownership
4436 'U' => qs.qualifiers.push(qualifier::OwnedByEuid),
4437 'G' => qs.qualifiers.push(qualifier::OwnedByEgid),
4438 'u' => {
4439 let uid = parse_uid_gid(&mut chars, false);
4440 qs.qualifiers.push(qualifier::OwnedByUid(uid));
4441 }
4442 'g' => {
4443 let gid = parse_uid_gid(&mut chars, true);
4444 qs.qualifiers.push(qualifier::OwnedByGid(gid));
4445 }
4446 // Size
4447 'L' => {
4448 let (unit, op, val) = parse_size_spec(&mut chars);
4449 qs.qualifiers.push(qualifier::Size {
4450 value: val,
4451 unit,
4452 op,
4453 });
4454 }
4455 // Link count
4456 'l' => {
4457 let (op, val) = parse_range_spec(&mut chars);
4458 qs.qualifiers.push(qualifier::Links { value: val, op });
4459 }
4460 // c:Src/glob.c:1445-1449 — `case 'd': func = qualdev;
4461 // data = qgetnum(&s);`. Matches files by device number
4462 // (`qualdev`, c:3688, is `buf->st_dev == dv`). Both the
4463 // `qualifier::Device` variant and the `qualdev` matcher were
4464 // already ported and wired at glob.rs:4894 — only this parser arm
4465 // was missing, so `*(d5)` never reached them and a bare `*(d)`
4466 // reported "unknown file attribute: d" where zsh says
4467 // "number expected".
4468 //
4469 // c:826-834 qgetnum takes PLAIN digits — deliberately NOT
4470 // `parse_range_spec`, which would also accept the `+`/`-` range
4471 // operators that `l`/`L`/`m` use and that C does not allow here.
4472 'd' => {
4473 let mut num = String::new();
4474 while let Some(&pc) = chars.peek() {
4475 if pc.is_ascii_digit() {
4476 num.push(pc);
4477 chars.next();
4478 } else {
4479 break;
4480 }
4481 }
4482 if num.is_empty() {
4483 // c:830-833 — `if (!idigit(**s)) { zerr("number expected");
4484 // return 0; }`. Matches the sibling error arms in this
4485 // parser (`Y`, `f`): report, set errflag, stop.
4486 crate::ported::utils::zerr("number expected"); // c:832
4487 crate::ported::utils::errflag.fetch_or(
4488 crate::ported::utils::ERRFLAG_ERROR,
4489 std::sync::atomic::Ordering::Relaxed,
4490 );
4491 return qs;
4492 }
4493 qs.qualifiers
4494 .push(qualifier::Device(num.parse().unwrap_or(0))); // c:1448
4495 }
4496 // Times
4497 'a' => {
4498 let (unit, op, val) = schedgetfn(&mut chars);
4499 qs.qualifiers.push(qualifier::Atime {
4500 value: val as i64,
4501 unit,
4502 op,
4503 });
4504 }
4505 'm' => {
4506 let (unit, op, val) = schedgetfn(&mut chars);
4507 qs.qualifiers.push(qualifier::Mtime {
4508 value: val as i64,
4509 unit,
4510 op,
4511 });
4512 }
4513 'c' => {
4514 let (unit, op, val) = schedgetfn(&mut chars);
4515 qs.qualifiers.push(qualifier::Ctime {
4516 value: val as i64,
4517 unit,
4518 op,
4519 });
4520 }
4521 // Sort qualifier — `o<key>` ascending / `O<key>`
4522 // descending. Mirrors the parser arm in zsh's glob.c
4523 // that builds `struct globsort` entries (tp = GS_* OR
4524 // GS_DESC, optionally shifted by GS_SHIFT for the
4525 // follow-links variant).
4526 'o' | 'O' => {
4527 let desc = c == 'O';
4528 if let Some(&sc) = chars.peek() {
4529 let key: i32 = match sc {
4530 'n' => {
4531 chars.next();
4532 GS_NAME
4533 }
4534 'L' => {
4535 chars.next();
4536 GS_SIZE
4537 }
4538 'l' => {
4539 chars.next();
4540 GS_LINKS
4541 }
4542 'a' => {
4543 chars.next();
4544 GS_ATIME
4545 }
4546 'm' => {
4547 chars.next();
4548 GS_MTIME
4549 }
4550 'c' => {
4551 chars.next();
4552 GS_CTIME
4553 }
4554 'd' => {
4555 chars.next();
4556 GS_DEPTH
4557 }
4558 'N' => {
4559 chars.next();
4560 GS_NONE
4561 }
4562 'e' | '+' => {
4563 // c:1672-1687 — GS_EXEC sort: `glob_exec_string`
4564 // parses the eval code (delimited `e:code:` or
4565 // identifier `+name`); store it indexed and pack
4566 // the index into the sort entry. The code is
4567 // consumed HERE (not left for the main loop), so
4568 // `oe:…:` is a SORT (eval = key, original name
4569 // emitted), not a separate eval qualifier.
4570 let is_plus = sc == '+';
4571 chars.next(); // consume 'e' / '+'
4572 let rest: String = chars.clone().collect();
4573 match glob_exec_string(&rest, is_plus) {
4574 Some((code, consumed)) => {
4575 for _ in 0..consumed {
4576 chars.next();
4577 }
4578 let idx = qs.sort_exec.len();
4579 qs.sort_exec.push(code);
4580 GS_EXEC | ((idx as i32) << 16)
4581 }
4582 None => {
4583 // glob_exec_string already emitted the
4584 // diagnostic + set errflag; bail.
4585 return qs;
4586 }
4587 }
4588 }
4589 _ => {
4590 // c:1689 — `default: zerr("unknown sort
4591 // specifier"); restore_globstate(saved);
4592 // return;`. Was `_ => GS_NAME`, which silently
4593 // accepted invalid keys (and reparsed them as
4594 // separate qualifiers, e.g. `oD` → o + D-dotglob
4595 // instead of erroring like zsh).
4596 crate::ported::utils::zerr("unknown sort specifier");
4597 crate::ported::utils::errflag.fetch_or(
4598 crate::ported::utils::ERRFLAG_ERROR,
4599 std::sync::atomic::Ordering::Relaxed,
4600 );
4601 return qs;
4602 }
4603 };
4604 let shifted = if follow && (key & GS_NORMAL) != 0 {
4605 key << GS_SHIFT // c:1692-1694
4606 } else {
4607 key
4608 };
4609 // c:1695-1702 — a sort key may not be repeated:
4610 // if (t != GS_EXEC) {
4611 // if (gf_sorts & t) {
4612 // zerr("doubled sort specifier");
4613 // restore_globstate(saved);
4614 // return;
4615 // }
4616 // }
4617 // gf_sorts |= t;
4618 // GS_EXEC is exempt — `oe:…:` may appear repeatedly, each
4619 // with its own code. This check was missing entirely, so
4620 // `*(.onon)` and `*(.onOn)` silently sorted where zsh
4621 // fails. Note the test is on the SHIFTED key, so `oL` and
4622 // `OL` collide (same key, different direction) while `oL`
4623 // and `oLm` do not (the follow variant shifts).
4624 if (shifted & GS_EXEC) == 0 && qs.sorts.iter().any(|s| (s & !GS_DESC) == shifted)
4625 {
4626 crate::ported::utils::zerr("doubled sort specifier"); // c:1697
4627 crate::ported::utils::errflag.fetch_or(
4628 crate::ported::utils::ERRFLAG_ERROR,
4629 std::sync::atomic::Ordering::Relaxed,
4630 );
4631 return qs; // c:1699
4632 }
4633 // c:1658-1662 — `if (gf_nsorts == MAX_SORTS) { zerr("too
4634 // many glob sort specifiers"); ... return; }`. MAX_SORTS is
4635 // 12 (c:164). Unreachable in practice now that the doubled
4636 // check above rejects repeats, but it is C's bound and the
4637 // list is otherwise unbounded.
4638 if qs.sorts.len() == MAX_SORTS {
4639 crate::ported::utils::zerr("too many glob sort specifiers"); // c:1659
4640 crate::ported::utils::errflag.fetch_or(
4641 crate::ported::utils::ERRFLAG_ERROR,
4642 std::sync::atomic::Ordering::Relaxed,
4643 );
4644 return qs; // c:1661
4645 }
4646 let tp = shifted | (if desc { GS_DESC } else { 0 });
4647 qs.sorts.push(tp);
4648 } else {
4649 // c:1666-1690 — C does not guard on "is there a next
4650 // character": it reads `switch (*s)` unconditionally, so an
4651 // `o`/`O` at the very end of the qualifier list sees the
4652 // terminating `)` and lands on `default: zerr("unknown sort
4653 // specifier")`. This port skipped the whole block when the
4654 // iterator was exhausted, so a trailing `o` was silently
4655 // ignored and `*(No)` listed every match where zsh fails.
4656 // A NON-trailing bad spec (`*(ozN)`) already errored — it
4657 // reaches the `_` arm above — which is why only the
4658 // end-of-list form survived.
4659 crate::ported::utils::zerr("unknown sort specifier"); // c:1688
4660 crate::ported::utils::errflag.fetch_or(
4661 crate::ported::utils::ERRFLAG_ERROR,
4662 std::sync::atomic::Ordering::Relaxed,
4663 );
4664 return qs; // c:1690
4665 }
4666 }
4667 // Flags
4668 // c:1567-1569 — `case 'N': gf_nullglob = !(sense & 1);`.
4669 // Per-glob nullglob: empty result on no-match, no error.
4670 'N' => qs.nullglob = !negated,
4671 'D' => qs.globdots = !negated,
4672 // c:1575-1577 — `case 'n': gf_numsort = !(sense & 1)`.
4673 // Standalone `(n)` enables numeric sort for this glob (the
4674 // `^n` toggle disables it). Consumed in sort_matches.
4675 'n' => qs.numsort = Some(!negated),
4676 // (M) / (T) — set per-qualifier-set flags. glob.c:1557-1566:
4677 // case 'M': gf_markdirs = !(sense & 1); break;
4678 // case 'T': gf_listtypes = !(sense & 1); break;
4679 // `sense & 1` = the `^`-toggle bit. zshrs's parser tracks
4680 // `negated`; mirror by `!negated`. Read at output-emit
4681 // time to mark dirs / list types like coreutils ls -F.
4682 'M' => qs.mark_dirs = !negated,
4683 'T' => qs.list_types = !negated,
4684 'F' => qs.qualifiers.push(qualifier::NonEmptyDir),
4685 // c:1744-1756 — `P:word:` adds `word` to gf_pre_words (or
4686 // gf_post_words under `^`), emitted as a separate element
4687 // before/after each match. `glob_exec_string` parses the
4688 // delimited word.
4689 'P' => {
4690 let rest: String = chars.clone().collect();
4691 if let Some((word, consumed)) = glob_exec_string(&rest, false) {
4692 for _ in 0..consumed {
4693 chars.next();
4694 }
4695 if negated {
4696 qs.post_words.push(word); // c:1750 gf_post_words
4697 } else {
4698 qs.pre_words.push(word); // c:1750 gf_pre_words
4699 }
4700 }
4701 }
4702 // Subscript
4703 '[' => {
4704 let (first, last) = parse_subscript(&mut chars);
4705 qs.first = first;
4706 qs.last = last;
4707 }
4708 // c:Src/glob.c:1579-1595 `case 'Y'` — short-circuit:
4709 // limit matches to at most N. Reads a numeric argument
4710 // immediately following the `Y`. Bug #41 in docs/BUGS.md.
4711 'Y' => {
4712 // c:1579-1594:
4713 // const char *s_saved = s;
4714 // shortcircuit = !(sense & 1);
4715 // if (shortcircuit) {
4716 // data = qgetnum(&s);
4717 // if ((shortcircuit = data) != data) {
4718 // /* Integer overflow */
4719 // zerr("value too big: Y%s", s_saved);
4720 // restore_globstate(saved);
4721 // return;
4722 // }
4723 // }
4724 // `data` is a zlong and `shortcircuit` an int, so the
4725 // assignment-then-compare is a 64→32 TRUNCATION test: a limit
4726 // that does not survive the narrowing is fatal. This parsed
4727 // straight into i32 and silently dropped the qualifier when
4728 // that failed, so `*(Y2147483648)` listed every match where
4729 // zsh errors — the limit was simply ignored.
4730 //
4731 // s_saved is the text AFTER the `Y`, running to the end of the
4732 // qualifier list, which is why the message carries any trailing
4733 // qualifiers too: `*(.NY99…)` reports `value too big: Y99…`
4734 // exactly as spelled.
4735 let s_saved: String = chars.clone().collect(); // c:1582
4736 let mut num_str = String::new();
4737 while let Some(&pc) = chars.peek() {
4738 if pc.is_ascii_digit() {
4739 num_str.push(pc);
4740 chars.next();
4741 } else {
4742 break;
4743 }
4744 }
4745 // c:1586 — qgetnum is `while (idigit(**s)) v = v * 10 + …`,
4746 // with no overflow check of its own; it simply wraps. Mirror
4747 // that rather than failing the parse, so the truncation test
4748 // below is what decides, exactly as in C.
4749 let mut data: i64 = 0;
4750 for ch in num_str.chars() {
4751 data = data
4752 .wrapping_mul(10)
4753 .wrapping_add((ch as i64) - ('0' as i64));
4754 }
4755 let sc = data as i32; // c:1587 `shortcircuit = data`
4756 if sc as i64 != data {
4757 // c:1587 `!= data`
4758 crate::ported::utils::zerr(&format!("value too big: Y{s_saved}")); // c:1589
4759 crate::ported::utils::errflag.fetch_or(
4760 crate::ported::utils::ERRFLAG_ERROR,
4761 std::sync::atomic::Ordering::Relaxed,
4762 );
4763 return qs; // c:1591
4764 }
4765 qs.short_circuit = Some(sc);
4766 }
4767 // c:Src/glob.c:1599-1620 `case 'e'` — `(e:CODE:)` shell-
4768 // eval qualifier. The body is delimited by the char
4769 // immediately following 'e' (matched at both ends).
4770 // Common forms in zsh scripts: `(e:'[[ -L $REPLY ]]':)`
4771 // for symlink filter, etc. The body runs against each
4772 // candidate path with $REPLY set; the file is kept iff
4773 // the body returns 0. zshrs's qualifier::Eval variant
4774 // was defined but had no parser arm — was rejected as
4775 // "unknown file attribute: e". Bug #469.
4776 'e' => {
4777 // c:1712-1719 — `e` routes through glob_exec_string →
4778 // get_strarg, which requires a delimiter char after `e` AND a
4779 // matching closer. A bare `e` (nothing after, delim would be the
4780 // `)`) or an unterminated body is `zerr("missing end of
4781 // string"); return NULL;` (c:1102), aborting the glob.
4782 let delim = match chars.next() {
4783 Some(d) => d,
4784 None => {
4785 crate::ported::utils::zerr("missing end of string");
4786 crate::ported::utils::errflag.fetch_or(
4787 crate::ported::utils::ERRFLAG_ERROR,
4788 std::sync::atomic::Ordering::Relaxed,
4789 );
4790 return qs;
4791 }
4792 };
4793 let mut body = String::new();
4794 let mut closed = false;
4795 while let Some(&pc) = chars.peek() {
4796 if pc == delim {
4797 chars.next();
4798 closed = true;
4799 break;
4800 }
4801 body.push(pc);
4802 chars.next();
4803 }
4804 if !closed {
4805 crate::ported::utils::zerr("missing end of string");
4806 crate::ported::utils::errflag.fetch_or(
4807 crate::ported::utils::ERRFLAG_ERROR,
4808 std::sync::atomic::Ordering::Relaxed,
4809 );
4810 return qs;
4811 }
4812 if negated {
4813 // (^e:CODE:) inverts; we route through Eval and
4814 // flip by wrapping the body. Simpler: just store
4815 // and let the negated flag at the qualifier_set
4816 // level handle inversion.
4817 }
4818 qs.qualifiers.push(qualifier::Eval(body));
4819 }
4820 // c:Src/glob.c:1708-1722 `case '+':` — `(+FUNC)` invokes
4821 // shell function FUNC on each candidate; keep file iff
4822 // function returns 0. C's glob_exec_string (c:1085) reads
4823 // the identifier name via `itype_end(s, IIDENT, 0)` when
4824 // the qualifier letter was '+'. The body wraps the call as
4825 // a shell expression `FUNC` that qualsheval (c:4769 zshrs
4826 // qualifier::Eval) runs as a one-shot. Bug #N — this arm
4827 // was missing entirely, so `*(+func)` and `*(s+0)`-style
4828 // mixed-qualifier strings errored "unknown file attribute:
4829 // +" instead of routing through the Eval path that would
4830 // either match files or fall through to "no matches found".
4831 '+' => {
4832 // c:1090-1097 — `tt = itype_end(s, IIDENT, 0); if
4833 // (tt == s) zerr("missing identifier after `+'")`.
4834 // Read identifier chars greedily.
4835 let mut ident = String::new();
4836 while let Some(&pc) = chars.peek() {
4837 if pc.is_ascii_alphanumeric() || pc == '_' {
4838 ident.push(pc);
4839 chars.next();
4840 } else {
4841 break;
4842 }
4843 }
4844 if ident.is_empty() {
4845 crate::ported::utils::zerr("missing identifier after `+'"); // c:1095
4846 crate::ported::utils::errflag.fetch_or(
4847 crate::ported::utils::ERRFLAG_ERROR,
4848 std::sync::atomic::Ordering::Relaxed,
4849 );
4850 return qs;
4851 }
4852 // c:1109-1117 — the identifier IS the body that
4853 // qualsheval will run. Push as Eval so the same
4854 // per-file evaluator at c:4769 fires.
4855 qs.qualifiers.push(qualifier::Eval(ident));
4856 }
4857 // c:Src/glob.c:1758-1762 — `default: zerr("unknown file
4858 // attribute: %c", *s); restore_globstate(saved); return;`.
4859 // Bug #583: zshrs's `_ => {}` arm silently accepted any
4860 // unknown qualifier letter, so `*(z)` returned all files
4861 // rc=0 instead of erroring "unknown file attribute: z".
4862 // Mirror C's strict behavior — but skip ASCII whitespace
4863 // / `\0` (parser leftovers) and chars below 0x21 since
4864 // C's qualifier loop stops on those naturally via its
4865 // `case ')'` arm before reaching default.
4866 ch if !ch.is_whitespace() && ch != '\0' && ch != ')' => {
4867 crate::ported::utils::zerr(&format!("unknown file attribute: {}", ch));
4868 crate::ported::utils::errflag.fetch_or(
4869 crate::ported::utils::ERRFLAG_ERROR,
4870 std::sync::atomic::Ordering::Relaxed,
4871 );
4872 return qs;
4873 }
4874 _ => {}
4875 }
4876 }
4877
4878 if !qs.qualifiers.is_empty() {
4879 qs.alternatives.push(std::mem::take(&mut qs.qualifiers));
4880 }
4881
4882 qs.follow_links = follow;
4883
4884 // Build the faithful glob.c `struct qual` arena from `alternatives`
4885 // (additive: matching still uses the enum). Each alternative is an
4886 // AND-chain (`next` links); alternatives chain via `or`. ToggleSense
4887 // markers toggle a running per-node `sense` (c:1346). Each variant
4888 // maps to its leaf TestMatchFunc + data, mirroring check_single_
4889 // qualifier's dispatch (Src/glob.c:1343-1620 case → func/data).
4890 {
4891 let op_range = |o: char| -> i32 {
4892 match o {
4893 '<' => -1,
4894 '>' => 1,
4895 _ => 0,
4896 }
4897 };
4898 let mut arena = QualArena::new();
4899 let mut prev_alt_head: Option<usize> = None;
4900 for alt in &qs.alternatives {
4901 let mut sense = 0i32;
4902 let mut chain_tail: Option<usize> = None;
4903 let mut chain_head: Option<usize> = None;
4904 for q in alt {
4905 // (func, data, range, amc, units, sdata) — None = no node.
4906 let fields: Option<(TestMatchFunc, i64, i32, i32, i32, Option<String>)> = match q {
4907 qualifier::ToggleSense => {
4908 sense ^= 1; // c:1346
4909 None
4910 }
4911 qualifier::IsRegular => Some((qualisreg, 0, 0, 0, 0, None)),
4912 qualifier::IsDirectory => Some((qualisdir, 0, 0, 0, 0, None)),
4913 qualifier::IsSymlink => Some((qualislnk, 0, 0, 0, 0, None)),
4914 qualifier::IsSocket => Some((qualissock, 0, 0, 0, 0, None)),
4915 qualifier::IsFifo => Some((qualisfifo, 0, 0, 0, 0, None)),
4916 qualifier::IsBlockDev => Some((qualisblk, 0, 0, 0, 0, None)),
4917 qualifier::IsCharDev => Some((qualischr, 0, 0, 0, 0, None)),
4918 qualifier::IsDevice => Some((qualisdev, 0, 0, 0, 0, None)),
4919 qualifier::IsExecutable => Some((qualiscom, 0, 0, 0, 0, None)),
4920 qualifier::Readable => Some((qualflags, 0o400, 0, 0, 0, None)),
4921 qualifier::Writable => Some((qualflags, 0o200, 0, 0, 0, None)),
4922 qualifier::Executable => Some((qualflags, 0o100, 0, 0, 0, None)),
4923 qualifier::WorldReadable => Some((qualflags, 0o004, 0, 0, 0, None)),
4924 qualifier::WorldWritable => Some((qualflags, 0o002, 0, 0, 0, None)),
4925 qualifier::WorldExecutable => Some((qualflags, 0o001, 0, 0, 0, None)),
4926 qualifier::GroupReadable => Some((qualflags, 0o040, 0, 0, 0, None)),
4927 qualifier::GroupWritable => Some((qualflags, 0o020, 0, 0, 0, None)),
4928 qualifier::GroupExecutable => Some((qualflags, 0o010, 0, 0, 0, None)),
4929 qualifier::Setuid => Some((qualflags, 0o4000, 0, 0, 0, None)),
4930 qualifier::Setgid => Some((qualflags, 0o2000, 0, 0, 0, None)),
4931 qualifier::Sticky => Some((qualflags, 0o1000, 0, 0, 0, None)),
4932 qualifier::OwnedByEuid => {
4933 Some((qualuid, unsafe { libc::geteuid() } as i64, 0, 0, 0, None))
4934 }
4935 qualifier::OwnedByEgid => {
4936 Some((qualgid, unsafe { libc::getegid() } as i64, 0, 0, 0, None))
4937 }
4938 qualifier::OwnedByUid(u) => Some((qualuid, *u as i64, 0, 0, 0, None)),
4939 qualifier::OwnedByGid(g) => Some((qualgid, *g as i64, 0, 0, 0, None)),
4940 qualifier::Size { value, unit, op } => {
4941 Some((qualsize, *value as i64, op_range(*op), -1, *unit, None))
4942 }
4943 qualifier::Links { value, op } => {
4944 Some((qualnlink, *value as i64, op_range(*op), 0, 0, None))
4945 }
4946 qualifier::Atime { value, unit, op } => {
4947 Some((qualtime, *value, op_range(*op), 0, *unit, None))
4948 }
4949 qualifier::Mtime { value, unit, op } => {
4950 Some((qualtime, *value, op_range(*op), 1, *unit, None))
4951 }
4952 qualifier::Ctime { value, unit, op } => {
4953 Some((qualtime, *value, op_range(*op), 2, *unit, None))
4954 }
4955 qualifier::Mode { yes, no } => Some((
4956 qualmodeflags,
4957 (*yes as i64) | ((*no as i64) << 12),
4958 0,
4959 0,
4960 0,
4961 None,
4962 )),
4963 qualifier::Device(d) => Some((qualdev, *d as i64, 0, 0, 0, None)),
4964 qualifier::NonEmptyDir => Some((qualnonemptydir, 0, 0, 0, 0, None)),
4965 qualifier::Eval(code) => Some((qualsheval, 0, 0, 0, 0, Some(code.clone()))),
4966 };
4967 let Some((func, data, range, amc, units, sdata)) = fields else {
4968 continue;
4969 };
4970 let idx = arena.alloc(); // c:1768 hcalloc
4971 {
4972 let node = &mut arena.nodes[idx];
4973 node.func = Some(func); // c:1774
4974 node.data = data; // c:1776
4975 node.sense = sense; // c:1775
4976 node.range = range; // c:1778
4977 node.units = units; // c:1779
4978 node.amc = amc; // c:1780
4979 node.sdata = sdata; // c:1777
4980 }
4981 if let Some(t) = chain_tail {
4982 arena.nodes[t].next = Some(idx); // c:1770
4983 }
4984 chain_tail = Some(idx);
4985 if chain_head.is_none() {
4986 chain_head = Some(idx);
4987 }
4988 }
4989 // Link this alternative's head into the or-chain (c:1324).
4990 if let Some(h) = chain_head {
4991 if let Some(p) = prev_alt_head {
4992 arena.nodes[p].or = Some(h);
4993 }
4994 prev_alt_head = Some(h);
4995 if arena.head.is_none() {
4996 arena.head = Some(h);
4997 }
4998 }
4999 }
5000 qs.quals = arena;
5001 }
5002
5003 qs
5004}
5005
5006/// Parse a numeric uid/gid or a delimited user/group name from a `u`/`g`
5007/// qualifier char stream. Port of the name-resolution arms of the `u`/`g`
5008/// qualifier cases at `Src/glob.c:1468-1535`:
5009/// `if (idigit(*s)) data = qgetnum(&s);` else `get_strarg`-delimited name
5010/// resolved through `getpwnam(3)` / `getgrnam(3)`. On an unknown name it
5011/// emits the C diagnostic (`zerr` sets errflag) and returns 0, matching
5012/// C's `data = 0;` arm. `is_group` selects getgrnam over getpwnam.
5013fn parse_uid_gid(chars: &mut std::iter::Peekable<std::str::Chars>, is_group: bool) -> u32 {
5014 // c:1468/1511 — `if (idigit(*s)) data = qgetnum(&s);`
5015 if chars.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
5016 let mut num = String::new();
5017 while let Some(&c) = chars.peek() {
5018 if c.is_ascii_digit() {
5019 num.push(c);
5020 chars.next();
5021 } else {
5022 break;
5023 }
5024 }
5025 return num.parse().unwrap_or(0);
5026 }
5027 // c:1471-1505 — `tt = get_strarg(s, &arglen);` the char right after the
5028 // qualifier is the delimiter; the name runs to the next delimiter.
5029 let delim = match chars.next() {
5030 Some(d) => d,
5031 None => {
5032 // c:1481/1524 — `zerr("missing delimiter for 'u'/'g' glob qualifier")`.
5033 zerr(if is_group {
5034 "missing delimiter for 'g' glob qualifier"
5035 } else {
5036 "missing delimiter for 'u' glob qualifier"
5037 });
5038 return 0;
5039 }
5040 };
5041 let mut name = String::new();
5042 for c in chars.by_ref() {
5043 if c == delim {
5044 break;
5045 }
5046 name.push(c);
5047 }
5048 // c:1488/1530 — resolve via getpwnam(3) / getgrnam(3).
5049 let cstr = match std::ffi::CString::new(name.as_bytes()) {
5050 Ok(c) => c,
5051 Err(_) => return 0,
5052 };
5053 unsafe {
5054 if is_group {
5055 let gr = libc::getgrnam(cstr.as_ptr()); // c:1530
5056 if !gr.is_null() {
5057 return (*gr).gr_gid; // c:1531
5058 }
5059 zerr("unknown group"); // c:1533
5060 } else {
5061 let pw = libc::getpwnam(cstr.as_ptr()); // c:1488
5062 if !pw.is_null() {
5063 return (*pw).pw_uid; // c:1489
5064 }
5065 // c:1491 — `zerr("unknown username '%s'", s + arglen);`
5066 zerr(&format!("unknown username '{}'", name));
5067 }
5068 }
5069 // c:1493/1535 — `data = 0;` after the unknown-name diagnostic.
5070 0
5071}
5072
5073/// Parse the unit/op/value tail of an `(L...)` size qualifier.
5074/// **RUST-ONLY** — C parses inline in parsepat; the size-unit
5075/// constants are `TT_BYTES`/`TT_KILOBYTES`/… at glob.c:128-133.
5076fn parse_size_spec(
5077 // RUST-ONLY
5078 chars: &mut std::iter::Peekable<std::str::Chars>,
5079) -> (i32, char, u64) {
5080 let unit: i32 = match chars.peek() {
5081 Some('p') | Some('P') => {
5082 chars.next();
5083 TT_POSIX_BLOCKS
5084 }
5085 Some('k') | Some('K') => {
5086 chars.next();
5087 TT_KILOBYTES
5088 }
5089 Some('m') | Some('M') => {
5090 chars.next();
5091 TT_MEGABYTES
5092 }
5093 Some('g') | Some('G') => {
5094 chars.next();
5095 TT_GIGABYTES
5096 }
5097 Some('t') | Some('T') => {
5098 chars.next();
5099 TT_TERABYTES
5100 }
5101 _ => TT_BYTES,
5102 };
5103 let (op, val) = parse_range_spec(chars);
5104 (unit, op, val)
5105}
5106
5107/// Parse the unit/op/value tail of an `(a/m/c...)` time qualifier.
5108/// **RUST-ONLY** — C parses inline in parsepat; time-unit constants
5109/// are `TT_SECONDS`/`TT_MINS`/… at glob.c:121-126.
5110fn schedgetfn(
5111 // RUST-ONLY (clashes with sched.c:341 name, unrelated fn)
5112 chars: &mut std::iter::Peekable<std::str::Chars>,
5113) -> (i32, char, u64) {
5114 let unit: i32 = match chars.peek() {
5115 Some('s') => {
5116 chars.next();
5117 TT_SECONDS
5118 }
5119 Some('m') => {
5120 chars.next();
5121 TT_MINS
5122 }
5123 Some('h') => {
5124 chars.next();
5125 TT_HOURS
5126 }
5127 Some('d') => {
5128 chars.next();
5129 TT_DAYS
5130 }
5131 Some('w') => {
5132 chars.next();
5133 TT_WEEKS
5134 }
5135 Some('M') => {
5136 chars.next();
5137 TT_MONTHS
5138 }
5139 _ => TT_DAYS,
5140 };
5141 let (op, val) = parse_range_spec(chars);
5142 (unit, op, val)
5143}
5144
5145/// Parse `[+-]?NUMBER` operator+value tail. Mirrors C qgetnum
5146/// (glob.c:827) which returns the int value; here we return the
5147/// operator char along with the value since callers need both.
5148fn parse_range_spec(chars: &mut std::iter::Peekable<std::str::Chars>) -> (char, u64) {
5149 // RUST-ONLY
5150 // C's qgetnum at glob.c:827 returns the operator char inline.
5151 // `+N` = greater, `-N` = less, bare digits = equal.
5152 let op: char = match chars.peek() {
5153 Some('+') => {
5154 chars.next();
5155 '>'
5156 }
5157 Some('-') => {
5158 chars.next();
5159 '<'
5160 }
5161 _ => '=',
5162 };
5163 let mut num = String::new();
5164 while let Some(&c) = chars.peek() {
5165 if c.is_ascii_digit() {
5166 num.push(c);
5167 chars.next();
5168 } else {
5169 break;
5170 }
5171 }
5172 // c:Src/glob.c:826-834 — `qgetnum` errors with "number expected"
5173 // when not followed by `idigit(**s)`. Previously the Rust parser
5174 // silently treated the missing number as 0 and let the outer
5175 // qualifier loop continue consuming bytes as fresh qualifier
5176 // letters — `*(Lr)` parsed as `L0 + r` (readable, size=0) and
5177 // dropped to the no-matches path. Setting errflag here propagates
5178 // the canonical error message and gates the "no matches found"
5179 // emission in zglob (c:1843-1854 `if (errflag) ... return`).
5180 if num.is_empty() {
5181 crate::ported::utils::zerr("number expected"); // c:832
5182 crate::ported::utils::errflag.fetch_or(
5183 crate::ported::zsh_h::ERRFLAG_ERROR,
5184 std::sync::atomic::Ordering::SeqCst,
5185 );
5186 return (op, 0); // c:833
5187 }
5188 let val = num.parse().unwrap_or(0);
5189 (op, val)
5190}
5191
5192/// Parse `[FIRST,LAST]` subscript range. **RUST-ONLY**.
5193fn parse_subscript(
5194 // RUST-ONLY
5195 chars: &mut std::iter::Peekable<std::str::Chars>,
5196) -> (Option<i32>, Option<i32>) {
5197 let mut first_str = String::new();
5198 let mut last_str = String::new();
5199 let mut in_last = false;
5200
5201 while let Some(&c) = chars.peek() {
5202 chars.next();
5203 if c == ']' {
5204 break;
5205 } else if c == ',' {
5206 in_last = true;
5207 } else if in_last {
5208 last_str.push(c);
5209 } else {
5210 first_str.push(c);
5211 }
5212 }
5213
5214 let first = first_str.parse().ok();
5215 let last = if in_last {
5216 last_str.parse().ok()
5217 } else {
5218 first
5219 };
5220 (first, last)
5221}
5222
5223/// Port of `static void insert(char *s, int checked)` from `Src/glob.c:346`.
5224/// Records one matched path into the glob result list: runs the qualifier
5225/// walk (`check_qualifiers` = the c:381-419 `struct qual` walk over the
5226/// arena) and, if accepted, builds the `gmatch` entry from the file's
5227/// stat and appends it (c:421-490) — honoring any `$reply`/`$REPLY`
5228/// replacement (INSERTS, c:428) set by an `(e:…:)` eval qualifier.
5229/// Called by the scanner per matched name, mirroring glob.c:576/668.
5230pub fn insert(state: &mut globdata, s: &Path, checked: i32) {
5231 use std::os::unix::fs::MetadataExt;
5232 let _ = checked; // c:347 already-stat'd hint; check_qualifiers re-stats.
5233 // c:381-419 — qualifier walk; reject the file if it fails.
5234 if !check_qualifiers(state, s) {
5235 return;
5236 }
5237 // c:421-490 — build + append the match entry. Symlink targets get
5238 // their own stat for the GS_LINKED (follow-link) sort variants.
5239 let Ok(meta) = fs::symlink_metadata(s) else {
5240 return;
5241 };
5242 let name = s
5243 .file_name()
5244 .map(|n| n.to_string_lossy().to_string())
5245 .unwrap_or_default();
5246 let (tsize, tatime, tmtime, tctime, tlinks, tansec, tmnsec, tcnsec) =
5247 if meta.file_type().is_symlink() {
5248 if let Ok(tm) = fs::metadata(s) {
5249 (
5250 tm.size(),
5251 tm.atime(),
5252 tm.mtime(),
5253 tm.ctime(),
5254 tm.nlink(),
5255 tm.atime_nsec(),
5256 tm.mtime_nsec(),
5257 tm.ctime_nsec(),
5258 )
5259 } else {
5260 (
5261 meta.size(),
5262 meta.atime(),
5263 meta.mtime(),
5264 meta.ctime(),
5265 meta.nlink(),
5266 meta.atime_nsec(),
5267 meta.mtime_nsec(),
5268 meta.ctime_nsec(),
5269 )
5270 }
5271 } else {
5272 (
5273 meta.size(),
5274 meta.atime(),
5275 meta.mtime(),
5276 meta.ctime(),
5277 meta.nlink(),
5278 meta.atime_nsec(),
5279 meta.mtime_nsec(),
5280 meta.ctime_nsec(),
5281 )
5282 };
5283 // c:428 — `while (!inserts || (news = *inserts++))`: an (e:…:) eval
5284 // may have set $reply/$REPLY (INSERTS), replacing this match with
5285 // zero-or-more names; otherwise emit the original name once. The
5286 // synthetic path makes glob_emit_path yield the replacement string;
5287 // the stat fields stay those of the matched file (C keeps
5288 // matchptr->size etc. from buf).
5289 let emit: Vec<(String, PathBuf)> = match INSERTS.lock().unwrap().take() {
5290 Some(list) => list
5291 .into_iter()
5292 .map(|n| (n.clone(), PathBuf::from(&n)))
5293 .collect(),
5294 None => vec![(name.clone(), s.to_path_buf())],
5295 };
5296 for (nm, pth) in emit {
5297 state.matches.push(gmatch {
5298 name: nm,
5299 path: pth,
5300 size: meta.size(),
5301 atime: meta.atime(),
5302 mtime: meta.mtime(),
5303 ctime: meta.ctime(),
5304 links: meta.nlink(),
5305 mode: meta.mode(),
5306 uid: meta.uid(),
5307 gid: meta.gid(),
5308 dev: meta.dev(),
5309 ino: meta.ino(),
5310 target_size: tsize,
5311 target_atime: tatime,
5312 target_mtime: tmtime,
5313 target_ctime: tctime,
5314 target_links: tlinks,
5315 ansec: meta.atime_nsec(),
5316 mnsec: meta.mtime_nsec(),
5317 cnsec: meta.ctime_nsec(),
5318 target_ansec: tansec,
5319 target_mnsec: tmnsec,
5320 target_cnsec: tcnsec,
5321 sort_strings: Vec::new(),
5322 });
5323 state.matchct += 1; // c:479 matchptr++
5324 }
5325}
5326
5327/// Drive the OR-of-AND qualifier filter against `path`. **RUST-ONLY**
5328/// — C glob.c does qualifier eval inline inside `insert()` (c:381+).
5329fn check_qualifiers(state: &globdata, path: &Path) -> bool {
5330 use std::os::unix::fs::MetadataExt;
5331 use std::sync::atomic::Ordering;
5332 // c:353 — `inserts = NULL;` at insert() entry: clear any reply/REPLY
5333 // replacement left by a previous file's (e:…:) eval before this walk.
5334 *INSERTS.lock().unwrap() = None;
5335 let qs = match &state.qualifiers {
5336 Some(q) => q,
5337 None => return true,
5338 };
5339 // No test qualifiers (only flags/sort/words) → no filtering.
5340 let arena = &qs.quals;
5341 if arena.head.is_none() {
5342 return true;
5343 }
5344
5345 let meta = match if qs.follow_links {
5346 // c:399-402 — `if (!S_ISLNK(buf.st_mode) || statfullpath(s,&buf2,0))
5347 // memcpy(&buf2, &buf, sizeof(buf));`
5348 // Follow the link, but fall back to the lstat buffer when the
5349 // target stat fails (a broken symlink) so `*(-@)` / `*(-^/)`
5350 // still see the dangling link rather than dropping it.
5351 fs::metadata(path).or_else(|_| fs::symlink_metadata(path))
5352 } else {
5353 fs::symlink_metadata(path)
5354 } {
5355 Ok(m) => m,
5356 Err(_) => return false,
5357 };
5358 // Bridge meta → libc::stat for the leaf test fns (no extra syscall).
5359 let mut st: libc::stat = unsafe { std::mem::zeroed() };
5360 st.st_mode = meta.mode() as _;
5361 st.st_uid = meta.uid() as _;
5362 st.st_gid = meta.gid() as _;
5363 st.st_dev = meta.dev() as _;
5364 st.st_nlink = meta.nlink() as _;
5365 st.st_size = meta.size() as _;
5366 st.st_atime = meta.atime() as _;
5367 st.st_mtime = meta.mtime() as _;
5368 st.st_ctime = meta.ctime() as _;
5369 // qualsheval / qualnonemptydir need the name (REPLY / opendir).
5370 let name = glob_emit_path(path);
5371
5372 // c:387-419 — insert()'s qual-walk over the `struct qual` arena: AND
5373 // via `next`, OR (alternatives) via `or`, per-node `sense`. `qo` is
5374 // the current alternative head; a rejected node advances to `qo->or`
5375 // (next alternative) or, if none, rejects the file. Falling off the
5376 // `next` chain (or hitting a func-less terminator) accepts.
5377 let mut qo = arena.head;
5378 let mut qn = qo;
5379 loop {
5380 let Some(qn_i) = qn else { return true }; // c:419 — chain matched
5381 let (func, data, sense, range, amc, units, sdata, next) = {
5382 let n = &arena.nodes[qn_i];
5383 (
5384 n.func,
5385 n.data,
5386 n.sense,
5387 n.range,
5388 n.amc,
5389 n.units,
5390 n.sdata.clone(),
5391 n.next,
5392 )
5393 };
5394 let func = match func {
5395 Some(f) => f,
5396 None => return true, // c:392 — `qn && qn->func` terminator
5397 };
5398 // c:393-395 — set the scratch the range/time/size leaf fns read.
5399 g_range.store(range, Ordering::SeqCst);
5400 g_amc.store(amc, Ordering::SeqCst);
5401 g_units.store(units, Ordering::SeqCst);
5402 let r = func(&name, &st, data, sdata.as_deref().unwrap_or(""));
5403 // c:407-409 — reject if `(!(func()) ^ sense) & 1`.
5404 let reject = ((if r == 0 { 1 } else { 0 }) ^ (sense & 1)) & 1 == 1;
5405 if reject {
5406 // c:411-415 — try the next alternative, else reject the file.
5407 match arena.nodes[qo.unwrap()].or {
5408 Some(or_i) => {
5409 qo = Some(or_i);
5410 qn = Some(or_i);
5411 }
5412 None => return false,
5413 }
5414 } else {
5415 qn = next; // c:417
5416 }
5417 }
5418}
5419
5420/// Sort the per-state matches per the qualifier `o`/`O` keys.
5421/// **RUST-ONLY** — C does this inline at the end of `scanner()`
5422/// (glob.c:1700ish, qsort with `gmatchcmp` comparator).
5423fn sort_matches(state: &mut globdata) {
5424 // RUST-ONLY
5425 // Default sort is GS_NAME ascending (c:204 gf_sortlist
5426 // initial setup). Per-qualifier `o<key>` / `O<key>` overrides.
5427 // c:1855-1857 — when gf_nsorts == 0 (no explicit o/O qualifier),
5428 // glob.c falls back to GS_NAME (or GS_NONE under shortcircuit).
5429 // Mirror that here so a qualifier set carrying ONLY non-sort
5430 // qualifiers (`Lk+0`, etc.) still sorts alphabetically.
5431 // c:1855-1857 is a CONDITIONAL default, not a fixed one:
5432 // if (!gf_nsorts) {
5433 // gf_sortlist[0].tp = gf_sorts = (shortcircuit ? GS_NONE : GS_NAME);
5434 // gf_nsorts = 1;
5435 // }
5436 // A `Y` limit means "give me the first N the scanner finds", so with no
5437 // explicit o/O spec the default becomes GS_NONE and the results stay in
5438 // SCAN order — sorting them would defeat the short-circuit, since the first
5439 // N by name are not the first N found. This always chose GS_NAME, so a
5440 // limited glob returned the alphabetically-first N instead of the
5441 // scan-order-first N, and even `*(Y99)` (a limit larger than the match
5442 // count, which discards nothing) came back sorted where zsh leaves it in
5443 // readdir order.
5444 //
5445 // `Y0` is the counter-case and must still sort: c:1583 parses the argument
5446 // over `shortcircuit`, so 0 lands there legitimately and c:518's
5447 // `if (shortcircuit && …)` reads it as "no limit" — hence the `> 0` test
5448 // rather than `.is_some()`.
5449 let limited = state
5450 .qualifiers
5451 .as_ref()
5452 .and_then(|q| q.short_circuit)
5453 .is_some_and(|n| n > 0); // c:1856 `shortcircuit ?`
5454 let default_spec = if limited { GS_NONE } else { GS_NAME }; // c:1856
5455 let mut specs: Vec<i32> = state
5456 .qualifiers
5457 .as_ref()
5458 .map(|q| {
5459 if q.sorts.is_empty() {
5460 vec![default_spec] // c:1856
5461 } else {
5462 q.sorts.clone()
5463 }
5464 })
5465 .unwrap_or_else(|| vec![GS_NAME]);
5466
5467 // GS_NONE marker — caller wants no sort at all.
5468 if specs.iter().any(|&tp| (tp & GS_NONE) != 0) {
5469 return;
5470 }
5471
5472 // c:936 gmatchcmp returns 0 when every sort key ties. In C the final
5473 // order of equal-key matches is then whatever the libc `qsort` does with
5474 // all-equal elements (glob.c:1976) — NON-stable on macOS/BSD (arbitrary,
5475 // input-dependent: `*(oL)` on three 0-byte files a/b/d prints `d a b`),
5476 // and on glibc it depends on readdir order. So zsh has NO portable,
5477 // defined tie-break here. zshrs is a cross-architecture shell and must be
5478 // DETERMINISTIC, so we impose an explicit GS_NAME final tie-breaker:
5479 // equal-key matches order by name ascending, identical on every platform.
5480 // This is an intentional determinism guarantee, NOT a bug-for-bug match of
5481 // zsh's qsort-defined order (which can't be reproduced portably). Skip
5482 // when the last key is already GS_NAME (the no-qualifier default, or an
5483 // explicit trailing `on`) so we don't double-compare.
5484 if specs.last().map_or(true, |&last| (last & !GS_DESC) != GS_NAME) {
5485 specs.push(GS_NAME); // deterministic name-ascending tie-break
5486 }
5487
5488 // c:1258/1575 — gf_numsort starts at the global NUMERIC_GLOB_SORT
5489 // option, then a per-glob `(n)`/`(^n)` qualifier OVERRIDES it
5490 // absolutely (so `(^n)` forces lexical even when the option is on).
5491 let numeric = state
5492 .qualifiers
5493 .as_ref()
5494 .and_then(|q| q.numsort)
5495 .unwrap_or_else(|| glob_isset(NUMERICGLOBSORT));
5496 state
5497 .matches
5498 .sort_by(|a, b| gmatchcmp(a, b, &specs, numeric));
5499}
5500
5501/// Apply `[FIRST,LAST]` qualifier subscript on the match list.
5502/// **RUST-ONLY** — C uses `gd_pre_first` / `gd_first` index tracking
5503/// during scanner emit; here we slice after the full walk.
5504fn apply_selection(state: &mut globdata) {
5505 // RUST-ONLY
5506 let (first, last, short_circuit) = match &state.qualifiers {
5507 Some(q) => (q.first, q.last, q.short_circuit),
5508 None => return,
5509 };
5510
5511 let len = state.matches.len() as i32;
5512 if len == 0 {
5513 return;
5514 }
5515
5516 let start = match first {
5517 Some(f) if f < 0 => (len + f).max(0) as usize,
5518 Some(f) => (f - 1).max(0) as usize,
5519 None => 0,
5520 };
5521
5522 let end = match last {
5523 Some(l) if l < 0 => (len + l + 1).max(0) as usize,
5524 Some(l) => l.min(len) as usize,
5525 None => len as usize,
5526 };
5527
5528 if start < end && start < state.matches.len() {
5529 state.matches = state.matches[start..end.min(state.matches.len())].to_vec();
5530 } else {
5531 state.matches.clear();
5532 }
5533
5534 // c:Src/glob.c:1579-1595 `case 'Y'` — apply the short-circuit
5535 // limit after the [first,last] slice (so `(Y3[2,4])` would yield
5536 // up to 3 from the [2,4] subscript range). C truncates DURING
5537 // the scanner walk via `if (shortcircuit == matchct) break;` —
5538 // here we slice post-walk which produces the same set on the
5539 // common short-glob case where the scanner doesn't recurse
5540 // unboundedly. Bug #41 in docs/BUGS.md.
5541 // NOTE: the `Y` short-circuit is deliberately NOT applied here. It bounds
5542 // the SCAN (c:Src/glob.c:518), so it must run BEFORE the sort — see the
5543 // truncation at the sort_matches call site. Only the `[first,last]`
5544 // subscript belongs in this post-sort selection.
5545 let _ = short_circuit;
5546}
5547
5548// `haswilds()` is defined in `Src/pattern.c:4306`, not glob.c — the
5549// canonical Rust port lives at `crate::ported::pattern::haswilds`.
5550// This file previously carried a divergent re-implementation tracking
5551// bracket/escape state instead of the token-aware `zpc_disables[]` /
5552// SHGLOB / KSHGLOB / EXTENDEDGLOB checks the C source actually does.
5553// Drift deleted; glob.rs callers route through the canonical port.
5554
5555#[cfg(test)]
5556mod gs_tt_tests {
5557 use super::*;
5558
5559 #[test]
5560 fn gs_size_offset_matches_c() {
5561 let _g = crate::test_util::global_state_lock();
5562 // c:83 — GS_SIZE = GS_SHIFT_BASE = 8.
5563 assert_eq!(GS_SIZE, 8);
5564 // c:84 — GS_ATIME = GS_SHIFT_BASE << 1 = 16.
5565 assert_eq!(GS_ATIME, 16);
5566 // c:87 — GS_LINKS = GS_SHIFT_BASE << 4 = 128.
5567 assert_eq!(GS_LINKS, 128);
5568 }
5569
5570 #[test]
5571 fn gs_normal_covers_all_size_keys() {
5572 let _g = crate::test_util::global_state_lock();
5573 // c:99 — GS_NORMAL = SIZE | ATIME | MTIME | CTIME | LINKS.
5574 assert_ne!(GS_NORMAL & GS_SIZE, 0);
5575 assert_ne!(GS_NORMAL & GS_ATIME, 0);
5576 assert_ne!(GS_NORMAL & GS_MTIME, 0);
5577 assert_ne!(GS_NORMAL & GS_CTIME, 0);
5578 assert_ne!(GS_NORMAL & GS_LINKS, 0);
5579 }
5580
5581 #[test]
5582 fn tt_namespaces_share_indices() {
5583 let _g = crate::test_util::global_state_lock();
5584 // c:121-126 vs c:128-133 — TT_DAYS == TT_BYTES == 0, etc.
5585 assert_eq!(TT_DAYS, TT_BYTES);
5586 assert_eq!(TT_HOURS, TT_POSIX_BLOCKS);
5587 assert_eq!(TT_MINS, TT_KILOBYTES);
5588 assert_eq!(TT_WEEKS, TT_MEGABYTES);
5589 assert_eq!(TT_MONTHS, TT_GIGABYTES);
5590 assert_eq!(TT_SECONDS, TT_TERABYTES);
5591 }
5592
5593 #[test]
5594 fn max_sorts_is_12() {
5595 let _g = crate::test_util::global_state_lock();
5596 assert_eq!(MAX_SORTS, 12);
5597 }
5598}
5599
5600fn expand_range(
5601 prefix: &str,
5602 content: &str,
5603 dotdot_pos: usize,
5604 suffix: &str,
5605) -> Option<Vec<String>> {
5606 // c:Src/glob.c::xpandbraces parses brace-range endpoints via
5607 // zstrtol, which is TOKEN-aware and treats Dash TOKEN (\u{9b})
5608 // as ASCII `-`. Rust's i64::parse only accepts ASCII, so
5609 // untokenize the content here before splitting/parsing — matches
5610 // C semantics without weakening the strict-TOKEN gates above.
5611 let owned: String;
5612 let content: &str = if content.chars().any(|c| {
5613 let cu = c as u32;
5614 (0x84..=0xa1).contains(&cu) && c != '\u{8f}' && c != '\u{90}' && c != '\u{9a}'
5615 }) {
5616 owned = crate::lex::untokenize(content);
5617 &owned
5618 } else {
5619 content
5620 };
5621 // `dotdot_pos` is a CHAR index (xpandbraces counts it over a char vector,
5622 // `i - start - 1`), but the slicing below is byte-based. For ASCII content
5623 // they coincide; for content holding multibyte/metafied chars (e.g.
5624 // `{$'\x80'..$'\x81'}`, where the bytes are Meta-escaped) a raw byte slice
5625 // at the char index lands mid-char and panics. Convert to a byte offset.
5626 let dotdot_byte = content
5627 .char_indices()
5628 .nth(dotdot_pos)
5629 .map(|(b, _)| b)
5630 .unwrap_or(content.len());
5631 let left = &content[..dotdot_byte];
5632 let right_start = dotdot_byte + 2; // ".." is two ASCII bytes
5633
5634 // Check for second `..` for `{N..M..S}` step form. Step may be
5635 // signed: negative-step REVERSES the natural direction sequence
5636 // per zsh's brace expansion (Src/glob.c::xpandbraces recursive
5637 // iteration with sign tracking). Examples:
5638 // {1..32..3} → 1,4,7,…,31 (natural ascending)
5639 // {1..32..-3} → 31,28,…,1 (same set, reversed)
5640 // {32..1..3} → 32,29,…,2 (natural descending)
5641 // {32..1..-3} → 2,5,…,32 (same set, reversed)
5642 //
5643 // c:Src/glob.c:2365-2369 — `if (dotdot == 2 && *p == '.' &&`
5644 // ` p[1] == '.') {`
5645 // ` rincr = zstrtol(p+2, &p, 10);`
5646 // ` wid3 = p - dots2 - 2;`
5647 // ` if (p != str2 || !rincr)`
5648 // ` err++;`
5649 // The `!rincr` test (c:2368) rejects a zero step: when err > 0,
5650 // the C code falls past the c:2374 `if (!err)` block, so the
5651 // brace expansion is abandoned and the literal pattern is
5652 // preserved. Parity bug #15: previously the Rust port silently
5653 // clamped step to 1 via `.max(1)`, producing `1 2 3 4 5` for
5654 // `{1..5..0}` instead of zsh's literal `1..5..0`.
5655 let (right, incr_abs, incr_sign_negative, step_text) =
5656 if let Some(pos) = content[right_start..].find("..") {
5657 let r = &content[right_start..right_start + pos];
5658 let s_text = &content[right_start + pos + 2..];
5659 let raw: i64 = s_text.parse().unwrap_or(1);
5660 if raw == 0 {
5661 // c:2368 — `!rincr` → err++ → no expansion. Return
5662 // None so xpandbraces falls back to literal.
5663 return None;
5664 }
5665 (r, raw.unsigned_abs(), raw < 0, s_text)
5666 } else {
5667 (&content[right_start..], 1u64, false, "")
5668 };
5669
5670 // Try numeric range
5671 if let (Ok(start), Ok(end)) = (left.parse::<i64>(), right.parse::<i64>()) {
5672 let mut results = Vec::new();
5673
5674 // Iterate from `start` toward `end` with abs(step). Sign of
5675 // start/end relative to each other determines natural
5676 // direction; step is always |step|.
5677 let step = incr_abs.max(1) as i64;
5678 let mut vals: Vec<i64> = Vec::new();
5679 if start <= end {
5680 let mut v = start;
5681 while v <= end {
5682 vals.push(v);
5683 v += step;
5684 }
5685 } else {
5686 let mut v = start;
5687 while v >= end {
5688 vals.push(v);
5689 v -= step;
5690 }
5691 }
5692 if incr_sign_negative {
5693 vals.reverse();
5694 }
5695
5696 // Padding: zsh pads with leading zeros when ANY of the three
5697 // textual fields (left endpoint, right endpoint, step) has a
5698 // leading zero after stripping the optional sign. Width is
5699 // the max textual width across left/right/step. For negative
5700 // values, the sign prefix counts toward width — we emit `-`
5701 // then zero-pad the remaining digits (`-02`, not `0-2`).
5702 // Direct port of Src/lex.c::dobrace_pad logic which detects
5703 // pad mode per the textual form of all three fields.
5704 // zsh's pad-detection: pad fires when any of the three
5705 // textual fields (left / right / step) is a multi-digit
5706 // form with a leading zero (e.g. "01", "00", "003"), with
5707 // ONE exception: when the LEFT endpoint is exactly "0"
5708 // (bare single zero, possibly signed), pad is suppressed
5709 // regardless of right's form. Empirical zsh:
5710 // {0..-5} → 0 -1 -2 -3 -4 -5 (left=0 → no pad)
5711 // {0..03} → 0 1 2 3 (left=0 → no pad)
5712 // {00..-5} → 00 -1 -2 -3 -4 -5 (left=00 → pad w=2)
5713 // {1..03} → 01 02 03 (right=03 → pad w=2)
5714 // {1..-03} → 001 000 -01 -02 -03 (right=-03 → pad w=3)
5715 let lstrip = left.trim_start_matches(['+', '-']);
5716 let rstrip = right.trim_start_matches(['+', '-']);
5717 let sstrip = step_text.trim_start_matches(['+', '-']);
5718 let is_padded_field = |stripped: &str| stripped.len() >= 2 && stripped.starts_with('0');
5719 // Suppress pad ONLY when LEFT is exactly the single-char "0"
5720 // (no sign, no extra digits). "-0" or "00" both pad.
5721 let left_is_bare_zero = left == "0";
5722 let pad = !left_is_bare_zero
5723 && (is_padded_field(lstrip)
5724 || is_padded_field(rstrip)
5725 || (!step_text.is_empty() && is_padded_field(sstrip)));
5726 let width = left.len().max(right.len()).max(step_text.len());
5727
5728 for v in vals {
5729 let formatted = if pad {
5730 if v < 0 {
5731 let abs = (-v).to_string();
5732 let inner_w = width.saturating_sub(1);
5733 format!("-{:0>w$}", abs, w = inner_w)
5734 } else {
5735 format!("{:0>w$}", v, w = width)
5736 }
5737 } else {
5738 v.to_string()
5739 };
5740 results.push(format!("{}{}{}", prefix, formatted, suffix));
5741 }
5742 return Some(results);
5743 }
5744
5745 // Try character range
5746 // c:Src/lex.c::dobrace alpha-range path — zsh only handles bare a..z,
5747 // NOT a..z..N (it emits literal `{a..z..3}` because the step form is
5748 // numeric-only). bash DOES support an alpha step (`{a..e..2}` → a c e),
5749 // so honor `step_text` only under --bash; --zsh keeps refusing it so the
5750 // literal survives (verified: real zsh emits `{a..e..2}` unchanged).
5751 // bash ignores the step's SIGN for alpha (`{a..e..-2}` == `{a..e..2}`),
5752 // so use abs(step) and take direction purely from start vs end.
5753 let alpha_step_ok = step_text.is_empty() || crate::dash_mode::bash_mode();
5754 if left.len() == 1 && right.len() == 1 && alpha_step_ok {
5755 let start = left.chars().next()?;
5756 let end = right.chars().next()?;
5757 let step = incr_abs.max(1) as u32;
5758 let (lo, hi, reverse) = if start <= end {
5759 (start, end, false)
5760 } else {
5761 (end, start, true)
5762 };
5763
5764 let mut results = Vec::new();
5765 let mut chars: Vec<char> = Vec::new();
5766 let mut v = lo as u32;
5767 while v <= hi as u32 {
5768 if let Some(c) = char::from_u32(v) {
5769 chars.push(c);
5770 }
5771 v += step;
5772 }
5773 if reverse {
5774 chars.reverse();
5775 }
5776
5777 for c in chars {
5778 results.push(format!("{}{}{}", prefix, c, suffix));
5779 }
5780 return Some(results);
5781 }
5782
5783 None
5784}
5785
5786fn expand_comma(
5787 prefix: &str,
5788 content: &str,
5789 positions: &[usize],
5790 suffix: &str,
5791) -> Option<Vec<String>> {
5792 // positions are CHAR indices into `content` (per the xpandbraces
5793 // walker above that builds them from `chars: Vec<char>`). Slice
5794 // by char-index, not byte-index — `&content[last..pos]` would
5795 // panic on multi-byte token chars like Comma TOKEN (`\u{9a}`,
5796 // 2 UTF-8 bytes) and Inbrace (`\u{8f}`, 2 bytes).
5797 let chars: Vec<char> = content.chars().collect();
5798 let mut results = Vec::new();
5799 let mut last: usize = 0;
5800 for &pos in positions {
5801 let part: String = chars[last..pos].iter().collect();
5802 results.push(format!("{}{}{}", prefix, part, suffix));
5803 last = pos + 1;
5804 }
5805 let tail: String = chars[last..].iter().collect();
5806 results.push(format!("{}{}{}", prefix, tail, suffix));
5807 Some(results)
5808}
5809
5810fn expand_ccl(prefix: &str, content: &str, suffix: &str) -> Option<Vec<String>> {
5811 // c:Src/glob.c:expand_ccl — char-class range expansion for the
5812 // `setopt braceccl` brace shape `{m-o}` → m,n,o. Accept both
5813 // ASCII `-` and Dash TOKEN (\u{9b}) as the range separator —
5814 // the bridge passthru path delivers `{m-o}` with Dash TOKEN
5815 // since the lexer tokenizes `-` to Dash inside word context.
5816 let mut chars_set = HashSet::new();
5817 let chars: Vec<char> = content.chars().collect();
5818 let mut i = 0;
5819
5820 while i < chars.len() {
5821 let is_range = i + 2 < chars.len() && (chars[i + 1] == '-' || chars[i + 1] == '\u{9b}');
5822 if is_range {
5823 let start = chars[i];
5824 let end = chars[i + 2];
5825 for c in start..=end {
5826 chars_set.insert(c);
5827 }
5828 i += 3;
5829 } else {
5830 chars_set.insert(chars[i]);
5831 i += 1;
5832 }
5833 }
5834
5835 let mut results: Vec<String> = chars_set
5836 .iter()
5837 .map(|c| format!("{}{}{}", prefix, c, suffix))
5838 .collect();
5839 results.sort();
5840 Some(results)
5841}
5842
5843// ============================================================================
5844// Convenience functions
5845// ============================================================================
5846
5847/// Split a pattern at its trailing zsh-style qualifier suffix. Returns
5848/// `(pattern_without_qualifier, qualifier_inner)` — the inner is the bytes
5849/// between the matching parens, without the surrounding `()` and without
5850/// any leading `#q`. Returns `(pattern, None)` when there is no qualifier
5851/// suffix. Useful for callers that want to use the pattern half with the
5852/// runtime [`matchpat`] (which has no qualifier semantics) while
5853/// reporting or applying the qualifier separately.
5854/// Split a glob pattern into (path-pattern, qualifier-string).
5855/// Port of the qualifier-detection step in `parsepat()`
5856/// (Src/glob.c:791).
5857pub fn split_qualifier(pattern: &str) -> (&str, Option<&str>) {
5858 if !pattern.ends_with(')') {
5859 return (pattern, None);
5860 }
5861 let bytes = pattern.as_bytes();
5862 let mut depth = 0;
5863 for i in (0..bytes.len()).rev() {
5864 match bytes[i] {
5865 b')' => depth += 1,
5866 b'(' => {
5867 depth -= 1;
5868 if depth == 0 {
5869 let inner = &pattern[i + 1..pattern.len() - 1];
5870 let inner = inner.strip_prefix("#q").unwrap_or(inner);
5871 return (&pattern[..i], Some(inner));
5872 }
5873 }
5874 _ => {}
5875 }
5876 }
5877 (pattern, None)
5878}
5879
5880/// Strip redundant `.` / `CurDir` segments from relative match paths for
5881/// output. Rust's `read_dir(".")` yields `entry.path()` like `./foo` while
5882/// `read_dir("foo")` yields `foo/bar` — zsh prints the latter shape for both.
5883fn glob_emit_path(path: &Path) -> String {
5884 match path.components().next() {
5885 Some(Component::Prefix(_) | Component::RootDir) => path.to_string_lossy().to_string(),
5886 None => ".".to_string(),
5887 _ => {
5888 let mut out = PathBuf::new();
5889 for c in path.components() {
5890 match c {
5891 Component::CurDir => {}
5892 Component::ParentDir => out.push(".."),
5893 Component::Normal(s) => out.push(s),
5894 Component::Prefix(_) | Component::RootDir => {}
5895 }
5896 }
5897 if out.as_os_str().is_empty() {
5898 ".".to_string()
5899 } else {
5900 out.to_string_lossy().to_string()
5901 }
5902 }
5903 }
5904}
5905
5906/// Check if path is a symlink
5907/// Check whether a glob match is a symlink.
5908/// Port of the `S_ISLNK(lstat.st_mode)` test in Src/glob.c.
5909pub fn is_symlink(path: &str) -> bool {
5910 fs::symlink_metadata(path)
5911 .map(|m| m.file_type().is_symlink())
5912 .unwrap_or(false)
5913}
5914
5915// ===========================================================
5916// Methods moved verbatim from src/ported/vm_helper because their
5917// C counterpart's source file maps 1:1 to this Rust module.
5918// Phase: glob
5919// ===========================================================
5920
5921// BEGIN moved-from-exec-rs
5922// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
5923
5924// END moved-from-exec-rs
5925
5926// ===========================================================
5927// Methods moved verbatim from src/ported/vm_helper because their
5928// C counterpart's source file maps 1:1 to this Rust module.
5929// Phase: drift
5930// ===========================================================
5931
5932// BEGIN moved-from-exec-rs
5933// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
5934
5935// END moved-from-exec-rs
5936
5937/// !!! RUST-ONLY adapter — NO DIRECT C COUNTERPART !!!
5938///
5939/// Thin "pattern string in, match list out" entry over the glob engine
5940/// for call sites holding a single pattern that want its matches as a
5941/// `Vec<String>`, rather than going through C's in-place
5942/// `zglob(LinkList, LinkNode, int)` list mutation. Snapshots the
5943/// glob-relevant options into TLS (`enter_glob_scope` — required because
5944/// zshrs runs glob on multiple threads; see that fn's doc) and drives
5945/// `globdata_glob`, the RUST-ONLY read_dir-based adaptation of C's
5946/// `scanner()` (Src/glob.c:500 — C descends with `lchdir`, which is
5947/// process-global and unsafe under zshrs's threading, so the port walks
5948/// absolute paths instead). ALL real glob semantics — brace expansion,
5949/// `(a|b)` alternation, `^`/`~` exclusion, qualifiers, `**`, sorting —
5950/// live in `globdata_glob`/`scanner`/`patcompile`, not here. (The 130
5951/// lines of ad-hoc `^`/`~`/alternation read_dir+matchpat code that used
5952/// to live here were verified redundant with `globdata_glob` and
5953/// removed.) The faithful C entry is `zglob` (c:1214).
5954pub fn glob_path(pattern: &str) -> Vec<String> {
5955 let _glob_scope = enter_glob_scope();
5956 let mut state = globdata::new();
5957 globdata_glob(&mut state, pattern)
5958}
5959
5960// The qualifier-comparison direction static is `g_range` (Src/glob.c, ported at
5961// glob.rs ~2799). The duplicate `G_RANGE` that used to live here was a Rust-only
5962// erroneous second copy — never written by the qual-eval, so qualnlink (its only
5963// reader) was stuck on `==`. Removed; qualnlink now reads `g_range`.
5964
5965// `mode_to_octal` lives at `crate::ported::utils::mode_to_octal`
5966// (port of `Src/utils.c:7634` — 12 bit-by-bit POSIX permission
5967// mappings). Local alias kept for the call sites at c:1610/1618
5968// which used the masked identity before the canonical port landed.
5969fn mode_to_octal(mode: u32) -> u32 {
5970 crate::ported::utils::mode_to_octal(mode) as u32
5971}
5972
5973#[cfg(test)]
5974mod tests {
5975 use super::*;
5976 use std::fs::{self, File};
5977 use tempfile::TempDir;
5978
5979 use crate::ported::options::{opt_state_set, opt_state_unset};
5980 use crate::ported::zsh_h::{redir, ERRFLAG_ERROR, REDIR_WRITE};
5981
5982 /// Convert ASCII brace-expansion source to the lexer-tokenized
5983 /// form `hasbraces` / `xpandbraces` consume per c:Src/glob.c —
5984 /// ASCII `{` → Inbrace (\u{8f}), `}` → Outbrace (\u{90}), `,` →
5985 /// Comma (\u{9a}). Backslash-escaped variants (`\{`, `\}`, `\,`)
5986 /// emit Bnull + literal so the canonical "escape via Bnull"
5987 /// distinction reaches the brace walker. Used by every test in
5988 /// this module that wants to drive the canonical TOKEN-strict
5989 /// path with readable ASCII source.
5990 fn tok(s: &str) -> String {
5991 let mut out = String::with_capacity(s.len());
5992 let mut chars = s.chars().peekable();
5993 while let Some(c) = chars.next() {
5994 match c {
5995 '\\' => match chars.peek() {
5996 Some('{') | Some('}') | Some(',') => {
5997 out.push('\u{9f}');
5998 out.push(chars.next().unwrap());
5999 }
6000 _ => out.push(c),
6001 },
6002 '{' => out.push('\u{8f}'),
6003 '}' => out.push('\u{90}'),
6004 ',' => out.push('\u{9a}'),
6005 _ => out.push(c),
6006 }
6007 }
6008 out
6009 }
6010
6011 fn setup_test_dir() -> TempDir {
6012 let dir = TempDir::new().unwrap();
6013 let base = dir.path();
6014
6015 // Create test files
6016 File::create(base.join("file1.txt")).unwrap();
6017 File::create(base.join("file2.txt")).unwrap();
6018 File::create(base.join("file3.rs")).unwrap();
6019 File::create(base.join(".hidden")).unwrap();
6020
6021 // Create subdirectory
6022 fs::create_dir(base.join("subdir")).unwrap();
6023 File::create(base.join("subdir/nested.txt")).unwrap();
6024
6025 dir
6026 }
6027
6028 #[test]
6029 fn test_haswilds() {
6030 let _g = crate::test_util::global_state_lock();
6031 // c:Src/glob.c:3548 — full glob tokenize (shadows the
6032 // brace-only `fn tok` test helper above on purpose:
6033 // haswilds consumes glob tokens, not just braces).
6034 let tok = |s: &str| {
6035 let mut t = s.to_string();
6036 tokenize(&mut t);
6037 t
6038 };
6039 assert!(haswilds(&tok("*.txt")));
6040 assert!(haswilds(&tok("file?.txt")));
6041 assert!(haswilds(&tok("file[12].txt")));
6042 assert!(!haswilds(&tok("file.txt")));
6043 assert!(!haswilds(&tok("path/to/file.txt")));
6044 }
6045
6046 #[test]
6047 fn test_pattern_match() {
6048 let _g = crate::test_util::global_state_lock();
6049 assert!(matchpat("*.txt", "file.txt", false, true));
6050 assert!(matchpat("file?.txt", "file1.txt", false, true));
6051 assert!(!matchpat("*.txt", "file.rs", false, true));
6052 assert!(matchpat("file[12].txt", "file1.txt", false, true));
6053 assert!(!matchpat("file[12].txt", "file3.txt", false, true));
6054 }
6055
6056 #[test]
6057 fn test_brace_expansion() {
6058 let _g = crate::test_util::global_state_lock();
6059 let result = xpandbraces(&tok("{a,b,c}"), false);
6060 assert_eq!(result, vec!["a", "b", "c"]);
6061
6062 let result = xpandbraces(&tok("file{1,2,3}.txt"), false);
6063 assert_eq!(result, vec!["file1.txt", "file2.txt", "file3.txt"]);
6064
6065 let result = xpandbraces(&tok("{1..5}"), false);
6066 assert_eq!(result, vec!["1", "2", "3", "4", "5"]);
6067
6068 let result = xpandbraces(&tok("{a..e}"), false);
6069 assert_eq!(result, vec!["a", "b", "c", "d", "e"]);
6070 }
6071
6072 #[test]
6073 fn test_glob_simple() {
6074 let _g = crate::test_util::global_state_lock();
6075 let dir = setup_test_dir();
6076 let pattern = format!("{}/*.txt", dir.path().display());
6077
6078 let mut state = globdata::new();
6079 let results = globdata_glob(&mut state, &pattern);
6080
6081 assert_eq!(results.len(), 2);
6082 assert!(results.iter().any(|s| s.ends_with("file1.txt")));
6083 assert!(results.iter().any(|s| s.ends_with("file2.txt")));
6084 }
6085
6086 #[test]
6087 fn test_glob_hidden() {
6088 let _g = crate::test_util::global_state_lock();
6089 let dir = setup_test_dir();
6090 let pattern = format!("{}/*", dir.path().display());
6091
6092 // Default (globdots off) → hidden files skipped. `dotglob`
6093 // is the bash alias for zsh's canonical `globdots` option;
6094 // the C-faithful read goes through `isset(GLOBDOTS)` which
6095 // resolves the canonical name.
6096 opt_state_set("globdots", false);
6097 let mut state = globdata::new();
6098 let results = globdata_glob(&mut state, &pattern);
6099 assert!(!results.iter().any(|s| s.contains(".hidden")));
6100
6101 // setopt globdots → hidden files included.
6102 opt_state_set("globdots", true);
6103 let mut state = globdata::new();
6104 let results = globdata_glob(&mut state, &pattern);
6105 assert!(results.iter().any(|s| s.contains(".hidden")));
6106 opt_state_set("globdots", false); // reset for other tests
6107 }
6108
6109 #[test]
6110 fn test_glob_emit_path_strips_read_dir_dot_slash() {
6111 let _g = crate::test_util::global_state_lock();
6112 assert_eq!(glob_emit_path(Path::new("./sub")), "sub");
6113 assert_eq!(glob_emit_path(Path::new("sub/deeper")), "sub/deeper");
6114 assert_eq!(glob_emit_path(Path::new("././x")), "x");
6115 assert_eq!(glob_emit_path(Path::new("../up")), "../up");
6116 }
6117
6118 #[test]
6119 fn test_file_type_char() {
6120 let _g = crate::test_util::global_state_lock();
6121 assert_eq!(file_type(libc::S_IFDIR as u32), '/');
6122 assert_eq!(file_type(libc::S_IFREG as u32), ' ');
6123 assert_eq!(file_type(libc::S_IFREG as u32 | 0o111), '*');
6124 assert_eq!(file_type(libc::S_IFLNK as u32), '@');
6125 }
6126
6127 /// `Src/glob.c:2018-2036` — `file_type(mode_t filemode)` returns a
6128 /// single-char marker keyed off S_ISBLK/CHR/DIR/FIFO/LNK/REG/SOCK,
6129 /// with `S_IXUGO` (== 0o111 = S_IXUSR|S_IXGRP|S_IXOTH) distinguishing
6130 /// executable regular files (`*`) from non-executable (` `). The
6131 /// catch-all (e.g. door, port, unknown types) returns `?`.
6132 /// Pin every branch by position.
6133 #[test]
6134 fn file_type_every_branch_matches_c_dispatch() {
6135 let _g = crate::test_util::global_state_lock();
6136 assert_eq!(
6137 file_type(libc::S_IFBLK as u32),
6138 '#',
6139 "c:2020 — S_ISBLK → '#'"
6140 );
6141 assert_eq!(
6142 file_type(libc::S_IFCHR as u32),
6143 '%',
6144 "c:2022 — S_ISCHR → '%'"
6145 );
6146 assert_eq!(
6147 file_type(libc::S_IFDIR as u32),
6148 '/',
6149 "c:2024 — S_ISDIR → '/'"
6150 );
6151 assert_eq!(
6152 file_type(libc::S_IFIFO as u32),
6153 '|',
6154 "c:2026 — S_ISFIFO → '|'"
6155 );
6156 assert_eq!(
6157 file_type(libc::S_IFLNK as u32),
6158 '@',
6159 "c:2028 — S_ISLNK → '@'"
6160 );
6161 // Regular file: ' ' if not executable, '*' if executable (any bit in S_IXUGO).
6162 assert_eq!(
6163 file_type(libc::S_IFREG as u32),
6164 ' ',
6165 "c:2030 — non-executable regular file → ' '"
6166 );
6167 // Each individual exec bit triggers '*'.
6168 assert_eq!(
6169 file_type(libc::S_IFREG as u32 | 0o100),
6170 '*',
6171 "c:2030 — S_IXUSR alone is enough"
6172 );
6173 assert_eq!(
6174 file_type(libc::S_IFREG as u32 | 0o010),
6175 '*',
6176 "c:2030 — S_IXGRP alone is enough"
6177 );
6178 assert_eq!(
6179 file_type(libc::S_IFREG as u32 | 0o001),
6180 '*',
6181 "c:2030 — S_IXOTH alone is enough"
6182 );
6183 assert_eq!(
6184 file_type(libc::S_IFSOCK as u32),
6185 '=',
6186 "c:2033 — S_ISSOCK → '='"
6187 );
6188 // Catch-all for unknown S_IFMT — bare 0 returns '?'.
6189 assert_eq!(file_type(0), '?', "c:2035 — unknown st_mode → '?'");
6190 }
6191
6192 #[test]
6193 fn test_zstrcmp_numeric() {
6194 let _g = crate::test_util::global_state_lock();
6195 let n = crate::zsh_h::SORTIT_NUMERICALLY as u32;
6196 assert_eq!(zstrcmp("file1", "file2", n), std::cmp::Ordering::Less);
6197 assert_eq!(zstrcmp("file10", "file2", n), std::cmp::Ordering::Greater);
6198 assert_eq!(zstrcmp("file10", "file10", n), std::cmp::Ordering::Equal);
6199 }
6200
6201 /// Race-fix verification: snapshot pins bareglobqual for the
6202 /// duration of a glob scope even when the live store flips
6203 /// underneath. Mimics the stryke pattern in
6204 /// MenkeTechnologies/strykelang#3 — one thread is mid-glob with
6205 /// bareglobqual=1 snapshot when another flips it off.
6206 #[test]
6207 fn glob_opts_snapshot_isolates_concurrent_setopt() {
6208 let _g = crate::test_util::global_state_lock();
6209
6210 // Preserve the existing live state so the test is hermetic.
6211 let saved = opt_state_get("bareglobqual");
6212
6213 // Live store says bareglobqual=true; enter scope captures that.
6214 opt_state_set("bareglobqual", true);
6215 let _scope = enter_glob_scope();
6216 assert!(
6217 glob_isset(BAREGLOBQUAL),
6218 "TLS snapshot reads bareglobqual=true at scope entry"
6219 );
6220
6221 // Simulate a concurrent thread flipping the live store.
6222 opt_state_set("bareglobqual", false);
6223 assert!(
6224 glob_isset(BAREGLOBQUAL),
6225 "TLS snapshot must still report bareglobqual=true \
6226 even though live store now reads false"
6227 );
6228
6229 // Restore.
6230 match saved {
6231 Some(v) => opt_state_set("bareglobqual", v),
6232 None => opt_state_unset("bareglobqual"),
6233 }
6234 }
6235
6236 /// After the scope guard drops, reads fall back to the live store.
6237 #[test]
6238 fn glob_opts_snapshot_clears_on_drop() {
6239 let _g = crate::test_util::global_state_lock();
6240
6241 let saved = opt_state_get("nullglob");
6242 opt_state_set("nullglob", true);
6243 {
6244 let _scope = enter_glob_scope();
6245 assert!(glob_isset(NULLGLOB), "snapshot live=true → true inside");
6246 }
6247 // Outside scope: flip live store, read should follow live.
6248 opt_state_set("nullglob", false);
6249 assert!(
6250 !glob_isset(NULLGLOB),
6251 "post-scope: glob_isset falls back to live store"
6252 );
6253
6254 match saved {
6255 Some(v) => opt_state_set("nullglob", v),
6256 None => opt_state_unset("nullglob"),
6257 }
6258 }
6259
6260 /// Nested glob scopes share the outer snapshot — inner doesn't
6261 /// re-capture or clear on its own Drop.
6262 #[test]
6263 fn glob_opts_snapshot_nested_is_noop() {
6264 let _g = crate::test_util::global_state_lock();
6265
6266 let saved = opt_state_get("extendedglob");
6267 opt_state_set("extendedglob", true);
6268 let _outer = enter_glob_scope();
6269 assert!(glob_isset(EXTENDEDGLOB));
6270 {
6271 let _inner = enter_glob_scope();
6272 // Live store flip while nested.
6273 opt_state_set("extendedglob", false);
6274 assert!(glob_isset(EXTENDEDGLOB), "inner observes outer snapshot");
6275 } // inner drops — outer snapshot still active.
6276 assert!(
6277 glob_isset(EXTENDEDGLOB),
6278 "outer snapshot survives inner drop"
6279 );
6280
6281 match saved {
6282 Some(v) => opt_state_set("extendedglob", v),
6283 None => opt_state_unset("extendedglob"),
6284 }
6285 }
6286
6287 /// c:2150 (xpandredir port) — when the redir name has no wildcard
6288 /// AND no `$var`/`!hist`/`{a,b}` to expand, prefork+globlist leave
6289 /// the name unchanged. The single-match path at c:2171 must
6290 /// rewrite `fn.name` to the same string and return 0 (no multi-fan).
6291 /// Regression that returns 1 would trigger MULTIOS dispatch on a
6292 /// single literal filename — wrong shell semantics.
6293 #[test]
6294 fn xpandredir_single_literal_filename_returns_zero() {
6295 let _g = crate::test_util::global_state_lock();
6296 let mut fn_ = redir {
6297 typ: REDIR_WRITE,
6298 flags: 0,
6299 fd1: 1,
6300 fd2: -1,
6301 name: Some("/tmp/zshrs_test_out".to_string()),
6302 varid: None,
6303 here_terminator: None,
6304 munged_here_terminator: None,
6305 };
6306 let mut tab: Vec<redir> = Vec::new();
6307 let r = xpandredir(&mut fn_, &mut tab);
6308 assert_eq!(r, 0, "literal filename → single match → ret=0");
6309 assert_eq!(
6310 fn_.name.as_deref(),
6311 Some("/tmp/zshrs_test_out"),
6312 "literal name must round-trip through prefork unchanged"
6313 );
6314 assert!(tab.is_empty(), "no multi-match → redirtab not appended");
6315 }
6316
6317 /// c:2176-2177 — `>&-` (REDIR_MERGEOUT with name "-") collapses to
6318 /// REDIR_CLOSE per the `IS_DASH(s[0]) && !s[1]` branch. Regression
6319 /// where this fails leaves `>&-` as a literal merge-fd attempt,
6320 /// which the executor would interpret as "merge with fd -1".
6321 #[test]
6322 fn xpandredir_dash_merge_collapses_to_close() {
6323 let _g = crate::test_util::global_state_lock();
6324 let mut fn_ = redir {
6325 typ: REDIR_MERGEOUT,
6326 flags: 0,
6327 fd1: 1,
6328 fd2: -1,
6329 name: Some("-".to_string()),
6330 varid: None,
6331 here_terminator: None,
6332 munged_here_terminator: None,
6333 };
6334 let mut tab: Vec<redir> = Vec::new();
6335 let _ = xpandredir(&mut fn_, &mut tab);
6336 assert_eq!(
6337 fn_.typ, REDIR_CLOSE,
6338 "`>&-` must rewrite typ to REDIR_CLOSE"
6339 );
6340 }
6341
6342 /// c:2150 — empty `fn.name` should return 0 cleanly. Catches a
6343 /// regression that panics on `.as_deref().unwrap()` for absent name.
6344 #[test]
6345 fn xpandredir_with_no_name_returns_zero_no_panic() {
6346 let _g = crate::test_util::global_state_lock();
6347 let mut fn_ = redir {
6348 typ: REDIR_WRITE,
6349 flags: 0,
6350 fd1: 1,
6351 fd2: -1,
6352 name: None,
6353 varid: None,
6354 here_terminator: None,
6355 munged_here_terminator: None,
6356 };
6357 let mut tab: Vec<redir> = Vec::new();
6358 assert_eq!(xpandredir(&mut fn_, &mut tab), 0);
6359 }
6360
6361 /// `IN_EXPANDREDIR` static defaults to 0 — set transiently inside
6362 /// xpandredir per c:2165/2167. After a normal call it must restore
6363 /// to 0. Regression that leaks the flag would skew unrelated glob
6364 /// expansions outside redirections.
6365 #[test]
6366 fn in_expandredir_flag_is_zero_at_rest() {
6367 let _g = crate::test_util::global_state_lock();
6368 assert_eq!(IN_EXPANDREDIR.load(Ordering::SeqCst), 0);
6369 }
6370
6371 /// c:4306 — `haswilds` honours backslash-escapes: `\*` is a literal
6372 /// star, NOT a wildcard. Regression that ignores the escape would
6373 /// trigger globbing on `printf '%s\n' \*`, breaking shell scripts
6374 /// that quote literal stars.
6375 #[test]
6376 fn haswilds_respects_backslash_escape() {
6377 let _g = crate::test_util::global_state_lock();
6378 // c:Src/glob.c:3548 — full glob tokenize (shadows the
6379 // brace-only `fn tok` test helper above on purpose:
6380 // haswilds consumes glob tokens, not just braces).
6381 let tok = |s: &str| {
6382 let mut t = s.to_string();
6383 tokenize(&mut t);
6384 t
6385 };
6386 assert!(haswilds(&tok("*.txt")), "bare * is wild");
6387 assert!(
6388 !haswilds(&tok(r"\*.txt")),
6389 "escaped \\* is literal — NOT wild"
6390 );
6391 assert!(
6392 !haswilds(&tok(r"\?.txt")),
6393 "escaped \\? is literal — NOT wild"
6394 );
6395 }
6396
6397 /// c:4306 — `[` immediately enters bracket mode AND counts as a
6398 /// wildcard. The early return on `[` is critical — even
6399 /// unterminated brackets must be flagged so `cd [` doesn't try
6400 /// to chdir to a literal `[`.
6401 #[test]
6402 fn haswilds_open_bracket_alone_is_a_wildcard() {
6403 let _g = crate::test_util::global_state_lock();
6404 // c:Src/glob.c:3548 — full glob tokenize (shadows the
6405 // brace-only `fn tok` test helper above on purpose:
6406 // haswilds consumes glob tokens, not just braces).
6407 let tok = |s: &str| {
6408 let mut t = s.to_string();
6409 tokenize(&mut t);
6410 t
6411 };
6412 assert!(haswilds(&tok("[abc]")), "char-class is wild");
6413 assert!(haswilds(&tok("foo[")), "even unterminated [ is wild");
6414 }
6415
6416 /// c:4306 — wildcard chars `*` `?` inside an OPEN bracket-context
6417 /// are NOT additional wildcards (the bracket already is one).
6418 /// Regression that double-counts would make haswilds report `[*]`
6419 /// as wildcard-twice — cosmetic, but confuses any caller using
6420 /// the bool as a "should I glob?" gate that AND-tests with another
6421 /// flag.
6422 #[test]
6423 fn haswilds_extglob_chars_inside_bracket_dont_double_count() {
6424 let _g = crate::test_util::global_state_lock();
6425 // c:Src/glob.c:3548 — full glob tokenize (shadows the
6426 // brace-only `fn tok` test helper above on purpose:
6427 // haswilds consumes glob tokens, not just braces).
6428 let tok = |s: &str| {
6429 let mut t = s.to_string();
6430 tokenize(&mut t);
6431 t
6432 };
6433 // Once `[` is seen, function returns true immediately, so the
6434 // post-bracket chars don't matter. But this docs the contract.
6435 assert!(haswilds(&tok("[*]")));
6436 }
6437
6438 /// c:4306 — plain text returns false. Catches a regression where
6439 /// any non-empty input is flagged as wild (would break `cd /tmp`
6440 /// by triggering glob expansion on a literal path).
6441 #[test]
6442 fn haswilds_plain_text_not_wild() {
6443 let _g = crate::test_util::global_state_lock();
6444 // c:Src/glob.c:3548 — full glob tokenize (shadows the
6445 // brace-only `fn tok` test helper above on purpose:
6446 // haswilds consumes glob tokens, not just braces).
6447 let tok = |s: &str| {
6448 let mut t = s.to_string();
6449 tokenize(&mut t);
6450 t
6451 };
6452 assert!(!haswilds(&tok("plain")));
6453 assert!(!haswilds(&tok("")));
6454 assert!(!haswilds(&tok("/usr/local/bin")));
6455 assert!(!haswilds(&tok("file.txt")));
6456 }
6457
6458 /// c:4363-4371 — `#` and `^` are recognised as wildcards by
6459 /// `haswilds` **only when `EXTENDEDGLOB` is set**, matching the C
6460 /// source's `isset(EXTENDEDGLOB)` gate at pattern.c:4364/4369. `~`
6461 /// is **not** a filename-generation wildcard (zsh handles it as
6462 /// tilde expansion in a separate pipeline stage); a prior
6463 /// glob.rs-local `haswilds` impl returned true for `~`, which
6464 /// caused zglob to mis-classify tilde-prefixed args as needing
6465 /// filename generation. Pin the corrected behavior.
6466 #[test]
6467 fn haswilds_extended_glob_chars_recognised() {
6468 let _g = crate::test_util::global_state_lock();
6469 // c:Src/glob.c:3548 — full glob tokenize (shadows the
6470 // brace-only `fn tok` test helper above on purpose:
6471 // haswilds consumes glob tokens, not just braces).
6472 let tok = |s: &str| {
6473 let mut t = s.to_string();
6474 tokenize(&mut t);
6475 t
6476 };
6477 // EXTENDEDGLOB off → `#` and `^` are not wild.
6478 crate::ported::options::opt_state_set("extendedglob", false);
6479 assert!(
6480 !haswilds(&tok("foo#bar")),
6481 "# not wild without EXTENDEDGLOB"
6482 );
6483 assert!(
6484 !haswilds(&tok("foo^bar")),
6485 "^ not wild without EXTENDEDGLOB"
6486 );
6487 // EXTENDEDGLOB on → `#` and `^` are wild (c:4364, c:4369).
6488 crate::ported::options::opt_state_set("extendedglob", true);
6489 assert!(haswilds(&tok("foo#bar")), "# is extglob wild");
6490 assert!(haswilds(&tok("foo^bar")), "^ is extglob wild");
6491 crate::ported::options::opt_state_set("extendedglob", false);
6492 // `~` is NOT in haswilds' switch (c:4324-4373) — tilde expansion
6493 // is a separate pipeline stage.
6494 assert!(
6495 !haswilds(&tok("~/file")),
6496 "~ is NOT a filename-generation wildcard"
6497 );
6498 }
6499
6500 /// c:2514 — `matchpat` returns true for exact match. Sanity check
6501 /// the simplest dispatch path (the matcher recipe builds a
6502 /// trivial pattern, runs it).
6503 #[test]
6504 fn matchpat_exact_literal_matches() {
6505 let _g = crate::test_util::global_state_lock();
6506 assert!(matchpat("hello", "hello", false, true));
6507 assert!(!matchpat("hello", "world", false, true));
6508 }
6509
6510 /// c:2514 — `matchpat` with case_sensitive=false MUST treat upper
6511 /// and lower as equal (`HELLO` matches `hello`). Regression keeping
6512 /// case-sensitive when flag is false would silently break every
6513 /// `[[ "$x" = (#i)foo ]]`-style match.
6514 #[test]
6515 fn matchpat_case_insensitive_when_flag_clear() {
6516 let _g = crate::test_util::global_state_lock();
6517 assert!(
6518 matchpat("hello", "HELLO", false, false),
6519 "case-insensitive match must succeed across cases"
6520 );
6521 assert!(matchpat("FoO", "foo", false, false));
6522 }
6523
6524 /// c:2514 — case-sensitive (default) MUST reject case-different
6525 /// inputs. Pinning the contract.
6526 #[test]
6527 fn matchpat_case_sensitive_rejects_case_different() {
6528 let _g = crate::test_util::global_state_lock();
6529 assert!(
6530 !matchpat("hello", "HELLO", false, true),
6531 "case-sensitive default must reject HELLO != hello"
6532 );
6533 }
6534
6535 /// c:2780 — `set_pat_start(p, offs)` sets `PAT_NOTSTART` when offs is
6536 /// nonzero (matched substring starts past the real start, so `(#s)`
6537 /// must fail) and clears it when offs is 0. A regression here would
6538 /// let `(#s)` anchors fire mid-string during substring globbing.
6539 #[test]
6540 fn set_pat_start_toggles_pat_notstart_flag() {
6541 let _g = crate::test_util::global_state_lock();
6542 let mut p = mk_test_patprog();
6543 set_pat_start(&mut p, 2);
6544 assert!(
6545 p.0.flags & PAT_NOTSTART != 0,
6546 "offs!=0 must set PAT_NOTSTART"
6547 );
6548 set_pat_start(&mut p, 0);
6549 assert!(
6550 p.0.flags & PAT_NOTSTART == 0,
6551 "offs==0 must clear PAT_NOTSTART"
6552 );
6553 }
6554
6555 /// c:2796 — `set_pat_end(p, null_me)` sets `PAT_NOTEND` when the char
6556 /// being zapped is non-NUL (string shortened at tail, so `(#e)` must
6557 /// fail) and clears it when that char is already NUL. A regression
6558 /// would let `(#e)` anchors fire before the real end.
6559 #[test]
6560 fn set_pat_end_toggles_pat_notend_flag() {
6561 let _g = crate::test_util::global_state_lock();
6562 let mut p = mk_test_patprog();
6563 set_pat_end(&mut p, b'x');
6564 assert!(
6565 p.0.flags & PAT_NOTEND != 0,
6566 "non-NUL null_me must set PAT_NOTEND"
6567 );
6568 set_pat_end(&mut p, 0);
6569 assert!(
6570 p.0.flags & PAT_NOTEND == 0,
6571 "NUL null_me must clear PAT_NOTEND"
6572 );
6573 }
6574
6575 /// Build a zeroed `Patprog` for flag-toggle tests.
6576 fn mk_test_patprog() -> Patprog {
6577 Box::new((
6578 crate::ported::zsh_h::patprog {
6579 startoff: 0,
6580 size: 0,
6581 mustoff: 0,
6582 patmlen: 0,
6583 globflags: 0,
6584 globend: 0,
6585 flags: 0,
6586 patnpar: 0,
6587 patstartch: 0,
6588 },
6589 Vec::new(),
6590 ))
6591 }
6592
6593 /// c:2773 — `freematchlist(None)` is a no-op (matches C's
6594 /// `if (repllist) freelinklist(...)`). Regression panicking on
6595 /// None would crash every glob-replace path with no matches.
6596 #[test]
6597 fn freematchlist_handles_none_safely() {
6598 let _g = crate::test_util::global_state_lock();
6599 freematchlist(None);
6600 // No assertion — survival is the test.
6601 }
6602
6603 /// c:2773 — `freematchlist(Some(&mut Vec))` clears the list.
6604 /// Regression that drops a stale entry would leak match positions
6605 /// across calls.
6606 #[test]
6607 fn freematchlist_clears_provided_vec() {
6608 let _g = crate::test_util::global_state_lock();
6609 let mut v = vec![
6610 repldata {
6611 b: 0,
6612 e: 5,
6613 replstr: None,
6614 },
6615 repldata {
6616 b: 10,
6617 e: 15,
6618 replstr: Some("x".to_string()),
6619 },
6620 ];
6621 freematchlist(Some(&mut v));
6622 assert!(v.is_empty(), "freematchlist must clear the input vec");
6623 }
6624
6625 /// `Src/glob.c:2042-2142` — `hasbraces(str)` returns true when
6626 /// `str` contains a brace-expansion candidate. A bare lbrace/rbrace
6627 /// is NOT a brace expansion (it's a literal). Detection requires a
6628 /// matched pair containing either a comma or a dotdot range. Pin
6629 /// the canonical contract.
6630 #[test]
6631 fn hasbraces_matched_pair_with_comma_or_dotdot() {
6632 let _g = crate::test_util::global_state_lock();
6633 // Matched + comma → true.
6634 assert!(
6635 hasbraces(&tok("a{b,c}d"), false),
6636 "c:2127 — lbrace + comma + rbrace is a brace expansion"
6637 );
6638 // Matched + dotdot → true.
6639 assert!(
6640 hasbraces(&tok("file{1..3}.txt"), false),
6641 "c:2082 — N..M range is a brace expansion"
6642 );
6643 // Matched WITHOUT comma/dotdot → false (it's a literal).
6644 assert!(
6645 !hasbraces(&tok("{abc}"), false),
6646 "literal braces are NOT a brace expansion without comma or dotdot"
6647 );
6648 // Unmatched → false.
6649 assert!(
6650 !hasbraces(&tok("{abc"), false),
6651 "lone lbrace without matching rbrace is not brace expansion"
6652 );
6653 assert!(
6654 !hasbraces(&tok("abc}"), false),
6655 "lone rbrace is not brace expansion"
6656 );
6657 // No braces → false.
6658 assert!(!hasbraces(&tok("plain"), false));
6659 assert!(!hasbraces(&tok(""), false));
6660 }
6661
6662 /// `Src/glob.c:2049-2063` — BRACECCL branch: when `isset(BRACECCL)`
6663 /// (the `brace_ccl` parameter), any non-empty matched braces
6664 /// become a character-class set, regardless of whether the body
6665 /// contains a comma. Empty pair is still not.
6666 #[test]
6667 fn hasbraces_brace_ccl_makes_any_pair_match() {
6668 let _g = crate::test_util::global_state_lock();
6669 // c:2049 — BRACECCL: non-empty pair is enough.
6670 assert!(
6671 hasbraces(&tok("{abc}"), true),
6672 "c:2049 — BRACECCL: non-empty pair is char-class set"
6673 );
6674 assert!(
6675 hasbraces(&tok("x{q}y"), true),
6676 "c:2049 — single-char pair counts under BRACECCL"
6677 );
6678 // Empty pair shouldn't trigger.
6679 assert!(
6680 !hasbraces(&tok("{}"), true),
6681 "empty pair still not a brace expansion even under BRACECCL"
6682 );
6683 // Without BRACECCL, plain literal-letter pair is NOT a brace expansion.
6684 assert!(
6685 !hasbraces(&tok("{abc}"), false),
6686 "c:2049 — BRACECCL off → plain literal pair stays literal"
6687 );
6688 }
6689
6690 /// `Src/glob.c:2042-2142` — depth tracking: comma/dotdot count
6691 /// only at depth==1 in our port. So nested inner comma doesn't
6692 /// qualify the outer pair, but TWO independent top-level pairs
6693 /// (each with comma) DO trigger detection.
6694 #[test]
6695 fn hasbraces_depth_1_check_for_comma_dotdot() {
6696 let _g = crate::test_util::global_state_lock();
6697 assert!(
6698 hasbraces(&tok("a{1,2}b{3,4}c"), false),
6699 "two independent top-level pairs, first one matches at depth 1"
6700 );
6701 }
6702
6703 /// Pin: `remnulargs` per `Src/glob.c:3649-3679`:
6704 /// - Strings with NO inull bytes are unchanged.
6705 /// - Strings with ONLY Bnullkeep: keep as-is in scan phase
6706 /// (Bnullkeep is "active backslash" that hasn't triggered
6707 /// copy phase yet).
6708 /// - After encountering any other inull: switch to copy phase:
6709 /// * Bnullkeep → '\\' (literal backslash).
6710 /// * Other inulls (Snull/Dnull/Bnull/Nularg) → stripped.
6711 /// * Non-inull → kept.
6712 /// - Empty post-strip → replaced with single Nularg.
6713 #[test]
6714 fn remnulargs_matches_c_inull_handling() {
6715 let _g = crate::test_util::global_state_lock();
6716
6717 // Plain ASCII unchanged (no inulls).
6718 let mut s = "hello".to_string();
6719 remnulargs(&mut s);
6720 assert_eq!(
6721 s, "hello",
6722 "c:3654 — no inull bytes leaves string unchanged"
6723 );
6724
6725 // Snull-triggered copy: Snull stripped, rest kept.
6726 let mut s = format!("ab{}cd", Snull);
6727 remnulargs(&mut s);
6728 assert_eq!(
6729 s, "abcd",
6730 "c:3663 — Snull triggers copy; itself stripped, rest kept"
6731 );
6732
6733 // Bnullkeep AFTER Snull trigger → '\\' (active backslash).
6734 let mut s = format!("ab{}c{}d", Snull, Bnullkeep);
6735 remnulargs(&mut s);
6736 assert_eq!(
6737 s, "abc\\d",
6738 "c:3666 — Bnullkeep in copy phase becomes literal '\\\\'"
6739 );
6740
6741 // Other inulls (Bnull, Dnull) stripped in copy phase.
6742 let mut s = format!("a{}b{}c{}d", Snull, Bnull, Dnull);
6743 remnulargs(&mut s);
6744 assert_eq!(s, "abcd", "c:3668 — Bnull/Dnull stripped in copy phase");
6745
6746 // Empty post-strip → Nularg.
6747 let mut s = format!("{}", Snull);
6748 remnulargs(&mut s);
6749 assert_eq!(
6750 s,
6751 format!("{}", Nularg),
6752 "c:3674 — empty result replaced by Nularg sentinel"
6753 );
6754 }
6755
6756 /// `Src/glob.c:1085-1117` — `glob_exec_string` is a PARSER, not
6757 /// an executor. Extracts the qualifier inner text from `*sp`
6758 /// (advancing past delimiters). Previous Rust port forked
6759 /// `/bin/sh` to execute the cmd — that's entirely the wrong
6760 /// layer; execution happens at the call sites via qualsheval.
6761 /// Pin: plus-form extracts identifier; delimited form extracts
6762 /// content + advances.
6763 #[test]
6764 fn glob_exec_string_parses_qualifier_text() {
6765 let _g = crate::test_util::global_state_lock();
6766 // Plus form: identifier ends at first non-ident char.
6767 // C: `(+myfunc:rest)` — `s` points past `+`, plus_form=true.
6768 let r = glob_exec_string("myfunc rest", true);
6769 assert!(r.is_some(), "c:1092 — identifier parse should succeed");
6770 let (ident, _adv) = r.unwrap();
6771 assert_eq!(
6772 ident, "myfunc",
6773 "c:1092 — itype_end stops at first non-IIDENT char"
6774 );
6775
6776 // Plus form with no identifier (immediate non-ident) — error.
6777 let r = glob_exec_string(" leading-space", true);
6778 assert!(
6779 r.is_none(),
6780 "c:1093-1096 — empty identifier emits zerr + returns None"
6781 );
6782 }
6783
6784 /// `Src/glob.c:3907-3943` — `qualsheval(name, _, _, expr)` sets
6785 /// `$REPLY` via paramtab, evaluates `expr` IN THE CURRENT SHELL,
6786 /// then restores errflag+lastval. Previous Rust port spawned
6787 /// `/bin/sh -c expr` which:
6788 /// 1. Couldn't access shell locals / functions / aliases.
6789 /// 2. Ran `sh` (POSIX), not `zsh` — every zsh-feature qualifier
6790 /// silently failed.
6791 ///
6792 /// Pin: after qualsheval, errflag is restored (mod ERRFLAG_INT),
6793 /// lastval is restored, and `$REPLY` is set to the filename.
6794 /// Easiest direct pin: errflag/lastval restore around an `expr`
6795 /// that mutates them.
6796 #[test]
6797 fn qualsheval_restores_errflag_and_lastval() {
6798 let _g = crate::test_util::global_state_lock();
6799 // Seed errflag and lastval with distinctive values.
6800 errflag.store(0, Ordering::Relaxed);
6801 LASTVAL.store(42, Ordering::Relaxed);
6802 // Even if the expr mutates these via the executor, c:3924-3925
6803 // restore them after.
6804 let st0: libc::stat = unsafe { std::mem::zeroed() };
6805 let _ = qualsheval("/tmp/file", &st0, 0, ":"); // no-op expr
6806 // c:3924 — errflag restored.
6807 assert_eq!(
6808 errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR,
6809 0,
6810 "c:3924 — qualsheval must restore errflag (no ERRFLAG_ERROR leak)"
6811 );
6812 // c:3925 — lastval restored.
6813 assert_eq!(
6814 LASTVAL.load(Ordering::Relaxed),
6815 42,
6816 "c:3925 — qualsheval must restore lastval to pre-call value"
6817 );
6818 // c:3916 — $REPLY visible in paramtab.
6819 assert_eq!(
6820 crate::ported::params::getsparam("REPLY"),
6821 Some("/tmp/file".to_string()),
6822 "c:3916 — qualsheval must set $REPLY to filename"
6823 );
6824 }
6825
6826 /// `Src/glob.c:2164` — `if (!errflag && isset(MULTIOS))` is the
6827 /// logical zero-check guarding globlist recursion. Previous Rust
6828 /// port used `!errflag.load(...) != 0` which is BITWISE NOT
6829 /// (`^-1`), evaluating to `errflag != -1` — almost-always-true.
6830 ///
6831 /// Pin: a `matchpat` smoke that DOES NOT mutate errflag, then a
6832 /// direct check that the fix sense is correct (errflag==0 enters
6833 /// the branch, errflag!=0 skips). Can't easily exercise the full
6834 /// xpandredir path without a redir struct + IN_EXPANDREDIR
6835 /// global, so this is a sense-of-the-condition pin.
6836 #[test]
6837 fn xpandredir_errflag_check_uses_logical_zero() {
6838 let _g = crate::test_util::global_state_lock();
6839 // Mirror the in-port logic with the canonical zero-check.
6840 let saved = errflag.load(Ordering::Relaxed);
6841 // c:2164 — errflag==0 path: branch should fire.
6842 errflag.store(0, Ordering::Relaxed);
6843 let zero_enters = errflag.load(Ordering::Relaxed) == 0;
6844 assert!(
6845 zero_enters,
6846 "c:2164 — errflag==0 enters the branch (the canonical sense)"
6847 );
6848 // c:2164 — errflag!=0 path: branch should skip.
6849 errflag.store(1, Ordering::Relaxed);
6850 let nonzero_skips = errflag.load(Ordering::Relaxed) == 0;
6851 assert!(
6852 !nonzero_skips,
6853 "c:2164 — errflag!=0 skips the branch (regression of bitwise-NOT bug fix)"
6854 );
6855 errflag.store(saved, Ordering::Relaxed);
6856 }
6857
6858 // ═══════════════════════════════════════════════════════════════════
6859 // Additional unit coverage — pure pattern/brace/glob behaviour that
6860 // doesn't need a real filesystem. Uses `matchpat` for high-level
6861 // pattern checks (extended=false, case_sensitive=true is the most
6862 // common path) and `xpandbraces` for brace expansion.
6863 // ═══════════════════════════════════════════════════════════════════
6864
6865 // ── matchpat: full-string pattern matching ───────────────────────
6866 #[test]
6867 fn matchpat_literal_no_wildcards() {
6868 let _g = crate::test_util::global_state_lock();
6869 assert!(matchpat("foo", "foo", false, true));
6870 assert!(!matchpat("foo", "bar", false, true));
6871 assert!(!matchpat("foo", "foobar", false, true));
6872 }
6873
6874 #[test]
6875 fn matchpat_star_consumes_substring() {
6876 let _g = crate::test_util::global_state_lock();
6877 assert!(matchpat("a*b", "ab", false, true));
6878 assert!(matchpat("a*b", "ahellob", false, true));
6879 assert!(!matchpat("a*b", "abc", false, true));
6880 }
6881
6882 #[test]
6883 fn matchpat_question_is_single_char() {
6884 let _g = crate::test_util::global_state_lock();
6885 assert!(matchpat("a?c", "abc", false, true));
6886 assert!(matchpat("a?c", "axc", false, true));
6887 assert!(!matchpat("a?c", "ac", false, true));
6888 assert!(!matchpat("a?c", "abbc", false, true));
6889 }
6890
6891 #[test]
6892 fn matchpat_bracket_class_inline() {
6893 let _g = crate::test_util::global_state_lock();
6894 assert!(matchpat("file[abc].txt", "filea.txt", false, true));
6895 assert!(matchpat("file[abc].txt", "fileb.txt", false, true));
6896 assert!(!matchpat("file[abc].txt", "filed.txt", false, true));
6897 }
6898
6899 #[test]
6900 fn matchpat_case_sensitive_strict() {
6901 let _g = crate::test_util::global_state_lock();
6902 assert!(matchpat("Foo", "Foo", false, true));
6903 assert!(!matchpat("Foo", "foo", false, true));
6904 assert!(!matchpat("Foo", "FOO", false, true));
6905 }
6906
6907 #[test]
6908 fn matchpat_empty_pattern_matches_only_empty() {
6909 let _g = crate::test_util::global_state_lock();
6910 assert!(matchpat("", "", false, true));
6911 assert!(!matchpat("", "x", false, true));
6912 }
6913
6914 #[test]
6915 fn matchpat_star_alone_matches_anything() {
6916 let _g = crate::test_util::global_state_lock();
6917 assert!(matchpat("*", "", false, true));
6918 assert!(matchpat("*", "a", false, true));
6919 assert!(matchpat("*", "abcdef", false, true));
6920 }
6921
6922 #[test]
6923 fn matchpat_question_alone_one_char() {
6924 let _g = crate::test_util::global_state_lock();
6925 assert!(!matchpat("?", "", false, true));
6926 assert!(matchpat("?", "a", false, true));
6927 assert!(!matchpat("?", "ab", false, true));
6928 }
6929
6930 // ── haswilds: meta-char detection ───────────────────────────────
6931 #[test]
6932 fn haswilds_each_glob_meta() {
6933 let _g = crate::test_util::global_state_lock();
6934 // c:Src/glob.c:3548 — full glob tokenize (shadows the
6935 // brace-only `fn tok` test helper above on purpose:
6936 // haswilds consumes glob tokens, not just braces).
6937 let tok = |s: &str| {
6938 let mut t = s.to_string();
6939 tokenize(&mut t);
6940 t
6941 };
6942 assert!(haswilds(&tok("*")));
6943 assert!(haswilds(&tok("?")));
6944 assert!(haswilds(&tok("[abc]")));
6945 assert!(haswilds(&tok("a*b")));
6946 assert!(haswilds(&tok("a?b")));
6947 }
6948
6949 #[test]
6950 fn haswilds_plain_strings_have_no_wildcards() {
6951 let _g = crate::test_util::global_state_lock();
6952 // c:Src/glob.c:3548 — full glob tokenize (shadows the
6953 // brace-only `fn tok` test helper above on purpose:
6954 // haswilds consumes glob tokens, not just braces).
6955 let tok = |s: &str| {
6956 let mut t = s.to_string();
6957 tokenize(&mut t);
6958 t
6959 };
6960 assert!(!haswilds(&tok("")));
6961 assert!(!haswilds(&tok("plain.txt")));
6962 assert!(!haswilds(&tok("/abs/path/file.rs")));
6963 assert!(!haswilds(&tok("./rel/file")));
6964 }
6965
6966 // ── xpandbraces: brace expansion (zsh extension, not POSIX) ─────
6967 #[test]
6968 fn xpandbraces_three_alternatives() {
6969 let _g = crate::test_util::global_state_lock();
6970 assert_eq!(xpandbraces(&tok("{a,b,c}"), false), vec!["a", "b", "c"]);
6971 }
6972
6973 #[test]
6974 fn xpandbraces_with_prefix_and_suffix() {
6975 let _g = crate::test_util::global_state_lock();
6976 assert_eq!(
6977 xpandbraces(&tok("pre{x,y,z}post"), false),
6978 vec!["prexpost", "preypost", "prezpost"]
6979 );
6980 }
6981
6982 #[test]
6983 fn xpandbraces_numeric_range_ascending() {
6984 let _g = crate::test_util::global_state_lock();
6985 assert_eq!(
6986 xpandbraces(&tok("{1..5}"), false),
6987 vec!["1", "2", "3", "4", "5"]
6988 );
6989 }
6990
6991 #[test]
6992 fn xpandbraces_alpha_range_ascending() {
6993 let _g = crate::test_util::global_state_lock();
6994 assert_eq!(
6995 xpandbraces(&tok("{a..e}"), false),
6996 vec!["a", "b", "c", "d", "e"]
6997 );
6998 }
6999
7000 #[test]
7001 fn xpandbraces_single_alternative_passes_through_literal() {
7002 // Real zsh 5.9 (`print -l {a}`) outputs `{a}` verbatim — no
7003 // expansion because there is no comma or range inside the
7004 // braces. xpandbraces operates on TOKEN form throughout
7005 // (c:Src/glob.c::xpandbraces); the eventual ASCII conversion
7006 // happens later in untokenize. So unit-level pass-through
7007 // preserves TOKEN bytes, not ASCII braces.
7008 let _g = crate::test_util::global_state_lock();
7009 let out = xpandbraces(&tok("{a}"), false);
7010 assert_eq!(
7011 out,
7012 vec![tok("{a}")],
7013 "zsh 5.9 returns the input verbatim (TOKEN form preserved at xpandbraces layer)"
7014 );
7015 }
7016
7017 #[test]
7018 fn xpandbraces_no_braces_returns_input() {
7019 let _g = crate::test_util::global_state_lock();
7020 // Pure literal has nothing to expand — should yield the input
7021 // as a single element (or empty for empty input).
7022 let out = xpandbraces(&tok("plain"), false);
7023 assert_eq!(out, vec!["plain"]);
7024 }
7025
7026 // ── file_type: mode → marker char (used by `ls -F`-style output) ─
7027 #[test]
7028 fn file_type_dir_marker_is_slash() {
7029 let _g = crate::test_util::global_state_lock();
7030 assert_eq!(file_type(libc::S_IFDIR as u32), '/');
7031 }
7032
7033 #[test]
7034 fn file_type_regular_plain_is_space() {
7035 let _g = crate::test_util::global_state_lock();
7036 assert_eq!(file_type(libc::S_IFREG as u32), ' ');
7037 }
7038
7039 #[test]
7040 fn file_type_regular_executable_is_star() {
7041 let _g = crate::test_util::global_state_lock();
7042 // Any of the three executable bits should switch the marker.
7043 for x in [0o100, 0o010, 0o001] {
7044 assert_eq!(
7045 file_type(libc::S_IFREG as u32 | x),
7046 '*',
7047 "exec bit 0o{x:o} should produce '*'"
7048 );
7049 }
7050 }
7051
7052 #[test]
7053 fn file_type_symlink_is_at() {
7054 let _g = crate::test_util::global_state_lock();
7055 assert_eq!(file_type(libc::S_IFLNK as u32), '@');
7056 }
7057
7058 #[test]
7059 fn file_type_fifo_is_pipe() {
7060 let _g = crate::test_util::global_state_lock();
7061 assert_eq!(file_type(libc::S_IFIFO as u32), '|');
7062 }
7063
7064 #[test]
7065 fn file_type_socket_is_equal() {
7066 let _g = crate::test_util::global_state_lock();
7067 assert_eq!(file_type(libc::S_IFSOCK as u32), '=');
7068 }
7069
7070 // ═══════════════════════════════════════════════════════════════════
7071 // xpandbraces edge cases — anchored to `print -l <pattern>` in zsh 5.9.
7072 // Where zshrs diverges, the test FAILS to expose the bug.
7073 // ═══════════════════════════════════════════════════════════════════
7074
7075 /// `{1..10..2}` → 1 3 5 7 9 (step range). zsh: 5 elements.
7076 #[test]
7077 fn xpandbraces_numeric_step_two() {
7078 let _g = crate::test_util::global_state_lock();
7079 assert_eq!(
7080 xpandbraces(&tok("{1..10..2}"), false),
7081 vec!["1", "3", "5", "7", "9"]
7082 );
7083 }
7084
7085 /// `{10..1..2}` → 10 8 6 4 2 (descending step).
7086 #[test]
7087 fn xpandbraces_numeric_descending_step_two() {
7088 let _g = crate::test_util::global_state_lock();
7089 assert_eq!(
7090 xpandbraces(&tok("{10..1..2}"), false),
7091 vec!["10", "8", "6", "4", "2"]
7092 );
7093 }
7094
7095 /// `{5..1}` → 5 4 3 2 1 (descending range, no step).
7096 #[test]
7097 fn xpandbraces_numeric_descending_range() {
7098 let _g = crate::test_util::global_state_lock();
7099 assert_eq!(
7100 xpandbraces(&tok("{5..1}"), false),
7101 vec!["5", "4", "3", "2", "1"]
7102 );
7103 }
7104
7105 /// `{01..10}` → 01 02 03 ... 10 (zero-padded; pad preserved).
7106 #[test]
7107 fn xpandbraces_zero_padded_numeric_range() {
7108 let _g = crate::test_util::global_state_lock();
7109 assert_eq!(
7110 xpandbraces(&tok("{01..10}"), false),
7111 vec!["01", "02", "03", "04", "05", "06", "07", "08", "09", "10"]
7112 );
7113 }
7114
7115 /// `{001..010}` → 001 002 ... 010 (3-digit pad preserved).
7116 #[test]
7117 fn xpandbraces_three_digit_pad_preserved() {
7118 let _g = crate::test_util::global_state_lock();
7119 assert_eq!(
7120 xpandbraces(&tok("{001..010}"), false),
7121 vec!["001", "002", "003", "004", "005", "006", "007", "008", "009", "010"]
7122 );
7123 }
7124
7125 /// `\{a,b,c\}` → no expansion (escaped braces aren't brace
7126 /// delimiters). xpandbraces returns input unchanged; the lexer
7127 /// upstream strips the backslashes before this layer sees them.
7128 /// Anchored test: zsh `print -r -- \{a,b,c\}` → `{a,b,c}` because
7129 /// the lexer tokenizes `\{`/`\}` to `Bnull{`/`Bnull}` before
7130 /// xpandbraces runs, then untokenize drops the Bnulls. At the
7131 /// xpandbraces-unit level (raw backslashes), the only invariant
7132 /// to verify is "no expansion fires" — the brace literal survives.
7133 #[test]
7134 fn xpandbraces_escaped_braces_remain_literal_anchored_to_zsh() {
7135 let _g = crate::test_util::global_state_lock();
7136 // tok() emits Bnull+ASCII `{` for `\{` and Bnull+ASCII `}` for
7137 // `\}`, with Comma TOKEN for the unescaped commas inside.
7138 // xpandbraces scans for Inbrace TOKEN only (per
7139 // c:Src/glob.c::xpandbraces) — finds none here, so the
7140 // string passes through unchanged at this layer. The Bnulls
7141 // are stripped later by remnulargs in prefork.
7142 assert_eq!(
7143 xpandbraces(&tok("\\{a,b,c\\}"), false),
7144 vec![tok("\\{a,b,c\\}")],
7145 "xpandbraces unit: escaped braces survive without expansion (no per-element splat)"
7146 );
7147 }
7148
7149 /// `{a,b}{c,d}` → ac ad bc bd (Cartesian product).
7150 /// 2 * 2 = 4 elements, in row-major order.
7151 #[test]
7152 fn xpandbraces_cartesian_product_two_by_two() {
7153 let _g = crate::test_util::global_state_lock();
7154 assert_eq!(
7155 xpandbraces(&tok("{a,b}{c,d}"), false),
7156 vec!["ac", "ad", "bc", "bd"]
7157 );
7158 }
7159
7160 /// `{a,{b,c}}` → a b c (nested flattened).
7161 #[test]
7162 fn xpandbraces_nested_braces_flatten() {
7163 let _g = crate::test_util::global_state_lock();
7164 assert_eq!(xpandbraces(&tok("{a,{b,c}}"), false), vec!["a", "b", "c"]);
7165 }
7166
7167 /// `{}` → `{}` literal (empty brace doesn't expand). xpandbraces
7168 /// preserves TOKEN form at this layer (c:Src/glob.c::xpandbraces);
7169 /// ASCII conversion happens later in untokenize.
7170 #[test]
7171 fn xpandbraces_empty_braces_remain_literal() {
7172 let _g = crate::test_util::global_state_lock();
7173 assert_eq!(xpandbraces(&tok("{}"), false), vec![tok("{}")]);
7174 }
7175
7176 /// `a{b,c}d{e,f}` → abde abdf acde acdf (cartesian with surrounding text).
7177 #[test]
7178 fn xpandbraces_cartesian_with_surrounding_text() {
7179 let _g = crate::test_util::global_state_lock();
7180 assert_eq!(
7181 xpandbraces(&tok("a{b,c}d{e,f}"), false),
7182 vec!["abde", "abdf", "acde", "acdf"]
7183 );
7184 }
7185
7186 /// `{a..z..3}` → `{a..z..3}` literal (alpha range with step NOT expanded
7187 /// by zsh — surprising but verified). xpandbraces preserves TOKEN form
7188 /// at this layer; ASCII conversion happens later in untokenize.
7189 #[test]
7190 fn xpandbraces_alpha_step_unsupported_anchored_to_zsh() {
7191 let _g = crate::test_util::global_state_lock();
7192 assert_eq!(
7193 xpandbraces(&tok("{a..z..3}"), false),
7194 vec![tok("{a..z..3}")],
7195 "zsh: alpha range with step → literal (TOKEN form preserved at xpandbraces layer)"
7196 );
7197 }
7198
7199 // ═══════════════════════════════════════════════════════════════════
7200 // zsh test-corpus pins — direct anchors to Test/D09brace.ztst.
7201 // Tests verify brace expansion matches zsh's documented output.
7202 // ═══════════════════════════════════════════════════════════════════
7203
7204 /// `Test/D09brace.ztst:10-12` — basic brace expansion with
7205 /// nested range: `X{1,2,{3..6},7,8}Y` expands all 8 values.
7206 #[test]
7207 fn zsh_corpus_basic_brace_with_nested_range() {
7208 let _g = crate::test_util::global_state_lock();
7209 assert_eq!(
7210 xpandbraces(&tok("X{1,2,{3..6},7,8}Y"), false),
7211 vec!["X1Y", "X2Y", "X3Y", "X4Y", "X5Y", "X6Y", "X7Y", "X8Y"],
7212 "ztst:11 — basic brace expansion with nested range",
7213 );
7214 }
7215
7216 /// `Test/D09brace.ztst:30-32` — numeric range with leading zero
7217 /// padding: `X{01..4}Y` → all 4 values 0-padded to 2 digits.
7218 #[test]
7219 fn zsh_corpus_numeric_range_zero_padding() {
7220 let _g = crate::test_util::global_state_lock();
7221 assert_eq!(
7222 xpandbraces(&tok("X{01..4}Y"), false),
7223 vec!["X01Y", "X02Y", "X03Y", "X04Y"],
7224 "ztst:32 — leading-zero padding propagates to all values",
7225 );
7226 }
7227
7228 /// `Test/D09brace.ztst:34-36` — padding comes from RHS too:
7229 /// `X{1..04}Y` → 2-digit zero-padded.
7230 #[test]
7231 fn zsh_corpus_numeric_range_padding_from_rhs() {
7232 let _g = crate::test_util::global_state_lock();
7233 assert_eq!(
7234 xpandbraces(&tok("X{1..04}Y"), false),
7235 vec!["X01Y", "X02Y", "X03Y", "X04Y"],
7236 "ztst:36 — RHS padding `04` propagates",
7237 );
7238 }
7239
7240 /// `Test/D09brace.ztst:38-40` — unpadded range >9: `X{7..12}Y`
7241 /// numbers stay unpadded.
7242 #[test]
7243 fn zsh_corpus_numeric_range_no_padding_when_unspecified() {
7244 let _g = crate::test_util::global_state_lock();
7245 assert_eq!(
7246 xpandbraces(&tok("X{7..12}Y"), false),
7247 vec!["X7Y", "X8Y", "X9Y", "X10Y", "X11Y", "X12Y"],
7248 "ztst:40 — no padding when neither end has leading zero",
7249 );
7250 }
7251
7252 /// `Test/D09brace.ztst:42-44` — `X{07..12}Y` → 2-digit padded.
7253 #[test]
7254 fn zsh_corpus_numeric_range_lhs_padding_propagates() {
7255 let _g = crate::test_util::global_state_lock();
7256 assert_eq!(
7257 xpandbraces(&tok("X{07..12}Y"), false),
7258 vec!["X07Y", "X08Y", "X09Y", "X10Y", "X11Y", "X12Y"],
7259 "ztst:44 — LHS padding `07` propagates",
7260 );
7261 }
7262
7263 /// `Test/D09brace.ztst:46-48` — wider RHS padding overrides:
7264 /// `X{7..012}Y` → 3-digit padded.
7265 #[test]
7266 fn zsh_corpus_numeric_range_max_padding_width_wins() {
7267 let _g = crate::test_util::global_state_lock();
7268 assert_eq!(
7269 xpandbraces(&tok("X{7..012}Y"), false),
7270 vec!["X007Y", "X008Y", "X009Y", "X010Y", "X011Y", "X012Y"],
7271 "ztst:48 — widest padding width wins",
7272 );
7273 }
7274
7275 /// `Test/D09brace.ztst:50-52` — decreasing range: `X{4..1}Y` →
7276 /// 4,3,2,1 (reversed).
7277 #[test]
7278 fn zsh_corpus_numeric_range_decreasing() {
7279 let _g = crate::test_util::global_state_lock();
7280 assert_eq!(
7281 xpandbraces(&tok("X{4..1}Y"), false),
7282 vec!["X4Y", "X3Y", "X2Y", "X1Y"],
7283 "ztst:52 — decreasing range emits in reverse",
7284 );
7285 }
7286
7287 /// `Test/D09brace.ztst:54-56` — combined braces: `X{1..4}{1..4}Y`
7288 /// → 16-element cross product.
7289 #[test]
7290 fn zsh_corpus_combined_braces_cross_product() {
7291 let _g = crate::test_util::global_state_lock();
7292 assert_eq!(
7293 xpandbraces(&tok("X{1..4}{1..4}Y"), false),
7294 vec![
7295 "X11Y", "X12Y", "X13Y", "X14Y", "X21Y", "X22Y", "X23Y", "X24Y", "X31Y", "X32Y",
7296 "X33Y", "X34Y", "X41Y", "X42Y", "X43Y", "X44Y",
7297 ],
7298 "ztst:56 — combined-brace cross product",
7299 );
7300 }
7301
7302 /// `Test/D09brace.ztst:58-60` — negative numbers in range:
7303 /// `X{-4..4}Y` → `-4`..`4` (9 values including 0).
7304 #[test]
7305 fn zsh_corpus_negative_numbers_in_range() {
7306 let _g = crate::test_util::global_state_lock();
7307 assert_eq!(
7308 xpandbraces(&tok("X{-4..4}Y"), false),
7309 vec!["X-4Y", "X-3Y", "X-2Y", "X-1Y", "X0Y", "X1Y", "X2Y", "X3Y", "X4Y",],
7310 "ztst:60 — negative-to-positive range",
7311 );
7312 }
7313
7314 /// `Test/D09brace.ztst:62-64` — reverse-direction numeric range:
7315 /// `X{4..-4}Y` walks from 4 to -4 descending.
7316 #[test]
7317 fn zsh_corpus_brace_descending_range_from_positive_to_negative() {
7318 let _g = crate::test_util::global_state_lock();
7319 assert_eq!(
7320 xpandbraces(&tok("X{4..-4}Y"), false),
7321 vec!["X4Y", "X3Y", "X2Y", "X1Y", "X0Y", "X-1Y", "X-2Y", "X-3Y", "X-4Y",],
7322 "ztst:64 — descending 4..-4 produces 9 values",
7323 );
7324 }
7325
7326 /// `Test/D09brace.ztst:66-68` — stepped padded range:
7327 /// `X{004..-4..2}Y` = X004Y X002Y X000Y X-02Y X-04Y.
7328 /// Padding width 3 from LHS; step is +2 (sign of step is ignored,
7329 /// direction is from start to end).
7330 #[test]
7331 fn zsh_corpus_brace_stepped_padded_descending() {
7332 let _g = crate::test_util::global_state_lock();
7333 assert_eq!(
7334 xpandbraces(&tok("X{004..-4..2}Y"), false),
7335 vec!["X004Y", "X002Y", "X000Y", "X-02Y", "X-04Y"],
7336 "ztst:68 — stepped+padded descending",
7337 );
7338 }
7339
7340 /// `Test/D09brace.ztst:74-76` — step alignment 1: `X{1..32..3}Y`
7341 /// = X1Y X4Y X7Y X10Y X13Y X16Y X19Y X22Y X25Y X28Y X31Y.
7342 /// Step 3 starting from 1, capped at 32.
7343 #[test]
7344 fn zsh_corpus_brace_step_alignment_1_to_32_step_3() {
7345 let _g = crate::test_util::global_state_lock();
7346 assert_eq!(
7347 xpandbraces(&tok("X{1..32..3}Y"), false),
7348 vec![
7349 "X1Y", "X4Y", "X7Y", "X10Y", "X13Y", "X16Y", "X19Y", "X22Y", "X25Y", "X28Y",
7350 "X31Y",
7351 ],
7352 "ztst:76 — {{1..32..3}} step alignment",
7353 );
7354 }
7355
7356 /// `Test/D09brace.ztst:100-102` — `hey{a..j}there` — char range.
7357 #[test]
7358 fn zsh_corpus_brace_char_range_simple() {
7359 let _g = crate::test_util::global_state_lock();
7360 assert_eq!(
7361 xpandbraces(&tok("hey{a..j}there"), false),
7362 vec![
7363 "heyathere",
7364 "heybthere",
7365 "heycthere",
7366 "heydthere",
7367 "heyethere",
7368 "heyfthere",
7369 "heygthere",
7370 "heyhthere",
7371 "heyithere",
7372 "heyjthere",
7373 ],
7374 "ztst:102 — char range a..j",
7375 );
7376 }
7377
7378 /// `Test/D09brace.ztst:108-110` — reverse char range:
7379 /// `crumbs{y..p}ooh` walks down y → p (10 values).
7380 #[test]
7381 fn zsh_corpus_brace_char_range_reverse() {
7382 let _g = crate::test_util::global_state_lock();
7383 assert_eq!(
7384 xpandbraces(&tok("crumbs{y..p}ooh"), false),
7385 vec![
7386 "crumbsyooh",
7387 "crumbsxooh",
7388 "crumbswooh",
7389 "crumbsvooh",
7390 "crumbsuooh",
7391 "crumbstooh",
7392 "crumbssooh",
7393 "crumbsrooh",
7394 "crumbsqooh",
7395 "crumbspooh",
7396 ],
7397 "ztst:110 — char range y..p reverse",
7398 );
7399 }
7400
7401 /// `Test/D09brace.ztst:104-106` — nested brace: `gosh{1,{Z..a},2}cripes`
7402 /// — inner `{Z..a}` is char range Z to a in ASCII order (10 chars).
7403 /// Full output: gosh1cripes goshZcripes gosh[cripes gosh\cripes
7404 /// gosh]cripes gosh^cripes gosh_cripes gosh`cripes goshacripes
7405 /// gosh2cripes.
7406 #[test]
7407 fn zsh_corpus_brace_nested_with_char_range_ascii() {
7408 let _g = crate::test_util::global_state_lock();
7409 assert_eq!(
7410 xpandbraces(&tok("gosh{1,{Z..a},2}cripes"), false),
7411 vec![
7412 "gosh1cripes",
7413 "goshZcripes",
7414 "gosh[cripes",
7415 "gosh\\cripes",
7416 "gosh]cripes",
7417 "gosh^cripes",
7418 "gosh_cripes",
7419 "gosh`cripes",
7420 "goshacripes",
7421 "gosh2cripes",
7422 ],
7423 "ztst:106 — nested brace with ASCII char range",
7424 );
7425 }
7426
7427 /// `Test/D09brace.ztst:116-118` — unmatched closing brace after
7428 /// matched braces stays literal: `{1..10}{..` → `1{.. 2{.. ...`.
7429 /// `xpandbraces` itself preserves TOKEN form (Inbrace=`\u{8f}`,
7430 /// Outbrace=`\u{90}`) per the convention pinned by
7431 /// `xpandbraces_alpha_step_unsupported_anchored_to_zsh` — the
7432 /// later untokenize pass in the user-output pipeline turns
7433 /// surviving brace tokens into literal `{` / `}`. This test
7434 /// asserts the tokenized intermediate form.
7435 #[test]
7436 fn zsh_corpus_brace_unmatched_after_matched_left_literal() {
7437 let _g = crate::test_util::global_state_lock();
7438 let tokb = '\u{8f}'; // Inbrace
7439 let expected: Vec<String> = (1..=10).map(|n| format!("{}{}..", n, tokb)).collect();
7440 assert_eq!(
7441 xpandbraces(&tok("{1..10}{.."), false),
7442 expected,
7443 "ztst:118 — unmatched trailing {{.. preserved as tokenized Inbrace at xpandbraces layer",
7444 );
7445 }
7446
7447 // ── split_qualifier: parse trailing (qual) block ─────────────────
7448 /// `*.txt` (no trailing parens) → (input, None).
7449 #[test]
7450 fn split_qualifier_no_parens_returns_input_and_none() {
7451 let _g = crate::test_util::global_state_lock();
7452 let (head, qual) = split_qualifier("*.txt");
7453 assert_eq!(head, "*.txt");
7454 assert_eq!(qual, None);
7455 }
7456
7457 /// `*(N)` → ("*", Some("N")) — null-glob qualifier.
7458 #[test]
7459 fn split_qualifier_star_paren_N_extracts_null_glob_qual() {
7460 let _g = crate::test_util::global_state_lock();
7461 let (head, qual) = split_qualifier("*(N)");
7462 assert_eq!(head, "*");
7463 assert_eq!(qual, Some("N"));
7464 }
7465
7466 /// `*(.)` → ("*", Some(".")) — regular-file qualifier.
7467 #[test]
7468 fn split_qualifier_star_paren_dot_extracts_regular_file_qual() {
7469 let _g = crate::test_util::global_state_lock();
7470 let (head, qual) = split_qualifier("*(.)");
7471 assert_eq!(head, "*");
7472 assert_eq!(qual, Some("."));
7473 }
7474
7475 /// `*.rs(.)` → ("*.rs", Some(".")) — qualifier on glob pattern.
7476 #[test]
7477 fn split_qualifier_pattern_with_qual_extracts_correctly() {
7478 let _g = crate::test_util::global_state_lock();
7479 let (head, qual) = split_qualifier("*.rs(.)");
7480 assert_eq!(head, "*.rs");
7481 assert_eq!(qual, Some("."));
7482 }
7483
7484 /// `*(#qN)` → ("*", Some("N")) — `#q` prefix stripped per zsh syntax.
7485 #[test]
7486 fn split_qualifier_hash_q_prefix_stripped() {
7487 let _g = crate::test_util::global_state_lock();
7488 let (head, qual) = split_qualifier("*(#qN)");
7489 assert_eq!(head, "*");
7490 assert_eq!(qual, Some("N"));
7491 }
7492
7493 /// `*(om[1])` → ("*", Some("om[1]")) — sort-by-mtime keep first.
7494 #[test]
7495 fn split_qualifier_multichar_qual_with_brackets() {
7496 let _g = crate::test_util::global_state_lock();
7497 let (head, qual) = split_qualifier("*(om[1])");
7498 assert_eq!(head, "*");
7499 assert_eq!(qual, Some("om[1]"));
7500 }
7501
7502 /// `(a)(b)` — multiple paren groups: split takes the OUTERMOST trailing.
7503 #[test]
7504 fn split_qualifier_multiple_groups_takes_outermost_trailing() {
7505 let _g = crate::test_util::global_state_lock();
7506 let (head, qual) = split_qualifier("(a)(b)");
7507 assert_eq!(head, "(a)");
7508 assert_eq!(qual, Some("b"));
7509 }
7510
7511 // ═══════════════════════════════════════════════════════════════════
7512 // parse_qualifier_string — file-type / permission / sort qualifiers.
7513 // Tests call the private fn directly (same crate, same file).
7514 // Each qualifier letter maps to a `qualifier::*` variant per the
7515 // zsh glob.c arm at c:1495+. Pin the parse, not the match — the
7516 // match path needs a real filesystem.
7517 //
7518 // IMPORTANT: at end of parse_qualifier_string (c:3433-3435 in this
7519 // file), if `qualifiers` is non-empty it gets moved into
7520 // `alternatives[]`. So tests read the qualifier from `alternatives[0]`
7521 // — qualifiers itself is empty after the move.
7522 // ═══════════════════════════════════════════════════════════════════
7523
7524 /// Helper: read the only qualifier from a single-letter parse result.
7525 fn first_qual(qs: &qualifier_set) -> &qualifier {
7526 // After the post-loop move, single-letter input lands in
7527 // alternatives[0][0]. Fall back to qualifiers[0] for safety in
7528 // case the move ever changes.
7529 if !qs.alternatives.is_empty() && !qs.alternatives[0].is_empty() {
7530 &qs.alternatives[0][0]
7531 } else {
7532 &qs.qualifiers[0]
7533 }
7534 }
7535
7536 /// Empty qualifier body → no alternatives, no qualifiers.
7537 #[test]
7538 fn parse_qualifier_string_empty_returns_empty_set() {
7539 let _g = crate::test_util::global_state_lock();
7540 let qs = parse_qualifier_string("");
7541 assert!(qs.qualifiers.is_empty());
7542 assert!(qs.alternatives.is_empty());
7543 assert!(!qs.nullglob);
7544 }
7545
7546 /// `/` → IsDirectory (in alternatives[0]).
7547 #[test]
7548 fn parse_qualifier_string_slash_is_directory() {
7549 let _g = crate::test_util::global_state_lock();
7550 let qs = parse_qualifier_string("/");
7551 assert!(matches!(first_qual(&qs), qualifier::IsDirectory));
7552 }
7553
7554 /// `.` → IsRegular.
7555 #[test]
7556 fn parse_qualifier_string_dot_is_regular() {
7557 let _g = crate::test_util::global_state_lock();
7558 let qs = parse_qualifier_string(".");
7559 assert!(matches!(first_qual(&qs), qualifier::IsRegular));
7560 }
7561
7562 /// `parsecomplist` splits a path into per-component `complist` nodes,
7563 /// each literal section a PAT_PURES Patprog (relies on patcompile's
7564 /// PAT_FILE PURES segment-stop at `/`, pattern.c:584-610).
7565 #[test]
7566 fn parsecomplist_splits_path_components() {
7567 let _g = crate::test_util::global_state_lock();
7568 fn pure(p: &crate::ported::pattern::Patprog) -> Option<String> {
7569 if (p.0.flags & crate::ported::zsh_h::PAT_PURES as i32) != 0 {
7570 let off = p.0.startoff as usize;
7571 let l = p.0.patmlen as usize;
7572 p.1.get(off..off + l)
7573 .map(|b| String::from_utf8_lossy(b).into_owned())
7574 } else {
7575 None
7576 }
7577 }
7578 fn sections(mut q: &complist) -> Vec<String> {
7579 let mut out = Vec::new();
7580 loop {
7581 out.push(pure(&q.pat).unwrap_or_else(|| "<pat>".into()));
7582 match &q.next {
7583 Some(n) => q = n,
7584 None => break,
7585 }
7586 }
7587 out
7588 }
7589 // Literal path → split into per-component PAT_PURES sections.
7590 let mut t = "a/b/c.txt".to_string();
7591 tokenize(&mut t);
7592 let cl = parsecomplist(&t).expect("parsecomplist None");
7593 assert_eq!(sections(&cl), vec!["a", "b", "c.txt"]);
7594
7595 // Literal prefix before a pattern still splits the literal dirs;
7596 // the trailing glob section is a real pattern (not PAT_PURES).
7597 let mut t2 = "a/b/*.txt".to_string();
7598 tokenize(&mut t2);
7599 let cl2 = parsecomplist(&t2).expect("parsecomplist None");
7600 let s2 = sections(&cl2);
7601 assert_eq!(s2.len(), 3, "a/b/*.txt → 3 sections, got {s2:?}");
7602 assert_eq!(&s2[..2], &["a".to_string(), "b".to_string()]);
7603 assert_eq!(s2[2], "<pat>", "trailing *.txt must be a pattern section");
7604
7605 // Pattern-FIRST then literal dir: */foo → [<pat>, foo].
7606 let mut t3 = "*/foo".to_string();
7607 tokenize(&mut t3);
7608 let cl3 = parsecomplist(&t3).expect("parsecomplist None for */foo");
7609 let s3 = sections(&cl3);
7610 assert_eq!(s3.len(), 2, "*/foo → 2 sections, got {s3:?}");
7611 assert_eq!(s3[0], "<pat>", "leading * must be a pattern section");
7612 assert_eq!(s3[1], "foo");
7613 }
7614
7615 /// The `struct qual` arena is built from the parse alongside the enum:
7616 /// AND via `next`, alternatives via `or`, per-node `sense` from `^`.
7617 #[test]
7618 fn parse_qualifier_string_builds_qual_arena() {
7619 let _g = crate::test_util::global_state_lock();
7620 // single qualifier → one node, sense 0, head set, func present.
7621 let q = parse_qualifier_string(".");
7622 assert_eq!(q.quals.nodes.len(), 1);
7623 assert_eq!(q.quals.head, Some(0));
7624 assert_eq!(q.quals.nodes[0].sense, 0);
7625 assert!(q.quals.nodes[0].func.is_some());
7626 assert_eq!(q.quals.nodes[0].next, None);
7627
7628 // leading `^` → per-node sense flipped (c:1346).
7629 let q = parse_qualifier_string("^.");
7630 assert_eq!(q.quals.nodes.len(), 1);
7631 assert_eq!(q.quals.nodes[0].sense, 1);
7632
7633 // `/^@` → AND-chain of 2: dir(sense 0) -next-> symlink(sense 1).
7634 let q = parse_qualifier_string("/^@");
7635 assert_eq!(q.quals.nodes.len(), 2);
7636 assert_eq!(q.quals.nodes[0].sense, 0);
7637 assert_eq!(q.quals.nodes[0].next, Some(1));
7638 assert_eq!(q.quals.nodes[1].sense, 1);
7639 assert_eq!(q.quals.nodes[1].next, None);
7640
7641 // `.,/` → two alternatives: node0 -or-> node1, neither chained.
7642 let q = parse_qualifier_string(".,/");
7643 assert_eq!(q.quals.nodes.len(), 2);
7644 assert_eq!(q.quals.nodes[0].or, Some(1));
7645 assert_eq!(q.quals.nodes[0].next, None);
7646 assert_eq!(q.quals.head, Some(0));
7647 }
7648
7649 /// `@` → IsSymlink.
7650 #[test]
7651 fn parse_qualifier_string_at_is_symlink() {
7652 let _g = crate::test_util::global_state_lock();
7653 let qs = parse_qualifier_string("@");
7654 assert!(matches!(first_qual(&qs), qualifier::IsSymlink));
7655 }
7656
7657 /// `=` → IsSocket.
7658 #[test]
7659 fn parse_qualifier_string_equals_is_socket() {
7660 let _g = crate::test_util::global_state_lock();
7661 let qs = parse_qualifier_string("=");
7662 assert!(matches!(first_qual(&qs), qualifier::IsSocket));
7663 }
7664
7665 /// `p` → IsFifo.
7666 #[test]
7667 fn parse_qualifier_string_p_is_fifo() {
7668 let _g = crate::test_util::global_state_lock();
7669 let qs = parse_qualifier_string("p");
7670 assert!(matches!(first_qual(&qs), qualifier::IsFifo));
7671 }
7672
7673 /// `*` → IsExecutable.
7674 #[test]
7675 fn parse_qualifier_string_star_is_executable() {
7676 let _g = crate::test_util::global_state_lock();
7677 let qs = parse_qualifier_string("*");
7678 assert!(matches!(first_qual(&qs), qualifier::IsExecutable));
7679 }
7680
7681 /// `%b` → IsBlockDev.
7682 #[test]
7683 fn parse_qualifier_string_pct_b_is_block_device() {
7684 let _g = crate::test_util::global_state_lock();
7685 let qs = parse_qualifier_string("%b");
7686 assert!(matches!(first_qual(&qs), qualifier::IsBlockDev));
7687 }
7688
7689 /// `%c` → IsCharDev.
7690 #[test]
7691 fn parse_qualifier_string_pct_c_is_char_device() {
7692 let _g = crate::test_util::global_state_lock();
7693 let qs = parse_qualifier_string("%c");
7694 assert!(matches!(first_qual(&qs), qualifier::IsCharDev));
7695 }
7696
7697 /// `r` / `w` / `x` → Readable / Writable / Executable.
7698 #[test]
7699 fn parse_qualifier_string_perm_letters_map_to_perm_qualifiers() {
7700 let _g = crate::test_util::global_state_lock();
7701 for (letter, expected) in [
7702 ("r", qualifier::Readable),
7703 ("w", qualifier::Writable),
7704 ("x", qualifier::Executable),
7705 ] {
7706 let qs = parse_qualifier_string(letter);
7707 assert_eq!(
7708 std::mem::discriminant(first_qual(&qs)),
7709 std::mem::discriminant(&expected),
7710 "letter {letter:?} should map to {expected:?}"
7711 );
7712 }
7713 }
7714
7715 /// `U` → OwnedByEuid (process EUID).
7716 #[test]
7717 fn parse_qualifier_string_capital_U_is_owned_by_euid() {
7718 let _g = crate::test_util::global_state_lock();
7719 let qs = parse_qualifier_string("U");
7720 assert!(matches!(first_qual(&qs), qualifier::OwnedByEuid));
7721 }
7722
7723 /// `G` → OwnedByEgid.
7724 #[test]
7725 fn parse_qualifier_string_capital_G_is_owned_by_egid() {
7726 let _g = crate::test_util::global_state_lock();
7727 let qs = parse_qualifier_string("G");
7728 assert!(matches!(first_qual(&qs), qualifier::OwnedByEgid));
7729 }
7730
7731 /// Multiple file-type qualifiers stack — `./` → IsRegular then IsDirectory
7732 /// both end up in alternatives[0].
7733 #[test]
7734 fn parse_qualifier_string_multiple_letters_stack_in_one_alternative() {
7735 let _g = crate::test_util::global_state_lock();
7736 let qs = parse_qualifier_string("./");
7737 assert_eq!(qs.alternatives.len(), 1);
7738 assert_eq!(qs.alternatives[0].len(), 2);
7739 assert!(matches!(qs.alternatives[0][0], qualifier::IsRegular));
7740 assert!(matches!(qs.alternatives[0][1], qualifier::IsDirectory));
7741 }
7742
7743 /// `,` separates alternatives — `/,.` → two alternatives, each with one
7744 /// qualifier (IsDirectory and IsRegular respectively).
7745 #[test]
7746 fn parse_qualifier_string_comma_creates_two_alternatives() {
7747 let _g = crate::test_util::global_state_lock();
7748 let qs = parse_qualifier_string("/,.");
7749 assert_eq!(qs.alternatives.len(), 2, "two alternatives expected");
7750 assert!(matches!(qs.alternatives[0][0], qualifier::IsDirectory));
7751 assert!(matches!(qs.alternatives[1][0], qualifier::IsRegular));
7752 }
7753
7754 /// `:` introduces colon-modifiers; rest captured into colon_mods.
7755 #[test]
7756 fn parse_qualifier_string_colon_captures_modifiers_in_field() {
7757 let _g = crate::test_util::global_state_lock();
7758 let qs = parse_qualifier_string(":h");
7759 assert_eq!(qs.colon_mods.as_deref(), Some(":h"));
7760 }
7761
7762 /// `:` after a qualifier captures BOTH.
7763 #[test]
7764 fn parse_qualifier_string_qualifier_then_colon_mod() {
7765 let _g = crate::test_util::global_state_lock();
7766 let qs = parse_qualifier_string("/:h");
7767 assert!(matches!(first_qual(&qs), qualifier::IsDirectory));
7768 assert_eq!(qs.colon_mods.as_deref(), Some(":h"));
7769 }
7770
7771 /// `N` flag → nullglob set, no qualifier pushed.
7772 #[test]
7773 fn parse_qualifier_string_capital_N_sets_nullglob_flag() {
7774 let _g = crate::test_util::global_state_lock();
7775 let qs = parse_qualifier_string("N");
7776 assert!(qs.nullglob, "(N) must set nullglob");
7777 assert!(qs.alternatives.is_empty(), "N doesn't push to qualifiers");
7778 }
7779
7780 /// `M` flag → mark_dirs.
7781 #[test]
7782 fn parse_qualifier_string_capital_M_sets_mark_dirs_flag() {
7783 let _g = crate::test_util::global_state_lock();
7784 let qs = parse_qualifier_string("M");
7785 assert!(qs.mark_dirs, "(M) must set mark_dirs");
7786 }
7787
7788 /// `T` flag → list_types.
7789 #[test]
7790 fn parse_qualifier_string_capital_T_sets_list_types_flag() {
7791 let _g = crate::test_util::global_state_lock();
7792 let qs = parse_qualifier_string("T");
7793 assert!(qs.list_types, "(T) must set list_types");
7794 }
7795
7796 // ── split_qualifier integration with parse_qualifier_string ──────
7797 /// `*(.)` — split yields ("*", Some(".")); parse yields IsRegular.
7798 #[test]
7799 fn split_then_parse_dot_qualifier_yields_is_regular() {
7800 let _g = crate::test_util::global_state_lock();
7801 let (head, qual) = split_qualifier("*(.)");
7802 assert_eq!(head, "*");
7803 let qs = parse_qualifier_string(qual.unwrap());
7804 assert!(matches!(first_qual(&qs), qualifier::IsRegular));
7805 }
7806
7807 /// `*(/)` — directory-only glob.
7808 #[test]
7809 fn split_then_parse_slash_qualifier_yields_is_directory() {
7810 let _g = crate::test_util::global_state_lock();
7811 let (_, qual) = split_qualifier("*(/)");
7812 let qs = parse_qualifier_string(qual.unwrap());
7813 assert!(matches!(first_qual(&qs), qualifier::IsDirectory));
7814 }
7815
7816 // ═══════════════════════════════════════════════════════════════════
7817 // Sort qualifiers — `o<key>` ascending, `O<key>` descending.
7818 // ═══════════════════════════════════════════════════════════════════
7819
7820 /// `oN` — sort by NONE (no sorting key requested).
7821 #[test]
7822 fn parse_qualifier_string_oN_pushes_gs_none() {
7823 let _g = crate::test_util::global_state_lock();
7824 let qs = parse_qualifier_string("oN");
7825 assert_eq!(qs.sorts.len(), 1);
7826 assert_ne!(qs.sorts[0] & GS_NONE, 0, "oN must set GS_NONE bit");
7827 }
7828
7829 /// `on` — sort by name ascending.
7830 #[test]
7831 fn parse_qualifier_string_on_pushes_gs_name_ascending() {
7832 let _g = crate::test_util::global_state_lock();
7833 let qs = parse_qualifier_string("on");
7834 assert_eq!(qs.sorts.len(), 1);
7835 assert_eq!(qs.sorts[0] & GS_NAME, GS_NAME);
7836 assert_eq!(qs.sorts[0] & GS_DESC, 0);
7837 }
7838
7839 /// `On` — sort by name descending.
7840 #[test]
7841 fn parse_qualifier_string_On_pushes_gs_name_descending() {
7842 let _g = crate::test_util::global_state_lock();
7843 let qs = parse_qualifier_string("On");
7844 assert_eq!(qs.sorts.len(), 1);
7845 assert_eq!(qs.sorts[0] & GS_NAME, GS_NAME);
7846 assert_eq!(qs.sorts[0] & GS_DESC, GS_DESC);
7847 }
7848
7849 /// Multiple sort keys stack.
7850 #[test]
7851 fn parse_qualifier_string_chained_sort_keys_stack() {
7852 let _g = crate::test_util::global_state_lock();
7853 let qs = parse_qualifier_string("onOL");
7854 assert_eq!(qs.sorts.len(), 2);
7855 }
7856
7857 // ── Size qualifier — `L<unit><op><value>` ───────────────────────
7858 // NOTE: zshrs's parse_range_spec normalises `+` (greater) → '>' and
7859 // `-` (less) → '<' for the internal op char, matching C's qgetnum
7860 // convention (Src/glob.c c:1620-ish).
7861
7862 /// `Lk+1` — size > 1 kilobyte. zsh syntax: `L<unit><op><value>`
7863 /// (unit char BEFORE op).
7864 #[test]
7865 fn parse_qualifier_string_Lk_plus_one_size_kilobyte() {
7866 let _g = crate::test_util::global_state_lock();
7867 let qs = parse_qualifier_string("Lk+1");
7868 match first_qual(&qs) {
7869 qualifier::Size { value, unit, op } => {
7870 assert_eq!(*value, 1);
7871 assert_eq!(*unit, TT_KILOBYTES);
7872 assert_eq!(*op, '>', "+ is stored as '>' (greater than)");
7873 }
7874 other => panic!("expected Size, got {other:?}"),
7875 }
7876 }
7877
7878 /// `L-100` — size < 100 (default unit bytes). Stored op is '<'.
7879 #[test]
7880 fn parse_qualifier_string_L_minus_100_size_bytes() {
7881 let _g = crate::test_util::global_state_lock();
7882 let qs = parse_qualifier_string("L-100");
7883 match first_qual(&qs) {
7884 qualifier::Size { value, unit, op } => {
7885 assert_eq!(*value, 100);
7886 assert_eq!(*unit, TT_BYTES);
7887 assert_eq!(*op, '<', "- is stored as '<' (less than)");
7888 }
7889 other => panic!("expected Size, got {other:?}"),
7890 }
7891 }
7892
7893 /// `Lm+1` — size > 1 megabyte; unit recognised.
7894 #[test]
7895 fn parse_qualifier_string_Lm_megabyte_unit() {
7896 let _g = crate::test_util::global_state_lock();
7897 let qs = parse_qualifier_string("Lm+1");
7898 match first_qual(&qs) {
7899 qualifier::Size { unit, .. } => assert_eq!(*unit, TT_MEGABYTES),
7900 other => panic!("expected Size, got {other:?}"),
7901 }
7902 }
7903
7904 // ── Time qualifier — same op normalisation ───────────────────────
7905 /// `m-1` — modified less than 1 day ago. Stored op '<'.
7906 #[test]
7907 fn parse_qualifier_string_m_minus_1_day_default_unit() {
7908 let _g = crate::test_util::global_state_lock();
7909 let qs = parse_qualifier_string("m-1");
7910 match first_qual(&qs) {
7911 qualifier::Mtime { value, unit, op } => {
7912 assert_eq!(*value, 1);
7913 assert_eq!(*unit, TT_DAYS);
7914 assert_eq!(*op, '<');
7915 }
7916 other => panic!("expected Mtime, got {other:?}"),
7917 }
7918 }
7919
7920 /// `mh+24` — modified more than 24 hours ago. Stored op '>'.
7921 #[test]
7922 fn parse_qualifier_string_mh_plus_24_hours() {
7923 let _g = crate::test_util::global_state_lock();
7924 let qs = parse_qualifier_string("mh+24");
7925 match first_qual(&qs) {
7926 qualifier::Mtime { value, unit, op } => {
7927 assert_eq!(*value, 24);
7928 assert_eq!(*unit, TT_HOURS);
7929 assert_eq!(*op, '>');
7930 }
7931 other => panic!("expected Mtime, got {other:?}"),
7932 }
7933 }
7934
7935 /// `a-7` — accessed less than 7 days ago. Stored op '<'.
7936 #[test]
7937 fn parse_qualifier_string_a_minus_7_days() {
7938 let _g = crate::test_util::global_state_lock();
7939 let qs = parse_qualifier_string("a-7");
7940 match first_qual(&qs) {
7941 qualifier::Atime { value, unit, op } => {
7942 assert_eq!(*value, 7);
7943 assert_eq!(*unit, TT_DAYS);
7944 assert_eq!(*op, '<');
7945 }
7946 other => panic!("expected Atime, got {other:?}"),
7947 }
7948 }
7949
7950 // ── Subscript qualifier — `[N]` keep first ───────────────────────
7951 /// `[1]` — first entry only.
7952 #[test]
7953 fn parse_qualifier_string_bracket_one_sets_first_to_one() {
7954 let _g = crate::test_util::global_state_lock();
7955 let qs = parse_qualifier_string("[1]");
7956 assert_eq!(qs.first, Some(1));
7957 }
7958
7959 /// `om[1]` — sort by mtime, keep first.
7960 #[test]
7961 fn parse_qualifier_string_om_bracket_one_sort_and_subscript() {
7962 let _g = crate::test_util::global_state_lock();
7963 let qs = parse_qualifier_string("om[1]");
7964 assert_eq!(qs.sorts.len(), 1);
7965 assert_eq!(qs.sorts[0] & GS_MTIME, GS_MTIME);
7966 assert_eq!(qs.first, Some(1));
7967 }
7968
7969 // ═══════════════════════════════════════════════════════════════════
7970 // tokenize / remnulargs C-parity tests — pin Src/glob.c:3551 (tokenize)
7971 // and Src/glob.c:3649 (remnulargs). These two functions form the
7972 // input → glob-token → output sandwich every glob pattern walks
7973 // through; regressions silently corrupt every pattern match.
7974 // Tests that capture KNOWN ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"].
7975 // ═══════════════════════════════════════════════════════════════════
7976
7977 /// `tokenize("abc")` is a no-op for plain ASCII — no glob metachars.
7978 /// C glob.c:3551 — `for (; (c = *s); s++) zstrtok(...)` — only
7979 /// recognized glob bytes get replaced with their token form.
7980 #[test]
7981 fn tokenize_pure_ascii_unchanged() {
7982 let _g = crate::test_util::global_state_lock();
7983 let mut s = String::from("abc");
7984 tokenize(&mut s);
7985 assert_eq!(s, "abc");
7986 }
7987
7988 /// `tokenize("")` on empty input returns empty unchanged.
7989 #[test]
7990 fn tokenize_empty_unchanged() {
7991 let _g = crate::test_util::global_state_lock();
7992 let mut s = String::new();
7993 tokenize(&mut s);
7994 assert_eq!(s, "");
7995 }
7996
7997 /// `remnulargs("abc")` is a no-op for pure ASCII (no inull bytes).
7998 /// C glob.c:3649-3680 — only Snull/Dnull/Bnull/Bnullkeep/Nularg
7999 /// are stripped or transformed.
8000 #[test]
8001 fn remnulargs_pure_ascii_unchanged() {
8002 let _g = crate::test_util::global_state_lock();
8003 let mut s = String::from("hello");
8004 remnulargs(&mut s);
8005 assert_eq!(s, "hello");
8006 }
8007
8008 /// `remnulargs("")` returns empty unchanged (c:3653 early exit).
8009 #[test]
8010 fn remnulargs_empty_returns_empty() {
8011 let _g = crate::test_util::global_state_lock();
8012 let mut s = String::new();
8013 remnulargs(&mut s);
8014 assert_eq!(s, "");
8015 }
8016
8017 /// `remnulargs` strips Snull (single-quote scope marker).
8018 /// C glob.c:3656 — inull predicate includes Snull (0x84).
8019 /// Input `"a\u{84}b"` → `"ab"` (strip the Snull byte).
8020 #[test]
8021 fn remnulargs_strips_snull_marker() {
8022 let _g = crate::test_util::global_state_lock();
8023 let mut s = format!("a{}b", crate::ported::zsh_h::Snull);
8024 remnulargs(&mut s);
8025 assert_eq!(s, "ab", "Snull byte should be stripped");
8026 }
8027
8028 /// `remnulargs` strips Dnull (double-quote scope marker).
8029 #[test]
8030 fn remnulargs_strips_dnull_marker() {
8031 let _g = crate::test_util::global_state_lock();
8032 let mut s = format!("a{}b", crate::ported::zsh_h::Dnull);
8033 remnulargs(&mut s);
8034 assert_eq!(s, "ab", "Dnull byte should be stripped");
8035 }
8036
8037 /// `remnulargs` strips Bnull (backslash-quoted marker — c:3658
8038 /// `inull(c)` includes Bnull). The non-Bnullkeep variant of the
8039 /// active-backslash marker.
8040 #[test]
8041 fn remnulargs_strips_bnull_marker() {
8042 let _g = crate::test_util::global_state_lock();
8043 let mut s = format!("a{}b", crate::ported::zsh_h::Bnull);
8044 remnulargs(&mut s);
8045 assert_eq!(s, "ab", "Bnull byte should be stripped");
8046 }
8047
8048 /// `remnulargs` on a single Nularg returns the Nularg sentinel
8049 /// preserved. C glob.c:3690-3692 — if post-strip output is empty
8050 /// AND original had any inull, emit Nularg as the empty-arg marker.
8051 #[test]
8052 fn remnulargs_empty_post_strip_emits_nularg_sentinel() {
8053 let _g = crate::test_util::global_state_lock();
8054 let mut s = format!("{}", crate::ported::zsh_h::Snull);
8055 remnulargs(&mut s);
8056 assert_eq!(
8057 s,
8058 format!("{}", crate::ported::zsh_h::Nularg),
8059 "all-inull input should collapse to single Nularg marker"
8060 );
8061 }
8062
8063 /// `remnulargs` on the Bnullkeep marker: c:3669 — Bnullkeep
8064 /// either (a) preserved as-is when it appears BEFORE any other
8065 /// inull (scan phase), or (b) transformed to literal `\` (copy
8066 /// phase). Pin the simpler case: a lone Bnullkeep is stripped.
8067 #[test]
8068 fn remnulargs_lone_bnullkeep_stripped() {
8069 let _g = crate::test_util::global_state_lock();
8070 let mut s = format!("a{}b", crate::ported::zsh_h::Bnullkeep);
8071 remnulargs(&mut s);
8072 // C behavior depends on whether the byte is in scan or copy
8073 // phase. Faithful port should give either "ab" (scan strip)
8074 // or "a\\b" (copy phase materialize). Either is acceptable;
8075 // pin that it's NOT the raw input.
8076 assert!(
8077 s == "ab" || s == "a\\b",
8078 "Bnullkeep should be stripped or materialized; got {s:?}"
8079 );
8080 }
8081
8082 // ═══════════════════════════════════════════════════════════════════
8083 // Additional C-parity tests for Src/glob.c file_type + hasbraces.
8084 // ═══════════════════════════════════════════════════════════════════
8085
8086 /// c:2018 — file_type for regular file with no exec bits → ' '.
8087 #[test]
8088 #[cfg(unix)]
8089 fn file_type_regular_non_exec_returns_space() {
8090 let r = file_type(libc::S_IFREG as u32 | 0o644);
8091 assert_eq!(r, ' ');
8092 }
8093
8094 /// c:2018 — file_type for regular file WITH exec bit → '*'.
8095 #[test]
8096 #[cfg(unix)]
8097 fn file_type_regular_exec_returns_star() {
8098 let r = file_type(libc::S_IFREG as u32 | 0o755);
8099 assert_eq!(r, '*');
8100 }
8101
8102 /// c:2018 — file_type for directory → '/'.
8103 #[test]
8104 #[cfg(unix)]
8105 fn file_type_directory_returns_slash() {
8106 let r = file_type(libc::S_IFDIR as u32 | 0o755);
8107 assert_eq!(r, '/');
8108 }
8109
8110 /// c:2018 — file_type for symlink → '@'.
8111 #[test]
8112 #[cfg(unix)]
8113 fn file_type_symlink_returns_at() {
8114 let r = file_type(libc::S_IFLNK as u32 | 0o777);
8115 assert_eq!(r, '@');
8116 }
8117
8118 /// c:2018 — file_type for FIFO → '|'.
8119 #[test]
8120 #[cfg(unix)]
8121 fn file_type_fifo_returns_pipe() {
8122 let r = file_type(libc::S_IFIFO as u32 | 0o644);
8123 assert_eq!(r, '|');
8124 }
8125
8126 /// c:2018 — file_type for socket → '='.
8127 #[test]
8128 #[cfg(unix)]
8129 fn file_type_socket_returns_equals() {
8130 let r = file_type(libc::S_IFSOCK as u32 | 0o777);
8131 assert_eq!(r, '=');
8132 }
8133
8134 /// c:2018 — file_type for char device → '%'.
8135 #[test]
8136 #[cfg(unix)]
8137 fn file_type_char_device_returns_percent() {
8138 let r = file_type(libc::S_IFCHR as u32 | 0o644);
8139 assert_eq!(r, '%');
8140 }
8141
8142 /// c:2018 — file_type for block device → '#'.
8143 #[test]
8144 #[cfg(unix)]
8145 fn file_type_block_device_returns_hash() {
8146 let r = file_type(libc::S_IFBLK as u32 | 0o644);
8147 assert_eq!(r, '#');
8148 }
8149
8150 /// c:2018 — file_type for unknown mode → '?'.
8151 #[test]
8152 fn file_type_unknown_mode_returns_question() {
8153 // Bare 0 mode (none of the S_IF* bits set) → '?'.
8154 assert_eq!(file_type(0), '?');
8155 }
8156
8157 /// c:2018 — file_type is deterministic.
8158 #[test]
8159 #[cfg(unix)]
8160 fn file_type_is_deterministic() {
8161 for mode in [
8162 libc::S_IFREG as u32 | 0o644,
8163 libc::S_IFDIR as u32,
8164 libc::S_IFLNK as u32,
8165 0u32,
8166 ] {
8167 let first = file_type(mode);
8168 for _ in 0..5 {
8169 assert_eq!(file_type(mode), first);
8170 }
8171 }
8172 }
8173
8174 /// c:2042 — hasbraces on plain string returns false.
8175 #[test]
8176 fn hasbraces_no_braces_returns_false() {
8177 let _g = crate::test_util::global_state_lock();
8178 assert!(!hasbraces("hello", false));
8179 assert!(!hasbraces("", false));
8180 }
8181
8182 /// c:2042 — hasbraces on plain ASCII `{a,b}` returns FALSE because
8183 /// the C port checks TOKEN bytes (Inbrace=0x8f / Outbrace=0x90 /
8184 /// Comma=0x9a) not literal ASCII. The lexer pre-tokenizes input
8185 /// before hasbraces runs in the real pipeline. Pin the actual
8186 /// behavior so a regen that adds ASCII fast-path is caught.
8187 #[test]
8188 fn hasbraces_plain_ascii_braces_returns_false_uses_tokens() {
8189 let _g = crate::test_util::global_state_lock();
8190 assert!(
8191 !hasbraces("{a,b}", false),
8192 "plain ASCII not tokenized → false; lexer pre-tokenizes in real pipeline"
8193 );
8194 }
8195
8196 /// c:2042 — hasbraces on tokenized brace expansion returns true.
8197 /// Construct the TOKEN form: Inbrace + 'a' + Comma + 'b' + Outbrace.
8198 #[test]
8199 fn hasbraces_tokenized_brace_expansion_returns_true() {
8200 let _g = crate::test_util::global_state_lock();
8201 let tokenized = format!(
8202 "{}a{}b{}",
8203 '\u{8f}', // Inbrace token
8204 '\u{9a}', // Comma token
8205 '\u{90}' // Outbrace token
8206 );
8207 assert!(
8208 hasbraces(&tokenized, false),
8209 "Inbrace+a+Comma+b+Outbrace must trigger brace expansion"
8210 );
8211 }
8212
8213 /// c:2042 — hasbraces on unclosed `{` returns false (no matching pair).
8214 #[test]
8215 fn hasbraces_unclosed_returns_false() {
8216 let _g = crate::test_util::global_state_lock();
8217 assert!(!hasbraces("{", false));
8218 assert!(!hasbraces("{abc", false));
8219 }
8220
8221 // ═══════════════════════════════════════════════════════════════════
8222 // Additional C-parity tests for Src/glob.c
8223 // c:950 qgetnum / c:994 qgetmodespec / c:1250 glob_exec_string /
8224 // c:1351 checkglobqual / c:1539 hasbraces / c:1617 bracechardots /
8225 // c:1802 matchpat / c:1899 getmatch / c:1505 file_type
8226 // ═══════════════════════════════════════════════════════════════════
8227
8228 /// c:950 — `qgetnum("")` empty returns None.
8229 #[test]
8230 fn qgetnum_empty_returns_none() {
8231 assert!(qgetnum("").is_none(), "empty → None");
8232 }
8233
8234 /// c:950 — `qgetnum` returns Option<(i64, &str)> (type pin).
8235 #[test]
8236 fn qgetnum_returns_option_i64_str_tuple_type() {
8237 let _: Option<(i64, &str)> = qgetnum("42");
8238 }
8239
8240 /// c:950 — `qgetnum("123")` returns Some((123, "")) (canonical decimal).
8241 #[test]
8242 fn qgetnum_canonical_decimal_parses() {
8243 let r = qgetnum("123");
8244 assert!(r.is_some(), "valid digits → Some");
8245 let (n, rest) = r.unwrap();
8246 assert_eq!(n, 123);
8247 assert_eq!(rest, "");
8248 }
8249
8250 /// c:994 — `qgetmodespec("")` empty returns None.
8251 #[test]
8252 fn qgetmodespec_empty_returns_none() {
8253 assert!(qgetmodespec("").is_none());
8254 }
8255
8256 /// c:1539 — `hasbraces("")` empty returns false.
8257 #[test]
8258 fn hasbraces_empty_returns_false() {
8259 let _g = crate::test_util::global_state_lock();
8260 assert!(!hasbraces("", false));
8261 }
8262
8263 /// c:1539 — `hasbraces` is pure (no side effects).
8264 #[test]
8265 fn hasbraces_is_pure() {
8266 let _g = crate::test_util::global_state_lock();
8267 for s in ["", "abc", "{", "{a,b}", "no braces"] {
8268 let first = hasbraces(s, false);
8269 for _ in 0..3 {
8270 assert_eq!(
8271 hasbraces(s, false),
8272 first,
8273 "hasbraces({:?}, false) must be pure",
8274 s
8275 );
8276 }
8277 }
8278 }
8279
8280 /// c:1505 — `file_type(0)` returns char (compile-time type pin).
8281 #[test]
8282 fn file_type_returns_char_type() {
8283 let _: char = file_type(0);
8284 }
8285
8286 /// c:1505 — `file_type` is pure for arbitrary mode.
8287 #[test]
8288 fn file_type_is_pure() {
8289 for m in [0u32, 0o100000, 0o040000, 0o120000, 0o140000, 0o160000] {
8290 let first = file_type(m);
8291 for _ in 0..3 {
8292 assert_eq!(file_type(m), first, "file_type({:o}) must be pure", m);
8293 }
8294 }
8295 }
8296
8297 /// c:1802 — `matchpat("", "", false, false)` returns bool (type pin).
8298 #[test]
8299 fn matchpat_returns_bool_type() {
8300 let _g = crate::test_util::global_state_lock();
8301 let _: bool = matchpat("", "", false, false);
8302 }
8303
8304 /// c:1899 — `getmatch("", "", 0, 0, None)` returns String (type pin).
8305 #[test]
8306 fn getmatch_returns_string_type() {
8307 let _g = crate::test_util::global_state_lock();
8308 let _: String = getmatch("", "", 0, 0, None);
8309 }
8310
8311 /// c:1862 — `compgetmatch("")` empty returns Option<(String, i32)>.
8312 #[test]
8313 fn compgetmatch_returns_option_tuple_type() {
8314 let _g = crate::test_util::global_state_lock();
8315 let _: Option<(String, i32)> = compgetmatch("");
8316 }
8317}