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_RETFAIL, 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::{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 /// c:47 `char *uname` — "Unmetafied file name; embedded nulls can't
54 /// occur in file names". The string `gmatchcmp`'s `GS_NAME` arm
55 /// collates (c:945). C fills it once per match, immediately before
56 /// the qsort (c:1963-1973); the port does the same in `sort_matches`
57 /// so the comparator never re-derives it. Empty until then.
58 pub uname: String,
59 /// `path` field.
60 pub path: PathBuf,
61 /// `size` field.
62 pub size: u64,
63 /// `atime` field.
64 pub atime: i64,
65 /// `mtime` field.
66 pub mtime: i64,
67 /// `ctime` field.
68 pub ctime: i64,
69 /// `links` field.
70 pub links: u64,
71 /// `mode` field.
72 pub mode: u32,
73 /// `uid` field.
74 pub uid: u32,
75 /// `gid` field.
76 pub gid: u32,
77 /// `dev` field.
78 pub dev: u64,
79 /// `ino` field.
80 pub ino: u64,
81 // For symlink targets (when following)
82 /// `target_size` field.
83 pub target_size: u64,
84 /// `target_atime` field.
85 pub target_atime: i64,
86 /// `target_mtime` field.
87 pub target_mtime: i64,
88 /// `target_ctime` field.
89 pub target_ctime: i64,
90 /// `target_links` field.
91 pub target_links: u64,
92 // Sub-second timestamp components — port of `long ansec/mnsec/cnsec` +
93 // `long _ansec/_mnsec/_cnsec` in `struct gmatch` (Src/glob.c:63-74).
94 // gmatchcmp tie-breaks equal-second mtimes/atimes/ctimes by these
95 // nanosecond fields (c:992/999/1006/1019/1026/1033) — without them,
96 // files touched <1s apart sort by name rather than mtime.
97 /// `ansec` field. c:64
98 pub ansec: i64,
99 /// `mnsec` field. c:68
100 pub mnsec: i64,
101 /// `cnsec` field. c:72
102 pub cnsec: i64,
103 /// `_ansec` field. c:65
104 pub target_ansec: i64,
105 /// `_mnsec` field. c:69
106 pub target_mnsec: i64,
107 /// `_cnsec` field. c:73
108 pub target_cnsec: i64,
109 // For exec sort strings
110 /// `sort_strings` field.
111 pub sort_strings: Vec<String>,
112}
113
114impl GlobOptSnapshot {
115 /// Read every glob-relevant option from the canonical live store
116 /// (`opt_state_get`) once. Returns a self-contained snapshot.
117 pub fn capture() -> Self {
118 Self {
119 bareglobqual: isset(BAREGLOBQUAL),
120 braceccl: isset(BRACECCL),
121 caseglob: isset(CASEGLOB),
122 extendedglob: isset(EXTENDEDGLOB),
123 globdots: isset(GLOBDOTS),
124 globstarshort: isset(GLOBSTARSHORT),
125 listtypes: isset(LISTTYPES),
126 markdirs: isset(MARKDIRS),
127 nullglob: isset(NULLGLOB),
128 numericglobsort: isset(NUMERICGLOBSORT),
129 }
130 }
131}
132
133thread_local! {
134 /// Thread-local glob option cache. `Some(snap)` while a glob()
135 /// call is in flight on this thread; `None` otherwise (reads
136 /// fall back to the live `isset()`).
137 static GLOB_OPTS_TLS: std::cell::RefCell<Option<GlobOptSnapshot>> =
138 const { std::cell::RefCell::new(None) };
139}
140
141// =====================================================================
142// GS_* — sort-specifier flag bits — `Src/glob.c:77-94`. The `glob -O`
143// + `glob -o` sort-spec parser stuffs these bits into the per-glob
144// sortspec struct so the qsort comparator picks the right key.
145// =====================================================================
146
147/// Port of `GS_NAME` from `Src/glob.c:77`. Sort by filename.
148pub const GS_NAME: i32 = 1; // c:77
149
150impl Drop for GlobOptsGuard {
151 fn drop(&mut self) {
152 if self.populated {
153 GLOB_OPTS_TLS.with_borrow_mut(|g| *g = None);
154 }
155 }
156}
157/// Port of `GS_DEPTH` from `Src/glob.c:78`. Sort by directory depth.
158pub const GS_DEPTH: i32 = 2; // c:78
159/// Port of `GS_EXEC` from `Src/glob.c:79`. Sort via external function.
160pub const GS_EXEC: i32 = 4; // c:79
161
162/// Port of `GS_SHIFT_BASE` from `Src/glob.c:81`. Bit position where
163/// the size/mtime/atime/ctime/links sort keys live.
164pub const GS_SHIFT_BASE: i32 = 8; // c:81
165
166/// Port of `GS_SIZE` from `Src/glob.c:83`. Sort by file size.
167pub const GS_SIZE: i32 = GS_SHIFT_BASE; // c:83
168/// Port of `GS_ATIME` from `Src/glob.c:84`. Sort by access time.
169pub const GS_ATIME: i32 = GS_SHIFT_BASE << 1; // c:84
170/// Port of `GS_MTIME` from `Src/glob.c:85`. Sort by modification time.
171pub const GS_MTIME: i32 = GS_SHIFT_BASE << 2; // c:85
172/// Port of `GS_CTIME` from `Src/glob.c:86`. Sort by inode-change time.
173pub const GS_CTIME: i32 = GS_SHIFT_BASE << 3; // c:86
174/// Port of `GS_LINKS` from `Src/glob.c:87`. Sort by hard-link count.
175pub const GS_LINKS: i32 = GS_SHIFT_BASE << 4; // c:87
176
177/// Port of `GS_SHIFT` from `Src/glob.c:89`. Bit-shift offset where the
178/// reverse-direction variants of the size/atime/mtime/ctime/links
179/// sort flags live.
180pub const GS_SHIFT: i32 = 5; // c:89
181
182/// Port of `GS__SIZE` from `Src/glob.c:90` (reverse-sort variant).
183pub const GS__SIZE: i32 = GS_SIZE << GS_SHIFT; // c:90
184/// Port of `GS__ATIME` from `Src/glob.c:91`.
185pub const GS__ATIME: i32 = GS_ATIME << GS_SHIFT; // c:91
186/// Port of `GS__MTIME` from `Src/glob.c:92`.
187pub const GS__MTIME: i32 = GS_MTIME << GS_SHIFT; // c:92
188/// Port of `GS__CTIME` from `Src/glob.c:93`.
189pub const GS__CTIME: i32 = GS_CTIME << GS_SHIFT; // c:93
190/// Port of `GS__LINKS` from `Src/glob.c:94`.
191pub const GS__LINKS: i32 = GS_LINKS << GS_SHIFT; // c:94
192
193/// Port of `GS_DESC` from `Src/glob.c:96`. Descending-order toggle.
194pub const GS_DESC: i32 = GS_SHIFT_BASE << (2 * GS_SHIFT); // c:96
195/// Port of `GS_NONE` from `Src/glob.c:97`. Marker for no-sort spec.
196pub const GS_NONE: i32 = GS_SHIFT_BASE << (2 * GS_SHIFT + 1); // c:97
197
198/// Port of `GS_NORMAL` from `Src/glob.c:99`. Forward-direction sort
199/// keys (excluding NAME/DEPTH/EXEC and the reverse variants).
200pub const GS_NORMAL: i32 = GS_SIZE | GS_ATIME | GS_MTIME | GS_CTIME | GS_LINKS; // c:99
201/// Port of `GS_LINKED` from `Src/glob.c:100`. Reverse-direction
202/// (linked) sort keys.
203pub const GS_LINKED: i32 = GS_NORMAL << GS_SHIFT; // c:100
204
205// =====================================================================
206// TT_* — time + size unit selectors. Two parallel namespaces using
207// the same numeric values.
208// =====================================================================
209
210/// Port of `TT_DAYS` from `Src/glob.c:121`. Time qualifier in days.
211pub const TT_DAYS: i32 = 0; // c:121
212/// Port of `TT_HOURS` from `Src/glob.c:122`. Time qualifier in hours.
213pub const TT_HOURS: i32 = 1; // c:122
214/// Port of `TT_MINS` from `Src/glob.c:123`. Time qualifier in minutes.
215pub const TT_MINS: i32 = 2; // c:123
216/// Port of `TT_WEEKS` from `Src/glob.c:124`. Time qualifier in weeks.
217pub const TT_WEEKS: i32 = 3; // c:124
218/// Port of `TT_MONTHS` from `Src/glob.c:125`. Time qualifier in months.
219pub const TT_MONTHS: i32 = 4; // c:125
220/// Port of `TT_SECONDS` from `Src/glob.c:126`. Time qualifier in seconds.
221pub const TT_SECONDS: i32 = 5; // c:126
222
223/// Port of `TT_BYTES` from `Src/glob.c:128`. Size qualifier in bytes.
224pub const TT_BYTES: i32 = 0; // c:128
225/// Port of `TT_POSIX_BLOCKS` from `Src/glob.c:129`. Size qualifier
226/// in POSIX 512-byte blocks (the `b` glob qualifier suffix).
227pub const TT_POSIX_BLOCKS: i32 = 1; // c:129
228/// Port of `TT_KILOBYTES` from `Src/glob.c:130`.
229pub const TT_KILOBYTES: i32 = 2; // c:130
230/// Port of `TT_MEGABYTES` from `Src/glob.c:131`.
231pub const TT_MEGABYTES: i32 = 3; // c:131
232/// Port of `TT_GIGABYTES` from `Src/glob.c:132`.
233pub const TT_GIGABYTES: i32 = 4; // c:132
234/// Port of `TT_TERABYTES` from `Src/glob.c:133`.
235pub const TT_TERABYTES: i32 = 5; // c:133
236
237/// Port of `MAX_SORTS` from `Src/glob.c:164`. Maximum sort-spec keys
238/// per glob (`glob -O 'reverse(name).size'` style).
239pub const MAX_SORTS: usize = 12; // c:164
240
241/// Main glob state — port of `struct globdata` from Src/glob.c:168.
242/// C zsh has a single file-static `static struct globdata curglobdata;`
243/// (glob.c:196) and accesses fields through a wall of #define macros
244/// (`matchsz`/`matchct`/`pathbuf`/`pathpos`/`quals`/...) that all
245/// resolve to `curglobdata.gd_*`. The Rust port collapses the
246/// `gd_matchsz`/`gd_matchct`/`gd_matchbuf`/`gd_matchptr` quartet into
247/// `matches: Vec<GlobMatch>` (the natural Rust shape) and folds the
248/// `gd_gf_*` glob-flag bag into `options: GlobOptions`, but the
249/// 1:1 correspondence to `struct globdata` is otherwise faithful.
250// struct to easily save/restore current state // c:166
251/// `globdata` — see fields for layout.
252#[allow(non_camel_case_types)]
253pub struct globdata {
254 // c:168
255 /// `matches` field.
256 pub matches: Vec<gmatch>,
257 /// `qualifiers` field.
258 pub qualifiers: Option<qualifier_set>,
259 /// c:206 — `quals` (`curglobdata.gd_quals`): the `struct qual` list
260 /// that `insert()` walks per candidate file. Arena-backed port of
261 /// the C pointer list; `None` when the glob has no qualifiers.
262 pub quals: Option<QualArena>,
263 pub pathbuf: String, // c:170 gd_pathbuf
264 pub pathpos: usize, // c:169 gd_pathpos
265 pub matchct: i32, // c:173 gd_matchct
266 pub pathbufsz: usize, // c:174 gd_pathbufsz
267 pub pathbufcwd: i32, // c:175 gd_pathbufcwd
268 pub gf_nullglob: i32, // c:186 gd_gf_nullglob
269 pub gf_markdirs: i32, // c:186 gd_gf_markdirs
270 pub gf_noglobdots: i32, // c:186 gd_gf_noglobdots
271 pub gf_listtypes: i32, // c:186 gd_gf_listtypes
272 pub gf_pre_words: Option<Vec<String>>, // c:190 gd_gf_pre_words
273 pub gf_post_words: Option<Vec<String>>, // c:190 gd_gf_post_words
274 /// The pattern's path text starts with a literal `./`, so every match
275 /// path is the pathbuf text verbatim (c:424 `dyncat(pathbuf, news)`) and
276 /// carries no scanner-added `./` for `glob_emit_path` to strip.
277 pub dot_prefix_verbatim: bool,
278}
279
280// c:197 — `static struct globdata curglobdata;`
281// C's single file-static glob state; macros at c:199-222 redirect
282// `pathbufsz`, `gf_noglobdots`, etc. to `curglobdata.gd_*`. Rust
283// mirror — accessed via lock for thread safety; C is single-threaded.
284/// `CURGLOBDATA` static.
285pub static CURGLOBDATA: std::sync::Mutex<globdata> = std::sync::Mutex::new(globdata {
286 matches: Vec::new(),
287 qualifiers: None,
288 quals: None,
289 pathbuf: String::new(),
290 pathpos: 0,
291 matchct: 0,
292 pathbufsz: 0,
293 pathbufcwd: 0,
294 gf_nullglob: 0,
295 gf_markdirs: 0,
296 gf_noglobdots: 0,
297 gf_listtypes: 0,
298 gf_pre_words: None,
299 gf_post_words: None,
300 dot_prefix_verbatim: false,
301});
302
303/// Port of `int badcshglob` from `Src/glob.c:103`. Tracks csh-glob
304/// diagnostic state across a single command line: bit 1 = "at
305/// least one expansion failed" (CSHNULLGLOB and no matches),
306/// bit 2 = "at least one expansion produced output". `globlist`
307/// resets to 0 at entry, ORs the bits as each `zglob` runs, then
308/// emits "no match" iff the final value is 1 (some failed, none
309/// succeeded). Used to make `*.nope *.ok` succeed without
310/// diagnostic, but `*.nope alone` error out under CSHNULLGLOB.
311pub static BADCSHGLOB: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:103
312
313/// Port of `static char **inserts;` from `Src/glob.c:340`. Set by
314/// `qualsheval` (c:3927-3937) from the `$reply` array / `$REPLY` scalar
315/// after an `(e:…:)` / `(+…)` qualifier eval; `insert()` then emits one
316/// match per element instead of the original name (c:428). `None` ==
317/// C's `inserts == NULL` (emit the original name once). Reset to `None`
318/// before each per-file qualifier walk (C: `inserts = NULL` at insert()
319/// entry, c:353).
320pub static INSERTS: std::sync::Mutex<Option<Vec<String>>> = std::sync::Mutex::new(None);
321
322/// Port of `struct complist` from `Src/glob.c:252`.
323/// C body:
324/// ```c
325/// struct complist {
326/// Complist next;
327/// Patprog pat;
328/// int closure; /* 1 if this is a (foo/)# */
329/// int follow; /* 1 to go thru symlinks */
330/// };
331/// ```
332#[allow(non_camel_case_types)]
333pub struct complist {
334 // c:252
335 pub next: Option<Box<complist>>, // c:253
336 pub pat: crate::ported::pattern::Patprog, // c:254
337 pub closure: i32, // c:255
338 pub follow: i32, // c:256
339}
340
341/// Add path component (from glob.c addpath lines 263-274)
342/// Append a path component to a glob path buffer.
343/// Port of `addpath(char *s, int l)` from Src/glob.c:265.
344///
345/// C body (c:270-273):
346/// ```c
347/// while (l--)
348/// pathbuf[pathpos++] = *s++;
349/// pathbuf[pathpos++] = '/';
350/// pathbuf[pathpos] = '\0';
351/// ```
352/// The `/` is appended UNCONDITIONALLY. That is load-bearing for EMPTY
353/// path components: `patcompile(..., PAT_FILET)` cuts a pure section at
354/// the first `/` (pattern.rs:891), so `//usr//b*` parses into the
355/// component chain `"" , "usr", "", "b*"` and each empty component must
356/// still contribute one `/` to `pathbuf` for the glob results to carry
357/// the literal `//` the user typed. A previous `if !s.ends_with('/')`
358/// guard here squeezed those empty components away (`//usr//bin` →
359/// `/usr/bin`), which broke `_path_files`: it anchors matches on the
360/// on-line path text (`${(@)tmp1#$prepath$realpath$testpath}`), so a
361/// squeezed result no longer shares the `//usr//` prefix and EVERY match
362/// was discarded — completion after `ls //usr//` produced nothing at all.
363/// A non-empty component can never end in `/` (patcompile cut it there),
364/// so removing the guard changes behaviour ONLY in the empty case.
365pub fn addpath(s: &mut String, l: &str) {
366 // c:265
367 s.push_str(l); // c:270-271
368 s.push('/'); // c:272 — unconditional, see doc above
369}
370
371/// PARTIAL port of `statfullpath(const char *s, struct stat *st, int l)` from
372/// `Src/glob.c:283` — NOT yet faithful, and currently has NO callers (the live
373/// qualifier matcher inlines `fs::metadata`/`symlink_metadata` on the already-
374/// full path). Two gaps remain, both blocked on the glob path-state flow:
375/// 1. C prepends the global `pathbuf[pathbufcwd..pathpos]` (the accumulated
376/// glob directory) to `s`; this port omits it. A faithful version must
377/// take the pathbuf as a parameter — it cannot read the `CURGLOBDATA`
378/// singleton because the matcher already holds that lock (deadlock).
379/// 2. C's `st` is the OUTPUT `struct stat *`; the second arg here is a string
380/// suffix, which is not the C contract.
381/// The `l` flag now matches C: `l ? lstat : stat` (l → `symlink_metadata`).
382/// Do not wire this in until gap #1 is resolved.
383pub fn statfullpath(s: &str, st: &str, l: bool) -> Option<Metadata> {
384 // c:283
385 let full = if st.is_empty() {
386 if s.is_empty() {
387 ".".to_string()
388 } else {
389 s.to_string()
390 }
391 } else {
392 format!("{}{}", s, st)
393 };
394
395 // c:308 — `return l ? lstat(buf, st) : stat(buf, st);`
396 if l {
397 fs::symlink_metadata(&full).ok() // l → lstat
398 } else {
399 fs::metadata(&full).ok() // !l → stat (follows symlinks)
400 }
401}
402// END moved-from-exec-rs (free ported)
403
404// ===========================================================
405// Direct ports of static helpers from Src/glob.c not yet covered
406// above. The Rust glob engine (`crate::glob`) re-implements the
407// lexer + matcher; these free-fn entries satisfy ABI/name parity
408// for the drift gate.
409// ===========================================================
410
411/// Port of `scanner(Complist q, int shortcircuit)` from `Src/glob.c:500`.
412///
413/// Walks the `complist` path-component chain built by `parsecomplist`,
414/// descending the directory tree one component per node. `q.closure`
415/// (set for `**` and `(dir/)#` / `(dir/)##`) drives the any-depth match:
416/// the zero-directory case scans `q.next` directly, then each matching
417/// subdirectory is re-scanned against `(q.closure) ? q : q.next` — i.e.
418/// the same node `q` repeats for `**`, matching at every depth.
419///
420/// Deviations from C, all pre-existing in this engine — NOT introduced
421/// by this port:
422/// * directory entries come from `fs::read_dir` + `zreaddir` (the
423/// ported `zreaddir`, glob.c:5217) instead of C `opendir`; same
424/// skip-`.`/`..` behaviour, and names are real (non-metafied) so no
425/// `unmeta` round-trip is needed;
426/// * the approximate-match error-reduction block (`forceerrs`/
427/// `errsfound`, glob.c:619-639) is omitted — the Rust matcher keeps
428/// no per-match error count;
429/// * `glob_pre`/`glob_suf` (ZLE prefix/suffix) filtering is omitted.
430/// Leading-dot files are filtered by `pattry`'s `PAT_NOGLD` check
431/// (pattern.rs:2890); `globdata_glob` sets `gf_noglobdots` so
432/// `parsecomplist` compiles the flag in (faithful to C's `glob()`).
433///
434/// The `in_closure_repeat` parameter is Rust-only: it carries C's
435/// `q->closure = 1` mutation across the recursion. A `(dir/)##` node
436/// (closure==2) requires at least one directory, so the zero-directory
437/// match is skipped only the FIRST time the node is entered; once we have
438/// descended one closure level it behaves like `(dir/)#` (zero-or-more).
439/// `q` is shared (`&complist`), so C's in-place field mutation is modelled
440/// by this argument instead. Callers pass `false`.
441fn scanner(state: &mut globdata, q: Option<&complist>, shortcircuit: i32, in_closure_repeat: bool) {
442 use std::sync::atomic::Ordering;
443 // c:506 — `if (!q || errflag) return;`
444 let Some(q) = q else { return };
445 if errflag.load(Ordering::SeqCst) & crate::ported::zsh_h::ERRFLAG_ERROR != 0 {
446 return;
447 }
448 let pbcwdsav = state.pathbufcwd; // c:503
449 let mut ds = init_dirsav(); // c:508
450 let path_max = crate::ported::zsh_system_h::PATH_MAX;
451
452 // c:510-518 — closure preamble: try zero directories via q.next first
453 // (skipped for `(dir/)##` until at least one directory is consumed).
454 let closure = q.closure;
455 if closure != 0 {
456 let skip_zero = q.closure == 2 && !in_closure_repeat;
457 if !skip_zero {
458 scanner(state, q.next.as_deref(), shortcircuit, false); // c:515
459 if shortcircuit != 0 && shortcircuit == state.matchct {
460 return;
461 }
462 }
463 }
464
465 let p = &q.pat; // c:519
466 if (p.0.flags & crate::ported::zsh_h::PAT_PURES as i32) != 0 {
467 // c:521-565 — pure literal section: append to the path (intermediate)
468 // or emit (final); no directory scan.
469 let start = p.0.startoff as usize;
470 let l = p.0.patmlen as usize;
471 let str_lit = String::from_utf8_lossy(&p.1[start..start + l]).into_owned();
472
473 // c:524-536 — PATH_MAX guard.
474 if l + (l == 0) as usize + state.pathpos - state.pathbufcwd as usize >= path_max {
475 if l >= path_max {
476 return;
477 }
478 let anchor = state
479 .pathbuf
480 .get(state.pathbufcwd as usize..)
481 .unwrap_or("")
482 .to_string();
483 let err = lchdir(&anchor, Some(&mut ds), 0);
484 if err == -1 {
485 return;
486 }
487 if err != 0 {
488 zerr("current directory lost during glob");
489 return;
490 }
491 state.pathbufcwd = state.pathpos as i32;
492 }
493
494 if q.next.is_some() {
495 // c:539-560 — not the last section: add to path, recurse.
496 let oppos = state.pathpos;
497 if errflag.load(Ordering::SeqCst) & crate::ported::zsh_h::ERRFLAG_ERROR == 0 {
498 // c:543-552 — `.`/`..` handling inside a closure walk.
499 let mut add = true;
500 if closure != 0 && !state.pathbuf.is_empty() {
501 if str_lit == "." {
502 add = false; // c:546
503 } else if str_lit == ".." {
504 // c:547-551 — drop `..` that would escape the root.
505 use std::os::unix::fs::MetadataExt;
506 let cur: &str = &state.pathbuf;
507 add = match (fs::metadata("/"), fs::metadata(cur)) {
508 (Ok(r), Ok(c)) => r.ino() != c.ino() || r.dev() != c.dev(),
509 _ => true,
510 };
511 }
512 }
513 if add {
514 addpath(&mut state.pathbuf, &str_lit); // c:553
515 state.pathpos = state.pathbuf.len();
516 // c:554 — closure: only recurse when the new path is a
517 // real directory (`!statfullpath("", NULL, 1)`).
518 let recurse = closure == 0
519 || fs::metadata(state.pathbuf.as_str())
520 .map(|m| m.is_dir())
521 .unwrap_or(false);
522 if recurse {
523 scanner(
524 state,
525 if closure != 0 {
526 Some(q)
527 } else {
528 q.next.as_deref()
529 },
530 shortcircuit,
531 closure != 0,
532 ); // c:555
533 if shortcircuit != 0 && shortcircuit == state.matchct {
534 return;
535 }
536 }
537 state.pathbuf.truncate(oppos); // c:558
538 state.pathpos = oppos;
539 }
540 }
541 } else {
542 // c:561-564 — last section: emit the literal.
543 let full = std::path::Path::new(&state.pathbuf).join(&str_lit);
544 insert(state, &full, 0);
545 if shortcircuit != 0 && shortcircuit == state.matchct {
546 return;
547 }
548 }
549 } else {
550 // c:567-685 — pattern-matched section: scan the directory.
551 let base = {
552 let from_cwd = state.pathbuf.get(state.pathbufcwd as usize..).unwrap_or("");
553 if from_cwd.is_empty() {
554 ".".to_string()
555 } else {
556 from_cwd.to_string()
557 }
558 };
559 let dirs = q.next.is_some(); // c:570
560 let mut rd = match fs::read_dir(&base) {
561 Ok(d) => d,
562 Err(_) => return, // c:573 — opendir == NULL
563 };
564 // c:572-573 — collect matching subdirs, descend AFTER the dir
565 // handle is dropped (C buffers them in `subdirs`). `zreaddir(lock, 1)`
566 // ALWAYS skips `.`/`..` (c:5217); leading-dot files are admitted and
567 // then accepted/rejected by `pattry`'s PAT_NOGLD rule, so dotglob /
568 // `(D)` is handled by the compiled flag, never by re-including
569 // `.`/`..` here (that previously leaked `.`/`..` under dotglob).
570 let mut subdirs: Vec<String> = Vec::new();
571 while let Some(name) = crate::ported::utils::zreaddir(&mut rd, 1) {
572 if errflag.load(Ordering::SeqCst) & crate::ported::zsh_h::ERRFLAG_ERROR != 0 {
573 break;
574 }
575 if !crate::ported::pattern::pattry(p, &name) {
576 continue; // c:583
577 }
578 // c:585-597 — PATH_MAX lchdir bookkeeping.
579 if pbcwdsav == state.pathbufcwd
580 && name.len() + state.pathpos - state.pathbufcwd as usize >= path_max
581 {
582 let anchor = state
583 .pathbuf
584 .get(state.pathbufcwd as usize..)
585 .unwrap_or("")
586 .to_string();
587 let err = lchdir(&anchor, Some(&mut ds), 0);
588 if err == -1 {
589 break;
590 }
591 if err != 0 {
592 zerr("current directory lost during glob");
593 break;
594 }
595 state.pathbufcwd = state.pathpos as i32;
596 }
597 if dirs {
598 // c:642-655 — closure: only descend real directories.
599 if closure != 0 {
600 let probe = std::path::Path::new(&base).join(&name);
601 let md = if q.follow != 0 {
602 fs::metadata(&probe)
603 } else {
604 fs::symlink_metadata(&probe)
605 };
606 match md {
607 Ok(m) if m.is_dir() => {}
608 _ => continue,
609 }
610 }
611 subdirs.push(name); // c:657-663
612 } else {
613 // c:665-670 — last component: emit.
614 let full = std::path::Path::new(&base).join(&name);
615 insert(state, &full, 1);
616 if shortcircuit != 0 && shortcircuit == state.matchct {
617 return;
618 }
619 }
620 }
621 drop(rd); // c:672 — closedir
622 // c:674-684 — descend into each collected subdir.
623 if !subdirs.is_empty() {
624 let oppos = state.pathpos;
625 // RUST-ONLY — fan the sibling subdirs of a `**` closure level out
626 // over the rayon pool instead of the serial `for` below, appending
627 // each sibling's matches in `subdirs` order so the result equals the
628 // serial descent (including `oN` unsorted globs). Each worker runs
629 // `scanner` on its own `globdata`; nested levels fan out again
630 // through the same pool. Stays serial when `[glob] recursive_parallel
631 // = false`, with fewer than two siblings, or with an `(e:…:)` /
632 // `(+…)` qualifier (`qualsheval` runs shell code and publishes
633 // `$reply` through the process-global `INSERTS`). The qualifier
634 // arena `insert` walks is `state.qualifiers.quals` (check_qualifiers);
635 // the guard only looked at `state.quals`, so `**/*(e:…:)` fanned out
636 // anyway and the workers raced on `$REPLY` / `INSERTS`. The test is
637 // structural: `sdata` is set only by the eval qualifier.
638 if closure != 0
639 && shortcircuit == 0
640 && subdirs.len() >= 2
641 && crate::extensions::config::current().glob.recursive_parallel
642 && !state
643 .quals
644 .as_ref()
645 .into_iter()
646 .chain(state.qualifiers.as_ref().map(|q| &q.quals))
647 .any(|arena| arena.nodes.iter().any(|n| n.sdata.is_some()))
648 {
649 use rayon::prelude::*;
650 // Workers are pool threads with their own TLS; hand each the
651 // option snapshot this glob started with.
652 let snap = GLOB_OPTS_TLS.with_borrow(|g| *g);
653 let shared: &globdata = state;
654 // The process's own pool, not rayon's global one: a forked child
655 // (`$(...)`, a pipeline stage) inherits the global pool's handles
656 // without its threads, and `par_iter` there never returns.
657 let results: Option<Vec<(Vec<gmatch>, i32)>> = crate::fork_safe_pool::pool().map(|pool| pool.install(|| subdirs
658 .par_iter()
659 .map(|name| {
660 let mut child = globdata::new();
661 child.qualifiers = shared.qualifiers.clone();
662 child.quals = shared.quals.clone();
663 child.gf_nullglob = shared.gf_nullglob;
664 child.gf_markdirs = shared.gf_markdirs;
665 child.gf_noglobdots = shared.gf_noglobdots;
666 child.gf_listtypes = shared.gf_listtypes;
667 child.gf_pre_words = shared.gf_pre_words.clone();
668 child.gf_post_words = shared.gf_post_words.clone();
669 child.dot_prefix_verbatim = shared.dot_prefix_verbatim;
670 child.pathbuf = shared.pathbuf.clone();
671 child.pathbufcwd = shared.pathbufcwd;
672 addpath(&mut child.pathbuf, name);
673 child.pathpos = child.pathbuf.len();
674 let prev = GLOB_OPTS_TLS.with_borrow_mut(|g| std::mem::replace(g, snap));
675 scanner(&mut child, Some(q), 0, true); // c:681
676 GLOB_OPTS_TLS.with_borrow_mut(|g| *g = prev);
677 (child.matches, child.matchct)
678 })
679 .collect()));
680 if let Some(results) = results {
681 for (matches, matchct) in results {
682 state.matches.extend(matches);
683 state.matchct += matchct;
684 }
685 subdirs.clear();
686 }
687 }
688 for name in subdirs {
689 addpath(&mut state.pathbuf, &name);
690 state.pathpos = state.pathbuf.len();
691 scanner(
692 state,
693 if closure != 0 {
694 Some(q)
695 } else {
696 q.next.as_deref()
697 },
698 shortcircuit,
699 closure != 0,
700 ); // c:681
701 if shortcircuit != 0 && shortcircuit == state.matchct {
702 return;
703 }
704 state.pathbuf.truncate(oppos);
705 state.pathpos = oppos;
706 }
707 }
708 }
709
710 // c:687-694 — restore cwd if we lchdir'd partway through.
711 if pbcwdsav < state.pathbufcwd {
712 if restoredir(&mut ds) != 0 {
713 zerr("current directory lost during glob"); // c:689
714 }
715 state.pathbufcwd = pbcwdsav;
716 }
717}
718
719/* This function tokenizes a zsh glob pattern */
720// c:706
721/// Port of `parsecomplist(char *instr)` from Src/glob.c:710.
722/// Tokenize a zsh glob path pattern into a `Complist` of path
723/// components, recursively. Returns `None` and sets `errflag |=
724/// ERRFLAG_ERROR` on parse failure. Reads `gf_noglobdots` from
725/// `CURGLOBDATA` (the curglobdata singleton at c:197).
726pub fn parsecomplist(instr: &str) -> Option<Box<complist>> {
727 // c:710
728 let p1: crate::ported::pattern::Patprog; // c:712
729 let l1: Box<complist>; // c:713
730 // c:714 `char *str;` — used as the skipparens cursor (parens branch).
731 // c:715 — compflags depend on gf_noglobdots from curglobdata.
732 let gf_noglobdots: i32 = CURGLOBDATA
733 .lock()
734 .unwrap_or_else(|e| e.into_inner())
735 .gf_noglobdots; // c:214 macro / c:186 field
736 let compflags: i32 = if gf_noglobdots != 0 {
737 crate::ported::zsh_h::PAT_FILE | crate::ported::zsh_h::PAT_NOGLD
738 } else {
739 crate::ported::zsh_h::PAT_FILE
740 }; // c:715
741
742 let chars: Vec<char> = instr.chars().collect();
743 if chars.len() >= 2
744 && chars[0] == crate::ported::zsh_h::Star
745 && chars[1] == crate::ported::zsh_h::Star
746 {
747 // c:717
748 let mut shortglob: i32 = 0; // c:718
749 let cond_a = chars.get(2) == Some(&'/'); // c:719
750 let cond_b =
751 chars.get(2) == Some(&crate::ported::zsh_h::Star) && chars.get(3) == Some(&'/'); // c:719
752 // c:719-720 — `instr[2] == '/' || (instr[2] == Star && instr[3] == '/')
753 // || (shortglob = isset(GLOBSTARSHORT))`. C's `||` SHORT-CIRCUITS:
754 // the `shortglob = isset(GLOBSTARSHORT)` assignment runs ONLY when
755 // `**` is not explicitly followed by `/`. Mirror that with a lazy
756 // `||` so cond_a/cond_b keep `shortglob == 0` — an eager
757 // `let cond_c = { shortglob = … }` set it unconditionally, making
758 // `**/x` advance by 1 (`shortglob ? 1 : 3`) instead of 3, leaving a
759 // stray `*` that collapsed `**/` to a single directory level.
760 let enter = cond_a || cond_b || {
761 shortglob = if crate::ported::zsh_h::isset(crate::ported::zsh_h::GLOBSTARSHORT) {
762 1
763 } else {
764 0
765 };
766 shortglob != 0
767 }; // c:720
768 if enter {
769 /* Match any number of directories. */
770 // c:721
771 /* with three stars, follow symbolic links */ // c:724
772 let follow: i32 = if chars.get(2) == Some(&crate::ported::zsh_h::Star) {
773 1
774 } else {
775 0
776 }; // c:725
777 /*
778 * With GLOBSTARSHORT, leave a star in place for the
779 * pattern inside the directory.
780 */ // c:726-729
781 let advance: usize = (if shortglob != 0 { 1 } else { 3 }) + follow as usize; // c:730
782
783 /* Now get the next path component if there is one. */
784 // c:732
785 let next_instr: String = chars[advance..].iter().collect();
786 // c:733 — `l1 = (Complist) zhalloc(sizeof *l1);`
787 let next_l = parsecomplist(&next_instr); // c:734
788 if next_l.is_none() {
789 crate::ported::utils::errflag.fetch_or(
790 crate::ported::zsh_h::ERRFLAG_ERROR,
791 std::sync::atomic::Ordering::Relaxed,
792 ); // c:735
793 return None; // c:736
794 }
795 let pat = crate::ported::pattern::patcompile(
796 "",
797 compflags | crate::ported::zsh_h::PAT_ANY,
798 None,
799 )?; // c:738
800 l1 = Box::new(complist {
801 next: next_l,
802 pat,
803 closure: 1, // c:739
804 follow, // c:740
805 });
806 return Some(l1); // c:741
807 }
808 }
809
810 /* Parse repeated directories such as (dir/)# and (dir/)## */
811 // c:745
812 let zpc = crate::ported::pattern::zpc_special
813 .lock()
814 .unwrap_or_else(|e| e.into_inner());
815 let inpar_c = zpc[crate::ported::zsh_h::ZPC_INPAR as usize] as char;
816 let hash_c = zpc[crate::ported::zsh_h::ZPC_HASH as usize] as char;
817 drop(zpc);
818
819 // c:746-748 tests `*instr == zpc_special[ZPC_INPAR]` and `*str ==
820 // zpc_special[ZPC_HASH]`, and C's `zpc_chars` (Src/pattern.c:248)
821 // stores the TOKENS there — `Inpar` and `Pound`, not `(` and `#` —
822 // because everything reaching patcompile in C is lexer-tokenized.
823 // zshrs's `patcompcharsset` deliberately stores the RAW spelling
824 // instead (see its note there: patcompile is reachable with
825 // untokenized patterns and ~80 tests depend on a raw `|`
826 // alternating), so a straight comparison against the table never
827 // matches the tokenized input `parsecomplist` actually receives
828 // from `globdata_glob`.
829 //
830 // Accept either spelling, but keep the option masking: when a slot
831 // is disabled (EXTENDEDGLOB off, SHGLOB on, `disable -p`)
832 // `patcompcharsset` overwrites it with `Marker`, and then NEITHER
833 // spelling may match — which is what keeps `setopt noextendedglob;
834 // (sub/)#end` a literal.
835 // The slots hold the Marker TRUNCATED to a byte (patcompcharsset stores
836 // `Marker as u8`), so compare against that truncation, not the full char.
837 let marker_c = (crate::ported::zsh_h::Marker as u32 as u8) as char;
838 let is_inpar =
839 |c: char| c == inpar_c || (c == crate::ported::zsh_h::Inpar && inpar_c != marker_c);
840 let is_hash = |c: char| c == hash_c || (c == crate::ported::zsh_h::Pound && hash_c != marker_c);
841
842 // c:746-748 — `if (*(str = instr) == zpc_special[ZPC_INPAR] &&
843 // !skipparens(Inpar, Outpar, (char **)&str) &&
844 // *str == zpc_special[ZPC_HASH] && str[-2] == '/')`.
845 // Routed through the canonical `skipparens` port for caller-coverage
846 // parity with C — was previously inlined as a divergent depth-walk
847 // because the old Rust signature returned usize. The C-faithful
848 // `skipparens(open, close, &mut &str)` now matches `int skipparens
849 // (char inpar, char outpar, char **s)` at utils.c:2409.
850 let instr_chars: String = chars.iter().collect();
851 let inpar_byte = crate::ported::zsh_h::Inpar as u32;
852 let outpar_byte = crate::ported::zsh_h::Outpar as u32;
853 let mut cursor: &str = &instr_chars;
854 let skip_level: i32 = crate::ported::utils::skipparens(
855 char::from_u32(inpar_byte).unwrap_or('('),
856 char::from_u32(outpar_byte).unwrap_or(')'),
857 &mut cursor,
858 );
859 let str_after_parens: Option<usize> = if chars.first().copied().is_some_and(is_inpar) {
860 Some(instr_chars.chars().count() - cursor.chars().count())
861 } else {
862 None
863 };
864 let parens_balanced = chars.first().copied().is_some_and(is_inpar) && skip_level == 0; // c:746-747 `!skipparens(...)`
865 let after_paren_idx = str_after_parens.unwrap_or(0);
866 let str_at_hash = parens_balanced && chars.get(after_paren_idx).copied().is_some_and(is_hash); // c:748 `*str == Pound`
867 // c:748 `str[-2] == '/'` — `str` is past `)`, so `str[-2]` is char
868 // before `)`. In chars, that's `chars[after_paren_idx - 2]`.
869 let preceded_by_slash =
870 parens_balanced && after_paren_idx >= 2 && chars.get(after_paren_idx - 2) == Some(&'/');
871
872 if parens_balanced && str_at_hash && preceded_by_slash {
873 // c:749 — `instr++;`
874 let mut cursor: String = chars[1..].iter().collect();
875 let mut endexp = String::new();
876 let p1_opt = crate::ported::pattern::patcompile(&cursor, compflags, Some(&mut endexp)); // c:750
877 if p1_opt.is_none() {
878 return None; // c:751
879 }
880 let p1_real = p1_opt.unwrap();
881 cursor = endexp; // C: `instr` advanced past compiled pattern
882 let c2: Vec<char> = cursor.chars().collect();
883 if c2.first() == Some(&'/')
884 && c2.get(1) == Some(&crate::ported::zsh_h::Outpar)
885 && c2.get(2) == Some(&crate::ported::zsh_h::Pound)
886 {
887 // c:752
888 let mut pdflag: i32 = 0; // c:753
889 let mut adv = 3; // c:755
890 if c2.get(3) == Some(&crate::ported::zsh_h::Pound) {
891 pdflag = 1; // c:757
892 adv = 4; // c:758
893 }
894 let next_instr: String = c2[adv..].iter().collect();
895 // c:760-761 — `l1 = (Complist) zhalloc(...); l1->pat = p1;`
896 /* special case (/)# to avoid infinite recursion */ // c:762
897 // c:763 — `(*((char *)p1 + p1->startoff)) ? 1 + pdflag : 0`
898 let pat_nonempty = !p1_real.1.is_empty()
899 && p1_real
900 .1
901 .get(p1_real.0.startoff as usize)
902 .copied()
903 .unwrap_or(0)
904 != 0;
905 let closure = if pat_nonempty { 1 + pdflag } else { 0 };
906 let next_l = parsecomplist(&next_instr); // c:765
907 // c:766 — `return (l1->pat) ? l1 : NULL;`
908 l1 = Box::new(complist {
909 next: next_l,
910 pat: p1_real,
911 closure,
912 follow: 0, // c:764
913 });
914 return Some(l1);
915 }
916 } else {
917 /* parse single path component */
918 // c:769
919 let mut endexp = String::new();
920 let p1_opt = crate::ported::pattern::patcompile(
921 instr,
922 compflags | crate::ported::zsh_h::PAT_FILET,
923 Some(&mut endexp),
924 ); // c:770
925 if p1_opt.is_none() {
926 return None; // c:771
927 }
928 p1 = p1_opt.unwrap();
929 let cursor: Vec<char> = endexp.chars().collect();
930 /* then do the remaining path components */
931 // c:772
932 let head = cursor.first().copied();
933 if head == Some('/') || head.is_none() {
934 // c:773
935 let ef: i32 = if head == Some('/') { 1 } else { 0 }; // c:774
936 let next_l: Option<Box<complist>> = if ef != 0 {
937 let rest: String = cursor[1..].iter().collect();
938 parsecomplist(&rest) // c:779
939 } else {
940 None
941 };
942 // c:780 — `return (ef && !l1->next) ? NULL : l1;`
943 if ef != 0 && next_l.is_none() {
944 return None;
945 }
946 l1 = Box::new(complist {
947 next: next_l,
948 pat: p1,
949 closure: 0, // c:778
950 follow: 0,
951 });
952 return Some(l1);
953 }
954 }
955 crate::ported::utils::errflag.fetch_or(
956 crate::ported::zsh_h::ERRFLAG_ERROR,
957 std::sync::atomic::Ordering::Relaxed,
958 ); // c:783
959 None // c:784
960}
961
962/* turn a string into a Complist struct: this has path components */
963// c:787
964/// Port of `parsepat(char *str)` from Src/glob.c:791.
965/// Top-level entry: strip leading `(#...)` flag block via
966/// `patgetglobflags`, then initialise `pathbuf`/`pathpos` per the
967/// pattern's absolute-vs-relative head, then dispatch to
968/// `parsecomplist`. Mutates `CURGLOBDATA.{pathbuf,pathpos,pathbufsz}`.
969pub fn parsepat(s: &str) -> Option<Box<complist>> {
970 // c:791
971 // c:793 `long assert; int ignore;` — captured into patgetglobflags result
972 crate::ported::pattern::patcompstart(); // c:796
973 let chars: Vec<char> = s.chars().collect();
974 let zpc = crate::ported::pattern::zpc_special
975 .lock()
976 .unwrap_or_else(|e| e.into_inner());
977 let inpar_c = zpc[crate::ported::zsh_h::ZPC_INPAR as usize] as char;
978 let hash_c = zpc[crate::ported::zsh_h::ZPC_HASH as usize] as char;
979 let ksh_at_c = zpc[crate::ported::zsh_h::ZPC_KSH_AT as usize] as char;
980 drop(zpc);
981
982 /*
983 * Check for initial globbing flags, so that they don't form
984 * a bogus path component.
985 */
986 // c:797-800
987 let mut cursor: String = s.to_string();
988 let first_is_inpar_hash = chars.first() == Some(&inpar_c) && chars.get(1) == Some(&hash_c); // c:801
989 let first_is_ksh_at_inpar_hash = chars.first() == Some(&ksh_at_c)
990 && chars.get(1) == Some(&crate::ported::zsh_h::Inpar)
991 && chars.get(2) == Some(&hash_c); // c:802-803
992 if first_is_inpar_hash || first_is_ksh_at_inpar_hash {
993 let skip = if chars.first() == Some(&crate::ported::zsh_h::Inpar) {
994 2
995 } else {
996 3
997 }; // c:804
998 cursor = chars[skip..].iter().collect();
999 let flag_result = crate::ported::pattern::patgetglobflags(&cursor); // c:805
1000 if flag_result.is_none() {
1001 return None; // c:806
1002 }
1003 let (_bits, _assertp, consumed) = flag_result.unwrap();
1004 cursor = cursor[consumed..].to_string();
1005 }
1006
1007 /* Now there is no (#X) in front, we can check the path. */
1008 // c:809
1009 {
1010 let mut gd = CURGLOBDATA.lock().unwrap_or_else(|e| e.into_inner());
1011 // c:810-811 — `if (!pathbuf) pathbuf = zalloc(pathbufsz = PATH_MAX+1);`
1012 if gd.pathbuf.capacity() == 0 {
1013 gd.pathbufsz = libc::PATH_MAX as usize + 1;
1014 gd.pathbuf = String::with_capacity(gd.pathbufsz);
1015 }
1016 // c:812 — `DPUTS(pathbufcwd, "BUG: glob changed directory");`
1017 debug_assert!(gd.pathbufcwd == 0, "BUG: glob changed directory");
1018 if cursor.starts_with('/') {
1019 // c:813
1020 /* pattern has absolute path */
1021 cursor = cursor[1..].to_string(); // c:814 `str++;`
1022 gd.pathbuf.clear();
1023 gd.pathbuf.push('/'); // c:815 `pathbuf[0] = '/';`
1024 gd.pathpos = 1; // c:816 `pathbuf[pathpos = 1] = '\0';`
1025 } else {
1026 /* pattern is relative to pwd */
1027 // c:817
1028 gd.pathbuf.clear(); // c:818 `pathbuf[pathpos = 0] = '\0';`
1029 gd.pathpos = 0;
1030 }
1031 }
1032
1033 parsecomplist(&cursor) // c:820
1034}
1035
1036/// Parse qualifier (from glob.c qgetnum)
1037/// Parse a numeric glob-qualifier argument.
1038/// Port of `qgetnum(char **s)` from Src/glob.c:827.
1039pub fn qgetnum(s: &str) -> Option<(i64, &str)> {
1040 // c:827
1041 let end = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
1042 if end == 0 {
1043 return None;
1044 }
1045 let num = s[..end].parse::<i64>().ok()?;
1046 Some((num, &s[end..]))
1047}
1048
1049impl globdata {
1050 /// `new` — see implementation.
1051 pub fn new() -> Self {
1052 globdata {
1053 matches: Vec::new(),
1054 qualifiers: None,
1055 quals: None,
1056 pathbuf: String::with_capacity(4096),
1057 pathpos: 0,
1058 matchct: 0,
1059 pathbufsz: 4096,
1060 pathbufcwd: 0,
1061 gf_nullglob: 0,
1062 gf_markdirs: 0,
1063 gf_noglobdots: 0,
1064 gf_listtypes: 0,
1065 gf_pre_words: None,
1066 gf_post_words: None,
1067 dot_prefix_verbatim: false,
1068 }
1069 }
1070}
1071
1072// ============================================================================
1073// Mode specification parsing (from glob.c qgetmodespec)
1074// ============================================================================
1075
1076/// Port of `qgetmodespec(char **s)` from `Src/glob.c:844`.
1077///
1078/// Parses a chmod-style spec for the `f` glob qualifier and returns
1079/// C's packed value `(yes & 07777) | ((no & 07777) << 12)` (c:931) —
1080/// the bits that must be set and the bits that must be clear, as
1081/// `qualmodeflags` (c:3817) reads them — plus the unparsed rest, which
1082/// is C's `*s = p` (c:930). `None` is C's `zerr("invalid mode
1083/// specification"); return 0;` (c:890/913/925); the message is already
1084/// out.
1085/// WARNING: param names don't match C — Rust=(s) vs C=(s); the `char **`
1086/// cursor is returned as the rest slice.
1087pub fn qgetmodespec(s: &str) -> Option<(i64, &str)> {
1088 // c:844
1089 use crate::ported::zsh_h::{Equals, Inang, Inbrace, Inbrack, Outang, Outbrace, Outbrack, Quest};
1090 let cs: Vec<(usize, char)> = s.char_indices().collect();
1091 // `*p` past the end reads the terminating NUL.
1092 let at = |i: usize| cs.get(i).map(|&(_, c)| c).unwrap_or('\0');
1093 let (mut yes, mut no): (i64, i64) = (0, 0); // c:846
1094 let mut p = 0usize; // c:847
1095 let mut c = at(p);
1096 let end: char;
1097 // c:849-860 — a bare spec starts with an operator or an octal digit;
1098 // anything else is the opening delimiter.
1099 if c == '=' || c == Equals || c == '+' || c == '-' || c == '?' || c == Quest || ('0'..='7').contains(&c) {
1100 end = '\0'; // c:851
1101 } else {
1102 end = match c {
1103 '<' => '>',
1104 '[' => ']',
1105 '{' => '}',
1106 x if x == Inang => Outang,
1107 x if x == Inbrack => Outbrack,
1108 x if x == Inbrace => Outbrace,
1109 other => other,
1110 }; // c:853-858
1111 p += 1; // c:859
1112 }
1113 let invalid = || {
1114 zerr("invalid mode specification");
1115 None
1116 };
1117 loop {
1118 // c:861 do {
1119 let mut mask: i64 = 0; // c:862
1120 loop {
1121 // c:863 — who letters, only inside a delimited spec.
1122 c = at(p);
1123 if !(matches!(c, 'u' | 'g' | 'o' | 'a') && end != '\0') {
1124 break;
1125 }
1126 mask |= match c {
1127 'o' => 0o1007, // c:865
1128 'g' => 0o2070, // c:866
1129 'u' => 0o4700, // c:867
1130 _ => 0o7777, // c:868 'a'
1131 };
1132 p += 1; // c:870
1133 }
1134 let how = if c == '+' || c == '-' { c } else { '=' }; // c:872
1135 if c == '+' || c == '-' || c == '=' || c == Equals {
1136 // c:873
1137 p += 1; // c:874
1138 }
1139 let mut val: i64 = 0; // c:875
1140 if mask != 0 {
1141 // c:876
1142 loop {
1143 // c:877 — `while ((c = *p++) != ',' && c != end)`
1144 c = at(p);
1145 p += 1;
1146 if c == ',' || c == end {
1147 break;
1148 }
1149 match c {
1150 'x' => val |= 0o0111, // c:879
1151 'w' => val |= 0o0222, // c:880
1152 'r' => val |= 0o0444, // c:881
1153 's' => val |= 0o6000, // c:882
1154 't' => val |= 0o1000, // c:883
1155 '0'..='7' => {
1156 // c:884-888
1157 let t = c as i64 - '0' as i64;
1158 val |= t | (t << 3) | (t << 6);
1159 }
1160 _ => return invalid(), // c:889-891
1161 }
1162 }
1163 if how == '=' || how == '+' {
1164 // c:894
1165 yes |= val & mask; // c:895
1166 val = !val; // c:896
1167 }
1168 if how == '=' || how == '-' {
1169 // c:898
1170 no |= val & mask; // c:899
1171 }
1172 } else if !(end != '\0' && c == end) && c != ',' && c != '\0' {
1173 // c:900
1174 let mut t: i64 = 0o7777; // c:901
1175 loop {
1176 // c:902-912
1177 c = at(p);
1178 if c == '?' || c == Quest {
1179 t = (t << 3) | 7; // c:905
1180 val <<= 3; // c:906
1181 } else if ('0'..='7').contains(&c) {
1182 t <<= 3; // c:908
1183 val = (val << 3) | (c as i64 - '0' as i64); // c:909
1184 } else {
1185 break;
1186 }
1187 p += 1; // c:911
1188 }
1189 if end != '\0' && c != end && c != ',' {
1190 // c:913
1191 return invalid(); // c:914-915
1192 }
1193 if how == '=' {
1194 // c:917
1195 yes = (yes & !t) | val; // c:918
1196 no = (no & !t) | (!val & !t); // c:919
1197 } else if how == '+' {
1198 yes |= val; // c:921
1199 } else {
1200 no |= val; // c:923
1201 }
1202 } else {
1203 return invalid(); // c:924-926
1204 }
1205 if !(end != '\0' && c != end) {
1206 // c:928 } while (end && c != end);
1207 break;
1208 }
1209 }
1210 // c:930 `*s = p;`
1211 let rest_off = cs.get(p).map(|&(o, _)| o).unwrap_or(s.len());
1212 Some(((yes & 0o7777) | ((no & 0o7777) << 12), &s[rest_off..])) // c:931
1213}
1214
1215// `impl GlobMatch` block deleted — C builds Gmatch entries inline
1216// in `insert()` (glob.c:346) and `gmatchcmp` (glob.c:936) is a
1217// free function taking two Gmatch pointers. The Rust port mirrors
1218// that shape: no methods on the struct; construction inlined at
1219// the scanner call site, comparator as a free fn below.
1220
1221/// Port of `gmatchcmp(Gmatch a, Gmatch b)` from Src/glob.c:936 —
1222/// the qsort comparator the `o`/`O` glob qualifier drives.
1223///
1224/// `specs` is the equivalent of the C `gf_sortlist` array, each
1225/// entry a packed i32 (the C `struct globsort.tp` field):
1226/// bits 0..=4 — primary key (GS_NAME / GS_DEPTH / GS_EXEC /
1227/// GS_SIZE / GS_ATIME / GS_MTIME / GS_CTIME /
1228/// GS_LINKS, plus GS_NONE marker)
1229/// bits << GS_SHIFT — same keys, follow-link variant
1230/// (GS__SIZE / GS__ATIME / …)
1231/// GS_DESC bit — reverse direction (`O` qualifier instead of `o`)
1232/// WARNING: param names don't match C — Rust=(b, specs, numeric_sort) vs C=(a, b)
1233pub fn gmatchcmp(
1234 // c:936
1235 a: &gmatch,
1236 b: &gmatch,
1237 specs: &[i32],
1238 numeric_sort: bool,
1239) -> std::cmp::Ordering {
1240 for &tp in specs {
1241 // c:943
1242 let key = tp & !GS_DESC; // c:944 s->tp & ~GS_DESC
1243 let follow = (key & GS_LINKED) != 0;
1244 let key_unshifted = if follow { key >> GS_SHIFT } else { key };
1245 let cmp = if key_unshifted == GS_NAME {
1246 // c:945 — `gmatch->name` in C zsh is the FULL match string
1247 // (Src/glob.c sets it from the accumulated path during the
1248 // scanner walk), so the qsort comparator inherently sorts by
1249 // full path. The Rust port stores the basename in `name`;
1250 // use `path` instead so `**/*` matches the full-path
1251 // lexicographic order zsh produces. Verified vs
1252 // /opt/homebrew/bin/zsh: `echo /tmp/rg/**/*` → `f sub sub/g`
1253 // (sorted by full path, not basename).
1254 // c:945 `zstrcmp(b->uname, a->uname, …)` — C hands the
1255 // comparator two ready-made `char *`; the key was built once
1256 // per match at c:1963-1973. `sort_matches` fills `uname` the
1257 // same way (including the "./" strip described there), so
1258 // this arm only collates. Deriving it here instead cost a
1259 // `Path::to_string_lossy` + `strip_prefix` pair on EVERY
1260 // comparison — 11.7% of a 60605-match `man` glob, since a
1261 // sort comparator runs O(n log n) times.
1262 zstrcmp(
1263 &a.uname,
1264 &b.uname,
1265 if numeric_sort {
1266 crate::zsh_h::SORTIT_NUMERICALLY as u32
1267 } else {
1268 0
1269 },
1270 )
1271 } else if key_unshifted == GS_DEPTH {
1272 // c:949-972 — pairwise: skip the common prefix of the two
1273 // full paths, then `slasha`/`slashb` = does the remainder of
1274 // each (excluding its final char) still contain a `/`, i.e.
1275 // is this path deeper past the divergence point. `r = slasha
1276 // - slashb`; ascending puts the deeper path first.
1277 // c:947 compares `a->name`, the match name WITHOUT the scanner's
1278 // depth-0 `./` (c:424) — `uname`, as for GS_NAME. The raw path
1279 // gave every top-level match a `/` and sorted `**/*(od)` as flat.
1280 let an = &a.uname;
1281 let bn = &b.uname;
1282 let ab = an.as_bytes();
1283 let bb = bn.as_bytes();
1284 // c:951 — `while (*aptr && *aptr == *bptr) aptr++,bptr++;`
1285 let mut i = 0;
1286 while i < ab.len() && i < bb.len() && ab[i] == bb[i] {
1287 i += 1;
1288 }
1289 // c:953-954 — if at the end of one and the prev char is `/`,
1290 // back up one so the trailing component is counted.
1291 let (mut ai, mut bi) = (i, i);
1292 if (ai >= ab.len() || bi >= bb.len()) && ai > 0 && ab[ai - 1] == b'/' {
1293 ai -= 1;
1294 bi -= 1;
1295 }
1296 // c:956-964 — slash present in the remainder, excluding last char.
1297 let has_slash = |s: &[u8]| s.len() > 1 && s[..s.len() - 1].contains(&b'/');
1298 let slasha = has_slash(&ab[ai.min(ab.len())..]) as i32;
1299 let slashb = has_slash(&bb[bi.min(bb.len())..]) as i32;
1300 // c:971 — `r = slasha - slashb`; deeper (slash=1) sorts first.
1301 slashb.cmp(&slasha)
1302 } else if key_unshifted == GS_SIZE {
1303 // c:985
1304 if follow {
1305 a.target_size.cmp(&b.target_size)
1306 } else {
1307 a.size.cmp(&b.size)
1308 }
1309 } else if key_unshifted == GS_ATIME {
1310 // c:988-994 — `r = a->atime - b->atime;` then tie-break by
1311 // `a->ansec - b->ansec` (under GET_ST_ATIME_NSEC).
1312 let primary = if follow {
1313 b.target_atime.cmp(&a.target_atime)
1314 } else {
1315 b.atime.cmp(&a.atime)
1316 };
1317 if primary != std::cmp::Ordering::Equal {
1318 primary
1319 } else if follow {
1320 b.target_ansec.cmp(&a.target_ansec) // c:1019
1321 } else {
1322 b.ansec.cmp(&a.ansec) // c:992
1323 }
1324 } else if key_unshifted == GS_MTIME {
1325 // c:995-1001 — `r = a->mtime - b->mtime;` then tie-break by
1326 // `a->mnsec - b->mnsec` (under GET_ST_MTIME_NSEC). Without
1327 // the nsec tie-break, files touched <1s apart sort by name.
1328 let primary = if follow {
1329 b.target_mtime.cmp(&a.target_mtime)
1330 } else {
1331 b.mtime.cmp(&a.mtime)
1332 };
1333 if primary != std::cmp::Ordering::Equal {
1334 primary
1335 } else if follow {
1336 b.target_mnsec.cmp(&a.target_mnsec) // c:1026
1337 } else {
1338 b.mnsec.cmp(&a.mnsec) // c:999
1339 }
1340 } else if key_unshifted == GS_CTIME {
1341 // c:1002-1008 — `r = a->ctime - b->ctime;` then tie-break by
1342 // `a->cnsec - b->cnsec` (under GET_ST_CTIME_NSEC).
1343 let primary = if follow {
1344 b.target_ctime.cmp(&a.target_ctime)
1345 } else {
1346 b.ctime.cmp(&a.ctime)
1347 };
1348 if primary != std::cmp::Ordering::Equal {
1349 primary
1350 } else if follow {
1351 b.target_cnsec.cmp(&a.target_cnsec) // c:1033
1352 } else {
1353 b.cnsec.cmp(&a.cnsec) // c:1006
1354 }
1355 } else if key_unshifted == GS_LINKS {
1356 // c:1010 — `r = b->links - a->links;` — SAME sign convention
1357 // as GS_SIZE (c:985), so ascending `ol` is fewest-links-first.
1358 // (Was reversed vs GS_SIZE, putting most-linked first.)
1359 if follow {
1360 a.target_links.cmp(&b.target_links)
1361 } else {
1362 a.links.cmp(&b.links)
1363 }
1364 } else if key_unshifted == GS_EXEC {
1365 // c:974
1366 let idx = ((key as u32) >> 16) as usize;
1367 let asx = a.sort_strings.get(idx).map(|s| s.as_str()).unwrap_or("");
1368 let bsx = b.sort_strings.get(idx).map(|s| s.as_str()).unwrap_or("");
1369 // c:981 `zstrcmp(*bsortstrp, *asortstrp, …)` — unlike GS_NAME's
1370 // unmetafied `uname`, the keys are METAFIED: c:1938-1941 stores
1371 // `getsparam("REPLY")` or `tmpptr->name` as-is. Under a UTF-8
1372 // locale that decides the order, so collate C's bytes.
1373 zstrcmp(
1374 crate::metafied_key::metafied_key(asx),
1375 crate::metafied_key::metafied_key(bsx),
1376 if numeric_sort {
1377 crate::zsh_h::SORTIT_NUMERICALLY as u32
1378 } else {
1379 0
1380 },
1381 )
1382 } else {
1383 std::cmp::Ordering::Equal // GS_NONE / unknown
1384 };
1385 if cmp != std::cmp::Ordering::Equal {
1386 return if (tp & GS_DESC) != 0 {
1387 cmp.reverse()
1388 } else {
1389 cmp
1390 };
1391 }
1392 }
1393 std::cmp::Ordering::Equal
1394}
1395
1396// `Redirect` struct + `RedirectType` enum + `xpandredir` fn
1397// DELETED. Both types were Rust-only duplicates of `parse::Redirect`
1398// / `parse::RedirectOp` with no callers. The `xpandredir` impl took
1399// the wrong signature anyway — C's `xpandredir(struct redir *fn,
1400// LinkList redirtab)` at `Src/glob.c:2150` mutates a linked-list
1401// in place and returns int; this Rust version returned `Vec<Redirect>`
1402// and operated on the duplicate Redirect type. Port `xpandredir`
1403// freshly against `parse::Redirect` when an actual caller appears.
1404
1405// ============================================================================
1406// Exec string for sorting (from glob.c glob_exec_string)
1407// ============================================================================
1408
1409/// Port of `static char *glob_exec_string(char **sp)` from
1410/// `Src/glob.c:1085`.
1411///
1412/// **C is a PARSER, not an executor.** It extracts the qualifier
1413/// argument string from `*sp` (advancing the pointer past the
1414/// delimiters) and returns the duplicated text. The actual eval
1415/// happens at the call sites (c:1680/1713/1747) which feed the
1416/// returned string into `qualsheval` (for `e:`/`+:`) or
1417/// `gf_sortlist[].exec` (for sort).
1418///
1419/// Previous Rust port was COMPLETELY MIS-IMPLEMENTED:
1420/// 1. **Signature wrong:** Rust took `(cmd, filename)` and returned
1421/// command output. C takes `(**sp)` and returns the parsed
1422/// qualifier string (the `cmd` itself, NOT its output).
1423/// 2. **Forked `/bin/sh`:** spawned a separate POSIX shell process
1424/// to run the cmd. That's even more broken than `qualsheval`
1425/// was, because the function isn't supposed to execute anything
1426/// at all at this layer.
1427///
1428/// Now mirrors C's parser body (c:1090-1117): handle the `+` prefix
1429/// (identifier form) vs the `e:` delimited form (get_strarg), set
1430/// `*sp` past the closing delimiter, return the dup'd inner text.
1431///
1432/// Returns `(parsed_string, advance_offset)` so the caller can
1433/// emulate C's `*sp = tt + plus;` pointer advance via slice
1434/// indexing.
1435/// WARNING: param names don't match C — Rust=(s, plus) vs C=(sp)
1436pub fn glob_exec_string(s: &str, plus_form: bool) -> Option<(String, usize)> {
1437 // c:1085
1438 if plus_form {
1439 // c:1090
1440 // c:1092 — `tt = itype_end(s, IIDENT, 0);`
1441 // c:1093-1097 — `if (tt == s) { zerr("missing identifier after `+'"); return NULL; }`
1442 let tt = crate::ported::utils::itype_end(s, crate::ported::ztype_h::IIDENT as u32, false);
1443 if tt == 0 {
1444 // c:1093
1445 zerr("missing identifier after `+'"); // c:1095
1446 return None; // c:1096
1447 }
1448 // c:1107 — `sdata = dupstring(s + plus);` (plus=0 here).
1449 // c:1108 — `untokenize(sdata);`
1450 // c:1113 — `*sp = tt;` → advance offset is `tt`.
1451 Some((crate::ported::lex::untokenize(&s[..tt]), tt))
1452 } else {
1453 // c:1099 — `tt = get_strarg(s, &plus);` — find matching delimiter.
1454 // get_strarg returns delimiter-balanced span; for `e:foo:` it
1455 // walks `s` past the inner expr and returns position of the
1456 // closing `:`.
1457 match crate::ported::subst::get_strarg(s) {
1458 Some((del, content, rest)) => {
1459 // Advance offset: bytes consumed of `s` = s.len() - rest.len().
1460 let advance = s.len() - rest.len();
1461 // c:1100-1104 — `if (!*tt) { zerr("missing end of string"); return NULL; }`
1462 // — `tt` sits on the closing delimiter; `*tt == '\0'` means
1463 // none was found. The Rust get_strarg returns an empty `rest`
1464 // both for a closer at the very end and for no closer, so
1465 // tell them apart by length: with a closer, the consumed span
1466 // is the opening delimiter + content + the closer.
1467 if advance <= del.len_utf8() + content.len() {
1468 zerr("missing end of string"); // c:1102
1469 return None; // c:1103
1470 }
1471 // c:1107 — `sdata = dupstring(s + plus);`
1472 // c:1108 — `untokenize(sdata);`
1473 // c:1110-1111 — `*sp = tt + plus;`.
1474 Some((crate::ported::lex::untokenize(&content), advance))
1475 }
1476 None => {
1477 zerr("missing end of string"); // c:1102
1478 None
1479 }
1480 }
1481 }
1482}
1483
1484/*
1485 * Insert a glob match.
1486 * If there were words to prepend given by the P glob qualifier, do so.
1487 */
1488// c:1120-1123
1489/// Port of `insert_glob_match(LinkList list, LinkNode next, char *data)`
1490/// from Src/glob.c:1125. Inserts `data` at `next`, with optional
1491/// `gf_pre_words` prefix and `gf_post_words` suffix injection from
1492/// CURGLOBDATA (the `P:before:after:` glob qualifier).
1493pub fn insert_glob_match(list: &mut Vec<String>, next: usize, data: &str) {
1494 // c:1125
1495 let (pre, post) = {
1496 let gd = CURGLOBDATA.lock().unwrap_or_else(|e| e.into_inner());
1497 (gd.gf_pre_words.clone(), gd.gf_post_words.clone()) // c:1127, c:1136
1498 };
1499 // C `insertlinknode(list, next, data)` inserts AFTER node `next`,
1500 // returning the newly added node (so subsequent inserts append in
1501 // order). Our Vec<String> analog inserts at index `next+1` and
1502 // bumps `next` to point at the just-inserted slot.
1503 let mut cur = next;
1504 let n = list.len();
1505 let mut clamp = |i: usize| -> usize {
1506 if i > n {
1507 n
1508 } else {
1509 i
1510 }
1511 };
1512 if let Some(pre_words) = pre {
1513 // c:1127 `if (gf_pre_words)`
1514 for w in pre_words.iter() {
1515 // c:1129
1516 let pos = clamp(cur + 1);
1517 list.insert(pos, w.clone()); // c:1130 `dupstring(getdata(added))`
1518 cur = pos; // c:1130 — return-value advances `next`
1519 }
1520 }
1521 let pos = clamp(cur + 1);
1522 list.insert(pos, data.to_string()); // c:1134
1523 cur = pos;
1524 if let Some(post_words) = post {
1525 // c:1136 `if (gf_post_words)`
1526 for w in post_words.iter() {
1527 // c:1138
1528 let pos = clamp(cur + 1);
1529 list.insert(pos, w.clone()); // c:1139
1530 cur = pos;
1531 }
1532 }
1533}
1534
1535/// Port of `checkglobqual(char *str, int sl, int nobareglob, char **sp)` from Src/glob.c:1160.
1536/// C: `int checkglobqual(char *str, int sl, int nobareglob, char **sp)` —
1537/// confirm the trailing `(...)` is a glob qualifier (not a set of
1538/// alternatives or an exclusion). Returns 1 for a bare qualifier
1539/// list, 2 for the explicit `(#q…)` form, 0 otherwise, and writes the
1540/// index of the opening parenthesis to `*sp` (C: the pointer itself).
1541///
1542/// `str` must be in LEXER-TOKENIZED form. EVERY test in the C body is
1543/// against a TOKEN, never the raw ASCII byte — `Outpar` at c:1163,
1544/// `Inpar` at c:1170/1189/1183, `Bar` at c:1175, `Tilde` at c:1179,
1545/// `Pound` at c:1192. That is exactly what stops a QUOTED `)`, which
1546/// reaches here as the plain byte, from closing the qualifier block:
1547/// in `*(.e['[[ $REPLY == a* ]]'])` the quoted `]` bytes are ordinary
1548/// text while the real closer is `Outbrack`.
1549///
1550/// The previous body here scanned raw ASCII `(`/`)`, dropped
1551/// `nobareglob` on the floor, and had no `#q` / `Bar` / `Tilde` arms,
1552/// so it could not distinguish a quoted delimiter from an active one.
1553pub fn checkglobqual(
1554 str: &[char],
1555 sl: i32,
1556 nobareglob: i32, // c:1160
1557 sp: &mut Option<usize>,
1558) -> i32 {
1559 use crate::ported::zsh_h::{Bar, Inpar, Outpar, Pound, Tilde};
1560
1561 let mut nobareglob = nobareglob;
1562 let mut ret = 1i32; // c:1162 `int paren, ret = 1;`
1563 let sl = sl as usize;
1564 // c:1163-1164 — `if (str[sl - 1] != Outpar) return 0;`
1565 if sl < 2 || str[sl - 1] != Outpar {
1566 return 0;
1567 }
1568
1569 // c:1169-1187 — walk back from `str + sl - 2` to the matching
1570 // `Inpar`, tracking nesting in `paren`. `Outpar` falls THROUGH to
1571 // the `Bar` arm, so a nested group, an alternation, or an
1572 // EXTENDEDGLOB exclusion each force `nobareglob`.
1573 let mut paren = 0i32; // c:1169
1574 let mut start: Option<usize> = None;
1575 let mut i = sl - 2;
1576 loop {
1577 // c:1170 loop condition — `*s && (*s != Inpar || paren)`
1578 if str[i] == Inpar && paren == 0 {
1579 start = Some(i);
1580 break;
1581 }
1582 match str[i] {
1583 // c:1172-1174 `case Outpar: paren++; /*FALLTHROUGH*/`
1584 Outpar => {
1585 paren += 1;
1586 nobareglob = 1; // c:1175-1177 via the fallthrough into Bar
1587 }
1588 // c:1175-1178 `case Bar:` — an alternation.
1589 Bar => nobareglob = 1,
1590 // c:1179-1182 `case Tilde:` — EXTENDEDGLOB exclusion.
1591 Tilde if glob_isset(EXTENDEDGLOB) => nobareglob = 1,
1592 // c:1183-1185 `case Inpar: paren--;`
1593 Inpar => paren -= 1,
1594 _ => {}
1595 }
1596 // c:1186-1187 `if (s == str) break;`
1597 if i == 0 {
1598 break;
1599 }
1600 i -= 1;
1601 }
1602 // c:1189-1190 `if (*s != Inpar) return 0;`
1603 let start = match start {
1604 Some(v) => v,
1605 None => return 0,
1606 };
1607
1608 // c:1191-1198 — under EXTENDEDGLOB a leading `Pound` marks the
1609 // explicit `(#q…)` qualifier; any other `(#X…)` is an inline
1610 // pattern flag, not a qualifier block.
1611 if glob_isset(EXTENDEDGLOB) && str.get(start + 1) == Some(&Pound) {
1612 if str.get(start + 2) != Some(&'q') {
1613 return 0; // c:1193-1194
1614 }
1615 ret = 2; // c:1195
1616 } else if nobareglob != 0 {
1617 return 0; // c:1197-1198
1618 }
1619
1620 // c:1200-1201 `if (sp) *sp = s;`
1621 *sp = Some(start);
1622 ret // c:1203
1623}
1624
1625/// Port of `zglob(LinkList list, LinkNode np, int nountok)` from
1626/// Src/glob.c:1214. Top-level glob expansion: gate on GLOBOPT/
1627/// EXECOPT/haswilds (c:1230-1234), remove the placeholder node,
1628/// expand the pattern via `glob_path` (which covers the c:1240-2012
1629/// qualifier+scanner+sort body), then splice the resulting matches
1630/// back at `node` via `insert_glob_match` (c:1995-2007).
1631pub fn zglob(list: &mut Vec<String>, np: usize, nountok: i32) {
1632 // c:1214
1633 if np >= list.len() {
1634 return;
1635 }
1636 // c:1217 — `LinkNode node = prevnode(np);` — the insertion point
1637 // after the placeholder is uremnode'd. Vec analog: insert at np.
1638 let node: usize = np; // c:1217
1639 let ostr = list[np].clone(); // c:1221 `ostr = getdata(np)`
1640 // c:1226 — `nobareglob = !isset(BAREGLOBQUAL);` (consumed by qualifier
1641 // parser inside glob_path).
1642
1643 // c:1230 — `if (unset(GLOBOPT) || !haswilds(ostr) || unset(EXECOPT))`
1644 if !crate::ported::zsh_h::isset(crate::ported::zsh_h::GLOBOPT)
1645 || !haswilds(&ostr)
1646 || !crate::ported::zsh_h::isset(crate::ported::zsh_h::EXECOPT)
1647 {
1648 if nountok == 0 {
1649 // c:1231
1650 // c:1232 — untokenize in-place; replace list[np] with the
1651 // untokenized form.
1652 list[np] = crate::ported::lex::untokenize(&ostr);
1653 }
1654 return; // c:1233
1655 }
1656
1657 // c:1235 — `save_globstate(saved);`. zshrs snapshots the
1658 // glob-relevant options into TLS (`enter_glob_scope`, required for
1659 // thread-safety) — the RAII guard restores them on return.
1660 let _glob_scope = enter_glob_scope();
1661
1662 // c:1237-1238 — `str = dupstring(ostr); uremnode(list, np);`
1663 list.remove(np); // c:1238
1664
1665 // c:1240-1995 — the scanner walk + match collection. zshrs drives
1666 // it through `globdata_glob`, the RUST-ONLY read_dir adaptation of
1667 // C's `scanner()` (c:500; C's `lchdir` descent is process-global and
1668 // unsafe under zshrs's threading). globdata_glob owns its own
1669 // globdata; call it directly rather than via the glob_path Vec
1670 // convenience.
1671 // c:1254 `gf_nullglob = isset(NULLGLOB);` then c:1567-1569
1672 // `case 'N': gf_nullglob = !(sense & 1);` — the `(N)` QUALIFIER raises
1673 // the per-glob bit, and c:1873's `else if (!gf_nullglob)` reads THAT, not
1674 // the option. The qualifier parse lives inside `globdata_glob`, whose
1675 // state this dropped on the floor, so the terminal block below fell back
1676 // to the global option and `zglob` treated `PAT(N)` with no match as a
1677 // hard NOMATCH error. Only the fusevm path (`vm_helper::expand_glob`)
1678 // honoured `(N)`; every programmatic `zglob` caller — `_files`,
1679 // `_path_files`, `_terminals`, `_time_zone`, `_correct_filename` — got the
1680 // diagnostic and the errflag instead of an empty list.
1681 let (matches, gf_nullglob) = {
1682 let mut st = globdata::new();
1683 let m = globdata_glob(&mut st, &ostr);
1684 let n = st
1685 .qualifiers
1686 .as_ref()
1687 .map(|q| q.nullglob)
1688 .unwrap_or(false); // c:1567-1569
1689 (m, n)
1690 };
1691
1692 // c:1871-1875 — badcshglob accounting. Each zglob run updates
1693 // the per-command-line counter so globlist's terminal diagnostic
1694 // can distinguish "some failures, no successes" (emit error)
1695 // from "some failures, some successes" (silent).
1696 if !matches.is_empty() {
1697 // c:1872 — `badcshglob |= 2;` (at least one expansion OK).
1698 BADCSHGLOB.fetch_or(2, std::sync::atomic::Ordering::Relaxed);
1699 } else if crate::ported::zsh_h::isset(crate::ported::zsh_h::CSHNULLGLOB) {
1700 // c:1874-1875 — `badcshglob |= 1;` (at least one expansion
1701 // failed) under CSHNULLGLOB.
1702 BADCSHGLOB.fetch_or(1, std::sync::atomic::Ordering::Relaxed);
1703 }
1704
1705 // c:1872-1888 — `Deal with failures to match depending on options`.
1706 // C body verbatim (c:1872-1888):
1707 // if (matchct)
1708 // badcshglob |= 2;
1709 // else if (!gf_nullglob) {
1710 // if (isset(CSHNULLGLOB)) {
1711 // badcshglob |= 1;
1712 // } else if (isset(NOMATCH)) {
1713 // zerr("no matches found: %s", ostr);
1714 // zfree(matchbuf, 0);
1715 // restore_globstate(saved);
1716 // return;
1717 // } else {
1718 // /* treat as an ordinary string */
1719 // untokenize(matchptr->name = dupstring(ostr));
1720 // matchptr++;
1721 // matchct = 1;
1722 // }
1723 // }
1724 // gf_nullglob (c:212) is the PER-GLOB nullglob bit: seeded from the
1725 // option at c:1254 and raised by the `(N)` qualifier at c:1567-1569.
1726 // Parity bug #13: previously this Rust arm fell through to the
1727 // ordinary-literal path (c:1882-1887) unconditionally, making
1728 // `echo /never/*` print the literal glob instead of erroring.
1729 if matches.is_empty() {
1730 // c:1254 + c:1567-1569 — `gf_nullglob = isset(NULLGLOB)` then the
1731 // qualifier override. Reading the option ALONE (what this did) made
1732 // `(N)` inert for every programmatic caller.
1733 let nullglob = gf_nullglob || isset(crate::ported::zsh_h::NULLGLOB); // c:1873 !gf_nullglob
1734 let csh_nullglob = isset(crate::ported::zsh_h::CSHNULLGLOB); // c:1874
1735 // c:Src/glob.c:1843-1854 — `if (!q || errflag) { ... zerr(
1736 // "bad pattern", ostr); return; }`. When the qualifier
1737 // parser already emitted a diagnostic (e.g. "number expected"
1738 // from qgetnum at c:832) and set errflag, the no-matches /
1739 // bad-pattern terminal block runs but the prior zerr is what
1740 // the user sees first. Skipping the redundant "no matches
1741 // found" here matches zsh which has already aborted glob
1742 // expansion via the errflag-gated return at c:1787 / c:1843.
1743 if errflag.load(Ordering::SeqCst) != 0 {
1744 return;
1745 }
1746 // c:1872 — `else if (!gf_nullglob) { ... }`. The ENTIRE no-match
1747 // handling (CSHNULLGLOB, NOMATCH, literal fallback) is gated on
1748 // nullglob being OFF. When nullglob is set the failed word is
1749 // simply dropped: C never enters the block, matchbuf stays empty,
1750 // and glob() removes the word from the list. Hoisting the
1751 // `!nullglob` test to a single outer gate (matching the C
1752 // `else if`) ensures the literal-insert fallback below is skipped
1753 // under nullglob too — previously it fell through unconditionally
1754 // and echoed the literal, diverging from c:1872.
1755 if !nullglob {
1756 if csh_nullglob {
1757 // c:1874-1875 — `if (isset(CSHNULLGLOB)) { badcshglob |= 1; }`
1758 // (already recorded above). The else-if chain means the
1759 // ordinary-string arm is NOT reached: the failed word is
1760 // DROPPED like nullglob, and globlist's terminal check
1761 // (subst.c:505-507, ported at subst.rs:1419) emits the
1762 // csh-style `no match` when NO word on the line matched.
1763 // Falling through to the literal insert here produced the
1764 // NOMATCH-style "no matches found: PAT" instead.
1765 return;
1766 }
1767 if isset(crate::ported::zsh_h::NOMATCH) {
1768 // c:1876-1880 — `else if (isset(NOMATCH)) { zerr; return; }`
1769 // c:1877 — `ostr` is tokenized; zerrmsg's `%s` renders
1770 // via nicezputs → sb_niceformat → `untokenize(ums)`
1771 // (Src/utils.c), so the message shows the source
1772 // spelling rather than raw token bytes.
1773 crate::ported::utils::zerr(&format!(
1774 "no matches found: {}",
1775 crate::ported::lex::untokenize(&ostr)
1776 ));
1777 // c:1878 — `zfree(matchbuf, 0);` (Rust drop handles it)
1778 // c:1879 — `restore_globstate(saved);` (handled by guard)
1779 return; // c:1880
1780 }
1781 // c:1882-1887 — `treat as an ordinary string`. The matchptr++
1782 // bookkeeping in C maps to our `list.insert`.
1783 let restored = if nountok == 0 {
1784 crate::ported::lex::untokenize(&ostr) // c:1884 untokenize(matchptr->name = dupstring(ostr))
1785 } else {
1786 ostr.clone()
1787 };
1788 let pos = if node > list.len() { list.len() } else { node };
1789 list.insert(pos, restored); // c:1885 matchptr++, c:1886 matchct = 1
1790 }
1791 // nullglob set → fall through with matchbuf empty: the word is
1792 // dropped (c:1872 skips the block entirely). c:1889's
1793 // `else if (in_expandredir)` redirect-failure arm is handled by
1794 // the caller (vm_helper.rs redirect-glob path), not here.
1795 return;
1796 }
1797
1798 // c:1995-2007 — splice matches back via insert_glob_match (honors
1799 // gf_pre_words / gf_post_words).
1800 let mut cur = if node == 0 { 0 } else { node - 1 }; // node is `prevnode(np)` semantics
1801 for m in matches.iter() {
1802 insert_glob_match(list, cur, m); // c:1995
1803 cur += 1;
1804 if let Some(g) = CURGLOBDATA.lock().ok() {
1805 // Advance past any pre_words that insert_glob_match added.
1806 if let Some(p) = &g.gf_pre_words {
1807 cur += p.len();
1808 }
1809 if let Some(p) = &g.gf_post_words {
1810 cur += p.len();
1811 }
1812 }
1813 }
1814}
1815
1816/// File type character for -F style listing
1817/// Render a mode bitmap as the `*` qualifier letter (`d`/`b`/
1818/// `c`/`l`/`s`/`p`/etc.).
1819/// Port of `file_type(mode_t filemode)` from Src/glob.c:2018.
1820pub fn file_type(filemode: u32) -> char {
1821 // c:2018
1822 let fmt = filemode & libc::S_IFMT as u32;
1823 if fmt == libc::S_IFBLK as u32 {
1824 '#'
1825 } else if fmt == libc::S_IFCHR as u32 {
1826 '%'
1827 } else if fmt == libc::S_IFDIR as u32 {
1828 '/'
1829 } else if fmt == libc::S_IFIFO as u32 {
1830 '|'
1831 } else if fmt == libc::S_IFLNK as u32 {
1832 '@'
1833 } else if fmt == libc::S_IFREG as u32 {
1834 if filemode & 0o111 != 0 {
1835 '*'
1836 } else {
1837 ' '
1838 }
1839 } else if fmt == libc::S_IFSOCK as u32 {
1840 '='
1841 } else {
1842 '?'
1843 }
1844}
1845
1846// ============================================================================
1847// Brace expansion
1848// ============================================================================
1849
1850/// Check if string has brace expansion
1851/// Check whether a string has brace-expansion `{a,b}` content.
1852/// Port of `hasbraces(char *str)` from Src/glob.c:2042.
1853/// WARNING: param names don't match C — Rust=(s, brace_ccl) vs C=(str)
1854pub fn hasbraces(s: &str, brace_ccl: bool) -> bool {
1855 // c:2042
1856 let mut depth = 0;
1857 let mut has_comma = false;
1858 let mut has_dotdot = false;
1859 let mut brace_open: Option<usize> = None;
1860
1861 // c:2042 — every `return 1` in the C walk sits inside the Outbrace
1862 // arm, which is only reachable after an Inbrace has raised `depth`.
1863 // No Inbrace in the string means the answer is 0, so skip the
1864 // `Vec<char>` build. Inbrace is U+e18F, whose UTF-8 encoding always
1865 // contains the byte 0x8F; testing for that byte can only
1866 // FALSE-POSITIVE (0x8F is also a continuation byte of other
1867 // characters), which just falls through to the real walk.
1868 // `<[u8]>::contains` is libcore's precompiled `memchr`.
1869 if !s.as_bytes().contains(&0x8f) {
1870 return false;
1871 }
1872
1873 let chars: Vec<char> = s.chars().collect();
1874 let len = chars.len();
1875
1876 let mut i = 0;
1877 while i < len {
1878 // c:Src/lex.c:3587-3600 — backslash escape converts the next
1879 // char into a tokenized literal (Bnull/Bnullkeep). xpandbraces
1880 // sees the tokenized form so `\{` never enters the brace
1881 // walk. Accept both the canonical Bnull (`\u{e19f}`,
1882 // Src/zsh.h:195) and ASCII `\` so direct callers (tests,
1883 // utility paths) and pipeline callers (bridge → multsub →
1884 // xpandbraces with Bnull markers from gettokstr) both behave
1885 // the same: skip the escape marker plus the next char.
1886 if (chars[i] == '\\' || chars[i] == '\u{e19f}') && i + 1 < len {
1887 i += 2; // c:3591 — skip Bnull/Bnullkeep + escaped char
1888 continue;
1889 }
1890 match chars[i] {
1891 // c:Src/glob.c:hasbraces — Inbrace/Outbrace/Comma TOKEN
1892 // strictly (\u{e18f} / \u{e190} / \u{e19a}). The lexer emits
1893 // TOKEN form for unescaped `{`/`,`/`}`; `\X` produces
1894 // Bnull + ASCII X. After remnulargs strips Bnull, the
1895 // ASCII X doesn't match the TOKEN check so escaped
1896 // braces correctly bypass expansion.
1897 '\u{e18f}' => {
1898 if depth == 0 {
1899 brace_open = Some(i);
1900 }
1901 depth += 1;
1902 }
1903 '\u{e190}' if depth > 0 => {
1904 depth -= 1;
1905 if depth == 0 {
1906 // c:2050-2061 — a comma group always expands.
1907 if has_comma {
1908 return true;
1909 }
1910 if has_dotdot {
1911 // c:2071-2096 — a `..` group expands ONLY when it
1912 // is a valid char range (bracechardots, c:2071) OR
1913 // a numeric range whose endpoints are immediately
1914 // bounded by the closing brace: C walks `[-]digits
1915 // .. [-]digits` and requires the next char to be
1916 // Outbrace (c:2084) or `..incr..` + Outbrace
1917 // (c:2087-2095). Trailing garbage (`{0..5%2}`,
1918 // `{1..3x}`, `{1.2..3}`, `{1..5.}`) fails this, so
1919 // the group stays LITERAL with braces. Nested
1920 // groups stay permissive — recursive xpandbraces
1921 // handles the inner range.
1922 let content: String =
1923 chars[brace_open.unwrap_or(0) + 1..i].iter().collect();
1924 let nested = content.contains('\u{e18f}');
1925 // c:2074-2096 — structural walk: optional `-`,
1926 // digits (0+), `..`, optional `-`, digits (0+),
1927 // then MUST end at the brace (Outbrace), or take a
1928 // `..incr` step then end. C allows EMPTY endpoints
1929 // (`{1..}`, `{..5}` pass hasbraces, then fail the
1930 // range parse and get brace-stripped) — what it
1931 // forbids is trailing non-range chars (`{0..5%2}`,
1932 // `{1.2..3}`). Closes with c:2085 `idigit(lbr[1])
1933 // || idigit(str[-1])` — a digit adjacent to `..`.
1934 // Dash TOKEN (\u{e19b}) reads as ASCII `-`.
1935 let numeric_ok = {
1936 let norm: String = content
1937 .chars()
1938 .map(|c| if c == '\u{e19b}' { '-' } else { c })
1939 .collect();
1940 let b = norm.as_bytes();
1941 let n = b.len();
1942 let mut j = 0;
1943 let mut shape_ok = false;
1944 if j < n && b[j] == b'-' {
1945 j += 1; // c:2074 leading `-`
1946 }
1947 while j < n && b[j].is_ascii_digit() {
1948 j += 1; // c:2076 first number
1949 }
1950 if j + 1 < n && b[j] == b'.' && b[j + 1] == b'.' {
1951 j += 2; // c:2078 `..`
1952 if j < n && b[j] == b'-' {
1953 j += 1; // c:2080
1954 }
1955 while j < n && b[j].is_ascii_digit() {
1956 j += 1; // c:2082 second number
1957 }
1958 if j == n {
1959 shape_ok = true; // c:2084 Outbrace
1960 } else if j + 1 < n && b[j] == b'.' && b[j + 1] == b'.' {
1961 j += 2; // c:2087 `..incr`
1962 if j < n && b[j] == b'-' {
1963 j += 1;
1964 }
1965 while j < n && b[j].is_ascii_digit() {
1966 j += 1; // c:2091
1967 }
1968 if j == n {
1969 shape_ok = true; // c:2093
1970 }
1971 }
1972 }
1973 // c:2085/2094 — digit adjacent to `..`.
1974 shape_ok
1975 && (b.first().is_some_and(|c| c.is_ascii_digit())
1976 || b.last().is_some_and(|c| c.is_ascii_digit()))
1977 };
1978 if nested
1979 || bracechardots(&content).is_some()
1980 || numeric_ok
1981 || crate::dash_mode::ksh93_char_step_range(&content)
1982 {
1983 return true;
1984 }
1985 }
1986 if brace_ccl {
1987 if let Some(open) = brace_open {
1988 if i > open + 1 {
1989 return true;
1990 }
1991 }
1992 }
1993 has_comma = false;
1994 has_dotdot = false;
1995 brace_open = None;
1996 }
1997 }
1998 // c:Src/glob.c:hasbraces — accept comma / `..` at ANY depth
1999 // (was `depth == 1` only). Nested groups like `{{1,2}}`
2000 // have the comma at depth 2; without this, hasbraces
2001 // returned false and the outer xpandbraces never ran,
2002 // leaving `{{1,2}}` literal instead of expanding to
2003 // `{1} {2}` per zsh's nested-brace pass.
2004 '\u{e19a}' if depth > 0 => has_comma = true,
2005 '.' if depth > 0 && i + 1 < len && chars[i + 1] == '.' => has_dotdot = true,
2006 _ => {}
2007 }
2008 i += 1;
2009 }
2010
2011 false
2012}
2013
2014// ============================================================================
2015// Brace char range parsing (from glob.c bracechardots)
2016// ============================================================================
2017
2018/// Parse character range in braces like {a..z} (from glob.c bracechardots line 2222)
2019/// Port of `bracechardots(char *str, convchar_t *c1p, convchar_t *c2p)` from `Src/glob.c:2222`.
2020/// WARNING: param names don't match C — Rust=(s) vs C=(str, c1p, c2p)
2021pub fn bracechardots(s: &str) -> Option<(char, char, i32)> {
2022 let chars: Vec<char> = s.chars().collect();
2023
2024 // Must be at least "a..b"
2025 if chars.len() < 4 {
2026 return None;
2027 }
2028
2029 // Find ..
2030 let dotdot_pos = s.find("..")?;
2031 if dotdot_pos == 0 {
2032 return None;
2033 }
2034
2035 let left = &s[..dotdot_pos];
2036 let right = &s[dotdot_pos + 2..];
2037
2038 // Check for increment. c:2246-2264 — C's form is exactly `c..c`
2039 // followed by Outbrace; a third `..step` part is not a character
2040 // range (`{1..2..x}` stays literal). Only bash's `{a..e..2}` takes one.
2041 let (end_str, incr) = if let Some(pos) = right.find("..") {
2042 if !crate::dash_mode::bash_mode() {
2043 return None;
2044 }
2045 let end = &right[..pos];
2046 // The step arrives TOKENIZED: a leading `-` is the Dash token, which
2047 // `str::parse` rejects, so `{a..e..-2}` stayed literal. zstrtol
2048 // (c:Src/utils.c:2427) reads Dash as `-`; untokenize to the same effect.
2049 let inc: i32 = crate::lex::untokenize(&right[pos + 2..]).parse().ok()?;
2050 (end, inc)
2051 } else {
2052 (right, 1)
2053 };
2054
2055 // Single character range
2056 // c:2235-2236 — `MB_METACHARINIT(); pnext += MB_METACHARLENCONV(pconv,
2057 // &cstart);` — the endpoint is decoded as ONE multibyte character out
2058 // of the METAFIED text, not counted as a Rust `char`. zshrs stores a
2059 // raw 8-bit byte as the metafied pair `Meta` + `byte ^ 32` (two
2060 // `char`s), so `chars().count() == 1` rejected `{$'\x80'..$'\x81'}`
2061 // outright, and a real multibyte endpoint (`{é..ê}`) was rejected by
2062 // the byte-length test in `expand_range`.
2063 // c:2237-2244 — `if (cstart == WEOF || pnext[0] != '.' ...) return 0;`
2064 // With the MULTIBYTE option off a unit is a single BYTE, which is what
2065 // makes the range over 8-bit endpoints legal only in that mode
2066 // (mb_metacharlenconv, Src/utils.c:5613).
2067 // c:2227-2234 / c:2248-2255 — an endpoint that is a lexer token stands
2068 // for its source character: `if (itok(*pnext)) { if (*pnext == Inbrace)
2069 // return 0; convstr[0] = ztokens[*pnext - Pound]; … }`. Decoding the
2070 // token char itself only happened to work while the locale was UTF-8:
2071 // under LANG=C `left{[..]}right` (Inbrack..Outbrack) decoded to WEOF
2072 // and the range was not recognised.
2073 let untok = |t: &str| -> Option<String> {
2074 let mut it = t.chars();
2075 match (it.next(), it.next()) {
2076 (Some(c), None) if crate::token_char::itok_char(c) => {
2077 if c == crate::ported::zsh_h::Inbrace {
2078 return None; // c:2229
2079 }
2080 let idx = (c as u32 - Pound as u32) as usize;
2081 Some(ZTOKENS[idx..idx + 1].to_string()) // c:2230
2082 }
2083 _ => Some(t.to_string()),
2084 }
2085 };
2086 let left = untok(left)?;
2087 let end_str = untok(end_str)?;
2088 let lb = crate::ported::utils::unmetafy_str(&left); // c:2236
2089 let (l_len, cstart, _) = crate::ported::utils::mb_metacharlenconv(&lb); // c:2236
2090 // c:2256-2257 — same decode for the last character of the range.
2091 let rb = crate::ported::utils::unmetafy_str(&end_str); // c:2257
2092 let (r_len, cend, _) = crate::ported::utils::mb_metacharlenconv(&rb); // c:2257
2093 // c:2239/2264 — `cstart == WEOF` / `*pnext != Outbrace`: the endpoint
2094 // must decode AND consume exactly the whole endpoint text.
2095 if l_len != 0 && l_len == lb.len() && r_len != 0 && r_len == rb.len() {
2096 if let (Some(c1), Some(c2)) = (cstart, cend) {
2097 return Some((c1, c2, incr)); // c:2266-2270
2098 }
2099 }
2100
2101 None
2102}
2103
2104/// Expand braces in a string
2105/// Brace-expand a string into a flat list.
2106/// Port of `xpandbraces(LinkList list, LinkNode *np)` from Src/glob.c:2276 — same
2107/// `{a,b}` / `{1..10}` / `{a-z}` handling.
2108/// WARNING: param names don't match C — Rust=(s, brace_ccl) vs C=(list, np)
2109pub fn xpandbraces(s: &str, brace_ccl: bool) -> Vec<String> {
2110 // c:2276
2111 if !hasbraces(s, brace_ccl) {
2112 return vec![s.to_string()];
2113 }
2114
2115 // Inline single-brace expansion — direct port of the per-iteration
2116 // brace-scan inside zsh's xpandbraces (Src/glob.c:2276). Walks the
2117 // string, finds the first `{`...`}` group, classifies as range
2118 // (`a..b`) / comma (`a,b`) / ccl (`[abc]`-style char-class), and
2119 // dispatches to the matching expander. Returns Some(parts) on
2120 // expansion, None if no brace group or unmatched.
2121 // c:Src/glob.c:2276 xpandbraces — advances `lbr` through every
2122 // candidate `{` and tries each. Bug #575: zshrs only tried the
2123 // FIRST `{` and returned None for the whole string when that
2124 // group was not expandable, so `{a-c}{1..3}` left the `{1..3}`
2125 // unexpanded. Mirror C by carrying a `from` offset and walking
2126 // to the next `{` on failure.
2127 //
2128 // Returns (Some(parts), _) on successful expansion. Returns
2129 // (None, Some(next_from)) when the current `{...}` was found but
2130 // didn't expand — outer loop should retry from next_from to scan
2131 // for a later expandable group. Returns (None, None) when no
2132 // `{...}` remains.
2133 let try_expand_from = |s: &str, from: usize| -> (Option<Vec<String>>, Option<usize>) {
2134 let chars: Vec<char> = s.chars().collect();
2135 let len = chars.len();
2136 // c:Src/glob.c:xpandbraces — Inbrace TOKEN strict.
2137 let start = match chars[from..].iter().position(|&c| c == '\u{e18f}') {
2138 Some(p) => from + p,
2139 None => return (None, None),
2140 };
2141 let mut depth = 1;
2142 let mut comma_positions = Vec::new();
2143 let mut dotdot_pos = None;
2144 for i in (start + 1)..len {
2145 match chars[i] {
2146 '\u{e18f}' => depth += 1,
2147 '\u{e190}' => {
2148 depth -= 1;
2149 if depth == 0 {
2150 let next_from = i + 1;
2151 let prefix: String = chars[..start].iter().collect();
2152 let suffix: String = chars[i + 1..].iter().collect();
2153 let content: String = chars[start + 1..i].iter().collect();
2154 if let Some(dp) = dotdot_pos {
2155 if comma_positions.is_empty() {
2156 if let Some(parts) = expand_range(&prefix, &content, dp, &suffix) {
2157 return (Some(parts), None);
2158 }
2159 // c:Src/glob.c:2476-2506 — when range
2160 // parsing fails INSIDE C's xpandbraces
2161 // (err != 0 set at c:2355/2360/2369/2371),
2162 // the function falls through to the
2163 // normal-comma-expansion path. With no
2164 // commas, that loop produces a single
2165 // `prefix + content + suffix` — i.e.
2166 // strips the outermost braces only.
2167 // BUT: C only reaches xpandbraces when
2168 // `hasbraces()` (c:2042) returned 1,
2169 // and hasbraces requires DIGITS in the
2170 // range endpoints (c:2076-2096) for the
2171 // numeric form. Patterns like
2172 // `{hello..world}` fail hasbraces and
2173 // never enter xpandbraces — they stay
2174 // as literal `{hello..world}`.
2175 //
2176 // Rust's hasbraces is more permissive
2177 // (accepts any `..` inside `{}`), so we
2178 // gate the strip-braces fallback on the
2179 // SAME shape C uses: at least one digit
2180 // must appear in the left or right
2181 // endpoint, mirroring c:2085 `idigit
2182 // (lbr[1]) || idigit(str[-1])`.
2183 // `dp` is a CHAR index; byte-slice via a
2184 // converted offset so multibyte/metafied
2185 // content (`{$'\x80'..$'\x81'}`) can't panic.
2186 let dp_byte = content
2187 .char_indices()
2188 .nth(dp)
2189 .map(|(b, _)| b)
2190 .unwrap_or(content.len());
2191 let left = &content[..dp_byte];
2192 let right = &content[dp_byte + 2..];
2193 let strip_end =
2194 right.find("..").map(|p| &right[..p]).unwrap_or(right);
2195 let has_digit = left.chars().any(|c| c.is_ascii_digit())
2196 || strip_end.chars().any(|c| c.is_ascii_digit());
2197 if has_digit && !crate::dash_mode::ksh93_mode() {
2198 // c:2495-2498 — strip braces.
2199 return (
2200 Some(vec![format!("{}{}{}", prefix, content, suffix)]),
2201 None,
2202 );
2203 }
2204 // Non-digit `..` content (e.g.
2205 // `{hello..world}`) — C wouldn't have
2206 // entered xpandbraces. Preserve the
2207 // literal pattern intact. Allow outer
2208 // loop to retry from next_from in case
2209 // a later brace group is expandable.
2210 return (None, Some(next_from));
2211 }
2212 }
2213 if !comma_positions.is_empty() {
2214 return (
2215 expand_comma(&prefix, &content, &comma_positions, &suffix),
2216 None,
2217 );
2218 }
2219 if brace_ccl && !content.is_empty() {
2220 return (expand_ccl(&prefix, &content, &suffix), None);
2221 }
2222 // Outer brace has no comma/dotdot at depth 1,
2223 // but content may contain nested braces (e.g.
2224 // `{{1,2}}` → `{1} {2}` — outer braces become
2225 // literal, inner expands).
2226 if content.contains('\u{e18f}') {
2227 let inner_expanded = xpandbraces(&content, brace_ccl);
2228 let mut out: Vec<String> = Vec::with_capacity(inner_expanded.len());
2229 for piece in inner_expanded {
2230 out.push(format!("{}{{{}}}{}", prefix, piece, suffix));
2231 }
2232 return (Some(out), None);
2233 }
2234 // Literal-only group: allow outer to retry
2235 // from next_from for a later expandable group.
2236 return (None, Some(next_from));
2237 }
2238 }
2239 '\u{e19a}' if depth == 1 => comma_positions.push(i - start - 1),
2240 '.' if depth == 1 && i + 1 < len && chars[i + 1] == '.' && dotdot_pos.is_none() => {
2241 dotdot_pos = Some(i - start - 1);
2242 }
2243 _ => {}
2244 }
2245 }
2246 (None, None)
2247 };
2248
2249 // Outer loop: try expanding starting from each `{` in turn.
2250 // Returns first successful expansion; None if none found.
2251 let try_expand_one = |s: &str| -> Option<Vec<String>> {
2252 let mut from = 0;
2253 loop {
2254 let (result, next) = try_expand_from(s, from);
2255 if let Some(parts) = result {
2256 return Some(parts);
2257 }
2258 match next {
2259 Some(nf) if nf > from => from = nf,
2260 _ => return None,
2261 }
2262 }
2263 };
2264
2265 let mut results = vec![s.to_string()];
2266 let mut changed = true;
2267 while changed {
2268 changed = false;
2269 let mut new_results = Vec::new();
2270 for item in &results {
2271 if let Some(expanded) = try_expand_one(item) {
2272 new_results.extend(expanded);
2273 changed = true;
2274 } else {
2275 new_results.push(item.clone());
2276 }
2277 }
2278 results = new_results;
2279 }
2280 results
2281}
2282
2283/// Simple glob pattern matching
2284/* check to see if a matches b (b is not a filename pattern) */ // c:2510
2285// !!! WARNING: RUST-ONLY HELPER — extra args + flipped arg order vs C !!!
2286// C signature: `int matchpat(char *a, char *b)` — `a` = text to match,
2287// `b` = pattern. Rust callers (cond.rs, watch.rs, glob.rs internal users)
2288// pass `(pattern, text, extended, case_sensitive)` — pattern-FIRST,
2289// flipped from C, plus two per-call option overrides. The BODY below
2290// is C-faithful for the matching path (patcompile + pattry, no Unicode
2291// case-folding); `extended`/`case_sensitive` drive transient
2292// `opt_state_set` overrides around the patcompile call. The transient
2293// swap is the WRONG mechanism — patcompile is not reentrant under
2294// concurrent option mutation. SOLE acceptable use: this same-process,
2295// single-threaded matchpat call path. Replace with: all callers
2296// migrate to (a) global setopt and (b) canonical C arg order
2297// `matchpat(text, pattern)`.
2298// !!! WARNING: RUST-ONLY HELPER — extra args + flipped arg order vs C !!!
2299/// Port of `matchpat(char *a, char *b)` from `Src/glob.c:2514`.
2300/// CALLER CONVENTION: Rust=(pattern, text, ext, cs) — pattern FIRST
2301/// (FLIPPED from C `matchpat(text=a, pattern=b)`). Body remaps to
2302/// C order internally.
2303pub fn matchpat(pattern_in: &str, text_in: &str, extended: bool, case_sensitive: bool) -> bool {
2304 // Remap to C names (a=text, b=pattern) at the function boundary so
2305 // the body below reads identically to Src/glob.c:2514-2530.
2306 let a = text_in;
2307 let b = pattern_in;
2308 // c:2514
2309 crate::ported::signals_h::queue_signals(); // c:2519
2310 // GF_IGNCASE handling: zshrs's pattern matcher DOES honor the
2311 // flag in `patmatch_internal` (pattern.rs:2245/2312/2333 — P_EXACTLY,
2312 // P_ANYOF, P_ANYBUT all check `glob_flags & (GF_IGNCASE|GF_LCMATCHUC)`
2313 // and case-fold appropriately). The pre-fold below is a defensive
2314 // fast path for the simple case-insensitive `:#`/`:#:` filter — it
2315 // bypasses the matcher's per-arm fold and avoids any stale-cache
2316 // edge case in the compile→match handoff. Costs one to_lowercase
2317 // call per side; correct under all matcher implementations.
2318 let (a_eff, b_eff) = if case_sensitive {
2319 (a.to_string(), b.to_string())
2320 } else {
2321 (a.to_lowercase(), b.to_lowercase())
2322 };
2323 // Per-call option override (Rust-only): snapshot, set, restore.
2324 let prev_extended = crate::ported::options::opt_state_get("extendedglob");
2325 let prev_caseglob = crate::ported::options::opt_state_get("caseglob");
2326 crate::ported::options::opt_state_set("extendedglob", extended);
2327 crate::ported::options::opt_state_set("caseglob", case_sensitive);
2328 // c:2521 — `if (!(p = patcompile(b, PAT_STATIC, NULL)))`. Rust uses
2329 // PAT_HEAPDUP (=0) — pattern::patmatch's canonical compile path;
2330 // PAT_STATIC's static-buffer path is incomplete in zshrs.
2331 let p_opt = crate::ported::pattern::patcompile(
2332 &{
2333 let mut __pat_tok = (&b_eff).to_string();
2334 crate::ported::glob::tokenize(&mut __pat_tok);
2335 __pat_tok
2336 },
2337 crate::ported::zsh_h::PAT_HEAPDUP,
2338 None,
2339 );
2340 if let Some(v) = prev_extended {
2341 crate::ported::options::opt_state_set("extendedglob", v);
2342 }
2343 if let Some(v) = prev_caseglob {
2344 crate::ported::options::opt_state_set("caseglob", v);
2345 }
2346 let ret = match p_opt {
2347 Some(p) => crate::ported::pattern::pattry(&p, &a_eff), // c:2525
2348 None => {
2349 crate::ported::utils::zerr(&format!("bad pattern: {}", b)); // c:2522
2350 false // c:2523
2351 }
2352 };
2353 crate::ported::signals_h::unqueue_signals(); // c:2527
2354 ret // c:2529
2355}
2356
2357/// Port of `get_match_ret(Imatchdata imd, int b, int e)` from `Src/glob.c:2550`.
2358///
2359/// C returns `char *`: `NULL` when there is nothing to emit, `imd->mstr`
2360/// when the match was recorded into `repllist` (SUB_GLOBAL / SUB_LIST),
2361/// else a freshly built buffer. Rust returns `Option<String>` (`None` ==
2362/// C `NULL`). `b`/`e` arrive as *unmetafied* byte offsets and are first
2363/// re-based to metafied byte offsets, exactly as C does. Takes `&mut`
2364/// because the SUB_GLOBAL/SUB_LIST branch pushes onto `imd->repllist`.
2365pub fn get_match_ret(imd: &mut imatchdata, b: usize, e: usize) -> Option<String> {
2366 let mut buf = String::new(); // c:2552 char buf[80]
2367 let mut ll: i64 = 0; // c:2553 ll = 0
2368 let mut bl: usize = 0; // c:2553 bl = 0
2369 let mut t = false; // c:2553 t = 0
2370 let mut add: usize = 0; // c:2553 add = 0
2371 let fl = imd.flags; // c:2553 fl = imd->flags
2372 let mut replstr: Option<String> = imd.replstr.clone(); // c:2552 *replstr = imd->replstr
2373
2374 let ustr_owned = imd.ustr.clone().unwrap_or_default();
2375 let ustr = ustr_owned.as_bytes();
2376 let mstr_owned = imd.mstr.clone().unwrap_or_default();
2377 let mstr = mstr_owned.as_bytes();
2378 let mlen = imd.mlen as usize; // c:2544 imd->mlen
2379
2380 // c:2555-2564 — account for b and e referring to unmetafied string.
2381 let mut p = 0usize; // c:2556 p = imd->ustr
2382 while p < b && p < ustr.len() {
2383 // c:2556 for (; p < imd->ustr + b; p++)
2384 if imeta(ustr[p]) {
2385 add += 1; // c:2558 add++
2386 }
2387 p += 1;
2388 }
2389 let b = b + add; // c:2559 b += add
2390 while p < e && p < ustr.len() {
2391 // c:2560 for (; p < imd->ustr + e; p++)
2392 if imeta(ustr[p]) {
2393 add += 1; // c:2562 add++
2394 }
2395 p += 1;
2396 }
2397 let e = e + add; // c:2563 e += add
2398
2399 // c:2566 — Everything now refers to metafied lengths.
2400 if replstr.is_some() || (fl & SUB_LIST) != 0 {
2401 // c:2567
2402 if (fl & SUB_DOSUBST) != 0 {
2403 // c:2568
2404 let mut rs = dupstring(replstr.as_deref().unwrap_or("")); // c:2569 dupstring(replstr)
2405 rs = singsub(&rs); // c:2570 singsub(&replstr)
2406 rs = untokenize(&rs); // c:2571 untokenize(replstr)
2407 replstr = Some(rs);
2408 }
2409 if (fl & (SUB_GLOBAL | SUB_LIST)) != 0 && imd.repllist.is_some() {
2410 // c:2573 — replacing the chunk, just add this to the list.
2411 let rd = repldata {
2412 b: b as i32, // c:2578 rd->b = b
2413 e: e as i32, // c:2579 rd->e = e
2414 replstr: replstr.clone(), // c:2580 rd->replstr = replstr
2415 };
2416 imd.repllist.as_mut().unwrap().push(rd); // c:2581-2584 z/addlinknode(repllist, rd)
2417 return imd.mstr.clone(); // c:2585 return imd->mstr
2418 }
2419 if let Some(ref r) = replstr {
2420 ll += r.len() as i64; // c:2588 ll += strlen(replstr)
2421 }
2422 }
2423 if (fl & SUB_MATCH) != 0 {
2424 // c:2590 matched portion
2425 ll += 1 + (e as i64 - b as i64); // c:2591 ll += 1 + (e - b)
2426 }
2427 if (fl & SUB_REST) != 0 {
2428 // c:2592 unmatched portion
2429 ll += 1 + (mlen as i64 - (e as i64 - b as i64)); // c:2593 ll += 1 + (mlen - (e - b))
2430 }
2431 if (fl & SUB_BIND) != 0 {
2432 // c:2594 position of start of matched portion
2433 buf = format!("{} ", MB_METASTRLEN2END(mstr_owned.as_str(), 0, b) + 1); // c:2596
2434 bl = buf.len(); // c:2597 bl = strlen(buf)
2435 ll += bl as i64; // c:2597 ll += bl
2436 }
2437 if (fl & SUB_EIND) != 0 {
2438 // c:2599 position of end of matched portion
2439 buf.push_str(&format!(
2440 "{} ",
2441 MB_METASTRLEN2END(mstr_owned.as_str(), 0, e) + 1
2442 )); // c:2601
2443 bl = buf.len(); // c:2602 bl = strlen(buf)
2444 ll += bl as i64; // c:2602 ll += bl
2445 }
2446 if (fl & SUB_LEN) != 0 {
2447 // c:2603 length of matched portion — MB_METASTRLEN2END(mstr+b, 0, mstr+e)
2448 let sub = if b <= mstr.len() {
2449 &mstr_owned[b..]
2450 } else {
2451 ""
2452 };
2453 buf.push_str(&format!(
2454 "{} ",
2455 MB_METASTRLEN2END(sub, 0, e.saturating_sub(b))
2456 )); // c:2605
2457 bl = buf.len(); // c:2606 bl = strlen(buf)
2458 ll += bl as i64; // c:2606 ll += bl
2459 }
2460 if bl != 0 {
2461 buf.pop(); // c:2609 buf[bl - 1] = '\0' — drop the trailing space
2462 }
2463
2464 if ll == 0 {
2465 return None; // c:2614 return NULL
2466 }
2467
2468 // c:2617 — rr = r = hcalloc(ll); build the result buffer.
2469 let mut r = String::new();
2470
2471 if (fl & SUB_MATCH) != 0 {
2472 // c:2619-2623 — copy matched portion to new buffer.
2473 let end = e.min(mstr.len());
2474 let start = b.min(end);
2475 r.push_str(&mstr_owned[start..end]); // c:2621
2476 t = true; // c:2622
2477 }
2478 if (fl & SUB_REST) != 0 {
2479 // c:2624 — copy unmatched portion. If both portions requested,
2480 // put a space in between (why?).
2481 if t {
2482 r.push(' '); // c:2627
2483 }
2484 // c:2629-2630 — unmatched bits before the match.
2485 let pre = b.min(mstr.len());
2486 r.push_str(&mstr_owned[..pre]); // c:2630
2487 if let Some(ref rs) = replstr {
2488 r.push_str(rs); // c:2632-2633 copy replstr
2489 }
2490 // c:2634-2635 — unmatched bits after the match.
2491 let post = e.min(mstr.len());
2492 r.push_str(&mstr_owned[post..]); // c:2635
2493 t = true; // c:2636
2494 }
2495 if bl != 0 {
2496 // c:2639-2643 — append the numeric buffer; space first if needed.
2497 if t {
2498 r.push(' '); // c:2642
2499 }
2500 r.push_str(&buf); // c:2643 strcpy(rr, buf)
2501 }
2502 Some(r) // c:2645 return r
2503}
2504
2505/// Port of `compgetmatch(char *pat, int *flp, char **replstrp)` from
2506/// `Src/glob.c:2650`.
2507///
2508/// Compiles the search pattern and decides how the replacement is
2509/// expanded. A pattern with backreferences (`(#b)`, `patnpar`) or match
2510/// references (`(#m)`, `GF_MATCHREF`) needs the replacement re-substituted
2511/// after EACH match, once `$match`/`$MATCH` hold that match's text, so it
2512/// sets `SUB_DOSUBST` (consumed by `igetmatch`, c:2567-2571). Otherwise
2513/// the replacement is substituted once, here.
2514pub fn compgetmatch(pat: &str, flp: &mut i32, replstrp: &mut Option<String>) -> Option<Patprog> {
2515 // c:2662 — `int patflags = PAT_SCAN|PAT_NOANCH | (*replstrp ? 0 : PAT_STATIC);`
2516 // PAT_STATIC's static-buffer path is not used by this port's compiler.
2517 let mut patflags = crate::ported::zsh_h::PAT_SCAN | crate::ported::zsh_h::PAT_NOANCH;
2518 // c:2668-2669 — anchored to the end of the string if we want to match
2519 // it all, or if we are matching at the end and not using substrings.
2520 if (*flp & SUB_ALL) != 0 || ((*flp & SUB_END) != 0 && (*flp & SUB_SUBSTR) == 0) {
2521 patflags &= !crate::ported::zsh_h::PAT_NOANCH; // c:2670
2522 }
2523 // c:2671 — `p = patcompile(pat, patflags, NULL);` — `pat` is already
2524 // tokenized by the caller (c:Src/hist.c:2365 parse_subst_string).
2525 let p = match crate::ported::pattern::patcompile(pat, patflags, None) {
2526 Some(p) => p,
2527 None => {
2528 zerr(&format!("bad pattern: {}", pat)); // c:2673
2529 return None; // c:2674
2530 }
2531 };
2532 if let Some(r) = replstrp.as_mut() {
2533 // c:2676
2534 if p.0.patnpar != 0 || (p.0.globend & crate::ported::zsh_h::GF_MATCHREF) != 0 {
2535 // c:2677-2682 — "Either backreferences or match references, so
2536 // we need to re-substitute replstr each time round."
2537 *flp |= SUB_DOSUBST; // c:2682
2538 } else {
2539 // c:2684-2685 — `singsub(replstrp); untokenize(*replstrp);`
2540 *r = untokenize(&crate::ported::subst::singsub(r));
2541 }
2542 }
2543 Some(p) // c:2689
2544}
2545
2546/// Port of `getmatch(char **sp, char *pat, int fl, int n, char *replstr)`
2547/// from `Src/glob.c:2710`. C body (4 lines):
2548/// `Patprog p;
2549/// if (!(p = compgetmatch(pat, &fl, &replstr))) return 1;
2550/// return igetmatch(sp, p, fl, n, replstr, NULL);`
2551///
2552/// `sp` is C's `char **sp` out-pointer — the result is written back
2553/// through it — and the return value is C's STATUS: 1 when the pattern
2554/// matched, `(fl & SUB_RETFAIL) ? 0 : 1` when it did not (c:3231, the
2555/// tail of `igetmatch`), the distinction `subst()` (c:Src/hist.c:2370)
2556/// branches on.
2557///
2558/// One adaptation: this port's `igetmatch` takes the pattern TEXT (it
2559/// re-compiles per candidate) and returns 0 for a match and 1 for no
2560/// match, INVERTED from C. That inversion is baked into every other
2561/// `igetmatch` caller here, so it is translated at this boundary.
2562pub fn getmatch(sp: &mut String, pat: &str, fl: i32, n: i32, replstr: Option<&str>) -> i32 {
2563 let mut fl = fl;
2564 let mut replstr: Option<String> = replstr.map(str::to_string);
2565 // c:2713 — `if (!(p = compgetmatch(pat, &fl, &replstr))) return 1;`
2566 if compgetmatch(pat, &mut fl, &mut replstr).is_none() {
2567 return 1;
2568 }
2569 // c:2716 `return igetmatch(sp, p, fl, n, replstr, NULL);`
2570 if igetmatch(sp, pat, fl, n, replstr.as_deref()) == 0 {
2571 return 1;
2572 }
2573 // c:3231 — `return (fl & SUB_RETFAIL) ? 0 : 1;`
2574 if fl & SUB_RETFAIL != 0 {
2575 0
2576 } else {
2577 1
2578 }
2579}
2580
2581/// Get match for array elements (from glob.c getmatcharr lines 2690-2750)
2582/// Port of `getmatcharr(char ***ap, char *pat, int fl, int n, char *replstr)` from `Src/glob.c:2727`.
2583///
2584/// WARNING: C drops the elements `igetmatch` reports 0 for (c:2734
2585/// `if (igetmatch(pp, ...)) pp++;`); this port keeps every element, as
2586/// it did before `getmatch` grew its status back. Unchanged here on
2587/// purpose — nothing in this change depends on it.
2588pub fn getmatcharr(
2589 ap: &[String],
2590 pat: &str,
2591 fl: i32,
2592 n: i32,
2593 replstr: Option<&str>,
2594) -> Vec<String> {
2595 ap.iter()
2596 .map(|s| {
2597 let mut buf = s.clone();
2598 getmatch(&mut buf, pat, fl, n, replstr); // c:2734
2599 buf
2600 })
2601 .collect()
2602}
2603
2604/// Port of `int getmatchlist(char *str, Patprog p, LinkList *repllistp)`
2605/// from `Src/glob.c:2749`.
2606/// ```c
2607/// int
2608/// getmatchlist(char *str, Patprog p, LinkList *repllistp)
2609/// {
2610/// char **sp = &str;
2611/// return igetmatch(sp, p, SUB_LONG|SUB_GLOBAL|SUB_SUBSTR|SUB_LIST,
2612/// 0, NULL, repllistp);
2613/// }
2614/// ```
2615/// 3-line delegation to `igetmatch` with the canonical flag set. The
2616/// `repllistp` out-param is the LinkList that receives the match
2617/// position pairs; the Rust port currently lacks a repllistp out-
2618/// channel on `igetmatch`, so this entry mirrors the C structure
2619/// and returns the igetmatch status.
2620pub fn getmatchlist(sp: &mut String, p: &str) -> i32 {
2621 // c:2749
2622 igetmatch(
2623 sp,
2624 p,
2625 SUB_LONG | SUB_GLOBAL | SUB_SUBSTR | SUB_LIST, // c:2761
2626 0,
2627 None,
2628 ) // c:2762
2629}
2630
2631/// File-static `static int in_expandredir = 0;` from `Src/glob.c:1206`
2632/// — set during the `globlist` call inside `xpandredir` so the glob
2633/// pipeline knows the result feeds a redirection (suppresses the
2634/// "no matches" warning in the caller's normal flow).
2635pub static IN_EXPANDREDIR: std::sync::atomic::AtomicI32 = // c:1206
2636 std::sync::atomic::AtomicI32::new(0);
2637
2638/// Direct port of `int xpandredir(struct redir *fn, LinkList redirtab)`
2639/// from `Src/glob.c:2150`. Expands `>>*.c`-style redirections by
2640/// running `prefork` + (when MULTIOS is set) `globlist` over the redir
2641/// name. Single match: rewrite `fn->name` in place and decode the
2642/// MERGEIN/MERGEOUT special syntax (`-` close, `p` pipe-fd, digits =
2643/// fd number). Multi match: clone the redir for each name and append
2644/// to `redirtab`. Returns 1 if multios produced multiple entries, 0
2645/// otherwise.
2646/// WARNING: param names don't match C — Rust=(fn_, redirtab) vs C=(fn, redirtab)
2647pub fn xpandredir(
2648 fn_: &mut redir, // c:2150
2649 redirtab: &mut Vec<redir>,
2650) -> i32 {
2651 use std::sync::atomic::Ordering::SeqCst;
2652 let mut ret = 0; // c:2156
2653 let name = match fn_.name.as_deref() {
2654 // c:2160 init_list1(fake, fn->name)
2655 Some(n) => n.to_string(),
2656 None => return 0,
2657 };
2658 let mut fake: LinkList = LinkList::new();
2659 fake.push_back(name); // c:2160
2660 let mut rf = 0i32;
2661 prefork(
2662 &mut fake, // c:2162 prefork
2663 if isset(MULTIOS) { 0 } else { PREFORK_SINGLE },
2664 &mut rf,
2665 );
2666 // c:2164 — `if (!errflag && isset(MULTIOS))`. C uses LOGICAL NOT
2667 // on `errflag` (an `int`), so the condition is "errflag is zero".
2668 // The previous Rust port wrote `!errflag.load(...) != 0` which is
2669 // BITWISE NOT in Rust (`!i32` is `^-1`), making the condition
2670 // equivalent to `errflag != -1` — true for every common errflag
2671 // value (0 / ERRFLAG_ERROR=1 / ERRFLAG_INT=2). Result: the
2672 // globlist call ran even when errflag was set, papering over
2673 // the abort path that C uses to bail early on prior errors.
2674 if errflag.load(SeqCst) == 0 && isset(MULTIOS) {
2675 // c:2164
2676 IN_EXPANDREDIR.store(1, SeqCst); // c:2165
2677 crate::ported::subst::globlist(&mut fake, 0); // c:2166
2678 IN_EXPANDREDIR.store(0, SeqCst); // c:2167
2679 }
2680 if errflag.load(SeqCst) != 0 {
2681 return 0;
2682 } // c:2169
2683 let names: Vec<String> = fake.iter().cloned().collect();
2684 if names.len() == 1 {
2685 // c:2171 nonempty(&fake) && !nextnode(firstnode)
2686 let s = crate::lex::untokenize(&names[0]); // c:2174 untokenize(s)
2687 fn_.name = Some(s.clone()); // c:2173 fn->name = s
2688 if fn_.typ == REDIR_MERGEIN || fn_.typ == REDIR_MERGEOUT {
2689 // c:2175
2690 let bytes = s.as_bytes();
2691 if bytes.len() == 1 && IS_DASH(bytes[0] as char) {
2692 // c:2176-2177 IS_DASH(s[0]) && !s[1]
2693 fn_.typ = REDIR_CLOSE;
2694 } else if bytes.len() == 1 && bytes[0] == b'p' {
2695 // c:2178 s[0]=='p' && !s[1]
2696 fn_.fd2 = -2;
2697 } else {
2698 let mut i = 0;
2699 while i < bytes.len() && bytes[i].is_ascii_digit() {
2700 i += 1;
2701 } // c:2181 idigit
2702 if i == bytes.len() && i > 0 {
2703 // c:2183 !*s && s > fn->name
2704 fn_.fd2 = crate::ported::utils::zstrtol(&s, 10).0 as i32; // c:2184 zstrtol(.., 10)
2705 } else if fn_.typ == REDIR_MERGEIN {
2706 // c:2185
2707 zerr("file number expected"); // c:2186
2708 } else if crate::dash_mode::dash_faithful() {
2709 // !!! DASH-FAITHFUL GATE (no C counterpart) !!! dash has no
2710 // `>&file` (both-streams) form: the word after `>&` must be
2711 // a descriptor number or `-`, else "Bad fd number".
2712 zerr(&format!("Bad fd number: {}", s));
2713 } else {
2714 fn_.typ = REDIR_ERRWRITE; // c:2188
2715 }
2716 }
2717 }
2718 } else if fn_.typ == REDIR_MERGEIN {
2719 // c:2192
2720 zerr("file number expected"); // c:2193
2721 } else {
2722 if fn_.typ == REDIR_MERGEOUT {
2723 fn_.typ = REDIR_ERRWRITE;
2724 } // c:2195
2725 for nam in names {
2726 // c:2196 while ((nam = ugetnode(&fake)))
2727 let mut ff = fn_.clone(); // c:2199-2200 zhalloc + *ff = *fn
2728 ff.name = Some(nam); // c:2201 ff->name = nam
2729 redirtab.push(ff); // c:2202 addlinknode(redirtab, ff)
2730 ret = 1; // c:2203
2731 }
2732 }
2733 ret // c:2206
2734}
2735
2736/// Port of `freerepldata(void *ptr)` from Src/glob.c:2766.
2737/// C: `static void freerepldata(void *ptr)` →
2738/// `zfree(ptr, sizeof(struct repldata));`
2739#[allow(unused_variables)]
2740pub fn freerepldata(ptr: *mut std::ffi::c_void) { // c:2766
2741 // Rust drop covers the equivalent.
2742}
2743
2744/// Port of `freematchlist(LinkList repllist)` from Src/glob.c:2773.
2745/// C: `void freematchlist(LinkList repllist)` →
2746/// `freelinklist(repllist, freerepldata);`
2747///
2748/// The C `repllist` is a `LinkList` of `struct repldata` (the same
2749/// node `get_match_ret` records for SUB_GLOBAL/SUB_LIST). The Rust
2750/// port operates on `Vec<repldata>` — matching the canonical
2751/// `imatchdata.repllist` type — not the prior ad-hoc `Vec<(usize,usize)>`.
2752/// Clearing the Vec drops each `repldata` (Rust's `freerepldata` +
2753/// `freelinklist` equivalent).
2754pub fn freematchlist(repllist: Option<&mut Vec<repldata>>) {
2755 // c:2773
2756 if let Some(l) = repllist {
2757 l.clear(); // c:2776 freelinklist(repllist, freerepldata)
2758 }
2759}
2760
2761/// Port of `set_pat_start(Patprog p, int offs)` from `Src/glob.c:2780`.
2762///
2763/// When we advance up the test string from its start, tell the pattern
2764/// matcher that a start-of-string assertion `(#s)` should fail: set
2765/// `PAT_NOTSTART` when `offs` is nonzero (the real start is past the
2766/// actual start), clear it when `offs == 0`. Mutates `p->flags`.
2767/// Replaces the prior fake that sliced the pattern string and returned
2768/// a substring — unrelated to the C behaviour (the matcher reads
2769/// `PAT_NOTSTART` off `prog.flags`, see pattern.rs:4792).
2770pub fn set_pat_start(p: &mut Patprog, offs: i32) {
2771 // c:2780
2772 if offs != 0 {
2773 p.0.flags |= PAT_NOTSTART; // c:2790 p->flags |= PAT_NOTSTART
2774 } else {
2775 p.0.flags &= !PAT_NOTSTART; // c:2792 p->flags &= ~PAT_NOTSTART
2776 }
2777}
2778
2779/// Port of `set_pat_end(Patprog p, char null_me)` from `Src/glob.c:2796`.
2780///
2781/// When we shorten the string at the tail, tell the pattern matcher
2782/// that an end-of-string assertion `(#e)` should fail: set `PAT_NOTEND`
2783/// when the char `null_me` about to be zapped is non-NUL, clear it when
2784/// it is already NUL. Mutates `p->flags`. Replaces the prior fake that
2785/// sliced the pattern string and returned a prefix (the matcher reads
2786/// `PAT_NOTEND` off `prog.flags`, see pattern.rs:2803).
2787pub fn set_pat_end(p: &mut Patprog, null_me: u8) {
2788 // c:2796
2789 if null_me != 0 {
2790 p.0.flags |= PAT_NOTEND; // c:2806 p->flags |= PAT_NOTEND
2791 } else {
2792 p.0.flags &= !PAT_NOTEND; // c:2808 p->flags &= ~PAT_NOTEND
2793 }
2794}
2795
2796/// Port of `igetmatch(char **sp, Patprog p, int fl, int n, char *replstr, LinkList *repllistp)` from Src/glob.c:2832.
2797/// C: `static int igetmatch(char **sp, Patprog p, int fl, int n,
2798/// char *replstr, LinkList *repllistp)` — pattern-replace inner
2799/// matcher; modifies `*sp` in place, optionally collects match
2800/// positions into `*repllistp`.
2801/// WARNING: param names don't match C — Rust=(sp, p, fl, n, replstr) vs C=(sp, p, fl, n, replstr, repllistp)
2802pub fn igetmatch(
2803 sp: &mut String,
2804 p: &str,
2805 fl: i32,
2806 _n: i32, // c:2832
2807 replstr: Option<&str>,
2808) -> i32 {
2809 // c:2840-3100+ — full SUB_* dispatch: longest/shortest/global/end-
2810 // anchor replacement loop with multibyte tracking. Rust port walks
2811 // chars + `matchpat`; full Patprog substrate (with chunked DFA
2812 // execution) lives in src/ported/pattern.rs. SUB_START imported
2813 // from zsh_h.rs at top-of-file rather than redeclared locally.
2814 // c:2669 — the Patprog reaching C's `igetmatch` was compiled by
2815 // `compgetmatch` through `patcompile(pat, patflags, NULL)` from the
2816 // ALREADY-TOKENIZED pattern text (`subst()` runs parse_subst_string
2817 // and singsub over it first, c:Src/hist.c:2365-2369), under the
2818 // ambient options. `matchpat` re-tokenizes its argument, which turns
2819 // a quoted literal — `\(` lexed to Bnull `(`, the Bnull then dropped
2820 // by singsub's remnulargs — back into an active Inpar, so
2821 // `${x:s/\\(/Z/}` died with "bad pattern: (". Compile the text as
2822 // given, once, as C does; each candidate is then tried against it.
2823 // This port anchors by slicing the subject, so the program is
2824 // compiled anchored (no PAT_NOANCH/PAT_SCAN).
2825 let prog = crate::ported::pattern::patcompile(p, crate::ported::zsh_h::PAT_HEAPDUP, None);
2826 let pat_try = |s: &str| -> bool {
2827 prog.as_ref().map_or(false, |pp| crate::ported::pattern::pattry(pp, s))
2828 };
2829 let anchored_start = (fl & SUB_START) != 0;
2830 let anchored_end = (fl & SUB_END) != 0;
2831 let substr_mode = (fl & SUB_SUBSTR) != 0;
2832 let shortest = (fl & SUB_LONG) == 0;
2833 let chars: Vec<char> = sp.chars().collect();
2834 let len = chars.len();
2835 // c:2567-2571 (get_match_ret) — under SUB_DOSUBST (set by compgetmatch
2836 // for a pattern with backreferences or match references) the
2837 // replacement is `singsub`ed afresh for each match, after the match
2838 // has set `$match`/`$MATCH`; otherwise compgetmatch already did it.
2839 let resolve_repl = || -> Option<String> {
2840 replstr.map(|r| {
2841 if (fl & SUB_DOSUBST) != 0 {
2842 untokenize(&crate::ported::subst::singsub(r)) // c:2569-2571
2843 } else {
2844 r.to_string()
2845 }
2846 })
2847 };
2848
2849 // c:2887-2898 — SUB_ALL: entire-string match flag.
2850 if (fl & SUB_ALL) != 0 {
2851 // c:2887
2852 let i = pat_try(sp); // c:2888 pattrylen
2853 if !i {
2854 // c:2889
2855 // c:2890-2893 — no match: clear replstr.
2856 if (fl & SUB_MATCH) != 0 {
2857 // c:2895
2858 *sp = String::new();
2859 return 0; // c:2896
2860 }
2861 return 1; // c:2897
2862 }
2863 if let Some(r) = resolve_repl() {
2864 // c:2894 get_match_ret
2865 *sp = r.to_string();
2866 }
2867 if sp.is_empty() && (fl & SUB_REST) != 0 && i {
2868 // c:2895
2869 return 0; // c:2896
2870 }
2871 return 1; // c:2897
2872 }
2873
2874 // An empty subject: every C arm below reduces to one `pattrylen` on the
2875 // empty string (c:2919 head, c:2950 tail, c:3008 substring), so
2876 // `a=; ${a:s/%/X/}` / `${a:s/*/X/}` give `X` and a non-empty-only
2877 // pattern leaves the subject alone.
2878 if len == 0 {
2879 if !pat_try("") {
2880 return 1;
2881 }
2882 if (fl & SUB_MATCH) == 0 {
2883 if let Some(r) = resolve_repl() {
2884 *sp = r;
2885 }
2886 }
2887 return 0;
2888 }
2889 // c:2998-3041 — SUB_LIST: collect all match offset pairs.
2890 if (fl & SUB_LIST) != 0 {
2891 // c:2998
2892 // Walk all substrings; collect (start, end) pairs the caller
2893 // can read via subsequent `getmatchlist` invocations. Without
2894 // a `repllistp` out-channel through the Rust signature, we
2895 // only walk to validate at-least-one-match; the canonical
2896 // collection point is the caller-supplied LinkList that the
2897 // C body populates at c:3000+. Defer until the out-vec lands.
2898 let mut found = false;
2899 for start in 0..len {
2900 // c:3008
2901 for end in (start + 1)..=len {
2902 // c:3009
2903 let s2: String = chars[start..end].iter().collect();
2904 if pat_try(&s2) {
2905 // c:3010
2906 found = true;
2907 if shortest {
2908 break;
2909 } // c:3011 SUB_LONG vs short
2910 }
2911 }
2912 if !substr_mode {
2913 break;
2914 }
2915 }
2916 return if found { 0 } else { 1 };
2917 }
2918
2919 // c:3064-3114 — SUB_GLOBAL: keep scanning after each match and
2920 // replace EVERY one, rather than stopping at the first. C reaches
2921 // this through the same substring walk (`if (fl & SUB_GLOBAL)` at
2922 // c:3072 records the match and carries on from `t`); the arms
2923 // below all stop at one match, so the flag was silently ignored.
2924 //
2925 // Only the un-anchored (SUB_SUBSTR) case needs the loop: with
2926 // SUB_START or SUB_END the pattern can only match in one place, so
2927 // those fall through to the single-match arms unchanged. No caller
2928 // other than `subst()` (c:Src/hist.c:2347) sets SUB_GLOBAL, so this
2929 // is new ground rather than a change to an existing path.
2930 if (fl & SUB_GLOBAL) != 0
2931 && !anchored_start
2932 && !anchored_end
2933 && (fl & (SUB_MATCH | SUB_LIST)) == 0
2934 {
2935 let mut out = String::new();
2936 let mut i = 0usize;
2937 let mut any = false;
2938 while i < len {
2939 // Longest match at this position unless SUB_LONG is off.
2940 // `end` starts at `i + 1`, so a pattern that also matches
2941 // the empty string cannot consume nothing and loop forever.
2942 let mut found: Option<usize> = None;
2943 for end in (i + 1)..=len {
2944 let substr: String = chars[i..end].iter().collect();
2945 if pat_try(&substr) {
2946 found = Some(end);
2947 if shortest {
2948 break;
2949 }
2950 }
2951 }
2952 match found {
2953 Some(end) => {
2954 any = true;
2955 if let Some(r) = resolve_repl() {
2956 out.push_str(&r);
2957 }
2958 i = end;
2959 }
2960 None => {
2961 out.push(chars[i]);
2962 i += 1;
2963 }
2964 }
2965 }
2966 if any {
2967 *sp = out;
2968 return 0;
2969 }
2970 return 1;
2971 }
2972
2973 // c:Src/glob.c:2900+ — SUB_MATCH inverts the disposition of the
2974 // anchored-strip operators. Default (no SUB_MATCH): the matched
2975 // portion is REMOVED and the rest is returned (`${var#pat}` →
2976 // tail after the matched prefix). With SUB_MATCH: the matched
2977 // portion is KEPT and the rest is removed (`${(M)var#pat}` → the
2978 // matched prefix only). Previously honoured only in the SUB_ALL
2979 // arm at c:2895; the anchored-prefix / anchored-suffix / substr
2980 // arms below always returned prefix+suffix, so `(M)` was a no-op
2981 // for `#pat` / `%pat`. Mirror the C SUB_MATCH branch by emitting
2982 // just the matched slice.
2983 let match_only = (fl & SUB_MATCH) != 0;
2984 let (match_start, match_end) = if anchored_start && anchored_end {
2985 if pat_try(sp) {
2986 (0, len)
2987 } else {
2988 if match_only {
2989 *sp = String::new();
2990 return 0;
2991 }
2992 return 1;
2993 }
2994 } else if anchored_start {
2995 // c:2913-2938 — the head match may be EMPTY: `pattrylen(p, s,
2996 // umltot, …)` runs unanchored at the end (PAT_NOANCH), so a
2997 // pattern that matches nothing at the head (`:s/#/X/`) matches the
2998 // empty prefix and the replacement is inserted before the string.
2999 let mut best_end: Option<usize> = None;
3000 for end in 0..=len {
3001 let substr: String = chars[..end].iter().collect();
3002 if pat_try(&substr) {
3003 if shortest {
3004 *sp = if match_only {
3005 chars[..end].iter().collect()
3006 } else {
3007 match resolve_repl() {
3008 Some(r) => format!("{}{}", r, chars[end..].iter().collect::<String>()),
3009 None => chars[end..].iter().collect(),
3010 }
3011 };
3012 return 0;
3013 }
3014 best_end = Some(end);
3015 }
3016 }
3017 if let Some(best_end) = best_end {
3018 (0, best_end)
3019 } else {
3020 if match_only {
3021 *sp = String::new();
3022 return 0;
3023 }
3024 return 1;
3025 }
3026 } else if anchored_end {
3027 // c:2949-2953 / c:2980-2999 — the tail match may be EMPTY: SUB_END
3028 // tries `pattrylen(p, send, 0, …)` first, and SUB_END|SUB_LONG walks
3029 // `t <= send`, so `:s/%/X/` appends the replacement.
3030 let mut best_start: Option<usize> = None;
3031 for start in (0..=len).rev() {
3032 let substr: String = chars[start..].iter().collect();
3033 if pat_try(&substr) {
3034 if shortest {
3035 *sp = if match_only {
3036 chars[start..].iter().collect()
3037 } else {
3038 match resolve_repl() {
3039 Some(r) => {
3040 format!("{}{}", chars[..start].iter().collect::<String>(), r)
3041 }
3042 None => chars[..start].iter().collect(),
3043 }
3044 };
3045 return 0;
3046 }
3047 best_start = Some(start);
3048 }
3049 }
3050 if let Some(best_start) = best_start {
3051 (best_start, len)
3052 } else {
3053 if match_only {
3054 *sp = String::new();
3055 return 0;
3056 }
3057 return 1;
3058 }
3059 } else {
3060 for start in 0..len {
3061 // c:3029-3051 — "Find the longest match from this position";
3062 // only without SUB_LONG is the shortest one searched for.
3063 let ends: Vec<usize> = if shortest {
3064 ((start + 1)..=len).collect()
3065 } else {
3066 ((start + 1)..=len).rev().collect()
3067 };
3068 for end in ends {
3069 let substr: String = chars[start..end].iter().collect();
3070 if pat_try(&substr) {
3071 if match_only {
3072 *sp = chars[start..end].iter().collect();
3073 return 0;
3074 }
3075 let prefix: String = chars[..start].iter().collect();
3076 let suffix: String = chars[end..].iter().collect();
3077 *sp = match resolve_repl() {
3078 Some(r) => format!("{}{}{}", prefix, r, suffix),
3079 None => format!("{}{}", prefix, suffix),
3080 };
3081 return 0;
3082 }
3083 }
3084 }
3085 if match_only {
3086 *sp = String::new();
3087 return 0;
3088 }
3089 return 1;
3090 };
3091 *sp = if match_only {
3092 chars[match_start..match_end].iter().collect()
3093 } else {
3094 let prefix: String = chars[..match_start].iter().collect();
3095 let suffix: String = chars[match_end..].iter().collect();
3096 match resolve_repl() {
3097 Some(r) => format!("{}{}{}", prefix, r, suffix),
3098 None => format!("{}{}", prefix, suffix),
3099 }
3100 };
3101 0
3102}
3103
3104// ============================================================================
3105// Tokenization (from glob.c tokenize family)
3106// ============================================================================
3107
3108// `enum GlobToken` deleted — C uses the byte-token constants
3109// (`Star`/`Quest`/`Inpar`/...) from `Src/zsh.h:159-200`, mirrored in
3110// the Rust port at `zsh_h.rs:128-160`. `tokenize()` (`Src/glob.c:3548`
3111// → `zshtokenize`) mutates the input string in place, replacing each
3112// glob-metacharacter with its high-bit byte token; the Rust port now
3113// matches.
3114
3115// `ztokens[]` from `Src/lex.c:38` — the source-char ↔ token-byte
3116// table the C tokenizer indexes with `(t - ztokens) + Pound`. Each
3117// position N in the string maps to high-bit byte `Pound + N`.
3118pub const ZTOKENS: &str = "#$^*(())$=|{}[]`<>>?~`,-!'\"\\\\";
3119
3120/// The `switch (*s)` labels of `zshtokenize` (c:3592-3639), as a byte
3121/// table: `true` for every byte that can reach an arm which MUTATES the
3122/// string. `\\` `<` `>` `^` `#` `~` `[` `]` `*` `?` `=` `-` `!` `(` `|`
3123/// `)` are the literal case labels; the `Meta` / `Bnull` / `Bnullkeep`
3124/// sentinels (c:3593/3597/3598) are non-ASCII chars, so every byte of
3125/// their UTF-8 encoding is >= 0x80 and the whole high half is marked —
3126/// conservatively, since a `>= 0x80` byte only sends the string down the
3127/// full walk, never past it.
3128///
3129/// C needs no such table: its loop is a byte-pointer walk that costs
3130/// about a cycle per uninteresting byte. The port has to materialize a
3131/// `Vec<char>` and rebuild the `String`, so the same "do nothing" answer
3132/// costs three allocations and a decode/encode pair — 33.4% of a `man
3133/// <TAB>` completion, which tokenizes every one of 60605 candidate
3134/// names and finds a metachar in 3105 of them (5%).
3135static ZSHTOK_TRIGGER: [bool; 256] = {
3136 let mut t = [false; 256];
3137 let mut i = 0;
3138 while i < 256 {
3139 let b = i as u8;
3140 t[i] = b >= 0x80
3141 || matches!(
3142 b,
3143 b'\\'
3144 | b'<'
3145 | b'>'
3146 | b'^'
3147 | b'#'
3148 | b'~'
3149 | b'['
3150 | b']'
3151 | b'*'
3152 | b'?'
3153 | b'='
3154 | b'-'
3155 | b'!'
3156 | b'('
3157 | b'|'
3158 | b')'
3159 );
3160 i += 1;
3161 }
3162 t
3163};
3164
3165/// Tokenize a glob pattern in place — port of `tokenize(char *s)` from
3166/// `Src/glob.c:3548`. One-line C delegation: `zshtokenize(s, 0)`.
3167pub fn tokenize(s: &mut String) {
3168 // c:3548
3169 zshtokenize(s, 0); // c:3552
3170}
3171
3172/// Tokenize for shell — port of `shtokenize(char *s)` from
3173/// `Src/glob.c:3563`. Builds flags from SHGLOB option then delegates
3174/// to `zshtokenize`.
3175pub fn shtokenize(s: &mut String) {
3176 // c:3563
3177 let mut flags = ZSHTOK_SUBST; // c:3567
3178 if isset(SHGLOB) {
3179 // c:3568
3180 flags |= ZSHTOK_SHGLOB; // c:3569
3181 }
3182 zshtokenize(s, flags); // c:3570
3183}
3184
3185/// Port of `zshtokenize(char *s, int flags)` from `Src/glob.c:3575`.
3186/// Walks `s` in place, replacing each glob-metachar with its high-bit
3187/// byte token from the `ZTOKENS` table; respects ZSHTOK_SUBST (use
3188/// Bnullkeep for escaped tokens) and ZSHTOK_SHGLOB (don't tokenize
3189/// `<` / `(` / `|` / `)`).
3190pub fn zshtokenize(s: &mut String, flags: i32) {
3191 // c:3575
3192 // c:3580-3651 — every arm that writes to the string is guarded by
3193 // one of the `switch` labels; a string holding none of them leaves
3194 // C's loop with `*s` byte-for-byte unchanged. Answer that case
3195 // without materializing anything (see ZSHTOK_TRIGGER).
3196 if !s.as_bytes().iter().any(|&b| ZSHTOK_TRIGGER[b as usize]) {
3197 return;
3198 }
3199 // c:3640 `for (t = ztokens; *t; t++)` — C scans the static table by
3200 // pointer. Every ZTOKENS entry is ASCII, so scanning its bytes is
3201 // the same comparison without the per-call `Vec<char>` build.
3202 let ztokens: &[u8] = ZTOKENS.as_bytes();
3203 let mut chars: Vec<char> = s.chars().collect();
3204 let mut bslash = false; // c:3578
3205 let mut i = 0;
3206 while i < chars.len() {
3207 // c:3580 for (; *s; s++)
3208 let c = chars[i];
3209 match c {
3210 x if x == Meta as char => { // c:3583 case Meta
3211 i += 2; // c:3585 skip both Meta and next
3212 bslash = false;
3213 continue;
3214 }
3215 x if x == Bnull || x == Bnullkeep || x == '\\' => { // c:3587-3589
3216 if bslash { // c:3590
3217 chars[i - 1] = if (flags & ZSHTOK_SUBST) != 0 { // c:3591
3218 Bnullkeep
3219 } else {
3220 Bnull
3221 };
3222 } else {
3223 bslash = true; // c:3595
3224 i += 1;
3225 continue; // c:3596 (skip bslash=0 reset)
3226 }
3227 }
3228 '<' => { // c:3598
3229 if (flags & ZSHTOK_SHGLOB) != 0 { // c:3599
3230 // break — no tokenization
3231 } else if bslash { // c:3601
3232 chars[i - 1] = if (flags & ZSHTOK_SUBST) != 0 {
3233 Bnullkeep
3234 } else {
3235 Bnull
3236 };
3237 } else {
3238 // c:3605-3614 — try to parse `<N-N>` redirection.
3239 let t = i; // c:3605
3240 let mut j = i + 1;
3241 while j < chars.len() && chars[j].is_ascii_digit() { // c:3606 idigit
3242 j += 1;
3243 }
3244 if j < chars.len() && (chars[j] == '-') { // c:3607 IS_DASH
3245 let mut k = j + 1;
3246 while k < chars.len() && chars[k].is_ascii_digit() { // c:3609
3247 k += 1;
3248 }
3249 if k < chars.len() && chars[k] == '>' { // c:3611
3250 chars[t] = Inang; // c:3613
3251 chars[k] = Outang; // c:3614
3252 i = k + 1;
3253 bslash = false;
3254 continue;
3255 }
3256 }
3257 // c:3608/3611 `goto cont` — re-switch on current *s;
3258 // since none of the conditions matched, fall through.
3259 }
3260 }
3261 '(' | '|' | ')' if (flags & ZSHTOK_SHGLOB) != 0 => { // c:3617-3620
3262 // no tokenization under SHGLOB
3263 }
3264 '>' | '^' | '#' | '~' | '[' | ']' | '*' | '?' // c:3621-3631
3265 | '=' | '-' | '!' | '(' | '|' | ')' => {
3266 for (n, &t) in ztokens.iter().enumerate() { // c:3633
3267 if t as char == c { // c:3634
3268 if bslash { // c:3635
3269 chars[i - 1] = if (flags & ZSHTOK_SUBST) != 0 {
3270 Bnullkeep
3271 } else {
3272 Bnull
3273 };
3274 } else {
3275 chars[i] = char::from_u32(Pound as u32 + n as u32)
3276 .unwrap_or(c); // c:3638
3277 }
3278 break; // c:3639
3279 }
3280 }
3281 }
3282 _ => {}
3283 }
3284 bslash = false; // c:3646
3285 i += 1;
3286 }
3287 *s = chars.into_iter().collect();
3288}
3289
3290/// Port of `void remnulargs(char *s)` from `Src/glob.c:3649`.
3291///
3292/// C body (c:3651-3676): walks `s` looking for INULL bytes.
3293/// - `Bnullkeep` in SCAN phase: skip (the `continue` at c:3658)
3294/// — don't treat as a null marker.
3295/// - Any other INULL: switch to COPY phase. In copy phase:
3296/// - `Bnullkeep` becomes literal `\` (the "active backslash"
3297/// is re-materialized).
3298/// - Other INULLs: stripped.
3299/// - Non-INULL: kept.
3300/// - If the post-copy string is empty, replace with `Nularg`
3301/// (single-char empty-arg marker).
3302///
3303/// The previous Rust port collapsed the body to
3304/// `s.retain(|c| c != '\0' && c != Bnullkeep)` — a simple
3305/// strip of NUL and Bnullkeep. Three divergences:
3306/// - Stripped Bnullkeep entirely instead of preserving it in
3307/// the scan-only phase (when it appears BEFORE any other
3308/// inull) or converting to `\` in the copy phase.
3309/// - Didn't strip Snull/Dnull/Bnull/Nularg — those inulls
3310/// stayed in the output, polluting downstream processing.
3311/// - Didn't emit Nularg sentinel for empty post-strip strings.
3312pub fn remnulargs(s: &mut String) {
3313 // c:3649
3314 if s.is_empty() {
3315 // c:3654
3316 return;
3317 }
3318 // c:3664-3688 — C's walk rewrites nothing until it meets a
3319 // `Bnullkeep` or an `inull(c)`. Both those sentinels, and the `Meta`
3320 // the port's walk also tracks, are non-ASCII chars, so an all-ASCII
3321 // string can't reach either branch and C leaves it untouched. Say so
3322 // before paying for the `Vec<char>` materialization: `<[u8]>::is_ascii`
3323 // is libcore's precompiled word-at-a-time scan, while the walk below
3324 // is monomorphised into this crate and built unoptimised.
3325 if s.is_ascii() {
3326 return;
3327 }
3328 // c:3656 `inull(c)` predicate: Snull / Dnull / Bnull / Bnullkeep / Nularg.
3329 let is_inull =
3330 |c: char| c == Snull || c == Dnull || c == Bnull || c == Bnullkeep || c == Nularg;
3331 let src: Vec<char> = s.chars().collect();
3332 let mut out: Vec<char> = Vec::with_capacity(src.len());
3333 let mut i = 0usize;
3334 // rs-only adaptation of C's byte walk: C strings are byte arrays and
3335 // store an eight-bit char (e.g. `$'\M-\C-a'` = 0x81) RAW, so its byte
3336 // never aliases an inull sentinel. A Rust `String` is UTF-8, so zshrs
3337 // METAFIES that byte — Meta (0x83) + (byte ^ 0x20) — and 0x81 ^ 0x20 =
3338 // 0xA1, which IS the Nularg sentinel (0xBD/0xBE/0xBF/0xBC likewise
3339 // alias Snull/Dnull/Bnull/Bnullkeep). A metafied byte is DATA, not a
3340 // sentinel, so treat `Meta` + next char as an opaque pair on BOTH the
3341 // scan and copy walks — otherwise `${(qq)v}` for v=$'\M-\C-a' dropped
3342 // the 0xA1 and left the lone Meta (rs emitted `'\xc2\x83'` where zsh
3343 // emits `'\x81'`). C's byte walk needs no such guard.
3344 // Meta is a byte constant (0x83); a metafied string stores it as the
3345 // char U+0083, so compare against the char form.
3346 let meta = char::from(crate::ported::zsh_h::Meta);
3347 // c:3656 — SCAN phase: copy chars, skip standalone Bnullkeep.
3348 while i < src.len() {
3349 let c = src[i];
3350 if c == meta && i + 1 < src.len() {
3351 out.push(c);
3352 out.push(src[i + 1]);
3353 i += 2;
3354 continue;
3355 }
3356 if c == Bnullkeep {
3357 // c:3657 continue
3358 i += 1;
3359 continue;
3360 }
3361 if is_inull(c) {
3362 // c:3663 inull(c)
3363 // c:3664+ — COPY phase: walk the rest, fold Bnullkeep
3364 // to `\\`, strip other inulls.
3365 i += 1;
3366 while i < src.len() {
3367 let d = src[i];
3368 if d == meta && i + 1 < src.len() {
3369 // Metafied data pair — keep verbatim.
3370 out.push(d);
3371 out.push(src[i + 1]);
3372 i += 2;
3373 continue;
3374 }
3375 if d == Bnullkeep {
3376 // c:3666
3377 out.push('\\'); // c:3667
3378 } else if !is_inull(d) {
3379 // c:3668
3380 out.push(d); // c:3669
3381 }
3382 i += 1;
3383 }
3384 break;
3385 }
3386 // SCAN phase non-inull: keep verbatim.
3387 out.push(c);
3388 i += 1;
3389 }
3390 // c:3673-3675 — empty result → Nularg sentinel.
3391 if out.is_empty() {
3392 out.push(Nularg);
3393 }
3394 *s = out.into_iter().collect();
3395}
3396
3397/// Port of `qualdev(UNUSED(char *name), struct stat *buf, off_t dv, UNUSED(char *dummy))` from Src/glob.c:3688.
3398/// C: `static int qualdev(UNUSED(char *name), struct stat *buf, off_t dv,
3399/// UNUSED(char *dummy))` → `return (off_t)buf->st_dev == dv;`
3400#[allow(unused_variables)]
3401pub fn qualdev(name: &str, buf: &libc::stat, dv: i64, dummy: &str) -> i32 {
3402 // c:3688
3403 (buf.st_dev as i64 == dv) as i32 // c:3697
3404}
3405
3406/// Port of `qualnlink(UNUSED(char *name), struct stat *buf, off_t ct, UNUSED(char *dummy))` from Src/glob.c:3697.
3407/// C: ternary on `g_range`: < / > / == against `st_nlink`.
3408#[allow(unused_variables)]
3409pub fn qualnlink(name: &str, buf: &libc::stat, ct: i64, dummy: &str) -> i32 {
3410 // c:3697
3411 // c:3699-3701 — `g_range` selects `<` / `>` / `==`. This MUST read the same
3412 // `g_range` static the qual-eval sets (glob.rs ~5020) and that qualsize /
3413 // qualtime read — not the stale Rust-only `G_RANGE` duplicate, which was
3414 // never stored, leaving every `l[+-]N` comparison stuck on `==`.
3415 let g = g_range.load(Ordering::Relaxed);
3416 let nl = buf.st_nlink as i64; // c:3708
3417 if g < 0 {
3418 (nl < ct) as i32
3419 } else if g > 0 {
3420 (nl > ct) as i32
3421 } else {
3422 (nl == ct) as i32
3423 }
3424}
3425
3426/// Port of `qualuid(UNUSED(char *name), struct stat *buf, off_t uid, UNUSED(char *dummy))` from Src/glob.c:3708.
3427/// C: `return buf->st_uid == uid;`
3428#[allow(unused_variables)]
3429pub fn qualuid(name: &str, buf: &libc::stat, uid: i64, dummy: &str) -> i32 {
3430 // c:3708
3431 (buf.st_uid as i64 == uid) as i32 // c:3717
3432}
3433
3434/// Port of `qualgid(UNUSED(char *name), struct stat *buf, off_t gid, UNUSED(char *dummy))` from Src/glob.c:3717.
3435/// C: `return buf->st_gid == gid;`
3436#[allow(unused_variables)]
3437pub fn qualgid(name: &str, buf: &libc::stat, gid: i64, dummy: &str) -> i32 {
3438 // c:3717
3439 (buf.st_gid as i64 == gid) as i32 // c:3726
3440}
3441
3442/// Port of `qualisdev(UNUSED(char *name), struct stat *buf, UNUSED(off_t junk), UNUSED(char *dummy))` from Src/glob.c:3726.
3443/// C: `return S_ISBLK(buf->st_mode) || S_ISCHR(buf->st_mode);`
3444#[allow(unused_variables)]
3445pub fn qualisdev(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
3446 // c:3726
3447 let m = buf.st_mode as u32 & libc::S_IFMT as u32;
3448 ((m == libc::S_IFBLK as u32) || (m == libc::S_IFCHR as u32)) as i32 // c:3735
3449}
3450
3451/// Port of `qualisblk(UNUSED(char *name), struct stat *buf, UNUSED(off_t junk), UNUSED(char *dummy))` from Src/glob.c:3735.
3452/// C: `return S_ISBLK(buf->st_mode);`
3453#[allow(unused_variables)]
3454pub fn qualisblk(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
3455 // c:3735
3456 ((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFBLK as u32) as i32 // c:3744
3457}
3458
3459/// Port of `qualischr(UNUSED(char *name), struct stat *buf, UNUSED(off_t junk), UNUSED(char *dummy))` from Src/glob.c:3744.
3460/// C: `return S_ISCHR(buf->st_mode);`
3461#[allow(unused_variables)]
3462pub fn qualischr(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
3463 // c:3744
3464 ((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFCHR as u32) as i32 // c:3753
3465}
3466
3467/// Port of `qualisdir(UNUSED(char *name), struct stat *buf, UNUSED(off_t junk), UNUSED(char *dummy))` from Src/glob.c:3753.
3468/// C: `return S_ISDIR(buf->st_mode);`
3469#[allow(unused_variables)]
3470pub fn qualisdir(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
3471 // c:3753
3472 ((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFDIR as u32) as i32 // c:3762
3473}
3474
3475/// Port of `qualisfifo(UNUSED(char *name), struct stat *buf, UNUSED(off_t junk), UNUSED(char *dummy))` from Src/glob.c:3762.
3476/// C: `return S_ISFIFO(buf->st_mode);`
3477#[allow(unused_variables)]
3478pub fn qualisfifo(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
3479 // c:3762
3480 ((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFIFO as u32) as i32 // c:3771
3481}
3482
3483/// Port of `qualislnk(UNUSED(char *name), struct stat *buf, UNUSED(off_t junk), UNUSED(char *dummy))` from Src/glob.c:3771.
3484/// C: `return S_ISLNK(buf->st_mode);`
3485#[allow(unused_variables)]
3486pub fn qualislnk(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
3487 // c:3771
3488 ((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFLNK as u32) as i32 // c:3780
3489}
3490
3491/// Port of `qualisreg(UNUSED(char *name), struct stat *buf, UNUSED(off_t junk), UNUSED(char *dummy))` from Src/glob.c:3780.
3492/// C: `return S_ISREG(buf->st_mode);`
3493#[allow(unused_variables)]
3494pub fn qualisreg(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
3495 // c:3780
3496 ((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFREG as u32) as i32 // c:3789
3497}
3498
3499/// Port of `qualissock(UNUSED(char *name), struct stat *buf, UNUSED(off_t junk), UNUSED(char *dummy))` from Src/glob.c:3789.
3500/// C: `return S_ISSOCK(buf->st_mode);`
3501#[allow(unused_variables)]
3502pub fn qualissock(name: &str, buf: &libc::stat, junk: i64, dummy: &str) -> i32 {
3503 // c:3789
3504 ((buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFSOCK as u32) as i32
3505 // c:3798
3506}
3507
3508/// Port of `qualflags(UNUSED(char *name), struct stat *buf, off_t mod, UNUSED(char *dummy))` from Src/glob.c:3798.
3509/// C: `return mode_to_octal(buf->st_mode) & mod;`
3510#[allow(unused_variables)]
3511/// WARNING: param names don't match C — Rust=(name, buf, dummy) vs C=(name, buf, mod, dummy)
3512pub fn qualflags(name: &str, buf: &libc::stat, r#mod: i64, dummy: &str) -> i32 {
3513 // c:3798
3514 (mode_to_octal(buf.st_mode as u32) as i64 & r#mod) as i32 // c:3807
3515}
3516
3517/// Port of `qualmodeflags(UNUSED(char *name), struct stat *buf, off_t mod, UNUSED(char *dummy))` from Src/glob.c:3807.
3518/// C: `((v & y) == y && !(v & n))` where `y = mod & 07777`, `n = mod >> 12`.
3519#[allow(unused_variables)]
3520/// WARNING: param names don't match C — Rust=(name, buf, dummy) vs C=(name, buf, mod, dummy)
3521pub fn qualmodeflags(name: &str, buf: &libc::stat, r#mod: i64, dummy: &str) -> i32 {
3522 // c:3807
3523 let v = mode_to_octal(buf.st_mode as u32) as i64; // c:3818
3524 let y = r#mod & 0o7777;
3525 let n = r#mod >> 12;
3526 (((v & y) == y) && (v & n) == 0) as i32 // c:3818
3527}
3528
3529/// Port of `qualiscom(UNUSED(char *name), struct stat *buf, UNUSED(off_t mod), UNUSED(char *dummy))` from Src/glob.c:3818.
3530/// C: `return S_ISREG(buf->st_mode) && (buf->st_mode & S_IXUGO);`
3531#[allow(unused_variables)]
3532pub fn qualiscom(name: &str, buf: &libc::stat, r#mod: i64, dummy: &str) -> i32 {
3533 // c:3818
3534 let is_reg = (buf.st_mode as u32 & libc::S_IFMT as u32) == libc::S_IFREG as u32;
3535 let s_ixugo: u32 = (libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH) as u32;
3536 (is_reg && (buf.st_mode as u32 & s_ixugo) != 0) as i32 // c:3820
3537}
3538
3539/// Port of `static int g_units;` from `Src/glob.c`. Time/size unit
3540/// selector for the qualtime / qualsize qualifier predicates. Values
3541/// come from the TT_* enum (TT_DAYS=0, TT_HOURS=1, TT_MINS=2,
3542/// TT_WEEKS=3, TT_MONTHS=4 for time; TT_BYTES=0, TT_POSIX_BLOCKS=1,
3543/// TT_KILOBYTES=2, etc. for size).
3544pub static g_units: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
3545
3546/// Port of `static int g_range;` from `Src/glob.c`. Comparison
3547/// direction for qualtime / qualsize / qualnlink: -1 = `<`, 0 = `==`,
3548/// +1 = `>`. Set by the glob qualifier parser.
3549pub static g_range: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
3550
3551/// Port of `static int g_amc;` from `Src/glob.c`. Which mtime to read
3552/// for qualtime: 0 = atime, 1 = mtime, 2 = ctime. Set by the `am`,
3553/// `mm`, `cm` qualifier letters.
3554pub static g_amc: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
3555
3556/// Direct port of `static int qualsize(UNUSED(char *name),
3557/// struct stat *buf, off_t size, UNUSED(char *dummy))` from
3558/// `Src/glob.c:3825-3866`. Glob-qualifier predicate: returns true
3559/// when `buf->st_size` (scaled by `g_units`) compares to `size`
3560/// via `g_range`.
3561///
3562/// **Previous fakery:** the file held a same-named helper that was
3563/// actually a unit-conversion *parser* `(s, units) -> Option<(i64,
3564/// &str)>` — wrong shape, wrong semantics, zero callers. The
3565/// canonical C signature is restored here; the qualifier-eval site
3566/// at glob.rs:3914 already inlines the equivalent (uses
3567/// qualifier::Size struct fields) and is the live path.
3568#[allow(non_snake_case)]
3569pub fn qualsize(_name: &str, buf: &libc::stat, size: i64, _dummy: &str) -> i32 {
3570 use std::sync::atomic::Ordering;
3571 // c:3831 `zlong scaled = buf->st_size;`
3572 let mut scaled: i64 = buf.st_size as i64;
3573 // c:3837-3860 — switch (g_units) ceiling-divide by the unit.
3574 match g_units.load(Ordering::SeqCst) {
3575 x if x == TT_POSIX_BLOCKS => {
3576 scaled = (scaled + 511) / 512; // c:3838-3841
3577 }
3578 x if x == TT_KILOBYTES => {
3579 scaled = (scaled + 1023) / 1024; // c:3842-3845
3580 }
3581 x if x == TT_MEGABYTES => {
3582 scaled = (scaled + 1_048_575) / 1_048_576; // c:3846-3849
3583 }
3584 x if x == TT_GIGABYTES => {
3585 scaled = (scaled + 1_073_741_823) / 1_073_741_824; // c:3851-3854
3586 }
3587 x if x == TT_TERABYTES => {
3588 scaled = (scaled + 1_099_511_627_775) / 1_099_511_627_776; // c:3855-3858
3589 }
3590 _ => {} // c:3860 default: bytes — no scaling.
3591 }
3592 // c:3862-3864 — `g_range < 0 ? < : g_range > 0 ? > : ==`.
3593 let r = g_range.load(Ordering::SeqCst);
3594 i32::from(if r < 0 {
3595 scaled < size
3596 } else if r > 0 {
3597 scaled > size
3598 } else {
3599 scaled == size
3600 })
3601}
3602
3603/// Direct port of `static int qualtime(UNUSED(char *name),
3604/// struct stat *buf, off_t days, UNUSED(char *dummy))` from
3605/// `Src/glob.c:3870-3901`. Glob-qualifier predicate: returns true
3606/// when `now - buf->{a,m,c}time` (selected by `g_amc`, scaled by
3607/// `g_units`) compares to `days` via `g_range`.
3608///
3609/// **Previous fakery:** same misshapen parser as the qualsize one
3610/// above. Restored to C signature; the qualifier-eval site at
3611/// glob.rs:3940 already inlines the equivalent (uses qualifier::
3612/// Atime struct fields with the live atime/mtime/ctime read).
3613#[allow(non_snake_case)]
3614pub fn qualtime(_name: &str, buf: &libc::stat, days: i64, _dummy: &str) -> i32 {
3615 use std::sync::atomic::Ordering;
3616 // c:3876-3878 — `time(&now); diff = now - (g_amc == 0 ? st_atime
3617 // : g_amc == 1 ? st_mtime : st_ctime);`
3618 let now: i64 = std::time::SystemTime::now()
3619 .duration_since(std::time::UNIX_EPOCH)
3620 .map(|d| d.as_secs() as i64)
3621 .unwrap_or(0);
3622 let amc = g_amc.load(Ordering::SeqCst);
3623 let stamp: i64 = match amc {
3624 0 => buf.st_atime as i64, // c:3877
3625 1 => buf.st_mtime as i64, // c:3877
3626 _ => buf.st_ctime as i64, // c:3878
3627 };
3628 let mut diff: i64 = now - stamp;
3629 // c:3880-3896 — scale by g_units.
3630 match g_units.load(Ordering::SeqCst) {
3631 x if x == TT_DAYS => diff /= 86400, // c:3881-3883
3632 x if x == TT_HOURS => diff /= 3600, // c:3884-3886
3633 x if x == TT_MINS => diff /= 60, // c:3887-3889
3634 x if x == TT_WEEKS => diff /= 604800, // c:3890-3892
3635 x if x == TT_MONTHS => diff /= 2592000, // c:3893-3895
3636 _ => {} // c:3896 default: seconds.
3637 }
3638 // c:3898-3900 — same range comparison as qualsize.
3639 let r = g_range.load(Ordering::SeqCst);
3640 i32::from(if r < 0 {
3641 diff < days
3642 } else if r > 0 {
3643 diff > days
3644 } else {
3645 diff == days
3646 })
3647}
3648
3649/// Port of `static int qualsheval(char *name, UNUSED(struct stat *buf),
3650/// UNUSED(off_t days), char *str)` from `Src/glob.c:3907`.
3651///
3652/// C body (c:3907-3943):
3653/// ```c
3654/// if ((prog = parse_string(str, 0))) {
3655/// int ef = errflag, lv = lastval; // c:3912 save
3656/// unsetparam("reply"); // c:3915
3657/// setsparam("REPLY", ztrdup(name)); // c:3916
3658/// execode(prog, 1, 0, "globqual"); // c:3919
3659/// ret = lastval; // c:3921
3660/// errflag = ef | (errflag & ERRFLAG_INT); // c:3924 restore
3661/// lastval = lv; // c:3925
3662/// return !ret;
3663/// }
3664/// return 0;
3665/// ```
3666///
3667/// The `e:EXPR:` glob qualifier runs `EXPR` against each match
3668/// candidate with `$REPLY` pre-set to the filename. The expr is
3669/// evaluated IN THE CURRENT SHELL — it can read shell locals,
3670/// reference shell functions, set $reply etc.
3671///
3672/// The previous Rust port spawned `/bin/sh -c <expr>` via
3673/// `Command::new("sh")`, which:
3674/// 1. Loses access to current zsh function/alias/local-var scope.
3675/// 2. Runs `sh` (POSIX), not `zsh` — entire `(e:zsh-feature:)`
3676/// class of qualifiers silently failed.
3677/// 3. Skipped errflag/lastval save+restore.
3678///
3679/// Fixed: route through the canonical `ShellExecutor` for in-shell
3680/// evaluation. Set `REPLY` via paramtab (C `setsparam`), restore
3681/// `errflag`/`lastval` per c:3924-3925, mask any parse-time
3682/// ERRFLAG_ERROR while preserving ERRFLAG_INT (user interrupt).
3683/// WARNING: param names don't match C — Rust=(filename, expr) vs C=(name, buf, days, str)
3684pub fn qualsheval(filename: &str, _buf: &libc::stat, _data: i64, expr: &str) -> i32 {
3685 // c:3907
3686 // c:3912 — save errflag + lastval.
3687 let saved_errflag = errflag.load(Ordering::Relaxed); // c:3912
3688 let saved_lastval = LASTVAL.load(Ordering::Relaxed); // c:3912
3689 // c:3915 — `unsetparam("reply");`
3690 crate::ported::params::unsetparam("reply"); // c:3915
3691 // c:3916 — `setsparam("REPLY", ztrdup(name));`. Set in the
3692 // canonical paramtab so the evaluated expression sees `$REPLY`.
3693 crate::ported::params::setsparam("REPLY", filename); // c:3916
3694 // c:3919 — `execode(prog, 1, 0, "globqual");`. Route through the
3695 // executor via the `crate::ported::exec` accessor wrappers
3696 // (resolve the live executor on demand). Direct ShellExecutor
3697 // reach-in from src/ported/ is forbidden — see memory
3698 // feedback_no_exec_script_from_ported.
3699 let rc = {
3700 // c:Src/glob.c:3919 — `execode(prog, 1, 0, "globqual");`. execode
3701 // (c:Src/exec.c:1245-1266) APPENDS its context for the duration, so
3702 // code inside an `(e:'…':)` / `(+cmd)` glob qualifier sees
3703 // `cmdarg:globqual`. Popped on every return path. Bug #1069.
3704 let sync_eval_ctx = |stack: &[String]| {
3705 let joined = stack.join(":");
3706 if let Ok(mut tab) = crate::ported::params::paramtab().write() {
3707 if let Some(pm) = tab.get_mut("zsh_eval_context") {
3708 pm.u_arr = Some(stack.to_vec());
3709 pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
3710 }
3711 if let Some(pm) = tab.get_mut("ZSH_EVAL_CONTEXT") {
3712 pm.u_str = Some(joined);
3713 pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
3714 }
3715 }
3716 };
3717 if let Ok(mut ctx) = crate::ported::exec::zsh_eval_context.lock() {
3718 ctx.push("globqual".to_string());
3719 sync_eval_ctx(&ctx);
3720 }
3721 struct GlobQualCtxGuard<F: Fn(&[String])>(F);
3722 impl<F: Fn(&[String])> Drop for GlobQualCtxGuard<F> {
3723 fn drop(&mut self) {
3724 if let Ok(mut ctx) = crate::ported::exec::zsh_eval_context.lock() {
3725 ctx.pop();
3726 (self.0)(&ctx);
3727 }
3728 }
3729 }
3730 let _ctx_guard = GlobQualCtxGuard(sync_eval_ctx);
3731 crate::ported::exec::execute_script(expr).unwrap_or(1) // c:3919
3732 };
3733 let ret = LASTVAL.load(Ordering::Relaxed); // c:3921 ret = lastval
3734 let _ = rc;
3735 // c:3924 — `errflag = ef | (errflag & ERRFLAG_INT);`. Restore
3736 // pre-call errflag plus any interrupt bit set during eval.
3737 let post_errflag = errflag.load(Ordering::Relaxed);
3738 errflag.store(
3739 saved_errflag | (post_errflag & ERRFLAG_INT),
3740 Ordering::Relaxed,
3741 ); // c:3924
3742 // c:3925 — `lastval = lv;`. Restore pre-call lastval.
3743 LASTVAL.store(saved_lastval, Ordering::Relaxed); // c:3925
3744 // c:3927-3937 — `inserts = getaparam("reply") || gethparam("reply")`,
3745 // else the `reply`/`REPLY` SCALAR as a one-element list. The eval can
3746 // thus REPLACE the matched name with zero-or-more names (`reply=(…)`)
3747 // or rename it (`REPLY=…`). REPLY was seeded to `filename` at c:3916,
3748 // so this is always Some — at minimum the (possibly modified) REPLY.
3749 let inserts: Vec<String> = match crate::ported::params::getaparam("reply") {
3750 Some(arr) => arr, // c:3927
3751 None => match crate::ported::params::getsparam("reply") // c:3931
3752 .or_else(|| crate::ported::params::getsparam("REPLY"))
3753 {
3754 Some(scalar) => vec![scalar], // c:3934-3936
3755 None => Vec::new(),
3756 },
3757 };
3758 *INSERTS.lock().unwrap() = Some(inserts);
3759 // c:3941 — `return !ret;` — qualifier passes iff lastval is 0.
3760 i32::from(ret == 0)
3761}
3762
3763/// Port of `qualnonemptydir(char *name, struct stat *buf, UNUSED(off_t days), UNUSED(char *str))` from Src/glob.c:3948.
3764/// C: opendir(name) and check if any non-`.`/`..` entries exist.
3765pub fn qualnonemptydir(name: &str, buf: &libc::stat, days: i64, str: &str) -> i32 {
3766 // c:3948
3767 // c:3948 — `if (!S_ISDIR(buf->st_mode)) return 0;`
3768 if (buf.st_mode as u32 & libc::S_IFMT as u32) != libc::S_IFDIR as u32 {
3769 // c:3950
3770 return 0;
3771 }
3772 // c:3953-3964 — opendir + readdir loop, skip "." and ".." entries.
3773 match fs::read_dir(name) {
3774 Ok(entries) => entries.filter_map(|e| e.ok()).any(|e| {
3775 let n = e.file_name();
3776 let s = n.to_string_lossy();
3777 s != "." && s != ".."
3778 }) as i32,
3779 Err(_) => 0,
3780 }
3781}
3782
3783// =====================================================================
3784// Thread-local glob option snapshot — race fix.
3785//
3786// `Src/glob.c` reads `isset(NULLGLOB)` / `isset(BAREGLOBQUAL)` / etc.
3787// inline at each callsite during a glob. In zsh those reads hit the
3788// thread-local C `opts[]` array (zsh is single-threaded). In zshrs
3789// they hit `OPTS_LIVE` — a process-wide `RwLock<HashMap>`.
3790//
3791// Embedders that snapshot/restore options around individual glob
3792// invocations on multiple threads (e.g. stryke's `StrykeGlobOptsGuard`
3793// + `glob_par` rayon parallelism) race: thread A sets bareglobqual=1,
3794// calls glob; while A is mid-parse, thread B's matching guard restores
3795// bareglobqual=0; A's qualifier-parse step sees bareglobqual=0 and
3796// drops the trailing `(N)` as a literal substring, producing false
3797// matches.
3798//
3799// Fix: snapshot every glob-relevant option ONCE at glob() entry into
3800// a thread-local cache, then read all isset() inside glob from the
3801// snapshot. The snapshot is dropped on glob exit via an RAII guard.
3802// Each thread's in-flight glob() sees a coherent set of options
3803// regardless of concurrent setopt/unsetopt on other threads.
3804// =====================================================================
3805
3806/// Snapshot of glob-relevant zsh options taken at glob entry. Holds
3807/// the live `isset(...)` value for each option that the glob engine
3808/// or its helpers (`parse_qualifiers`, `scanner`,
3809/// `sort_matches`, `glob_emit_path`) consult.
3810#[derive(Debug, Clone, Copy, Default)]
3811pub struct GlobOptSnapshot {
3812 pub bareglobqual: bool,
3813 pub braceccl: bool,
3814 pub caseglob: bool,
3815 pub extendedglob: bool,
3816 pub globdots: bool,
3817 pub globstarshort: bool,
3818 pub listtypes: bool,
3819 pub markdirs: bool,
3820 pub nullglob: bool,
3821 pub numericglobsort: bool,
3822}
3823
3824/// RAII guard that drops the thread-local snapshot on scope exit.
3825/// Constructed by `enter_glob_scope()`. Nested glob calls on the
3826/// same thread reuse the outer snapshot (the guard's `populated`
3827/// field tracks whether we installed or just observed).
3828pub struct GlobOptsGuard {
3829 populated: bool,
3830}
3831
3832/// Sort specifier flags
3833// `GlobSort` / `SortOrder` / `SortSpec` deleted — C uses bit-flag
3834// `int tp` in `struct globsort { int tp; char *exec; }` (Src/glob.c:155):
3835// tp & ~GS_DESC selects the sort key (GS_NAME/GS_DEPTH/GS_EXEC/…),
3836// tp & GS_DESC reverses direction (`O` vs `o` qualifier),
3837// tp << GS_SHIFT carries the follow-link variants.
3838// All those bits already exist as i32 constants at glob.rs:33+
3839// (GS_NAME / GS_DEPTH / GS_SIZE / … / GS_DESC / GS_NONE / GS__SIZE /
3840// …). The enum + Ascending/Descending + struct triple-wrapper was a
3841// Rust-only convenience with no C counterpart; callers now operate
3842// on the raw i32 the same way `gmatchcmp()` at glob.c:936 does.
3843
3844// `TimeUnit` / `SizeUnit` / `RangeOp` enums deleted — Rust-only
3845// wrappers around constants/chars that exist as raw values in C:
3846// `TimeUnit::Seconds` → TT_SECONDS i32 (glob.c:126 → glob.rs:99)
3847// `TimeUnit::Minutes` → TT_MINS (c:123)
3848// `TimeUnit::Hours` → TT_HOURS (c:122)
3849// `TimeUnit::Days` → TT_DAYS (c:121)
3850// `TimeUnit::Weeks` → TT_WEEKS (c:124)
3851// `TimeUnit::Months` → TT_MONTHS (c:125)
3852// `SizeUnit::Bytes` → TT_BYTES (c:128)
3853// `SizeUnit::PosixBlocks` → TT_POSIX_BLOCKS (c:129)
3854// `SizeUnit::Kilobytes` → TT_KILOBYTES (c:130)
3855// `SizeUnit::Megabytes` → TT_MEGABYTES (c:131)
3856// `SizeUnit::Gigabytes` → TT_GIGABYTES (c:132)
3857// `SizeUnit::Terabytes` → TT_TERABYTES (c:133)
3858// `RangeOp::Less`/`Equal`/`Greater` → raw chars `<` `=` `>`
3859// (zsh's qgetnum at glob.c:827 returns the raw operator char
3860// and the qualifier handlers switch on it inline).
3861
3862/// Port of `TestMatchFunc` — the `int (*)(char *, struct stat *, off_t,
3863/// char *)` function-pointer type held by `struct qual.func` (Src/glob.c).
3864/// Every glob-qualifier test (qualuid, qualisreg, qualiscom, qualsize, …)
3865/// has this signature so `insert()` can call them uniformly through the
3866/// `func` slot. `name` is the metafied path, `buf` the stat, `data` the
3867/// qualifier argument (uid, mode bits, day count, …), `sdata` the
3868/// expression text (only `qualsheval` uses it; others ignore it).
3869pub type TestMatchFunc = fn(name: &str, buf: &libc::stat, data: i64, sdata: &str) -> i32;
3870
3871/// Port of `struct qual` from `Src/glob.c:138`. A node in the linked list
3872/// of glob qualifiers parsed from a `(…)` suffix. `insert()` (glob.c:346)
3873/// walks this list per candidate file, calling `func` on each node and
3874/// accepting/rejecting the file by `sense`. The terminating node carries
3875/// `func == None` (C tests `qn && qn->func` to stop).
3876///
3877/// REPRESENTATION: C's `next`/`or` are `struct qual *` pointers into a
3878/// graph that `parsepat` builds and relinks through aliased cursors (the
3879/// OR-distribution at glob.c:1797-1820 aliases one node from several
3880/// chains at once). Rust single-ownership `Box` cannot express that, so
3881/// the port uses an arena ([`QualArena`]): nodes live in a `Vec<qual>`
3882/// and `next`/`or` are `Option<usize>` indices. C pointer assignments map
3883/// to index assignments, so the aliased-cursor algorithm translates
3884/// almost verbatim.
3885#[allow(non_camel_case_types)]
3886#[derive(Clone, Debug)]
3887pub struct qual {
3888 /// c:139 — `struct qual *next;` next qualifier in the AND-chain.
3889 pub next: Option<usize>,
3890 /// c:140 — `struct qual *or;` head of an alternative OR-chain.
3891 pub or: Option<usize>,
3892 /// c:141 — `TestMatchFunc func;` the test to call; `None` terminates.
3893 pub func: Option<TestMatchFunc>,
3894 /// c:142 — `off_t data;` argument passed to `func`.
3895 pub data: i64,
3896 /// c:143 — `int sense;` bit0: 0=assert / 1=negate; bit1: follow links.
3897 pub sense: i32,
3898 /// c:144 — `int amc;` which timestamp to test (0=a, 1=m, 2=c).
3899 pub amc: i32,
3900 /// c:145 — `int range;` test `<` / `>` / `=` (signum of comparison).
3901 pub range: i32,
3902 /// c:146 — `int units;` multiplier for time (TT_DAYS…) or size.
3903 pub units: i32,
3904 /// c:147 — `char *sdata;` expression to eval (currently qualsheval only).
3905 pub sdata: Option<String>,
3906}
3907
3908impl qual {
3909 /// A fresh node with the C zero-initialised defaults (`hcalloc`).
3910 pub fn new() -> qual {
3911 qual {
3912 next: None,
3913 or: None,
3914 func: None,
3915 data: 0,
3916 sense: 0,
3917 amc: 0,
3918 range: 0,
3919 units: 0,
3920 sdata: None,
3921 }
3922 }
3923}
3924
3925impl Default for qual {
3926 fn default() -> qual {
3927 qual::new()
3928 }
3929}
3930
3931/// Arena backing the [`qual`] graph. C allocates each `struct qual` with
3932/// `hcalloc` and links them by pointer; the Rust port allocates them in
3933/// this `Vec` and links them by index. `alloc()` mirrors a single
3934/// `hcalloc(sizeof *qn)` and returns the new node's index; `head` holds
3935/// the index of `quals` (the list `insert()` walks), `None` when empty.
3936#[derive(Clone, Default, Debug)]
3937pub struct QualArena {
3938 pub nodes: Vec<qual>,
3939 pub head: Option<usize>,
3940}
3941
3942impl QualArena {
3943 pub fn new() -> QualArena {
3944 QualArena {
3945 nodes: Vec::new(),
3946 head: None,
3947 }
3948 }
3949
3950 /// c:`qn = (struct qual *)hcalloc(sizeof *qn);` — allocate a fresh
3951 /// zeroed node and return its arena index.
3952 pub fn alloc(&mut self) -> usize {
3953 self.nodes.push(qual::new());
3954 self.nodes.len() - 1
3955 }
3956}
3957
3958/// A glob qualifier function
3959#[derive(Debug, Clone)]
3960/// One glob qualifier — Rust-extension sum type. C uses a linked
3961/// list of `struct qual` (`Src/zsh.h:140-152`) with function-pointer
3962/// `func` per node; each variant here maps to one of C's `q*` test
3963/// ported (`qisreg`, `qisdir`, `qowner`, `qtime`, ...) at
3964/// `Src/glob.c:1080-1340`. The full `struct qual` port + per-test fn
3965/// dispatch lives in a later phase; this enum keeps the parsed-form
3966/// the per-match filter inside `scanner()` (line 500) needs.
3967#[allow(non_camel_case_types)]
3968pub enum qualifier {
3969 /// File type qualifiers
3970 IsRegular,
3971 IsDirectory,
3972 IsSymlink,
3973 IsSocket,
3974 IsFifo,
3975 IsBlockDev,
3976 IsCharDev,
3977 IsDevice,
3978 IsExecutable,
3979
3980 /// Permission qualifiers
3981 Readable,
3982 Writable,
3983 Executable,
3984 WorldReadable,
3985 WorldWritable,
3986 WorldExecutable,
3987 GroupReadable,
3988 GroupWritable,
3989 GroupExecutable,
3990 Setuid,
3991 Setgid,
3992 Sticky,
3993
3994 /// Ownership qualifiers
3995 OwnedByEuid,
3996 OwnedByEgid,
3997 OwnedByUid(u32),
3998 OwnedByGid(u32),
3999
4000 /// Numeric qualifiers with range. `unit` is a `TT_*` i32
4001 /// (glob.rs:99-113 / glob.c:121-133); `op` is the raw range
4002 /// operator char (`<`, `=`, `>`) — mirrors C's qgetnum which
4003 /// returns the operator char and the handler switches inline.
4004 Size {
4005 value: u64,
4006 unit: i32,
4007 op: char,
4008 },
4009 Links {
4010 value: u64,
4011 op: char,
4012 },
4013 Atime {
4014 value: i64,
4015 unit: i32,
4016 op: char,
4017 },
4018 Mtime {
4019 value: i64,
4020 unit: i32,
4021 op: char,
4022 },
4023 Ctime {
4024 value: i64,
4025 unit: i32,
4026 op: char,
4027 },
4028
4029 /// Mode specification
4030 Mode {
4031 yes: u32,
4032 no: u32,
4033 },
4034
4035 /// Device number
4036 Device(u64),
4037
4038 /// Non-empty directory
4039 NonEmptyDir,
4040
4041 /// Shell evaluation
4042 Eval(String),
4043
4044 /// `^` — toggle the running match sense for SUBSEQUENT qualifiers in
4045 /// this list. C uses a running `sense ^= 1` (Src/glob.c:1346) stored
4046 /// per `struct qual.sense`; the enum can't carry per-node sense, so a
4047 /// marker in the list reproduces it: `check_qualifier_list` flips a
4048 /// running sense bit on each marker and XORs it into later tests.
4049 /// Fixes `*(/^@)` (dir AND not-symlink), `*(.^x)`, etc.
4050 ToggleSense,
4051
4052 /// `-` — c:1348-1352 `sense ^= 2;` toggles link-following for the
4053 /// SUBSEQUENT qualifiers only: each node then tests the target's stat
4054 /// (c:399-403 `bp = (qn->sense & 2) ? &buf2 : &buf;`). `*(@-.)` is a
4055 /// symlink whose target is a regular file.
4056 ToggleFollow,
4057}
4058
4059// Misnamed `gmatchcmp(&str, &str)` deleted — was a Rust-only
4060// locale-aware string compare claiming to be the C qsort
4061// comparator. The real C `gmatchcmp(Gmatch, Gmatch)` at glob.c:936
4062// is now ported as `gmatchcmp(&GlobMatch, &GlobMatch, &[i32], bool)`
4063// below. The string-compare case the old name claimed routes
4064// through canonical `zstrcmp` (sort.c:191).
4065
4066// `GlobOptions` struct deleted — Rust-only Bag-of-options with no
4067// C counterpart. C reads each option directly from the global
4068// `opts[]` array via `isset(NULL_GLOB)` / `isset(EXTENDED_GLOB)`
4069// / etc. (Src/options.c). The Rust port uses the canonical
4070// `opt_state_get(name) -> Option<bool>`
4071// which reads from the same global store. Inlined at each
4072// callsite as `opt_state_get("name").unwrap_or(default)`.
4073
4074/// Parsed glob qualifier set
4075#[derive(Debug, Clone, Default)]
4076/// Compiled qualifier list for one glob.
4077/// Mirrors the `struct qual *` linked list `parsepat()`
4078/// (Src/glob.c:791) builds — every `(qual)` after a glob pattern
4079/// adds to it.
4080#[allow(non_camel_case_types)]
4081pub struct qualifier_set {
4082 // c:138
4083 pub qualifiers: Vec<qualifier>,
4084 pub alternatives: Vec<Vec<qualifier>>,
4085 /// Packed sort-spec flags, one per `o`/`O` qualifier in the pattern.
4086 /// Each entry is the C `struct globsort.tp` field — `GS_NAME` /
4087 /// `GS_DEPTH` / `GS_EXEC` / `GS_SIZE` / `GS_ATIME` / `GS_MTIME` /
4088 /// `GS_CTIME` / `GS_LINKS` (or their `<< GS_SHIFT` follow-link
4089 /// variants), OR'd with `GS_DESC` for reverse direction.
4090 pub sorts: Vec<i32>, // c:155 struct globsort.tp[]
4091 /// c:74 `struct globsort.exec` — the eval code for each `GS_EXEC`
4092 /// (`oe:…:` / `o+…`) sort spec, indexed by the `idx` packed into the
4093 /// sort entry's high bits (`GS_EXEC | (idx << 16)`). Evaluated per
4094 /// match with `$REPLY` = the match name; the resulting `$REPLY` is
4095 /// the sort key (the original name is still emitted).
4096 pub sort_exec: Vec<String>, // c:74 globsort.exec
4097 /// The faithful glob.c `struct qual` representation of `alternatives`,
4098 /// built at parse end (additive — the enum is kept for now). AND-chain
4099 /// via `next`, alternatives via `or`, per-node `sense`/`range`/`amc`/
4100 /// `units`/`sdata`. The convergence target: `check_qualifiers` walks
4101 /// THIS via `insert()` semantics (glob.c:392-419), and the enum is
4102 /// eventually deleted.
4103 pub quals: QualArena, // c:206 curglobdata.gd_quals
4104 pub first: Option<i32>,
4105 pub last: Option<i32>,
4106 pub colon_mods: Option<String>,
4107 pub pre_words: Vec<String>,
4108 pub post_words: Vec<String>,
4109 /// `(M)` qualifier — append `/` to directory entries in output.
4110 /// Direct port of zsh/Src/glob.c:1557-1561 (`case 'M'`):
4111 /// `gf_markdirs = !(sense & 1)` — set when the qualifier appears
4112 /// without a `^` toggle. Stored per-qualifier-set rather than per
4113 /// GlobOptions so a single glob call's qualifier picks it up.
4114 /// `None` = no `M` qualifier (the MARK_DIRS option decides, c:1255
4115 /// `gf_markdirs = isset(MARKDIRS)`); `Some(false)` = `^M`, which
4116 /// turns marking OFF even under `setopt markdirs`.
4117 pub mark_dirs: Option<bool>,
4118 /// `(T)` qualifier — append type-char (ls -F style) to every entry.
4119 /// Direct port of zsh/Src/glob.c:1562-1566 (`case 'T'`).
4120 pub list_types: bool,
4121 /// `gf_follow` — c:1559/1564 `gf_follow = sense & 2;`: a `-` before
4122 /// `M`/`T` makes the marker describe the symlink TARGET (c:366-371).
4123 pub mark_follow: bool,
4124 /// `(N)` qualifier — per-glob nullglob: empty result on no-match,
4125 /// no error. Direct port of zsh/Src/glob.c:1567-1569 (`case 'N'`):
4126 /// `gf_nullglob = !(sense & 1)` — set when `(N)` appears without
4127 /// the `^` toggle.
4128 pub nullglob: bool,
4129 /// `(D)` qualifier — include dotfiles in the glob result (override
4130 /// the global `globdots`/`dotglob` option for this glob alone).
4131 /// Direct port of zsh/Src/glob.c case 'D' which sets
4132 /// `gf_glob.dots = 1` for the duration of this glob expansion.
4133 pub globdots: bool,
4134 /// `(YN)` qualifier — short-circuit: limit number of matches to
4135 /// at most N. Direct port of zsh/Src/glob.c:1579-1595 (`case 'Y'`)
4136 /// which sets the C `shortcircuit` int. The matcher should stop
4137 /// after collecting N entries; the Rust port truncates after the
4138 /// match walk completes (the optimization gain is the same in
4139 /// the typical short-glob case). Bug #41 in docs/BUGS.md.
4140 pub short_circuit: Option<i32>,
4141 /// `(n)` qualifier — per-glob numeric sort. Direct port of
4142 /// zsh/Src/glob.c:1575-1577 (`case 'n': gf_numsort = !(sense & 1)`).
4143 /// Distinct from the `o`/`O` sort KEY `n` (= GS_NAME, lexical):
4144 /// standalone `(n)` makes the name comparison NUMERIC (so `*(n)`
4145 /// orders `f2` before `f10`), OR'd with the global NUMERIC_GLOB_SORT
4146 /// option at sort time (glob.c:947/983 `gf_numsort ?
4147 /// SORTIT_NUMERICALLY : 0`). `None` = not specified (fall back to
4148 /// the global option); `Some(true)` = `(n)`; `Some(false)` = `(^n)`
4149 /// which OVERRIDES the global option to force lexical.
4150 pub numsort: Option<bool>,
4151}
4152
4153/// Enter a glob scope: snapshot options into TLS if no outer scope
4154/// is active, returning a guard that clears the snapshot on Drop.
4155/// Re-entrant calls (nested globdata_glob via brace expansion etc.)
4156/// return a no-op guard that observes the existing snapshot.
4157pub fn enter_glob_scope() -> GlobOptsGuard {
4158 let already = GLOB_OPTS_TLS.with_borrow(|g| g.is_some());
4159 if already {
4160 return GlobOptsGuard { populated: false };
4161 }
4162 let snap = GlobOptSnapshot::capture();
4163 GLOB_OPTS_TLS.with_borrow_mut(|g| *g = Some(snap));
4164 GlobOptsGuard { populated: true }
4165}
4166
4167/// Read a glob-relevant option through the TLS snapshot when one is
4168/// active; fall back to the live `isset()` otherwise. Use this in
4169/// any glob-engine helper that previously read `isset(OPT)` for one
4170/// of the snapshotted options.
4171#[inline]
4172pub fn glob_isset(opt: i32) -> bool {
4173 GLOB_OPTS_TLS.with_borrow(|g| match g {
4174 Some(snap) => match opt {
4175 x if x == BAREGLOBQUAL => snap.bareglobqual,
4176 x if x == BRACECCL => snap.braceccl,
4177 x if x == CASEGLOB => snap.caseglob,
4178 x if x == EXTENDEDGLOB => snap.extendedglob,
4179 x if x == GLOBDOTS => snap.globdots,
4180 x if x == GLOBSTARSHORT => snap.globstarshort,
4181 x if x == LISTTYPES => snap.listtypes,
4182 x if x == MARKDIRS => snap.markdirs,
4183 x if x == NULLGLOB => snap.nullglob,
4184 x if x == NUMERICGLOBSORT => snap.numericglobsort,
4185 _ => isset(opt),
4186 },
4187 None => isset(opt),
4188 })
4189}
4190
4191// ===========================================================
4192// `impl globdata` block above kept only for the `new()`
4193// constructor (Default-style). All scanner / parser / qualifier
4194// ported below are top-level — C glob.c has them as top-level
4195// statics that mutate the file-static `curglobdata`. Each is
4196// flagged with `// RUST-ONLY` and a comment naming the closest
4197// C equivalent + the proper-port target (typically `scanner`
4198// at glob.c:500 driving `insert` at c:346).
4199// ===========================================================
4200
4201/// Main entry point: expand a glob pattern.
4202///
4203/// Closest C equivalent: `zglob` driver at glob.c:1214 calls
4204/// `parsepat` (c:791) and then `scanner` (c:500). The Rust port
4205/// here orchestrates qualifier parsing, brace expansion (which C
4206/// does separately in `xpandbraces`), then drives the faithful
4207/// `parsecomplist` (c:710) → `complist` → `scanner` (c:500) path.
4208pub fn globdata_glob(state: &mut globdata, pattern: &str) -> Vec<String> {
4209 // RUST-ONLY
4210 // Brace pre-expansion. In zsh, `xpandbraces` (zsh/Src/glob.c:2275)
4211 // runs during substitution before glob — patterns reaching glob()
4212 // are already brace-free in the production path (vm_helper handles
4213 // it). For direct programmatic callers of glob_with_options, run
4214 // the brace pass here so `GlobOptions.brace_ccl` is actually
4215 // consulted: with brace_ccl set, `{a-mnop}` expands to a..m,n,o,p
4216 // per glob.c:2424 BRACECCL block; without, only `{a,b}` lists and
4217 // `{1..5}`/`{a..e}` ranges expand. Recurse on each variant and
4218 // concatenate matches.
4219 let brace_ccl = glob_isset(BRACECCL);
4220 if hasbraces(pattern, brace_ccl) {
4221 let mut all = Vec::new();
4222 for variant in xpandbraces(pattern, brace_ccl) {
4223 all.extend(globdata_glob(state, &variant));
4224 }
4225 return all;
4226 }
4227
4228 state.matches.clear();
4229 state.pathbuf.clear();
4230 state.pathpos = 0;
4231
4232 // Parse qualifiers first so a bare-qualifier pattern like `dir(/)`
4233 // (no wildcard, just a stat-based filter) still enters the expansion
4234 // path. Without this, `haswilds("dir(/)")` returns false and the
4235 // pattern echoes back unfiltered, which defeats the whole point of
4236 // qualifiers.
4237 let (pat, quals) = parse_qualifiers(pattern);
4238 state.qualifiers = quals;
4239
4240 // !!! RUST-ONLY decision — NO DIRECT C COUNTERPART !!!
4241 // Is `pat` a lexer-TOKENIZED word (the only form C's `zglob`, c:1221,
4242 // ever receives) or a raw programmatic pattern? In a tokenized word
4243 // only the glob tokens are active: a plain `*`, `?`, `[`, `~` … was
4244 // quoted or substituted and must not be tokenized again. `glob_path`
4245 // also serves raw callers (compsys, builtins) whose metachars are
4246 // plain, so the word's spelling decides: any token means tokenized,
4247 // none means raw.
4248 //
4249 // Under GLOB_SUBST the plain characters are substituted values that C
4250 // would have shtokenized in `strcatsub` (c:Src/subst.c:822/829-830).
4251 // The port's paramsubst does not tokenize them — it carries the option
4252 // on the global table (including the `${~spec}` carrier, subst.rs
4253 // `TILDE_GLOBSUBST_CARRIER`) for the glob layer to apply — so the raw
4254 // treatment, which activates every plain metachar, stands in for that
4255 // shtokenize.
4256 //
4257 // Ask the WHOLE word, qualifier block included: in `"*"(N)` the only
4258 // tokens are the qualifier's Inpar/Outpar, so the stripped `*` alone
4259 // looked raw and was re-tokenized into a live Star — the quoted star
4260 // listed the directory where zsh expands to nothing.
4261 let word_is_tokenized =
4262 crate::ported::lex::has_token(pattern) && !isset(crate::ported::zsh_h::GLOBSUBST);
4263
4264 // c:Src/glob.c — `A~B` exclusion. zsh compiles the ENTIRE glob
4265 // (path components + `~B` exclusion + `**`) into ONE Patprog and
4266 // matches each candidate PATH against it, so the `~B` applies to the
4267 // whole path. zshrs's component scanner can't express a path-level
4268 // exclusion, so split a TOP-LEVEL `~` (extendedglob; depth 0, not in
4269 // `[...]`/`(...)`, not Bnull-escaped) off here into the main pattern
4270 // plus full-path exclusion patterns, glob the main pattern, then drop
4271 // matched paths matching any exclusion below. Without this the `~B`
4272 // (which may contain `/`) was split across path components and either
4273 // excluded everything (`~*/.git/*` → `*.txt~*` matched all) or
4274 // nothing. `~` inside one component still works the same (main globs,
4275 // the post-filter applies the exclusion).
4276 let (pat, glob_exclusions): (String, Vec<String>) =
4277 if glob_isset(EXTENDEDGLOB) && (pat.contains('~') || pat.contains('\u{e198}')) {
4278 // In a TOKENIZED word (see the `pat_tok` note below) only the
4279 // Tilde token is the operator (c:Src/pattern.c:800 compares
4280 // against `zpc_special[ZPC_TILDE]`, the token); a plain `~` was
4281 // quoted or substituted. A raw programmatic pattern keeps the
4282 // plain spelling as the operator.
4283 let raw_tilde_is_op = !word_is_tokenized;
4284 let cv: Vec<char> = pat.chars().collect();
4285 let mut parts: Vec<String> = Vec::new();
4286 let mut cur = String::new();
4287 let mut bd = 0i32; // `[...]` depth
4288 let mut pd = 0i32; // `(...)` depth
4289 let mut k = 0usize;
4290 while k < cv.len() {
4291 let c = cv[k];
4292 match c {
4293 // Escape — the next char is a literal, never the
4294 // exclusion operator. Three spellings reach here:
4295 // `zshtokenize` emits Bnull for `\X` (c:3591) and
4296 // Bnullkeep under ZSHTOK_SUBST (the flag `shtokenize`
4297 // uses, so every `${~var}` pattern), and an UNtokenized
4298 // raw `\X` arrives from zshrs's programmatic glob entry
4299 // (`glob_path`) — the same raw form `patcomppiece`
4300 // already accepts (pattern.rs, `b'\\'` arm). Recognising
4301 // only Bnull split `a\~b*` at an ESCAPED tilde and turned
4302 // the literal `~` into an exclusion, so `_path_files`'
4303 // `tmp1=( $~tmp1 )` — whose patterns have the typed word
4304 // quoted with QT_BACKSLASH_PATTERN (computil.c:5001) —
4305 // matched nothing: `cp /etc/paths~or<TAB>` completed to
4306 // nothing where zsh inserts `/etc/paths\~orig`.
4307 Bnull | Bnullkeep | '\\' => {
4308 cur.push(c);
4309 if k + 1 < cv.len() {
4310 cur.push(cv[k + 1]);
4311 k += 2;
4312 continue;
4313 }
4314 }
4315 '[' => {
4316 bd += 1;
4317 cur.push(c);
4318 }
4319 ']' => {
4320 if bd > 0 {
4321 bd -= 1;
4322 }
4323 cur.push(c);
4324 }
4325 '(' | '\u{e188}' => {
4326 pd += 1;
4327 cur.push(c);
4328 }
4329 ')' | '\u{e18a}' => {
4330 if pd > 0 {
4331 pd -= 1;
4332 }
4333 cur.push(c);
4334 }
4335 '~' | '\u{e198}' if bd == 0 && pd == 0 && (c == '\u{e198}' || raw_tilde_is_op) => {
4336 parts.push(std::mem::take(&mut cur));
4337 }
4338 _ => cur.push(c),
4339 }
4340 k += 1;
4341 }
4342 parts.push(cur);
4343 // Only split when there's a non-empty main pattern AND at
4344 // least one exclusion (a leading `~` is home-dir, handled by
4345 // filesub before glob — never reaches here as an exclusion).
4346 if parts.len() > 1 && !parts[0].is_empty() {
4347 let main = parts.remove(0);
4348 (main, parts)
4349 } else {
4350 // No usable split — restore the original pattern verbatim
4351 // (the `~` separators dropped during the scan).
4352 (cv.iter().collect(), Vec::new())
4353 }
4354 } else {
4355 (pat, Vec::new())
4356 };
4357
4358 // c:Src/glob.c:1843-1854 — `if (!q || errflag) { ... return; }`. When
4359 // the qualifier parser already emitted a diagnostic (e.g. "number
4360 // expected" from qgetnum at c:832) and set errflag, abort glob
4361 // expansion entirely rather than continuing to scan with a partial
4362 // qualifier set. Without this gate, `?(a)` (where `(a)` is an
4363 // invalid qualifier) error-prints "number expected" then continues
4364 // to expand `?` against the dir, emitting matches alongside the
4365 // error — bug #549.
4366 if crate::ported::utils::errflag.load(std::sync::atomic::Ordering::SeqCst)
4367 & crate::ported::zsh_h::ERRFLAG_ERROR
4368 != 0
4369 {
4370 return Vec::new();
4371 }
4372
4373 // Now check wildcards on the qualifier-stripped pattern. A pure
4374 // literal with a qualifier (`name(.)`) still needs to enter the
4375 // scanner so the qualifier filter can run against the literal name.
4376 // haswilds scans TOKENIZED strings (c:Src/glob.c:1230 runs on the
4377 // lexer-tokenized word); glob_path receives both tokenized (zglob)
4378 // and untokenized (expand_glob fast paths) patterns, so tokenize a
4379 // local copy for the check — existing token chars pass through
4380 // zshtokenize untouched, untokenized metachars gain their tokens.
4381 //
4382 // Only an UNTOKENIZED pattern may be tokenized, though. C never runs
4383 // zshtokenize over a word that reaches zglob (c:1221 `ostr` is the
4384 // lexer's tokenized word after prefork): a plain `*` in it is a QUOTED
4385 // or SUBSTITUTED character and must stay literal. Tokenizing it again
4386 // made `x=a; print $x\**` (node `a*<Star>`) match `ab` as well as
4387 // `a*b`. See `word_is_tokenized` above.
4388 let mut pat_tok = pat.clone();
4389 if !word_is_tokenized {
4390 tokenize(&mut pat_tok); // c:Src/glob.c:3548
4391 }
4392 if !haswilds(&pat_tok) && state.qualifiers.is_none() {
4393 return vec![pattern.to_string()];
4394 }
4395
4396 // c:glob() — `gd.gf_noglobdots = !isset(GLOBDOTS)` (plus the `(D)`
4397 // qualifier). parsecomplist reads this from CURGLOBDATA to compile
4398 // PAT_NOGLD into each component, which is what makes pattry reject
4399 // leading-dot files (pattern.rs:2890). Set it before parsing so the
4400 // faithful flag-driven dot filtering replaces the old manual skip.
4401 {
4402 let globdots = glob_isset(GLOBDOTS)
4403 || state
4404 .qualifiers
4405 .as_ref()
4406 .map(|qf| qf.globdots)
4407 .unwrap_or(false);
4408 let mut gd = CURGLOBDATA.lock().unwrap_or_else(|e| e.into_inner());
4409 gd.gf_noglobdots = if globdots { 0 } else { 1 };
4410 }
4411
4412 // c:parsepat (glob.c:809-820) — init pathbuf for absolute vs relative,
4413 // strip the leading `/`, then parse into a `complist` and run the
4414 // faithful `scanner`. parsecomplist consumes TOKENIZED input (it tests
4415 // for the `Star` token and hands segments to patcompile), so feed it
4416 // `pat_tok` (already tokenized above; tokenize is idempotent).
4417 // c:Src/glob.c:796 — `patcompstart();` is the FIRST statement of
4418 // parsepat, before the pathbuf setup and parsecomplist below. It
4419 // runs patcompcharsset (c:464), which fills `zpc_special` from
4420 // `zpc_chars` and marks the option-disabled entries. This inlined
4421 // copy of parsepat's body (c:809-820) omitted it, so `zpc_special`
4422 // stayed all-zero on this path and every gate that compares against
4423 // it was dead: `parsecomplist`'s `(dir/)#` branch (c:746-748) tests
4424 // `*instr == zpc_special[ZPC_INPAR]` and `*str ==
4425 // zpc_special[ZPC_HASH]`, both of which compared Inpar/Pound against
4426 // NUL and never fired. `(sub/)#end` therefore matched only the
4427 // zero-repetition case (`end`), not `sub/end`, `sub/sub/end`, …
4428 crate::ported::pattern::patcompstart(); // c:796
4429
4430 // c:797-807 — `Check for initial globbing flags, so that they don't form
4431 // a bogus path component.` This inlined copy of parsepat started at c:809
4432 // and skipped the flag strip entirely, which is load-bearing on an
4433 // ABSOLUTE pattern: the `/` split below then made the leading `(#…)` a
4434 // path component of its own and looked for a directory literally named
4435 // `(#a1)`. Measured against zsh 5.9.2:
4436 //
4437 // % zsh -f -c 'setopt extendedglob; print -r -- (#i)/ETC/hosts'
4438 // /etc/hosts
4439 // % zshrs (before)
4440 // zsh:1: no matches found: (#i)/ETC/hosts
4441 //
4442 // A RELATIVE pattern was unaffected — the flags stay glued to the first
4443 // component, where `patcompile`'s own `(#…)` hoist loop picks them up —
4444 // so this only ever showed on an absolute path, and every pattern-level
4445 // flag was affected, not just `(#a)`: `(#i)`, `(#l)`, `(#b)`, `(#m)`.
4446 //
4447 // `patgetglobflags` writes the `patglobflags` atomic, and `patcompile`'s
4448 // PAT_FILE branch (pattern.rs:615) deliberately leaves it alone, so the
4449 // flags reach EVERY path component exactly as c:568 has them.
4450 let mut globflags_bad = false;
4451 {
4452 let cv: Vec<char> = pat_tok.chars().collect();
4453 // c:801-803 tests `zpc_special[ZPC_INPAR]` / `[ZPC_HASH]` /
4454 // `[ZPC_KSH_AT]`. zshrs seeds those slots with RAW ASCII
4455 // (pattern.rs:443-457) while `pat_tok` here is TOKENIZED, so accept
4456 // either spelling of the pair — but only while the slot is live:
4457 // patcompcharsset (c:Src/pattern.c:480-483) sets ZPC_HASH to Marker
4458 // without EXTENDEDGLOB, ZPC_INPAR under SHGLOB and ZPC_KSH_AT without
4459 // KSHGLOB, and then `(#i)a` is an ordinary group holding a literal
4460 // `#`, not a flag block.
4461 let (inpar_live, hash_live, ksh_at_live) = {
4462 let sp = crate::ported::pattern::zpc_special
4463 .lock()
4464 .unwrap_or_else(|e| e.into_inner());
4465 let marker = crate::ported::zsh_h::Marker as u32 as u8;
4466 (
4467 sp[crate::ported::zsh_h::ZPC_INPAR as usize] != marker,
4468 sp[crate::ported::zsh_h::ZPC_HASH as usize] != marker,
4469 sp[crate::ported::zsh_h::ZPC_KSH_AT as usize] != marker,
4470 )
4471 };
4472 let is_inpar = |c: char| inpar_live && (c == '(' || c == crate::ported::zsh_h::Inpar);
4473 let is_hash = |c: char| hash_live && (c == '#' || c == crate::ported::zsh_h::Pound);
4474 let is_outpar = |c: char| c == ')' || c == crate::ported::zsh_h::Outpar;
4475 // c:804 — `str += (*str == Inpar) ? 2 : 3;`. The Rust
4476 // `patgetglobflags` consumes the leading `(#` itself (pattern.rs:1927
4477 // `s.starts_with("(#")`), so only the ksh `@` is skipped here.
4478 let skip = if cv.len() >= 3 && ksh_at_live && cv[0] == '@' && is_inpar(cv[1]) && is_hash(cv[2]) {
4479 Some(1) // c:802-803 ksh `@(#…)`
4480 } else if cv.len() >= 2 && is_inpar(cv[0]) && is_hash(cv[1]) {
4481 Some(0) // c:801 plain `(#…)`
4482 } else {
4483 None
4484 };
4485 if let Some(skip) = skip {
4486 // Feed `patgetglobflags` the flag block in the ASCII spelling it
4487 // parses, bounded at the closing paren so the rest of the path
4488 // is never rewritten. Its loop terminates on a literal `)`
4489 // (pattern.rs:1937) and its `consumed` is the BYTE offset just
4490 // past it (pattern.rs:2049).
4491 let end = cv[skip..].iter().position(|&c| is_outpar(c));
4492 let ascii: Option<String> = end.map(|e| {
4493 cv[skip..=skip + e]
4494 .iter()
4495 .map(|&c| match c {
4496 x if is_inpar(x) => '(',
4497 x if is_hash(x) => '#',
4498 x if is_outpar(x) => ')',
4499 other => other,
4500 })
4501 .collect()
4502 });
4503 match ascii
4504 .as_deref()
4505 .and_then(crate::ported::pattern::patgetglobflags)
4506 {
4507 Some((_bits, _assertp, consumed)) => {
4508 // c:804-806 — `str` now points past the flag block.
4509 let nchars = ascii.as_deref().unwrap_or("")[..consumed].chars().count();
4510 pat_tok = cv[skip + nchars..].iter().collect();
4511 }
4512 // c:805-806 — `if (!patgetglobflags(&str, …)) return NULL;`.
4513 // parsepat's NULL is the same failure `parsecomplist` reports
4514 // below, so route it to the one error path (c:1842-1854).
4515 None => globflags_bad = true,
4516 }
4517 }
4518 }
4519
4520 state.pathbufcwd = 0; // c:812 — `DPUTS(pathbufcwd, ...)` invariant
4521 let parse_src = if let Some(rest) = pat_tok.strip_prefix('/') {
4522 // c:813-816 — absolute path.
4523 state.pathbuf.push('/');
4524 state.pathpos = 1;
4525 rest.to_string()
4526 } else {
4527 // c:817-818 — relative to pwd.
4528 pat_tok.clone()
4529 };
4530 // c:424 — C's match name is the pathbuf text plus the entry name, so a
4531 // typed `./` / `.//` / `././` prefix survives verbatim. The scanner only
4532 // adds a `./` of its own when the pathbuf is EMPTY at depth 0, which a
4533 // pattern starting with `./` never has.
4534 state.dot_prefix_verbatim = parse_src.starts_with("./");
4535 let complist = if globflags_bad {
4536 None // c:806 — parsepat returned NULL before parsecomplist ran
4537 } else {
4538 parsecomplist(&parse_src)
4539 };
4540 if let Some(complist) = complist {
4541 scanner(state, Some(&complist), 0, false);
4542 } else {
4543 // c:Src/glob.c:1842-1854 — `q = parsepat(str); if (!q || errflag)`.
4544 // This is the `!q` half: the pattern failed to COMPILE (e.g. an
4545 // unclosed `[` character class — patcomppiece returns -1 at
4546 // pattern.rs:1687). C then either treats the word as a literal
4547 // string (when `unset(BADPATTERN)`) or aborts with
4548 // `zerr("bad pattern: %s", ostr)`. zshrs previously dropped the
4549 // `None` silently, so `echo abc[def` fell through to the
4550 // no-matches / literal-under-NO_NOMATCH path instead of the
4551 // compile diagnostic — and BADPATTERN was ignored entirely.
4552 // (The `errflag` half — a diagnostic already emitted by the
4553 // qualifier parser — is handled by the gate at glob.rs:3770.)
4554 if !isset(crate::ported::zsh_h::BADPATTERN) {
4555 // c:1846-1848 —
4556 // if (!nountok)
4557 // untokenize(ostr);
4558 // insertlinknode(list, node, ostr);
4559 // `ostr` is the TOKENIZED word, so C untokenizes it before
4560 // handing it back as an ordinary literal string. Returning
4561 // `pattern` verbatim kept the Inbrack/Outbrack/Star token
4562 // bytes in the result and they were dropped downstream:
4563 // `unsetopt badpattern; print [a` printed `a`, not `[a`
4564 // (E01options.ztst:5).
4565 return vec![crate::ported::lex::untokenize(pattern)];
4566 }
4567 // c:1851-1852 — clear any stale error bit so zerr fires, then
4568 // emit. utils::zerr re-sets ERRFLAG_ERROR, which zglob's gate at
4569 // glob.rs:1541 reads to suppress the redundant "no matches found".
4570 errflag.fetch_and(
4571 !crate::ported::zsh_h::ERRFLAG_ERROR,
4572 std::sync::atomic::Ordering::SeqCst,
4573 );
4574 // c:1852 `zerr("bad pattern: %s", ostr)` — `ostr` is still
4575 // TOKENIZED here (Inbrack/Outbrack/Star/… ), and C renders the
4576 // `%s` through `nicezputs` (c:Src/utils.c:316), whose
4577 // sb_niceformat maps a token byte back through `ztokens`
4578 // (c:Src/utils.c niceztrlen: `if (itok(c)) c = ztokens[c - Pound]`).
4579 // Formatting `pattern` with Rust's `{}` skipped that step, so
4580 // `echo [[` reported `bad pattern: \u{e191}\u{e191}` (printed as two
4581 // invisible C1 bytes) where zsh prints `bad pattern: [[`.
4582 zerr(&format!(
4583 "bad pattern: {}",
4584 crate::ported::utils::nicedup(pattern, 0)
4585 ));
4586 // No lastval write here: C's zglob leaves it alone. The CALLER owns the
4587 // status — c:Src/exec.c:3702-3705 `if (errflag) { lastval = 1; goto err; }`
4588 // after an argv globlist, but c:4218-4222 (a typeset assignment array)
4589 // only `break`s, so `typeset a=([y z])` keeps the previous status.
4590 return Vec::new();
4591 }
4592
4593 // c:Src/glob.c — apply top-level `~B` exclusions to the full matched
4594 // paths (split off above). A match is dropped if its emitted path
4595 // matches ANY exclusion pattern (`*` matches `/` here, matching zsh's
4596 // flat-string exclusion semantics).
4597 let verbatim = state.dot_prefix_verbatim;
4598 if !glob_exclusions.is_empty() {
4599 let eg = glob_isset(EXTENDEDGLOB);
4600 let cg = glob_isset(CASEGLOB);
4601 state.matches.retain(|m| {
4602 // c:424 — verbatim pathbuf text when the pattern starts with `./`.
4603 let p = if verbatim { m.path.to_string_lossy().into_owned() } else { glob_emit_path(&m.path) };
4604 !glob_exclusions.iter().any(|ex| matchpat(ex, &p, eg, cg))
4605 });
4606 }
4607
4608 // c:1680 — populate GS_EXEC sort keys before sorting. Evaluate each
4609 // `oe:…:` / `o+…` code per match with $REPLY = the match name, and
4610 // capture the resulting $REPLY as that match's sort string (gmatchcmp
4611 // reads gmatch.sort_strings[idx]). Exec sort changes ORDER only — the
4612 // original name is still what's emitted.
4613 let sort_codes: Vec<String> = state
4614 .qualifiers
4615 .as_ref()
4616 .map(|q| q.sort_exec.clone())
4617 .unwrap_or_default();
4618 if !sort_codes.is_empty() {
4619 for m in state.matches.iter_mut() {
4620 // c:424 — verbatim pathbuf text when the pattern starts with `./`.
4621 let name = if verbatim { m.path.to_string_lossy().into_owned() } else { glob_emit_path(&m.path) };
4622 for code in &sort_codes {
4623 crate::ported::params::setsparam("REPLY", &name);
4624 // c:1936 — `execode(prog, 1, 0, "globsort");`: the sort code
4625 // runs with `globsort` appended to $zsh_eval_context, as the
4626 // `(e…)` qualifier runs with `globqual` (c:3930).
4627 let globsort_ctx = crate::ported::exec::EvalContextFrame::push("globsort");
4628 let _ = crate::ported::exec::execute_script(code);
4629 drop(globsort_ctx);
4630 let key = crate::ported::params::getsparam("REPLY").unwrap_or_else(|| name.clone());
4631 m.sort_strings.push(key);
4632 }
4633 }
4634 }
4635
4636 // c:Src/glob.c:518 — the `Y` limit belongs to the SCAN, not to the result:
4637 // scanner(q->next, shortcircuit);
4638 // if (shortcircuit && shortcircuit == matchct)
4639 // return;
4640 // The scanner simply stops once N matches exist, so the survivors are the
4641 // first N *found*, and only then does c:1868's sort run over them. Applying
4642 // the limit after sorting (as apply_selection used to) keeps the first N by
4643 // SORT ORDER instead — a different set: `*(.Y2on)` must be the two files
4644 // the scan reached, re-ordered by name, not the two alphabetically-first.
4645 //
4646 // This port collects every match before sorting, so the faithful place for
4647 // the truncation is here — after collection, before sort_matches. The
4648 // `[first,last]` subscript stays in apply_selection: c:1868's sort precedes
4649 // it, so THAT one really is a post-sort selection.
4650 //
4651 // Zero means "no limit" (c:518's leading `shortcircuit &&`), hence `> 0`.
4652 if let Some(n) = state.qualifiers.as_ref().and_then(|q| q.short_circuit) {
4653 if n > 0 {
4654 let n = n as usize;
4655 if state.matches.len() > n {
4656 state.matches.truncate(n); // c:518
4657 }
4658 }
4659 }
4660
4661 // Sort results
4662 let unsorted = sort_matches(state);
4663
4664 // c:481 — `++matchct` per insert(): the count zglob's no-match
4665 // dispatch reads (c:1871-1886) is taken BEFORE the `[first,last]`
4666 // subscript (c:1981-1989) trims the list, so `*([100])` over a
4667 // non-empty directory expands to nothing rather than tripping
4668 // NOMATCH. Publish it for the caller (`expand_glob`), which only
4669 // sees the trimmed Vec.
4670 CURGLOBDATA
4671 .lock()
4672 .unwrap_or_else(|e| e.into_inner())
4673 .matchct = state.matches.len() as i32;
4674
4675 // Apply subscript selection
4676 apply_selection(state, unsorted);
4677
4678 // Extract filenames. Mark-dirs / list-types come from EITHER
4679 // the canonical global option store OR the parsed `(M)`/`(T)`
4680 // qualifier on this glob. Direct port of zsh/Src/glob.c:355,372
4681 // — output marker emission consults the per-glob `gf_markdirs`
4682 // / `gf_listtypes` flags which the qualifier parser at
4683 // glob.c:1557-1566 sets.
4684 let mark_dirs = state
4685 .qualifiers
4686 .as_ref()
4687 .and_then(|q| q.mark_dirs)
4688 .unwrap_or_else(|| glob_isset(MARKDIRS)); // c:1255 / c:1558
4689 // c:1562-1566 — `gf_listtypes` is set ONLY by the `T` glob
4690 // qualifier (`*(T)`), NOT by the global LISTTYPES option.
4691 // LISTTYPES is the completion-listing option (see man zshoptions
4692 // "LIST_TYPES") and doesn't affect glob output. Including the
4693 // option here mis-decorated every executable-file glob with a
4694 // trailing `*` and every directory with `/` for users with the
4695 // default `setopt listtypes` ON.
4696 let list_types = state
4697 .qualifiers
4698 .as_ref()
4699 .map(|q| q.list_types)
4700 .unwrap_or(false);
4701 let mark_follow = state.qualifiers.as_ref().is_some_and(|q| q.mark_follow);
4702 let colon_mods = state.qualifiers.as_ref().and_then(|q| q.colon_mods.clone());
4703 // c:Src/glob.c — a pattern ending in `/` ("trailing slash" syntax)
4704 // forces matches to be directories AND preserves the slash in the
4705 // output. zsh's parsepat treats `pat/` as `pat` + an empty trailing
4706 // component; the scanner restricts to directories and the emitter
4707 // appends `/`. The Rust port's parser short-circuits the empty
4708 // trailing component, so we filter + re-add the suffix here at emit
4709 // time. `/` is never a glob metachar so a literal trailing-slash
4710 // check on the raw pattern is sufficient.
4711 // Use the qualifier-stripped `pat` for the trailing-slash check.
4712 // For `*/(N)` the original `pattern` ends with `)`, so the check
4713 // on `pattern` returned false and the trailing-slash directory-
4714 // filter was skipped — `*/(N)` then matched every file, not just
4715 // directories. The qualifier-stripped `pat` is `*/` which still
4716 // carries the trailing `/`.
4717 let trailing_slash = pat.ends_with('/') && pat.len() > 1;
4718 // c:Src/glob.c — preserve user-typed `./` / `../` prefix in
4719 // output. glob_emit_path strips CurDir uniformly so the
4720 // leading `./` would be lost. Detect the source prefix here
4721 // and re-prepend after emit. Same `pat` (post-qualifier) — the
4722 // prefix is on the path part, not the qualifier.
4723 let leading_dot_prefix: &str = if pat.starts_with("../") {
4724 "../"
4725 } else if pat.starts_with("./") {
4726 "./"
4727 } else {
4728 ""
4729 };
4730 let mut results: Vec<String> = state
4731 .matches
4732 .iter()
4733 .filter(|m| !trailing_slash || fs::metadata(&m.path).map(|md| md.is_dir()).unwrap_or(false))
4734 .map(|m| {
4735 // c:424 — verbatim pathbuf text when the pattern starts with `./`.
4736 let mut s = if verbatim { m.path.to_string_lossy().into_owned() } else { glob_emit_path(&m.path) };
4737 if !verbatim
4738 && !leading_dot_prefix.is_empty()
4739 && !s.starts_with(leading_dot_prefix)
4740 && !s.starts_with('/')
4741 {
4742 s = format!("{}{}", leading_dot_prefix, s);
4743 }
4744 if trailing_slash && !s.ends_with('/') {
4745 s.push('/');
4746 }
4747 if mark_dirs || list_types {
4748 // c:358-371 — lstat; under gf_follow a symlink is re-stat'ed,
4749 // falling back to the lstat.
4750 if let Ok(lmeta) = fs::symlink_metadata(&m.path) {
4751 let meta = if mark_follow && lmeta.file_type().is_symlink() {
4752 fs::metadata(&m.path).unwrap_or(lmeta)
4753 } else {
4754 lmeta
4755 };
4756 let ch = file_type(meta.mode());
4757 if list_types || (mark_dirs && ch == '/') {
4758 // c:372-376 — `*t++ = file_type(mode);` appended
4759 // unconditionally: a `*/` match already ends in the
4760 // path's `/`, so zsh prints `d1//` under MARK_DIRS.
4761 s.push(ch);
4762 }
4763 }
4764 }
4765 // Apply colon modifiers AFTER mark/list-type appendage —
4766 // zsh applies them last in glob.c:432 modify() per emitted
4767 // node, so `(M:t)` would mark THEN tail (effectively just
4768 // tail since the slash is gone). Faithful order.
4769 if let Some(ref m) = colon_mods {
4770 // c:430-432 — `modify(&news, &mod, 1);` with no check of
4771 // where `mod` stopped: an unknown modifier ends the chain
4772 // silently (`*(:u:x:t)` is `*(:u)`); only paramsubst reports.
4773 s = crate::ported::subst::modify(&s, m).0; // c:432 — tail ignored
4774 }
4775 s
4776 })
4777 .collect();
4778
4779 // c:421-426 — zsh applies the colon modifier in insert() (during
4780 // collection) BEFORE the match sort, so the default (name) sort
4781 // orders the MODIFIED names: `*(.:e)` on (apple.z,banana.a,cherry.m)
4782 // sorts the extensions → (a,m,z), not name order (z,a,m). The Rust
4783 // pipeline modifies at emit (after sort_matches), so re-sort the
4784 // modified results here — but ONLY for the default name sort; an
4785 // explicit `o`/`O` keeps its stat-keyed order with modified output.
4786 let explicit_sort = state
4787 .qualifiers
4788 .as_ref()
4789 .map(|q| !q.sorts.is_empty())
4790 .unwrap_or(false);
4791 if colon_mods.is_some() && !explicit_sort {
4792 // c:945 — that sort compares the keys with `zstrcmp` (Src/sort.c:191),
4793 // i.e. `strcoll` under the current locale, and honours gf_numsort.
4794 // Rust's `Vec::sort` is BYTEWISE, so `*.txt(:r)` came back as
4795 // `Bravo Charlie alpha delta` while the very same directory globbed
4796 // as plain `*.txt` — which goes through `sort_matches` → `gmatchcmp`
4797 // → `zstrcmp` — gave zsh's `alpha Bravo Charlie delta`. The two
4798 // orderings inside one shell disagreed; only the modified one was
4799 // wrong.
4800 let numeric = state
4801 .qualifiers
4802 .as_ref()
4803 .and_then(|q| q.numsort)
4804 .unwrap_or_else(|| glob_isset(NUMERICGLOBSORT)); // c:1258/1575
4805 results.sort_by(|a, b| {
4806 crate::ported::sort::zstrcmp(
4807 a,
4808 b,
4809 if numeric {
4810 crate::zsh_h::SORTIT_NUMERICALLY as u32
4811 } else {
4812 0
4813 },
4814 )
4815 });
4816 }
4817
4818 // c:1744-1756 / insert_glob_match c:1430 — `P:word:` (gf_pre_words)
4819 // emits `word` as a separate element BEFORE each match; `^P:word:`
4820 // (gf_post_words) emits it AFTER. Applied last, after sort/modifiers.
4821 let (pre_words, post_words) = state
4822 .qualifiers
4823 .as_ref()
4824 .map(|q| (q.pre_words.clone(), q.post_words.clone()))
4825 .unwrap_or_default();
4826 if !pre_words.is_empty() || !post_words.is_empty() {
4827 let mut expanded =
4828 Vec::with_capacity(results.len() * (1 + pre_words.len() + post_words.len()));
4829 for s in results {
4830 expanded.extend(pre_words.iter().cloned());
4831 expanded.push(s);
4832 expanded.extend(post_words.iter().cloned());
4833 }
4834 results = expanded;
4835 }
4836
4837 // c:Src/glob.c:1872-1888 — no-match path. Return Vec::new() so
4838 // the caller (vm_helper.rs::expand_glob) drives the
4839 // NULLGLOB / NOMATCH / literal-fallback policy. Previously
4840 // this pushed `pattern.to_string()` on no-match, which made
4841 // `expand_glob` see a non-empty result and skip the NOMATCH
4842 // check entirely. zsh's `setopt nomatch` is on by default, so
4843 // `echo /never_real/*` should emit "no matches found" and
4844 // exit 1 — not silently print the literal. Parity bug #13
4845 // recurrence. Per-glob `(N)` qualifier (nullglob_per_qual)
4846 // and global NULLGLOB are now consulted in expand_glob too.
4847 let _ = state.qualifiers.as_ref().map(|q| q.nullglob);
4848 results
4849}
4850
4851// `sort_matches_by_type` deleted — dead code (no callers anywhere
4852// in the tree). The sort path lives in `GlobMatch::compare` which
4853// dispatches off the canonical `GS_*` tp bits exactly like C's
4854// `gmatchcmp()` at glob.c:936.
4855
4856// ============================================================================
4857// Pattern matching with replacement (from glob.c getmatch family)
4858// ============================================================================
4859
4860// `MatchFlags` deleted — Rust-only bool-bag wrapper. C uses bare
4861// `int fl` with `SUB_*` bits from `Src/zsh.h:1981+` (mirrored at
4862// `zsh_h.rs:2463+`). Callers now pass an `i32` and test bits.
4863
4864// Trimmed local `struct imatchdata` deleted — it was a fake 5-field
4865// duplicate (str/pattern/match_start/match_end/replacement) of the
4866// canonical `Src/zsh.h:1740` port that already lives in
4867// `zsh_h.rs::imatchdata` (mstr/mlen/ustr/ulen/flags/replstr/repllist).
4868// `get_match_ret` now uses the canonical struct; see its port above.
4869
4870/// Strip a trailing `(qual)` block from a glob pattern.
4871/// **RUST-ONLY** — C glob.c handles qualifier parsing inline in
4872/// `parsepat` (c:791) as it builds the Complist. Move to that
4873/// shape when porting parsepat for real.
4874pub fn parse_qualifiers(pattern: &str) -> (String, Option<qualifier_set>) {
4875 // RUST-ONLY
4876 // c:Src/glob.c:1797-1844 — merge the qualifier group parsed EARLIER
4877 // (`old`, the right-hand one) into the group just parsed to its left
4878 // (`newq`). Shared by the tokenized and the untokenized qualifier scans
4879 // in [`parse_qualifiers`], which both peel groups off the end of the word.
4880 let merge_qualifier_groups = |old: qualifier_set, newq: qualifier_set| -> qualifier_set {
4881 // c:1797-1842 — merge the previous group into the new
4882 // (left-hand) one. The AND-chains are distributed over
4883 // every pair of alternatives, new set first ("we are
4884 // searching for sets of qualifiers from the right");
4885 // with no `or` on either side that is plain chaining.
4886 let alts = |a: &QualArena| -> Vec<Vec<qual>> {
4887 let mut out = Vec::new();
4888 let mut h = a.head;
4889 while let Some(hi) = h {
4890 let mut chain = Vec::new();
4891 let mut n = Some(hi);
4892 while let Some(ni) = n {
4893 chain.push(a.nodes[ni].clone());
4894 n = a.nodes[ni].next;
4895 }
4896 out.push(chain);
4897 h = a.nodes[hi].or;
4898 }
4899 out
4900 };
4901 let mut q = newq;
4902 let (na, oa) = (alts(&q.quals), alts(&old.quals));
4903 if !oa.is_empty() {
4904 let mut arena = QualArena::new();
4905 let mut prev_head: Option<usize> = None;
4906 let news = if na.is_empty() { vec![Vec::new()] } else { na };
4907 for qn in &news {
4908 for qo in &oa {
4909 let base = arena.nodes.len();
4910 for (k, node) in qn.iter().chain(qo.iter()).enumerate() {
4911 let mut node = node.clone();
4912 node.or = None;
4913 node.next = Some(base + k + 1);
4914 arena.nodes.push(node);
4915 }
4916 let last = arena.nodes.len() - 1;
4917 arena.nodes[last].next = None;
4918 match prev_head {
4919 None => arena.head = Some(base),
4920 Some(p) => arena.nodes[p].or = Some(base),
4921 }
4922 prev_head = Some(base);
4923 }
4924 }
4925 q.quals = arena;
4926 }
4927 // The enum mirror of the same distribution.
4928 if !old.alternatives.is_empty() {
4929 let news = if q.alternatives.is_empty() {
4930 vec![Vec::new()]
4931 } else {
4932 std::mem::take(&mut q.alternatives)
4933 };
4934 q.alternatives = news
4935 .iter()
4936 .flat_map(|n| old.alternatives.iter().map(move |o| [n.clone(), o.clone()].concat()))
4937 .collect();
4938 }
4939 // gf_sortlist (c:1702) fills in parse order: the right
4940 // group's specs first. GS_EXEC entries carry an index
4941 // into `sort_exec`, shifted past the earlier ones.
4942 let shift = old.sort_exec.len() as i32;
4943 let new_sorts: Vec<i32> = q
4944 .sorts
4945 .iter()
4946 .map(|&t| if (t & GS_EXEC) != 0 { t + (shift << 16) } else { t })
4947 .collect();
4948 q.sorts = old.sorts.iter().copied().chain(new_sorts).collect();
4949 q.sort_exec = old.sort_exec.iter().cloned().chain(q.sort_exec).collect();
4950 // c:1336-1340 — `colonmod = dyncat(newcolonmod, colonmod)`.
4951 q.colon_mods = match (q.colon_mods.take(), old.colon_mods) {
4952 (Some(n), Some(o)) => Some(n + &o),
4953 (n, o) => n.or(o),
4954 };
4955 // The gf_* globals are simply overwritten by the group
4956 // parsed later (the left one) when it sets them.
4957 q.pre_words = old.pre_words.into_iter().chain(q.pre_words).collect();
4958 q.post_words = old.post_words.into_iter().chain(q.post_words).collect();
4959 q.first = q.first.or(old.first);
4960 q.last = q.last.or(old.last);
4961 q.mark_dirs = q.mark_dirs.or(old.mark_dirs);
4962 q.numsort = q.numsort.or(old.numsort);
4963 q.short_circuit = q.short_circuit.or(old.short_circuit);
4964 q.list_types |= old.list_types;
4965 q.mark_follow |= old.mark_follow;
4966 q.nullglob |= old.nullglob;
4967 q.globdots |= old.globdots;
4968 q
4969 };
4970 // c:Src/glob.c:1158-1202 `checkglobqual` — C decides where the
4971 // qualifier block starts by testing the LEXER TOKENS `Outpar` /
4972 // `Inpar` (c:1163 `if (str[sl - 1] != Outpar) return 0;`, c:1170
4973 // `*s != Inpar`, c:1189), never the raw ASCII bytes. That is the
4974 // whole mechanism by which a QUOTED metachar stays literal: in
4975 // `*(.e['[[ $REPLY == a* ]]'])` the body's `]` bytes are raw ASCII
4976 // while the qualifier's real closer is `Outbrack`, so the scan
4977 // walks straight past the quoted ones.
4978 //
4979 // zshrs's glob layer is reachable BOTH ways: `zglob` (c:1221) and
4980 // the fusevm word path hand it the tokenized word, while
4981 // programmatic callers (compsys `glob_path`, `builtin.rs`,
4982 // `subst.rs`) hand it an untokenized string. Pick the scanner off
4983 // the terminator so both keep working — token mode is the faithful
4984 // c:1163 test, raw mode is the untokenized-caller fallback that
4985 // reconstructs "was this escaped" from backslashes instead.
4986 if pattern.ends_with(crate::ported::zsh_h::Outpar) {
4987 // c:1260-1261 — `while (!nobareglob || (isset(EXTENDEDGLOB) &&
4988 // !zpc_disables[ZPC_HASH]))`: qualifier groups are peeled off the
4989 // END of the word one at a time. After the first, `nobareglob = 1`
4990 // (c:1302), so only further explicit `(#q…)` groups are taken —
4991 // `*(#q/)(#q^F)` applies BOTH (empty directories); the port used
4992 // to keep the last group and glob the rest as pattern text.
4993 let mut word: &str = pattern;
4994 let mut acc: Option<qualifier_set> = None;
4995 // c:1226 `nobareglob = !isset(BAREGLOBQUAL);`
4996 let mut nobareglob = i32::from(!glob_isset(BAREGLOBQUAL));
4997 let hash_live = || {
4998 glob_isset(EXTENDEDGLOB)
4999 && crate::ported::pattern::zpc_disables
5000 .lock()
5001 .unwrap_or_else(|e| e.into_inner())[crate::ported::zsh_h::ZPC_HASH as usize]
5002 == 0
5003 };
5004 while nobareglob == 0 || hash_live() {
5005 if !word.ends_with(crate::ported::zsh_h::Outpar) {
5006 break; // c:1163 — no further qualifier block
5007 }
5008 // c:1296-1310 — checkglobqual, then cut the block out at the
5009 // returned `Inpar` (`*s++ = 0`), stepping over the `#q` when
5010 // qualsfound == 2 (c:1310 `s += 2`).
5011 let cv: Vec<char> = word.chars().collect();
5012 let mut sp: Option<usize> = None;
5013 let qualsfound = checkglobqual(&cv, cv.len() as i32, nobareglob, &mut sp);
5014 let start = match sp {
5015 Some(v) if qualsfound != 0 => v,
5016 _ => break, // c:1297-1298
5017 };
5018 nobareglob = 1; // c:1302
5019 let body_start = if qualsfound == 2 { start + 3 } else { start + 1 };
5020 let qual_content: String = cv[body_start..cv.len() - 1].iter().collect();
5021 let byte_start: usize = cv[..start].iter().map(|c| c.len_utf8()).sum();
5022 let newq = parse_qualifier_string(&qual_content);
5023 word = &word[..byte_start];
5024 let failed = crate::ported::utils::errflag.load(std::sync::atomic::Ordering::Relaxed) != 0;
5025 acc = Some(match acc {
5026 None => newq, // c:1843-1844 `else if (newquals) quals = newquals;`
5027 Some(old) => merge_qualifier_groups(old, newq),
5028 });
5029 if failed {
5030 break; // c:1786-1788 — errflag: zglob returns
5031 }
5032 }
5033 return match acc {
5034 Some(qs) => (word.to_string(), Some(qs)),
5035 None => (pattern.to_string(), None),
5036 };
5037 }
5038
5039 // Untokenized fallback, for the programmatic `glob_path` callers
5040 // that never went through the lexer (compsys, `builtin.rs`,
5041 // `subst.rs`). No `Bnull` tokens are present to carry escaping, so
5042 // it is recovered from literal backslashes instead.
5043 //
5044 // Same c:1260-1302 loop as the tokenized scan above: groups are peeled
5045 // off the END one at a time, and after the first only an explicit
5046 // `(#q…)` is taken. Taking just the last group globbed `*(#qom)(-/)`
5047 // (what `_path_files -/` builds for `file-sort`, sh:178) with the
5048 // `(#qom)` left in the pattern text, where it sorts nothing.
5049 // One step of the untokenized qualifier scan in [`parse_qualifiers`]: strip
5050 // the LAST `(…)` group of `pattern` when it is a qualifier block. With
5051 // `nobareglob` set (every group after the first, c:1302) only an explicit
5052 // `(#q…)` qualifies.
5053 let parse_qualifiers_raw = |pattern: &str, nobareglob_in: bool| -> (String, Option<qualifier_set>) {
5054 if !pattern.ends_with(')') {
5055 return (pattern.to_string(), None);
5056 }
5057
5058 let bytes = pattern.as_bytes();
5059 // Backslash-escaped trailing `)` is literal — no qualifier block
5060 // present. C's glob path handles this via Bnull preservation in
5061 // the lexer; zshrs's path arrives here with `\)` for quoted
5062 // close-parens (e.g. assoc-value `'\)'`). Without the gate, the
5063 // qualifier-extraction walk below treats the literal `)` as the
5064 // qualifier terminator.
5065 let last_paren_escaped = {
5066 let mut bs = 0usize;
5067 let mut j = bytes.len() - 1;
5068 while j > 0 && bytes[j - 1] == b'\\' {
5069 bs += 1;
5070 j -= 1;
5071 }
5072 bs % 2 == 1
5073 };
5074 if last_paren_escaped {
5075 return (pattern.to_string(), None);
5076 }
5077
5078 // Find matching open paren
5079 let mut depth = 0;
5080 let mut qual_start = None;
5081
5082 // c:Src/glob.c haswilds backslash handling — a `\(` / `\)` is a
5083 // literal paren, not a qualifier delimiter. Track which paren
5084 // bytes are escaped by a preceding `\` (counting consecutive
5085 // backslashes — even count means the paren is unescaped, odd
5086 // count means escaped). Without this gate, `\(*\)` is mis-parsed
5087 // as `(*\)` qualifier + empty pattern prefix, sending `*\` to
5088 // the qualifier-letter switch which emits "unknown file
5089 // attribute: \".
5090 let is_escaped = |idx: usize| -> bool {
5091 let mut bs = 0usize;
5092 let mut j = idx;
5093 while j > 0 && bytes[j - 1] == b'\\' {
5094 bs += 1;
5095 j -= 1;
5096 }
5097 bs % 2 == 1
5098 };
5099
5100 for i in (0..bytes.len()).rev() {
5101 match bytes[i] {
5102 b')' => {
5103 if !is_escaped(i) {
5104 depth += 1;
5105 }
5106 }
5107 b'(' => {
5108 if !is_escaped(i) {
5109 depth -= 1;
5110 if depth == 0 {
5111 qual_start = Some(i);
5112 break;
5113 }
5114 }
5115 }
5116 _ => {}
5117 }
5118 }
5119
5120 let start = match qual_start {
5121 Some(s) => s,
5122 None => return (pattern.to_string(), None),
5123 };
5124
5125 // Check for (#q...) explicit qualifier syntax.
5126 let qual_str = &pattern[start + 1..pattern.len() - 1];
5127 // c:Src/glob.c:1192-1197 — `if (isset(EXTENDEDGLOB) &&
5128 // !zpc_disables[ZPC_HASH] && s[1] == Pound) { if (s[2] != 'q')
5129 // return 0; ret = 2; }`. The leading `#` inside `(...)` is ONLY
5130 // special under EXTENDEDGLOB: `(#q...)` is the explicit glob
5131 // qualifier, and `(#X...)` for any other X is an inline pattern flag
5132 // (passed through). Without EXTENDEDGLOB the `#` is not special at
5133 // all — the `(...)` is a bare qualifier group and a leading `#` is an
5134 // (unknown) attribute char, so `*(#q.)` errors `unknown file
5135 // attribute: #` rather than silently applying the `.` qualifier.
5136 let (is_explicit, qual_content) = if glob_isset(EXTENDEDGLOB) && qual_str.starts_with('#') {
5137 if let Some(after) = qual_str.strip_prefix("#q") {
5138 (true, after) // c:1195 ret = 2 — explicit glob qualifier
5139 } else {
5140 // c:1194 `if (s[2] != 'q') return 0;` — inline pattern flag
5141 // (`(#i)`, `(#c1,2)`, `(#a)`, `(#l)`, `(#s)`, `(#e)`, `(#m)`).
5142 return (pattern.to_string(), None);
5143 }
5144 } else if glob_isset(BAREGLOBQUAL) && !nobareglob_in {
5145 (false, qual_str)
5146 } else {
5147 return (pattern.to_string(), None);
5148 };
5149
5150 // c:1169-1186 — alternatives or exclusions, not qualifiers: an inner
5151 // `)` (a nested group) falls through to the `Bar` arm, and both set
5152 // `nobareglob` unless `disable -p '|'`; `~` only does under
5153 // EXTENDEDGLOB (unless disabled). Only an explicit `(#q…)` survives
5154 // that (c:1192-1197). A `$~v` value lands here untokenized, but C
5155 // shtokenized it (c:Src/subst.c:822/830), so its parens are the
5156 // Inpar/Outpar tokens this scan stands for: zsh answers
5157 // `v='*(e:reply=($REPLY):)'; print $~v` with "no matches found".
5158 let nobareglob = {
5159 let disp = crate::ported::pattern::zpc_disables
5160 .lock()
5161 .unwrap_or_else(|e| e.into_inner());
5162 (start + 1..bytes.len() - 1).any(|k| {
5163 !is_escaped(k)
5164 && match bytes[k] {
5165 b')' | b'|' => disp[crate::ported::zsh_h::ZPC_BAR as usize] == 0,
5166 b'~' => {
5167 glob_isset(EXTENDEDGLOB)
5168 && disp[crate::ported::zsh_h::ZPC_TILDE as usize] == 0
5169 }
5170 _ => false,
5171 }
5172 })
5173 };
5174 if !is_explicit && nobareglob {
5175 return (pattern.to_string(), None);
5176 }
5177
5178 // Parse the qualifiers
5179 let qs = parse_qualifier_string(qual_content);
5180 (pattern[..start].to_string(), Some(qs))
5181 };
5182 let hash_live = || {
5183 glob_isset(EXTENDEDGLOB)
5184 && crate::ported::pattern::zpc_disables
5185 .lock()
5186 .unwrap_or_else(|e| e.into_inner())[crate::ported::zsh_h::ZPC_HASH as usize]
5187 == 0
5188 };
5189 let mut word = pattern.to_string();
5190 let mut acc: Option<qualifier_set> = None;
5191 let mut nobareglob = false; // c:1226, gated per group inside the raw scan
5192 while !nobareglob || hash_live() {
5193 let (rest, newq) = match parse_qualifiers_raw(&word, nobareglob) {
5194 (rest, Some(q)) => (rest, q),
5195 _ => break, // c:1297-1298
5196 };
5197 nobareglob = true; // c:1302
5198 word = rest;
5199 acc = Some(match acc {
5200 None => newq, // c:1843-1844
5201 Some(old) => merge_qualifier_groups(old, newq),
5202 });
5203 if crate::ported::utils::errflag.load(std::sync::atomic::Ordering::Relaxed) != 0 {
5204 break; // c:1786-1788
5205 }
5206 }
5207 match acc {
5208 Some(qs) => (word, Some(qs)),
5209 None => (pattern.to_string(), None),
5210 }
5211}
5212
5213/// Parse the body of a `(...)` qualifier block into a qualifier_set.
5214/// **RUST-ONLY** — see header on `parse_qualifiers`.
5215fn parse_qualifier_string(s: &str) -> qualifier_set {
5216 // RUST-ONLY
5217 let mut qs = qualifier_set::default();
5218 // c:Src/glob.c:1312-1314 — `for (ptr = s; *ptr; ptr++) if (*ptr ==
5219 // Dash) *ptr = '-';`. The qualifier body arrives in LEXER-TOKENIZED
5220 // form, and C normalises the one token it rewrites wholesale before
5221 // the switch runs. Every OTHER token that can appear here (`Star`,
5222 // `Hat`, `Equals`, `Inbrack`) keeps its token spelling and is
5223 // matched by a dedicated `case` in the switch below — see c:1343,
5224 // c:1358, c:1385, c:1723. Untokenized callers reach here with no
5225 // tokens at all, so this pass is a no-op for them.
5226 let s: String = s
5227 .chars()
5228 .map(|c| {
5229 if c == crate::ported::zsh_h::Dash {
5230 '-'
5231 } else {
5232 c
5233 }
5234 })
5235 .collect();
5236 let mut chars = s.chars().peekable();
5237 let mut negated = false;
5238 let mut follow = false;
5239
5240 while let Some(c) = chars.next() {
5241 match c {
5242 // c:1343-1344 `case Hat: case '^':`
5243 '^' | crate::ported::zsh_h::Hat => {
5244 // c:1346 — `sense ^= 1`. Keep the running `negated` for
5245 // the flag qualifiers (N/D/M/T) that read it, AND emit a
5246 // marker so the per-qualifier sense reaches the list walk.
5247 negated = !negated;
5248 qs.qualifiers.push(qualifier::ToggleSense);
5249 }
5250 // c:1348-1352 — `case '-': case Dash: sense ^= 2;`
5251 '-' => {
5252 follow = !follow;
5253 qs.qualifiers.push(qualifier::ToggleFollow);
5254 }
5255 ',' => {
5256 // Start new alternative
5257 if !qs.qualifiers.is_empty() {
5258 qs.alternatives.push(std::mem::take(&mut qs.qualifiers));
5259 }
5260 negated = false;
5261 follow = false;
5262 }
5263 ':' => {
5264 // Colon modifiers - rest of string
5265 let rest: String = chars.collect();
5266 qs.colon_mods = Some(format!(":{}", rest));
5267 break;
5268 }
5269 // File type qualifiers
5270 '/' => qs.qualifiers.push(qualifier::IsDirectory),
5271 '.' => qs.qualifiers.push(qualifier::IsRegular),
5272 '@' => qs.qualifiers.push(qualifier::IsSymlink),
5273 // c:1358-1359 `case Equals: case '=':`
5274 '=' | crate::ported::zsh_h::Equals => qs.qualifiers.push(qualifier::IsSocket),
5275 'p' => qs.qualifiers.push(qualifier::IsFifo),
5276 '%' => match chars.peek() {
5277 Some('b') => {
5278 chars.next();
5279 qs.qualifiers.push(qualifier::IsBlockDev);
5280 }
5281 Some('c') => {
5282 chars.next();
5283 qs.qualifiers.push(qualifier::IsCharDev);
5284 }
5285 _ => qs.qualifiers.push(qualifier::IsDevice),
5286 },
5287 // c:1385 `case Star:` — C matches ONLY the token, since an
5288 // unquoted `*` in a qualifier list always reaches the glob
5289 // layer tokenized. The raw spelling is kept alongside it for
5290 // the untokenized programmatic `glob_path` callers.
5291 '*' | crate::ported::zsh_h::Star => qs.qualifiers.push(qualifier::IsExecutable),
5292 // Permission qualifiers
5293 'r' => qs.qualifiers.push(qualifier::Readable),
5294 'w' => qs.qualifiers.push(qualifier::Writable),
5295 'x' => qs.qualifiers.push(qualifier::Executable),
5296 'R' => qs.qualifiers.push(qualifier::WorldReadable),
5297 'W' => qs.qualifiers.push(qualifier::WorldWritable),
5298 'X' => qs.qualifiers.push(qualifier::WorldExecutable),
5299 'A' => qs.qualifiers.push(qualifier::GroupReadable),
5300 'I' => qs.qualifiers.push(qualifier::GroupWritable),
5301 'E' => qs.qualifiers.push(qualifier::GroupExecutable),
5302 's' => qs.qualifiers.push(qualifier::Setuid),
5303 'S' => qs.qualifiers.push(qualifier::Setgid),
5304 't' => qs.qualifiers.push(qualifier::Sticky),
5305 // c:Src/glob.c case 'f' — file-mode qualifier accepts
5306 // an octal mode (`f755`) or chmod-style symbolic spec
5307 // (`fu+x`/`fg-w`/`fa=r`) and matches files whose mode
5308 // bits agree. Multiple delimited specs can be chained
5309 // by separator character. Direct port of qgetmodespec
5310 // dispatch + mode-bit accumulator in qgetmodespec.
5311 // c:1548-1552 — `case 'f': func = qualmodeflags; data =
5312 // qgetmodespec(&s);`. qgetmodespec owns the whole spec: the
5313 // bare-vs-delimited split, who letters, `,`-chained clauses, and
5314 // leaves the cursor where C's `*s = p` does.
5315 'f' => {
5316 let rest: String = chars.clone().collect();
5317 match qgetmodespec(&rest) {
5318 Some((data, r)) => {
5319 for _ in 0..rest[..rest.len() - r.len()].chars().count() {
5320 chars.next();
5321 }
5322 // c:3819 — `y = mod & 07777, n = mod >> 12`.
5323 qs.qualifiers.push(qualifier::Mode {
5324 yes: (data & 0o7777) as u32,
5325 no: (data >> 12) as u32,
5326 });
5327 }
5328 None => {
5329 // qgetmodespec already said "invalid mode
5330 // specification"; errflag aborts the glob.
5331 crate::ported::utils::errflag.fetch_or(
5332 crate::ported::utils::ERRFLAG_ERROR,
5333 std::sync::atomic::Ordering::Relaxed,
5334 );
5335 return qs;
5336 }
5337 }
5338 }
5339 // Ownership
5340 'U' => qs.qualifiers.push(qualifier::OwnedByEuid),
5341 'G' => qs.qualifiers.push(qualifier::OwnedByEgid),
5342 'u' => {
5343 let uid = parse_uid_gid(&mut chars, false);
5344 qs.qualifiers.push(qualifier::OwnedByUid(uid));
5345 }
5346 'g' => {
5347 let gid = parse_uid_gid(&mut chars, true);
5348 qs.qualifiers.push(qualifier::OwnedByGid(gid));
5349 }
5350 // Size
5351 'L' => {
5352 let (unit, op, val) = parse_size_spec(&mut chars);
5353 qs.qualifiers.push(qualifier::Size {
5354 value: val,
5355 unit,
5356 op,
5357 });
5358 }
5359 // Link count
5360 'l' => {
5361 let (op, val) = parse_range_spec(&mut chars);
5362 qs.qualifiers.push(qualifier::Links { value: val, op });
5363 }
5364 // c:Src/glob.c:1445-1449 — `case 'd': func = qualdev;
5365 // data = qgetnum(&s);`. Matches files by device number
5366 // (`qualdev`, c:3688, is `buf->st_dev == dv`). Both the
5367 // `qualifier::Device` variant and the `qualdev` matcher were
5368 // already ported and wired at glob.rs:4894 — only this parser arm
5369 // was missing, so `*(d5)` never reached them and a bare `*(d)`
5370 // reported "unknown file attribute: d" where zsh says
5371 // "number expected".
5372 //
5373 // c:826-834 qgetnum takes PLAIN digits — deliberately NOT
5374 // `parse_range_spec`, which would also accept the `+`/`-` range
5375 // operators that `l`/`L`/`m` use and that C does not allow here.
5376 'd' => {
5377 let mut num = String::new();
5378 while let Some(&pc) = chars.peek() {
5379 if pc.is_ascii_digit() {
5380 num.push(pc);
5381 chars.next();
5382 } else {
5383 break;
5384 }
5385 }
5386 if num.is_empty() {
5387 // c:830-833 — `if (!idigit(**s)) { zerr("number expected");
5388 // return 0; }`. Matches the sibling error arms in this
5389 // parser (`Y`, `f`): report, set errflag, stop.
5390 crate::ported::utils::zerr("number expected"); // c:832
5391 crate::ported::utils::errflag.fetch_or(
5392 crate::ported::utils::ERRFLAG_ERROR,
5393 std::sync::atomic::Ordering::Relaxed,
5394 );
5395 return qs;
5396 }
5397 qs.qualifiers
5398 .push(qualifier::Device(num.parse().unwrap_or(0))); // c:1448
5399 }
5400 // Times
5401 'a' => {
5402 let (unit, op, val) = parse_time_spec(&mut chars);
5403 qs.qualifiers.push(qualifier::Atime {
5404 value: val as i64,
5405 unit,
5406 op,
5407 });
5408 }
5409 'm' => {
5410 let (unit, op, val) = parse_time_spec(&mut chars);
5411 qs.qualifiers.push(qualifier::Mtime {
5412 value: val as i64,
5413 unit,
5414 op,
5415 });
5416 }
5417 'c' => {
5418 let (unit, op, val) = parse_time_spec(&mut chars);
5419 qs.qualifiers.push(qualifier::Ctime {
5420 value: val as i64,
5421 unit,
5422 op,
5423 });
5424 }
5425 // Sort qualifier — `o<key>` ascending / `O<key>`
5426 // descending. Mirrors the parser arm in zsh's glob.c
5427 // that builds `struct globsort` entries (tp = GS_* OR
5428 // GS_DESC, optionally shifted by GS_SHIFT for the
5429 // follow-links variant).
5430 'o' | 'O' => {
5431 let desc = c == 'O';
5432 if let Some(&sc) = chars.peek() {
5433 let key: i32 = match sc {
5434 'n' => {
5435 chars.next();
5436 GS_NAME
5437 }
5438 'L' => {
5439 chars.next();
5440 GS_SIZE
5441 }
5442 'l' => {
5443 chars.next();
5444 GS_LINKS
5445 }
5446 'a' => {
5447 chars.next();
5448 GS_ATIME
5449 }
5450 'm' => {
5451 chars.next();
5452 GS_MTIME
5453 }
5454 'c' => {
5455 chars.next();
5456 GS_CTIME
5457 }
5458 'd' => {
5459 chars.next();
5460 GS_DEPTH
5461 }
5462 'N' => {
5463 chars.next();
5464 GS_NONE
5465 }
5466 'e' | '+' => {
5467 // c:1672-1687 — GS_EXEC sort: `glob_exec_string`
5468 // parses the eval code (delimited `e:code:` or
5469 // identifier `+name`); store it indexed and pack
5470 // the index into the sort entry. The code is
5471 // consumed HERE (not left for the main loop), so
5472 // `oe:…:` is a SORT (eval = key, original name
5473 // emitted), not a separate eval qualifier.
5474 let is_plus = sc == '+';
5475 chars.next(); // consume 'e' / '+'
5476 let rest: String = chars.clone().collect();
5477 match glob_exec_string(&rest, is_plus) {
5478 Some((code, consumed)) => {
5479 for _ in 0..rest[..consumed].chars().count() {
5480 chars.next();
5481 }
5482 let idx = qs.sort_exec.len();
5483 qs.sort_exec.push(code);
5484 GS_EXEC | ((idx as i32) << 16)
5485 }
5486 None => {
5487 // glob_exec_string already emitted the
5488 // diagnostic + set errflag; bail.
5489 return qs;
5490 }
5491 }
5492 }
5493 _ => {
5494 // c:1689 — `default: zerr("unknown sort
5495 // specifier"); restore_globstate(saved);
5496 // return;`. Was `_ => GS_NAME`, which silently
5497 // accepted invalid keys (and reparsed them as
5498 // separate qualifiers, e.g. `oD` → o + D-dotglob
5499 // instead of erroring like zsh).
5500 crate::ported::utils::zerr("unknown sort specifier");
5501 crate::ported::utils::errflag.fetch_or(
5502 crate::ported::utils::ERRFLAG_ERROR,
5503 std::sync::atomic::Ordering::Relaxed,
5504 );
5505 return qs;
5506 }
5507 };
5508 let shifted = if follow && (key & GS_NORMAL) != 0 {
5509 key << GS_SHIFT // c:1692-1694
5510 } else {
5511 key
5512 };
5513 // c:1695-1702 — a sort key may not be repeated:
5514 // if (t != GS_EXEC) {
5515 // if (gf_sorts & t) {
5516 // zerr("doubled sort specifier");
5517 // restore_globstate(saved);
5518 // return;
5519 // }
5520 // }
5521 // gf_sorts |= t;
5522 // GS_EXEC is exempt — `oe:…:` may appear repeatedly, each
5523 // with its own code. This check was missing entirely, so
5524 // `*(.onon)` and `*(.onOn)` silently sorted where zsh
5525 // fails. Note the test is on the SHIFTED key, so `oL` and
5526 // `OL` collide (same key, different direction) while `oL`
5527 // and `oLm` do not (the follow variant shifts).
5528 if (shifted & GS_EXEC) == 0
5529 && qs.sorts.iter().any(|s| (s & !GS_DESC) == shifted)
5530 {
5531 crate::ported::utils::zerr("doubled sort specifier"); // c:1697
5532 crate::ported::utils::errflag.fetch_or(
5533 crate::ported::utils::ERRFLAG_ERROR,
5534 std::sync::atomic::Ordering::Relaxed,
5535 );
5536 return qs; // c:1699
5537 }
5538 // c:1658-1662 — `if (gf_nsorts == MAX_SORTS) { zerr("too
5539 // many glob sort specifiers"); ... return; }`. MAX_SORTS is
5540 // 12 (c:164). Unreachable in practice now that the doubled
5541 // check above rejects repeats, but it is C's bound and the
5542 // list is otherwise unbounded.
5543 if qs.sorts.len() == MAX_SORTS {
5544 crate::ported::utils::zerr("too many glob sort specifiers"); // c:1659
5545 crate::ported::utils::errflag.fetch_or(
5546 crate::ported::utils::ERRFLAG_ERROR,
5547 std::sync::atomic::Ordering::Relaxed,
5548 );
5549 return qs; // c:1661
5550 }
5551 let tp = shifted | (if desc { GS_DESC } else { 0 });
5552 qs.sorts.push(tp);
5553 } else {
5554 // c:1666-1690 — C does not guard on "is there a next
5555 // character": it reads `switch (*s)` unconditionally, so an
5556 // `o`/`O` at the very end of the qualifier list sees the
5557 // terminating `)` and lands on `default: zerr("unknown sort
5558 // specifier")`. This port skipped the whole block when the
5559 // iterator was exhausted, so a trailing `o` was silently
5560 // ignored and `*(No)` listed every match where zsh fails.
5561 // A NON-trailing bad spec (`*(ozN)`) already errored — it
5562 // reaches the `_` arm above — which is why only the
5563 // end-of-list form survived.
5564 crate::ported::utils::zerr("unknown sort specifier"); // c:1688
5565 crate::ported::utils::errflag.fetch_or(
5566 crate::ported::utils::ERRFLAG_ERROR,
5567 std::sync::atomic::Ordering::Relaxed,
5568 );
5569 return qs; // c:1690
5570 }
5571 }
5572 // Flags
5573 // c:1567-1569 — `case 'N': gf_nullglob = !(sense & 1);`.
5574 // Per-glob nullglob: empty result on no-match, no error.
5575 'N' => qs.nullglob = !negated,
5576 'D' => qs.globdots = !negated,
5577 // c:1575-1577 — `case 'n': gf_numsort = !(sense & 1)`.
5578 // Standalone `(n)` enables numeric sort for this glob (the
5579 // `^n` toggle disables it). Consumed in sort_matches.
5580 'n' => qs.numsort = Some(!negated),
5581 // (M) / (T) — set per-qualifier-set flags. glob.c:1557-1566:
5582 // case 'M': gf_markdirs = !(sense & 1); break;
5583 // case 'T': gf_listtypes = !(sense & 1); break;
5584 // `sense & 1` = the `^`-toggle bit. zshrs's parser tracks
5585 // `negated`; mirror by `!negated`. Read at output-emit
5586 // time to mark dirs / list types like coreutils ls -F.
5587 // c:1557-1566 — `if ((gf_markdirs = !(sense & 1))) gf_follow = sense & 2;`
5588 'M' => {
5589 qs.mark_dirs = Some(!negated);
5590 if !negated {
5591 qs.mark_follow = follow;
5592 }
5593 }
5594 'T' => {
5595 qs.list_types = !negated;
5596 if qs.list_types {
5597 qs.mark_follow = follow;
5598 }
5599 }
5600 'F' => qs.qualifiers.push(qualifier::NonEmptyDir),
5601 // c:1744-1756 — `P:word:` adds `word` to gf_pre_words (or
5602 // gf_post_words under `^`), emitted as a separate element
5603 // before/after each match. `glob_exec_string` parses the
5604 // delimited word.
5605 'P' => {
5606 let rest: String = chars.clone().collect();
5607 if let Some((word, consumed)) = glob_exec_string(&rest, false) {
5608 for _ in 0..rest[..consumed].chars().count() {
5609 chars.next();
5610 }
5611 if negated {
5612 qs.post_words.push(word); // c:1750 gf_post_words
5613 } else {
5614 qs.pre_words.push(word); // c:1750 gf_pre_words
5615 }
5616 }
5617 }
5618 // Subscript — c:1723-1741 `case '[': case Inbrack:`
5619 //
5620 // char *os = --s;
5621 // struct value v;
5622 // v.scanflags = SCANPM_WANTVALS;
5623 // v.pm = NULL; v.start = 0; v.end = -1;
5624 // v.valflags = 0; v.arr = NULL;
5625 // if (getindex(&s, &v, 0) || s == os) {
5626 // zerr("invalid subscript");
5627 // restore_globstate(saved);
5628 // return;
5629 // }
5630 // first = v.start;
5631 // end = v.end;
5632 //
5633 // C hands the bracket straight to the ONE subscript parser
5634 // the shell has (`getindex`, Src/params.c:2001), so a glob
5635 // qualifier subscript gets the same unterminated-bracket
5636 // diagnostic and the same arithmetic evaluation of its
5637 // operands as `${a[...]}`. This port called a private
5638 // RUST-ONLY `parse_subscript` that split on `,` and
5639 // `str::parse`d each half, dropping the qualifier whenever
5640 // either half was not a decimal literal — so `*(N[)`,
5641 // `*(N[a])` and `*(N[1,])` all listed every match at rc=0
5642 // where zsh reports `invalid subscript`, an empty result
5643 // and `bad math expression: empty string` respectively.
5644 '[' | crate::ported::zsh_h::Inbrack => {
5645 // c:1725 `char *os = --s;` — rewind onto the bracket;
5646 // getindex consumes it itself (c:2006 `*s++ = '['`).
5647 let os: String = std::iter::once(c).chain(chars.clone()).collect();
5648 // c:1727-1732 — the Value getindex fills in.
5649 let mut v = crate::ported::zsh_h::value {
5650 pm: None, // c:1728
5651 arr: Vec::new(), // c:1732
5652 scanflags: crate::ported::zsh_h::SCANPM_WANTVALS as i32, // c:1727
5653 valflags: 0, // c:1731
5654 start: 0, // c:1729
5655 end: -1, // c:1730
5656 };
5657 let mut s: &str = &os;
5658 let rc = crate::ported::params::getindex(&mut s, &mut v, 0); // c:1735
5659 if rc != 0 || s.len() == os.len() {
5660 // c:1735 `|| s == os`
5661 crate::ported::utils::zerr("invalid subscript"); // c:1736
5662 crate::ported::utils::errflag.fetch_or(
5663 crate::ported::utils::ERRFLAG_ERROR,
5664 std::sync::atomic::Ordering::Relaxed,
5665 );
5666 return qs; // c:1738
5667 }
5668 // Re-sync the qualifier walker onto the text getindex
5669 // stopped at. `os` carries the already-consumed `[`, so
5670 // step the iterator over the remainder only.
5671 let consumed = os.len() - s.len();
5672 for _ in 1..os[..consumed].chars().count() {
5673 chars.next();
5674 }
5675 // c:1785-1788 — `if (errflag) { restore_globstate(saved);
5676 // return; }`. getindex returns 0 after `mathevalarg`
5677 // rejects an empty operand (`*(N[1,])`), so the abort has
5678 // to come off errflag exactly as it does in C.
5679 if crate::ported::utils::errflag.load(std::sync::atomic::Ordering::Relaxed) != 0 {
5680 return qs; // c:1787
5681 }
5682 qs.first = Some(v.start); // c:1740
5683 qs.last = Some(v.end); // c:1741
5684 }
5685 // c:Src/glob.c:1579-1595 `case 'Y'` — short-circuit:
5686 // limit matches to at most N. Reads a numeric argument
5687 // immediately following the `Y`. Bug #41 in docs/BUGS.md.
5688 'Y' => {
5689 // c:1579-1594:
5690 // const char *s_saved = s;
5691 // shortcircuit = !(sense & 1);
5692 // if (shortcircuit) {
5693 // data = qgetnum(&s);
5694 // if ((shortcircuit = data) != data) {
5695 // /* Integer overflow */
5696 // zerr("value too big: Y%s", s_saved);
5697 // restore_globstate(saved);
5698 // return;
5699 // }
5700 // }
5701 // `data` is a zlong and `shortcircuit` an int, so the
5702 // assignment-then-compare is a 64→32 TRUNCATION test: a limit
5703 // that does not survive the narrowing is fatal. This parsed
5704 // straight into i32 and silently dropped the qualifier when
5705 // that failed, so `*(Y2147483648)` listed every match where
5706 // zsh errors — the limit was simply ignored.
5707 //
5708 // s_saved is the text AFTER the `Y`, running to the end of the
5709 // qualifier list, which is why the message carries any trailing
5710 // qualifiers too: `*(.NY99…)` reports `value too big: Y99…`
5711 // exactly as spelled.
5712 let s_saved: String = chars.clone().collect(); // c:1582
5713 // c:1582-1583 — `shortcircuit = !(sense & 1); if
5714 // (shortcircuit) { … }`. The NEGATED spelling `^Y` clears
5715 // the limit and consumes NO argument, so the qgetnum call
5716 // (and its "number expected" diagnostic) is skipped
5717 // entirely — `*(Y1^Y)` is legal. `Some(0)` is the same
5718 // "no limit" the apply sites read via their `n > 0` test,
5719 // matching c:518's leading `shortcircuit &&`.
5720 if negated {
5721 qs.short_circuit = Some(0); // c:1582
5722 continue;
5723 }
5724 let mut num_str = String::new();
5725 while let Some(&pc) = chars.peek() {
5726 if pc.is_ascii_digit() {
5727 num_str.push(pc);
5728 chars.next();
5729 } else {
5730 break;
5731 }
5732 }
5733 // c:830-833 — `qgetnum` opens with `if (!idigit(**s)) {
5734 // zerr("number expected"); return 0; }`. A bare `*(Y)` is
5735 // therefore a hard error that aborts the glob, not a
5736 // silently-ignored qualifier; the `d` arm below already
5737 // spells this out the same way.
5738 if num_str.is_empty() {
5739 crate::ported::utils::zerr("number expected"); // c:832
5740 crate::ported::utils::errflag.fetch_or(
5741 crate::ported::utils::ERRFLAG_ERROR,
5742 std::sync::atomic::Ordering::Relaxed,
5743 );
5744 return qs; // c:833 (`return 0` → caller aborts on errflag)
5745 }
5746 // c:1586 — qgetnum is `while (idigit(**s)) v = v * 10 + …`,
5747 // with no overflow check of its own; it simply wraps. Mirror
5748 // that rather than failing the parse, so the truncation test
5749 // below is what decides, exactly as in C.
5750 let mut data: i64 = 0;
5751 for ch in num_str.chars() {
5752 data = data
5753 .wrapping_mul(10)
5754 .wrapping_add((ch as i64) - ('0' as i64));
5755 }
5756 let sc = data as i32; // c:1587 `shortcircuit = data`
5757 if sc as i64 != data {
5758 // c:1587 `!= data`
5759 crate::ported::utils::zerr(&format!("value too big: Y{s_saved}")); // c:1589
5760 crate::ported::utils::errflag.fetch_or(
5761 crate::ported::utils::ERRFLAG_ERROR,
5762 std::sync::atomic::Ordering::Relaxed,
5763 );
5764 return qs; // c:1591
5765 }
5766 qs.short_circuit = Some(sc);
5767 }
5768 // c:Src/glob.c:1599-1620 `case 'e'` — `(e:CODE:)` shell-
5769 // eval qualifier. The body is delimited by the char
5770 // immediately following 'e' (matched at both ends).
5771 // Common forms in zsh scripts: `(e:'[[ -L $REPLY ]]':)`
5772 // for symlink filter, etc. The body runs against each
5773 // candidate path with $REPLY set; the file is kept iff
5774 // the body returns 0. zshrs's qualifier::Eval variant
5775 // was defined but had no parser arm — was rejected as
5776 // "unknown file attribute: e". Bug #469.
5777 // c:1707-1720 — `case '+': case 'e':` —
5778 // tt = glob_exec_string(&s);
5779 // if (tt == NULL) { data = 0; }
5780 // else { func = qualsheval; sdata = tt; }
5781 // glob_exec_string reads the `+IDENT` or the delimited `e:CODE:`
5782 // form (get_strarg bracket pairing, c:Src/subst.c:1366-1391) and
5783 // UNTOKENIZES it (c:1108), so an unquoted `e:REPLY=x:` runs
5784 // `REPLY=x` rather than a word carrying an Equals token.
5785 'e' | '+' => {
5786 let rest: String = chars.clone().collect();
5787 match glob_exec_string(&rest, c == '+') {
5788 Some((code, consumed)) => {
5789 // c:1110-1113 — `*sp = tt + plus;`
5790 for _ in 0..rest[..consumed].chars().count() {
5791 chars.next();
5792 }
5793 qs.qualifiers.push(qualifier::Eval(code));
5794 }
5795 None => {
5796 // glob_exec_string emitted the diagnostic; errflag
5797 // aborts the glob (c:1786-1788).
5798 crate::ported::utils::errflag.fetch_or(
5799 crate::ported::utils::ERRFLAG_ERROR,
5800 std::sync::atomic::Ordering::Relaxed,
5801 );
5802 return qs;
5803 }
5804 }
5805 }
5806 // c:Src/glob.c:1758-1762 — `default: zerr("unknown file
5807 // attribute: %c", *s); restore_globstate(saved); return;`.
5808 // Bug #583: zshrs's `_ => {}` arm silently accepted any
5809 // unknown qualifier letter, so `*(z)` returned all files
5810 // rc=0 instead of erroring "unknown file attribute: z".
5811 // Mirror C's strict behavior. The exemption for ASCII
5812 // whitespace that used to sit here was justified as "C's
5813 // qualifier loop stops on those naturally via its `case ')'`
5814 // arm" — C has no such arm. c:1308-1310 NULs the trailing
5815 // `)` (`str[sl-1] = 0`) before the walk, so the loop
5816 // condition is `while (*s && !newcolonmod)` over the body
5817 // ALONE and a space reaches `default:` like any other
5818 // unhandled byte: zsh answers `*(N )` with `unknown file
5819 // attribute: ` at rc=1 where the port listed every match at
5820 // rc=0. `\0` still terminates the C loop. The overwritten
5821 // closer is the Outpar TOKEN only; a QUOTED `)` left in the
5822 // body (`*(e"(true))")`, whose get_strarg stops at the first
5823 // `)`) is an ordinary byte and reaches `default:` too.
5824 ch if ch != '\0' => {
5825 // c:1758-1760 — `untokenize(--s); convchar_t attr =
5826 // unmeta_one(s, NULL); zerr("unknown file attribute:
5827 // %c", attr);`. C rewinds onto the offending char,
5828 // UNTOKENIZES it (Src/lex.c:2080 `ztokens[c - Pound]`)
5829 // and reports the source character. The port printed
5830 // the raw token instead, so `echo *(#q.)` without
5831 // EXTENDEDGLOB — where the lexer has already turned `#`
5832 // into Pound (U+e184) — said `unknown file attribute:
5833 // \u{e184}` where zsh says `unknown file attribute: #`.
5834 // `unmeta_one` is a no-op here: zshrs holds characters,
5835 // not metafied bytes.
5836 let attr = crate::ported::lex::untokenize_ztokens(&ch.to_string()); // c:1758
5837 crate::ported::utils::zerr(&format!("unknown file attribute: {}", attr));
5838 crate::ported::utils::errflag.fetch_or(
5839 crate::ported::utils::ERRFLAG_ERROR,
5840 std::sync::atomic::Ordering::Relaxed,
5841 );
5842 return qs;
5843 }
5844 _ => {}
5845 }
5846 }
5847
5848 if !qs.qualifiers.is_empty() {
5849 qs.alternatives.push(std::mem::take(&mut qs.qualifiers));
5850 }
5851
5852 // Build the faithful glob.c `struct qual` arena from `alternatives`
5853 // (additive: matching still uses the enum). Each alternative is an
5854 // AND-chain (`next` links); alternatives chain via `or`. ToggleSense
5855 // markers toggle a running per-node `sense` (c:1346). Each variant
5856 // maps to its leaf TestMatchFunc + data, mirroring check_single_
5857 // qualifier's dispatch (Src/glob.c:1343-1620 case → func/data).
5858 {
5859 let op_range = |o: char| -> i32 {
5860 match o {
5861 '<' => -1,
5862 '>' => 1,
5863 _ => 0,
5864 }
5865 };
5866 let mut arena = QualArena::new();
5867 let mut prev_alt_head: Option<usize> = None;
5868 for alt in &qs.alternatives {
5869 let mut sense = 0i32;
5870 let mut chain_tail: Option<usize> = None;
5871 let mut chain_head: Option<usize> = None;
5872 for q in alt {
5873 // (func, data, range, amc, units, sdata) — None = no node.
5874 let fields: Option<(TestMatchFunc, i64, i32, i32, i32, Option<String>)> = match q {
5875 qualifier::ToggleSense => {
5876 sense ^= 1; // c:1346
5877 None
5878 }
5879 qualifier::ToggleFollow => {
5880 sense ^= 2; // c:1351
5881 None
5882 }
5883 qualifier::IsRegular => Some((qualisreg, 0, 0, 0, 0, None)),
5884 qualifier::IsDirectory => Some((qualisdir, 0, 0, 0, 0, None)),
5885 qualifier::IsSymlink => Some((qualislnk, 0, 0, 0, 0, None)),
5886 qualifier::IsSocket => Some((qualissock, 0, 0, 0, 0, None)),
5887 qualifier::IsFifo => Some((qualisfifo, 0, 0, 0, 0, None)),
5888 qualifier::IsBlockDev => Some((qualisblk, 0, 0, 0, 0, None)),
5889 qualifier::IsCharDev => Some((qualischr, 0, 0, 0, 0, None)),
5890 qualifier::IsDevice => Some((qualisdev, 0, 0, 0, 0, None)),
5891 qualifier::IsExecutable => Some((qualiscom, 0, 0, 0, 0, None)),
5892 qualifier::Readable => Some((qualflags, 0o400, 0, 0, 0, None)),
5893 qualifier::Writable => Some((qualflags, 0o200, 0, 0, 0, None)),
5894 qualifier::Executable => Some((qualflags, 0o100, 0, 0, 0, None)),
5895 qualifier::WorldReadable => Some((qualflags, 0o004, 0, 0, 0, None)),
5896 qualifier::WorldWritable => Some((qualflags, 0o002, 0, 0, 0, None)),
5897 qualifier::WorldExecutable => Some((qualflags, 0o001, 0, 0, 0, None)),
5898 qualifier::GroupReadable => Some((qualflags, 0o040, 0, 0, 0, None)),
5899 qualifier::GroupWritable => Some((qualflags, 0o020, 0, 0, 0, None)),
5900 qualifier::GroupExecutable => Some((qualflags, 0o010, 0, 0, 0, None)),
5901 qualifier::Setuid => Some((qualflags, 0o4000, 0, 0, 0, None)),
5902 qualifier::Setgid => Some((qualflags, 0o2000, 0, 0, 0, None)),
5903 qualifier::Sticky => Some((qualflags, 0o1000, 0, 0, 0, None)),
5904 qualifier::OwnedByEuid => {
5905 Some((qualuid, unsafe { libc::geteuid() } as i64, 0, 0, 0, None))
5906 }
5907 qualifier::OwnedByEgid => {
5908 Some((qualgid, unsafe { libc::getegid() } as i64, 0, 0, 0, None))
5909 }
5910 qualifier::OwnedByUid(u) => Some((qualuid, *u as i64, 0, 0, 0, None)),
5911 qualifier::OwnedByGid(g) => Some((qualgid, *g as i64, 0, 0, 0, None)),
5912 qualifier::Size { value, unit, op } => {
5913 Some((qualsize, *value as i64, op_range(*op), -1, *unit, None))
5914 }
5915 qualifier::Links { value, op } => {
5916 Some((qualnlink, *value as i64, op_range(*op), 0, 0, None))
5917 }
5918 qualifier::Atime { value, unit, op } => {
5919 Some((qualtime, *value, op_range(*op), 0, *unit, None))
5920 }
5921 qualifier::Mtime { value, unit, op } => {
5922 Some((qualtime, *value, op_range(*op), 1, *unit, None))
5923 }
5924 qualifier::Ctime { value, unit, op } => {
5925 Some((qualtime, *value, op_range(*op), 2, *unit, None))
5926 }
5927 qualifier::Mode { yes, no } => Some((
5928 qualmodeflags,
5929 (*yes as i64) | ((*no as i64) << 12),
5930 0,
5931 0,
5932 0,
5933 None,
5934 )),
5935 qualifier::Device(d) => Some((qualdev, *d as i64, 0, 0, 0, None)),
5936 qualifier::NonEmptyDir => Some((qualnonemptydir, 0, 0, 0, 0, None)),
5937 qualifier::Eval(code) => Some((qualsheval, 0, 0, 0, 0, Some(code.clone()))),
5938 };
5939 let Some((func, data, range, amc, units, sdata)) = fields else {
5940 continue;
5941 };
5942 let idx = arena.alloc(); // c:1768 hcalloc
5943 {
5944 let node = &mut arena.nodes[idx];
5945 node.func = Some(func); // c:1774
5946 node.data = data; // c:1776
5947 node.sense = sense; // c:1775
5948 node.range = range; // c:1778
5949 node.units = units; // c:1779
5950 node.amc = amc; // c:1780
5951 node.sdata = sdata; // c:1777
5952 }
5953 if let Some(t) = chain_tail {
5954 arena.nodes[t].next = Some(idx); // c:1770
5955 }
5956 chain_tail = Some(idx);
5957 if chain_head.is_none() {
5958 chain_head = Some(idx);
5959 }
5960 }
5961 // Link this alternative's head into the or-chain (c:1324).
5962 if let Some(h) = chain_head {
5963 if let Some(p) = prev_alt_head {
5964 arena.nodes[p].or = Some(h);
5965 }
5966 prev_alt_head = Some(h);
5967 if arena.head.is_none() {
5968 arena.head = Some(h);
5969 }
5970 }
5971 }
5972 qs.quals = arena;
5973 }
5974
5975 qs
5976}
5977
5978/// Parse a numeric uid/gid or a delimited user/group name from a `u`/`g`
5979/// qualifier char stream. Port of the name-resolution arms of the `u`/`g`
5980/// qualifier cases at `Src/glob.c:1468-1535`:
5981/// `if (idigit(*s)) data = qgetnum(&s);` else `get_strarg`-delimited name
5982/// resolved through `getpwnam(3)` / `getgrnam(3)`. On an unknown name it
5983/// emits the C diagnostic (`zerr` sets errflag) and returns 0, matching
5984/// C's `data = 0;` arm. `is_group` selects getgrnam over getpwnam.
5985fn parse_uid_gid(chars: &mut std::iter::Peekable<std::str::Chars>, is_group: bool) -> u32 {
5986 // c:1468/1511 — `if (idigit(*s)) data = qgetnum(&s);`
5987 if chars.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
5988 let mut num = String::new();
5989 while let Some(&c) = chars.peek() {
5990 if c.is_ascii_digit() {
5991 num.push(c);
5992 chars.next();
5993 } else {
5994 break;
5995 }
5996 }
5997 return num.parse().unwrap_or(0);
5998 }
5999 // c:1471-1505 — `tt = get_strarg(s, &arglen);` the char right after the
6000 // qualifier is the delimiter; the name runs to the next delimiter.
6001 let delim = match chars.next() {
6002 Some(d) => d,
6003 None => {
6004 // c:1481/1524 — `zerr("missing delimiter for 'u'/'g' glob qualifier")`.
6005 zerr(if is_group {
6006 "missing delimiter for 'g' glob qualifier"
6007 } else {
6008 "missing delimiter for 'u' glob qualifier"
6009 });
6010 return 0;
6011 }
6012 };
6013 // c:Src/subst.c:1366-1391 — `get_strarg` maps the four bracket families
6014 // to their closing partner (raw ASCII at c:1367-1378, tokenized at
6015 // c:1379-1390); every other delimiter closes itself (c:1391). Scanning
6016 // for a repeat of the OPENING char left the close bracket unconsumed,
6017 // so `*(u{0})` resolved the name "0}" instead of "0".
6018 let close_delim = match delim {
6019 '(' => ')', // c:1367-1369
6020 '[' => ']', // c:1370-1372
6021 '{' => '}', // c:1373-1375
6022 '<' => '>', // c:1376-1378
6023 crate::ported::zsh_h::Inpar => crate::ported::zsh_h::Outpar, // c:1379-1381
6024 crate::ported::zsh_h::Inang => crate::ported::zsh_h::Outang, // c:1382-1384
6025 crate::ported::zsh_h::Inbrace => crate::ported::zsh_h::Outbrace, // c:1385-1387
6026 crate::ported::zsh_h::Inbrack => crate::ported::zsh_h::Outbrack, // c:1388-1390
6027 _ => delim, // c:1391
6028 };
6029 let mut name = String::new();
6030 let mut closed = false;
6031 for c in chars.by_ref() {
6032 if c == close_delim {
6033 closed = true;
6034 break;
6035 }
6036 name.push(c);
6037 }
6038 // c:1477-1479 / c:1519-1521 — `if (!*tt)`: get_strarg ran off the end
6039 // without finding the closer, so `*(u+1)` (`+` opens a name) is a
6040 // missing delimiter, not a lookup of the user `1`.
6041 if !closed {
6042 zerr(if is_group {
6043 "missing delimiter for 'g' glob qualifier"
6044 } else {
6045 "missing delimiter for 'u' glob qualifier"
6046 });
6047 return 0;
6048 }
6049 // c:1488/1530 — resolve via getpwnam(3) / getgrnam(3).
6050 let cstr = match std::ffi::CString::new(name.as_bytes()) {
6051 Ok(c) => c,
6052 Err(_) => return 0,
6053 };
6054 unsafe {
6055 if is_group {
6056 let gr = libc::getgrnam(cstr.as_ptr()); // c:1530
6057 if !gr.is_null() {
6058 return (*gr).gr_gid; // c:1531
6059 }
6060 zerr("unknown group"); // c:1533
6061 } else {
6062 let pw = libc::getpwnam(cstr.as_ptr()); // c:1488
6063 if !pw.is_null() {
6064 return (*pw).pw_uid; // c:1489
6065 }
6066 // c:1491 — `zerr("unknown username '%s'", s + arglen);`
6067 zerr(&format!("unknown username '{}'", name));
6068 }
6069 }
6070 // c:1493/1535 — `data = 0;` after the unknown-name diagnostic.
6071 0
6072}
6073
6074/// Parse the unit/op/value tail of an `(L...)` size qualifier.
6075/// **RUST-ONLY** — C parses inline in parsepat; the size-unit
6076/// constants are `TT_BYTES`/`TT_KILOBYTES`/… at glob.c:128-133.
6077fn parse_size_spec(
6078 // RUST-ONLY
6079 chars: &mut std::iter::Peekable<std::str::Chars>,
6080) -> (i32, char, u64) {
6081 let unit: i32 = match chars.peek() {
6082 Some('p') | Some('P') => {
6083 chars.next();
6084 TT_POSIX_BLOCKS
6085 }
6086 Some('k') | Some('K') => {
6087 chars.next();
6088 TT_KILOBYTES
6089 }
6090 Some('m') | Some('M') => {
6091 chars.next();
6092 TT_MEGABYTES
6093 }
6094 Some('g') | Some('G') => {
6095 chars.next();
6096 TT_GIGABYTES
6097 }
6098 Some('t') | Some('T') => {
6099 chars.next();
6100 TT_TERABYTES
6101 }
6102 _ => TT_BYTES,
6103 };
6104 let (op, val) = parse_range_spec(chars);
6105 (unit, op, val)
6106}
6107
6108/// Parse the unit/op/value tail of an `(a/m/c...)` time qualifier.
6109///
6110/// !!! WARNING: RUST-ONLY HELPER — NO C COUNTERPART !!!
6111/// C parses this inline in `parsepat`; the time-unit constants are
6112/// `TT_SECONDS`/`TT_MINS`/… at c:Src/glob.c:121-126.
6113///
6114/// It was previously NAMED `schedgetfn` — a real and entirely unrelated C
6115/// function (`Src/sched.c:341`, the `$zsh_scheduled_events` getfn) — which
6116/// made `build.rs`'s no-new-functions gate accept it. That disguised a
6117/// Rust-only helper as a port and put it in permanent violation of
6118/// `ported_fn_names_match_c`, which correctly saw a `sched.c` name in
6119/// `glob.rs`. Its two siblings, `parse_size_spec` and `parse_range_spec`,
6120/// were always handled the honest way — descriptive names, listed in
6121/// `tests/data/fake_fn_allowlist.txt` — and this now joins them.
6122fn parse_time_spec(
6123 // RUST-ONLY (clashes with sched.c:341 name, unrelated fn)
6124 chars: &mut std::iter::Peekable<std::str::Chars>,
6125) -> (i32, char, u64) {
6126 let unit: i32 = match chars.peek() {
6127 Some('s') => {
6128 chars.next();
6129 TT_SECONDS
6130 }
6131 Some('m') => {
6132 chars.next();
6133 TT_MINS
6134 }
6135 Some('h') => {
6136 chars.next();
6137 TT_HOURS
6138 }
6139 Some('d') => {
6140 chars.next();
6141 TT_DAYS
6142 }
6143 Some('w') => {
6144 chars.next();
6145 TT_WEEKS
6146 }
6147 Some('M') => {
6148 chars.next();
6149 TT_MONTHS
6150 }
6151 _ => TT_DAYS,
6152 };
6153 let (op, val) = parse_range_spec(chars);
6154 (unit, op, val)
6155}
6156
6157/// Parse `[+-]?NUMBER` operator+value tail. Mirrors C qgetnum
6158/// (glob.c:827) which returns the int value; here we return the
6159/// operator char along with the value since callers need both.
6160fn parse_range_spec(chars: &mut std::iter::Peekable<std::str::Chars>) -> (char, u64) {
6161 // RUST-ONLY
6162 // C's qgetnum at glob.c:827 returns the operator char inline.
6163 // `+N` = greater, `-N` = less, bare digits = equal.
6164 let op: char = match chars.peek() {
6165 Some('+') => {
6166 chars.next();
6167 '>'
6168 }
6169 Some('-') => {
6170 chars.next();
6171 '<'
6172 }
6173 _ => '=',
6174 };
6175 let mut num = String::new();
6176 while let Some(&c) = chars.peek() {
6177 if c.is_ascii_digit() {
6178 num.push(c);
6179 chars.next();
6180 } else {
6181 break;
6182 }
6183 }
6184 // c:Src/glob.c:826-834 — `qgetnum` errors with "number expected"
6185 // when not followed by `idigit(**s)`. Previously the Rust parser
6186 // silently treated the missing number as 0 and let the outer
6187 // qualifier loop continue consuming bytes as fresh qualifier
6188 // letters — `*(Lr)` parsed as `L0 + r` (readable, size=0) and
6189 // dropped to the no-matches path. Setting errflag here propagates
6190 // the canonical error message and gates the "no matches found"
6191 // emission in zglob (c:1843-1854 `if (errflag) ... return`).
6192 if num.is_empty() {
6193 crate::ported::utils::zerr("number expected"); // c:832
6194 crate::ported::utils::errflag.fetch_or(
6195 crate::ported::zsh_h::ERRFLAG_ERROR,
6196 std::sync::atomic::Ordering::SeqCst,
6197 );
6198 return (op, 0); // c:833
6199 }
6200 let val = num.parse().unwrap_or(0);
6201 (op, val)
6202}
6203
6204// `parse_subscript` (a RUST-ONLY `[FIRST,LAST]` splitter that
6205// `str::parse`d each half and dropped the qualifier on any non-decimal
6206// operand) is deleted: c:1735 routes the glob qualifier subscript
6207// through `getindex` (Src/params.c:2001), the shell's one subscript
6208// parser, and the `case '[':` arm above now does the same.
6209
6210/// Port of `static void insert(char *s, int checked)` from `Src/glob.c:346`.
6211/// Records one matched path into the glob result list: runs the qualifier
6212/// walk (`check_qualifiers` = the c:381-419 `struct qual` walk over the
6213/// arena) and, if accepted, builds the `gmatch` entry from the file's
6214/// stat and appends it (c:421-490) — honoring any `$reply`/`$REPLY`
6215/// replacement (INSERTS, c:428) set by an `(e:…:)` eval qualifier.
6216/// Called by the scanner per matched name, mirroring glob.c:576/668.
6217pub fn insert(state: &mut globdata, s: &Path, checked: i32) {
6218 use std::os::unix::fs::MetadataExt;
6219 // c:348-350 — `struct stat buf, buf2, *bp; int statted = 0;`. `buf`
6220 // is the lstat result, `buf2` the link-followed one; `statted` keeps
6221 // C's bit meaning (1 = buf valid, 2 = buf2 valid). C stats the file
6222 // AT MOST ONCE per variant and only when something actually reads
6223 // the result — the scanner already proved the file exists by
6224 // readdir'ing it (`checked`), so a plain `dir/*(N)` costs no stat at
6225 // all. The port used to `symlink_metadata` every match
6226 // unconditionally: 60605 wasted lstat + 60605 wasted stat on the
6227 // `man` completion glob, 37.6% of its CPU.
6228 let mut statted: i32 = 0;
6229 let mut buf: Option<fs::Metadata> = None;
6230 let mut buf2: Option<fs::Metadata> = None;
6231 // c:353 — `inserts = NULL;`: drop any $reply/$REPLY replacement left
6232 // by a previous file's (e:…:) eval before this file's walk.
6233 *INSERTS.lock().unwrap() = None;
6234 // c:187 `gd_gf_sorts` (macro at c:218) — the OR of every sort key the
6235 // glob's `o`/`O` qualifiers asked for. c:434/c:438 consult it to
6236 // decide whether this match needs its stat fields filled in. With no
6237 // explicit sort qualifier C leaves it at the c:1856 default GS_NAME,
6238 // which is in neither GS_NORMAL nor GS_LINKED.
6239 let gf_sorts: i32 = state.qualifiers.as_ref().map_or(0, |q| {
6240 q.sorts.iter().fold(0, |acc, &tp| acc | (tp & !GS_DESC))
6241 });
6242
6243 // c:355-364 — `if (gf_listtypes || gf_markdirs)` stat for the type
6244 // marker, and drop the match if the stat fails. The port appends the
6245 // marker at emit time (glob_emit_path), so only C's `checked =
6246 // statted = 1` side effect on the arms below is reproduced here.
6247 if state.gf_listtypes != 0 || state.gf_markdirs != 0 {
6248 match fs::symlink_metadata(s) {
6249 Ok(m) => {
6250 buf = Some(m);
6251 statted = 1; // c:363 `checked = statted = 1;`
6252 }
6253 Err(_) => return, // c:358-360
6254 }
6255 }
6256
6257 // c:381-418 — qualifier walk; reject the file if any test fails. The
6258 // walk needs the stat itself, so it hands its buffer back for the
6259 // arms below (C's `statted = 1` at c:391).
6260 if !check_qualifiers(state, s, &mut buf) {
6261 return;
6262 }
6263 if buf.is_some() {
6264 statted |= 1;
6265 }
6266 // c:419-423 — `else if (!checked) { if (statfullpath(s, NULL, 1))
6267 // return; }`: a match the scanner did NOT readdir (a pure literal
6268 // path section, c:576 `insert(str, 0)`) still has to be proven to
6269 // exist. Nothing reads the buffer, so C passes NULL.
6270 if statted & 1 == 0 && checked == 0 && fs::symlink_metadata(s).is_err() {
6271 return;
6272 }
6273 // c:434-437 — `if (!statted && (gf_sorts & GS_NORMAL))`: a
6274 // size/time/link sort key needs the stat fields, so fill them now.
6275 if statted & 1 == 0 && (gf_sorts & GS_NORMAL) != 0 {
6276 buf = fs::symlink_metadata(s).ok(); // c:435
6277 statted |= 1;
6278 }
6279 // c:438-445 — `if (!(statted & 2) && (gf_sorts & GS_LINKED))`: the
6280 // follow-link sort variants need the TARGET's stat. C reuses `buf`
6281 // verbatim when the match is not a symlink (c:440-441 memcpy), and
6282 // falls back to it when the target stat fails (a dangling link).
6283 if statted & 2 == 0 && (gf_sorts & GS_LINKED) != 0 {
6284 buf2 = match &buf {
6285 // c:439-441
6286 Some(m) if !m.file_type().is_symlink() => Some(m.clone()),
6287 Some(m) => fs::metadata(s).ok().or_else(|| Some(m.clone())),
6288 // c:442-443 `if (statfullpath(s,&buf2,0)) statfullpath(s,&buf2,1);`
6289 None => fs::metadata(s)
6290 .ok()
6291 .or_else(|| fs::symlink_metadata(s).ok()),
6292 };
6293 statted |= 2;
6294 }
6295
6296 // c:446-478 — copy the stat fields into the match entry, but only
6297 // the ones actually stat'd (`if (statted & 1)` / `if (statted & 2)`);
6298 // the rest stay zero as C's calloc'd match buffer leaves them.
6299 let name = s
6300 .file_name()
6301 .map(|n| n.to_string_lossy().to_string())
6302 .unwrap_or_default();
6303 let meta = buf;
6304 // c:463-478 — `if (statted & 2)` fills the `_size`/`_atime`/… twins.
6305 // When only the lstat ran, C's GS_LINKED keys are never consulted, so
6306 // mirroring the lstat values here is equivalent and keeps a match
6307 // that IS a symlink self-consistent.
6308 let target = buf2.as_ref().or(meta.as_ref());
6309 let (tsize, tatime, tmtime, tctime, tlinks, tansec, tmnsec, tcnsec) = match target {
6310 Some(tm) => (
6311 tm.size(),
6312 tm.atime(),
6313 tm.mtime(),
6314 tm.ctime(),
6315 tm.nlink(),
6316 tm.atime_nsec(),
6317 tm.mtime_nsec(),
6318 tm.ctime_nsec(),
6319 ),
6320 None => (0, 0, 0, 0, 0, 0, 0, 0),
6321 };
6322 // c:428 — `while (!inserts || (news = *inserts++))`: an (e:…:) eval
6323 // may have set $reply/$REPLY (INSERTS), replacing this match with
6324 // zero-or-more names; otherwise emit the original name once. The
6325 // synthetic path makes glob_emit_path yield the replacement string;
6326 // the stat fields stay those of the matched file (C keeps
6327 // matchptr->size etc. from buf).
6328 let emit: Vec<(String, PathBuf)> = match INSERTS.lock().unwrap().take() {
6329 Some(list) => list
6330 .into_iter()
6331 .map(|n| (n.clone(), PathBuf::from(&n)))
6332 .collect(),
6333 None => vec![(name.clone(), s.to_path_buf())],
6334 };
6335 for (nm, pth) in emit {
6336 state.matches.push(gmatch {
6337 name: nm,
6338 uname: String::new(), // c:1963-1973 — filled before the sort
6339 path: pth,
6340 size: meta.as_ref().map_or(0, |m| m.size()),
6341 atime: meta.as_ref().map_or(0, |m| m.atime()),
6342 mtime: meta.as_ref().map_or(0, |m| m.mtime()),
6343 ctime: meta.as_ref().map_or(0, |m| m.ctime()),
6344 links: meta.as_ref().map_or(0, |m| m.nlink()),
6345 mode: meta.as_ref().map_or(0, |m| m.mode()),
6346 uid: meta.as_ref().map_or(0, |m| m.uid()),
6347 gid: meta.as_ref().map_or(0, |m| m.gid()),
6348 dev: meta.as_ref().map_or(0, |m| m.dev()),
6349 ino: meta.as_ref().map_or(0, |m| m.ino()),
6350 target_size: tsize,
6351 target_atime: tatime,
6352 target_mtime: tmtime,
6353 target_ctime: tctime,
6354 target_links: tlinks,
6355 ansec: meta.as_ref().map_or(0, |m| m.atime_nsec()),
6356 mnsec: meta.as_ref().map_or(0, |m| m.mtime_nsec()),
6357 cnsec: meta.as_ref().map_or(0, |m| m.ctime_nsec()),
6358 target_ansec: tansec,
6359 target_mnsec: tmnsec,
6360 target_cnsec: tcnsec,
6361 sort_strings: Vec::new(),
6362 });
6363 state.matchct += 1; // c:479 matchptr++
6364 }
6365}
6366
6367/// Drive the OR-of-AND qualifier filter against `path`. **RUST-ONLY**
6368/// — C glob.c does qualifier eval inline inside `insert()` (c:381+).
6369///
6370/// `stat_out` is C's `struct stat buf` (c:348), which `insert()` shares
6371/// with this walk: c:385 fills it here and c:391's `statted = 1` stops
6372/// c:434 from stat'ing the same file a second time. Only the plain lstat
6373/// is handed back — a node with the follow bit reads C's `buf2`
6374/// (c:399-402), which the arms after c:419 track separately.
6375fn check_qualifiers(state: &globdata, path: &Path, stat_out: &mut Option<fs::Metadata>) -> bool {
6376 use std::os::unix::fs::MetadataExt;
6377 use std::sync::atomic::Ordering;
6378 let qs = match &state.qualifiers {
6379 Some(q) => q,
6380 None => return true,
6381 };
6382 // No test qualifiers (only flags/sort/words) → no filtering.
6383 let arena = &qs.quals;
6384 if arena.head.is_none() {
6385 return true;
6386 }
6387
6388 // c:385 `statfullpath(s, &buf, 1)` — this IS insert()'s `buf`.
6389 let meta = match fs::symlink_metadata(path) {
6390 Ok(m) => m,
6391 Err(_) => return false, // c:385-387
6392 };
6393 *stat_out = Some(meta.clone());
6394 // Bridge meta → libc::stat for the leaf test fns (no extra syscall).
6395 let to_stat = |meta: &fs::Metadata| -> libc::stat {
6396 let mut st: libc::stat = unsafe { std::mem::zeroed() };
6397 st.st_mode = meta.mode() as _;
6398 st.st_uid = meta.uid() as _;
6399 st.st_gid = meta.gid() as _;
6400 st.st_dev = meta.dev() as _;
6401 st.st_nlink = meta.nlink() as _;
6402 st.st_size = meta.size() as _;
6403 st.st_atime = meta.atime() as _;
6404 st.st_mtime = meta.mtime() as _;
6405 st.st_ctime = meta.ctime() as _;
6406 st
6407 };
6408 let st = to_stat(&meta);
6409 // c:397-402 — `buf2`, stat'ed once, the first time a node with the
6410 // follow bit (sense & 2) is reached:
6411 // if (!S_ISLNK(buf.st_mode) || statfullpath(s, &buf2, 0))
6412 // memcpy(&buf2, &buf, sizeof(buf));
6413 // A broken symlink keeps its lstat, so `*(-@)` still sees it.
6414 let mut st2: Option<libc::stat> = None;
6415 // qualsheval / qualnonemptydir need the name (REPLY / opendir).
6416 // c:424 — verbatim pathbuf text when the pattern starts with `./`.
6417 let name = if state.dot_prefix_verbatim { path.to_string_lossy().into_owned() } else { glob_emit_path(path) };
6418
6419 // c:387-419 — insert()'s qual-walk over the `struct qual` arena: AND
6420 // via `next`, OR (alternatives) via `or`, per-node `sense`. `qo` is
6421 // the current alternative head; a rejected node advances to `qo->or`
6422 // (next alternative) or, if none, rejects the file. Falling off the
6423 // `next` chain (or hitting a func-less terminator) accepts.
6424 let mut qo = arena.head;
6425 let mut qn = qo;
6426 loop {
6427 let Some(qn_i) = qn else { return true }; // c:419 — chain matched
6428 let (func, data, sense, range, amc, units, sdata, next) = {
6429 let n = &arena.nodes[qn_i];
6430 (
6431 n.func,
6432 n.data,
6433 n.sense,
6434 n.range,
6435 n.amc,
6436 n.units,
6437 n.sdata.clone(),
6438 n.next,
6439 )
6440 };
6441 let func = match func {
6442 Some(f) => f,
6443 None => return true, // c:392 — `qn && qn->func` terminator
6444 };
6445 // c:393-395 — set the scratch the range/time/size leaf fns read.
6446 g_range.store(range, Ordering::SeqCst);
6447 g_amc.store(amc, Ordering::SeqCst);
6448 g_units.store(units, Ordering::SeqCst);
6449 // c:403 — `bp = (qn->sense & 2) ? &buf2 : &buf;`
6450 let bp = if sense & 2 != 0 {
6451 &*st2.get_or_insert_with(|| {
6452 if meta.file_type().is_symlink() {
6453 fs::metadata(path).map(|m| to_stat(&m)).unwrap_or(st)
6454 } else {
6455 st
6456 }
6457 })
6458 } else {
6459 &st
6460 };
6461 let r = func(&name, bp, data, sdata.as_deref().unwrap_or(""));
6462 // c:407-409 — reject if `(!(func()) ^ sense) & 1`.
6463 let reject = ((if r == 0 { 1 } else { 0 }) ^ (sense & 1)) & 1 == 1;
6464 if reject {
6465 // c:411-415 — try the next alternative, else reject the file.
6466 match arena.nodes[qo.unwrap()].or {
6467 Some(or_i) => {
6468 qo = Some(or_i);
6469 qn = Some(or_i);
6470 }
6471 None => return false,
6472 }
6473 } else {
6474 qn = next; // c:417
6475 }
6476 }
6477}
6478
6479/// Sort the per-state matches per the qualifier `o`/`O` keys.
6480/// **RUST-ONLY** — C does this inline at the end of `scanner()`
6481/// (glob.c:1700ish, qsort with `gmatchcmp` comparator).
6482/// Returns whether `gf_sortlist[0]` is `GS_NONE`, which decides the
6483/// direction of the `[first,last]` walk (c:1991).
6484fn sort_matches(state: &mut globdata) -> bool {
6485 // RUST-ONLY
6486 // Default sort is GS_NAME ascending (c:204 gf_sortlist
6487 // initial setup). Per-qualifier `o<key>` / `O<key>` overrides.
6488 // c:1855-1857 — when gf_nsorts == 0 (no explicit o/O qualifier),
6489 // glob.c falls back to GS_NAME (or GS_NONE under shortcircuit).
6490 // Mirror that here so a qualifier set carrying ONLY non-sort
6491 // qualifiers (`Lk+0`, etc.) still sorts alphabetically.
6492 // c:1855-1857 is a CONDITIONAL default, not a fixed one:
6493 // if (!gf_nsorts) {
6494 // gf_sortlist[0].tp = gf_sorts = (shortcircuit ? GS_NONE : GS_NAME);
6495 // gf_nsorts = 1;
6496 // }
6497 // A `Y` limit means "give me the first N the scanner finds", so with no
6498 // explicit o/O spec the default becomes GS_NONE and the results stay in
6499 // SCAN order — sorting them would defeat the short-circuit, since the first
6500 // N by name are not the first N found. This always chose GS_NAME, so a
6501 // limited glob returned the alphabetically-first N instead of the
6502 // scan-order-first N, and even `*(Y99)` (a limit larger than the match
6503 // count, which discards nothing) came back sorted where zsh leaves it in
6504 // readdir order.
6505 //
6506 // `Y0` is the counter-case and must still sort: c:1583 parses the argument
6507 // over `shortcircuit`, so 0 lands there legitimately and c:518's
6508 // `if (shortcircuit && …)` reads it as "no limit" — hence the `> 0` test
6509 // rather than `.is_some()`.
6510 let limited = state
6511 .qualifiers
6512 .as_ref()
6513 .and_then(|q| q.short_circuit)
6514 .is_some_and(|n| n > 0); // c:1856 `shortcircuit ?`
6515 let default_spec = if limited { GS_NONE } else { GS_NAME }; // c:1856
6516 let mut specs: Vec<i32> = state
6517 .qualifiers
6518 .as_ref()
6519 .map(|q| {
6520 if q.sorts.is_empty() {
6521 vec![default_spec] // c:1856
6522 } else {
6523 q.sorts.clone()
6524 }
6525 })
6526 .unwrap_or_else(|| vec![GS_NAME]);
6527 // c:1991 — the subscript walk tests only `gf_sortlist[0]`.
6528 let unsorted = specs[0] & GS_NONE != 0;
6529
6530 // GS_NONE marker — caller wants no sort at all.
6531 if specs.iter().any(|&tp| (tp & GS_NONE) != 0) {
6532 return unsorted;
6533 }
6534
6535 // c:936 gmatchcmp returns 0 when every sort key ties. In C the final
6536 // order of equal-key matches is then whatever the libc `qsort` does with
6537 // all-equal elements (glob.c:1976) — NON-stable on macOS/BSD (arbitrary,
6538 // input-dependent: `*(oL)` on three 0-byte files a/b/d prints `d a b`),
6539 // and on glibc it depends on readdir order. So zsh has NO portable,
6540 // defined tie-break here. zshrs is a cross-architecture shell and must be
6541 // DETERMINISTIC, so we impose an explicit GS_NAME final tie-breaker:
6542 // equal-key matches order by name ascending, identical on every platform.
6543 // This is an intentional determinism guarantee, NOT a bug-for-bug match of
6544 // zsh's qsort-defined order (which can't be reproduced portably). Skip
6545 // when the last key is already GS_NAME (the no-qualifier default, or an
6546 // explicit trailing `on`) so we don't double-compare.
6547 if specs
6548 .last()
6549 .map_or(true, |&last| (last & !GS_DESC) != GS_NAME)
6550 {
6551 specs.push(GS_NAME); // deterministic name-ascending tie-break
6552 }
6553
6554 // c:1958-1973 — "Where necessary, create unmetafied version of names
6555 // for comparison." C runs this loop once over the match list, right
6556 // before the qsort at c:1977, so `gmatchcmp` (c:945) collates two
6557 // ready-made strings. The port's key is the full match path with the
6558 // scanner's depth-0 "./" prefix stripped:
6559 //
6560 // c:424 — C stores bare-relative match names (`dyncat(pathbuf,
6561 // news)`, no "./"), so its single qsort sorts the exact strings it
6562 // emits. The Rust scanner joins depth-0 matches against base "."
6563 // (glob.rs:527 + :584), giving a leading "./" that deeper matches
6564 // lack and that `glob_emit_path` strips at emit. Since '.' (0x2E)
6565 // sorts before any letter, that stray prefix clustered every
6566 // top-level entry ahead of any nested path (`**/*` → `d e m d/z`
6567 // instead of `d d/z e m`). Strip it so the sort key matches the
6568 // emit key.
6569 //
6570 // c:355-378 — insert() appends the `(M)`/`(T)`/MARK_DIRS type marker to
6571 // the name BEFORE storing it (c:444 `matchptr->name = news`), so the
6572 // marker is part of the sort key: under en_US collation `foo.c` sorts
6573 // before `foo/`, while the unmarked `foo` sorts first. The port stamps
6574 // the marker at emit (after this sort), so add it to the key here.
6575 let mark_dirs = state
6576 .qualifiers
6577 .as_ref()
6578 .and_then(|q| q.mark_dirs)
6579 .unwrap_or_else(|| glob_isset(MARKDIRS)); // c:1255 / c:1558
6580 let list_types = state.qualifiers.as_ref().is_some_and(|q| q.list_types);
6581 let mark_follow = state.qualifiers.as_ref().is_some_and(|q| q.mark_follow);
6582 for m in state.matches.iter_mut() {
6583 let full = m.path.to_string_lossy();
6584 m.uname = full.strip_prefix("./").unwrap_or(&full).to_string();
6585 // c:355 — `if (gf_listtypes || gf_markdirs)`
6586 if mark_dirs || list_types {
6587 // c:358 — `statfullpath(s, &buf, 1)` (lstat); c:366-371 — under
6588 // gf_follow a symlink is re-stat'ed, falling back to the lstat.
6589 if let Ok(lmeta) = fs::symlink_metadata(&m.path) {
6590 let meta = if mark_follow && lmeta.file_type().is_symlink() {
6591 fs::metadata(&m.path).unwrap_or(lmeta)
6592 } else {
6593 lmeta
6594 };
6595 let ch = file_type(meta.mode()); // c:377
6596 // c:372 — `if (gf_listtypes || S_ISDIR(mode))`
6597 if list_types || ch == '/' {
6598 m.uname.push(ch); // c:376-378
6599 }
6600 }
6601 }
6602 }
6603
6604 // c:1258/1575 — gf_numsort starts at the global NUMERIC_GLOB_SORT
6605 // option, then a per-glob `(n)`/`(^n)` qualifier OVERRIDES it
6606 // absolutely (so `(^n)` forces lexical even when the option is on).
6607 let numeric = state
6608 .qualifiers
6609 .as_ref()
6610 .and_then(|q| q.numsort)
6611 .unwrap_or_else(|| glob_isset(NUMERICGLOBSORT));
6612 state
6613 .matches
6614 .sort_by(|a, b| gmatchcmp(a, b, &specs, numeric));
6615 unsorted
6616}
6617
6618/// Apply `[FIRST,LAST]` qualifier subscript on the match list.
6619/// **RUST-ONLY** — C uses `gd_pre_first` / `gd_first` index tracking
6620/// during scanner emit; here we slice after the full walk.
6621fn apply_selection(state: &mut globdata, unsorted: bool) {
6622 // RUST-ONLY
6623 let (first, last, short_circuit) = match &state.qualifiers {
6624 Some(q) => (q.first, q.last, q.short_circuit),
6625 None => return,
6626 };
6627
6628 let len = state.matches.len() as i32;
6629 if len == 0 {
6630 return;
6631 }
6632
6633 let start = match first {
6634 Some(f) if f < 0 => (len + f).max(0) as usize,
6635 Some(f) => f as usize, // c:1739 `first = v.start` (getindex: 0-based, c:2145)
6636 None => 0,
6637 };
6638
6639 let end = match last {
6640 Some(l) if l < 0 => (len + l + 1).max(0) as usize,
6641 Some(l) => l.min(len) as usize,
6642 None => len as usize,
6643 };
6644
6645 // c:1991-1998 — with `GS_NONE` first (`oN`, or the `Y` default) C never
6646 // sorted `matchbuf`, so it inserts "back to front": it starts at
6647 // `matchbuf + matchct - first - 1` and walks DOWN, each match landing
6648 // right after the same node (insert_glob_match, c:1124-1141). The
6649 // subscript therefore counts from the END of the scan order, while the
6650 // words still come out in scan order: files scanned `1 10 2` give
6651 // `*(oN[1,2])` → `10 2` and `*(oN[1])` → `2`.
6652 let (start, end) = if unsorted {
6653 let n = len as usize;
6654 (n.saturating_sub(end), n.saturating_sub(start))
6655 } else {
6656 (start, end)
6657 };
6658
6659 // c:1990-2004 — C never copies the match array for the subscript: it
6660 // walks the existing `matchbuf` from `matchbuf + matchct - first - end`
6661 // and inserts straight out of it. The Rust equivalent that keeps the
6662 // elements where they are is truncate-then-drain: both move `gmatch`
6663 // values (each owning a PathBuf + String) instead of deep-cloning the
6664 // whole 60k-entry list the way `to_vec()` did.
6665 if start < end && start < state.matches.len() {
6666 state.matches.truncate(end.min(state.matches.len())); // c:1986-1989 `end` clamp
6667 state.matches.drain(..start); // c:1981-1985 `first` offset
6668 } else {
6669 state.matches.clear();
6670 }
6671
6672 // c:Src/glob.c:1579-1595 `case 'Y'` — apply the short-circuit
6673 // limit after the [first,last] slice (so `(Y3[2,4])` would yield
6674 // up to 3 from the [2,4] subscript range). C truncates DURING
6675 // the scanner walk via `if (shortcircuit == matchct) break;` —
6676 // here we slice post-walk which produces the same set on the
6677 // common short-glob case where the scanner doesn't recurse
6678 // unboundedly. Bug #41 in docs/BUGS.md.
6679 // NOTE: the `Y` short-circuit is deliberately NOT applied here. It bounds
6680 // the SCAN (c:Src/glob.c:518), so it must run BEFORE the sort — see the
6681 // truncation at the sort_matches call site. Only the `[first,last]`
6682 // subscript belongs in this post-sort selection.
6683 let _ = short_circuit;
6684}
6685
6686// `haswilds()` is defined in `Src/pattern.c:4306`, not glob.c — the
6687// canonical Rust port lives at `crate::ported::pattern::haswilds`.
6688// This file previously carried a divergent re-implementation tracking
6689// bracket/escape state instead of the token-aware `zpc_disables[]` /
6690// SHGLOB / KSHGLOB / EXTENDEDGLOB checks the C source actually does.
6691// Drift deleted; glob.rs callers route through the canonical port.
6692
6693#[cfg(test)]
6694mod gs_tt_tests {
6695 use super::*;
6696
6697 #[test]
6698 fn gs_size_offset_matches_c() {
6699 let _g = crate::test_util::global_state_lock();
6700 // c:83 — GS_SIZE = GS_SHIFT_BASE = 8.
6701 assert_eq!(GS_SIZE, 8);
6702 // c:84 — GS_ATIME = GS_SHIFT_BASE << 1 = 16.
6703 assert_eq!(GS_ATIME, 16);
6704 // c:87 — GS_LINKS = GS_SHIFT_BASE << 4 = 128.
6705 assert_eq!(GS_LINKS, 128);
6706 }
6707
6708 #[test]
6709 fn gs_normal_covers_all_size_keys() {
6710 let _g = crate::test_util::global_state_lock();
6711 // c:99 — GS_NORMAL = SIZE | ATIME | MTIME | CTIME | LINKS.
6712 assert_ne!(GS_NORMAL & GS_SIZE, 0);
6713 assert_ne!(GS_NORMAL & GS_ATIME, 0);
6714 assert_ne!(GS_NORMAL & GS_MTIME, 0);
6715 assert_ne!(GS_NORMAL & GS_CTIME, 0);
6716 assert_ne!(GS_NORMAL & GS_LINKS, 0);
6717 }
6718
6719 #[test]
6720 fn tt_namespaces_share_indices() {
6721 let _g = crate::test_util::global_state_lock();
6722 // c:121-126 vs c:128-133 — TT_DAYS == TT_BYTES == 0, etc.
6723 assert_eq!(TT_DAYS, TT_BYTES);
6724 assert_eq!(TT_HOURS, TT_POSIX_BLOCKS);
6725 assert_eq!(TT_MINS, TT_KILOBYTES);
6726 assert_eq!(TT_WEEKS, TT_MEGABYTES);
6727 assert_eq!(TT_MONTHS, TT_GIGABYTES);
6728 assert_eq!(TT_SECONDS, TT_TERABYTES);
6729 }
6730
6731 #[test]
6732 fn max_sorts_is_12() {
6733 let _g = crate::test_util::global_state_lock();
6734 assert_eq!(MAX_SORTS, 12);
6735 }
6736}
6737
6738fn expand_range(
6739 prefix: &str,
6740 content: &str,
6741 dotdot_pos: usize,
6742 suffix: &str,
6743) -> Option<Vec<String>> {
6744 // c:Src/glob.c::xpandbraces parses brace-range endpoints via
6745 // zstrtol, which is TOKEN-aware and treats Dash TOKEN (\u{e19b})
6746 // as ASCII `-`. Rust's i64::parse only accepts ASCII, so
6747 // untokenize the content here before splitting/parsing — matches
6748 // C semantics without weakening the strict-TOKEN gates above.
6749 // A `Meta` (`\u{83}`) starts a METAFIED byte pair (`Meta` + `byte ^
6750 // 32`), NOT a token: C's `itok` set begins at `Pound` (Src/zsh.h:164)
6751 // and `Meta` sits below it, so `bracechardots` (c:2227) leaves such a
6752 // pair to MB_METACHARLENCONV instead of the `ztokens` lookup. The
6753 // escaped byte routinely LANDS in the token range (`$'\x80'` →
6754 // `\u{83}\u{a0}`), so scanning it as a token here untokenized the
6755 // pair and destroyed the range endpoint. Skip the pair when deciding
6756 // whether the content needs untokenizing.
6757 let owned: String;
6758 let content: &str = if {
6759 let cs: Vec<char> = content.chars().collect();
6760 let mut i = 0usize;
6761 let mut found = false;
6762 while i < cs.len() {
6763 if cs[i] == char::from(crate::ported::zsh_h::Meta) {
6764 i += 2; // metafied byte pair — not a token
6765 continue;
6766 }
6767 let cu = cs[i] as u32;
6768 if (0xe184..=0xe1a1).contains(&cu)
6769 && cs[i] != '\u{e18f}'
6770 && cs[i] != '\u{e190}'
6771 && cs[i] != '\u{e19a}'
6772 {
6773 found = true;
6774 break;
6775 }
6776 i += 1;
6777 }
6778 found
6779 } {
6780 owned = crate::lex::untokenize(content);
6781 &owned
6782 } else {
6783 content
6784 };
6785 // `dotdot_pos` is a CHAR index (xpandbraces counts it over a char vector,
6786 // `i - start - 1`), but the slicing below is byte-based. For ASCII content
6787 // they coincide; for content holding multibyte/metafied chars (e.g.
6788 // `{$'\x80'..$'\x81'}`, where the bytes are Meta-escaped) a raw byte slice
6789 // at the char index lands mid-char and panics. Convert to a byte offset.
6790 let dotdot_byte = content
6791 .char_indices()
6792 .nth(dotdot_pos)
6793 .map(|(b, _)| b)
6794 .unwrap_or(content.len());
6795 let left = &content[..dotdot_byte];
6796 let right_start = dotdot_byte + 2; // ".." is two ASCII bytes
6797
6798 // Check for second `..` for `{N..M..S}` step form. Step may be
6799 // signed: negative-step REVERSES the natural direction sequence
6800 // per zsh's brace expansion (Src/glob.c::xpandbraces recursive
6801 // iteration with sign tracking). Examples:
6802 // {1..32..3} → 1,4,7,…,31 (natural ascending)
6803 // {1..32..-3} → 31,28,…,1 (same set, reversed)
6804 // {32..1..3} → 32,29,…,2 (natural descending)
6805 // {32..1..-3} → 2,5,…,32 (same set, reversed)
6806 //
6807 // c:Src/glob.c:2365-2369 — `if (dotdot == 2 && *p == '.' &&`
6808 // ` p[1] == '.') {`
6809 // ` rincr = zstrtol(p+2, &p, 10);`
6810 // ` wid3 = p - dots2 - 2;`
6811 // ` if (p != str2 || !rincr)`
6812 // ` err++;`
6813 // The `!rincr` test (c:2368) rejects a zero step: when err > 0,
6814 // the C code falls past the c:2374 `if (!err)` block, so the
6815 // brace expansion is abandoned and the literal pattern is
6816 // preserved. Parity bug #15: previously the Rust port silently
6817 // clamped step to 1 via `.max(1)`, producing `1 2 3 4 5` for
6818 // `{1..5..0}` instead of zsh's literal `1..5..0`.
6819 let (right, incr_abs, incr_sign_negative, step_text, raw_step) =
6820 if let Some(pos) = content[right_start..].find("..") {
6821 let r = &content[right_start..right_start + pos];
6822 let s_text = &content[right_start + pos + 2..];
6823 // c:2366-2369 — `rincr = zstrtol(p+2, &p, 10); … if (p != str2
6824 // || !rincr) err++;` — an empty or non-numeric step (`{1..2..}`)
6825 // is an error just like a zero one, not a step of 1.
6826 let raw: i64 = match s_text.parse() {
6827 Ok(v) => v,
6828 Err(_) => return None,
6829 };
6830 // bash treats a zero step as 1 (`{1..3..0}` → 1 2 3); the
6831 // `.max(1)` at each use site applies that.
6832 if raw == 0 && !crate::dash_mode::bash_mode() {
6833 // c:2368 — `!rincr` → err++ → no expansion. Return
6834 // None so xpandbraces falls back to literal.
6835 return None;
6836 }
6837 (r, raw.unsigned_abs(), raw < 0, s_text, Some(raw))
6838 } else {
6839 (&content[right_start..], 1u64, false, "", None)
6840 };
6841
6842 // Try numeric range
6843 if let (Ok(start), Ok(end)) = (left.parse::<i64>(), right.parse::<i64>()) {
6844 let mut results = Vec::new();
6845
6846 // Iterate from `start` toward `end` with abs(step). Sign of
6847 // start/end relative to each other determines natural
6848 // direction; step is always |step|.
6849 let step = incr_abs.max(1) as i64;
6850 let mut vals: Vec<i64> = Vec::new();
6851 if crate::dash_mode::ksh93_mode() {
6852 vals = crate::dash_mode::ksh93_range_values(start, end, raw_step);
6853 } else if start <= end {
6854 let mut v = start;
6855 while v <= end {
6856 vals.push(v);
6857 v += step;
6858 }
6859 } else {
6860 let mut v = start;
6861 while v >= end {
6862 vals.push(v);
6863 v -= step;
6864 }
6865 }
6866 // bash takes the direction from the endpoints alone and ignores the
6867 // step's sign (`{6..1..-2}` → 6 4 2, `{1..6..-2}` → 1 3 5); only zsh
6868 // reverses the sequence for a negative step.
6869 if incr_sign_negative && !crate::dash_mode::bash_mode() && !crate::dash_mode::ksh93_mode() {
6870 vals.reverse();
6871 }
6872
6873 // Padding: zsh pads with leading zeros when ANY of the three
6874 // textual fields (left endpoint, right endpoint, step) has a
6875 // leading zero after stripping the optional sign. Width is
6876 // the max textual width across left/right/step. For negative
6877 // values, the sign prefix counts toward width — we emit `-`
6878 // then zero-pad the remaining digits (`-02`, not `0-2`).
6879 // Direct port of Src/lex.c::dobrace_pad logic which detects
6880 // pad mode per the textual form of all three fields.
6881 // zsh's pad-detection: pad fires when any of the three
6882 // textual fields (left / right / step) is a multi-digit
6883 // form with a leading zero (e.g. "01", "00", "003"), with
6884 // ONE exception: when the LEFT endpoint is exactly "0"
6885 // (bare single zero, possibly signed), pad is suppressed
6886 // regardless of right's form. Empirical zsh:
6887 // {0..-5} → 0 -1 -2 -3 -4 -5 (left=0 → no pad)
6888 // {0..03} → 0 1 2 3 (left=0 → no pad)
6889 // {00..-5} → 00 -1 -2 -3 -4 -5 (left=00 → pad w=2)
6890 // {1..03} → 01 02 03 (right=03 → pad w=2)
6891 // {1..-03} → 001 000 -01 -02 -03 (right=-03 → pad w=3)
6892 let lstrip = left.trim_start_matches(['+', '-']);
6893 let rstrip = right.trim_start_matches(['+', '-']);
6894 let sstrip = step_text.trim_start_matches(['+', '-']);
6895 let is_padded_field = |stripped: &str| stripped.len() >= 2 && stripped.starts_with('0');
6896 // Suppress pad ONLY when LEFT is exactly the single-char "0"
6897 // (no sign, no extra digits). "-0" or "00" both pad.
6898 let left_is_bare_zero = left == "0";
6899 // ksh93 never zero-pads a numeric range (`{01..03}` -> `1 2 3`).
6900 let pad = !crate::dash_mode::ksh93_mode()
6901 && !left_is_bare_zero
6902 && (is_padded_field(lstrip)
6903 || is_padded_field(rstrip)
6904 || (!step_text.is_empty() && is_padded_field(sstrip)));
6905 let width = left.len().max(right.len()).max(step_text.len());
6906
6907 for v in vals {
6908 let formatted = if pad {
6909 if v < 0 {
6910 let abs = (-v).to_string();
6911 let inner_w = width.saturating_sub(1);
6912 format!("-{:0>w$}", abs, w = inner_w)
6913 } else {
6914 format!("{:0>w$}", v, w = width)
6915 }
6916 } else {
6917 v.to_string()
6918 };
6919 results.push(format!("{}{}{}", prefix, formatted, suffix));
6920 }
6921 return Some(results);
6922 }
6923
6924 // Try character range
6925 // c:Src/lex.c::dobrace alpha-range path — zsh only handles bare a..z,
6926 // NOT a..z..N (it emits literal `{a..z..3}` because the step form is
6927 // numeric-only). bash DOES support an alpha step (`{a..e..2}` → a c e),
6928 // so honor `step_text` only under --bash; --zsh keeps refusing it so the
6929 // literal survives (verified: real zsh emits `{a..e..2}` unchanged).
6930 // bash ignores the step's SIGN for alpha (`{a..e..-2}` == `{a..e..2}`),
6931 // so use abs(step) and take direction purely from start vs end.
6932 let alpha_step_ok = step_text.is_empty()
6933 || crate::dash_mode::bash_mode()
6934 || crate::dash_mode::ksh93_mode();
6935 // c:2311 — `if (bracechardots(str, &cstart, &cend))` decides the
6936 // CHARACTER range. The endpoints are METAFIED text, decoded through
6937 // MB_METACHARLENCONV (c:2236/2257), so a metafied 8-bit byte
6938 // (`$'\x80'` → `Meta` `\u{e1a0}`) and a real multibyte character both
6939 // resolve to their scalar. The old byte-length test (`left.len() ==
6940 // 1`) accepted only ASCII and left `{é..ê}` / `{$'\x80'..$'\x81'}`
6941 // unexpanded.
6942 let lb = crate::ported::utils::unmetafy_str(left); // c:2236
6943 let (l_len, cstart, _) = crate::ported::utils::mb_metacharlenconv(&lb); // c:2236
6944 let rb = crate::ported::utils::unmetafy_str(right); // c:2257
6945 let (r_len, cend, _) = crate::ported::utils::mb_metacharlenconv(&rb); // c:2257
6946 let endpoints_ok = l_len != 0 && l_len == lb.len() && r_len != 0 && r_len == rb.len();
6947 if endpoints_ok && cstart.is_some() && cend.is_some() && alpha_step_ok {
6948 let start = cstart?; // c:2311 cstart
6949 let end = cend?; // c:2311 cend
6950 // Walk FROM `start` toward `end`: bash steps from the left endpoint,
6951 // so `{f..a..2}` is `f d b`. Stepping up from the low end and
6952 // reversing gave `e c a` whenever the span is not a step multiple.
6953 let step = incr_abs.max(1) as i64;
6954 let (from, to) = (start as i64, end as i64);
6955 let dir = if from <= to { step } else { -step };
6956
6957 let mut results = Vec::new();
6958 let mut chars: Vec<char> = Vec::new();
6959 if crate::dash_mode::ksh93_mode() {
6960 chars.extend(
6961 crate::dash_mode::ksh93_range_values(from, to, raw_step)
6962 .into_iter()
6963 .filter_map(|v| char::from_u32(v as u32)),
6964 );
6965 } else {
6966 let mut v = from;
6967 while (dir > 0 && v <= to) || (dir < 0 && v >= to) {
6968 if let Some(c) = char::from_u32(v as u32) {
6969 chars.push(c);
6970 }
6971 v += dir;
6972 }
6973 }
6974
6975 for c in chars {
6976 // c:2328-2334 — `MB_CHARINIT(); ncptr = MB_NICECHAR(cend);
6977 // ... memcpy(p + strp, ncptr, nclen);` — every element of a
6978 // CHARACTER range is spliced in through `MB_NICECHAR`
6979 // (Src/zsh.h:3288 `wcs_nicechar(cp, NULL, NULL)`), not as the
6980 // raw character. Printable characters render as themselves, so
6981 // `{a..e}` is unchanged, while a non-printable 8-bit endpoint
6982 // comes out as its `\M-^@`-style escape — which is exactly what
6983 // `{$'\x80'..$'\x81'}` prints in zsh.
6984 let ncptr = crate::ported::utils::wcs_nicechar(c, None, None); // c:2329
6985 results.push(format!("{}{}{}", prefix, ncptr, suffix)); // c:2332-2334
6986 }
6987 return Some(results);
6988 }
6989
6990 None
6991}
6992
6993fn expand_comma(
6994 prefix: &str,
6995 content: &str,
6996 positions: &[usize],
6997 suffix: &str,
6998) -> Option<Vec<String>> {
6999 // positions are CHAR indices into `content` (per the xpandbraces
7000 // walker above that builds them from `chars: Vec<char>`). Slice
7001 // by char-index, not byte-index — `&content[last..pos]` would
7002 // panic on multi-byte token chars like Comma TOKEN (`\u{e19a}`,
7003 // 2 UTF-8 bytes) and Inbrace (`\u{e18f}`, 2 bytes).
7004 let chars: Vec<char> = content.chars().collect();
7005 let mut results = Vec::new();
7006 let mut last: usize = 0;
7007 for &pos in positions {
7008 let part: String = chars[last..pos].iter().collect();
7009 results.push(format!("{}{}{}", prefix, part, suffix));
7010 last = pos + 1;
7011 }
7012 let tail: String = chars[last..].iter().collect();
7013 results.push(format!("{}{}{}", prefix, tail, suffix));
7014 Some(results)
7015}
7016
7017fn expand_ccl(prefix: &str, content: &str, suffix: &str) -> Option<Vec<String>> {
7018 // c:2421-2470 — `if (!comma && isset(BRACECCL)) { /* {a-mnop} */`
7019 // "Here we expand each character to a separate node, but also
7020 // ranges of characters like a-m. ccl is a set of flags saying
7021 // whether each character is present; the final list is in
7022 // lexical order."
7023 use crate::ported::zsh_h::{Meta, Pound};
7024 use crate::ported::ztype_h::{imeta, itok};
7025 // !!! RUST-ONLY adapter (no C counterpart) !!! C walks the metafied
7026 // BYTES of the word. A zshrs word is a UTF-8 `str` holding tokens as
7027 // PUA chars (crate::token_char), a byte that is not valid UTF-8 as `Meta` +
7028 // `byte ^ 32`, and NUL as a plain char. Rebuild C's byte string: a
7029 // token is its one byte, a Meta pair stays a pair, and any other char
7030 // contributes its UTF-8 bytes, metafied where C's would be (`imeta`,
7031 // which includes NUL).
7032 let mut str: Vec<u8> = Vec::with_capacity(content.len());
7033 let mut chars = content.chars();
7034 while let Some(c) = chars.next() {
7035 let cu = c as u32;
7036 if cu == Meta as u32 {
7037 str.push(Meta);
7038 if let Some(n) = chars.next() {
7039 str.push(n as u32 as u8);
7040 }
7041 } else if crate::token_char::itok_char(c) {
7042 str.push(cu as u8); // the PUA token's low byte IS the C token byte
7043 } else {
7044 let mut buf = [0u8; 4];
7045 for &b in c.encode_utf8(&mut buf).as_bytes() {
7046 if imeta(b) {
7047 str.push(Meta);
7048 str.push(b ^ 32);
7049 } else {
7050 str.push(b);
7051 }
7052 }
7053 }
7054 }
7055 let untok = |b: u8| -> u8 {
7056 if itok(b) {
7057 ZTOKENS.as_bytes()[(b - Pound as u8) as usize]
7058 } else {
7059 b
7060 }
7061 };
7062 let str2 = str.len();
7063 let mut ccl = [false; 256]; // c:2426 `char ccl[256]`
7064 let mut lastch: i32 = -1; // c:2429
7065 let mut p = 0usize;
7066 while p < str2 {
7067 // c:2433
7068 let mut c1 = untok(str[p]); // c:2434-2435
7069 p += 1;
7070 if c1 == Meta && p < str2 {
7071 c1 = 32 ^ str[p]; // c:2436-2437 `c1 = 32 ^ *p++;`
7072 p += 1;
7073 }
7074 // c:2438-2441 — `if (itok(c2 = *p)) …; if ((char) c2 == Meta) c2 = 32 ^ p[1];`
7075 let mut c2 = str.get(p).map_or(0, |&b| untok(b));
7076 if c2 == Meta {
7077 c2 = 32 ^ str.get(p + 1).copied().unwrap_or(0);
7078 }
7079 // c:2442-2446 — `if (IS_DASH((char)c1) && lastch >= 0 &&
7080 // p < str2 && lastch <= (int)c2)`
7081 if c1 == b'-' && lastch >= 0 && p < str2 && lastch <= c2 as i32 {
7082 while lastch < c2 as i32 {
7083 ccl[lastch as usize] = true; // c:2445 `ccl[lastch++] = 1;`
7084 lastch += 1;
7085 }
7086 lastch = -1; // c:2446
7087 } else {
7088 lastch = c1 as i32; // c:2448 `ccl[lastch = c1] = 1;`
7089 ccl[c1 as usize] = true;
7090 }
7091 }
7092 // c:2452-2466 — walk ccl from the top, inserting each after `last`, so
7093 // the list comes out in ascending byte order.
7094 let mut results = Vec::new();
7095 for c1 in 0..=255u8 {
7096 if !ccl[c1 as usize] {
7097 continue;
7098 }
7099 // c:2455-2463 — `if (imeta(c1)) { str[pl] = Meta; str[pl+1] = c1 ^ 32; }
7100 // else str[pl] = c1;`. In zshrs' representation (see above) an ASCII
7101 // byte, NUL included, is its own char, and any byte >= 0x80 is a
7102 // lone non-UTF-8 byte, held as `Meta` + `byte ^ 32`. Emitting NUL as
7103 // C's Meta pair left a stray Meta char in the word.
7104 let mid = if c1 < 0x80 {
7105 char::from(c1).to_string()
7106 } else {
7107 [char::from(Meta), char::from(c1 ^ 32)].iter().collect()
7108 };
7109 results.push(format!("{}{}{}", prefix, mid, suffix));
7110 }
7111 Some(results)
7112}
7113
7114// ============================================================================
7115// Convenience functions
7116// ============================================================================
7117
7118/// Split a pattern at its trailing zsh-style qualifier suffix. Returns
7119/// `(pattern_without_qualifier, qualifier_inner)` — the inner is the bytes
7120/// between the matching parens, without the surrounding `()` and without
7121/// any leading `#q`. Returns `(pattern, None)` when there is no qualifier
7122/// suffix. Useful for callers that want to use the pattern half with the
7123/// runtime [`matchpat`] (which has no qualifier semantics) while
7124/// reporting or applying the qualifier separately.
7125/// Split a glob pattern into (path-pattern, qualifier-string).
7126/// Port of the qualifier-detection step in `parsepat()`
7127/// (Src/glob.c:791).
7128pub fn split_qualifier(pattern: &str) -> (&str, Option<&str>) {
7129 if !pattern.ends_with(')') {
7130 return (pattern, None);
7131 }
7132 let bytes = pattern.as_bytes();
7133 let mut depth = 0;
7134 for i in (0..bytes.len()).rev() {
7135 match bytes[i] {
7136 b')' => depth += 1,
7137 b'(' => {
7138 depth -= 1;
7139 if depth == 0 {
7140 let inner = &pattern[i + 1..pattern.len() - 1];
7141 let inner = inner.strip_prefix("#q").unwrap_or(inner);
7142 return (&pattern[..i], Some(inner));
7143 }
7144 }
7145 _ => {}
7146 }
7147 }
7148 (pattern, None)
7149}
7150
7151/// Strip the `./` prefix that `read_dir(".")` introduces on relative match
7152/// paths. Rust's `read_dir(".")` yields `entry.path()` like `./foo` while
7153/// `read_dir("foo")` yields `foo/bar` — zsh prints the latter shape for both.
7154/// `scanner` uses base `"."` only for the top-level relative scan (the
7155/// `from_cwd.is_empty()` arm), so that prefix is the ONLY text this port adds
7156/// to a match; a USER-typed leading `./` / `../` is re-prepended from the
7157/// pattern by `globdata_glob`.
7158///
7159/// Everything else is emitted VERBATIM. zsh preserves the literal path text
7160/// the user typed, including empty (`//`) and `.` components:
7161/// ```text
7162/// $ zsh -f -c 'print -r -- sub//f*(N) sub/./f*(N) ./sub//f*(N)'
7163/// sub//foo sub/./foo ./sub//foo
7164/// ```
7165/// This used to rebuild the string via `Path::components()`, which drops
7166/// `CurDir` and never yields empty components — so `sub//foo` came back as
7167/// `sub/foo` and `sub/./foo` as `sub/foo`. That is the same class of bug as
7168/// the `addpath` slash-squeeze (glob.rs:354): `_path_files` anchors its
7169/// matches on the on-line path text, so a rewritten path drops every match
7170/// and completion after `ls sub//` produces nothing at all.
7171fn glob_emit_path(path: &Path) -> String {
7172 let s = path.to_string_lossy();
7173 let mut rest: &str = &s;
7174 while let Some(stripped) = rest.strip_prefix("./") {
7175 rest = stripped;
7176 }
7177 if rest.is_empty() {
7178 ".".to_string()
7179 } else {
7180 rest.to_string()
7181 }
7182}
7183
7184/// Check if path is a symlink
7185/// Check whether a glob match is a symlink.
7186/// Port of the `S_ISLNK(lstat.st_mode)` test in Src/glob.c.
7187pub fn is_symlink(path: &str) -> bool {
7188 fs::symlink_metadata(path)
7189 .map(|m| m.file_type().is_symlink())
7190 .unwrap_or(false)
7191}
7192
7193// ===========================================================
7194// Methods moved verbatim from src/ported/vm_helper because their
7195// C counterpart's source file maps 1:1 to this Rust module.
7196// Phase: glob
7197// ===========================================================
7198
7199// BEGIN moved-from-exec-rs
7200// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
7201
7202// END moved-from-exec-rs
7203
7204// ===========================================================
7205// Methods moved verbatim from src/ported/vm_helper because their
7206// C counterpart's source file maps 1:1 to this Rust module.
7207// Phase: drift
7208// ===========================================================
7209
7210// BEGIN moved-from-exec-rs
7211// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
7212
7213// END moved-from-exec-rs
7214
7215/// !!! RUST-ONLY adapter — NO DIRECT C COUNTERPART !!!
7216///
7217/// Thin "pattern string in, match list out" entry over the glob engine
7218/// for call sites holding a single pattern that want its matches as a
7219/// `Vec<String>`, rather than going through C's in-place
7220/// `zglob(LinkList, LinkNode, int)` list mutation. Snapshots the
7221/// glob-relevant options into TLS (`enter_glob_scope` — required because
7222/// zshrs runs glob on multiple threads; see that fn's doc) and drives
7223/// `globdata_glob`, the RUST-ONLY read_dir-based adaptation of C's
7224/// `scanner()` (Src/glob.c:500 — C descends with `lchdir`, which is
7225/// process-global and unsafe under zshrs's threading, so the port walks
7226/// absolute paths instead). ALL real glob semantics — brace expansion,
7227/// `(a|b)` alternation, `^`/`~` exclusion, qualifiers, `**`, sorting —
7228/// live in `globdata_glob`/`scanner`/`patcompile`, not here. (The 130
7229/// lines of ad-hoc `^`/`~`/alternation read_dir+matchpat code that used
7230/// to live here were verified redundant with `globdata_glob` and
7231/// removed.) The faithful C entry is `zglob` (c:1214).
7232pub fn glob_path(pattern: &str) -> Vec<String> {
7233 let _glob_scope = enter_glob_scope();
7234 // c:1864 `matchct = 0;` — nothing matched yet for this glob.
7235 CURGLOBDATA.lock().unwrap_or_else(|e| e.into_inner()).matchct = 0;
7236 let mut state = globdata::new();
7237 globdata_glob(&mut state, pattern)
7238}
7239
7240// The qualifier-comparison direction static is `g_range` (Src/glob.c, ported at
7241// glob.rs ~2799). The duplicate `G_RANGE` that used to live here was a Rust-only
7242// erroneous second copy — never written by the qual-eval, so qualnlink (its only
7243// reader) was stuck on `==`. Removed; qualnlink now reads `g_range`.
7244
7245// `mode_to_octal` lives at `crate::ported::utils::mode_to_octal`
7246// (port of `Src/utils.c:7634` — 12 bit-by-bit POSIX permission
7247// mappings). Local alias kept for the call sites at c:1610/1618
7248// which used the masked identity before the canonical port landed.
7249fn mode_to_octal(mode: u32) -> u32 {
7250 crate::ported::utils::mode_to_octal(mode) as u32
7251}
7252
7253#[cfg(test)]
7254mod tests {
7255 use super::*;
7256 use std::fs::{self, File};
7257 use tempfile::TempDir;
7258
7259 use crate::ported::options::{opt_state_set, opt_state_unset};
7260 use crate::ported::zsh_h::{redir, ERRFLAG_ERROR, REDIR_WRITE};
7261
7262 /// Convert ASCII brace-expansion source to the lexer-tokenized
7263 /// form `hasbraces` / `xpandbraces` consume per c:Src/glob.c —
7264 /// ASCII `{` → Inbrace (\u{e18f}), `}` → Outbrace (\u{e190}), `,` →
7265 /// Comma (\u{e19a}). Backslash-escaped variants (`\{`, `\}`, `\,`)
7266 /// emit Bnull + literal so the canonical "escape via Bnull"
7267 /// distinction reaches the brace walker. Used by every test in
7268 /// this module that wants to drive the canonical TOKEN-strict
7269 /// path with readable ASCII source.
7270 fn tok(s: &str) -> String {
7271 let mut out = String::with_capacity(s.len());
7272 let mut chars = s.chars().peekable();
7273 while let Some(c) = chars.next() {
7274 match c {
7275 '\\' => match chars.peek() {
7276 Some('{') | Some('}') | Some(',') => {
7277 out.push('\u{e19f}');
7278 out.push(chars.next().unwrap());
7279 }
7280 _ => out.push(c),
7281 },
7282 '{' => out.push('\u{e18f}'),
7283 '}' => out.push('\u{e190}'),
7284 ',' => out.push('\u{e19a}'),
7285 _ => out.push(c),
7286 }
7287 }
7288 out
7289 }
7290
7291 fn setup_test_dir() -> TempDir {
7292 let dir = TempDir::new().unwrap();
7293 let base = dir.path();
7294
7295 // Create test files
7296 File::create(base.join("file1.txt")).unwrap();
7297 File::create(base.join("file2.txt")).unwrap();
7298 File::create(base.join("file3.rs")).unwrap();
7299 File::create(base.join(".hidden")).unwrap();
7300
7301 // Create subdirectory
7302 fs::create_dir(base.join("subdir")).unwrap();
7303 File::create(base.join("subdir/nested.txt")).unwrap();
7304
7305 dir
7306 }
7307
7308 #[test]
7309 fn test_haswilds() {
7310 let _g = crate::test_util::global_state_lock();
7311 // c:Src/glob.c:3548 — full glob tokenize (shadows the
7312 // brace-only `fn tok` test helper above on purpose:
7313 // haswilds consumes glob tokens, not just braces).
7314 let tok = |s: &str| {
7315 let mut t = s.to_string();
7316 tokenize(&mut t);
7317 t
7318 };
7319 assert!(haswilds(&tok("*.txt")));
7320 assert!(haswilds(&tok("file?.txt")));
7321 assert!(haswilds(&tok("file[12].txt")));
7322 assert!(!haswilds(&tok("file.txt")));
7323 assert!(!haswilds(&tok("path/to/file.txt")));
7324 }
7325
7326 #[test]
7327 fn test_pattern_match() {
7328 let _g = crate::test_util::global_state_lock();
7329 /// `**/` fans sibling subdirs out over the rayon pool; the merged result
7330 /// must be the same set a plain recursive walk finds, with dot dirs and
7331 /// non-matching files excluded.
7332 #[test]
7333 fn recursive_glob_parallel_matches_plain_walk() {
7334 let _g = crate::test_util::global_state_lock();
7335 let dir = TempDir::new().unwrap();
7336 let base = dir.path();
7337 let mut expected: Vec<String> = Vec::new();
7338 for a in 0..6 {
7339 for b in 0..4 {
7340 let d = base.join(format!("d{a}/s{b}/leaf"));
7341 fs::create_dir_all(&d).unwrap();
7342 for (parent, tag) in [(d.parent().unwrap().to_path_buf(), "s"), (d.clone(), "l")] {
7343 let hit = parent.join(format!("{tag}.zsh"));
7344 File::create(&hit).unwrap();
7345 File::create(parent.join(format!("{tag}.txt"))).unwrap();
7346 expected.push(hit.to_string_lossy().into_owned());
7347 }
7348 }
7349 }
7350 fs::create_dir_all(base.join(".hid/x")).unwrap();
7351 File::create(base.join(".hid/x/h.zsh")).unwrap();
7352 expected.sort();
7353
7354 let mut pattern = format!("{}/**/*.zsh", base.display());
7355 zshtokenize(&mut pattern, ZSHTOK_SUBST);
7356 let mut state = globdata::new();
7357 let mut got = globdata_glob(&mut state, &pattern);
7358 got.sort();
7359 assert_eq!(got, expected);
7360 }
7361
7362 assert!(matchpat("*.txt", "file.txt", false, true));
7363 assert!(matchpat("file?.txt", "file1.txt", false, true));
7364 assert!(!matchpat("*.txt", "file.rs", false, true));
7365 assert!(matchpat("file[12].txt", "file1.txt", false, true));
7366 assert!(!matchpat("file[12].txt", "file3.txt", false, true));
7367 }
7368
7369 #[test]
7370 fn test_brace_expansion() {
7371 let _g = crate::test_util::global_state_lock();
7372 let result = xpandbraces(&tok("{a,b,c}"), false);
7373 assert_eq!(result, vec!["a", "b", "c"]);
7374
7375 let result = xpandbraces(&tok("file{1,2,3}.txt"), false);
7376 assert_eq!(result, vec!["file1.txt", "file2.txt", "file3.txt"]);
7377
7378 let result = xpandbraces(&tok("{1..5}"), false);
7379 assert_eq!(result, vec!["1", "2", "3", "4", "5"]);
7380
7381 let result = xpandbraces(&tok("{a..e}"), false);
7382 assert_eq!(result, vec!["a", "b", "c", "d", "e"]);
7383 }
7384
7385 /// c:Src/glob.c — an ESCAPED `~` is a literal, never the
7386 /// extendedglob exclusion operator. `shtokenize` (ZSHTOK_SUBST,
7387 /// c:3591) encodes `\X` as `Bnullkeep X` — that is the form every
7388 /// `${~var}` pattern arrives in, including `_path_files`' final
7389 /// `tmp1=( $~tmp1 )` whose patterns were quoted with
7390 /// QT_BACKSLASH_PATTERN by `compfiles` (computil.c:5001). The
7391 /// exclusion splitter recognised only `Bnull`, so it split at the
7392 /// escaped tilde and the glob matched nothing:
7393 /// `cp /etc/paths~or<TAB>` inserted nothing where zsh inserts
7394 /// `/etc/paths\~orig`.
7395 #[test]
7396 fn glob_escaped_tilde_is_literal_not_exclusion() {
7397 let _g = crate::test_util::global_state_lock();
7398 let dir = TempDir::new().unwrap();
7399 let base = dir.path();
7400 File::create(base.join("a~b")).unwrap();
7401 File::create(base.join("aXb")).unwrap();
7402
7403 let saved = opt_state_get("extendedglob");
7404 opt_state_set("extendedglob", true);
7405
7406 // Bnullkeep (`${~var}`), Bnull (plain tokenize) and the raw
7407 // backslash form that `glob_path` callers such as `_path_files`
7408 // hand over untokenized must all be treated as an escape.
7409 for tokflags in [Some(ZSHTOK_SUBST), Some(0), None] {
7410 let mut pattern = format!("{}/a\\~b*", base.display());
7411 if let Some(flags) = tokflags {
7412 zshtokenize(&mut pattern, flags);
7413 }
7414 let mut state = globdata::new();
7415 let results = globdata_glob(&mut state, &pattern);
7416 assert_eq!(
7417 results.len(),
7418 1,
7419 "escaped tilde must match literally (tokflags={tokflags:?}), got {results:?}"
7420 );
7421 assert!(results[0].ends_with("a~b"), "matched {:?}", results[0]);
7422 }
7423
7424 // An UNescaped top-level `~` still excludes.
7425 let mut pattern = format!("{}/a*~*Xb", base.display());
7426 zshtokenize(&mut pattern, ZSHTOK_SUBST);
7427 let mut state = globdata::new();
7428 let results = globdata_glob(&mut state, &pattern);
7429 assert_eq!(results.len(), 1, "exclusion still applies, got {results:?}");
7430 assert!(results[0].ends_with("a~b"), "matched {:?}", results[0]);
7431
7432 match saved {
7433 Some(v) => opt_state_set("extendedglob", v),
7434 None => opt_state_unset("extendedglob"),
7435 }
7436 }
7437
7438 /// c:Src/glob.c:1260-1302 — qualifier groups are peeled off the end one
7439 /// at a time, so `*(#qOn)(.)` applies BOTH. `_path_files` builds exactly
7440 /// this shape for `file-sort` (sh:178, `*(#qom)(-/)` under `-/`) and
7441 /// hands it over untokenized; the untokenized scan took only the last
7442 /// group, leaving `(#qOn)` as pattern text that sorts nothing. zsh 5.9.2:
7443 /// `setopt extendedglob; print *(#qOn)(.)` in {a,b,c,d/} prints `c b a`.
7444 #[test]
7445 fn test_glob_two_qualifier_groups_apply_both() {
7446 let _g = crate::test_util::global_state_lock();
7447 let dir = TempDir::new().unwrap();
7448 let base = dir.path();
7449 for f in ["a", "b", "c"] {
7450 File::create(base.join(f)).unwrap();
7451 }
7452 fs::create_dir(base.join("d")).unwrap();
7453
7454 let saved = opt_state_get("extendedglob");
7455 opt_state_set("extendedglob", true);
7456 let mut got = Vec::new();
7457 for tokflags in [Some(ZSHTOK_SUBST), None] {
7458 let mut pattern = format!("{}/*(#qOn)(.)", base.display());
7459 if let Some(flags) = tokflags {
7460 zshtokenize(&mut pattern, flags);
7461 }
7462 let mut state = globdata::new();
7463 let names: Vec<String> = globdata_glob(&mut state, &pattern)
7464 .iter()
7465 .map(|p| p.rsplit('/').next().unwrap_or("").to_string())
7466 .collect();
7467 got.push((tokflags, names));
7468 }
7469 match saved {
7470 Some(v) => opt_state_set("extendedglob", v),
7471 None => opt_state_unset("extendedglob"),
7472 }
7473 for (tokflags, names) in got {
7474 assert_eq!(names, vec!["c", "b", "a"], "tokflags={tokflags:?}");
7475 }
7476 }
7477
7478 #[test]
7479 fn test_glob_simple() {
7480 let _g = crate::test_util::global_state_lock();
7481 let dir = setup_test_dir();
7482 let pattern = format!("{}/*.txt", dir.path().display());
7483
7484 let mut state = globdata::new();
7485 let results = globdata_glob(&mut state, &pattern);
7486
7487 assert_eq!(results.len(), 2);
7488 assert!(results.iter().any(|s| s.ends_with("file1.txt")));
7489 assert!(results.iter().any(|s| s.ends_with("file2.txt")));
7490 }
7491
7492 #[test]
7493 fn test_glob_hidden() {
7494 let _g = crate::test_util::global_state_lock();
7495 let dir = setup_test_dir();
7496 let pattern = format!("{}/*", dir.path().display());
7497
7498 // Default (globdots off) → hidden files skipped. `dotglob`
7499 // is the bash alias for zsh's canonical `globdots` option;
7500 // the C-faithful read goes through `isset(GLOBDOTS)` which
7501 // resolves the canonical name.
7502 opt_state_set("globdots", false);
7503 let mut state = globdata::new();
7504 let results = globdata_glob(&mut state, &pattern);
7505 assert!(!results.iter().any(|s| s.contains(".hidden")));
7506
7507 // setopt globdots → hidden files included.
7508 opt_state_set("globdots", true);
7509 let mut state = globdata::new();
7510 let results = globdata_glob(&mut state, &pattern);
7511 assert!(results.iter().any(|s| s.contains(".hidden")));
7512 opt_state_set("globdots", false); // reset for other tests
7513 }
7514
7515 #[test]
7516 fn test_glob_emit_path_strips_read_dir_dot_slash() {
7517 let _g = crate::test_util::global_state_lock();
7518 assert_eq!(glob_emit_path(Path::new("./sub")), "sub");
7519 assert_eq!(glob_emit_path(Path::new("sub/deeper")), "sub/deeper");
7520 assert_eq!(glob_emit_path(Path::new("././x")), "x");
7521 assert_eq!(glob_emit_path(Path::new("../up")), "../up");
7522 // Empty path (`Path::new("")`) → "." as before.
7523 assert_eq!(glob_emit_path(Path::new("")), ".");
7524 assert_eq!(glob_emit_path(Path::new("./")), ".");
7525 }
7526
7527 /// Beyond the leading `read_dir(".")` prefix, `glob_emit_path` must emit
7528 /// the path VERBATIM — it must not re-normalize it through
7529 /// `Path::components()`, which silently drops empty (`//`) and `.`
7530 /// components. zsh keeps the literal text the user typed:
7531 /// ```text
7532 /// $ zsh -f -c 'print -r -- sub//f*(N) sub/./f*(N) ./sub//f*(N)'
7533 /// sub//foo sub/./foo ./sub//foo
7534 /// ```
7535 /// `_path_files` anchors its matches on that text, so any rewrite drops
7536 /// every match and completion after `ls sub//` yields nothing at all.
7537 #[test]
7538 fn test_glob_emit_path_keeps_empty_and_dot_components_verbatim() {
7539 let _g = crate::test_util::global_state_lock();
7540 // Empty components survive (`//`), relative and absolute.
7541 assert_eq!(glob_emit_path(Path::new("sub//foo")), "sub//foo");
7542 assert_eq!(glob_emit_path(Path::new("//usr//bin")), "//usr//bin");
7543 // Interior `.` / `..` survive.
7544 assert_eq!(glob_emit_path(Path::new("sub/./foo")), "sub/./foo");
7545 assert_eq!(glob_emit_path(Path::new("/usr/./bin")), "/usr/./bin");
7546 assert_eq!(glob_emit_path(Path::new("a/..//a/b")), "a/..//a/b");
7547 // Only the engine-introduced leading `./` is removed; what follows
7548 // it is untouched (globdata_glob re-prepends a user-typed `./`).
7549 assert_eq!(glob_emit_path(Path::new("./sub//foo")), "sub//foo");
7550 // Trailing separator is preserved (previously dropped).
7551 assert_eq!(glob_emit_path(Path::new("sub/")), "sub/");
7552 }
7553
7554 /// `Src/glob.c:270-273` — `addpath` appends the component then a `/`
7555 /// UNCONDITIONALLY. An empty component (`l == 0`, which is exactly
7556 /// what a `//` in the pattern produces, since `patcompile` with
7557 /// `PAT_FILET` cuts a pure section at the first `/`) must therefore
7558 /// still push one `/`. A `if !s.ends_with('/')` guard used to live
7559 /// here and silently collapsed `//` → `/`.
7560 #[test]
7561 fn addpath_appends_slash_unconditionally() {
7562 // Empty component after an existing separator: C writes nothing
7563 // from `s`, then writes '/', so `/` becomes `//`.
7564 let mut p = String::from("/");
7565 addpath(&mut p, "");
7566 assert_eq!(p, "//");
7567
7568 // Two empty components in a row (pattern `///x`).
7569 addpath(&mut p, "");
7570 assert_eq!(p, "///");
7571
7572 // Ordinary components are unaffected — a non-empty component can
7573 // never itself end in `/` (patcompile cut the section there).
7574 let mut q = String::from("/");
7575 addpath(&mut q, "usr");
7576 assert_eq!(q, "/usr/");
7577 addpath(&mut q, "");
7578 assert_eq!(q, "/usr//");
7579 addpath(&mut q, "local");
7580 assert_eq!(q, "/usr//local/");
7581
7582 // Relative start (`pathbuf` empty per c:818).
7583 let mut r = String::new();
7584 addpath(&mut r, "a");
7585 assert_eq!(r, "a/");
7586 addpath(&mut r, ".");
7587 assert_eq!(r, "a/./");
7588 addpath(&mut r, "..");
7589 assert_eq!(r, "a/./../");
7590 }
7591
7592 /// End-to-end: the globber must reproduce the literal path text the
7593 /// user typed, including empty (`//`) and `.` / `..` components.
7594 /// `_path_files` anchors its completion matches on that exact text
7595 /// (`${(@)tmp1#$prepath$realpath$testpath}`), so any squeezing of the
7596 /// path discards every match and completion after `ls //usr//`
7597 /// produces nothing at all.
7598 ///
7599 /// zsh reference:
7600 /// ```text
7601 /// $ zsh -f -c 'print -r -- //usr//b*(N) /usr/./b*(N)'
7602 /// //usr//bin /usr/./bin
7603 /// ```
7604 #[test]
7605 fn glob_preserves_empty_and_dot_path_components() {
7606 let _g = crate::test_util::global_state_lock();
7607 let dir = setup_test_dir();
7608 let base = dir.path().display().to_string();
7609
7610 // `//` between the directory and the wildcard component.
7611 let mut state = globdata::new();
7612 let results = globdata_glob(&mut state, &format!("{}//file1.tx?", base));
7613 assert_eq!(results, vec![format!("{}//file1.txt", base)]);
7614
7615 // `/./` — a non-empty component, already preserved; pinned so a
7616 // future "normalize the path" change can't quietly drop it.
7617 let mut state = globdata::new();
7618 let results = globdata_glob(&mut state, &format!("{}/./file1.tx?", base));
7619 assert_eq!(results, vec![format!("{}/./file1.txt", base)]);
7620
7621 // `/..//` — `..` followed by an empty component, round-tripped
7622 // verbatim even though it resolves back to the same directory.
7623 let mut state = globdata::new();
7624 let results = globdata_glob(
7625 &mut state,
7626 &format!("{}/subdir/..//subdir/nested.tx?", base),
7627 );
7628 assert_eq!(
7629 results,
7630 vec![format!("{}/subdir/..//subdir/nested.txt", base)]
7631 );
7632
7633 // Leading `//` on an absolute pattern (`parsepat` c:813-816 strips
7634 // exactly ONE leading `/`; the second becomes an empty component).
7635 let mut state = globdata::new();
7636 let results = globdata_glob(&mut state, &format!("/{}//file2.tx?", base));
7637 assert_eq!(results, vec![format!("/{}//file2.txt", base)]);
7638 }
7639
7640 #[test]
7641 fn test_file_type_char() {
7642 let _g = crate::test_util::global_state_lock();
7643 assert_eq!(file_type(libc::S_IFDIR as u32), '/');
7644 assert_eq!(file_type(libc::S_IFREG as u32), ' ');
7645 assert_eq!(file_type(libc::S_IFREG as u32 | 0o111), '*');
7646 assert_eq!(file_type(libc::S_IFLNK as u32), '@');
7647 }
7648
7649 /// `Src/glob.c:2018-2036` — `file_type(mode_t filemode)` returns a
7650 /// single-char marker keyed off S_ISBLK/CHR/DIR/FIFO/LNK/REG/SOCK,
7651 /// with `S_IXUGO` (== 0o111 = S_IXUSR|S_IXGRP|S_IXOTH) distinguishing
7652 /// executable regular files (`*`) from non-executable (` `). The
7653 /// catch-all (e.g. door, port, unknown types) returns `?`.
7654 /// Pin every branch by position.
7655 #[test]
7656 fn file_type_every_branch_matches_c_dispatch() {
7657 let _g = crate::test_util::global_state_lock();
7658 assert_eq!(
7659 file_type(libc::S_IFBLK as u32),
7660 '#',
7661 "c:2020 — S_ISBLK → '#'"
7662 );
7663 assert_eq!(
7664 file_type(libc::S_IFCHR as u32),
7665 '%',
7666 "c:2022 — S_ISCHR → '%'"
7667 );
7668 assert_eq!(
7669 file_type(libc::S_IFDIR as u32),
7670 '/',
7671 "c:2024 — S_ISDIR → '/'"
7672 );
7673 assert_eq!(
7674 file_type(libc::S_IFIFO as u32),
7675 '|',
7676 "c:2026 — S_ISFIFO → '|'"
7677 );
7678 assert_eq!(
7679 file_type(libc::S_IFLNK as u32),
7680 '@',
7681 "c:2028 — S_ISLNK → '@'"
7682 );
7683 // Regular file: ' ' if not executable, '*' if executable (any bit in S_IXUGO).
7684 assert_eq!(
7685 file_type(libc::S_IFREG as u32),
7686 ' ',
7687 "c:2030 — non-executable regular file → ' '"
7688 );
7689 // Each individual exec bit triggers '*'.
7690 assert_eq!(
7691 file_type(libc::S_IFREG as u32 | 0o100),
7692 '*',
7693 "c:2030 — S_IXUSR alone is enough"
7694 );
7695 assert_eq!(
7696 file_type(libc::S_IFREG as u32 | 0o010),
7697 '*',
7698 "c:2030 — S_IXGRP alone is enough"
7699 );
7700 assert_eq!(
7701 file_type(libc::S_IFREG as u32 | 0o001),
7702 '*',
7703 "c:2030 — S_IXOTH alone is enough"
7704 );
7705 assert_eq!(
7706 file_type(libc::S_IFSOCK as u32),
7707 '=',
7708 "c:2033 — S_ISSOCK → '='"
7709 );
7710 // Catch-all for unknown S_IFMT — bare 0 returns '?'.
7711 assert_eq!(file_type(0), '?', "c:2035 — unknown st_mode → '?'");
7712 }
7713
7714 #[test]
7715 fn test_zstrcmp_numeric() {
7716 let _g = crate::test_util::global_state_lock();
7717 let n = crate::zsh_h::SORTIT_NUMERICALLY as u32;
7718 assert_eq!(zstrcmp("file1", "file2", n), std::cmp::Ordering::Less);
7719 assert_eq!(zstrcmp("file10", "file2", n), std::cmp::Ordering::Greater);
7720 assert_eq!(zstrcmp("file10", "file10", n), std::cmp::Ordering::Equal);
7721 }
7722
7723 /// Race-fix verification: snapshot pins bareglobqual for the
7724 /// duration of a glob scope even when the live store flips
7725 /// underneath. Mimics the stryke pattern in
7726 /// MenkeTechnologies/strykelang#3 — one thread is mid-glob with
7727 /// bareglobqual=1 snapshot when another flips it off.
7728 #[test]
7729 fn glob_opts_snapshot_isolates_concurrent_setopt() {
7730 let _g = crate::test_util::global_state_lock();
7731
7732 // Preserve the existing live state so the test is hermetic.
7733 let saved = opt_state_get("bareglobqual");
7734
7735 // Live store says bareglobqual=true; enter scope captures that.
7736 opt_state_set("bareglobqual", true);
7737 let _scope = enter_glob_scope();
7738 assert!(
7739 glob_isset(BAREGLOBQUAL),
7740 "TLS snapshot reads bareglobqual=true at scope entry"
7741 );
7742
7743 // Simulate a concurrent thread flipping the live store.
7744 opt_state_set("bareglobqual", false);
7745 assert!(
7746 glob_isset(BAREGLOBQUAL),
7747 "TLS snapshot must still report bareglobqual=true \
7748 even though live store now reads false"
7749 );
7750
7751 // Restore.
7752 match saved {
7753 Some(v) => opt_state_set("bareglobqual", v),
7754 None => opt_state_unset("bareglobqual"),
7755 }
7756 }
7757
7758 /// After the scope guard drops, reads fall back to the live store.
7759 #[test]
7760 fn glob_opts_snapshot_clears_on_drop() {
7761 let _g = crate::test_util::global_state_lock();
7762
7763 let saved = opt_state_get("nullglob");
7764 opt_state_set("nullglob", true);
7765 {
7766 let _scope = enter_glob_scope();
7767 assert!(glob_isset(NULLGLOB), "snapshot live=true → true inside");
7768 }
7769 // Outside scope: flip live store, read should follow live.
7770 opt_state_set("nullglob", false);
7771 assert!(
7772 !glob_isset(NULLGLOB),
7773 "post-scope: glob_isset falls back to live store"
7774 );
7775
7776 match saved {
7777 Some(v) => opt_state_set("nullglob", v),
7778 None => opt_state_unset("nullglob"),
7779 }
7780 }
7781
7782 /// Nested glob scopes share the outer snapshot — inner doesn't
7783 /// re-capture or clear on its own Drop.
7784 #[test]
7785 fn glob_opts_snapshot_nested_is_noop() {
7786 let _g = crate::test_util::global_state_lock();
7787
7788 let saved = opt_state_get("extendedglob");
7789 opt_state_set("extendedglob", true);
7790 let _outer = enter_glob_scope();
7791 assert!(glob_isset(EXTENDEDGLOB));
7792 {
7793 let _inner = enter_glob_scope();
7794 // Live store flip while nested.
7795 opt_state_set("extendedglob", false);
7796 assert!(glob_isset(EXTENDEDGLOB), "inner observes outer snapshot");
7797 } // inner drops — outer snapshot still active.
7798 assert!(
7799 glob_isset(EXTENDEDGLOB),
7800 "outer snapshot survives inner drop"
7801 );
7802
7803 match saved {
7804 Some(v) => opt_state_set("extendedglob", v),
7805 None => opt_state_unset("extendedglob"),
7806 }
7807 }
7808
7809 /// c:2150 (xpandredir port) — when the redir name has no wildcard
7810 /// AND no `$var`/`!hist`/`{a,b}` to expand, prefork+globlist leave
7811 /// the name unchanged. The single-match path at c:2171 must
7812 /// rewrite `fn.name` to the same string and return 0 (no multi-fan).
7813 /// Regression that returns 1 would trigger MULTIOS dispatch on a
7814 /// single literal filename — wrong shell semantics.
7815 #[test]
7816 fn xpandredir_single_literal_filename_returns_zero() {
7817 let _g = crate::test_util::global_state_lock();
7818 let mut fn_ = redir {
7819 typ: REDIR_WRITE,
7820 flags: 0,
7821 fd1: 1,
7822 fd2: -1,
7823 name: Some("/tmp/zshrs_test_out".to_string()),
7824 varid: None,
7825 here_terminator: None,
7826 munged_here_terminator: None,
7827 };
7828 let mut tab: Vec<redir> = Vec::new();
7829 let r = xpandredir(&mut fn_, &mut tab);
7830 assert_eq!(r, 0, "literal filename → single match → ret=0");
7831 assert_eq!(
7832 fn_.name.as_deref(),
7833 Some("/tmp/zshrs_test_out"),
7834 "literal name must round-trip through prefork unchanged"
7835 );
7836 assert!(tab.is_empty(), "no multi-match → redirtab not appended");
7837 }
7838
7839 /// c:2176-2177 — `>&-` (REDIR_MERGEOUT with name "-") collapses to
7840 /// REDIR_CLOSE per the `IS_DASH(s[0]) && !s[1]` branch. Regression
7841 /// where this fails leaves `>&-` as a literal merge-fd attempt,
7842 /// which the executor would interpret as "merge with fd -1".
7843 #[test]
7844 fn xpandredir_dash_merge_collapses_to_close() {
7845 let _g = crate::test_util::global_state_lock();
7846 let mut fn_ = redir {
7847 typ: REDIR_MERGEOUT,
7848 flags: 0,
7849 fd1: 1,
7850 fd2: -1,
7851 name: Some("-".to_string()),
7852 varid: None,
7853 here_terminator: None,
7854 munged_here_terminator: None,
7855 };
7856 let mut tab: Vec<redir> = Vec::new();
7857 let _ = xpandredir(&mut fn_, &mut tab);
7858 assert_eq!(
7859 fn_.typ, REDIR_CLOSE,
7860 "`>&-` must rewrite typ to REDIR_CLOSE"
7861 );
7862 }
7863
7864 /// c:2150 — empty `fn.name` should return 0 cleanly. Catches a
7865 /// regression that panics on `.as_deref().unwrap()` for absent name.
7866 #[test]
7867 fn xpandredir_with_no_name_returns_zero_no_panic() {
7868 let _g = crate::test_util::global_state_lock();
7869 let mut fn_ = redir {
7870 typ: REDIR_WRITE,
7871 flags: 0,
7872 fd1: 1,
7873 fd2: -1,
7874 name: None,
7875 varid: None,
7876 here_terminator: None,
7877 munged_here_terminator: None,
7878 };
7879 let mut tab: Vec<redir> = Vec::new();
7880 assert_eq!(xpandredir(&mut fn_, &mut tab), 0);
7881 }
7882
7883 /// `IN_EXPANDREDIR` static defaults to 0 — set transiently inside
7884 /// xpandredir per c:2165/2167. After a normal call it must restore
7885 /// to 0. Regression that leaks the flag would skew unrelated glob
7886 /// expansions outside redirections.
7887 #[test]
7888 fn in_expandredir_flag_is_zero_at_rest() {
7889 let _g = crate::test_util::global_state_lock();
7890 assert_eq!(IN_EXPANDREDIR.load(Ordering::SeqCst), 0);
7891 }
7892
7893 /// c:4306 — `haswilds` honours backslash-escapes: `\*` is a literal
7894 /// star, NOT a wildcard. Regression that ignores the escape would
7895 /// trigger globbing on `printf '%s\n' \*`, breaking shell scripts
7896 /// that quote literal stars.
7897 #[test]
7898 fn haswilds_respects_backslash_escape() {
7899 let _g = crate::test_util::global_state_lock();
7900 // c:Src/glob.c:3548 — full glob tokenize (shadows the
7901 // brace-only `fn tok` test helper above on purpose:
7902 // haswilds consumes glob tokens, not just braces).
7903 let tok = |s: &str| {
7904 let mut t = s.to_string();
7905 tokenize(&mut t);
7906 t
7907 };
7908 assert!(haswilds(&tok("*.txt")), "bare * is wild");
7909 assert!(
7910 !haswilds(&tok(r"\*.txt")),
7911 "escaped \\* is literal — NOT wild"
7912 );
7913 assert!(
7914 !haswilds(&tok(r"\?.txt")),
7915 "escaped \\? is literal — NOT wild"
7916 );
7917 }
7918
7919 /// c:4306 — `[` immediately enters bracket mode AND counts as a
7920 /// wildcard. The early return on `[` is critical — even
7921 /// unterminated brackets must be flagged so `cd [` doesn't try
7922 /// to chdir to a literal `[`.
7923 #[test]
7924 fn haswilds_open_bracket_alone_is_a_wildcard() {
7925 let _g = crate::test_util::global_state_lock();
7926 // c:Src/glob.c:3548 — full glob tokenize (shadows the
7927 // brace-only `fn tok` test helper above on purpose:
7928 // haswilds consumes glob tokens, not just braces).
7929 let tok = |s: &str| {
7930 let mut t = s.to_string();
7931 tokenize(&mut t);
7932 t
7933 };
7934 assert!(haswilds(&tok("[abc]")), "char-class is wild");
7935 assert!(haswilds(&tok("foo[")), "even unterminated [ is wild");
7936 }
7937
7938 /// c:4306 — wildcard chars `*` `?` inside an OPEN bracket-context
7939 /// are NOT additional wildcards (the bracket already is one).
7940 /// Regression that double-counts would make haswilds report `[*]`
7941 /// as wildcard-twice — cosmetic, but confuses any caller using
7942 /// the bool as a "should I glob?" gate that AND-tests with another
7943 /// flag.
7944 #[test]
7945 fn haswilds_extglob_chars_inside_bracket_dont_double_count() {
7946 let _g = crate::test_util::global_state_lock();
7947 // c:Src/glob.c:3548 — full glob tokenize (shadows the
7948 // brace-only `fn tok` test helper above on purpose:
7949 // haswilds consumes glob tokens, not just braces).
7950 let tok = |s: &str| {
7951 let mut t = s.to_string();
7952 tokenize(&mut t);
7953 t
7954 };
7955 // Once `[` is seen, function returns true immediately, so the
7956 // post-bracket chars don't matter. But this docs the contract.
7957 assert!(haswilds(&tok("[*]")));
7958 }
7959
7960 /// c:4306 — plain text returns false. Catches a regression where
7961 /// any non-empty input is flagged as wild (would break `cd /tmp`
7962 /// by triggering glob expansion on a literal path).
7963 #[test]
7964 fn haswilds_plain_text_not_wild() {
7965 let _g = crate::test_util::global_state_lock();
7966 // c:Src/glob.c:3548 — full glob tokenize (shadows the
7967 // brace-only `fn tok` test helper above on purpose:
7968 // haswilds consumes glob tokens, not just braces).
7969 let tok = |s: &str| {
7970 let mut t = s.to_string();
7971 tokenize(&mut t);
7972 t
7973 };
7974 assert!(!haswilds(&tok("plain")));
7975 assert!(!haswilds(&tok("")));
7976 assert!(!haswilds(&tok("/usr/local/bin")));
7977 assert!(!haswilds(&tok("file.txt")));
7978 }
7979
7980 /// c:4363-4371 — `#` and `^` are recognised as wildcards by
7981 /// `haswilds` **only when `EXTENDEDGLOB` is set**, matching the C
7982 /// source's `isset(EXTENDEDGLOB)` gate at pattern.c:4364/4369. `~`
7983 /// is **not** a filename-generation wildcard (zsh handles it as
7984 /// tilde expansion in a separate pipeline stage); a prior
7985 /// glob.rs-local `haswilds` impl returned true for `~`, which
7986 /// caused zglob to mis-classify tilde-prefixed args as needing
7987 /// filename generation. Pin the corrected behavior.
7988 #[test]
7989 fn haswilds_extended_glob_chars_recognised() {
7990 let _g = crate::test_util::global_state_lock();
7991 // c:Src/glob.c:3548 — full glob tokenize (shadows the
7992 // brace-only `fn tok` test helper above on purpose:
7993 // haswilds consumes glob tokens, not just braces).
7994 let tok = |s: &str| {
7995 let mut t = s.to_string();
7996 tokenize(&mut t);
7997 t
7998 };
7999 // EXTENDEDGLOB off → `#` and `^` are not wild.
8000 crate::ported::options::opt_state_set("extendedglob", false);
8001 assert!(
8002 !haswilds(&tok("foo#bar")),
8003 "# not wild without EXTENDEDGLOB"
8004 );
8005 assert!(
8006 !haswilds(&tok("foo^bar")),
8007 "^ not wild without EXTENDEDGLOB"
8008 );
8009 // EXTENDEDGLOB on → `#` and `^` are wild (c:4364, c:4369).
8010 crate::ported::options::opt_state_set("extendedglob", true);
8011 assert!(haswilds(&tok("foo#bar")), "# is extglob wild");
8012 assert!(haswilds(&tok("foo^bar")), "^ is extglob wild");
8013 crate::ported::options::opt_state_set("extendedglob", false);
8014 // `~` is NOT in haswilds' switch (c:4324-4373) — tilde expansion
8015 // is a separate pipeline stage.
8016 assert!(
8017 !haswilds(&tok("~/file")),
8018 "~ is NOT a filename-generation wildcard"
8019 );
8020 }
8021
8022 /// c:2514 — `matchpat` returns true for exact match. Sanity check
8023 /// the simplest dispatch path (the matcher recipe builds a
8024 /// trivial pattern, runs it).
8025 #[test]
8026 fn matchpat_exact_literal_matches() {
8027 let _g = crate::test_util::global_state_lock();
8028 assert!(matchpat("hello", "hello", false, true));
8029 assert!(!matchpat("hello", "world", false, true));
8030 }
8031
8032 /// c:2514 — `matchpat` with case_sensitive=false MUST treat upper
8033 /// and lower as equal (`HELLO` matches `hello`). Regression keeping
8034 /// case-sensitive when flag is false would silently break every
8035 /// `[[ "$x" = (#i)foo ]]`-style match.
8036 #[test]
8037 fn matchpat_case_insensitive_when_flag_clear() {
8038 let _g = crate::test_util::global_state_lock();
8039 assert!(
8040 matchpat("hello", "HELLO", false, false),
8041 "case-insensitive match must succeed across cases"
8042 );
8043 assert!(matchpat("FoO", "foo", false, false));
8044 }
8045
8046 /// c:2514 — case-sensitive (default) MUST reject case-different
8047 /// inputs. Pinning the contract.
8048 #[test]
8049 fn matchpat_case_sensitive_rejects_case_different() {
8050 let _g = crate::test_util::global_state_lock();
8051 assert!(
8052 !matchpat("hello", "HELLO", false, true),
8053 "case-sensitive default must reject HELLO != hello"
8054 );
8055 }
8056
8057 /// c:2780 — `set_pat_start(p, offs)` sets `PAT_NOTSTART` when offs is
8058 /// nonzero (matched substring starts past the real start, so `(#s)`
8059 /// must fail) and clears it when offs is 0. A regression here would
8060 /// let `(#s)` anchors fire mid-string during substring globbing.
8061 #[test]
8062 fn set_pat_start_toggles_pat_notstart_flag() {
8063 let _g = crate::test_util::global_state_lock();
8064 let mut p = mk_test_patprog();
8065 set_pat_start(&mut p, 2);
8066 assert!(
8067 p.0.flags & PAT_NOTSTART != 0,
8068 "offs!=0 must set PAT_NOTSTART"
8069 );
8070 set_pat_start(&mut p, 0);
8071 assert!(
8072 p.0.flags & PAT_NOTSTART == 0,
8073 "offs==0 must clear PAT_NOTSTART"
8074 );
8075 }
8076
8077 /// c:2796 — `set_pat_end(p, null_me)` sets `PAT_NOTEND` when the char
8078 /// being zapped is non-NUL (string shortened at tail, so `(#e)` must
8079 /// fail) and clears it when that char is already NUL. A regression
8080 /// would let `(#e)` anchors fire before the real end.
8081 #[test]
8082 fn set_pat_end_toggles_pat_notend_flag() {
8083 let _g = crate::test_util::global_state_lock();
8084 let mut p = mk_test_patprog();
8085 set_pat_end(&mut p, b'x');
8086 assert!(
8087 p.0.flags & PAT_NOTEND != 0,
8088 "non-NUL null_me must set PAT_NOTEND"
8089 );
8090 set_pat_end(&mut p, 0);
8091 assert!(
8092 p.0.flags & PAT_NOTEND == 0,
8093 "NUL null_me must clear PAT_NOTEND"
8094 );
8095 }
8096
8097 /// Build a zeroed `Patprog` for flag-toggle tests.
8098 fn mk_test_patprog() -> Patprog {
8099 Box::new((
8100 crate::ported::zsh_h::patprog {
8101 startoff: 0,
8102 size: 0,
8103 mustoff: 0,
8104 patmlen: 0,
8105 globflags: 0,
8106 globend: 0,
8107 flags: 0,
8108 patnpar: 0,
8109 patstartch: 0,
8110 },
8111 Vec::new(),
8112 ))
8113 }
8114
8115 /// c:2773 — `freematchlist(None)` is a no-op (matches C's
8116 /// `if (repllist) freelinklist(...)`). Regression panicking on
8117 /// None would crash every glob-replace path with no matches.
8118 #[test]
8119 fn freematchlist_handles_none_safely() {
8120 let _g = crate::test_util::global_state_lock();
8121 freematchlist(None);
8122 // No assertion — survival is the test.
8123 }
8124
8125 /// c:2773 — `freematchlist(Some(&mut Vec))` clears the list.
8126 /// Regression that drops a stale entry would leak match positions
8127 /// across calls.
8128 #[test]
8129 fn freematchlist_clears_provided_vec() {
8130 let _g = crate::test_util::global_state_lock();
8131 let mut v = vec![
8132 repldata {
8133 b: 0,
8134 e: 5,
8135 replstr: None,
8136 },
8137 repldata {
8138 b: 10,
8139 e: 15,
8140 replstr: Some("x".to_string()),
8141 },
8142 ];
8143 freematchlist(Some(&mut v));
8144 assert!(v.is_empty(), "freematchlist must clear the input vec");
8145 }
8146
8147 /// `Src/glob.c:2042-2142` — `hasbraces(str)` returns true when
8148 /// `str` contains a brace-expansion candidate. A bare lbrace/rbrace
8149 /// is NOT a brace expansion (it's a literal). Detection requires a
8150 /// matched pair containing either a comma or a dotdot range. Pin
8151 /// the canonical contract.
8152 #[test]
8153 fn hasbraces_matched_pair_with_comma_or_dotdot() {
8154 let _g = crate::test_util::global_state_lock();
8155 // Matched + comma → true.
8156 assert!(
8157 hasbraces(&tok("a{b,c}d"), false),
8158 "c:2127 — lbrace + comma + rbrace is a brace expansion"
8159 );
8160 // Matched + dotdot → true.
8161 assert!(
8162 hasbraces(&tok("file{1..3}.txt"), false),
8163 "c:2082 — N..M range is a brace expansion"
8164 );
8165 // Matched WITHOUT comma/dotdot → false (it's a literal).
8166 assert!(
8167 !hasbraces(&tok("{abc}"), false),
8168 "literal braces are NOT a brace expansion without comma or dotdot"
8169 );
8170 // Unmatched → false.
8171 assert!(
8172 !hasbraces(&tok("{abc"), false),
8173 "lone lbrace without matching rbrace is not brace expansion"
8174 );
8175 assert!(
8176 !hasbraces(&tok("abc}"), false),
8177 "lone rbrace is not brace expansion"
8178 );
8179 // No braces → false.
8180 assert!(!hasbraces(&tok("plain"), false));
8181 assert!(!hasbraces(&tok(""), false));
8182 }
8183
8184 /// `Src/glob.c:2049-2063` — BRACECCL branch: when `isset(BRACECCL)`
8185 /// (the `brace_ccl` parameter), any non-empty matched braces
8186 /// become a character-class set, regardless of whether the body
8187 /// contains a comma. Empty pair is still not.
8188 #[test]
8189 fn hasbraces_brace_ccl_makes_any_pair_match() {
8190 let _g = crate::test_util::global_state_lock();
8191 // c:2049 — BRACECCL: non-empty pair is enough.
8192 assert!(
8193 hasbraces(&tok("{abc}"), true),
8194 "c:2049 — BRACECCL: non-empty pair is char-class set"
8195 );
8196 assert!(
8197 hasbraces(&tok("x{q}y"), true),
8198 "c:2049 — single-char pair counts under BRACECCL"
8199 );
8200 // Empty pair shouldn't trigger.
8201 assert!(
8202 !hasbraces(&tok("{}"), true),
8203 "empty pair still not a brace expansion even under BRACECCL"
8204 );
8205 // Without BRACECCL, plain literal-letter pair is NOT a brace expansion.
8206 assert!(
8207 !hasbraces(&tok("{abc}"), false),
8208 "c:2049 — BRACECCL off → plain literal pair stays literal"
8209 );
8210 }
8211
8212 /// `Src/glob.c:2042-2142` — depth tracking: comma/dotdot count
8213 /// only at depth==1 in our port. So nested inner comma doesn't
8214 /// qualify the outer pair, but TWO independent top-level pairs
8215 /// (each with comma) DO trigger detection.
8216 #[test]
8217 fn hasbraces_depth_1_check_for_comma_dotdot() {
8218 let _g = crate::test_util::global_state_lock();
8219 assert!(
8220 hasbraces(&tok("a{1,2}b{3,4}c"), false),
8221 "two independent top-level pairs, first one matches at depth 1"
8222 );
8223 }
8224
8225 /// Pin: `remnulargs` per `Src/glob.c:3649-3679`:
8226 /// - Strings with NO inull bytes are unchanged.
8227 /// - Strings with ONLY Bnullkeep: keep as-is in scan phase
8228 /// (Bnullkeep is "active backslash" that hasn't triggered
8229 /// copy phase yet).
8230 /// - After encountering any other inull: switch to copy phase:
8231 /// * Bnullkeep → '\\' (literal backslash).
8232 /// * Other inulls (Snull/Dnull/Bnull/Nularg) → stripped.
8233 /// * Non-inull → kept.
8234 /// - Empty post-strip → replaced with single Nularg.
8235 #[test]
8236 fn remnulargs_matches_c_inull_handling() {
8237 let _g = crate::test_util::global_state_lock();
8238
8239 // Plain ASCII unchanged (no inulls).
8240 let mut s = "hello".to_string();
8241 remnulargs(&mut s);
8242 assert_eq!(
8243 s, "hello",
8244 "c:3654 — no inull bytes leaves string unchanged"
8245 );
8246
8247 // Snull-triggered copy: Snull stripped, rest kept.
8248 let mut s = format!("ab{}cd", Snull);
8249 remnulargs(&mut s);
8250 assert_eq!(
8251 s, "abcd",
8252 "c:3663 — Snull triggers copy; itself stripped, rest kept"
8253 );
8254
8255 // Bnullkeep AFTER Snull trigger → '\\' (active backslash).
8256 let mut s = format!("ab{}c{}d", Snull, Bnullkeep);
8257 remnulargs(&mut s);
8258 assert_eq!(
8259 s, "abc\\d",
8260 "c:3666 — Bnullkeep in copy phase becomes literal '\\\\'"
8261 );
8262
8263 // Other inulls (Bnull, Dnull) stripped in copy phase.
8264 let mut s = format!("a{}b{}c{}d", Snull, Bnull, Dnull);
8265 remnulargs(&mut s);
8266 assert_eq!(s, "abcd", "c:3668 — Bnull/Dnull stripped in copy phase");
8267
8268 // Empty post-strip → Nularg.
8269 let mut s = format!("{}", Snull);
8270 remnulargs(&mut s);
8271 assert_eq!(
8272 s,
8273 format!("{}", Nularg),
8274 "c:3674 — empty result replaced by Nularg sentinel"
8275 );
8276 }
8277
8278 /// `Src/glob.c:1085-1117` — `glob_exec_string` is a PARSER, not
8279 /// an executor. Extracts the qualifier inner text from `*sp`
8280 /// (advancing past delimiters). Previous Rust port forked
8281 /// `/bin/sh` to execute the cmd — that's entirely the wrong
8282 /// layer; execution happens at the call sites via qualsheval.
8283 /// Pin: plus-form extracts identifier; delimited form extracts
8284 /// content + advances.
8285 #[test]
8286 fn glob_exec_string_parses_qualifier_text() {
8287 let _g = crate::test_util::global_state_lock();
8288 // Plus form: identifier ends at first non-ident char.
8289 // C: `(+myfunc:rest)` — `s` points past `+`, plus_form=true.
8290 let r = glob_exec_string("myfunc rest", true);
8291 assert!(r.is_some(), "c:1092 — identifier parse should succeed");
8292 let (ident, _adv) = r.unwrap();
8293 assert_eq!(
8294 ident, "myfunc",
8295 "c:1092 — itype_end stops at first non-IIDENT char"
8296 );
8297
8298 // Plus form with no identifier (immediate non-ident) — error.
8299 let r = glob_exec_string(" leading-space", true);
8300 assert!(
8301 r.is_none(),
8302 "c:1093-1096 — empty identifier emits zerr + returns None"
8303 );
8304 }
8305
8306 /// `Src/glob.c:3907-3943` — `qualsheval(name, _, _, expr)` sets
8307 /// `$REPLY` via paramtab, evaluates `expr` IN THE CURRENT SHELL,
8308 /// then restores errflag+lastval. Previous Rust port spawned
8309 /// `/bin/sh -c expr` which:
8310 /// 1. Couldn't access shell locals / functions / aliases.
8311 /// 2. Ran `sh` (POSIX), not `zsh` — every zsh-feature qualifier
8312 /// silently failed.
8313 ///
8314 /// Pin: after qualsheval, errflag is restored (mod ERRFLAG_INT),
8315 /// lastval is restored, and `$REPLY` is set to the filename.
8316 /// Easiest direct pin: errflag/lastval restore around an `expr`
8317 /// that mutates them.
8318 #[test]
8319 fn qualsheval_restores_errflag_and_lastval() {
8320 let _g = crate::test_util::global_state_lock();
8321 // Seed errflag and lastval with distinctive values.
8322 errflag.store(0, Ordering::Relaxed);
8323 LASTVAL.store(42, Ordering::Relaxed);
8324 // Even if the expr mutates these via the executor, c:3924-3925
8325 // restore them after.
8326 let st0: libc::stat = unsafe { std::mem::zeroed() };
8327 let _ = qualsheval("/tmp/file", &st0, 0, ":"); // no-op expr
8328 // c:3924 — errflag restored.
8329 assert_eq!(
8330 errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR,
8331 0,
8332 "c:3924 — qualsheval must restore errflag (no ERRFLAG_ERROR leak)"
8333 );
8334 // c:3925 — lastval restored.
8335 assert_eq!(
8336 LASTVAL.load(Ordering::Relaxed),
8337 42,
8338 "c:3925 — qualsheval must restore lastval to pre-call value"
8339 );
8340 // c:3916 — $REPLY visible in paramtab.
8341 assert_eq!(
8342 crate::ported::params::getsparam("REPLY"),
8343 Some("/tmp/file".to_string()),
8344 "c:3916 — qualsheval must set $REPLY to filename"
8345 );
8346 }
8347
8348 /// `Src/glob.c:2164` — `if (!errflag && isset(MULTIOS))` is the
8349 /// logical zero-check guarding globlist recursion. Previous Rust
8350 /// port used `!errflag.load(...) != 0` which is BITWISE NOT
8351 /// (`^-1`), evaluating to `errflag != -1` — almost-always-true.
8352 ///
8353 /// Pin: a `matchpat` smoke that DOES NOT mutate errflag, then a
8354 /// direct check that the fix sense is correct (errflag==0 enters
8355 /// the branch, errflag!=0 skips). Can't easily exercise the full
8356 /// xpandredir path without a redir struct + IN_EXPANDREDIR
8357 /// global, so this is a sense-of-the-condition pin.
8358 #[test]
8359 fn xpandredir_errflag_check_uses_logical_zero() {
8360 let _g = crate::test_util::global_state_lock();
8361 // Mirror the in-port logic with the canonical zero-check.
8362 let saved = errflag.load(Ordering::Relaxed);
8363 // c:2164 — errflag==0 path: branch should fire.
8364 errflag.store(0, Ordering::Relaxed);
8365 let zero_enters = errflag.load(Ordering::Relaxed) == 0;
8366 assert!(
8367 zero_enters,
8368 "c:2164 — errflag==0 enters the branch (the canonical sense)"
8369 );
8370 // c:2164 — errflag!=0 path: branch should skip.
8371 errflag.store(1, Ordering::Relaxed);
8372 let nonzero_skips = errflag.load(Ordering::Relaxed) == 0;
8373 assert!(
8374 !nonzero_skips,
8375 "c:2164 — errflag!=0 skips the branch (regression of bitwise-NOT bug fix)"
8376 );
8377 errflag.store(saved, Ordering::Relaxed);
8378 }
8379
8380 // ═══════════════════════════════════════════════════════════════════
8381 // Additional unit coverage — pure pattern/brace/glob behaviour that
8382 // doesn't need a real filesystem. Uses `matchpat` for high-level
8383 // pattern checks (extended=false, case_sensitive=true is the most
8384 // common path) and `xpandbraces` for brace expansion.
8385 // ═══════════════════════════════════════════════════════════════════
8386
8387 // ── matchpat: full-string pattern matching ───────────────────────
8388 #[test]
8389 fn matchpat_literal_no_wildcards() {
8390 let _g = crate::test_util::global_state_lock();
8391 assert!(matchpat("foo", "foo", false, true));
8392 assert!(!matchpat("foo", "bar", false, true));
8393 assert!(!matchpat("foo", "foobar", false, true));
8394 }
8395
8396 #[test]
8397 fn matchpat_star_consumes_substring() {
8398 let _g = crate::test_util::global_state_lock();
8399 assert!(matchpat("a*b", "ab", false, true));
8400 assert!(matchpat("a*b", "ahellob", false, true));
8401 assert!(!matchpat("a*b", "abc", false, true));
8402 }
8403
8404 #[test]
8405 fn matchpat_question_is_single_char() {
8406 let _g = crate::test_util::global_state_lock();
8407 assert!(matchpat("a?c", "abc", false, true));
8408 assert!(matchpat("a?c", "axc", false, true));
8409 assert!(!matchpat("a?c", "ac", false, true));
8410 assert!(!matchpat("a?c", "abbc", false, true));
8411 }
8412
8413 #[test]
8414 fn matchpat_bracket_class_inline() {
8415 let _g = crate::test_util::global_state_lock();
8416 assert!(matchpat("file[abc].txt", "filea.txt", false, true));
8417 assert!(matchpat("file[abc].txt", "fileb.txt", false, true));
8418 assert!(!matchpat("file[abc].txt", "filed.txt", false, true));
8419 }
8420
8421 #[test]
8422 fn matchpat_case_sensitive_strict() {
8423 let _g = crate::test_util::global_state_lock();
8424 assert!(matchpat("Foo", "Foo", false, true));
8425 assert!(!matchpat("Foo", "foo", false, true));
8426 assert!(!matchpat("Foo", "FOO", false, true));
8427 }
8428
8429 #[test]
8430 fn matchpat_empty_pattern_matches_only_empty() {
8431 let _g = crate::test_util::global_state_lock();
8432 assert!(matchpat("", "", false, true));
8433 assert!(!matchpat("", "x", false, true));
8434 }
8435
8436 #[test]
8437 fn matchpat_star_alone_matches_anything() {
8438 let _g = crate::test_util::global_state_lock();
8439 assert!(matchpat("*", "", false, true));
8440 assert!(matchpat("*", "a", false, true));
8441 assert!(matchpat("*", "abcdef", false, true));
8442 }
8443
8444 #[test]
8445 fn matchpat_question_alone_one_char() {
8446 let _g = crate::test_util::global_state_lock();
8447 assert!(!matchpat("?", "", false, true));
8448 assert!(matchpat("?", "a", false, true));
8449 assert!(!matchpat("?", "ab", false, true));
8450 }
8451
8452 // ── haswilds: meta-char detection ───────────────────────────────
8453 #[test]
8454 fn haswilds_each_glob_meta() {
8455 let _g = crate::test_util::global_state_lock();
8456 // c:Src/glob.c:3548 — full glob tokenize (shadows the
8457 // brace-only `fn tok` test helper above on purpose:
8458 // haswilds consumes glob tokens, not just braces).
8459 let tok = |s: &str| {
8460 let mut t = s.to_string();
8461 tokenize(&mut t);
8462 t
8463 };
8464 assert!(haswilds(&tok("*")));
8465 assert!(haswilds(&tok("?")));
8466 assert!(haswilds(&tok("[abc]")));
8467 assert!(haswilds(&tok("a*b")));
8468 assert!(haswilds(&tok("a?b")));
8469 }
8470
8471 #[test]
8472 fn haswilds_plain_strings_have_no_wildcards() {
8473 let _g = crate::test_util::global_state_lock();
8474 // c:Src/glob.c:3548 — full glob tokenize (shadows the
8475 // brace-only `fn tok` test helper above on purpose:
8476 // haswilds consumes glob tokens, not just braces).
8477 let tok = |s: &str| {
8478 let mut t = s.to_string();
8479 tokenize(&mut t);
8480 t
8481 };
8482 assert!(!haswilds(&tok("")));
8483 assert!(!haswilds(&tok("plain.txt")));
8484 assert!(!haswilds(&tok("/abs/path/file.rs")));
8485 assert!(!haswilds(&tok("./rel/file")));
8486 }
8487
8488 // ── xpandbraces: brace expansion (zsh extension, not POSIX) ─────
8489 #[test]
8490 fn xpandbraces_three_alternatives() {
8491 let _g = crate::test_util::global_state_lock();
8492 assert_eq!(xpandbraces(&tok("{a,b,c}"), false), vec!["a", "b", "c"]);
8493 }
8494
8495 #[test]
8496 fn xpandbraces_with_prefix_and_suffix() {
8497 let _g = crate::test_util::global_state_lock();
8498 assert_eq!(
8499 xpandbraces(&tok("pre{x,y,z}post"), false),
8500 vec!["prexpost", "preypost", "prezpost"]
8501 );
8502 }
8503
8504 #[test]
8505 fn xpandbraces_numeric_range_ascending() {
8506 let _g = crate::test_util::global_state_lock();
8507 assert_eq!(
8508 xpandbraces(&tok("{1..5}"), false),
8509 vec!["1", "2", "3", "4", "5"]
8510 );
8511 }
8512
8513 #[test]
8514 fn xpandbraces_alpha_range_ascending() {
8515 let _g = crate::test_util::global_state_lock();
8516 assert_eq!(
8517 xpandbraces(&tok("{a..e}"), false),
8518 vec!["a", "b", "c", "d", "e"]
8519 );
8520 }
8521
8522 #[test]
8523 fn xpandbraces_single_alternative_passes_through_literal() {
8524 // Real zsh 5.9 (`print -l {a}`) outputs `{a}` verbatim — no
8525 // expansion because there is no comma or range inside the
8526 // braces. xpandbraces operates on TOKEN form throughout
8527 // (c:Src/glob.c::xpandbraces); the eventual ASCII conversion
8528 // happens later in untokenize. So unit-level pass-through
8529 // preserves TOKEN bytes, not ASCII braces.
8530 let _g = crate::test_util::global_state_lock();
8531 let out = xpandbraces(&tok("{a}"), false);
8532 assert_eq!(
8533 out,
8534 vec![tok("{a}")],
8535 "zsh 5.9 returns the input verbatim (TOKEN form preserved at xpandbraces layer)"
8536 );
8537 }
8538
8539 #[test]
8540 fn xpandbraces_no_braces_returns_input() {
8541 let _g = crate::test_util::global_state_lock();
8542 // Pure literal has nothing to expand — should yield the input
8543 // as a single element (or empty for empty input).
8544 let out = xpandbraces(&tok("plain"), false);
8545 assert_eq!(out, vec!["plain"]);
8546 }
8547
8548 // ── file_type: mode → marker char (used by `ls -F`-style output) ─
8549 #[test]
8550 fn file_type_dir_marker_is_slash() {
8551 let _g = crate::test_util::global_state_lock();
8552 assert_eq!(file_type(libc::S_IFDIR as u32), '/');
8553 }
8554
8555 #[test]
8556 fn file_type_regular_plain_is_space() {
8557 let _g = crate::test_util::global_state_lock();
8558 assert_eq!(file_type(libc::S_IFREG as u32), ' ');
8559 }
8560
8561 #[test]
8562 fn file_type_regular_executable_is_star() {
8563 let _g = crate::test_util::global_state_lock();
8564 // Any of the three executable bits should switch the marker.
8565 for x in [0o100, 0o010, 0o001] {
8566 assert_eq!(
8567 file_type(libc::S_IFREG as u32 | x),
8568 '*',
8569 "exec bit 0o{x:o} should produce '*'"
8570 );
8571 }
8572 }
8573
8574 #[test]
8575 fn file_type_symlink_is_at() {
8576 let _g = crate::test_util::global_state_lock();
8577 assert_eq!(file_type(libc::S_IFLNK as u32), '@');
8578 }
8579
8580 #[test]
8581 fn file_type_fifo_is_pipe() {
8582 let _g = crate::test_util::global_state_lock();
8583 assert_eq!(file_type(libc::S_IFIFO as u32), '|');
8584 }
8585
8586 #[test]
8587 fn file_type_socket_is_equal() {
8588 let _g = crate::test_util::global_state_lock();
8589 assert_eq!(file_type(libc::S_IFSOCK as u32), '=');
8590 }
8591
8592 // ═══════════════════════════════════════════════════════════════════
8593 // xpandbraces edge cases — anchored to `print -l <pattern>` in zsh 5.9.
8594 // Where zshrs diverges, the test FAILS to expose the bug.
8595 // ═══════════════════════════════════════════════════════════════════
8596
8597 /// `{1..10..2}` → 1 3 5 7 9 (step range). zsh: 5 elements.
8598 #[test]
8599 fn xpandbraces_numeric_step_two() {
8600 let _g = crate::test_util::global_state_lock();
8601 assert_eq!(
8602 xpandbraces(&tok("{1..10..2}"), false),
8603 vec!["1", "3", "5", "7", "9"]
8604 );
8605 }
8606
8607 /// `{10..1..2}` → 10 8 6 4 2 (descending step).
8608 #[test]
8609 fn xpandbraces_numeric_descending_step_two() {
8610 let _g = crate::test_util::global_state_lock();
8611 assert_eq!(
8612 xpandbraces(&tok("{10..1..2}"), false),
8613 vec!["10", "8", "6", "4", "2"]
8614 );
8615 }
8616
8617 /// `{5..1}` → 5 4 3 2 1 (descending range, no step).
8618 #[test]
8619 fn xpandbraces_numeric_descending_range() {
8620 let _g = crate::test_util::global_state_lock();
8621 assert_eq!(
8622 xpandbraces(&tok("{5..1}"), false),
8623 vec!["5", "4", "3", "2", "1"]
8624 );
8625 }
8626
8627 /// `{01..10}` → 01 02 03 ... 10 (zero-padded; pad preserved).
8628 #[test]
8629 fn xpandbraces_zero_padded_numeric_range() {
8630 let _g = crate::test_util::global_state_lock();
8631 assert_eq!(
8632 xpandbraces(&tok("{01..10}"), false),
8633 vec!["01", "02", "03", "04", "05", "06", "07", "08", "09", "10"]
8634 );
8635 }
8636
8637 /// `{001..010}` → 001 002 ... 010 (3-digit pad preserved).
8638 #[test]
8639 fn xpandbraces_three_digit_pad_preserved() {
8640 let _g = crate::test_util::global_state_lock();
8641 assert_eq!(
8642 xpandbraces(&tok("{001..010}"), false),
8643 vec!["001", "002", "003", "004", "005", "006", "007", "008", "009", "010"]
8644 );
8645 }
8646
8647 /// `\{a,b,c\}` → no expansion (escaped braces aren't brace
8648 /// delimiters). xpandbraces returns input unchanged; the lexer
8649 /// upstream strips the backslashes before this layer sees them.
8650 /// Anchored test: zsh `print -r -- \{a,b,c\}` → `{a,b,c}` because
8651 /// the lexer tokenizes `\{`/`\}` to `Bnull{`/`Bnull}` before
8652 /// xpandbraces runs, then untokenize drops the Bnulls. At the
8653 /// xpandbraces-unit level (raw backslashes), the only invariant
8654 /// to verify is "no expansion fires" — the brace literal survives.
8655 #[test]
8656 fn xpandbraces_escaped_braces_remain_literal_anchored_to_zsh() {
8657 let _g = crate::test_util::global_state_lock();
8658 // tok() emits Bnull+ASCII `{` for `\{` and Bnull+ASCII `}` for
8659 // `\}`, with Comma TOKEN for the unescaped commas inside.
8660 // xpandbraces scans for Inbrace TOKEN only (per
8661 // c:Src/glob.c::xpandbraces) — finds none here, so the
8662 // string passes through unchanged at this layer. The Bnulls
8663 // are stripped later by remnulargs in prefork.
8664 assert_eq!(
8665 xpandbraces(&tok("\\{a,b,c\\}"), false),
8666 vec![tok("\\{a,b,c\\}")],
8667 "xpandbraces unit: escaped braces survive without expansion (no per-element splat)"
8668 );
8669 }
8670
8671 /// `{a,b}{c,d}` → ac ad bc bd (Cartesian product).
8672 /// 2 * 2 = 4 elements, in row-major order.
8673 #[test]
8674 fn xpandbraces_cartesian_product_two_by_two() {
8675 let _g = crate::test_util::global_state_lock();
8676 assert_eq!(
8677 xpandbraces(&tok("{a,b}{c,d}"), false),
8678 vec!["ac", "ad", "bc", "bd"]
8679 );
8680 }
8681
8682 /// `{a,{b,c}}` → a b c (nested flattened).
8683 #[test]
8684 fn xpandbraces_nested_braces_flatten() {
8685 let _g = crate::test_util::global_state_lock();
8686 assert_eq!(xpandbraces(&tok("{a,{b,c}}"), false), vec!["a", "b", "c"]);
8687 }
8688
8689 /// `{}` → `{}` literal (empty brace doesn't expand). xpandbraces
8690 /// preserves TOKEN form at this layer (c:Src/glob.c::xpandbraces);
8691 /// ASCII conversion happens later in untokenize.
8692 #[test]
8693 fn xpandbraces_empty_braces_remain_literal() {
8694 let _g = crate::test_util::global_state_lock();
8695 assert_eq!(xpandbraces(&tok("{}"), false), vec![tok("{}")]);
8696 }
8697
8698 /// `a{b,c}d{e,f}` → abde abdf acde acdf (cartesian with surrounding text).
8699 #[test]
8700 fn xpandbraces_cartesian_with_surrounding_text() {
8701 let _g = crate::test_util::global_state_lock();
8702 assert_eq!(
8703 xpandbraces(&tok("a{b,c}d{e,f}"), false),
8704 vec!["abde", "abdf", "acde", "acdf"]
8705 );
8706 }
8707
8708 /// `{a..z..3}` → `{a..z..3}` literal (alpha range with step NOT expanded
8709 /// by zsh — surprising but verified). xpandbraces preserves TOKEN form
8710 /// at this layer; ASCII conversion happens later in untokenize.
8711 #[test]
8712 fn xpandbraces_alpha_step_unsupported_anchored_to_zsh() {
8713 let _g = crate::test_util::global_state_lock();
8714 assert_eq!(
8715 xpandbraces(&tok("{a..z..3}"), false),
8716 vec![tok("{a..z..3}")],
8717 "zsh: alpha range with step → literal (TOKEN form preserved at xpandbraces layer)"
8718 );
8719 }
8720
8721 // ═══════════════════════════════════════════════════════════════════
8722 // zsh test-corpus pins — direct anchors to Test/D09brace.ztst.
8723 // Tests verify brace expansion matches zsh's documented output.
8724 // ═══════════════════════════════════════════════════════════════════
8725
8726 /// `Test/D09brace.ztst:10-12` — basic brace expansion with
8727 /// nested range: `X{1,2,{3..6},7,8}Y` expands all 8 values.
8728 #[test]
8729 fn zsh_corpus_basic_brace_with_nested_range() {
8730 let _g = crate::test_util::global_state_lock();
8731 assert_eq!(
8732 xpandbraces(&tok("X{1,2,{3..6},7,8}Y"), false),
8733 vec!["X1Y", "X2Y", "X3Y", "X4Y", "X5Y", "X6Y", "X7Y", "X8Y"],
8734 "ztst:11 — basic brace expansion with nested range",
8735 );
8736 }
8737
8738 /// `Test/D09brace.ztst:30-32` — numeric range with leading zero
8739 /// padding: `X{01..4}Y` → all 4 values 0-padded to 2 digits.
8740 #[test]
8741 fn zsh_corpus_numeric_range_zero_padding() {
8742 let _g = crate::test_util::global_state_lock();
8743 assert_eq!(
8744 xpandbraces(&tok("X{01..4}Y"), false),
8745 vec!["X01Y", "X02Y", "X03Y", "X04Y"],
8746 "ztst:32 — leading-zero padding propagates to all values",
8747 );
8748 }
8749
8750 /// `Test/D09brace.ztst:34-36` — padding comes from RHS too:
8751 /// `X{1..04}Y` → 2-digit zero-padded.
8752 #[test]
8753 fn zsh_corpus_numeric_range_padding_from_rhs() {
8754 let _g = crate::test_util::global_state_lock();
8755 assert_eq!(
8756 xpandbraces(&tok("X{1..04}Y"), false),
8757 vec!["X01Y", "X02Y", "X03Y", "X04Y"],
8758 "ztst:36 — RHS padding `04` propagates",
8759 );
8760 }
8761
8762 /// `Test/D09brace.ztst:38-40` — unpadded range >9: `X{7..12}Y`
8763 /// numbers stay unpadded.
8764 #[test]
8765 fn zsh_corpus_numeric_range_no_padding_when_unspecified() {
8766 let _g = crate::test_util::global_state_lock();
8767 assert_eq!(
8768 xpandbraces(&tok("X{7..12}Y"), false),
8769 vec!["X7Y", "X8Y", "X9Y", "X10Y", "X11Y", "X12Y"],
8770 "ztst:40 — no padding when neither end has leading zero",
8771 );
8772 }
8773
8774 /// `Test/D09brace.ztst:42-44` — `X{07..12}Y` → 2-digit padded.
8775 #[test]
8776 fn zsh_corpus_numeric_range_lhs_padding_propagates() {
8777 let _g = crate::test_util::global_state_lock();
8778 assert_eq!(
8779 xpandbraces(&tok("X{07..12}Y"), false),
8780 vec!["X07Y", "X08Y", "X09Y", "X10Y", "X11Y", "X12Y"],
8781 "ztst:44 — LHS padding `07` propagates",
8782 );
8783 }
8784
8785 /// `Test/D09brace.ztst:46-48` — wider RHS padding overrides:
8786 /// `X{7..012}Y` → 3-digit padded.
8787 #[test]
8788 fn zsh_corpus_numeric_range_max_padding_width_wins() {
8789 let _g = crate::test_util::global_state_lock();
8790 assert_eq!(
8791 xpandbraces(&tok("X{7..012}Y"), false),
8792 vec!["X007Y", "X008Y", "X009Y", "X010Y", "X011Y", "X012Y"],
8793 "ztst:48 — widest padding width wins",
8794 );
8795 }
8796
8797 /// `Test/D09brace.ztst:50-52` — decreasing range: `X{4..1}Y` →
8798 /// 4,3,2,1 (reversed).
8799 #[test]
8800 fn zsh_corpus_numeric_range_decreasing() {
8801 let _g = crate::test_util::global_state_lock();
8802 assert_eq!(
8803 xpandbraces(&tok("X{4..1}Y"), false),
8804 vec!["X4Y", "X3Y", "X2Y", "X1Y"],
8805 "ztst:52 — decreasing range emits in reverse",
8806 );
8807 }
8808
8809 /// `Test/D09brace.ztst:54-56` — combined braces: `X{1..4}{1..4}Y`
8810 /// → 16-element cross product.
8811 #[test]
8812 fn zsh_corpus_combined_braces_cross_product() {
8813 let _g = crate::test_util::global_state_lock();
8814 assert_eq!(
8815 xpandbraces(&tok("X{1..4}{1..4}Y"), false),
8816 vec![
8817 "X11Y", "X12Y", "X13Y", "X14Y", "X21Y", "X22Y", "X23Y", "X24Y", "X31Y", "X32Y",
8818 "X33Y", "X34Y", "X41Y", "X42Y", "X43Y", "X44Y",
8819 ],
8820 "ztst:56 — combined-brace cross product",
8821 );
8822 }
8823
8824 /// `Test/D09brace.ztst:58-60` — negative numbers in range:
8825 /// `X{-4..4}Y` → `-4`..`4` (9 values including 0).
8826 #[test]
8827 fn zsh_corpus_negative_numbers_in_range() {
8828 let _g = crate::test_util::global_state_lock();
8829 assert_eq!(
8830 xpandbraces(&tok("X{-4..4}Y"), false),
8831 vec!["X-4Y", "X-3Y", "X-2Y", "X-1Y", "X0Y", "X1Y", "X2Y", "X3Y", "X4Y",],
8832 "ztst:60 — negative-to-positive range",
8833 );
8834 }
8835
8836 /// `Test/D09brace.ztst:62-64` — reverse-direction numeric range:
8837 /// `X{4..-4}Y` walks from 4 to -4 descending.
8838 #[test]
8839 fn zsh_corpus_brace_descending_range_from_positive_to_negative() {
8840 let _g = crate::test_util::global_state_lock();
8841 assert_eq!(
8842 xpandbraces(&tok("X{4..-4}Y"), false),
8843 vec!["X4Y", "X3Y", "X2Y", "X1Y", "X0Y", "X-1Y", "X-2Y", "X-3Y", "X-4Y",],
8844 "ztst:64 — descending 4..-4 produces 9 values",
8845 );
8846 }
8847
8848 /// `Test/D09brace.ztst:66-68` — stepped padded range:
8849 /// `X{004..-4..2}Y` = X004Y X002Y X000Y X-02Y X-04Y.
8850 /// Padding width 3 from LHS; step is +2 (sign of step is ignored,
8851 /// direction is from start to end).
8852 #[test]
8853 fn zsh_corpus_brace_stepped_padded_descending() {
8854 let _g = crate::test_util::global_state_lock();
8855 assert_eq!(
8856 xpandbraces(&tok("X{004..-4..2}Y"), false),
8857 vec!["X004Y", "X002Y", "X000Y", "X-02Y", "X-04Y"],
8858 "ztst:68 — stepped+padded descending",
8859 );
8860 }
8861
8862 /// `Test/D09brace.ztst:74-76` — step alignment 1: `X{1..32..3}Y`
8863 /// = X1Y X4Y X7Y X10Y X13Y X16Y X19Y X22Y X25Y X28Y X31Y.
8864 /// Step 3 starting from 1, capped at 32.
8865 #[test]
8866 fn zsh_corpus_brace_step_alignment_1_to_32_step_3() {
8867 let _g = crate::test_util::global_state_lock();
8868 assert_eq!(
8869 xpandbraces(&tok("X{1..32..3}Y"), false),
8870 vec![
8871 "X1Y", "X4Y", "X7Y", "X10Y", "X13Y", "X16Y", "X19Y", "X22Y", "X25Y", "X28Y",
8872 "X31Y",
8873 ],
8874 "ztst:76 — {{1..32..3}} step alignment",
8875 );
8876 }
8877
8878 /// `Test/D09brace.ztst:100-102` — `hey{a..j}there` — char range.
8879 #[test]
8880 fn zsh_corpus_brace_char_range_simple() {
8881 let _g = crate::test_util::global_state_lock();
8882 assert_eq!(
8883 xpandbraces(&tok("hey{a..j}there"), false),
8884 vec![
8885 "heyathere",
8886 "heybthere",
8887 "heycthere",
8888 "heydthere",
8889 "heyethere",
8890 "heyfthere",
8891 "heygthere",
8892 "heyhthere",
8893 "heyithere",
8894 "heyjthere",
8895 ],
8896 "ztst:102 — char range a..j",
8897 );
8898 }
8899
8900 /// `Test/D09brace.ztst:108-110` — reverse char range:
8901 /// `crumbs{y..p}ooh` walks down y → p (10 values).
8902 #[test]
8903 fn zsh_corpus_brace_char_range_reverse() {
8904 let _g = crate::test_util::global_state_lock();
8905 assert_eq!(
8906 xpandbraces(&tok("crumbs{y..p}ooh"), false),
8907 vec![
8908 "crumbsyooh",
8909 "crumbsxooh",
8910 "crumbswooh",
8911 "crumbsvooh",
8912 "crumbsuooh",
8913 "crumbstooh",
8914 "crumbssooh",
8915 "crumbsrooh",
8916 "crumbsqooh",
8917 "crumbspooh",
8918 ],
8919 "ztst:110 — char range y..p reverse",
8920 );
8921 }
8922
8923 /// `Test/D09brace.ztst:104-106` — nested brace: `gosh{1,{Z..a},2}cripes`
8924 /// — inner `{Z..a}` is char range Z to a in ASCII order (10 chars).
8925 /// Full output: gosh1cripes goshZcripes gosh[cripes gosh\cripes
8926 /// gosh]cripes gosh^cripes gosh_cripes gosh`cripes goshacripes
8927 /// gosh2cripes.
8928 #[test]
8929 fn zsh_corpus_brace_nested_with_char_range_ascii() {
8930 let _g = crate::test_util::global_state_lock();
8931 assert_eq!(
8932 xpandbraces(&tok("gosh{1,{Z..a},2}cripes"), false),
8933 vec![
8934 "gosh1cripes",
8935 "goshZcripes",
8936 "gosh[cripes",
8937 "gosh\\cripes",
8938 "gosh]cripes",
8939 "gosh^cripes",
8940 "gosh_cripes",
8941 "gosh`cripes",
8942 "goshacripes",
8943 "gosh2cripes",
8944 ],
8945 "ztst:106 — nested brace with ASCII char range",
8946 );
8947 }
8948
8949 /// `Test/D09brace.ztst:116-118` — unmatched closing brace after
8950 /// matched braces stays literal: `{1..10}{..` → `1{.. 2{.. ...`.
8951 /// `xpandbraces` itself preserves TOKEN form (Inbrace=`\u{e18f}`,
8952 /// Outbrace=`\u{e190}`) per the convention pinned by
8953 /// `xpandbraces_alpha_step_unsupported_anchored_to_zsh` — the
8954 /// later untokenize pass in the user-output pipeline turns
8955 /// surviving brace tokens into literal `{` / `}`. This test
8956 /// asserts the tokenized intermediate form.
8957 #[test]
8958 fn zsh_corpus_brace_unmatched_after_matched_left_literal() {
8959 let _g = crate::test_util::global_state_lock();
8960 let tokb = '\u{e18f}'; // Inbrace
8961 let expected: Vec<String> = (1..=10).map(|n| format!("{}{}..", n, tokb)).collect();
8962 assert_eq!(
8963 xpandbraces(&tok("{1..10}{.."), false),
8964 expected,
8965 "ztst:118 — unmatched trailing {{.. preserved as tokenized Inbrace at xpandbraces layer",
8966 );
8967 }
8968
8969 // ── split_qualifier: parse trailing (qual) block ─────────────────
8970 /// `*.txt` (no trailing parens) → (input, None).
8971 #[test]
8972 fn split_qualifier_no_parens_returns_input_and_none() {
8973 let _g = crate::test_util::global_state_lock();
8974 let (head, qual) = split_qualifier("*.txt");
8975 assert_eq!(head, "*.txt");
8976 assert_eq!(qual, None);
8977 }
8978
8979 /// `*(N)` → ("*", Some("N")) — null-glob qualifier.
8980 #[test]
8981 fn split_qualifier_star_paren_N_extracts_null_glob_qual() {
8982 let _g = crate::test_util::global_state_lock();
8983 let (head, qual) = split_qualifier("*(N)");
8984 assert_eq!(head, "*");
8985 assert_eq!(qual, Some("N"));
8986 }
8987
8988 /// `*(.)` → ("*", Some(".")) — regular-file qualifier.
8989 #[test]
8990 fn split_qualifier_star_paren_dot_extracts_regular_file_qual() {
8991 let _g = crate::test_util::global_state_lock();
8992 let (head, qual) = split_qualifier("*(.)");
8993 assert_eq!(head, "*");
8994 assert_eq!(qual, Some("."));
8995 }
8996
8997 /// `*.rs(.)` → ("*.rs", Some(".")) — qualifier on glob pattern.
8998 #[test]
8999 fn split_qualifier_pattern_with_qual_extracts_correctly() {
9000 let _g = crate::test_util::global_state_lock();
9001 let (head, qual) = split_qualifier("*.rs(.)");
9002 assert_eq!(head, "*.rs");
9003 assert_eq!(qual, Some("."));
9004 }
9005
9006 /// `*(#qN)` → ("*", Some("N")) — `#q` prefix stripped per zsh syntax.
9007 #[test]
9008 fn split_qualifier_hash_q_prefix_stripped() {
9009 let _g = crate::test_util::global_state_lock();
9010 let (head, qual) = split_qualifier("*(#qN)");
9011 assert_eq!(head, "*");
9012 assert_eq!(qual, Some("N"));
9013 }
9014
9015 /// `*(om[1])` → ("*", Some("om[1]")) — sort-by-mtime keep first.
9016 #[test]
9017 fn split_qualifier_multichar_qual_with_brackets() {
9018 let _g = crate::test_util::global_state_lock();
9019 let (head, qual) = split_qualifier("*(om[1])");
9020 assert_eq!(head, "*");
9021 assert_eq!(qual, Some("om[1]"));
9022 }
9023
9024 /// `(a)(b)` — multiple paren groups: split takes the OUTERMOST trailing.
9025 #[test]
9026 fn split_qualifier_multiple_groups_takes_outermost_trailing() {
9027 let _g = crate::test_util::global_state_lock();
9028 let (head, qual) = split_qualifier("(a)(b)");
9029 assert_eq!(head, "(a)");
9030 assert_eq!(qual, Some("b"));
9031 }
9032
9033 // ═══════════════════════════════════════════════════════════════════
9034 // parse_qualifier_string — file-type / permission / sort qualifiers.
9035 // Tests call the private fn directly (same crate, same file).
9036 // Each qualifier letter maps to a `qualifier::*` variant per the
9037 // zsh glob.c arm at c:1495+. Pin the parse, not the match — the
9038 // match path needs a real filesystem.
9039 //
9040 // IMPORTANT: at end of parse_qualifier_string (c:3433-3435 in this
9041 // file), if `qualifiers` is non-empty it gets moved into
9042 // `alternatives[]`. So tests read the qualifier from `alternatives[0]`
9043 // — qualifiers itself is empty after the move.
9044 // ═══════════════════════════════════════════════════════════════════
9045
9046 /// Helper: read the only qualifier from a single-letter parse result.
9047 fn first_qual(qs: &qualifier_set) -> &qualifier {
9048 // After the post-loop move, single-letter input lands in
9049 // alternatives[0][0]. Fall back to qualifiers[0] for safety in
9050 // case the move ever changes.
9051 if !qs.alternatives.is_empty() && !qs.alternatives[0].is_empty() {
9052 &qs.alternatives[0][0]
9053 } else {
9054 &qs.qualifiers[0]
9055 }
9056 }
9057
9058 /// Empty qualifier body → no alternatives, no qualifiers.
9059 #[test]
9060 fn parse_qualifier_string_empty_returns_empty_set() {
9061 let _g = crate::test_util::global_state_lock();
9062 let qs = parse_qualifier_string("");
9063 assert!(qs.qualifiers.is_empty());
9064 assert!(qs.alternatives.is_empty());
9065 assert!(!qs.nullglob);
9066 }
9067
9068 /// `/` → IsDirectory (in alternatives[0]).
9069 #[test]
9070 fn parse_qualifier_string_slash_is_directory() {
9071 let _g = crate::test_util::global_state_lock();
9072 let qs = parse_qualifier_string("/");
9073 assert!(matches!(first_qual(&qs), qualifier::IsDirectory));
9074 }
9075
9076 /// `.` → IsRegular.
9077 #[test]
9078 fn parse_qualifier_string_dot_is_regular() {
9079 let _g = crate::test_util::global_state_lock();
9080 let qs = parse_qualifier_string(".");
9081 assert!(matches!(first_qual(&qs), qualifier::IsRegular));
9082 }
9083
9084 /// `parsecomplist` splits a path into per-component `complist` nodes,
9085 /// each literal section a PAT_PURES Patprog (relies on patcompile's
9086 /// PAT_FILE PURES segment-stop at `/`, pattern.c:584-610).
9087 #[test]
9088 fn parsecomplist_splits_path_components() {
9089 let _g = crate::test_util::global_state_lock();
9090 fn pure(p: &crate::ported::pattern::Patprog) -> Option<String> {
9091 if (p.0.flags & crate::ported::zsh_h::PAT_PURES as i32) != 0 {
9092 let off = p.0.startoff as usize;
9093 let l = p.0.patmlen as usize;
9094 p.1.get(off..off + l)
9095 .map(|b| String::from_utf8_lossy(b).into_owned())
9096 } else {
9097 None
9098 }
9099 }
9100 fn sections(mut q: &complist) -> Vec<String> {
9101 let mut out = Vec::new();
9102 loop {
9103 out.push(pure(&q.pat).unwrap_or_else(|| "<pat>".into()));
9104 match &q.next {
9105 Some(n) => q = n,
9106 None => break,
9107 }
9108 }
9109 out
9110 }
9111 // Literal path → split into per-component PAT_PURES sections.
9112 let mut t = "a/b/c.txt".to_string();
9113 tokenize(&mut t);
9114 let cl = parsecomplist(&t).expect("parsecomplist None");
9115 assert_eq!(sections(&cl), vec!["a", "b", "c.txt"]);
9116
9117 // Literal prefix before a pattern still splits the literal dirs;
9118 // the trailing glob section is a real pattern (not PAT_PURES).
9119 let mut t2 = "a/b/*.txt".to_string();
9120 tokenize(&mut t2);
9121 let cl2 = parsecomplist(&t2).expect("parsecomplist None");
9122 let s2 = sections(&cl2);
9123 assert_eq!(s2.len(), 3, "a/b/*.txt → 3 sections, got {s2:?}");
9124 assert_eq!(&s2[..2], &["a".to_string(), "b".to_string()]);
9125 assert_eq!(s2[2], "<pat>", "trailing *.txt must be a pattern section");
9126
9127 // Pattern-FIRST then literal dir: */foo → [<pat>, foo].
9128 let mut t3 = "*/foo".to_string();
9129 tokenize(&mut t3);
9130 let cl3 = parsecomplist(&t3).expect("parsecomplist None for */foo");
9131 let s3 = sections(&cl3);
9132 assert_eq!(s3.len(), 2, "*/foo → 2 sections, got {s3:?}");
9133 assert_eq!(s3[0], "<pat>", "leading * must be a pattern section");
9134 assert_eq!(s3[1], "foo");
9135 }
9136
9137 /// The `struct qual` arena is built from the parse alongside the enum:
9138 /// AND via `next`, alternatives via `or`, per-node `sense` from `^`.
9139 #[test]
9140 fn parse_qualifier_string_builds_qual_arena() {
9141 let _g = crate::test_util::global_state_lock();
9142 // single qualifier → one node, sense 0, head set, func present.
9143 let q = parse_qualifier_string(".");
9144 assert_eq!(q.quals.nodes.len(), 1);
9145 assert_eq!(q.quals.head, Some(0));
9146 assert_eq!(q.quals.nodes[0].sense, 0);
9147 assert!(q.quals.nodes[0].func.is_some());
9148 assert_eq!(q.quals.nodes[0].next, None);
9149
9150 // leading `^` → per-node sense flipped (c:1346).
9151 let q = parse_qualifier_string("^.");
9152 assert_eq!(q.quals.nodes.len(), 1);
9153 assert_eq!(q.quals.nodes[0].sense, 1);
9154
9155 // `/^@` → AND-chain of 2: dir(sense 0) -next-> symlink(sense 1).
9156 let q = parse_qualifier_string("/^@");
9157 assert_eq!(q.quals.nodes.len(), 2);
9158 assert_eq!(q.quals.nodes[0].sense, 0);
9159 assert_eq!(q.quals.nodes[0].next, Some(1));
9160 assert_eq!(q.quals.nodes[1].sense, 1);
9161 assert_eq!(q.quals.nodes[1].next, None);
9162
9163 // `.,/` → two alternatives: node0 -or-> node1, neither chained.
9164 let q = parse_qualifier_string(".,/");
9165 assert_eq!(q.quals.nodes.len(), 2);
9166 assert_eq!(q.quals.nodes[0].or, Some(1));
9167 assert_eq!(q.quals.nodes[0].next, None);
9168 assert_eq!(q.quals.head, Some(0));
9169 }
9170
9171 /// `@` → IsSymlink.
9172 #[test]
9173 fn parse_qualifier_string_at_is_symlink() {
9174 let _g = crate::test_util::global_state_lock();
9175 let qs = parse_qualifier_string("@");
9176 assert!(matches!(first_qual(&qs), qualifier::IsSymlink));
9177 }
9178
9179 /// `=` → IsSocket.
9180 #[test]
9181 fn parse_qualifier_string_equals_is_socket() {
9182 let _g = crate::test_util::global_state_lock();
9183 let qs = parse_qualifier_string("=");
9184 assert!(matches!(first_qual(&qs), qualifier::IsSocket));
9185 }
9186
9187 /// `p` → IsFifo.
9188 #[test]
9189 fn parse_qualifier_string_p_is_fifo() {
9190 let _g = crate::test_util::global_state_lock();
9191 let qs = parse_qualifier_string("p");
9192 assert!(matches!(first_qual(&qs), qualifier::IsFifo));
9193 }
9194
9195 /// `*` → IsExecutable.
9196 #[test]
9197 fn parse_qualifier_string_star_is_executable() {
9198 let _g = crate::test_util::global_state_lock();
9199 let qs = parse_qualifier_string("*");
9200 assert!(matches!(first_qual(&qs), qualifier::IsExecutable));
9201 }
9202
9203 /// `%b` → IsBlockDev.
9204 #[test]
9205 fn parse_qualifier_string_pct_b_is_block_device() {
9206 let _g = crate::test_util::global_state_lock();
9207 let qs = parse_qualifier_string("%b");
9208 assert!(matches!(first_qual(&qs), qualifier::IsBlockDev));
9209 }
9210
9211 /// `%c` → IsCharDev.
9212 #[test]
9213 fn parse_qualifier_string_pct_c_is_char_device() {
9214 let _g = crate::test_util::global_state_lock();
9215 let qs = parse_qualifier_string("%c");
9216 assert!(matches!(first_qual(&qs), qualifier::IsCharDev));
9217 }
9218
9219 /// `r` / `w` / `x` → Readable / Writable / Executable.
9220 #[test]
9221 fn parse_qualifier_string_perm_letters_map_to_perm_qualifiers() {
9222 let _g = crate::test_util::global_state_lock();
9223 for (letter, expected) in [
9224 ("r", qualifier::Readable),
9225 ("w", qualifier::Writable),
9226 ("x", qualifier::Executable),
9227 ] {
9228 let qs = parse_qualifier_string(letter);
9229 assert_eq!(
9230 std::mem::discriminant(first_qual(&qs)),
9231 std::mem::discriminant(&expected),
9232 "letter {letter:?} should map to {expected:?}"
9233 );
9234 }
9235 }
9236
9237 /// `U` → OwnedByEuid (process EUID).
9238 #[test]
9239 fn parse_qualifier_string_capital_U_is_owned_by_euid() {
9240 let _g = crate::test_util::global_state_lock();
9241 let qs = parse_qualifier_string("U");
9242 assert!(matches!(first_qual(&qs), qualifier::OwnedByEuid));
9243 }
9244
9245 /// `G` → OwnedByEgid.
9246 #[test]
9247 fn parse_qualifier_string_capital_G_is_owned_by_egid() {
9248 let _g = crate::test_util::global_state_lock();
9249 let qs = parse_qualifier_string("G");
9250 assert!(matches!(first_qual(&qs), qualifier::OwnedByEgid));
9251 }
9252
9253 /// Multiple file-type qualifiers stack — `./` → IsRegular then IsDirectory
9254 /// both end up in alternatives[0].
9255 #[test]
9256 fn parse_qualifier_string_multiple_letters_stack_in_one_alternative() {
9257 let _g = crate::test_util::global_state_lock();
9258 let qs = parse_qualifier_string("./");
9259 assert_eq!(qs.alternatives.len(), 1);
9260 assert_eq!(qs.alternatives[0].len(), 2);
9261 assert!(matches!(qs.alternatives[0][0], qualifier::IsRegular));
9262 assert!(matches!(qs.alternatives[0][1], qualifier::IsDirectory));
9263 }
9264
9265 /// `,` separates alternatives — `/,.` → two alternatives, each with one
9266 /// qualifier (IsDirectory and IsRegular respectively).
9267 #[test]
9268 fn parse_qualifier_string_comma_creates_two_alternatives() {
9269 let _g = crate::test_util::global_state_lock();
9270 let qs = parse_qualifier_string("/,.");
9271 assert_eq!(qs.alternatives.len(), 2, "two alternatives expected");
9272 assert!(matches!(qs.alternatives[0][0], qualifier::IsDirectory));
9273 assert!(matches!(qs.alternatives[1][0], qualifier::IsRegular));
9274 }
9275
9276 /// `:` introduces colon-modifiers; rest captured into colon_mods.
9277 #[test]
9278 fn parse_qualifier_string_colon_captures_modifiers_in_field() {
9279 let _g = crate::test_util::global_state_lock();
9280 let qs = parse_qualifier_string(":h");
9281 assert_eq!(qs.colon_mods.as_deref(), Some(":h"));
9282 }
9283
9284 /// `:` after a qualifier captures BOTH.
9285 #[test]
9286 fn parse_qualifier_string_qualifier_then_colon_mod() {
9287 let _g = crate::test_util::global_state_lock();
9288 let qs = parse_qualifier_string("/:h");
9289 assert!(matches!(first_qual(&qs), qualifier::IsDirectory));
9290 assert_eq!(qs.colon_mods.as_deref(), Some(":h"));
9291 }
9292
9293 /// `N` flag → nullglob set, no qualifier pushed.
9294 #[test]
9295 fn parse_qualifier_string_capital_N_sets_nullglob_flag() {
9296 let _g = crate::test_util::global_state_lock();
9297 let qs = parse_qualifier_string("N");
9298 assert!(qs.nullglob, "(N) must set nullglob");
9299 assert!(qs.alternatives.is_empty(), "N doesn't push to qualifiers");
9300 }
9301
9302 /// `M` flag → mark_dirs.
9303 #[test]
9304 fn parse_qualifier_string_capital_M_sets_mark_dirs_flag() {
9305 let _g = crate::test_util::global_state_lock();
9306 let qs = parse_qualifier_string("M");
9307 assert_eq!(qs.mark_dirs, Some(true), "(M) must set mark_dirs");
9308 }
9309
9310 /// `T` flag → list_types.
9311 #[test]
9312 fn parse_qualifier_string_capital_T_sets_list_types_flag() {
9313 let _g = crate::test_util::global_state_lock();
9314 let qs = parse_qualifier_string("T");
9315 assert!(qs.list_types, "(T) must set list_types");
9316 }
9317
9318 // ── split_qualifier integration with parse_qualifier_string ──────
9319 /// `*(.)` — split yields ("*", Some(".")); parse yields IsRegular.
9320 #[test]
9321 fn split_then_parse_dot_qualifier_yields_is_regular() {
9322 let _g = crate::test_util::global_state_lock();
9323 let (head, qual) = split_qualifier("*(.)");
9324 assert_eq!(head, "*");
9325 let qs = parse_qualifier_string(qual.unwrap());
9326 assert!(matches!(first_qual(&qs), qualifier::IsRegular));
9327 }
9328
9329 /// `*(/)` — directory-only glob.
9330 #[test]
9331 fn split_then_parse_slash_qualifier_yields_is_directory() {
9332 let _g = crate::test_util::global_state_lock();
9333 let (_, qual) = split_qualifier("*(/)");
9334 let qs = parse_qualifier_string(qual.unwrap());
9335 assert!(matches!(first_qual(&qs), qualifier::IsDirectory));
9336 }
9337
9338 // ═══════════════════════════════════════════════════════════════════
9339 // Sort qualifiers — `o<key>` ascending, `O<key>` descending.
9340 // ═══════════════════════════════════════════════════════════════════
9341
9342 /// `oN` — sort by NONE (no sorting key requested).
9343 #[test]
9344 fn parse_qualifier_string_oN_pushes_gs_none() {
9345 let _g = crate::test_util::global_state_lock();
9346 let qs = parse_qualifier_string("oN");
9347 assert_eq!(qs.sorts.len(), 1);
9348 assert_ne!(qs.sorts[0] & GS_NONE, 0, "oN must set GS_NONE bit");
9349 }
9350
9351 /// `on` — sort by name ascending.
9352 #[test]
9353 fn parse_qualifier_string_on_pushes_gs_name_ascending() {
9354 let _g = crate::test_util::global_state_lock();
9355 let qs = parse_qualifier_string("on");
9356 assert_eq!(qs.sorts.len(), 1);
9357 assert_eq!(qs.sorts[0] & GS_NAME, GS_NAME);
9358 assert_eq!(qs.sorts[0] & GS_DESC, 0);
9359 }
9360
9361 /// `On` — sort by name descending.
9362 #[test]
9363 fn parse_qualifier_string_On_pushes_gs_name_descending() {
9364 let _g = crate::test_util::global_state_lock();
9365 let qs = parse_qualifier_string("On");
9366 assert_eq!(qs.sorts.len(), 1);
9367 assert_eq!(qs.sorts[0] & GS_NAME, GS_NAME);
9368 assert_eq!(qs.sorts[0] & GS_DESC, GS_DESC);
9369 }
9370
9371 /// Multiple sort keys stack.
9372 #[test]
9373 fn parse_qualifier_string_chained_sort_keys_stack() {
9374 let _g = crate::test_util::global_state_lock();
9375 let qs = parse_qualifier_string("onOL");
9376 assert_eq!(qs.sorts.len(), 2);
9377 }
9378
9379 // ── Size qualifier — `L<unit><op><value>` ───────────────────────
9380 // NOTE: zshrs's parse_range_spec normalises `+` (greater) → '>' and
9381 // `-` (less) → '<' for the internal op char, matching C's qgetnum
9382 // convention (Src/glob.c c:1620-ish).
9383
9384 /// `Lk+1` — size > 1 kilobyte. zsh syntax: `L<unit><op><value>`
9385 /// (unit char BEFORE op).
9386 #[test]
9387 fn parse_qualifier_string_Lk_plus_one_size_kilobyte() {
9388 let _g = crate::test_util::global_state_lock();
9389 let qs = parse_qualifier_string("Lk+1");
9390 match first_qual(&qs) {
9391 qualifier::Size { value, unit, op } => {
9392 assert_eq!(*value, 1);
9393 assert_eq!(*unit, TT_KILOBYTES);
9394 assert_eq!(*op, '>', "+ is stored as '>' (greater than)");
9395 }
9396 other => panic!("expected Size, got {other:?}"),
9397 }
9398 }
9399
9400 /// `L-100` — size < 100 (default unit bytes). Stored op is '<'.
9401 #[test]
9402 fn parse_qualifier_string_L_minus_100_size_bytes() {
9403 let _g = crate::test_util::global_state_lock();
9404 let qs = parse_qualifier_string("L-100");
9405 match first_qual(&qs) {
9406 qualifier::Size { value, unit, op } => {
9407 assert_eq!(*value, 100);
9408 assert_eq!(*unit, TT_BYTES);
9409 assert_eq!(*op, '<', "- is stored as '<' (less than)");
9410 }
9411 other => panic!("expected Size, got {other:?}"),
9412 }
9413 }
9414
9415 /// `Lm+1` — size > 1 megabyte; unit recognised.
9416 #[test]
9417 fn parse_qualifier_string_Lm_megabyte_unit() {
9418 let _g = crate::test_util::global_state_lock();
9419 let qs = parse_qualifier_string("Lm+1");
9420 match first_qual(&qs) {
9421 qualifier::Size { unit, .. } => assert_eq!(*unit, TT_MEGABYTES),
9422 other => panic!("expected Size, got {other:?}"),
9423 }
9424 }
9425
9426 // ── Time qualifier — same op normalisation ───────────────────────
9427 /// `m-1` — modified less than 1 day ago. Stored op '<'.
9428 #[test]
9429 fn parse_qualifier_string_m_minus_1_day_default_unit() {
9430 let _g = crate::test_util::global_state_lock();
9431 let qs = parse_qualifier_string("m-1");
9432 match first_qual(&qs) {
9433 qualifier::Mtime { value, unit, op } => {
9434 assert_eq!(*value, 1);
9435 assert_eq!(*unit, TT_DAYS);
9436 assert_eq!(*op, '<');
9437 }
9438 other => panic!("expected Mtime, got {other:?}"),
9439 }
9440 }
9441
9442 /// `mh+24` — modified more than 24 hours ago. Stored op '>'.
9443 #[test]
9444 fn parse_qualifier_string_mh_plus_24_hours() {
9445 let _g = crate::test_util::global_state_lock();
9446 let qs = parse_qualifier_string("mh+24");
9447 match first_qual(&qs) {
9448 qualifier::Mtime { value, unit, op } => {
9449 assert_eq!(*value, 24);
9450 assert_eq!(*unit, TT_HOURS);
9451 assert_eq!(*op, '>');
9452 }
9453 other => panic!("expected Mtime, got {other:?}"),
9454 }
9455 }
9456
9457 /// `a-7` — accessed less than 7 days ago. Stored op '<'.
9458 #[test]
9459 fn parse_qualifier_string_a_minus_7_days() {
9460 let _g = crate::test_util::global_state_lock();
9461 let qs = parse_qualifier_string("a-7");
9462 match first_qual(&qs) {
9463 qualifier::Atime { value, unit, op } => {
9464 assert_eq!(*value, 7);
9465 assert_eq!(*unit, TT_DAYS);
9466 assert_eq!(*op, '<');
9467 }
9468 other => panic!("expected Atime, got {other:?}"),
9469 }
9470 }
9471
9472 // ── Subscript qualifier — `[N]` keep first ───────────────────────
9473 /// `[1]` — first entry only. C's `first = v.start` (c:1739) is getindex's
9474 /// 0-based offset (c:2145) and `end = v.end` the 1-based bound.
9475 #[test]
9476 fn parse_qualifier_string_bracket_one_sets_first_to_zero() {
9477 let _g = crate::test_util::global_state_lock();
9478 let qs = parse_qualifier_string("[1]");
9479 assert_eq!(qs.first, Some(0));
9480 assert_eq!(qs.last, Some(1));
9481 }
9482
9483 /// `om[1]` — sort by mtime, keep first.
9484 #[test]
9485 fn parse_qualifier_string_om_bracket_one_sort_and_subscript() {
9486 let _g = crate::test_util::global_state_lock();
9487 let qs = parse_qualifier_string("om[1]");
9488 assert_eq!(qs.sorts.len(), 1);
9489 assert_eq!(qs.sorts[0] & GS_MTIME, GS_MTIME);
9490 assert_eq!(qs.first, Some(0));
9491 }
9492
9493 // ═══════════════════════════════════════════════════════════════════
9494 // tokenize / remnulargs C-parity tests — pin Src/glob.c:3551 (tokenize)
9495 // and Src/glob.c:3649 (remnulargs). These two functions form the
9496 // input → glob-token → output sandwich every glob pattern walks
9497 // through; regressions silently corrupt every pattern match.
9498 // Tests that capture KNOWN ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"].
9499 // ═══════════════════════════════════════════════════════════════════
9500
9501 /// `tokenize("abc")` is a no-op for plain ASCII — no glob metachars.
9502 /// C glob.c:3551 — `for (; (c = *s); s++) zstrtok(...)` — only
9503 /// recognized glob bytes get replaced with their token form.
9504 #[test]
9505 fn tokenize_pure_ascii_unchanged() {
9506 let _g = crate::test_util::global_state_lock();
9507 let mut s = String::from("abc");
9508 tokenize(&mut s);
9509 assert_eq!(s, "abc");
9510 }
9511
9512 /// `tokenize("")` on empty input returns empty unchanged.
9513 #[test]
9514 fn tokenize_empty_unchanged() {
9515 let _g = crate::test_util::global_state_lock();
9516 let mut s = String::new();
9517 tokenize(&mut s);
9518 assert_eq!(s, "");
9519 }
9520
9521 /// `remnulargs("abc")` is a no-op for pure ASCII (no inull bytes).
9522 /// C glob.c:3649-3680 — only Snull/Dnull/Bnull/Bnullkeep/Nularg
9523 /// are stripped or transformed.
9524 #[test]
9525 fn remnulargs_pure_ascii_unchanged() {
9526 let _g = crate::test_util::global_state_lock();
9527 let mut s = String::from("hello");
9528 remnulargs(&mut s);
9529 assert_eq!(s, "hello");
9530 }
9531
9532 /// `remnulargs("")` returns empty unchanged (c:3653 early exit).
9533 #[test]
9534 fn remnulargs_empty_returns_empty() {
9535 let _g = crate::test_util::global_state_lock();
9536 let mut s = String::new();
9537 remnulargs(&mut s);
9538 assert_eq!(s, "");
9539 }
9540
9541 /// `remnulargs` strips Snull (single-quote scope marker).
9542 /// C glob.c:3656 — inull predicate includes Snull (0x84).
9543 /// Input `"a\u{e184}b"` → `"ab"` (strip the Snull byte).
9544 #[test]
9545 fn remnulargs_strips_snull_marker() {
9546 let _g = crate::test_util::global_state_lock();
9547 let mut s = format!("a{}b", crate::ported::zsh_h::Snull);
9548 remnulargs(&mut s);
9549 assert_eq!(s, "ab", "Snull byte should be stripped");
9550 }
9551
9552 /// `remnulargs` strips Dnull (double-quote scope marker).
9553 #[test]
9554 fn remnulargs_strips_dnull_marker() {
9555 let _g = crate::test_util::global_state_lock();
9556 let mut s = format!("a{}b", crate::ported::zsh_h::Dnull);
9557 remnulargs(&mut s);
9558 assert_eq!(s, "ab", "Dnull byte should be stripped");
9559 }
9560
9561 /// `remnulargs` strips Bnull (backslash-quoted marker — c:3658
9562 /// `inull(c)` includes Bnull). The non-Bnullkeep variant of the
9563 /// active-backslash marker.
9564 #[test]
9565 fn remnulargs_strips_bnull_marker() {
9566 let _g = crate::test_util::global_state_lock();
9567 let mut s = format!("a{}b", crate::ported::zsh_h::Bnull);
9568 remnulargs(&mut s);
9569 assert_eq!(s, "ab", "Bnull byte should be stripped");
9570 }
9571
9572 /// `remnulargs` on a single Nularg returns the Nularg sentinel
9573 /// preserved. C glob.c:3690-3692 — if post-strip output is empty
9574 /// AND original had any inull, emit Nularg as the empty-arg marker.
9575 #[test]
9576 fn remnulargs_empty_post_strip_emits_nularg_sentinel() {
9577 let _g = crate::test_util::global_state_lock();
9578 let mut s = format!("{}", crate::ported::zsh_h::Snull);
9579 remnulargs(&mut s);
9580 assert_eq!(
9581 s,
9582 format!("{}", crate::ported::zsh_h::Nularg),
9583 "all-inull input should collapse to single Nularg marker"
9584 );
9585 }
9586
9587 /// `remnulargs` on the Bnullkeep marker: c:3669 — Bnullkeep
9588 /// either (a) preserved as-is when it appears BEFORE any other
9589 /// inull (scan phase), or (b) transformed to literal `\` (copy
9590 /// phase). Pin the simpler case: a lone Bnullkeep is stripped.
9591 #[test]
9592 fn remnulargs_lone_bnullkeep_stripped() {
9593 let _g = crate::test_util::global_state_lock();
9594 let mut s = format!("a{}b", crate::ported::zsh_h::Bnullkeep);
9595 remnulargs(&mut s);
9596 // C behavior depends on whether the byte is in scan or copy
9597 // phase. Faithful port should give either "ab" (scan strip)
9598 // or "a\\b" (copy phase materialize). Either is acceptable;
9599 // pin that it's NOT the raw input.
9600 assert!(
9601 s == "ab" || s == "a\\b",
9602 "Bnullkeep should be stripped or materialized; got {s:?}"
9603 );
9604 }
9605
9606 // ═══════════════════════════════════════════════════════════════════
9607 // Additional C-parity tests for Src/glob.c file_type + hasbraces.
9608 // ═══════════════════════════════════════════════════════════════════
9609
9610 /// c:2018 — file_type for regular file with no exec bits → ' '.
9611 #[test]
9612 #[cfg(unix)]
9613 fn file_type_regular_non_exec_returns_space() {
9614 let r = file_type(libc::S_IFREG as u32 | 0o644);
9615 assert_eq!(r, ' ');
9616 }
9617
9618 /// c:2018 — file_type for regular file WITH exec bit → '*'.
9619 #[test]
9620 #[cfg(unix)]
9621 fn file_type_regular_exec_returns_star() {
9622 let r = file_type(libc::S_IFREG as u32 | 0o755);
9623 assert_eq!(r, '*');
9624 }
9625
9626 /// c:2018 — file_type for directory → '/'.
9627 #[test]
9628 #[cfg(unix)]
9629 fn file_type_directory_returns_slash() {
9630 let r = file_type(libc::S_IFDIR as u32 | 0o755);
9631 assert_eq!(r, '/');
9632 }
9633
9634 /// c:2018 — file_type for symlink → '@'.
9635 #[test]
9636 #[cfg(unix)]
9637 fn file_type_symlink_returns_at() {
9638 let r = file_type(libc::S_IFLNK as u32 | 0o777);
9639 assert_eq!(r, '@');
9640 }
9641
9642 /// c:2018 — file_type for FIFO → '|'.
9643 #[test]
9644 #[cfg(unix)]
9645 fn file_type_fifo_returns_pipe() {
9646 let r = file_type(libc::S_IFIFO as u32 | 0o644);
9647 assert_eq!(r, '|');
9648 }
9649
9650 /// c:2018 — file_type for socket → '='.
9651 #[test]
9652 #[cfg(unix)]
9653 fn file_type_socket_returns_equals() {
9654 let r = file_type(libc::S_IFSOCK as u32 | 0o777);
9655 assert_eq!(r, '=');
9656 }
9657
9658 /// c:2018 — file_type for char device → '%'.
9659 #[test]
9660 #[cfg(unix)]
9661 fn file_type_char_device_returns_percent() {
9662 let r = file_type(libc::S_IFCHR as u32 | 0o644);
9663 assert_eq!(r, '%');
9664 }
9665
9666 /// c:2018 — file_type for block device → '#'.
9667 #[test]
9668 #[cfg(unix)]
9669 fn file_type_block_device_returns_hash() {
9670 let r = file_type(libc::S_IFBLK as u32 | 0o644);
9671 assert_eq!(r, '#');
9672 }
9673
9674 /// c:2018 — file_type for unknown mode → '?'.
9675 #[test]
9676 fn file_type_unknown_mode_returns_question() {
9677 // Bare 0 mode (none of the S_IF* bits set) → '?'.
9678 assert_eq!(file_type(0), '?');
9679 }
9680
9681 /// c:2018 — file_type is deterministic.
9682 #[test]
9683 #[cfg(unix)]
9684 fn file_type_is_deterministic() {
9685 for mode in [
9686 libc::S_IFREG as u32 | 0o644,
9687 libc::S_IFDIR as u32,
9688 libc::S_IFLNK as u32,
9689 0u32,
9690 ] {
9691 let first = file_type(mode);
9692 for _ in 0..5 {
9693 assert_eq!(file_type(mode), first);
9694 }
9695 }
9696 }
9697
9698 /// c:2042 — hasbraces on plain string returns false.
9699 #[test]
9700 fn hasbraces_no_braces_returns_false() {
9701 let _g = crate::test_util::global_state_lock();
9702 assert!(!hasbraces("hello", false));
9703 assert!(!hasbraces("", false));
9704 }
9705
9706 /// c:2042 — hasbraces on plain ASCII `{a,b}` returns FALSE because
9707 /// the C port checks TOKEN bytes (Inbrace=0x8f / Outbrace=0x90 /
9708 /// Comma=0x9a) not literal ASCII. The lexer pre-tokenizes input
9709 /// before hasbraces runs in the real pipeline. Pin the actual
9710 /// behavior so a regen that adds ASCII fast-path is caught.
9711 #[test]
9712 fn hasbraces_plain_ascii_braces_returns_false_uses_tokens() {
9713 let _g = crate::test_util::global_state_lock();
9714 assert!(
9715 !hasbraces("{a,b}", false),
9716 "plain ASCII not tokenized → false; lexer pre-tokenizes in real pipeline"
9717 );
9718 }
9719
9720 /// c:2042 — hasbraces on tokenized brace expansion returns true.
9721 /// Construct the TOKEN form: Inbrace + 'a' + Comma + 'b' + Outbrace.
9722 #[test]
9723 fn hasbraces_tokenized_brace_expansion_returns_true() {
9724 let _g = crate::test_util::global_state_lock();
9725 let tokenized = format!(
9726 "{}a{}b{}",
9727 '\u{e18f}', // Inbrace token
9728 '\u{e19a}', // Comma token
9729 '\u{e190}' // Outbrace token
9730 );
9731 assert!(
9732 hasbraces(&tokenized, false),
9733 "Inbrace+a+Comma+b+Outbrace must trigger brace expansion"
9734 );
9735 }
9736
9737 /// c:2042 — hasbraces on unclosed `{` returns false (no matching pair).
9738 #[test]
9739 fn hasbraces_unclosed_returns_false() {
9740 let _g = crate::test_util::global_state_lock();
9741 assert!(!hasbraces("{", false));
9742 assert!(!hasbraces("{abc", false));
9743 }
9744
9745 // ═══════════════════════════════════════════════════════════════════
9746 // Additional C-parity tests for Src/glob.c
9747 // c:950 qgetnum / c:994 qgetmodespec / c:1250 glob_exec_string /
9748 // c:1351 checkglobqual / c:1539 hasbraces / c:1617 bracechardots /
9749 // c:1802 matchpat / c:1899 getmatch / c:1505 file_type
9750 // ═══════════════════════════════════════════════════════════════════
9751
9752 /// c:950 — `qgetnum("")` empty returns None.
9753 #[test]
9754 fn qgetnum_empty_returns_none() {
9755 assert!(qgetnum("").is_none(), "empty → None");
9756 }
9757
9758 /// c:950 — `qgetnum` returns Option<(i64, &str)> (type pin).
9759 #[test]
9760 fn qgetnum_returns_option_i64_str_tuple_type() {
9761 let _: Option<(i64, &str)> = qgetnum("42");
9762 }
9763
9764 /// c:950 — `qgetnum("123")` returns Some((123, "")) (canonical decimal).
9765 #[test]
9766 fn qgetnum_canonical_decimal_parses() {
9767 let r = qgetnum("123");
9768 assert!(r.is_some(), "valid digits → Some");
9769 let (n, rest) = r.unwrap();
9770 assert_eq!(n, 123);
9771 assert_eq!(rest, "");
9772 }
9773
9774 /// c:994 — `qgetmodespec("")` empty returns None.
9775 #[test]
9776 fn qgetmodespec_empty_returns_none() {
9777 assert!(qgetmodespec("").is_none());
9778 }
9779
9780 /// c:1539 — `hasbraces("")` empty returns false.
9781 #[test]
9782 fn hasbraces_empty_returns_false() {
9783 let _g = crate::test_util::global_state_lock();
9784 assert!(!hasbraces("", false));
9785 }
9786
9787 /// c:1539 — `hasbraces` is pure (no side effects).
9788 #[test]
9789 fn hasbraces_is_pure() {
9790 let _g = crate::test_util::global_state_lock();
9791 for s in ["", "abc", "{", "{a,b}", "no braces"] {
9792 let first = hasbraces(s, false);
9793 for _ in 0..3 {
9794 assert_eq!(
9795 hasbraces(s, false),
9796 first,
9797 "hasbraces({:?}, false) must be pure",
9798 s
9799 );
9800 }
9801 }
9802 }
9803
9804 /// c:1505 — `file_type(0)` returns char (compile-time type pin).
9805 #[test]
9806 fn file_type_returns_char_type() {
9807 let _: char = file_type(0);
9808 }
9809
9810 /// c:1505 — `file_type` is pure for arbitrary mode.
9811 #[test]
9812 fn file_type_is_pure() {
9813 for m in [0u32, 0o100000, 0o040000, 0o120000, 0o140000, 0o160000] {
9814 let first = file_type(m);
9815 for _ in 0..3 {
9816 assert_eq!(file_type(m), first, "file_type({:o}) must be pure", m);
9817 }
9818 }
9819 }
9820
9821 /// c:1802 — `matchpat("", "", false, false)` returns bool (type pin).
9822 #[test]
9823 fn matchpat_returns_bool_type() {
9824 let _g = crate::test_util::global_state_lock();
9825 let _: bool = matchpat("", "", false, false);
9826 }
9827
9828 /// c:2710 — `getmatch` returns C's int status and writes the result
9829 /// back through the `char **sp` out-pointer (type pin).
9830 #[test]
9831 fn getmatch_returns_string_type() {
9832 let _g = crate::test_util::global_state_lock();
9833 let mut sp = String::new();
9834 let _: i32 = getmatch(&mut sp, "", 0, 0, None);
9835 }
9836
9837 /// c:2676-2686 — a pattern without backreferences substitutes the
9838 /// replacement once and leaves SUB_DOSUBST clear.
9839 #[test]
9840 fn compgetmatch_plain_pattern_substitutes_replstr_once() {
9841 let _g = crate::test_util::global_state_lock();
9842 let mut fl = 0;
9843 let mut r = Some("X".to_string());
9844 assert!(compgetmatch("a*", &mut fl, &mut r).is_some());
9845 assert_eq!(fl & SUB_DOSUBST, 0);
9846 assert_eq!(r.as_deref(), Some("X"));
9847 }
9848}