zsh/ported/zle/compmatch.rs
1//! Completion matching engine for ZLE
2//!
3//! Port from zsh/Src/Zle/compmatch.c (2,974 lines)
4//!
5//! This compares two cmatchers and returns non-zero if they are equal. // c:80
6//! Add the given matchers to the bmatcher list. // c:97
7//! This returns a new Cline structure. // c:140
8//!
9//! The full matching engine is in compsys/matching.rs (458 lines).
10//! This module provides the pattern matching, anchor handling, and
11//! match line construction used during completion.
12//!
13//! Key C functions and their Rust locations:
14//! - match_str → crate::compsys::matching::match_str()
15//! - match_parts → crate::compsys::matching::match_parts()
16//! - comp_match → crate::compsys::matching::comp_match()
17//! - pattern_match_equivalence → crate::compsys::matching (inline)
18//! - add_match_str/part/sub → crate::compsys::matching (inline)
19//! - cline_* (match line ops) → inline below; the compsys::base
20//! `CompletionLine` shim was deleted.
21
22// CompMatcher / MatchFlags / CompLine deleted — Rust-invented structs
23// with no C counterpart. The legit C types `Cmatcher` (comp.h:153),
24// `Cline` (comp.h:245), and `Cpattern` (comp.h:197) are ported in
25// `comp_h.rs` and used by the real porters of `match_str` /
26// `pattern_match` / `add_match_str` etc. below.
27
28use crate::ported::pattern::pattry;
29use crate::ported::utils::set_noerrs;
30use crate::ported::zle::comp_h::{
31 Cline, Cmatcher, Cmlist, Cpattern, CLF_DIFF, CLF_JOIN, CLF_LINE, CLF_MATCHED, CLF_MISS,
32 CLF_NEW, CLF_SUF, CMF_INTER, CMF_LEFT, CMF_LINE, CMF_RIGHT, CPAT_ANY, CPAT_CCLASS, CPAT_CHAR,
33 CPAT_EQUIV, CPAT_NCLASS,
34};
35use crate::ported::zle::compcore::{mstack, multiquote, tildequote, useqbr};
36use crate::ported::zle::zle_h::{brinfo, ZC_tolower, ZC_toupper};
37#[allow(unused_imports)]
38use crate::ported::zle::{
39 deltochar::*, textobjects::*, zle_hist::*, zle_main::*, zle_misc::*, zle_move::*,
40 zle_params::*, zle_refresh::*, zle_tricky::*, zle_utils::*, zle_vi::*, zle_word::*,
41};
42use crate::ported::zsh_h::{PP_LOWER, PP_RANGE, PP_UPPER};
43use std::sync::{Mutex, OnceLock};
44
45/// Port of `cpatterns_same(Cpattern a, Cpattern b)` from `Src/Zle/compmatch.c:42`.
46/// ```c
47/// static int
48/// cpatterns_same(Cpattern a, Cpattern b)
49/// {
50/// while (a) {
51/// if (!b) return 0;
52/// if (a->tp != b->tp) return 0;
53/// switch (a->tp) {
54/// case CPAT_CCLASS: case CPAT_NCLASS: case CPAT_EQUIV:
55/// if (strcmp(a->u.str, b->u.str) != 0) return 0;
56/// break;
57/// case CPAT_CHAR:
58/// if (a->u.chr != b->u.chr) return 0;
59/// break;
60/// default:
61/// break;
62/// }
63/// a = a->next;
64/// b = b->next;
65/// }
66/// return !b;
67/// }
68/// ```
69/// Walk two parallel `Cpattern` chains testing structural equality
70/// (same `tp` + same `str` for class types or same `chr` for
71/// CPAT_CHAR). Used by `cmatchers_same` to dedupe matcher specs.
72/// WARNING: param names don't match C — Rust=(b) vs C=(a, b)
73
74// --- AUTO: cross-zle hoisted-fn use glob ---
75/// `cpatterns_same` — see implementation.
76#[allow(unused_imports)]
77#[allow(unused_imports)]
78
79pub fn cpatterns_same(
80 // c:44
81 mut a: Option<&Cpattern>,
82 mut b: Option<&Cpattern>,
83) -> bool {
84 // c:42
85 while let Some(ap) = a {
86 // c:46 while (a)
87 let bp = match b {
88 // c:47
89 None => return false, // c:48 if(!b) return 0
90 Some(p) => p,
91 };
92 if ap.tp != bp.tp {
93 // c:49
94 return false; // c:50
95 }
96 match ap.tp {
97 // c:51
98 x if x == CPAT_CCLASS || x == CPAT_NCLASS || x == CPAT_EQUIV => {
99 // c:52-54
100 // c:55-58 — equivalent ranges might compare same even when
101 // strings differ; the C source admits this is unhandled.
102 if ap.str != bp.str {
103 // c:60 strcmp(a->u.str,b->u.str)
104 return false; // c:61
105 }
106 }
107 x if x == CPAT_CHAR => {
108 // c:64
109 if ap.chr != bp.chr {
110 // c:65
111 return false; // c:66
112 }
113 }
114 _ => { // c:69 default
115 // c:70 — "here to silence compiler"
116 }
117 }
118 a = ap.next.as_deref(); // c:74 a = a->next
119 b = bp.next.as_deref(); // c:75 b = b->next
120 }
121 b.is_none() // c:77 return !b
122}
123
124/// Port of `cmatchers_same(Cmatcher a, Cmatcher b)` from `Src/Zle/compmatch.c:82`.
125/// ```c
126/// static int
127/// cmatchers_same(Cmatcher a, Cmatcher b)
128/// {
129/// return (a == b ||
130/// (a->flags == b->flags &&
131/// a->llen == b->llen && a->wlen == b->wlen &&
132/// (!a->llen || cpatterns_same(a->line, b->line)) &&
133/// (a->wlen <= 0 || cpatterns_same(a->word, b->word)) &&
134/// (!(a->flags & (CMF_LEFT | CMF_RIGHT)) ||
135/// (a->lalen == b->lalen && a->ralen == b->ralen &&
136/// (!a->lalen || cpatterns_same(a->left, b->left)) &&
137/// (!a->ralen || cpatterns_same(a->right, b->right))))));
138/// }
139/// ```
140/// Test two matchers for full structural equality — flags, lengths,
141/// patterns, and (if anchored) anchor patterns must all match.
142/// WARNING: param names don't match C — Rust=(b) vs C=(a, b)
143pub fn cmatchers_same(
144 // c:84
145 a: &Cmatcher,
146 b: &Cmatcher,
147) -> bool {
148 // c:82
149 // c:86 — `a == b` short-circuit (pointer identity). Rust uses
150 // `std::ptr::eq` for the same effect.
151 if std::ptr::eq(a, b) {
152 return true;
153 }
154 // c:87 — `a->flags == b->flags && a->llen == b->llen && a->wlen == b->wlen`.
155 if a.flags != b.flags || a.llen != b.llen || a.wlen != b.wlen {
156 return false;
157 }
158 // c:89 — `(!a->llen || cpatterns_same(a->line, b->line))`.
159 if a.llen != 0 && !cpatterns_same(a.line.as_deref(), b.line.as_deref()) {
160 return false;
161 }
162 // c:90 — `(a->wlen <= 0 || cpatterns_same(a->word, b->word))`.
163 if a.wlen > 0 && !cpatterns_same(a.word.as_deref(), b.word.as_deref()) {
164 return false;
165 }
166 // c:91-94 — anchor checks only if CMF_LEFT/CMF_RIGHT flagged.
167 if (a.flags & (CMF_LEFT | CMF_RIGHT)) != 0 {
168 if a.lalen != b.lalen || a.ralen != b.ralen {
169 // c:92
170 return false;
171 }
172 if a.lalen != 0 && !cpatterns_same(a.left.as_deref(), b.left.as_deref()) {
173 return false; // c:93
174 }
175 if a.ralen != 0 && !cpatterns_same(a.right.as_deref(), b.right.as_deref()) {
176 return false; // c:94
177 }
178 }
179 true
180}
181
182/// Direct port of `mod_export void add_bmatchers(Cmatcher m)` from
183/// `Src/Zle/compmatch.c:101`. Walks the supplied Cmatcher chain
184/// (the head of `def->matcher` at call sites) and prepends each
185/// matcher that qualifies for brace-matching to the file-scope
186/// `bmatchers` Cmlist. Original chain head is appended after the new
187/// entries so the final list is `[new_entries..., old_bmatchers...]`.
188pub fn add_bmatchers(m: Option<&Cmatcher>) {
189 // c:101
190 let cell = crate::ported::zle::compcore::bmatchers.get_or_init(|| Mutex::new(None));
191 let old = cell.lock().ok().and_then(|mut g| g.take()); // c:104 Cmlist old = bmatchers
192 // c:105-113 — qualify each m; prepend matches in C order (reversed
193 // iter so the final list is `[new_entries..., old]` per c:114 *q=old).
194 let mut head = old;
195 for mat in std::iter::successors(m, |p| p.next.as_deref())
196 .collect::<Vec<_>>()
197 .into_iter()
198 .rev()
199 // c:105 walk m
200 {
201 let qual = (mat.flags == 0 && mat.wlen > 0 && mat.llen > 0) // c:107-108
202 || (mat.flags == CMF_RIGHT && mat.wlen < 0 && mat.llen == 0);
203 if qual {
204 // c:109-112
205 head = Some(Box::new(Cmlist {
206 next: head,
207 matcher: Box::new(mat.clone()),
208 str: String::new(),
209 }));
210 }
211 }
212 if let Ok(mut g) = cell.lock() {
213 *g = head;
214 }
215}
216
217/// Direct port of `mod_export void update_bmatchers(void)` from
218/// `Src/Zle/compmatch.c:121`. Called when mstack changes — ensures
219/// `bmatchers` contains no matchers absent from `mstack`.
220pub fn update_bmatchers() {
221 // c:121
222 let bm_cell = crate::ported::zle::compcore::bmatchers.get_or_init(|| Mutex::new(None));
223 let ms_cell = mstack.get_or_init(|| Mutex::new(None));
224 let mut p = bm_cell.lock().ok().and_then(|mut g| g.take()); // c:124 Cmlist p = bmatchers
225 let ms_head = ms_cell
226 .lock()
227 .ok()
228 .and_then(|g| g.as_ref().map(|b| (**b).clone()));
229 let mut new_bmatchers: Option<Box<Cmlist>> = p.as_ref().map(|b| (**b).clone()).map(Box::new);
230 while let Some(node) = p {
231 // c:128 while (p)
232 let mut t = false; // c:129 t = 0
233 let mut ms = ms_head.as_ref(); // c:130 ms = mstack
234 while let Some(mscur) = ms {
235 if t {
236 break;
237 }
238 let mut mp = Some(mscur.matcher.as_ref()); // c:131 mp = ms->matcher
239 while let Some(mpcur) = mp {
240 if t {
241 break;
242 }
243 t = cmatchers_same(mpcur, &*node.matcher); // c:132 cmatchers_same
244 mp = mpcur.next.as_deref();
245 }
246 ms = mscur.next.as_deref();
247 }
248 p = node.next; // c:134 p = p->next
249 if !t {
250 // c:135 if (!t)
251 new_bmatchers = p.as_ref().map(|b| (**b).clone()).map(Box::new); // c:136 bmatchers = p
252 }
253 }
254 if let Ok(mut g) = bm_cell.lock() {
255 *g = new_bmatchers;
256 }
257}
258
259/// Port of `Cline get_cline(char *l, int ll, char *w, int wl, char *o,
260/// int ol, int fl)` from Src/Zle/compmatch.c:144.
261///
262/// "Returns a new Cline structure." The C version pools freed Clines
263/// via the `freecl` heap; Rust uses normal allocation so the pool
264/// dance collapses to a `Box::new`. Sets `word`/`wlen`/`line`/`llen`/
265/// `orig`/`olen`/`flags` per the args; clears `prefix`/`suffix`/`min`/
266/// `max`/`slen`.
267pub fn get_cline(
268 l: Option<String>,
269 ll: i32,
270 w: Option<String>,
271 wl: i32, // c:144
272 o: Option<String>,
273 ol: i32,
274 fl: i32,
275) -> Box<Cline> {
276 Box::new(Cline {
277 next: None, // c:156
278 line: l, // c:157
279 llen: ll,
280 word: w, // c:158
281 wlen: wl,
282 orig: o, // c:160
283 olen: ol,
284 slen: 0, // c:161
285 flags: fl, // c:162
286 prefix: None, // c:163
287 suffix: None,
288 min: 0, // c:164
289 max: 0,
290 })
291}
292
293/// Port of `free_cline(Cline l)` from `Src/Zle/compmatch.c:171`.
294/// ```c
295/// void
296/// free_cline(Cline l)
297/// {
298/// Cline n;
299/// while (l) {
300/// n = l->next;
301/// l->next = freecl;
302/// freecl = l;
303/// free_cline(l->prefix);
304/// free_cline(l->suffix);
305/// l = n;
306/// }
307/// }
308/// ```
309/// Free a Cline list. C pushes onto a `freecl` free-list to recycle;
310/// Rust just drops via Box.
311pub fn free_cline(l: Option<Box<Cline>>) {
312 // c:172
313 // c:172-183 — walk; free each prefix/suffix recursively. In Rust
314 // dropping the Box of the list head triggers Drop on `next`/
315 // `prefix`/`suffix` chains automatically. `freecl` recycling
316 // is a C-only zhalloc optimisation that doesn't apply here.
317 drop(l);
318}
319
320/// Port of `cp_cline(Cline l, int deep)` from `Src/Zle/compmatch.c:189`.
321/// ```c
322/// Cline
323/// cp_cline(Cline l, int deep)
324/// {
325/// Cline r = NULL, *p = &r, t, lp = NULL;
326/// while (l) {
327/// if ((t = freecl)) freecl = t->next;
328/// else t = (Cline) zhalloc(sizeof(*t));
329/// memcpy(t, l, sizeof(*t));
330/// if (deep) {
331/// if (t->prefix) t->prefix = cp_cline(t->prefix, 0);
332/// if (t->suffix) t->suffix = cp_cline(t->suffix, 0);
333/// }
334/// *p = lp = t;
335/// p = &(t->next);
336/// l = l->next;
337/// }
338/// *p = NULL;
339/// return r;
340/// }
341/// ```
342/// Deep- or shallow-copy a Cline list. `deep` recursively copies
343/// the prefix/suffix sub-lists too. The C source draws from a
344/// freecl free-list when available — Rust just heap-allocates.
345/// WARNING: param names don't match C — Rust=(deep) vs C=(l, deep)
346pub fn cp_cline(
347 // c:190
348 l: Option<&Cline>,
349 deep: i32,
350) -> Option<Box<Cline>> {
351 // c:189
352 let mut r: Option<Box<Cline>> = None; // c:192 r = NULL
353 let mut tail: *mut Option<Box<Cline>> = &mut r;
354 let mut cur = l;
355 while let Some(node) = cur {
356 // c:194 while (l)
357 // c:198 — `t = (Cline) zhalloc(sizeof(*t))`.
358 // c:199 — `memcpy(t, l, sizeof(*t))`.
359 let mut t: Box<Cline> = Box::new(node.clone());
360 // Reset `next` so the memcpy-equivalent doesn't link to the
361 // source's next (the loop sets it via the tail pointer).
362 t.next = None;
363 if deep != 0 {
364 // c:200 if (deep)
365 // c:201-202 — `t->prefix = cp_cline(t->prefix, 0)`. Already
366 // a Box-clone via memcpy; rebuild as deep copy.
367 if let Some(pre) = node.prefix.as_deref() {
368 t.prefix = cp_cline(Some(pre), 0); // c:202
369 }
370 if let Some(suf) = node.suffix.as_deref() {
371 t.suffix = cp_cline(Some(suf), 0); // c:204
372 }
373 }
374 // c:206 — `*p = lp = t`. Append to tail.
375 // SAFETY: `tail` points into `r` or into the previous node's
376 // `next` field; both stay valid for the loop's lifetime.
377 unsafe {
378 *tail = Some(t);
379 // c:207 — `p = &(t->next)`. Re-aim tail at the new entry's `next`.
380 let new_node = (*tail).as_mut().unwrap();
381 tail = &mut new_node.next;
382 }
383 cur = node.next.as_deref(); // c:208 l = l->next
384 }
385 // c:210 — `*p = NULL`. Already None by default.
386 r // c:212 return r
387}
388
389// =====================================================================
390// cline_sublen / cline_setlens / cline_matched / revert_cline / cp_cline
391// — `Src/Zle/compmatch.c:217-281`.
392// =====================================================================
393
394/// Port of `cline_sublen(Cline l)` from `Src/Zle/compmatch.c:218`.
395/// ```c
396/// int
397/// cline_sublen(Cline l)
398/// {
399/// int len = ((l->flags & CLF_LINE) ? l->llen : l->wlen);
400/// if (l->olen && !((l->flags & CLF_SUF) ? l->suffix : l->prefix))
401/// len += l->olen;
402/// else {
403/// Cline p;
404/// for (p = l->prefix; p; p = p->next)
405/// len += ((p->flags & CLF_LINE) ? p->llen : p->wlen);
406/// for (p = l->suffix; p; p = p->next)
407/// len += ((p->flags & CLF_LINE) ? p->llen : p->wlen);
408/// }
409/// return len;
410/// }
411/// ```
412/// Total visual length of one Cline plus its prefix/suffix sub-lists.
413pub fn cline_sublen(l: &Cline) -> i32 {
414 // c:219
415 // c:221 — `len = (CLF_LINE ? llen : wlen)`.
416 let mut len: i32 = if (l.flags & CLF_LINE) != 0 {
417 l.llen
418 } else {
419 l.wlen
420 };
421 // c:223 — `if (olen && !((CLF_SUF ? suffix : prefix))) len += olen`.
422 let no_subs = if (l.flags & CLF_SUF) != 0 {
423 l.suffix.is_none()
424 } else {
425 l.prefix.is_none()
426 };
427 if l.olen != 0 && no_subs {
428 len += l.olen; // c:224
429 } else {
430 // c:225
431 // c:228-229 — walk prefix sub-list summing per-part length.
432 let mut p = l.prefix.as_deref();
433 while let Some(pp) = p {
434 len += if (pp.flags & CLF_LINE) != 0 {
435 pp.llen
436 } else {
437 pp.wlen
438 };
439 p = pp.next.as_deref();
440 }
441 // c:230-231 — walk suffix sub-list.
442 let mut p = l.suffix.as_deref();
443 while let Some(pp) = p {
444 len += if (pp.flags & CLF_LINE) != 0 {
445 pp.llen
446 } else {
447 pp.wlen
448 };
449 p = pp.next.as_deref();
450 }
451 }
452 len // c:233 return len
453}
454
455/// Port of `cline_setlens(Cline l, int both)` from `Src/Zle/compmatch.c:240`.
456/// ```c
457/// void
458/// cline_setlens(Cline l, int both)
459/// {
460/// while (l) {
461/// l->min = cline_sublen(l);
462/// if (both)
463/// l->max = l->min;
464/// l = l->next;
465/// }
466/// }
467/// ```
468/// Walk a Cline list setting `min` (and optionally `max`) from
469/// `cline_sublen`.
470pub fn cline_setlens(l: &mut Option<Box<Cline>>, both: i32) {
471 // c:240
472 let mut cur = l.as_deref_mut();
473 while let Some(node) = cur {
474 // c:242 while (l)
475 let s = cline_sublen(node); // c:243 cline_sublen(l)
476 node.min = s; // c:243 l->min = ...
477 if both != 0 {
478 // c:244 if (both)
479 node.max = s; // c:245 l->max = l->min
480 }
481 cur = node.next.as_deref_mut(); // c:246 l = l->next
482 }
483}
484
485// =====================================================================
486// matchbuf / matchparts / matchsubs globals + start_match / abort_match
487// — `Src/Zle/compmatch.c:283-317`.
488// =====================================================================
489
490/// Port of `cline_matched(Cline p)` from `Src/Zle/compmatch.c:254`.
491/// ```c
492/// void
493/// cline_matched(Cline p)
494/// {
495/// while (p) {
496/// p->flags |= CLF_MATCHED;
497/// cline_matched(p->prefix);
498/// cline_matched(p->suffix);
499/// p = p->next;
500/// }
501/// }
502/// ```
503/// Set `CLF_MATCHED` on every Cline reachable through next/prefix/
504/// suffix from `p`.
505pub fn cline_matched(p: &mut Option<Box<Cline>>) {
506 // c:254
507 let mut cur = p.as_deref_mut();
508 while let Some(node) = cur {
509 // c:256 while (p)
510 node.flags |= CLF_MATCHED; // c:257
511 cline_matched(&mut node.prefix); // c:258
512 cline_matched(&mut node.suffix); // c:259
513 cur = node.next.as_deref_mut(); // c:261 p = p->next
514 }
515}
516
517/// Port of `revert_cline(Cline p)` from `Src/Zle/compmatch.c:269`.
518/// ```c
519/// Cline
520/// revert_cline(Cline p)
521/// {
522/// Cline r = NULL, n;
523/// while (p) {
524/// n = p->next;
525/// p->next = r;
526/// r = p;
527/// p = n;
528/// }
529/// return r;
530/// }
531/// ```
532/// Reverse a Cline `next`-chained list in place; returns the new head.
533/// WARNING: param names don't match C — Rust=() vs C=(p)
534pub fn revert_cline(
535 // c:270
536 mut p: Option<Box<Cline>>,
537) -> Option<Box<Cline>> {
538 // c:269
539 let mut r: Option<Box<Cline>> = None; // c:272 r = NULL
540 while let Some(mut node) = p {
541 // c:274 while (p)
542 let n = node.next.take(); // c:275 n = p->next
543 node.next = r; // c:276 p->next = r
544 r = Some(node); // c:277 r = p
545 p = n; // c:278 p = n
546 }
547 r // c:280 return r
548}
549
550/// Port of `start_match()` from `Src/Zle/compmatch.c:300`.
551/// ```c
552/// static void
553/// start_match(void)
554/// {
555/// if (matchbuf)
556/// *matchbuf = '\0';
557/// matchbufadded = 0;
558/// matchparts = matchlastpart = matchsubs = matchlastsub = NULL;
559/// }
560/// ```
561/// Reset the per-match globals so a fresh pattern run starts clean.
562pub fn start_match() {
563 // c:300
564 // c:300-303 — `if (matchbuf) *matchbuf = '\0'`.
565 MATCHBUF
566 .get_or_init(|| Mutex::new(String::new()))
567 .lock()
568 .unwrap()
569 .clear();
570 // c:305 — `matchparts = matchlastpart = matchsubs = matchlastsub = NULL`.
571 // All FOUR must reset. Omitting MATCHLASTPART/MATCHLASTSUB left them stale
572 // across matches: the next add_match_part read a Some MATCHLASTPART and took
573 // the append branch (`lastpart->next = p`) against the disconnected old tail
574 // while MATCHPARTS head stayed None, so `pli = matchparts` came back empty —
575 // partial-match Cline lost (e.g. `ls /dir/a<TAB>` dropped the typed `a`).
576 *MATCHPARTS.get_or_init(|| Mutex::new(None)).lock().unwrap() = None;
577 *MATCHLASTPART.get_or_init(|| Mutex::new(None)).lock().unwrap() = None;
578 *MATCHSUBS.get_or_init(|| Mutex::new(None)).lock().unwrap() = None;
579 *MATCHLASTSUB.get_or_init(|| Mutex::new(None)).lock().unwrap() = None;
580}
581
582/// Port of `abort_match()` from `Src/Zle/compmatch.c:312`.
583/// ```c
584/// static void
585/// abort_match(void)
586/// {
587/// free_cline(matchparts);
588/// free_cline(matchsubs);
589/// matchparts = matchsubs = NULL;
590/// }
591/// ```
592/// C body (compmatch.c:312, 3 lines):
593/// `free_cline(matchparts);
594/// free_cline(matchsubs);
595/// matchparts = matchsubs = NULL;`
596/// The `take()` on each guard discards the old chain (Rust drop runs
597/// `free_cline`) and leaves the slot None — same observable state.
598pub fn abort_match() {
599 // c:312
600 free_cline(
601 MATCHPARTS
602 .get_or_init(|| Mutex::new(None))
603 .lock()
604 .unwrap()
605 .take(),
606 ); // c:313
607 free_cline(
608 MATCHSUBS
609 .get_or_init(|| Mutex::new(None))
610 .lock()
611 .unwrap()
612 .take(),
613 ); // c:314
614}
615
616/// Direct port of `static void add_match_str(Cmatcher m, char *l,
617/// char *w, int wl, int sfx)`
618/// from `Src/Zle/compmatch.c:327`. Pushes the string `w` (or
619/// `l` when `m & CMF_LINE`) of length `wl` into the file-scope
620/// `MATCHBUF` accumulator; `sfx` prepends instead of appends.
621pub fn add_match_str(
622 m: Option<&Cmatcher>, // c:327
623 l: &str,
624 w: &str,
625 mut wl: i32,
626 sfx: i32,
627) {
628 // c:332-334 — `if (m && (m->flags & CMF_LINE)) { wl = m->llen; w = l; }`.
629 let (eff_w_owned, eff_w): (String, &str) = match m {
630 Some(mat) if (mat.flags & CMF_LINE) != 0 => {
631 wl = mat.llen;
632 let owned = l.to_string();
633 let s = owned.clone();
634 (owned, Box::leak(s.into_boxed_str()))
635 }
636 _ => (String::new(), w),
637 };
638 let _ = eff_w_owned;
639
640 if wl <= 0 {
641 return;
642 } // c:335
643
644 // c:337-353 — buffer-grow + insert. Rust's String handles the
645 // grow path; we still mirror the matchbufadded counter for parity
646 // with `MATCHBUFLEN`-checking C call sites.
647 if let Ok(mut buf) = MATCHBUF.get_or_init(|| Mutex::new(String::new())).lock() {
648 let take_n = wl as usize;
649 let new_chunk: String = eff_w.chars().take(take_n).collect();
650 if sfx != 0 {
651 // c:354 prefix-mode
652 *buf = format!("{}{}", new_chunk, *buf); // c:356
653 } else {
654 // c:358
655 buf.push_str(&new_chunk);
656 }
657 MATCHBUFADDED.fetch_add(wl, std::sync::atomic::Ordering::Relaxed); // c:362
658 }
659}
660
661/// Direct port of `static void add_match_part(Cmatcher m, char *l,
662/// char *w, int wl,
663/// char *o, int ol,
664/// char *s, int sl,
665/// int osl, int sfx)`
666/// from `Src/Zle/compmatch.c:373`. Appends a partial match into
667/// `MATCHPARTS`, splitting the new part via `bld_parts` per the
668/// matcher's anchor rules and consuming any pending `MATCHSUBS`
669/// nodes into the new tail.
670pub fn add_match_part(
671 m: Option<&Cmatcher>, // c:373
672 l: Option<&str>,
673 _ll: i32,
674 w: &str,
675 wl: i32,
676 o: Option<&str>,
677 ol: i32,
678 s: &str,
679 sl: i32,
680 osl: i32,
681 sfx: i32,
682) {
683 // c:382 — `if (l && !strncmp(l, w, wl)) l = NULL` — drop redundant anchor.
684 let l_eff: Option<String> = match l {
685 Some(lstr)
686 if lstr.len() >= wl as usize && wl > 0 && &lstr[..wl as usize] == &w[..wl as usize] =>
687 {
688 None
689 }
690 Some(lstr) => Some(lstr.to_string()),
691 None => None,
692 };
693
694 // c:392 — `p = bld_parts(s, sl, osl, &lp, &lprem)`.
695 let mut lp: Option<Box<Cline>> = None;
696 let mut lprem: Option<Box<Cline>> = None;
697 let mut p = bld_parts(s, sl, osl, Some(&mut lp), Some(&mut lprem));
698
699 // c:394 — `if (lprem && m && (m->flags & CLF_LEFT))`.
700 if let Some(rem) = lprem.as_mut() {
701 if m.map(|mat| (mat.flags & CMF_LEFT) != 0).unwrap_or(false) {
702 rem.flags |= CLF_SUF; // c:395
703 rem.suffix = rem.prefix.take(); // c:396 swap
704 }
705 }
706
707 // c:402 — `if (sfx) p = revert_cline(lp = p)`.
708 if sfx != 0 {
709 if let Some(chain) = p.take() {
710 p = revert_cline(Some(chain));
711 }
712 }
713
714 // c:405-419 — merge MATCHSUBS into the head/tail.
715 let subs = MATCHSUBS
716 .get_or_init(|| Mutex::new(None))
717 .lock()
718 .ok()
719 .and_then(|mut g| g.take());
720 if let Some(subs_chain) = subs {
721 // c:405
722 if let Some(lp_node) = lp.as_mut() {
723 if sfx != 0 {
724 // c:407 lp->prefix tail-append
725 let mut tail_ref: *mut Option<Box<Cline>> = &mut lp_node.prefix;
726 unsafe {
727 while let Some(ref mut next_node) = *tail_ref {
728 tail_ref = &mut next_node.next as *mut _;
729 }
730 *tail_ref = Some(subs_chain);
731 }
732 } else if let Some(ref mut p_node) = p {
733 // c:415 p->prefix prepend
734 let old_prefix = p_node.prefix.take();
735 let mut new_head = subs_chain;
736 {
737 let mut tail_ref: *mut Option<Box<Cline>> = &mut new_head.next;
738 unsafe {
739 while let Some(ref mut nn) = *tail_ref {
740 tail_ref = &mut nn.next as *mut _;
741 }
742 *tail_ref = old_prefix;
743 }
744 }
745 p_node.prefix = Some(new_head);
746 }
747 }
748 // c:417 — `matchsubs = matchlastsub = NULL`.
749 if let Ok(mut g) = MATCHLASTSUB.get_or_init(|| Mutex::new(None)).lock() {
750 *g = None;
751 }
752 }
753
754 // c:421-435 — store args in the last part-cline.
755 if let Some(lp_node) = lp.as_mut() {
756 if lp_node.llen != 0 || lp_node.wlen != 0 {
757 // c:421
758 let next = get_cline(
759 l_eff.clone(),
760 wl,
761 Some(w.to_string()),
762 wl,
763 o.map(|s| s.to_string()),
764 ol,
765 CLF_NEW,
766 );
767 lp_node.next = Some(next); // c:423
768 } else {
769 // c:425
770 lp_node.line = l_eff.clone(); // c:426
771 lp_node.llen = wl;
772 lp_node.word = Some(w.to_string()); // c:428
773 lp_node.wlen = wl;
774 lp_node.orig = o.map(|s| s.to_string()); // c:430
775 lp_node.olen = ol;
776 }
777 if o.is_some() || ol != 0 {
778 // c:432
779 lp_node.flags &= !CLF_NEW;
780 }
781 }
782
783 // c:439-444 — append `p` to MATCHPARTS via MATCHLASTPART.
784 let last_present = MATCHLASTPART
785 .get()
786 .and_then(|c| c.lock().ok().map(|g| g.is_some()))
787 .unwrap_or(false);
788 if last_present {
789 // c:440
790 if let Ok(mut tail) = MATCHLASTPART.get_or_init(|| Mutex::new(None)).lock() {
791 if let Some(t) = tail.as_mut() {
792 t.next = p.clone();
793 }
794 }
795 } else if let Ok(mut head) = MATCHPARTS.get_or_init(|| Mutex::new(None)).lock() {
796 *head = p.clone(); // c:442
797 }
798 if let Some(lp_node) = lp {
799 if let Ok(mut tail) = MATCHLASTPART.get_or_init(|| Mutex::new(None)).lock() {
800 *tail = Some(lp_node); // c:443
801 }
802 }
803}
804
805// Fake `parse_cmatcher` / `update_bmatchers` deleted.
806// `parse_cmatcher` already exists at `complete.rs:992` as a real
807// port of `Src/Zle/complete.c:242`. `update_bmatchers` is at
808// `Src/Zle/compmatch.c:121` with signature `void update_bmatchers(void)`
809// — the Rust placeholder had the wrong arity and type, will land
810// alongside the matcher-engine driver.
811
812/// Direct port of `static void add_match_sub(Cmatcher m, char *l, int ll,
813/// char *w, int wl)` from
814/// `Src/Zle/compmatch.c:446`. Pushes one sub-match cline node
815/// into the file-scope `MATCHSUBS` / `MATCHLASTSUB` linked list.
816/// Called from match_str during a CMF_RIGHT anchor match.
817pub fn add_match_sub(
818 m: Option<&Cmatcher>, // c:446
819 l: Option<&str>,
820 ll: i32,
821 w: Option<&str>,
822 wl: i32,
823) {
824 // c:450-453 — `if (m && (m->flags & CMF_LINE)) { wl = m->llen; w = l; }`.
825 let (eff_w, eff_wl) = match m {
826 Some(mat) if (mat.flags & CMF_LINE) != 0 => (l, mat.llen),
827 _ => (w, wl),
828 };
829
830 // c:455-456 — short-circuit if no length.
831 if eff_wl <= 0 && ll <= 0 {
832 return;
833 }
834
835 // c:464-484 — build a fresh Cline node and append to matchsubs.
836 let node = Box::new(Cline {
837 flags: CLF_NEW,
838 line: l.map(|s| s.to_string()),
839 llen: ll,
840 word: eff_w.map(|s| s.to_string()),
841 wlen: eff_wl,
842 ..Default::default()
843 });
844
845 let last_cell = MATCHLASTSUB.get_or_init(|| Mutex::new(None));
846 let head_cell = MATCHSUBS.get_or_init(|| Mutex::new(None));
847 let last_present = last_cell.lock().ok().map(|g| g.is_some()).unwrap_or(false);
848 if last_present {
849 // c:494 — chain to existing tail
850 if let Ok(mut tail) = last_cell.lock() {
851 if let Some(t) = tail.as_mut() {
852 t.next = Some(node.clone()); // c:495 matchlastsub->next = n
853 }
854 }
855 } else {
856 // c:496 — first node
857 if let Ok(mut h) = head_cell.lock() {
858 *h = Some(node.clone()); // c:497 matchsubs = n
859 }
860 }
861 if let Ok(mut tail) = last_cell.lock() {
862 *tail = Some(node); // c:499 matchlastsub = n
863 }
864}
865
866// Real-port of `match_str` lands below. The exact-char skip fast
867// path (c:569-590), non-* matcher loop with CMF_LEFT/RIGHT anchors
868// (c:868-989), and *-pattern matcher loop in both prefix and
869// suffix modes (c:603-867 / c:735-776) are all real-bodied.
870
871/// Direct port of `static int match_str(char *l, char *w, Brinfo *bpp,
872/// int bc, int *rwlp, const int sfx,
873/// int test, int part)`
874/// from `Src/Zle/compmatch.c:500-1085`. The matcher application
875/// engine: walks the line string `l` against the word string `w`
876/// using each `Cmlist` in the global `mstack` chain. Builds
877/// `matchparts` / `matchsubs` along the way, threads brace-position
878/// info via `bpp`. Returns the number of `w` bytes consumed on a
879/// full match, -1 on no match.
880///
881/// **Port scope:** all matcher paths real-bodied — exact-char skip
882/// fast path (c:569-590), non-* matcher loop with CMF_LEFT/RIGHT
883/// anchors + pattern_match + add_match_str/sub emit (c:868-989),
884/// *-pattern matcher loop in prefix mode (c:603-867) and suffix
885/// mode (c:735-776 with bounded recursive call), exact-rewind
886/// retry (c:1020-1034), test/part-mode returns (c:1046-1084).
887pub fn match_str(
888 // c:500
889 l_in: &str,
890 w_in: &str,
891 _bpp: Option<&mut Option<Box<brinfo>>>,
892 bc: i32,
893 rwlp: Option<&mut i32>,
894 sfx: i32,
895 test: i32,
896 part: i32,
897) -> i32 {
898 let l_bytes = l_in.as_bytes();
899 let w_bytes = w_in.as_bytes();
900 let mut ll = l_bytes.len() as i32;
901 let mut lw = w_bytes.len() as i32;
902 // c:517 — `const int original_ll = ll, original_lw = lw;` used by the
903 // anti-recursion guard below (c:592-597).
904 let original_ll = ll;
905 let original_lw = lw;
906 let mut il: i32 = 0;
907 let mut iw: i32 = 0;
908 let mut exact: i32 = 0;
909 let mut wexact: i32 = 0;
910 let mut bc = bc;
911 let _obc = bc;
912 let add: i32 = if sfx != 0 { -1 } else { 1 };
913 let ind: i32 = if sfx != 0 { -1 } else { 0 };
914
915 if test == 0 {
916 // c:523
917 start_match();
918 }
919
920 // Track positions as byte indices. In sfx mode we walk from the
921 // end backwards; ind=-1 means "previous byte". We use signed
922 // cursors so the arithmetic mirrors C's pointer arithmetic.
923 let mut l_pos: i32 = if sfx != 0 { ll } else { 0 };
924 let mut w_pos: i32 = if sfx != 0 { lw } else { 0 };
925 let mut ow_pos: i32 = w_pos;
926 let mut lm: Option<Box<Cmatcher>> = None;
927 let mut he = 0i32;
928
929 // Snapshot the mstack chain into a Vec for stable iteration.
930 let mstack_snapshot: Vec<Box<Cmatcher>> = {
931 let g = mstack.get_or_init(|| Mutex::new(None)).lock().ok();
932 let mut out = Vec::new();
933 if let Some(g) = g {
934 let mut cur = g.as_deref();
935 while let Some(ms) = cur {
936 let mut mp_cur: Option<&Cmatcher> = Some(&*ms.matcher);
937 while let Some(mp) = mp_cur {
938 out.push(Box::new(mp.clone()));
939 mp_cur = mp.next.as_deref();
940 }
941 cur = ms.next.as_deref();
942 }
943 }
944 out
945 };
946
947 // c:591 `retry:` — the label sits AFTER the exact-char fast path, so
948 // C's `goto retry` (c:1029) re-runs the matcher loop WITHOUT re-doing
949 // the fast path. This one-shot flag reproduces that: when set, the
950 // next iteration skips the fast path and goes straight to the matcher
951 // loop. Without it the rewind below re-matches the same exact char
952 // forever (infinite loop on any word that shares a leading char with
953 // the prefix but then diverges, e.g. prefix "ec" vs "emulator").
954 let mut retry_skip_fastpath = false;
955 'outer: while ll > 0 {
956 // c:546
957 let do_fastpath = !retry_skip_fastpath;
958 retry_skip_fastpath = false;
959 // c:569-590 — exact-char skip fast path.
960 if do_fastpath && sfx == 0 && lw > 0 && (part == 0 || test != 0) {
961 let l_idx = (l_pos + ind) as usize;
962 let w_idx = (w_pos + ind) as usize;
963 if l_idx < l_bytes.len() && w_idx < w_bytes.len() {
964 let l_ch = l_bytes[l_idx];
965 let w_ch = w_bytes[w_idx];
966 let bslash = lw > 1
967 && w_ch == b'\\'
968 && w_idx + 1 < w_bytes.len()
969 && w_bytes[w_idx + 1] == l_bytes[(l_pos + ind) as usize];
970 if l_ch == w_ch || bslash {
971 let advance_w = if bslash { 2 } else { 1 };
972 l_pos += add;
973 w_pos += if bslash { add + add } else { add };
974 il += 1;
975 iw += advance_w;
976 ll -= 1;
977 lw -= advance_w;
978 bc += 1;
979 exact += 1;
980 wexact += advance_w;
981 lm = None;
982 he = 0;
983 continue 'outer; // c:589
984 }
985 }
986 }
987
988 // c:591 retry: walk the snapshotted matcher chain looking for
989 // a non-* matcher we can apply at the current cursor.
990 let mut matched: Option<Box<Cmatcher>> = None;
991 for mp in mstack_snapshot.iter() {
992 if let Some(ref lm_box) = lm {
993 if std::ptr::addr_eq(lm_box.as_ref() as *const _, mp.as_ref() as *const _) {
994 continue; // c:595
995 }
996 }
997 // c:592-597 — anti-recursion guard: in a recursive (test) call,
998 // don't apply a `*` (wlen<0) matcher at the very start of the line
999 // or word. Without this, a zero-progress `*`-match (e.g.
1000 // `r:|[._-]=*` against line "-") recurses match_str↔match_parts
1001 // forever → stack-overflow SIGBUS on `zsh -<TAB>`.
1002 if (original_ll == ll || original_lw == lw)
1003 && (test == 1 || (test != 0 && mp.left.is_none() && mp.right.is_none()))
1004 && mp.wlen < 0
1005 {
1006 continue; // c:597
1007 }
1008 if mp.wlen < 0 {
1009 // c:603-867 — `*`-pattern matcher. Handles both prefix
1010 // (sfx == 0) and suffix (sfx != 0) modes.
1011
1012 // c:689-694 — set up llen / alen / aol per CMF_LEFT.
1013 let llen_p = mp.llen;
1014 let (alen, aol): (i32, i32) = if (mp.flags & CMF_LEFT) != 0 {
1015 (mp.lalen, mp.ralen)
1016 } else {
1017 (mp.ralen, mp.lalen)
1018 };
1019 if ll < llen_p + alen || lw < alen + aol {
1020 // c:698
1021 continue;
1022 }
1023
1024 // c:701-715 — set ap/aop/moff/loff/aoff/both per CMF_LEFT
1025 // × sfx. Four combinations.
1026 let (ap, aop, moff, both, loff, aoff): (
1027 Option<&Cpattern>,
1028 Option<&Cpattern>,
1029 i32,
1030 i32,
1031 i32,
1032 i32,
1033 );
1034 if (mp.flags & CMF_LEFT) != 0 {
1035 // c:701
1036 ap = mp.left.as_deref();
1037 aop = mp.right.as_deref();
1038 moff = alen;
1039 if sfx != 0 {
1040 // c:703
1041 both = 0;
1042 loff = -llen_p;
1043 aoff = -(llen_p + alen);
1044 } else {
1045 // c:706
1046 both = 1;
1047 loff = alen;
1048 aoff = 0;
1049 }
1050 } else {
1051 // c:708
1052 ap = mp.right.as_deref();
1053 aop = mp.left.as_deref();
1054 moff = 0;
1055 if sfx != 0 {
1056 // c:710
1057 both = 1;
1058 loff = -(llen_p + alen);
1059 aoff = -alen;
1060 } else {
1061 // c:712
1062 both = 0;
1063 loff = 0;
1064 aoff = llen_p;
1065 }
1066 }
1067
1068 // c:717 — pattern_match(mp.line, l + loff).
1069 let l_off_idx = (l_pos + loff).max(0) as usize;
1070 if l_off_idx >= l_bytes.len() {
1071 continue;
1072 }
1073 let line_slice = std::str::from_utf8(&l_bytes[l_off_idx..]).unwrap_or("");
1074 if pattern_match(mp.line.as_deref(), line_slice, None, "") == 0 {
1075 continue;
1076 }
1077 // c:719-731 — anchor test.
1078 if let Some(ap_pat) = ap {
1079 let l_anchor_idx = (l_pos + aoff).max(0) as usize;
1080 let l_anchor = std::str::from_utf8(&l_bytes[l_anchor_idx..]).unwrap_or("");
1081 if pattern_match(Some(ap_pat), l_anchor, None, "") == 0 {
1082 continue;
1083 }
1084 if both != 0 {
1085 // c:721
1086 let w_anchor_idx = (w_pos + aoff).max(0) as usize;
1087 let w_anchor = std::str::from_utf8(&w_bytes[w_anchor_idx..]).unwrap_or("");
1088 if pattern_match(Some(ap_pat), w_anchor, None, "") == 0 {
1089 continue;
1090 }
1091 if aol > 0 && aol <= aoff + iw {
1092 let w_op_idx = (w_pos + aoff - aol).max(0) as usize;
1093 let w_op = std::str::from_utf8(&w_bytes[w_op_idx..]).unwrap_or("");
1094 if pattern_match(aop, w_op, None, "") == 0 {
1095 continue;
1096 }
1097 }
1098 // c:726 — match_parts to confirm anchor span.
1099 let mp_l = std::str::from_utf8(&l_bytes[l_anchor_idx..]).unwrap_or("");
1100 let mp_w = std::str::from_utf8(&w_bytes[(w_pos + aoff).max(0) as usize..])
1101 .unwrap_or("");
1102 if match_parts(mp_l, mp_w, alen, part) == 0 {
1103 continue;
1104 }
1105 }
1106 } else {
1107 // c:728
1108 let cmf_check = if (mp.flags & CMF_INTER) != 0 {
1109 if (mp.flags & CMF_LINE) != 0 {
1110 iw
1111 } else {
1112 il
1113 }
1114 } else {
1115 il | iw
1116 };
1117 if both == 0 || cmf_check != 0 {
1118 continue;
1119 }
1120 }
1121
1122 // c:737-773 — recursive scan: try each tp from w forward
1123 // looking for a position where `l + llen + moff` matches.
1124 let mut t = 0i32;
1125 let mut ct = 0i32;
1126 let ict_total = lw - alen + 1;
1127 let mut found_tp_pos: i32 = w_pos;
1128 // c:737 — tp walks from w outward. In prefix mode (add=+1)
1129 // forward through w[w_pos..]; in sfx mode (add=-1)
1130 // backward through w[..w_pos]. We iterate ict_total
1131 // steps, computing tp_pos as w_pos + ct*add.
1132 for step in 0..ict_total.max(0) {
1133 let tp_pos = w_pos + step * add;
1134 let mut accept = false;
1135 if both != 0 {
1136 // c:740-745 — both-mode: succeed only if ap stops
1137 // matching at the current tp (the `*` consumed
1138 // characters before reaching the anchor).
1139 let ap_fails = ap.is_none() || test == 0 || {
1140 let tp_anchor_idx = (tp_pos + aoff).max(0) as usize;
1141 let tp_slice =
1142 std::str::from_utf8(&w_bytes.get(tp_anchor_idx..).unwrap_or(&[]))
1143 .unwrap_or("");
1144 pattern_match(ap, tp_slice, None, "") == 0
1145 };
1146 if ap_fails {
1147 accept = true;
1148 }
1149 } else {
1150 // c:746-753 — non-both: succeed when ap matches at
1151 // tp - moff and aop matches at tp - moff - aol.
1152 let tp_anchor_idx = (tp_pos - moff).max(0) as usize;
1153 let tp_slice =
1154 std::str::from_utf8(&w_bytes.get(tp_anchor_idx..).unwrap_or(&[]))
1155 .unwrap_or("");
1156 if pattern_match(ap, tp_slice, None, "") != 0 {
1157 let aol_ok = aol == 0
1158 || (aol <= iw + ct - moff && {
1159 let aop_idx = (tp_pos - moff - aol).max(0) as usize;
1160 let aop_slice =
1161 std::str::from_utf8(&w_bytes.get(aop_idx..).unwrap_or(&[]))
1162 .unwrap_or("");
1163 pattern_match(aop, aop_slice, None, "") != 0
1164 });
1165 if aol_ok {
1166 let l_aoff_idx = (l_pos + aoff).max(0) as usize;
1167 let l_aoff_slice =
1168 std::str::from_utf8(&l_bytes[l_aoff_idx..]).unwrap_or("");
1169 let mp_ok = mp.wlen == -1
1170 || match_parts(l_aoff_slice, tp_slice, alen, part) != 0;
1171 if mp_ok {
1172 accept = true;
1173 }
1174 }
1175 }
1176 }
1177
1178 // c:753-755 — for a variable-length (`*`) non-both matcher,
1179 // hard-verify the anchor span with match_parts; C BREAKS the
1180 // tp-scan if it fails. The port omitted this guard, so a
1181 // zero-progress `*`-match (e.g. `r:|[._-]=*` against line
1182 // "-") recursed match_str→match_str forever → stack-overflow
1183 // SIGBUS on `zsh -<TAB>`.
1184 if accept && both == 0 && mp.wlen == -1 {
1185 let l_aoff_idx = (l_pos + aoff).max(0) as usize;
1186 let l_aoff_slice =
1187 std::str::from_utf8(&l_bytes[l_aoff_idx..]).unwrap_or("");
1188 let tp_anchor_idx = (tp_pos - moff).max(0) as usize;
1189 let tp_slice =
1190 std::str::from_utf8(&w_bytes.get(tp_anchor_idx..).unwrap_or(&[]))
1191 .unwrap_or("");
1192 if match_parts(l_aoff_slice, tp_slice, alen, part) == 0 {
1193 break;
1194 }
1195 }
1196
1197 if accept {
1198 // c:757-769 — recursive match_str call.
1199 if sfx != 0 {
1200 // c:763 — l-ll, w-lw with bounded slices.
1201 // C uses savl + tp[-alen] NUL trick; in Rust
1202 // we pass slice up to position (l_pos - llen_p
1203 // - alen) for l (the "savl" boundary) and up
1204 // to (tp_pos - alen) for w (the "savw"
1205 // boundary).
1206 let l_bound = (l_pos - llen_p - alen).max(0) as usize;
1207 let w_bound = (tp_pos - alen).max(0) as usize;
1208 let l_rest =
1209 std::str::from_utf8(&l_bytes[..l_bound.min(l_bytes.len())])
1210 .unwrap_or("");
1211 let w_rest =
1212 std::str::from_utf8(&w_bytes[..w_bound.min(w_bytes.len())])
1213 .unwrap_or("");
1214 t = match_str(l_rest, w_rest, None, 0, None, sfx, 2, part);
1215 } else {
1216 // c:768 — l + llen + moff, tp + moff.
1217 let l_rest_start = (l_pos + llen_p + moff) as usize;
1218 let l_rest =
1219 std::str::from_utf8(&l_bytes.get(l_rest_start..).unwrap_or(&[]))
1220 .unwrap_or("");
1221 let w_rest_start = (tp_pos + moff) as usize;
1222 let w_rest =
1223 std::str::from_utf8(&w_bytes.get(w_rest_start..).unwrap_or(&[]))
1224 .unwrap_or("");
1225 t = match_str(l_rest, w_rest, None, 0, None, sfx, 1, part);
1226 }
1227 if t != 0 || (mp.wlen == -1 && both == 0) {
1228 found_tp_pos = tp_pos;
1229 break;
1230 }
1231 }
1232 ct += 1;
1233 }
1234
1235 // c:780 — no match found in the recursive scan.
1236 if t == 0 {
1237 continue;
1238 }
1239
1240 // c:783-833 — emit Cline parts via add_match_*.
1241 let _tp_pos = found_tp_pos;
1242 if test == 0 && (he == 0 || (llen_p + alen) != 0) {
1243 // c:789-805 — op/ol/lp/map/wap/wmp computed per sfx mode.
1244 let (op_start, ol, lp_start, map_start, wap_start, wmp_start);
1245 if sfx != 0 {
1246 // c:789
1247 op_start = w_pos as usize;
1248 ol = (ow_pos - w_pos).max(0);
1249 lp_start = (l_pos - (llen_p + alen)).max(0) as usize;
1250 map_start = (found_tp_pos - alen).max(0) as usize;
1251 if (mp.flags & CMF_LEFT) != 0 {
1252 // c:792
1253 wap_start = (found_tp_pos - alen).max(0) as usize;
1254 wmp_start = found_tp_pos as usize;
1255 } else {
1256 // c:794
1257 wap_start = (w_pos - alen).max(0) as usize;
1258 wmp_start = (found_tp_pos - alen).max(0) as usize;
1259 }
1260 } else {
1261 // c:797
1262 op_start = ow_pos as usize;
1263 ol = (w_pos - ow_pos).max(0);
1264 lp_start = l_pos as usize;
1265 map_start = ow_pos as usize;
1266 if (mp.flags & CMF_LEFT) != 0 {
1267 // c:800
1268 wap_start = w_pos as usize;
1269 wmp_start = (w_pos + alen) as usize;
1270 } else {
1271 // c:802
1272 wap_start = found_tp_pos as usize;
1273 wmp_start = ow_pos as usize;
1274 }
1275 }
1276
1277 if (mp.flags & CMF_LINE) != 0 {
1278 // c:810
1279 let op_str =
1280 std::str::from_utf8(&w_bytes[op_start..op_start + ol as usize])
1281 .unwrap_or("");
1282 let lp_str = std::str::from_utf8(
1283 &l_bytes[lp_start..lp_start + (llen_p + alen) as usize],
1284 )
1285 .unwrap_or("");
1286 add_match_str(None, "", op_str, ol, sfx);
1287 add_match_str(None, "", lp_str, llen_p + alen, sfx);
1288 add_match_sub(None, None, ol, Some(op_str), ol);
1289 add_match_sub(None, None, llen_p + alen, Some(lp_str), llen_p + alen);
1290 } else {
1291 // c:822
1292 let map_len = ct + ol + alen;
1293 let map_str = std::str::from_utf8(
1294 &w_bytes[map_start
1295 ..(map_start + map_len.max(0) as usize).min(w_bytes.len())],
1296 )
1297 .unwrap_or("");
1298 add_match_str(None, "", map_str, map_len, sfx);
1299 let ol_eff = if both != 0 {
1300 let op_str =
1301 std::str::from_utf8(&w_bytes[op_start..op_start + ol as usize])
1302 .unwrap_or("");
1303 add_match_sub(None, None, ol, Some(op_str), ol);
1304 -1
1305 } else {
1306 ct + ol
1307 };
1308 let l_aoff_idx = (l_pos + aoff).max(0) as usize;
1309 let l_loff_idx = (l_pos + loff).max(0) as usize;
1310 let l_aoff_str = std::str::from_utf8(
1311 &l_bytes[l_aoff_idx..l_aoff_idx + alen.max(0) as usize],
1312 )
1313 .unwrap_or("");
1314 let l_loff_str = std::str::from_utf8(
1315 &l_bytes[l_loff_idx..l_loff_idx + llen_p.max(0) as usize],
1316 )
1317 .unwrap_or("");
1318 let wap_str = std::str::from_utf8(
1319 &w_bytes
1320 [wap_start..(wap_start + alen.max(0) as usize).min(w_bytes.len())],
1321 )
1322 .unwrap_or("");
1323 let wmp_str = std::str::from_utf8(
1324 &w_bytes[wmp_start
1325 ..(wmp_start + ol_eff.max(0) as usize).min(w_bytes.len())],
1326 )
1327 .unwrap_or("");
1328 add_match_part(
1329 Some(mp),
1330 Some(l_aoff_str),
1331 alen,
1332 wap_str,
1333 alen,
1334 Some(l_loff_str),
1335 llen_p,
1336 wmp_str,
1337 ol_eff,
1338 ol_eff,
1339 sfx,
1340 );
1341 }
1342 }
1343
1344 // c:834-866 — advance pointers past the matched portion
1345 // + anchor. In sfx mode positions decrement; in prefix
1346 // mode they increment.
1347 let llen_new = llen_p + alen;
1348 let alen_new = alen + ct;
1349 if sfx != 0 {
1350 // c:836
1351 l_pos -= llen_new;
1352 w_pos -= alen_new;
1353 } else {
1354 // c:839
1355 l_pos += llen_new;
1356 w_pos += alen_new;
1357 }
1358 ll -= llen_new;
1359 il += llen_new;
1360 lw -= alen_new;
1361 iw += alen_new;
1362 bc += llen_new;
1363 exact = 0;
1364 ow_pos = w_pos;
1365
1366 if llen_new == 0 && alen_new == 0 {
1367 // c:856
1368 lm = Some(Box::new((**mp).clone()));
1369 if he == 0 {
1370 he = 1;
1371 } else {
1372 // signal outer loop continue
1373 matched = Some(mp.clone());
1374 break;
1375 }
1376 } else {
1377 lm = None;
1378 he = 0;
1379 }
1380 matched = Some(mp.clone());
1381 break;
1382 }
1383 if ll < mp.llen || lw < mp.wlen {
1384 continue;
1385 } // c:868
1386
1387 // c:880-884 — skip if line and word substrings are identical
1388 // (the exact-char skip above already handled trivial overlap).
1389 if (mp.flags & (CMF_LEFT | CMF_RIGHT)) == 0 && mp.llen == mp.wlen {
1390 let (l_start, w_start) = if sfx != 0 {
1391 ((l_pos - mp.llen) as usize, (w_pos - mp.wlen) as usize)
1392 } else {
1393 (l_pos as usize, w_pos as usize)
1394 };
1395 let l_chunk = &l_bytes[l_start..l_start + mp.llen as usize];
1396 let w_chunk = &w_bytes[w_start..w_start + mp.wlen as usize];
1397 if l_chunk == w_chunk {
1398 continue;
1399 }
1400 }
1401
1402 // c:889-897 — local cursors tl/tw/tll/tlw/til/tiw.
1403 let (tl_pos, tw_pos, til, tiw, tll, tlw) = if sfx != 0 {
1404 (
1405 l_pos - mp.llen,
1406 w_pos - mp.wlen,
1407 ll - mp.llen,
1408 lw - mp.wlen,
1409 il + mp.llen,
1410 iw + mp.wlen,
1411 )
1412 } else {
1413 (l_pos, w_pos, il, iw, ll, lw)
1414 };
1415
1416 let mut t: i32 = 1;
1417 // c:898-915 — CMF_LEFT anchor test.
1418 if (mp.flags & CMF_LEFT) != 0 {
1419 if til < mp.lalen || tiw < mp.lalen + mp.ralen {
1420 continue;
1421 }
1422 if let Some(ref left_pat) = mp.left {
1423 let l_anchor_start = (tl_pos - mp.lalen) as usize;
1424 let w_anchor_start = (tw_pos - mp.lalen) as usize;
1425 let l_slice = std::str::from_utf8(&l_bytes[l_anchor_start..]).unwrap_or("");
1426 let w_slice = std::str::from_utf8(&w_bytes[w_anchor_start..]).unwrap_or("");
1427 let lm_ok = pattern_match(Some(left_pat), l_slice, None, "") != 0;
1428 let wm_ok = pattern_match(Some(left_pat), w_slice, None, "") != 0;
1429 let r_ok = mp.ralen == 0 || {
1430 let r_anchor_start = (tw_pos - mp.lalen - mp.ralen) as usize;
1431 let r_slice = std::str::from_utf8(&w_bytes[r_anchor_start..]).unwrap_or("");
1432 let right_pat = mp.right.as_deref();
1433 pattern_match(right_pat, r_slice, None, "") != 0
1434 };
1435 t = if lm_ok && wm_ok && r_ok { 1 } else { 0 };
1436 } else {
1437 let cmf_check = if (mp.flags & CMF_INTER) != 0 {
1438 if (mp.flags & CMF_LINE) != 0 {
1439 iw
1440 } else {
1441 il
1442 }
1443 } else {
1444 il | iw
1445 };
1446 t = if sfx == 0 && cmf_check == 0 { 1 } else { 0 };
1447 }
1448 }
1449 // c:916-938 — CMF_RIGHT anchor test.
1450 if (mp.flags & CMF_RIGHT) != 0 {
1451 if tll < mp.llen + mp.ralen || tlw < mp.wlen + mp.ralen + mp.lalen {
1452 continue;
1453 }
1454 if let Some(ref right_pat) = mp.right {
1455 let l_anchor_start = (tl_pos + mp.llen) as usize;
1456 let w_anchor_start = (tw_pos + mp.wlen) as usize;
1457 let l_slice = std::str::from_utf8(&l_bytes[l_anchor_start..]).unwrap_or("");
1458 let w_slice = std::str::from_utf8(&w_bytes[w_anchor_start..]).unwrap_or("");
1459 let lm_ok = pattern_match(Some(right_pat), l_slice, None, "") != 0;
1460 let wm_ok = pattern_match(Some(right_pat), w_slice, None, "") != 0;
1461 let l_ok = mp.lalen == 0 || {
1462 let l_anchor_2 = (tw_pos + mp.wlen - mp.ralen - mp.lalen) as usize;
1463 let l_slice_2 = std::str::from_utf8(&w_bytes[l_anchor_2..]).unwrap_or("");
1464 let left_pat = mp.left.as_deref();
1465 pattern_match(left_pat, l_slice_2, None, "") != 0
1466 };
1467 t = if lm_ok && wm_ok && l_ok { 1 } else { 0 };
1468 } else {
1469 let cmf_check = if (mp.flags & CMF_INTER) != 0 {
1470 if (mp.flags & CMF_LINE) != 0 {
1471 iw
1472 } else {
1473 il
1474 }
1475 } else {
1476 il | iw
1477 };
1478 t = if sfx != 0 && cmf_check == 0 { 1 } else { 0 };
1479 }
1480 }
1481
1482 // c:940 — main pattern_match call.
1483 if t == 0 {
1484 continue;
1485 }
1486 let line_pat = mp.line.as_deref();
1487 let word_pat = mp.word.as_deref();
1488 let tl_slice = std::str::from_utf8(&l_bytes[tl_pos as usize..]).unwrap_or("");
1489 let tw_slice = std::str::from_utf8(&w_bytes[tw_pos as usize..]).unwrap_or("");
1490 if pattern_match(line_pat, tl_slice, word_pat, tw_slice) == 0 {
1491 continue;
1492 }
1493
1494 // c:944-967 — emit Cline parts via add_match_str/sub.
1495 if test == 0 {
1496 let carry_l = if sfx != 0 {
1497 if ow_pos >= w_pos {
1498 w_pos as usize
1499 } else {
1500 ow_pos as usize
1501 }
1502 } else {
1503 if w_pos >= ow_pos {
1504 ow_pos as usize
1505 } else {
1506 w_pos as usize
1507 }
1508 };
1509 let carry_len = if sfx != 0 {
1510 (ow_pos - w_pos).max(0)
1511 } else {
1512 (w_pos - ow_pos).max(0)
1513 };
1514 if carry_len > 0 {
1515 let carry_slice =
1516 std::str::from_utf8(&w_bytes[carry_l..carry_l + carry_len as usize])
1517 .unwrap_or("");
1518 add_match_str(None, "", carry_slice, carry_len, sfx);
1519 add_match_sub(None, None, 0, Some(carry_slice), carry_len);
1520 }
1521 // c:955 — main matcher str.
1522 let tw_str =
1523 std::str::from_utf8(&w_bytes[tw_pos as usize..(tw_pos + mp.wlen) as usize])
1524 .unwrap_or("");
1525 add_match_str(Some(mp), tl_slice, tw_str, mp.wlen, sfx);
1526 add_match_sub(Some(mp), Some(tl_slice), mp.llen, Some(tw_str), mp.wlen);
1527 }
1528
1529 // c:968-988 — advance pointers.
1530 if sfx != 0 {
1531 l_pos = tl_pos;
1532 w_pos = tw_pos;
1533 } else {
1534 l_pos += mp.llen;
1535 w_pos += mp.wlen;
1536 }
1537 il += mp.llen;
1538 iw += mp.wlen;
1539 ll -= mp.llen;
1540 lw -= mp.wlen;
1541 bc += mp.llen;
1542 exact = 0;
1543 ow_pos = w_pos;
1544 lm = None;
1545 he = 0;
1546 matched = Some(mp.clone());
1547 break;
1548 }
1549
1550 if matched.is_some() {
1551 // c:993
1552 continue 'outer;
1553 }
1554
1555 // c:998-1042 — no matcher matched at this position. Try the
1556 // "same character" skip again (in case the retry path failed).
1557 if (test == 0 || sfx != 0) && lw > 0 {
1558 let l_idx = (l_pos + ind) as usize;
1559 let w_idx = (w_pos + ind) as usize;
1560 if l_idx < l_bytes.len() && w_idx < w_bytes.len() {
1561 let l_ch = l_bytes[l_idx];
1562 let w_ch = w_bytes[w_idx];
1563 let bslash = lw > 1
1564 && w_ch == b'\\'
1565 && (w_idx + 1) < w_bytes.len()
1566 && w_bytes[w_idx + 1] == l_bytes[l_idx];
1567 if l_ch == w_ch || bslash {
1568 let advance_w = if bslash { 2 } else { 1 };
1569 l_pos += add;
1570 w_pos += if bslash { add + add } else { add };
1571 il += 1;
1572 iw += advance_w;
1573 ll -= 1;
1574 lw -= advance_w;
1575 bc += 1;
1576 lm = None;
1577 he = 0;
1578 continue 'outer;
1579 }
1580 }
1581 }
1582
1583 // c:1017 — break on lw=0 (suffix exhausted in non-test mode).
1584 if lw == 0 {
1585 break;
1586 }
1587
1588 // c:1020-1034 — retry path: rewind exact-skip if we have any
1589 // and retry the matcher loop preferring matchers.
1590 if exact > 0 && part == 0 {
1591 il -= exact;
1592 iw -= wexact;
1593 ll += exact;
1594 lw += wexact;
1595 bc -= exact;
1596 l_pos -= add * exact;
1597 w_pos -= add * wexact;
1598 exact = 0;
1599 wexact = 0;
1600 // c:1029 `goto retry` — re-enter the matcher loop but SKIP the
1601 // exact-char fast path (else it re-matches the just-rewound
1602 // char and loops forever). The flag makes the next iteration
1603 // start at the matcher loop.
1604 retry_skip_fastpath = true;
1605 continue 'outer;
1606 }
1607
1608 // c:1036-1041 — divergence with no matcher and no exact-rewind.
1609 if test != 0 {
1610 return 0;
1611 }
1612 abort_match();
1613 return -1;
1614 }
1615
1616 // c:1044-1046 — test-mode return.
1617 if test != 0 {
1618 return if part != 0 || ll == 0 { 1 } else { 0 };
1619 }
1620
1621 // c:1050-1054 — top-level: any remaining ll means abort.
1622 if part == 0 && ll != 0 {
1623 abort_match();
1624 return -1;
1625 }
1626
1627 // c:1055-1056 — rwlp writeback.
1628 if let Some(out) = rwlp {
1629 *out = iw
1630 - if sfx != 0 {
1631 ow_pos - w_pos
1632 } else {
1633 w_pos - ow_pos
1634 };
1635 }
1636
1637 // c:1083 — `*bpp = bp` (Brinfo writeback) — caller's bp is already
1638 // unmodified since the deep brace-pos tracking is conservative.
1639
1640 let _ = (lm, he);
1641 // c:1084 — return iw on full match, il in part mode.
1642 if part != 0 {
1643 il
1644 } else {
1645 iw
1646 }
1647}
1648
1649/// Direct port of `static int match_parts(char *l, char *w, int n,
1650/// int part)` from
1651/// `Src/Zle/compmatch.c:1092-1108`. Tests whether the first `n` bytes
1652/// of `l` match the first `n` bytes of `w` using the active mstack
1653/// matcher chain. C truncates both strings to length n with `'\0'`
1654/// (saving/restoring the boundary bytes); Rust takes slices.
1655pub fn match_parts(l: &str, w: &str, n: i32, part: i32) -> i32 {
1656 // c:1092
1657 let ln = (n as usize).min(l.len());
1658 let wn = (n as usize).min(w.len());
1659 let l_slice = &l[..ln];
1660 let w_slice = &w[..wn];
1661 // c:1101 — match_str(l, w, NULL, 0, NULL, 0, 1, part).
1662 match_str(l_slice, w_slice, None, 0, None, 0, 1, part)
1663}
1664
1665/// Direct port of `mod_export char *comp_match(char *pfx, char *sfx,
1666/// char *w, Patprog cp,
1667/// Cline *clp, int qu,
1668/// Brinfo *bpl, int bcp,
1669/// Brinfo *bsl, int bcs,
1670/// int *exact)`
1671/// from `Src/Zle/compmatch.c:1123-1257`. Applies the matcher chain to
1672/// candidate `w` against prefix `pfx` and suffix `sfx`. Returns the
1673/// matched string on success, None on no match. Writes the Cline
1674/// structure into `clp`, the "is exact match" flag into `exact`.
1675#[allow(clippy::too_many_arguments)]
1676pub fn comp_match(
1677 // c:1123
1678 pfx: &str,
1679 sfx: &str,
1680 w: &str,
1681 cp: Option<&crate::ported::pattern::Patprog>,
1682 clp: Option<&mut Option<Box<Cline>>>,
1683 qu: i32,
1684 _bpl: Option<&mut Option<Box<brinfo>>>,
1685 bcp: i32,
1686 _bsl: Option<&mut Option<Box<brinfo>>>,
1687 bcs: i32,
1688 exact: &mut i32,
1689) -> Option<String> {
1690 use crate::ported::glob::{remnulargs, tokenize};
1691 use crate::ported::lex::{parse_subst_string, untokenize};
1692 use std::sync::atomic::Ordering;
1693
1694 let r: String;
1695 if let Some(prog) = cp {
1696 // c:1129
1697 // c:1129-1167 — globcomplete pattern path.
1698 r = w.to_string();
1699 let teststr: String = if qu == 0 {
1700 // c:1135
1701 // c:1145-1153 — unquote a copy then pattry against the prog.
1702 let mut t = r.clone();
1703 tokenize(&mut t);
1704 set_noerrs(1);
1705 let parsed = parse_subst_string(&t).ok();
1706 set_noerrs(0);
1707 if let Some(p) = parsed {
1708 let mut p = p;
1709 remnulargs(&mut p);
1710 untokenize(&p)
1711 } else {
1712 r.clone()
1713 }
1714 } else {
1715 r.clone()
1716 };
1717 if !pattry(prog, &teststr) {
1718 // c:1157
1719 return None;
1720 }
1721 let r_final = if qu == 2 {
1722 tildequote(&r, 0)
1723 }
1724 // c:1160
1725 else {
1726 multiquote(&r, if qu != 0 { 0 } else { 1 })
1727 };
1728 // c:1164-1166 — build a Cline chain from the matched word.
1729 let wl = w.len() as i32;
1730 let lc = bld_parts(w, wl, wl, None, None);
1731 if let Some(out) = clp {
1732 *out = lc;
1733 }
1734 *exact = 0; // c:1167
1735 return Some(r_final);
1736 }
1737
1738 // c:1169 — mstack-driven path.
1739 let w_quoted = if qu == 2 {
1740 tildequote(w, 0)
1741 }
1742 // c:1172
1743 else {
1744 multiquote(w, if qu != 0 { 0 } else { 1 })
1745 };
1746 let wl = w_quoted.len() as i32;
1747
1748 // c:1177 — useqbr = qu.
1749 useqbr.store(qu, Ordering::Relaxed);
1750
1751 let mut rpl: i32 = 0;
1752 let mpl = match_str(pfx, &w_quoted, None, bcp, Some(&mut rpl), 0, 0, 0); // c:1178
1753 if mpl < 0 {
1754 return None;
1755 }
1756
1757 if !sfx.is_empty() {
1758 // c:1181
1759 // c:1182-1232 — also match suffix; combine prefix+suffix Cline.
1760 let mut rsl: i32 = 0;
1761 let suffix_start = (mpl as usize).min(w_quoted.len());
1762 let suffix_part = &w_quoted[suffix_start..];
1763 let msl = match_str(sfx, suffix_part, None, bcs, Some(&mut rsl), 1, 0, 0);
1764 if msl < 0 {
1765 return None; // c:1204
1766 }
1767 // c:1220 — add_match_str for the middle and saved prefix.
1768 let middle_len = (wl - rpl - rsl).max(0) as usize;
1769 let middle_start = (rpl as usize).min(w_quoted.len());
1770 let middle =
1771 &w_quoted[middle_start..middle_start + middle_len.min(w_quoted.len() - middle_start)];
1772 // c:1223 — bld_parts on the middle portion.
1773 let mid_lc = bld_parts(
1774 middle,
1775 (wl - rpl - rsl).max(0),
1776 (mpl - rpl) + (msl - rsl),
1777 None,
1778 None,
1779 );
1780 if let Some(out) = clp {
1781 *out = mid_lc;
1782 }
1783
1784 // c:1245-1251 — exact-match test.
1785 let pl = pfx.len();
1786 *exact =
1787 if w_quoted.len() >= pl && w_quoted.starts_with(pfx) && w_quoted[pl..].ends_with(sfx) {
1788 1
1789 } else {
1790 0
1791 };
1792 } else {
1793 // c:1233
1794 // c:1235-1239 — prefix-only path.
1795 let after_pfx_start = (rpl as usize).min(w_quoted.len());
1796 let after_pfx = &w_quoted[after_pfx_start..];
1797 // c:1235 — append the unmatched word remainder onto MATCHBUF so `r`
1798 // (below) reconstructs the full word, not just the matcher's own
1799 // contribution.
1800 add_match_str(None, "", after_pfx, (wl - rpl).max(0), 0); // c:1235
1801 // c:1237-1238 — `add_match_part(NULL, NULL, NULL, 0, NULL, 0,
1802 // w + rpl, wl - rpl, mpl - rpl, 0); pli = matchparts;`
1803 // This APPENDS to the matchparts chain already populated by
1804 // match_str (matcher subs + exact runs live in matchsubs and are
1805 // folded in here) — a bare bld_parts() would discard those and
1806 // drop exactly-matched interior chars from the reconstruction.
1807 add_match_part(
1808 None,
1809 None,
1810 0,
1811 "",
1812 0,
1813 None,
1814 0,
1815 after_pfx,
1816 (wl - rpl).max(0),
1817 mpl - rpl,
1818 0,
1819 ); // c:1237
1820 let pli = MATCHPARTS
1821 .get_or_init(|| Mutex::new(None))
1822 .lock()
1823 .ok()
1824 .and_then(|mut g| g.take()); // c:1239 pli = matchparts
1825 if let Some(out) = clp {
1826 *out = pli;
1827 }
1828
1829 // c:1251 — exact = !strcmp(pfx, w).
1830 *exact = if pfx == w_quoted.as_str() { 1 } else { 0 };
1831 }
1832
1833 // c:1241 — r = dupstring(matchbuf ? matchbuf : "").
1834 r = MATCHBUF
1835 .get()
1836 .and_then(|m| m.lock().ok().map(|g| g.clone()))
1837 .unwrap_or_default();
1838 let r = if r.is_empty() { w_quoted } else { r };
1839 Some(r)
1840}
1841
1842/// Port of `pattern_match1(Cpattern p, convchar_t c, int *mtp)` from Src/Zle/compmatch.c:1269.
1843/// Direct port of `mod_export convchar_t pattern_match1(Cpattern p,
1844/// convchar_t c, int *mtp)`
1845/// from `Src/Zle/compmatch.c:1269`. Tests whether `p` matches
1846/// the single char `c`, returning the matched-char (1 for ANY, the
1847/// char for CHAR, or for EQUIV the equivalence-class index+1) or 0
1848/// on miss. `mtp` is non-zero only for the EQUIV path.
1849/// WARNING: param names don't match C — Rust=(p, mtp) vs C=(p, c, mtp)
1850pub fn pattern_match1(
1851 p: &Cpattern, // c:1269
1852 c: u32,
1853 mtp: &mut i32,
1854) -> u32 {
1855 *mtp = 0; // c:1273
1856 match p.tp {
1857 // c:1274
1858 x if x == CPAT_CCLASS => {
1859 // c:1275
1860 // PATMATCHRANGE(p->u.str, c, NULL, NULL)
1861 patmatchrange(p.str.as_deref(), c, None, None) as u32 // c:1276
1862 }
1863 x if x == CPAT_NCLASS => {
1864 // c:1278
1865 if patmatchrange(p.str.as_deref(), c, None, None) {
1866 0
1867 } else {
1868 1
1869 } // c:1279
1870 }
1871 x if x == CPAT_EQUIV => {
1872 // c:1281
1873 let mut ind: u32 = 0;
1874 if patmatchrange(p.str.as_deref(), c, Some(&mut ind), Some(mtp)) {
1875 ind + 1 // c:1283
1876 } else {
1877 0 // c:1285
1878 }
1879 }
1880 x if x == CPAT_ANY => 1, // c:1288-1289
1881 x if x == CPAT_CHAR => {
1882 if p.chr == c {
1883 c
1884 } else {
1885 0
1886 }
1887 } // c:1291-1292
1888 _ => 0, // c:1294
1889 }
1890}
1891
1892/// Direct port of `mod_export convchar_t pattern_match_equivalence(
1893/// Cpattern lp, convchar_t wind, int wmtp,
1894/// convchar_t wchr)`
1895/// from `Src/Zle/compmatch.c:1316`. Looks up the line-side
1896/// equivalence-class member that pairs with word-side index
1897/// `wind` (1-based), then resolves case-class crossings via the
1898/// PP_UPPER/PP_LOWER pair.
1899///
1900/// Returns `CHR_INVALID` (u32::MAX) on miss; the matched line
1901/// char on success.
1902pub fn pattern_match_equivalence(
1903 lp: &Cpattern, // c:1316
1904 wind: u32,
1905 wmtp: i32,
1906 wchr: u32,
1907) -> u32 {
1908 // c:1324 — PATMATCHINDEX(lp->u.str, wind-1, &lchr, &lmtp).
1909 // Walk lp.str's encoded byte sequence finding the entry at index
1910 // (wind-1). Encoding (from parse_class):
1911 // 0x80 + PP_RANGE (=0x95): next two bytes are lo,hi range
1912 // 0x80 + PP_* (POSIX class id): single-byte class marker
1913 // plain byte: literal character
1914 let Some(ref bytes) = lp.str else {
1915 return u32::MAX;
1916 };
1917 let Some(target_idx) = (wind as i64).checked_sub(1) else {
1918 return u32::MAX;
1919 };
1920 if target_idx < 0 {
1921 return u32::MAX;
1922 }
1923 let mut lchr: Option<u32> = None;
1924 let mut lmtp: i32 = 0;
1925 let mut idx: i64 = 0;
1926 let mut i = 0usize;
1927 let pp_range_marker = (0x80u8).wrapping_add(PP_RANGE as u8);
1928 while i < bytes.len() {
1929 let b = bytes[i];
1930 if b == pp_range_marker {
1931 // c:4049 PP_RANGE
1932 // Next two bytes are range start / end.
1933 if i + 2 >= bytes.len() {
1934 break;
1935 }
1936 let r1 = bytes[i + 1];
1937 let r2 = bytes[i + 2];
1938 let span = (r2 as i64) - (r1 as i64);
1939 if span >= 0 && idx + span >= target_idx {
1940 // c:4057
1941 lchr = Some(((r1 as i64) + (target_idx - idx)) as u32);
1942 break;
1943 }
1944 idx += span + 1; // c:4062
1945 i += 3;
1946 } else if b >= 0x80 {
1947 // c:4024-4047 — POSIX class marker (PP_ALPHA/LOWER/UPPER/etc.).
1948 let swtype = (b as i32) - 0x80;
1949 if idx == target_idx {
1950 // c:4043
1951 lmtp = swtype;
1952 break;
1953 }
1954 idx += 1;
1955 i += 1;
1956 } else {
1957 // c:4071-4076 — literal char.
1958 if idx == target_idx {
1959 lchr = Some(b as u32);
1960 break;
1961 }
1962 idx += 1;
1963 i += 1;
1964 }
1965 }
1966
1967 // c:1335 — `if (lchr != CHR_INVALID) return lchr` — exact-char hit.
1968 if let Some(ch) = lchr {
1969 if ch != u32::MAX {
1970 return ch;
1971 }
1972 }
1973
1974 // c:1342 — case-class crossings using the now-tracked lmtp.
1975 let wch = char::from_u32(wchr).unwrap_or('\0');
1976 if wmtp == PP_UPPER && lmtp == PP_LOWER {
1977 return ZC_tolower(wch) as u32;
1978 }
1979 if wmtp == PP_LOWER && lmtp == PP_UPPER {
1980 return ZC_toupper(wch) as u32;
1981 }
1982 if wmtp != 0 && wmtp == lmtp {
1983 return wchr;
1984 }
1985 u32::MAX // c:1378
1986}
1987
1988/// Direct port of `static int pattern_match_restrict(Cpattern p,
1989/// Cpattern wp, convchar_t *wsc,
1990/// int wsclen, Cpattern prestrict,
1991/// ZLE_STRING_T new_line)`
1992/// from `Src/Zle/compmatch.c:1383`. The restricted variant of
1993/// `pattern_match`: each line-side char must additionally match
1994/// the corresponding `prestrict` Cpattern. Used when building the
1995/// line-string from a partial match. Writes the deduced line chars
1996/// into `new_line` and returns 1 on full match, 0 otherwise.
1997pub fn pattern_match_restrict(
1998 p: Option<&Cpattern>, // c:1383
1999 wp: Option<&Cpattern>,
2000 wsc: &[u32],
2001 prestrict: Option<&Cpattern>,
2002 new_line: &mut Vec<char>,
2003) -> i32 {
2004 let mut p_cur = p;
2005 let mut wp_cur = wp;
2006 let mut pr_cur = prestrict;
2007 let mut wsc_idx = 0usize;
2008
2009 while p_cur.is_some() && wp_cur.is_some() // c:1392
2010 && wsc_idx < wsc.len() && pr_cur.is_some()
2011 {
2012 let pat = p_cur.unwrap();
2013 let wpat = wp_cur.unwrap();
2014 let pre = pr_cur.unwrap();
2015 let wc = wsc[wsc_idx];
2016
2017 let mut wmt: i32 = 0;
2018 let wind = pattern_match1(wpat, wc, &mut wmt); // c:1394
2019 if wind == 0 {
2020 return 0;
2021 } // c:1395
2022
2023 // c:1399-1450 — deduce the line character `c`.
2024 let c: u32 = if pre.tp == CPAT_CHAR {
2025 // c:1402
2026 pre.chr // c:1407
2027 } else if pat.tp == CPAT_CHAR {
2028 // c:1410
2029 pat.chr // c:1414
2030 } else if pat.tp == CPAT_EQUIV {
2031 // c:1416
2032 // c:1424 — pattern_match_equivalence resolves the line-side
2033 // equivalence-class member paired with the word's wind/wmt.
2034 let r = pattern_match_equivalence(pat, wind, wmt, wc);
2035 if r == u32::MAX {
2036 return 0;
2037 } // c:1426 CHR_INVALID
2038 r
2039 } else {
2040 // c:1432
2041 wc // c:1442 use *wsc
2042 };
2043
2044 // c:1448 — restriction-side check.
2045 if pre.tp != CPAT_CHAR {
2046 let mut mt: i32 = 0;
2047 if pattern_match1(pre, c, &mut mt) == 0 {
2048 return 0;
2049 } // c:1449
2050 }
2051
2052 // c:1457-1485 — case-class equivalence (mt vs wmt mismatch).
2053 if pat.tp != CPAT_ANY || wpat.tp != CPAT_ANY {
2054 // c:1459
2055 let mut mt: i32 = 0;
2056 let ind = pattern_match1(pat, c, &mut mt); // c:1461
2057 if ind == 0 || ind != wind {
2058 return 0;
2059 } // c:1462-1465
2060 if mt != wmt {
2061 let case_pair =
2062 (mt == PP_LOWER || mt == PP_UPPER) && (wmt == PP_LOWER || wmt == PP_UPPER);
2063 if case_pair {
2064 let cc = char::from_u32(c).unwrap_or('\0');
2065 let wcc = char::from_u32(wc).unwrap_or('\0');
2066 if ZC_tolower(cc) != ZC_tolower(wcc) {
2067 return 0;
2068 } // c:1477
2069 } else {
2070 return 0; // c:1481
2071 }
2072 }
2073 }
2074
2075 // c:1496 — append deduced char to new_line.
2076 if let Some(ch) = char::from_u32(c) {
2077 new_line.push(ch);
2078 }
2079 pr_cur = pre.next.as_deref(); // c:1498
2080 wsc_idx += 1;
2081 p_cur = pat.next.as_deref();
2082 wp_cur = wpat.next.as_deref();
2083 }
2084
2085 // c:1505-1540 — tail loop: continue matching when wsc exhausted
2086 // but prestrict still has more chars (deduced solely from p).
2087 while p_cur.is_some() && pr_cur.is_some() {
2088 // c:1505
2089 let pat = p_cur.unwrap();
2090 let pre = pr_cur.unwrap();
2091 let c: u32 = if pre.tp == CPAT_CHAR {
2092 pre.chr
2093 } else if pat.tp == CPAT_CHAR {
2094 pat.chr
2095 } else {
2096 return 0; // c:1522 not enough info
2097 };
2098 let mut mt: i32 = 0;
2099 if pre.tp != CPAT_CHAR && pattern_match1(pre, c, &mut mt) == 0 {
2100 return 0;
2101 }
2102 if let Some(ch) = char::from_u32(c) {
2103 new_line.push(ch);
2104 }
2105 pr_cur = pre.next.as_deref();
2106 p_cur = pat.next.as_deref();
2107 }
2108
2109 // c:1542 — `p_cur.is_none() && pr_cur.is_none() && (wp_cur.is_none() || wsc empty)`.
2110 if p_cur.is_none() && pr_cur.is_none() && (wp_cur.is_none() || wsc_idx >= wsc.len()) {
2111 1 // c:1544 full match
2112 } else {
2113 0 // c:1545
2114 }
2115}
2116
2117/// Port of `pattern_match(Cpattern p, char *s, Cpattern wp, char *ws)` from Src/Zle/compmatch.c:1548.
2118/// Direct port of `mod_export int pattern_match(Cpattern p, char *s,
2119/// Cpattern wp, char *ws)`
2120/// from `Src/Zle/compmatch.c:1548`. Walks two parallel pattern +
2121/// string pairs (line `p`/`s` vs word `wp`/`ws`) verifying that each
2122/// position matches and that paired pattern-class indices line up.
2123/// WARNING: param names don't match C — Rust=(p, wp, ws) vs C=(p, s, wp, ws)
2124pub fn pattern_match(
2125 p: Option<&Cpattern>, // c:1548
2126 s: &str,
2127 wp: Option<&Cpattern>,
2128 ws: &str,
2129) -> i32 {
2130 let (mut p_cur, mut wp_cur) = (p, wp); // c:1551 walking p / wp
2131 let mut s_bytes = s.chars().peekable();
2132 let mut ws_bytes = ws.chars().peekable();
2133
2134 while p_cur.is_some() && wp_cur.is_some() // c:1553
2135 && s_bytes.peek().is_some() && ws_bytes.peek().is_some()
2136 {
2137 let pat = p_cur.unwrap();
2138 let wpat = wp_cur.unwrap();
2139 let wc = ws_bytes.next().unwrap() as u32; // c:1555
2140 let mut wmt: i32 = 0;
2141 let wind = pattern_match1(wpat, wc, &mut wmt); // c:1556
2142 if wind == 0 {
2143 return 0;
2144 } // c:1557
2145
2146 let c = s_bytes.next().unwrap() as u32; // c:1561
2147 if pat.tp != CPAT_ANY || wpat.tp != CPAT_ANY {
2148 // c:1567
2149 let mut mt: i32 = 0;
2150 let ind = pattern_match1(pat, c, &mut mt); // c:1569
2151 if ind == 0 {
2152 return 0;
2153 } // c:1570
2154 if ind != wind {
2155 return 0;
2156 } // c:1572
2157 if mt != wmt {
2158 // c:1574
2159 let case_pair =
2160 (mt == PP_LOWER || mt == PP_UPPER) && (wmt == PP_LOWER || wmt == PP_UPPER);
2161 if case_pair {
2162 let cc = char::from_u32(c).unwrap_or('\0');
2163 let wcc = char::from_u32(wc).unwrap_or('\0');
2164 if ZC_tolower(cc) != ZC_tolower(wcc) {
2165 // c:1584
2166 return 0;
2167 }
2168 } else {
2169 return 0; // c:1588
2170 }
2171 }
2172 }
2173 p_cur = pat.next.as_deref(); // c:1599
2174 wp_cur = wpat.next.as_deref();
2175 }
2176 // c:1601-1607 — consume remaining LINE pattern chars (stop when EITHER
2177 // the pattern OR the string is exhausted).
2178 while let (Some(pat), Some(&c_ch)) = (p_cur, s_bytes.peek()) {
2179 let c = c_ch as u32;
2180 let mut mt: i32 = 0;
2181 if pattern_match1(pat, c, &mut mt) == 0 {
2182 return 0; // c:1604
2183 }
2184 s_bytes.next();
2185 p_cur = pat.next.as_deref(); // c:1605
2186 }
2187 // c:1609-1615 — remaining WORD pattern chars, symmetrically.
2188 while let (Some(wpat), Some(&wc_ch)) = (wp_cur, ws_bytes.peek()) {
2189 let wc = wc_ch as u32;
2190 let mut wmt: i32 = 0;
2191 if pattern_match1(wpat, wc, &mut wmt) == 0 {
2192 return 0; // c:1611
2193 }
2194 ws_bytes.next();
2195 wp_cur = wpat.next.as_deref(); // c:1613
2196 }
2197 // c:1617 — the port previously required BOTH strings fully consumed;
2198 // a fixed-length matcher against a LONGER word failed on the remainder.
2199 // C stops at pattern exhaustion; caller handles the rest of the word.
2200 1
2201}
2202
2203/// Port of `bld_parts(char *str, int len, int plen, Cline *lp, Cline *lprem)` from Src/Zle/compmatch.c:1638.
2204/// Direct port of `static Cline bld_parts(char *str, int len, int plen,
2205/// Cline *lp, Cline *lprem)`
2206/// from `Src/Zle/compmatch.c:1638`. Splits the candidate string
2207/// `str[..len]` into a Cline chain anchored by every CMF_RIGHT
2208/// matcher in `bmatchers`. `plen` is the active prefix length;
2209/// trailing remainder (after the last anchor) goes into `*lprem`,
2210/// last node into `*lp`.
2211/// WARNING: param names don't match C — Rust=(str, len, plen, lprem) vs C=(str, len, plen, lp, lprem)
2212pub fn bld_parts(
2213 str: &str,
2214 len: i32,
2215 mut plen: i32, // c:1638
2216 lp: Option<&mut Option<Box<Cline>>>,
2217 lprem: Option<&mut Option<Box<Cline>>>,
2218) -> Option<Box<Cline>> {
2219 let bytes = str.as_bytes();
2220 let total: usize = (len as usize).min(bytes.len());
2221 let mut op = plen;
2222 let mut p_start = 0usize;
2223 let mut str_pos = 0usize;
2224 let mut remaining = total as i32;
2225
2226 let mut head: Option<Box<Cline>> = None;
2227 let mut tail_ref: *mut Option<Box<Cline>> = &mut head;
2228 let mut last_n: Option<Box<Cline>> = None;
2229
2230 while remaining > 0 {
2231 // c:1647
2232 // c:1648-1685 — walk bmatchers looking for a CMF_RIGHT-anchored
2233 // wlen<0 matcher whose right anchor matches at the current
2234 // position. On hit, emit a Cline for the run-so-far + the
2235 // anchored portion, advance str/plen past the anchor.
2236 let mut found_anchor = false;
2237 let bmatchers_chain = crate::ported::zle::compcore::bmatchers
2238 .get_or_init(|| Mutex::new(None))
2239 .lock()
2240 .ok()
2241 .and_then(|g| g.clone());
2242 let mut cur = bmatchers_chain.as_deref();
2243 while let Some(ms) = cur {
2244 let mp = &*ms.matcher;
2245 let preds_ok = mp.flags == CMF_RIGHT
2246 && mp.wlen < 0
2247 && mp.ralen > 0
2248 && mp.llen == 0
2249 && remaining >= mp.ralen
2250 && (str_pos as i32 - p_start as i32) >= mp.lalen;
2251 if !preds_ok {
2252 cur = ms.next.as_deref();
2253 continue;
2254 }
2255 let str_at = std::str::from_utf8(&bytes[str_pos..]).unwrap_or("");
2256 if pattern_match(mp.right.as_deref(), str_at, None, "") == 0 {
2257 cur = ms.next.as_deref();
2258 continue;
2259 }
2260 let l_anchor_ok = mp.lalen == 0 || {
2261 let off = str_pos as i32 - mp.lalen;
2262 if off < 0 {
2263 false
2264 } else {
2265 let l_slice = std::str::from_utf8(&bytes[off as usize..]).unwrap_or("");
2266 pattern_match(mp.left.as_deref(), l_slice, None, "") != 0
2267 }
2268 };
2269 if !l_anchor_ok {
2270 cur = ms.next.as_deref();
2271 continue;
2272 }
2273
2274 // c:1655-1672 — emit anchor cline; optional prefix run.
2275 let olen = (str_pos - p_start) as i32;
2276 let flags = if plen <= 0 { CLF_NEW } else { 0 };
2277 let anchor_word: String =
2278 std::str::from_utf8(&bytes[str_pos..str_pos + mp.ralen as usize])
2279 .unwrap_or("")
2280 .into();
2281 let mut node = Box::new(Cline {
2282 llen: mp.ralen,
2283 word: Some(anchor_word.clone()),
2284 wlen: mp.ralen,
2285 flags,
2286 ..Default::default()
2287 });
2288 if p_start != str_pos {
2289 let mut llen = if op < 0 { 0 } else { op };
2290 if llen > olen {
2291 llen = olen;
2292 }
2293 let prefix_word: String =
2294 std::str::from_utf8(&bytes[p_start..p_start + olen as usize])
2295 .unwrap_or("")
2296 .into();
2297 node.prefix = Some(Box::new(Cline {
2298 llen,
2299 word: Some(prefix_word),
2300 wlen: olen,
2301 ..Default::default()
2302 }));
2303 }
2304 unsafe {
2305 *tail_ref = Some(node);
2306 tail_ref = &mut (*tail_ref).as_mut().unwrap().next;
2307 }
2308 // c:1674-1677 — advance past the anchor.
2309 str_pos += mp.ralen as usize;
2310 remaining -= mp.ralen;
2311 plen -= mp.ralen;
2312 op -= olen;
2313 p_start = str_pos;
2314 found_anchor = true;
2315 break;
2316 }
2317 if !found_anchor {
2318 // c:1683 — no anchor: str++; len--; plen--.
2319 str_pos += 1;
2320 remaining -= 1;
2321 plen -= 1;
2322 }
2323 }
2324
2325 // c:1701-1717 — emit a Cline for the trailing portion.
2326 if p_start != str_pos {
2327 // c:1701
2328 let olen = (str_pos - p_start) as i32;
2329 let mut llen = if op < 0 { 0 } else { op };
2330 if llen > olen {
2331 llen = olen;
2332 }
2333 let flags = if plen <= 0 { CLF_NEW } else { 0 };
2334 let mut node = Box::new(Cline {
2335 flags,
2336 ..Default::default()
2337 });
2338 let prefix_word: String = std::str::from_utf8(&bytes[p_start..p_start + olen as usize])
2339 .unwrap_or("")
2340 .into();
2341 node.prefix = Some(Box::new(Cline {
2342 llen,
2343 word: Some(prefix_word.clone()),
2344 wlen: olen,
2345 ..Default::default()
2346 }));
2347 if let Some(out) = lprem {
2348 *out = Some(node.clone());
2349 } // c:1714
2350 last_n = Some(node.clone());
2351 unsafe {
2352 *tail_ref = Some(node);
2353 }
2354 } else if head.is_none() {
2355 // c:1716
2356 let flags = if plen <= 0 { CLF_NEW } else { 0 };
2357 let node = Box::new(Cline {
2358 flags,
2359 ..Default::default()
2360 });
2361 if let Some(out) = lprem {
2362 *out = Some(node.clone());
2363 } // c:1721
2364 last_n = Some(node.clone());
2365 head = Some(node);
2366 } else if let Some(out) = lprem {
2367 // c:1722
2368 *out = None;
2369 }
2370
2371 if let (Some(out_lp), Some(n)) = (lp, last_n) {
2372 // c:1731
2373 *out_lp = Some(n);
2374 }
2375
2376 let _ = p_start;
2377 let _ = op;
2378 head // c:1733 return ret
2379}
2380
2381/// Direct port of `static int bld_line(Cmatcher mp, ZLE_STRING_T line,
2382/// char *mword, char *word,
2383/// int wlen, int sfx)`
2384/// from `Src/Zle/compmatch.c:1734-1992`. Builds all possible line
2385/// patterns for `mp` and tests whether they match `word`, returning
2386/// the number of word chars matched (0 on failure). On success the
2387/// synthesized line chars are written into `line`.
2388///
2389/// Faithful two-pass implementation (matching the C):
2390///
2391/// Pass 1 (c:1772-1846) — build `genpatarr`, a per-line-char array of
2392/// `Cpattern`. For each `mp->line` entry: if it is a `CPAT_EQUIV` and
2393/// `mp->word` + `mword` are still available, consume one `mword` char,
2394/// query the word pattern via `pattern_match1`, and if that yields a
2395/// word-side equivalence index resolve the concrete line char via
2396/// `pattern_match_equivalence` (which tracks the PP_LOWER/PP_UPPER
2397/// case crossings, compmatch.rs:1825), storing it as a `CPAT_CHAR`.
2398/// Otherwise the line pattern is copied verbatim (char / class / any).
2399/// A `CHR_INVALID` equivalence resolution aborts with 0.
2400///
2401/// Pass 2 (c:1847-1988) — walk `genpatarr` against `wordchars`. Where
2402/// `pattern_match1` accepts the word char directly, emit the fixed
2403/// `CPAT_CHAR` value (or, for a generic pattern, the word char). Where
2404/// it does not, fall back to the `bmatchers` chain and let
2405/// `pattern_match_restrict` deduce the line chars (the "nightmare"
2406/// multi-matcher case). `sfx` builds both strings from the end.
2407pub fn bld_line(
2408 mp: &Cmatcher, // c:1734
2409 line: &mut Vec<char>,
2410 mword: &str,
2411 word: &str,
2412 wlen: i32,
2413 sfx: i32,
2414) -> i32 {
2415 let sfx = sfx != 0;
2416
2417 // c:1745-1762 — convert `word` to an array of (wide) chars so we
2418 // can index it from either end.
2419 let wordchars: Vec<u32> = word.chars().map(|c| c as u32).collect();
2420 let wlen0 = wlen.max(0) as usize;
2421
2422 // Links a slice of genpatarr entries into a throwaway Cpattern
2423 // chain, so `pattern_match_restrict` can walk it via `->next`
2424 // (c:1841-1845 links curgenpat->next = curgenpat+1).
2425 fn link_chain(entries: &[Cpattern]) -> Option<Box<Cpattern>> {
2426 let mut head: Option<Box<Cpattern>> = None;
2427 for e in entries.iter().rev() {
2428 let mut node = e.clone();
2429 node.next = head.take();
2430 head = Some(Box::new(node));
2431 }
2432 head
2433 }
2434
2435 // --- Pass 1: build genpatarr (c:1772-1846). ---
2436 let mword_chars: Vec<u32> = mword.chars().map(|c| c as u32).collect();
2437 let mut mword_idx = 0usize;
2438 let mut wpat = mp.word.as_deref();
2439 let mut lpat = mp.line.as_deref();
2440 let mut genpatarr: Vec<Cpattern> = Vec::with_capacity(mp.llen.max(0) as usize);
2441 while let Some(lp) = lpat {
2442 // c:1780-1799 — resolve the word side of an equivalence.
2443 let mut wind: u32 = 0;
2444 let mut wmtp: i32 = 0;
2445 let mut wchr: u32 = 0;
2446 if lp.tp == CPAT_EQUIV && wpat.is_some() && mword_idx < mword_chars.len() {
2447 wchr = mword_chars[mword_idx];
2448 mword_idx += 1;
2449 let wp = wpat.unwrap();
2450 wind = pattern_match1(wp, wchr, &mut wmtp); // c:1794
2451 wpat = wp.next.as_deref(); // c:1795
2452 }
2453
2454 let mut gp = Cpattern::default();
2455 if wind != 0 {
2456 // c:1800-1822 — successful word-side equivalence; find the
2457 // line equivalent and pin it as a concrete char.
2458 let lchr = pattern_match_equivalence(lp, wind, wmtp, wchr); // c:1817
2459 if lchr == u32::MAX {
2460 return 0; // c:1820 — no equivalent, give up
2461 }
2462 gp.tp = CPAT_CHAR; // c:1826
2463 gp.chr = lchr; // c:1827
2464 } else {
2465 // c:1828-1846 — copy the line pattern verbatim.
2466 gp.tp = lp.tp; // c:1834
2467 if lp.tp == CPAT_CHAR {
2468 gp.chr = lp.chr; // c:1836
2469 } else if lp.tp != CPAT_ANY {
2470 gp.str = lp.str.clone(); // c:1843 (shared/copied class)
2471 }
2472 }
2473 genpatarr.push(gp);
2474 lpat = lp.next.as_deref();
2475 }
2476
2477 // --- Pass 2: match wordchars against genpatarr (c:1847-1988). ---
2478 let llen0 = mp.llen.max(0) as usize;
2479 let mut line_buf: Vec<char> = vec!['\0'; llen0];
2480 let mut llen_rem: i32 = mp.llen; // c:1855
2481 let mut wlen_rem: i32 = wlen;
2482 let mut rl: i32 = 0; // c:1856
2483
2484 // Cursors into line_buf / wordchars / genpatarr (c:1858-1874).
2485 let mut line_pos: usize = if sfx { llen0 } else { 0 };
2486 let mut word_pos: usize = if sfx { wlen0 } else { 0 };
2487 let mut gp_pos: usize = if sfx { llen0 } else { 0 };
2488
2489 // Snapshot the global bmatchers chain for the fallback loop.
2490 let bm = crate::ported::zle::compcore::bmatchers
2491 .get_or_init(|| Mutex::new(None))
2492 .lock()
2493 .ok()
2494 .and_then(|g| g.clone());
2495
2496 while llen_rem > 0 && wlen_rem > 0 {
2497 // c:1877-1885 — pick the char/pattern under inspection.
2498 let (wp_idx, gp_idx) = if sfx {
2499 (word_pos - 1, gp_pos - 1)
2500 } else {
2501 (word_pos, gp_pos)
2502 };
2503 // `.get`-guarded: callers may pass a byte length as `wlen`
2504 // that exceeds the char count on multibyte input.
2505 let wc = *wordchars.get(wp_idx).unwrap_or(&0);
2506
2507 let mut wmtp: i32 = 0;
2508 if gp_idx < genpatarr.len() && pattern_match1(&genpatarr[gp_idx], wc, &mut wmtp) != 0 {
2509 // c:1890-1920 — direct match. Keep the fixed char for a
2510 // CPAT_CHAR genpat, else keep the word char.
2511 let lchr = if genpatarr[gp_idx].tp == CPAT_CHAR {
2512 genpatarr[gp_idx].chr
2513 } else {
2514 wc
2515 };
2516 let lch = char::from_u32(lchr).unwrap_or('\0');
2517 if sfx {
2518 line_pos -= 1; // c:1908 *--line = lchr
2519 line_buf[line_pos] = lch;
2520 } else {
2521 line_buf[line_pos] = lch; // c:1910 *line++ = lchr
2522 line_pos += 1;
2523 }
2524 llen_rem -= 1; // c:1912
2525 wlen_rem -= 1; // c:1913
2526 rl += 1; // c:1914
2527
2528 if sfx {
2529 word_pos = wp_idx; // c:1917
2530 gp_pos = gp_idx; // c:1918
2531 } else {
2532 if llen_rem > 0 {
2533 gp_pos += 1; // c:1920
2534 }
2535 word_pos += 1; // c:1921
2536 }
2537 } else {
2538 // c:1925-1978 — nightmare case: dispatch to the pattern
2539 // matchers in bmatchers via pattern_match_restrict.
2540 let mut matched = false;
2541 let mut ms = bm.as_deref();
2542 while let Some(node) = ms {
2543 let bmp = &*node.matcher; // c:1932 mp = ms->matcher
2544 if bmp.flags == 0
2545 && bmp.wlen <= wlen_rem
2546 && bmp.llen <= llen_rem
2547 && bmp.wlen >= 0
2548 && bmp.llen >= 0
2549 {
2550 // c:1943-1949 — position the sub-window.
2551 let (lp_idx, wp2_idx, gp2_idx) = if sfx {
2552 (
2553 line_pos - bmp.llen as usize,
2554 word_pos - bmp.wlen as usize,
2555 gp_pos - bmp.llen as usize,
2556 )
2557 } else {
2558 (line_pos, word_pos, gp_pos)
2559 };
2560
2561 // c:1951 — wsclen = wlen - (wp - wordchars).
2562 let wsclen = wlen_rem - wp2_idx as i32;
2563 let start = wp2_idx;
2564 let end = if wsclen <= 0 {
2565 start
2566 } else {
2567 (start + wsclen as usize).min(wordchars.len())
2568 };
2569 let wsc = &wordchars[start.min(end)..end];
2570
2571 let pr_end = (gp2_idx + bmp.llen as usize).min(genpatarr.len());
2572 let prestrict = link_chain(&genpatarr[gp2_idx.min(pr_end)..pr_end]);
2573
2574 let mut tmp_line: Vec<char> = Vec::new();
2575 if pattern_match_restrict(
2576 bmp.line.as_deref(),
2577 bmp.word.as_deref(),
2578 wsc,
2579 prestrict.as_deref(),
2580 &mut tmp_line,
2581 ) != 0
2582 {
2583 // c:1958-1978 — matched: copy deduced line chars
2584 // into place and advance all cursors.
2585 for (k, ch) in tmp_line.iter().enumerate() {
2586 if lp_idx + k < line_buf.len() {
2587 line_buf[lp_idx + k] = *ch;
2588 }
2589 }
2590 if sfx {
2591 line_pos = lp_idx; // c:1965
2592 word_pos = wp2_idx; // c:1966
2593 gp_pos = gp2_idx; // c:1967
2594 } else {
2595 line_pos += bmp.llen as usize; // c:1969
2596 word_pos += bmp.wlen as usize; // c:1970
2597 gp_pos += bmp.llen as usize; // c:1971
2598 }
2599 llen_rem -= bmp.llen; // c:1973
2600 wlen_rem -= bmp.wlen; // c:1974
2601 rl += bmp.wlen; // c:1975
2602 matched = true;
2603 break;
2604 }
2605 }
2606 ms = node.next.as_deref();
2607 }
2608 if !matched {
2609 return 0; // c:1983 — didn't match, give up
2610 }
2611 }
2612 }
2613
2614 if llen_rem == 0 {
2615 // c:1986 — whole line built; commit and return matched length.
2616 line.extend(line_buf.iter().copied());
2617 return rl; // c:1987
2618 }
2619 0 // c:1990
2620}
2621
2622/// Port of `static char *join_strs(int la, char *sa, int lb, char *sb)`
2623/// from Src/Zle/compmatch.c:1994.
2624///
2625/// "Joins two strings via the matcher equivalence map; returns the
2626/// merged string or NULL if they can't be merged." The full body
2627/// walks the global `bmatchers` Cmlist for each character of `sa`
2628/// vs `sb`, applying matcher patterns to find a unifying byte.
2629///
2630/// Blocked on: `bmatchers` global Cmlist, `pattern_match1`, the
2631/// `cmatcher`-driven equivalence map, `matchbuf`/`matchbuflen`
2632/// growable buffer, `start_match`/`end_match` framing. Returns
2633/// `None` until `pattern_match1` lands.
2634/// char *sb)` from
2635/// `Src/Zle/compmatch.c:1994`. Tries to construct a common
2636/// string for `sa[..la]` and `sb[..lb]` by either taking equal
2637/// chars verbatim or using a no-anchor matcher's bld_line synthesis.
2638/// Returns the merged string on success, None when no match advances
2639/// either input.
2640pub fn join_strs(mut la: i32, sa: &str, mut lb: i32, sb: &str) -> Option<String> {
2641 let mut out = String::new();
2642 let mut a_idx = 0usize;
2643 let mut b_idx = 0usize;
2644 let a_bytes = sa.as_bytes();
2645 let b_bytes = sb.as_bytes();
2646
2647 while la > 0 && lb > 0 && a_idx < a_bytes.len() && b_idx < b_bytes.len() {
2648 if a_bytes[a_idx] == b_bytes[b_idx] {
2649 // c:2085 equal-char path
2650 // c:2092 — append + advance both.
2651 out.push(a_bytes[a_idx] as char);
2652 a_idx += 1;
2653 b_idx += 1;
2654 la -= 1;
2655 lb -= 1;
2656 } else {
2657 // c:2013 — matcher-driven branch. Walks bmatchers looking
2658 // for a no-anchor matcher that pattern_matches one of the
2659 // input strings; on hit calls bld_line to synthesize a
2660 // line that matches the OTHER string, copies the result
2661 // into `out`, and advances both inputs.
2662 let bmatchers = crate::ported::zle::compcore::bmatchers
2663 .get_or_init(|| Mutex::new(None))
2664 .lock()
2665 .ok()
2666 .and_then(|g| g.clone());
2667 let mut advanced = false;
2668 let mut cur = bmatchers.as_deref();
2669 while let Some(ms) = cur {
2670 // c:2018
2671 let mp = &*ms.matcher;
2672 let ok =
2673 mp.flags == 0 && mp.wlen > 0 && mp.llen > 0 && mp.wlen <= la && mp.wlen <= lb;
2674 if ok {
2675 // c:2025-2027 — try the word pattern against either side.
2676 let mp_word = mp.word.as_deref();
2677 let a_slice = &sa[a_idx..];
2678 let b_slice = &sb[b_idx..];
2679 let t = if pattern_match(mp_word, a_slice, None, "") != 0 {
2680 1
2681 } else if pattern_match(mp_word, b_slice, None, "") != 0 {
2682 2
2683 } else {
2684 0
2685 };
2686 if t != 0 {
2687 // c:2057-2087 — bld_line writes the synthesized
2688 // line into a local buffer + returns the
2689 // count consumed from the other string.
2690 let mut line: Vec<char> = Vec::new();
2691 let bl = bld_line(
2692 mp,
2693 &mut line,
2694 "", // mword empty here; bld_line runs its equivalence pass as a no-op (c:2103 passes "")
2695 if t == 1 { b_slice } else { a_slice },
2696 if t == 1 { lb } else { la },
2697 0,
2698 );
2699 if bl > 0 {
2700 // c:2068
2701 for ch in &line {
2702 out.push(*ch);
2703 }
2704 // Advance per t-direction:
2705 if t == 1 {
2706 a_idx += mp.wlen as usize;
2707 la -= mp.wlen;
2708 b_idx += bl as usize;
2709 lb -= bl;
2710 } else {
2711 b_idx += mp.wlen as usize;
2712 lb -= mp.wlen;
2713 a_idx += bl as usize;
2714 la -= bl;
2715 }
2716 advanced = true;
2717 break;
2718 }
2719 }
2720 }
2721 cur = ms.next.as_deref();
2722 }
2723 if !advanced {
2724 break;
2725 }
2726 }
2727 }
2728
2729 if !out.is_empty() {
2730 Some(out)
2731 } else {
2732 None
2733 } // c:2100-2104
2734}
2735
2736// (cline_setlens / cline_sublen wrong-sig duplicates removed —
2737// real C-faithful ports are above keyed off comp_h::Cline.)
2738
2739/// Port of `static int cmp_anchors(Cline o, Cline n, int join)` from
2740/// Src/Zle/compmatch.c:2107.
2741///
2742/// Compares two Cline anchors. Returns:
2743/// - `1` if exact word/line match (and may set `CLF_LINE` on `o`)
2744/// - `2` if `join` is set and `join_strs` produced a merged anchor
2745/// (sets `CLF_JOIN` and rewrites `o->word`/`wlen`)
2746/// - `0` otherwise.
2747pub fn cmp_anchors(
2748 o: &mut Cline, // c:2107
2749 n: &Cline,
2750 join: i32,
2751) -> i32 {
2752 // Inline `!strncmp(a, b, n)` predicate from C.
2753 let strncmp_eq = |a: &Option<String>, b: &Option<String>, n: usize| -> bool {
2754 match (a, b) {
2755 (Some(x), Some(y)) => {
2756 let xb = x.as_bytes();
2757 let yb = y.as_bytes();
2758 xb.len() >= n && yb.len() >= n && xb[..n] == yb[..n]
2759 }
2760 _ => false,
2761 }
2762 };
2763 // c:2113 — try exact word/line match.
2764 let word_match = (o.flags & CLF_LINE) == 0
2765 && o.wlen == n.wlen
2766 && (o.word.is_none() || strncmp_eq(&o.word, &n.word, o.wlen as usize));
2767 let line_match = !word_match && {
2768 let both_empty = o.line.is_none() && n.line.is_none() && o.wlen == 0 && n.wlen == 0;
2769 let both_lines = o.llen == n.llen
2770 && o.line.is_some()
2771 && n.line.is_some()
2772 && strncmp_eq(&o.line, &n.line, o.llen as usize);
2773 both_empty || both_lines // c:2115-2117
2774 };
2775 if word_match || line_match {
2776 // c:2118
2777 if line_match {
2778 o.flags |= CLF_LINE;
2779 o.word = None; // c:2120
2780 o.wlen = 0; // c:2121
2781 }
2782 return 1; // c:2123
2783 }
2784 // c:2126-2132 — fall back to merged anchor via join_strs.
2785 if join != 0 && (o.flags & CLF_JOIN) == 0 && o.word.is_some() && n.word.is_some() {
2786 if let Some(j) = join_strs(
2787 o.wlen,
2788 o.word.as_deref().unwrap(),
2789 n.wlen,
2790 n.word.as_deref().unwrap(),
2791 ) {
2792 o.flags |= CLF_JOIN; // c:2128
2793 o.wlen = j.len() as i32; // c:2129
2794 o.word = Some(j); // c:2130
2795 return 2; // c:2132
2796 }
2797 }
2798 0 // c:2134
2799}
2800
2801/// Port of `struct cmdata` from `Src/Zle/compmatch.c:2142-2147`.
2802/// Working state for `check_cmdata` / `undo_cmdata` / `sub_match`.
2803#[derive(Default, Clone, Debug)]
2804#[allow(non_camel_case_types)]
2805pub struct cmdata {
2806 // c:2142
2807 pub cl: Option<Box<Cline>>, // c:2143
2808 pub pcl: Option<Box<Cline>>, // c:2143
2809 pub str: String, // c:2152
2810 pub astr: String, // c:2152
2811 pub len: i32, // c:2152
2812 pub alen: i32, // c:2152
2813 pub olen: i32, // c:2152
2814 pub line: i32, // c:2152
2815}
2816
2817/// Direct port of `static int check_cmdata(cmdata md, int sfx)` from
2818/// `Src/Zle/compmatch.c:2152`. Refills `md` from the next Cline
2819/// node when its `len` runs to zero; returns 1 when the chain is
2820/// exhausted, 0 otherwise.
2821pub fn check_cmdata(md: &mut cmdata, sfx: i32) -> i32 {
2822 // c:2152
2823
2824 if md.len != 0 {
2825 return 0;
2826 } // c:2155
2827 let next = match md.cl.as_deref() {
2828 // c:2158
2829 None => return 1,
2830 Some(n) => n.clone(),
2831 };
2832
2833 if (next.flags & CLF_LINE) != 0 {
2834 // c:2163
2835 md.line = 1;
2836 md.len = next.llen; // c:2164
2837 md.str = next.line.clone().unwrap_or_default(); // c:2165
2838 } else {
2839 md.line = 0;
2840 md.len = next.wlen; // c:2168
2841 md.olen = next.wlen; // c:2168
2842 if let Some(ref w) = next.word {
2843 md.str = if sfx != 0 {
2844 w[md.len as usize..].to_string()
2845 }
2846 // c:2171
2847 else {
2848 w.clone()
2849 };
2850 }
2851 md.alen = next.llen; // c:2173
2852 if let Some(ref l) = next.line {
2853 md.astr = if sfx != 0 {
2854 l[md.alen as usize..].to_string()
2855 }
2856 // c:2176
2857 else {
2858 l.clone()
2859 };
2860 }
2861 }
2862 md.pcl = Some(Box::new(next.clone())); // c:2179
2863 md.cl = next.next.clone(); // c:2180
2864 0 // c:2182
2865}
2866
2867/// Port of `undo_cmdata(Cmdata md, int sfx)` from Src/Zle/compmatch.c:2188.
2868/// Direct port of `static Cline undo_cmdata(cmdata md, int sfx)` from
2869/// `Src/Zle/compmatch.c:2188`. Puts the not-yet-matched portion
2870/// of `md` back into the previous cline node so it can be revisited
2871/// on a different match path.
2872pub fn undo_cmdata(md: &cmdata, sfx: i32) -> Option<Box<Cline>> {
2873 // c:2188
2874 let mut r = md.pcl.as_deref().cloned()?; // c:2189 r = md->pcl
2875
2876 if md.line != 0 {
2877 // c:2191
2878 r.word = None; // c:2192
2879 r.wlen = 0; // c:2193
2880 r.flags |= CLF_LINE; // c:2194
2881 r.llen = md.len; // c:2195
2882 // c:2197 — line = str - (sfx ? len : 0).
2883 let off = if sfx != 0 { md.len as usize } else { 0 };
2884 r.line = Some(
2885 md.str
2886 .chars()
2887 .skip(md.str.len().saturating_sub(off + md.len as usize))
2888 .collect(),
2889 );
2890 } else if md.len != md.olen {
2891 // c:2199
2892 r.wlen = md.len; // c:2201
2893 let off = if sfx != 0 { md.len as usize } else { 0 };
2894 r.word = Some(
2895 md.str
2896 .chars()
2897 .skip(md.str.len().saturating_sub(off + md.len as usize))
2898 .collect(),
2899 );
2900 }
2901 Some(Box::new(r)) // c:2206
2902}
2903
2904/// Direct port of `static Cline join_sub(cmdata md, char *str, int len,
2905/// int *mlen, int sfx, int join)`
2906/// from `Src/Zle/compmatch.c:2212`. Tries to match the new
2907/// substring `str[..len]` against the data currently in `md` via
2908/// one of the no-anchor matchers in `bmatchers`; on success
2909/// returns the matched-portion Cline and updates `md`/`*mlen`.
2910pub fn join_sub(
2911 md: &mut cmdata,
2912 str: &str,
2913 len: i32,
2914 mlen: &mut i32, // c:2212
2915 sfx: i32,
2916 join: i32,
2917) -> Option<Box<Cline>> {
2918 // c:2214 — `if (!check_cmdata(md, sfx))`. Refill md from next
2919 // Cline; bail when chain exhausted.
2920 if check_cmdata(md, sfx) != 0 {
2921 return None;
2922 }
2923
2924 let ow = str;
2925 let nw = md.str.clone();
2926 let ol = len;
2927 let nl = md.len;
2928
2929 // c:2226 — walk bmatchers for a no-anchor matcher.
2930 let bmatchers = crate::ported::zle::compcore::bmatchers
2931 .get_or_init(|| Mutex::new(None))
2932 .lock()
2933 .ok()
2934 .and_then(|g| g.clone());
2935
2936 let mut cur = bmatchers.as_deref();
2937 while let Some(ms) = cur {
2938 // c:2226
2939 let mp = &*ms.matcher;
2940 if mp.flags == 0 && mp.wlen > 0 && mp.llen > 0 {
2941 // c:2231
2942 // c:2235-2249 — early-return: if the old string already
2943 // matches the new word pattern, advance md and return a
2944 // cline for the matched portion.
2945 if mp.llen <= ol && mp.wlen <= nl {
2946 // c:2236
2947 let ow_off = if sfx != 0 { ol - mp.llen } else { 0 };
2948 let nw_off = if sfx != 0 { nl - mp.wlen } else { 0 };
2949 let line_slice = &ow[ow_off as usize..];
2950 let word_slice = &nw[nw_off as usize..];
2951 if pattern_match(
2952 mp.line.as_deref(),
2953 line_slice,
2954 mp.word.as_deref(),
2955 word_slice,
2956 ) != 0
2957 {
2958 // c:2241-2243 — update md.str.
2959 if sfx != 0 {
2960 md.str = md
2961 .str
2962 .chars()
2963 .take(md.str.chars().count().saturating_sub(mp.wlen as usize))
2964 .collect();
2965 } else {
2966 md.str = md.str.chars().skip(mp.wlen as usize).collect();
2967 }
2968 md.len -= mp.wlen;
2969 *mlen = mp.llen; // c:2247
2970 return Some(get_cline(
2971 // c:2249
2972 None,
2973 0,
2974 Some(line_slice[..mp.llen as usize].to_string()),
2975 mp.llen,
2976 None,
2977 0,
2978 0,
2979 ));
2980 }
2981 }
2982 // c:2255-2294 — the bld_line-driven branch (join != 0)
2983 // tries to construct a synthetic line that matches both
2984 // strings.
2985 if join != 0 && mp.wlen <= ol && mp.wlen <= nl {
2986 // c:2255
2987 let ow_off = if sfx != 0 { ol - mp.wlen } else { 0 };
2988 let nw_off = if sfx != 0 { nl - mp.wlen } else { 0 };
2989 let mp_word = mp.word.as_deref();
2990 let ow_slice = &ow[ow_off as usize..];
2991 let nw_slice = &nw[nw_off as usize..];
2992
2993 let t = if pattern_match(mp_word, ow_slice, None, "") != 0 {
2994 1
2995 } else if pattern_match(mp_word, nw_slice, None, "") != 0 {
2996 2
2997 } else {
2998 0
2999 };
3000
3001 if t != 0 {
3002 // c:2258
3003 let (mw_slice, other_slice, other_len) = if t == 1 {
3004 (ow_slice, nw_slice, nl)
3005 } else {
3006 (nw_slice, ow_slice, ol)
3007 };
3008 let _ = mw_slice;
3009
3010 let mut line: Vec<char> = Vec::new();
3011 let bl = bld_line(mp, &mut line, "", other_slice, other_len, sfx);
3012 if bl > 0 {
3013 // c:2274
3014 let new_nl = if t == 1 { bl } else { mp.wlen };
3015 let new_ol = if t == 1 { mp.wlen } else { bl };
3016 if sfx != 0 {
3017 md.str = md
3018 .str
3019 .chars()
3020 .take(md.str.chars().count().saturating_sub(new_nl as usize))
3021 .collect();
3022 } else {
3023 md.str = md.str.chars().skip(new_nl as usize).collect();
3024 }
3025 md.len -= new_nl; // c:2281
3026 *mlen = new_ol; // c:2283
3027
3028 let line_str: String = line.iter().collect();
3029 return Some(get_cline(
3030 // c:2285
3031 None,
3032 0,
3033 Some(line_str),
3034 mp.llen,
3035 None,
3036 0,
3037 CLF_JOIN,
3038 ));
3039 }
3040 }
3041 }
3042 }
3043 cur = ms.next.as_deref();
3044 }
3045 None // c:2298
3046}
3047
3048/// Direct port of `static int sub_match(cmdata md, char *str, int len,
3049/// int sfx)` from
3050/// `Src/Zle/compmatch.c:2301`. Accumulates the longest common
3051/// prefix (or suffix when `sfx` set) between the substring
3052/// `str[..len]` and the data in `md`, advancing `md.str`/`md.len`
3053/// as it consumes characters.
3054///
3055/// Returns the count of matched bytes — the C source's "ret" value.
3056pub fn sub_match(md: &mut cmdata, str: &str, len: i32, sfx: i32) -> i32 {
3057 // c:2301
3058 let mut ret = 0i32;
3059 let str_bytes = str.as_bytes();
3060 let mut remaining = len as usize;
3061 let start_idx: usize = if sfx != 0 {
3062 (len as usize).min(str_bytes.len())
3063 } else {
3064 0
3065 };
3066
3067 // c:2319 — outer while-len loop: refill md, find common prefix
3068 // (or suffix), accumulate ret, then re-enter for next cline node.
3069 while remaining > 0 {
3070 // c:2319
3071 if check_cmdata(md, sfx) != 0 {
3072 // c:2320
3073 return ret;
3074 }
3075
3076 let md_bytes = md.str.as_bytes();
3077 let mut l: usize = 0;
3078 let md_len_usize = md.len as usize;
3079 let cap = remaining.min(md_len_usize);
3080
3081 // c:2329-2331 — accumulate matching chars from the chosen end.
3082 while l < cap {
3083 let s_idx: isize = if sfx != 0 {
3084 start_idx as isize - (l as isize) - 1 - (ret as isize)
3085 } else {
3086 (ret as isize) + (l as isize)
3087 };
3088 let m_len = md_bytes.len();
3089 let m_idx: isize = if sfx != 0 {
3090 m_len as isize - (l as isize) - 1
3091 } else {
3092 l as isize
3093 };
3094 if s_idx < 0 || m_idx < 0 {
3095 break;
3096 }
3097 let s_pos = s_idx as usize;
3098 let m_pos = m_idx as usize;
3099 if s_pos >= str_bytes.len() || m_pos >= md_bytes.len() {
3100 break;
3101 }
3102 if str_bytes[s_pos] != md_bytes[m_pos] {
3103 break;
3104 }
3105 l += 1;
3106 }
3107
3108 if l == 0 {
3109 return ret;
3110 } // c:2380 no progress
3111
3112 // c:2335-2349 — meta-character boundary correction. Avoid
3113 // ending in the middle of a `Meta x` 2-byte sequence.
3114 const META_BYTE: u8 = 0x83;
3115 let check_pos: isize = if sfx != 0 {
3116 start_idx as isize - (l as isize) - (ret as isize)
3117 } else {
3118 (ret as isize) + (l as isize) - 1
3119 };
3120 if check_pos >= 0
3121 && (check_pos as usize) < str_bytes.len()
3122 && str_bytes[check_pos as usize] == META_BYTE
3123 && l > 0
3124 {
3125 l -= 1;
3126 }
3127
3128 // c:2400 — md.len -= l; md.str = md.str + l (or md.str - l for sfx).
3129 md.len -= l as i32;
3130 if sfx != 0 {
3131 // suffix-mode: strip from the END of md.str.
3132 md.str = md
3133 .str
3134 .chars()
3135 .take(md.str.chars().count().saturating_sub(l))
3136 .collect();
3137 } else {
3138 // prefix-mode: skip first l bytes.
3139 md.str = md.str.chars().skip(l).collect();
3140 }
3141
3142 ret += l as i32; // c:2418
3143 remaining = remaining.saturating_sub(l);
3144
3145 if remaining == 0 || md.len == 0 {
3146 // c:2421
3147 break;
3148 }
3149 }
3150 ret // c:2441
3151}
3152
3153/// Port of `join_psfx(Cline ot, Cline nt, Cline *orest, Cline *nrest, int sfx)` from Src/Zle/compmatch.c:2444.
3154/// Direct port of `static void join_psfx(Cline ot, Cline nt, Cline
3155/// *orest, Cline *nrest, int sfx)`
3156/// from `Src/Zle/compmatch.c:2444-2606`. Walks both prefix/suffix
3157/// chains of `ot` and `nt`, computing the joined chain and any
3158/// trailing rest into `orest` / `nrest`.
3159///
3160/// Body shell handles the c:2452-2465 empty-chain short-circuit:
3161/// when `o` is None, the rest is `n` and CLF_MISS marks `ot` if
3162/// `n` has work to do.
3163///
3164/// The full inner merge loop (c:2470-2600) walks both o/n chains
3165/// in parallel, calling `sub_match` / `join_sub` / `sub_join` to
3166/// classify each pair and accumulate min/max. Those three helpers
3167/// are now real-bodied (sub_match common-prefix/suffix, join_sub
3168/// bmatchers+bld_line, sub_join min/max diff). The outer-loop chain
3169/// walk + per-node CLF_DIFF/MISS emit isn't expanded here because
3170/// the helpers' return signals already feed the merge state the
3171/// caller (`join_clines`) inspects.
3172pub fn join_psfx(
3173 ot: &mut Cline, // c:2444
3174 nt: &mut Cline,
3175 orest: Option<&mut Option<Box<Cline>>>,
3176 nrest: Option<&mut Option<Box<Cline>>>,
3177 sfx: i32,
3178) {
3179 // c:2451-2455 — pick prefix/suffix chains.
3180 let mut remaining: Option<Box<Cline>> = if sfx != 0 {
3181 ot.suffix.take()
3182 } else {
3183 ot.prefix.take()
3184 };
3185 let n_chain = if sfx != 0 {
3186 nt.suffix.clone()
3187 } else {
3188 nt.prefix.clone()
3189 };
3190
3191 // c:2456-2465 — `o == NULL` shortcut.
3192 if remaining.is_none() {
3193 if let Some(out) = orest {
3194 *out = None;
3195 } // c:2458
3196 if let Some(out) = nrest {
3197 *out = n_chain.clone();
3198 } // c:2459
3199 if let Some(ref nn) = n_chain {
3200 // c:2461
3201 if nn.wlen != 0 {
3202 ot.flags |= CLF_MISS; // c:2462
3203 }
3204 }
3205 if sfx != 0 {
3206 ot.suffix = remaining;
3207 } else {
3208 ot.prefix = remaining;
3209 }
3210 return; // c:2464
3211 }
3212
3213 // c:2466-2479 — `n == NULL` shortcut: drain o into orest (or free).
3214 if n_chain.is_none() {
3215 if let Some(out) = orest {
3216 // c:2472
3217 *out = remaining.take();
3218 } else {
3219 free_cline(remaining.take()); // c:2475
3220 }
3221 if let Some(out) = nrest {
3222 *out = None;
3223 } // c:2477
3224 // ot.prefix/suffix already cleared by take() above.
3225 return; // c:2478
3226 }
3227
3228 // c:2480 — md.cl = n; md.len = 0.
3229 let mut md = cmdata {
3230 cl: n_chain.clone(),
3231 pcl: None,
3232 str: String::new(),
3233 astr: String::new(),
3234 len: 0,
3235 alen: 0,
3236 olen: 0,
3237 line: 0,
3238 };
3239
3240 // Build the rewritten o-chain into result_head; result_tail_ptr tracks
3241 // the tail position so we can append in O(1).
3242 let mut result_head: Option<Box<Cline>> = None;
3243 let mut result_tail_ptr: *mut Option<Box<Cline>> = &mut result_head;
3244 let mut have_prev = false; // mirrors C's `p` non-null check
3245
3246 let ot_slen = ot.slen;
3247
3248 // c:2484 — `while (o)`.
3249 'walk: while let Some(mut o_node) = remaining.take() {
3250 // Detach the rest of the chain so we can either re-prepend
3251 // (continue retry case) or splice (join_sub success).
3252 remaining = o_node.next.take();
3253
3254 let omd = md.clone(); // c:2486
3255 let mut len: i32;
3256 let mut join = 0;
3257 let mut line = 0;
3258
3259 // c:2489-2494 — compute longest matching prefix/suffix.
3260 if (o_node.flags & CLF_LINE) != 0 {
3261 let line_str = o_node.line.clone().unwrap_or_default();
3262 len = sub_match(&mut md, &line_str, o_node.llen, sfx);
3263 if len != o_node.llen && len >= 0 {
3264 join = 1;
3265 line = 1;
3266 }
3267 } else {
3268 let word_str = o_node.word.clone().unwrap_or_default();
3269 len = sub_match(&mut md, &word_str, o_node.wlen, sfx);
3270 if len != o_node.wlen && len >= 0 {
3271 // c:2496 — if o->line, retry as line.
3272 if o_node.line.is_some() {
3273 md = omd;
3274 o_node.flags |= CLF_LINE | CLF_DIFF; // c:2498
3275 o_node.next = remaining.take();
3276 remaining = Some(o_node);
3277 continue 'walk; // c:2500
3278 }
3279 // c:2502 — adjust o->llen.
3280 o_node.llen -= ot_slen;
3281 join = 1;
3282 line = 0;
3283 }
3284 }
3285
3286 if join != 0 {
3287 // c:2511 — attempt to build a unifying cline for the remainder.
3288 let (sstr_owned, slen) = if line != 0 {
3289 (o_node.line.clone().unwrap_or_default(), o_node.llen)
3290 } else {
3291 (o_node.word.clone().unwrap_or_default(), o_node.wlen)
3292 };
3293 let sstr_bytes = sstr_owned.as_bytes();
3294 // c:2511 — `*sstr + len` is "start from byte index len" in both
3295 // sfx and !sfx — the C macro `*sstr` already points at the
3296 // active portion. For our string-owned representation we slice
3297 // from len bytes onward.
3298 let rest_start = (len as usize).min(sstr_bytes.len());
3299 let rest_str = String::from_utf8_lossy(&sstr_bytes[rest_start..]).into_owned();
3300 let mut jlen: i32 = 0;
3301 let new_join_flag = if (o_node.flags & CLF_JOIN) != 0 { 0 } else { 1 };
3302 let joinl_opt = join_sub(
3303 &mut md,
3304 &rest_str,
3305 slen - len,
3306 &mut jlen,
3307 sfx,
3308 new_join_flag,
3309 );
3310 if let Some(mut joinl) = joinl_opt {
3311 joinl.flags |= CLF_DIFF; // c:2514
3312 if len + jlen != slen {
3313 // c:2515-2522 — build rest from the unconsumed tail.
3314 let off = if sfx != 0 {
3315 0usize
3316 } else {
3317 (len + jlen) as usize
3318 };
3319 let off = off.min(sstr_bytes.len());
3320 let take_n = ((slen - len - jlen).max(0) as usize).min(sstr_bytes.len() - off);
3321 let rest_word_str =
3322 String::from_utf8_lossy(&sstr_bytes[off..off + take_n]).into_owned();
3323 let mut rest =
3324 get_cline(None, 0, Some(rest_word_str), slen - len - jlen, None, 0, 0);
3325 rest.next = remaining.take(); // c:2521
3326 joinl.next = Some(rest);
3327 } else {
3328 joinl.next = remaining.take(); // c:2524
3329 }
3330
3331 if len != 0 {
3332 // c:2526-2530 — keep o, trim to len, then advance to joinl.
3333 if sfx != 0 {
3334 let drop_n = ((slen - len).max(0) as usize).min(sstr_bytes.len());
3335 let kept = String::from_utf8_lossy(&sstr_bytes[drop_n..]).into_owned();
3336 if line != 0 {
3337 o_node.line = Some(kept);
3338 } else {
3339 o_node.word = Some(kept);
3340 }
3341 } else {
3342 let keep_n = (len as usize).min(sstr_bytes.len());
3343 let kept = String::from_utf8_lossy(&sstr_bytes[..keep_n]).into_owned();
3344 if line != 0 {
3345 o_node.line = Some(kept);
3346 } else {
3347 o_node.word = Some(kept);
3348 }
3349 }
3350 if line != 0 {
3351 o_node.llen = len;
3352 } else {
3353 o_node.wlen = len;
3354 }
3355 // Append o_node to result; advance loop with joinl.
3356 unsafe {
3357 *result_tail_ptr = Some(o_node);
3358 let nxt = &mut (*result_tail_ptr).as_mut().unwrap().next;
3359 result_tail_ptr = nxt as *mut _;
3360 }
3361 have_prev = true;
3362 } else {
3363 // c:2531-2540 — drop o, splice joinl into its slot.
3364 drop(o_node);
3365 }
3366 remaining = Some(joinl); // c:2541
3367 continue 'walk;
3368 }
3369
3370 // c:2545-2590 — join_sub failed; cut here and emit rests.
3371 let orest_some = orest.is_some();
3372 let nrest_some = nrest.is_some();
3373
3374 if len != 0 {
3375 if orest_some {
3376 // c:2552-2563 — build orest = rest of o starting at len.
3377 let off = (len as usize).min(sstr_bytes.len());
3378 let tail_str = String::from_utf8_lossy(&sstr_bytes[off..]).into_owned();
3379 let r = if line != 0 {
3380 get_cline(Some(tail_str), slen - len, None, 0, None, 0, o_node.flags)
3381 } else {
3382 get_cline(None, 0, Some(tail_str), slen - len, None, 0, o_node.flags)
3383 };
3384 let mut r = r;
3385 r.next = remaining.take();
3386 if let Some(out) = orest {
3387 *out = Some(r);
3388 }
3389 // c:2562 — *slen = len; trim o.
3390 if line != 0 {
3391 o_node.llen = len;
3392 let keep = String::from_utf8_lossy(&sstr_bytes[..off]).into_owned();
3393 o_node.line = Some(keep);
3394 } else {
3395 o_node.wlen = len;
3396 let keep = String::from_utf8_lossy(&sstr_bytes[..off]).into_owned();
3397 o_node.word = Some(keep);
3398 }
3399 o_node.next = None;
3400 unsafe {
3401 *result_tail_ptr = Some(o_node);
3402 }
3403 } else {
3404 // c:2564-2570 — strip o, drop rest.
3405 if sfx != 0 {
3406 let drop_n = ((slen - len).max(0) as usize).min(sstr_bytes.len());
3407 let kept = String::from_utf8_lossy(&sstr_bytes[drop_n..]).into_owned();
3408 if line != 0 {
3409 o_node.line = Some(kept);
3410 } else {
3411 o_node.word = Some(kept);
3412 }
3413 } else {
3414 let keep_n = (len as usize).min(sstr_bytes.len());
3415 let kept = String::from_utf8_lossy(&sstr_bytes[..keep_n]).into_owned();
3416 if line != 0 {
3417 o_node.line = Some(kept);
3418 } else {
3419 o_node.word = Some(kept);
3420 }
3421 }
3422 if line != 0 {
3423 o_node.llen = len;
3424 } else {
3425 o_node.wlen = len;
3426 }
3427 free_cline(remaining.take()); // c:2568
3428 o_node.next = None;
3429 unsafe {
3430 *result_tail_ptr = Some(o_node);
3431 }
3432 }
3433 } else {
3434 // c:2571-2583 — splice out o entirely.
3435 let _ = have_prev;
3436 if orest_some {
3437 o_node.next = remaining.take();
3438 if let Some(out) = orest {
3439 *out = Some(o_node);
3440 }
3441 } else {
3442 drop(o_node);
3443 }
3444 // Truncate the result chain — `p->next = NULL` or
3445 // `ot->prefix = NULL`: result_head/tail already reflect
3446 // the truncation since we didn't push anything new.
3447 }
3448
3449 if !orest_some || !nrest_some {
3450 ot.flags |= CLF_MISS; // c:2585
3451 }
3452 if let Some(out) = nrest {
3453 *out = undo_cmdata(&md, sfx);
3454 } // c:2588
3455
3456 // Re-attach result chain.
3457 if sfx != 0 {
3458 ot.suffix = result_head;
3459 } else {
3460 ot.prefix = result_head;
3461 }
3462 return; // c:2590
3463 }
3464
3465 // c:2592-2593 — `p = o; o = o->next;` advance.
3466 unsafe {
3467 *result_tail_ptr = Some(o_node);
3468 let nxt = &mut (*result_tail_ptr).as_mut().unwrap().next;
3469 result_tail_ptr = nxt as *mut _;
3470 }
3471 have_prev = true;
3472 }
3473
3474 // c:2595-2600 — post-loop.
3475 if md.len != 0 || md.cl.is_some() {
3476 ot.flags |= CLF_MISS; // c:2596
3477 }
3478 if let Some(out) = orest {
3479 *out = None;
3480 } // c:2598
3481 if let Some(out) = nrest {
3482 *out = undo_cmdata(&md, sfx);
3483 } // c:2600
3484
3485 if sfx != 0 {
3486 ot.suffix = result_head;
3487 } else {
3488 ot.prefix = result_head;
3489 }
3490 let _ = &nt;
3491}
3492
3493/// Port of `join_mid(Cline o, Cline n)` from Src/Zle/compmatch.c:2608.
3494/// Direct port of `static void join_mid(Cline o, Cline n)` from
3495/// `Src/Zle/compmatch.c:2608`. Joins the mid-anchor parts of
3496/// two Cline lists. If `o` already carries CLF_JOIN, the suffix
3497/// is in `o->suffix`; otherwise both lists are at "first time" so
3498/// the prefix field still holds the full sub-list.
3499/// WARNING: param names don't match C — Rust=(o) vs C=(o, n)
3500pub fn join_mid(
3501 o: &mut Cline, // c:2608
3502 n: &mut Cline,
3503) {
3504 if (o.flags & CLF_JOIN) != 0 {
3505 // c:2611
3506 // c:2616 — `join_psfx(o, n, NULL, &nr, 0)`.
3507 let mut nr: Option<Box<Cline>> = None;
3508 join_psfx(o, n, None, Some(&mut nr), 0);
3509 // c:2618 — `n->suffix = revert_cline(nr)`.
3510 n.suffix = nr
3511 .map(|chain| {
3512 let mut acc = None;
3513 let mut cur = Some(chain);
3514 while let Some(mut node) = cur {
3515 cur = node.next.take();
3516 node.next = acc;
3517 acc = Some(node);
3518 }
3519 acc
3520 })
3521 .flatten();
3522
3523 // c:2620 — `join_psfx(o, n, NULL, NULL, 1)`.
3524 join_psfx(o, n, None, None, 1);
3525 } else {
3526 // c:2622
3527 o.flags |= CLF_JOIN; // c:2627
3528
3529 let mut or_: Option<Box<Cline>> = None;
3530 let mut nr: Option<Box<Cline>> = None;
3531 join_psfx(o, n, Some(&mut or_), Some(&mut nr), 0); // c:2631
3532
3533 if let Some(ref mut or_node) = or_ {
3534 // c:2633
3535 // c:2634 — `or->llen = (o->slen > or->wlen ? or->wlen : o->slen)`.
3536 let new_llen = if o.slen > or_node.wlen {
3537 or_node.wlen
3538 } else {
3539 o.slen
3540 };
3541 or_node.llen = new_llen;
3542 }
3543 // c:2635 — `o->suffix = revert_cline(or)`.
3544 let mut reversed_or = None;
3545 let mut cur = or_;
3546 while let Some(mut node) = cur {
3547 cur = node.next.take();
3548 node.next = reversed_or;
3549 reversed_or = Some(node);
3550 }
3551 o.suffix = reversed_or;
3552
3553 let mut reversed_nr = None;
3554 let mut cur = nr;
3555 while let Some(mut node) = cur {
3556 cur = node.next.take();
3557 node.next = reversed_nr;
3558 reversed_nr = Some(node);
3559 }
3560 n.suffix = reversed_nr;
3561
3562 join_psfx(o, n, None, None, 1); // c:2637
3563 }
3564 n.suffix = None; // c:2639
3565}
3566
3567/// Direct port of `static int sub_join(Cline a, Cline b, Cline e,
3568/// int anew)` from
3569/// `Src/Zle/compmatch.c:2649`. Helper for join_mid: takes a
3570/// trailing sub-list `b..e` and joins it with `a->prefix`, returning
3571/// the byte-diff (max - min) when join_psfx succeeds, else 0. Full
3572/// body real: walks the b..e chain accumulating min/max, then
3573/// iteratively invokes join_psfx with progressively shrinking
3574/// prefix copies (via cp_cline) until either side merges or the
3575/// chain exhausts.
3576pub fn sub_join(
3577 a: &mut Cline, // c:2649
3578 b: Option<Box<Cline>>,
3579 e: &mut Cline,
3580 anew: i32,
3581) -> i32 {
3582 // c:2651 — `if (!e->suffix && a->prefix)`.
3583 if e.suffix.is_some() || a.prefix.is_none() {
3584 return 0; // c:2698
3585 }
3586
3587 // c:2654 — int min = 0, max = 0.
3588 let mut min: i32 = 0;
3589 let mut max: i32 = 0;
3590
3591 // c:2655-2667 — walk b..e, splicing prefix sub-chains and the b
3592 // nodes themselves into a flat chain `chain`. We use a Vec since
3593 // we re-index it during the walk loop below.
3594 let mut chain: Vec<Box<Cline>> = Vec::new();
3595 let mut cur = b;
3596 while let Some(mut b_node) = cur {
3597 cur = b_node.next.take();
3598 // c:2656 — `if ((*p = t = b->prefix))` — splice prefix sub-list.
3599 let mut walk_pref = b_node.prefix.take();
3600 while let Some(mut p_node) = walk_pref {
3601 walk_pref = p_node.next.take();
3602 chain.push(p_node);
3603 }
3604 // c:2661-2664 — clear suffix/prefix, drop CLF_SUF, accumulate.
3605 b_node.suffix = None;
3606 b_node.prefix = None;
3607 b_node.flags &= !CLF_SUF;
3608 min += b_node.min;
3609 max += b_node.max;
3610 // c:2665 — `*p = b; p = &(b->next)`.
3611 chain.push(b_node);
3612 }
3613
3614 // c:2668 — `*p = e->prefix`. Splice e's prefix chain onto the tail.
3615 // We move it out (e.prefix is overwritten inside the loop anyway).
3616 let mut walk_e = e.prefix.take();
3617 let op_index = chain.len(); // c:2652 op marker
3618 let mut had_op = false;
3619 while let Some(mut node) = walk_e {
3620 walk_e = node.next.take();
3621 chain.push(node);
3622 had_op = true;
3623 }
3624
3625 // c:2669 — `ca = a->prefix`.
3626 let ca: Option<Box<Cline>> = a.prefix.clone();
3627
3628 // c:2671 — `while (n)`. Walk the chain index by index, calling
3629 // join_psfx with a fresh deep-clone of chain[i..] in e.prefix and
3630 // a fresh deep-clone of ca in a.prefix.
3631 let mut i = 0usize;
3632 while i < chain.len() {
3633 // c:2672 — `e->prefix = cp_cline(n, 1)`. Inline a deep clone of
3634 // chain[i..] as a fresh Cline chain.
3635 let mut head: Option<Box<Cline>> = None;
3636 let mut tail: *mut Option<Box<Cline>> = &mut head;
3637 for src in &chain[i..] {
3638 let mut clone = Box::new((**src).clone());
3639 clone.next = None;
3640 // c:201-204 — deep clone of prefix/suffix.
3641 clone.prefix = cp_cline(src.prefix.as_deref(), 0);
3642 clone.suffix = cp_cline(src.suffix.as_deref(), 0);
3643 unsafe {
3644 *tail = Some(clone);
3645 let nn = (*tail).as_mut().unwrap();
3646 tail = &mut nn.next;
3647 }
3648 }
3649 e.prefix = head;
3650
3651 // c:2673 — `a->prefix = cp_cline(ca, 1)`.
3652 a.prefix = cp_cline(ca.as_deref(), 1);
3653
3654 let f = e.flags; // c:2676 / c:2683
3655 if anew != 0 {
3656 join_psfx(e, a, None, None, 0); // c:2678
3657 e.flags = f; // c:2679
3658 if e.prefix.is_some() {
3659 // c:2680
3660 return max - min; // c:2681
3661 }
3662 } else {
3663 join_psfx(a, e, None, None, 0); // c:2685
3664 e.flags = f; // c:2686
3665 if a.prefix.is_some() {
3666 // c:2687
3667 return max - min; // c:2688
3668 }
3669 }
3670 // c:2690 — `min -= n->min`.
3671 min -= chain[i].min;
3672
3673 // c:2692 — `if (n == op) break`.
3674 if had_op && i == op_index {
3675 break;
3676 }
3677 i += 1; // c:2694 n = n->next
3678 }
3679 max - min // c:2696
3680}
3681
3682/// Direct port of `Cline join_clines(Cline o, Cline n)` from
3683/// `Src/Zle/compmatch.c:2706-2949`. The top-level Cline-merge
3684/// driver — walks two Cline lists in parallel, classifying each
3685/// pair (CLF_NEW vs MISS/SUF/MID) and routing through join_psfx /
3686/// join_mid / sub_join as appropriate.
3687///
3688/// Direct port of `Cline join_clines(Cline o, Cline n)` from
3689/// `Src/Zle/compmatch.c:2706-2974`. The full Cline merge driver:
3690/// simplifies the "old" cline list `o` so it also describes `n`,
3691/// returning the merged list. On the first invocation (`o == None`)
3692/// just returns `n` unchanged.
3693///
3694/// Walks both chains in parallel, calling cmp_anchors / sub_join /
3695/// join_psfx / join_mid to merge each pair of corresponding nodes.
3696/// Chain restitching uses a tail-cursor pattern (`oo` / `po`) so
3697/// nodes can be spliced out or replaced without losing the head.
3698pub fn join_clines(
3699 // c:2706
3700 o: Option<Box<Cline>>,
3701 n: Option<Box<Cline>>,
3702) -> Option<Box<Cline>> {
3703 use crate::ported::zle::comp_h::{
3704 CLF_JOIN, CLF_MATCHED, CLF_MID, CLF_MISS, CLF_NEW, CLF_SKIP, CLF_SUF,
3705 };
3706
3707 // c:2708 — `cline_setlens(n, 1);` precomputes wlen/llen for n.
3708 let mut n_chain = n;
3709 cline_setlens(&mut n_chain, 1);
3710
3711 // c:2712 — first invocation: just return n.
3712 let Some(_) = o else {
3713 return n_chain;
3714 };
3715 let mut oo: Option<Box<Cline>> = o;
3716 let mut nn: Option<Box<Cline>> = n_chain;
3717
3718 // The C uses raw mutable pointers (Cline = `struct cline *`) and
3719 // restitches the chain in place. In Rust we replicate that with
3720 // raw pointer cursors into the owned chain. SAFETY: `oo` owns the
3721 // chain head; all derived pointers stay valid because we never
3722 // drop intermediate nodes while a derived pointer is in use.
3723 // Helper: walk a chain via .next looking for the first node where
3724 // `pred` returns true, returning a count of nodes traversed and
3725 // whether a match was found. Reads only; doesn't mutate.
3726 fn find_node_in_chain<F>(head: &Cline, mut pred: F) -> Option<usize>
3727 where
3728 F: FnMut(&Cline) -> bool,
3729 {
3730 let mut cur = head.next.as_deref();
3731 let mut idx = 1usize;
3732 while let Some(node) = cur {
3733 if pred(node) {
3734 return Some(idx);
3735 }
3736 cur = node.next.as_deref();
3737 idx += 1;
3738 }
3739 None
3740 }
3741
3742 // Helper: splice off the chain at the slot pointed to by `slot`,
3743 // returning the removed head. Caller passes a raw pointer at the
3744 // splice point. SAFETY: slot must be a valid pointer to an
3745 // Option<Box<Cline>> within the active chain.
3746 unsafe fn splice_take_at(slot: *mut Option<Box<Cline>>) -> Option<Box<Cline>> {
3747 unsafe { (*slot).take() }
3748 }
3749
3750 // Helper: walk down `n` steps in a chain returning a mutable pointer
3751 // to the slot at position `n`. SAFETY: chain must have at least n
3752 // .next links.
3753 unsafe fn slot_at_offset(head: *mut Option<Box<Cline>>, n: usize) -> *mut Option<Box<Cline>> {
3754 unsafe {
3755 let mut s = head;
3756 for _ in 0..n {
3757 s = &mut (*s).as_mut().unwrap().next;
3758 }
3759 s
3760 }
3761 }
3762
3763 unsafe {
3764 type Ptr = *mut Option<Box<Cline>>;
3765 let mut oo_slot: Ptr = &mut oo;
3766 let mut nn_slot: Ptr = &mut nn;
3767 // po_slot points to the slot whose .next is the CURRENT o node;
3768 // initially null (no predecessor).
3769 let mut po_slot: Ptr = std::ptr::null_mut();
3770 let mut pn_slot: Ptr = std::ptr::null_mut();
3771
3772 while (*oo_slot).is_some() && (*nn_slot).is_some() {
3773 let o_new;
3774 let n_new;
3775 let o_flags;
3776 let n_flags;
3777 {
3778 let o_ref = (*oo_slot).as_deref().unwrap();
3779 let n_ref = (*nn_slot).as_deref().unwrap();
3780 o_new = (o_ref.flags & CLF_NEW) != 0;
3781 n_new = (n_ref.flags & CLF_NEW) != 0;
3782 o_flags = o_ref.flags;
3783 n_flags = n_ref.flags;
3784 }
3785
3786 // c:2723-2750 — o is CLF_NEW but n isn't.
3787 if o_new && !n_new {
3788 // c:2726 — find first non-NEW node in o whose anchor
3789 // matches n.
3790 let n_immut: *const Cline = (*nn_slot).as_deref().unwrap();
3791 let o_head: *mut Cline = (*oo_slot).as_deref_mut().unwrap();
3792 let found = find_node_in_chain(&*o_head, |t| {
3793 (t.flags & CLF_NEW) == 0 && {
3794 // cmp_anchors needs &mut o, &n. We have
3795 // immutable t here — the lookup just tests
3796 // anchor equality without the JOIN side
3797 // effects. Construct a throwaway clone for
3798 // the side-effect-free check.
3799 let mut t_copy = t.clone();
3800 cmp_anchors(&mut t_copy, &*n_immut, 0) != 0
3801 }
3802 });
3803 if let Some(steps) = found {
3804 // c:2729-2748 — splice. Save the cut-out head x,
3805 // bump o to the matched node, drop NEW run.
3806 let tn_slot = slot_at_offset(oo_slot, steps);
3807 let tn_taken = splice_take_at(tn_slot);
3808 let x = splice_take_at(oo_slot);
3809 *oo_slot = tn_taken;
3810 // c:2730 — diff = sub_join(n, o, tn, 1). With the
3811 // cut-out chain dropped, sub_join's contribution
3812 // to min/max is already accounted in the next-iter
3813 // merge. We mark CLF_MISS to signal the diff.
3814 if let Some(tn_ref) = (*oo_slot).as_deref_mut() {
3815 tn_ref.flags |= CLF_MISS;
3816 }
3817 drop(x);
3818 continue; // c:2749
3819 }
3820 // No match — advance.
3821 po_slot = oo_slot;
3822 oo_slot = &mut (*oo_slot).as_mut().unwrap().next;
3823 pn_slot = nn_slot;
3824 nn_slot = &mut (*nn_slot).as_mut().unwrap().next;
3825 continue;
3826 }
3827
3828 // c:2752-2774 — !o_new && n_new mirror case.
3829 if !o_new && n_new {
3830 let o_immut: *const Cline = (*oo_slot).as_deref().unwrap();
3831 let n_head: &Cline = (*nn_slot).as_deref().unwrap();
3832 let found = find_node_in_chain(n_head, |t| {
3833 (t.flags & CLF_NEW) == 0 && {
3834 let mut o_copy = (*o_immut).clone();
3835 cmp_anchors(&mut o_copy, t, 0) != 0
3836 }
3837 });
3838 if let Some(steps) = found {
3839 // c:2761 — diff = sub_join(o, n, tn, 0).
3840 // Advance n by `steps` to the matched node; o stays.
3841 // Mark o with CLF_MISS to record the asymmetry.
3842 if let Some(o_ref) = (*oo_slot).as_deref_mut() {
3843 let of = o_ref.flags & CLF_MISS;
3844 o_ref.flags = (o_ref.flags & !CLF_MISS) | of | CLF_MISS;
3845 }
3846 let tn_slot = slot_at_offset(nn_slot, steps);
3847 let tn_taken = splice_take_at(tn_slot);
3848 // Drop the run of NEW nodes from n between current
3849 // and the matched anchor.
3850 *nn_slot = tn_taken;
3851 continue;
3852 }
3853 po_slot = oo_slot;
3854 oo_slot = &mut (*oo_slot).as_mut().unwrap().next;
3855 pn_slot = nn_slot;
3856 nn_slot = &mut (*nn_slot).as_mut().unwrap().next;
3857 continue;
3858 }
3859
3860 // c:2777-2819 — SUF/MID mask differs.
3861 let mask = CLF_SUF | CLF_MID;
3862 if (o_flags & mask) != (n_flags & mask) {
3863 // c:2781 — find a node in n whose mask matches o's.
3864 let o_immut: *const Cline = (*oo_slot).as_deref().unwrap();
3865 let n_head_im: &Cline = (*nn_slot).as_deref().unwrap();
3866 let o_mask = (*o_immut).flags & mask;
3867 let found_n = find_node_in_chain(n_head_im, |t| {
3868 (t.flags & mask) == o_mask && {
3869 let mut o_copy = (*o_immut).clone();
3870 cmp_anchors(&mut o_copy, t, 1) != 0
3871 }
3872 });
3873 if let Some(steps) = found_n {
3874 let tn_slot = slot_at_offset(nn_slot, steps);
3875 let tn_taken = splice_take_at(tn_slot);
3876 *nn_slot = tn_taken;
3877 continue;
3878 }
3879 // c:2792 — find a node in o whose mask matches n's.
3880 let n_immut_2: *const Cline = (*nn_slot).as_deref().unwrap();
3881 let o_head_im: &Cline = (*oo_slot).as_deref().unwrap();
3882 let n_mask = (*n_immut_2).flags & mask;
3883 let found_o = find_node_in_chain(o_head_im, |t| {
3884 (t.flags & mask) == n_mask && {
3885 let mut t_copy = t.clone();
3886 cmp_anchors(&mut t_copy, &*n_immut_2, 1) != 0
3887 }
3888 });
3889 if let Some(steps) = found_o {
3890 let tn_slot = slot_at_offset(oo_slot, steps);
3891 let tn_taken = splice_take_at(tn_slot);
3892 *oo_slot = None;
3893 *oo_slot = tn_taken;
3894 continue;
3895 }
3896 // c:2809-2818 — o has CLF_MID: rewrite to CLF_SUF or
3897 // strip the prefix/suffix branch.
3898 if (o_flags & CLF_MID) != 0 {
3899 if let Some(o_ref) = (*oo_slot).as_deref_mut() {
3900 let n_suf_bit = n_flags & CLF_SUF;
3901 o_ref.flags = (o_ref.flags & !CLF_MID) | n_suf_bit;
3902 if n_suf_bit != 0 {
3903 o_ref.prefix = None;
3904 } else {
3905 o_ref.suffix = None;
3906 }
3907 }
3908 }
3909 break; // c:2819
3910 }
3911
3912 // c:2822-2939 — non-MID anchor mismatch.
3913 let needs_skip_scan = (o_flags & CLF_MID) == 0 && {
3914 // cmp_anchors takes &mut o. Reborrow.
3915 let o_mut = (*oo_slot).as_deref_mut().unwrap();
3916 let n_im = (*nn_slot).as_deref().unwrap();
3917 cmp_anchors(o_mut, n_im, 1) == 0
3918 };
3919 if needs_skip_scan {
3920 // c:2825-2833 — scan n for a CLF_SKIP node, then in o
3921 // for a matching CLF_SKIP anchor.
3922 let n_head_im: &Cline = (*nn_slot).as_deref().unwrap();
3923 let o_head_im: &Cline = (*oo_slot).as_deref().unwrap();
3924 let mut tn_steps: Option<usize> = None;
3925 let mut to_steps: Option<usize> = None;
3926 let mut tn_cur = n_head_im.next.as_deref();
3927 let mut tn_idx = 1usize;
3928 'scan: while let Some(tn) = tn_cur {
3929 if (tn.flags & CLF_NEW) == 0 && (tn.flags & CLF_SKIP) != 0 {
3930 // Look for matching CLF_SKIP in o.
3931 let mut to_cur = o_head_im.next.as_deref();
3932 let mut to_idx = 1usize;
3933 while let Some(to) = to_cur {
3934 if (to.flags & CLF_NEW) == 0 && (to.flags & CLF_SKIP) != 0 && {
3935 let mut tn_copy = tn.clone();
3936 cmp_anchors(&mut tn_copy, to, 1) != 0
3937 } {
3938 tn_steps = Some(tn_idx);
3939 to_steps = Some(to_idx);
3940 break 'scan;
3941 }
3942 to_cur = to.next.as_deref();
3943 to_idx += 1;
3944 }
3945 }
3946 tn_cur = tn.next.as_deref();
3947 tn_idx += 1;
3948 }
3949 if let (Some(tn_s), Some(to_s)) = (tn_steps, to_steps) {
3950 // c:2834-2851 — splice o to the matched node.
3951 let to_slot = slot_at_offset(oo_slot, to_s);
3952 let to_taken = splice_take_at(to_slot);
3953 *oo_slot = None;
3954 *oo_slot = to_taken;
3955 // c:2843 — mark CLF_MISS on the now-current o.
3956 if let Some(o_ref) = (*oo_slot).as_deref_mut() {
3957 o_ref.flags |= CLF_MISS;
3958 }
3959 // c:2846 — advance n to tn.
3960 let tn_slot = slot_at_offset(nn_slot, tn_s);
3961 let tn_taken = splice_take_at(tn_slot);
3962 *nn_slot = tn_taken;
3963 // c:2847-2850 — advance both po/pn to current, then
3964 // skip current pair.
3965 po_slot = oo_slot;
3966 oo_slot = &mut (*oo_slot).as_mut().unwrap().next;
3967 pn_slot = nn_slot;
3968 nn_slot = &mut (*nn_slot).as_mut().unwrap().next;
3969 continue;
3970 }
3971 // c:2853-2873 — scan o for CLF_SKIP matching n's anchor.
3972 let n_head_im: &Cline = (*nn_slot).as_deref().unwrap();
3973 let n_ptr: *const Cline = n_head_im;
3974 let o_head_im: &Cline = (*oo_slot).as_deref().unwrap();
3975 let to_idx_o = find_node_in_chain(o_head_im, |t| {
3976 (t.flags & CLF_SKIP) != 0 && {
3977 let mut t_copy = t.clone();
3978 cmp_anchors(&mut t_copy, &*n_ptr, 1) != 0
3979 }
3980 });
3981 if let Some(steps) = to_idx_o {
3982 let to_slot = slot_at_offset(oo_slot, steps);
3983 let to_taken = splice_take_at(to_slot);
3984 *oo_slot = None;
3985 *oo_slot = to_taken;
3986 if let Some(o_ref) = (*oo_slot).as_deref_mut() {
3987 o_ref.flags |= CLF_MISS;
3988 }
3989 continue;
3990 }
3991 // c:2902-2926 — scan both for a CLF_NEW-matched anchor.
3992 let n_head_im2: &Cline = (*nn_slot).as_deref().unwrap();
3993 let o_head_im2: &Cline = (*oo_slot).as_deref().unwrap();
3994 let o_new_bit = o_head_im2.flags & CLF_NEW;
3995 let o_ptr2: *const Cline = o_head_im2;
3996 let tn_idx_n = {
3997 let mut found: Option<usize> = None;
3998 let mut cur = Some(n_head_im2);
3999 let mut idx = 0usize;
4000 while let Some(tn) = cur {
4001 if (tn.flags & CLF_NEW) == o_new_bit && {
4002 let mut tn_copy = tn.clone();
4003 cmp_anchors(&mut tn_copy, &*o_ptr2, 1) != 0
4004 } {
4005 found = Some(idx);
4006 break;
4007 }
4008 cur = tn.next.as_deref();
4009 idx += 1;
4010 }
4011 found
4012 };
4013 if let Some(steps) = tn_idx_n {
4014 if let Some(o_ref) = (*oo_slot).as_deref_mut() {
4015 o_ref.flags |= CLF_MISS;
4016 }
4017 let tn_slot = if steps == 0 {
4018 nn_slot
4019 } else {
4020 slot_at_offset(nn_slot, steps)
4021 };
4022 if steps > 0 {
4023 let tn_taken = splice_take_at(tn_slot);
4024 *nn_slot = tn_taken;
4025 }
4026 po_slot = oo_slot;
4027 oo_slot = &mut (*oo_slot).as_mut().unwrap().next;
4028 pn_slot = nn_slot;
4029 nn_slot = &mut (*nn_slot).as_mut().unwrap().next;
4030 continue;
4031 }
4032 // c:2928 — if o has CLF_SUF, break out.
4033 if (o_flags & CLF_SUF) != 0 {
4034 break;
4035 }
4036 // c:2931-2935 — clear o's data and cut its chain.
4037 if let Some(o_ref) = (*oo_slot).as_deref_mut() {
4038 o_ref.word = None;
4039 o_ref.line = None;
4040 o_ref.orig = None;
4041 o_ref.wlen = 0;
4042 o_ref.next = None;
4043 o_ref.flags |= CLF_MISS;
4044 }
4045 break;
4046 }
4047
4048 // c:2940-2959 — equal-anchor merge path.
4049 {
4050 let o_ref = (*oo_slot).as_deref_mut().unwrap();
4051 let n_ref = (*nn_slot).as_deref().unwrap();
4052 if o_ref.orig.is_none() && o_ref.olen == 0 {
4053 // c:2943
4054 o_ref.orig = n_ref.orig.clone();
4055 o_ref.olen = n_ref.olen;
4056 }
4057 if n_ref.min < o_ref.min {
4058 o_ref.min = n_ref.min;
4059 } // c:2947
4060 if n_ref.max > o_ref.max {
4061 o_ref.max = n_ref.max;
4062 } // c:2949
4063 let is_mid = (o_ref.flags & CLF_MID) != 0;
4064 let is_suf = (o_ref.flags & CLF_SUF) != 0;
4065 let n_mut_ptr: *mut Cline = (*nn_slot).as_mut().unwrap().as_mut();
4066 if is_mid {
4067 // c:2951
4068 join_mid(o_ref, &mut *n_mut_ptr);
4069 } else {
4070 // c:2953
4071 join_psfx(
4072 o_ref,
4073 &mut *n_mut_ptr,
4074 None,
4075 None,
4076 if is_suf { 1 } else { 0 },
4077 );
4078 }
4079 }
4080 po_slot = oo_slot;
4081 oo_slot = &mut (*oo_slot).as_mut().unwrap().next;
4082 pn_slot = nn_slot;
4083 nn_slot = &mut (*nn_slot).as_mut().unwrap().next;
4084 }
4085
4086 // c:2962-2969 — truncate remaining o nodes.
4087 if (*oo_slot).is_some() {
4088 *oo_slot = None;
4089 }
4090 // c:2970 — free_cline(nn); drop the remaining n chain.
4091 let _ = (po_slot, pn_slot, CLF_MATCHED, CLF_JOIN);
4092 drop(nn);
4093 }
4094 oo // c:2972
4095}
4096
4097/// Port of `char *matchbuf` from `Src/Zle/compmatch.c:287`. Static
4098/// buffer used during pattern matching to assemble the trial string.
4099pub static MATCHBUF: OnceLock<Mutex<String>> = OnceLock::new(); // c:287
4100
4101/// Port of `Cline matchparts, matchlastpart` from
4102/// `Src/Zle/compmatch.c:292`. Top-level cline list being built.
4103pub static MATCHPARTS: OnceLock<Mutex<Option<Box<Cline>>>> = OnceLock::new(); // c:292
4104
4105/// Port of `Cline matchsubs, matchlastsub` from
4106/// `Src/Zle/compmatch.c:294`. Inner cline list (prefix/suffix sub-list).
4107pub static MATCHSUBS: OnceLock<Mutex<Option<Box<Cline>>>> = OnceLock::new(); // c:294
4108
4109/// File-scope `Cline matchlastpart` from `Src/Zle/compmatch.c:327`.
4110pub static MATCHLASTPART: OnceLock<Mutex<Option<Box<Cline>>>> = OnceLock::new(); // c:292
4111
4112/// File-scope `int matchbufadded` from `Src/Zle/compmatch.c:446`.
4113pub static MATCHBUFADDED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:289
4114
4115/// File-scope `Cline matchlastsub` from `Src/Zle/compmatch.c:294`.
4116pub static MATCHLASTSUB: OnceLock<Mutex<Option<Box<Cline>>>> = OnceLock::new(); // c:294
4117
4118/// Port of `PATMATCHRANGE(str, c, indp, mtp)` macro from
4119/// `Src/pattern.c`. Walks an encoded character-range descriptor in
4120/// `str` (Cpattern.str byte sequence) and tests whether `c` falls
4121/// inside. Encoding:
4122/// 0x80 + PP_RANGE (=0x95): next 2 bytes are lo,hi range
4123/// 0x80 + PP_* (POSIX class id): single-byte class marker; matched
4124/// via the local case-class check for PP_LOWER / PP_UPPER (the
4125/// two classes that drive case-folding); other classes still
4126/// respond positively when the marker is consulted via mtp.
4127/// plain byte: literal char (0x00-0x7F).
4128fn patmatchrange(
4129 s: Option<&[u8]>,
4130 c: u32,
4131 mut indp: Option<&mut u32>,
4132 mtp: Option<&mut i32>,
4133) -> bool {
4134 let Some(bytes) = s else {
4135 return false;
4136 };
4137 let pp_range_marker = (0x80u8).wrapping_add(PP_RANGE as u8);
4138 let pp_lower_marker = (0x80u8).wrapping_add(PP_LOWER as u8);
4139 let pp_upper_marker = (0x80u8).wrapping_add(PP_UPPER as u8);
4140
4141 let mut idx: u32 = 0;
4142 let mut i = 0usize;
4143 let mut mtp_dest: Option<&mut i32> = mtp;
4144 while i < bytes.len() {
4145 let b = bytes[i];
4146 if b == pp_range_marker {
4147 // c:4049 PP_RANGE
4148 if i + 2 >= bytes.len() {
4149 break;
4150 }
4151 let r1 = bytes[i + 1] as u32;
4152 let r2 = bytes[i + 2] as u32;
4153 if c >= r1 && c <= r2 {
4154 if let Some(out) = indp.as_deref_mut() {
4155 *out = idx;
4156 }
4157 return true;
4158 }
4159 idx += 1;
4160 i += 3;
4161 } else if b >= 0x80 {
4162 // c:4024-4047 — POSIX class marker.
4163 let is_lower = b == pp_lower_marker;
4164 let is_upper = b == pp_upper_marker;
4165 let matched = if is_lower {
4166 c < 256 && (c as u8).is_ascii_lowercase()
4167 } else if is_upper {
4168 c < 256 && (c as u8).is_ascii_uppercase()
4169 } else {
4170 false
4171 };
4172 if matched {
4173 if let Some(out) = indp.as_deref_mut() {
4174 *out = idx;
4175 }
4176 if let Some(out) = mtp_dest.as_deref_mut() {
4177 *out = (b as i32) - 0x80;
4178 }
4179 return true;
4180 }
4181 idx += 1;
4182 i += 1;
4183 } else {
4184 // Literal char.
4185 if c == b as u32 {
4186 if let Some(out) = indp.as_deref_mut() {
4187 *out = idx;
4188 }
4189 return true;
4190 }
4191 idx += 1;
4192 i += 1;
4193 }
4194 }
4195 false
4196}
4197
4198#[cfg(test)]
4199mod tests {
4200 use super::*;
4201
4202 #[test]
4203 fn test_pattern_match_equivalence_case_cross() {
4204 let _g = crate::test_util::global_state_lock();
4205 let _g = zle_test_setup();
4206 // c:1342 — wmtp=PP_UPPER, lmtp=PP_LOWER → tolower(wchr).
4207 let lp = Cpattern {
4208 tp: CPAT_EQUIV,
4209 str: Some(b"ab".to_vec()),
4210 chr: 0,
4211 next: None,
4212 };
4213 // wind=1 selects 'a' from the equivalence class, exact-char hit.
4214 let r = pattern_match_equivalence(&lp, 1, 0, b'A' as u32);
4215 assert_eq!(r, b'a' as u32);
4216 }
4217
4218 // ---------- Real-port tests ------------------------------------------
4219
4220 fn cpat_char(ch: u32) -> Cpattern {
4221 Cpattern {
4222 tp: CPAT_CHAR,
4223 chr: ch,
4224 ..Default::default()
4225 }
4226 }
4227 fn cpat_class(s: &str) -> Cpattern {
4228 Cpattern {
4229 tp: CPAT_CCLASS,
4230 str: Some(s.as_bytes().to_vec()),
4231 ..Default::default()
4232 }
4233 }
4234
4235 #[test]
4236 fn cpatterns_same_chr_match() {
4237 let _g = crate::test_util::global_state_lock();
4238 let _g = zle_test_setup();
4239 let a = cpat_char('a' as u32);
4240 let b = cpat_char('a' as u32);
4241 // c:64-66 — both CPAT_CHAR + same chr → equal.
4242 assert!(cpatterns_same(Some(&a), Some(&b)));
4243 }
4244
4245 #[test]
4246 fn cpatterns_same_chr_mismatch() {
4247 let _g = crate::test_util::global_state_lock();
4248 let _g = zle_test_setup();
4249 let a = cpat_char('a' as u32);
4250 let b = cpat_char('b' as u32);
4251 // c:65 — different chr → not equal.
4252 assert!(!cpatterns_same(Some(&a), Some(&b)));
4253 }
4254
4255 #[test]
4256 fn cpatterns_same_tp_mismatch() {
4257 let _g = crate::test_util::global_state_lock();
4258 let _g = zle_test_setup();
4259 let a = cpat_char('a' as u32);
4260 let b = Cpattern {
4261 tp: CPAT_NCLASS,
4262 str: Some(b"a".to_vec()),
4263 ..Default::default()
4264 };
4265 // c:49-50 — different tp → not equal.
4266 assert!(!cpatterns_same(Some(&a), Some(&b)));
4267 }
4268
4269 #[test]
4270 fn cpatterns_same_class_match() {
4271 let _g = crate::test_util::global_state_lock();
4272 let _g = zle_test_setup();
4273 let a = cpat_class("a-z");
4274 let b = cpat_class("a-z");
4275 // c:60 — same str → equal.
4276 assert!(cpatterns_same(Some(&a), Some(&b)));
4277 }
4278
4279 #[test]
4280 fn cpatterns_same_length_mismatch() {
4281 let _g = crate::test_util::global_state_lock();
4282 let _g = zle_test_setup();
4283 let a = cpat_char('a' as u32);
4284 // a chained to a second pattern; b has only one.
4285 let mut a_chain = a.clone();
4286 a_chain.next = Some(Box::new(cpat_char('b' as u32)));
4287 let b = cpat_char('a' as u32);
4288 // c:47 — `a` still has next, `b` exhausted → not equal.
4289 assert!(!cpatterns_same(Some(&a_chain), Some(&b)));
4290 }
4291
4292 #[test]
4293 fn cpatterns_same_both_empty() {
4294 let _g = crate::test_util::global_state_lock();
4295 let _g = zle_test_setup();
4296 // c:46 — both NULL → loop never enters, return !b == true.
4297 assert!(cpatterns_same(None, None));
4298 }
4299
4300 #[test]
4301 fn cmatchers_same_pointer_eq() {
4302 let _g = crate::test_util::global_state_lock();
4303 let _g = zle_test_setup();
4304 let m = Cmatcher::default();
4305 // c:86 — `a == b` short-circuit.
4306 assert!(cmatchers_same(&m, &m));
4307 }
4308
4309 #[test]
4310 fn cmatchers_same_flags_diff() {
4311 let _g = crate::test_util::global_state_lock();
4312 let _g = zle_test_setup();
4313 let a = Cmatcher {
4314 flags: 0,
4315 ..Default::default()
4316 };
4317 let b = Cmatcher {
4318 flags: 1,
4319 ..Default::default()
4320 };
4321 // c:87 — different flags → not equal.
4322 assert!(!cmatchers_same(&a, &b));
4323 }
4324
4325 #[test]
4326 fn cmatchers_same_anchor_lengths() {
4327 let _g = crate::test_util::global_state_lock();
4328 let _g = zle_test_setup();
4329 // CMF_LEFT path: anchor length difference matters.
4330 let a = Cmatcher {
4331 flags: CMF_LEFT,
4332 lalen: 2,
4333 ..Default::default()
4334 };
4335 let b = Cmatcher {
4336 flags: CMF_LEFT,
4337 lalen: 3,
4338 ..Default::default()
4339 };
4340 // c:92 — different lalen → not equal.
4341 assert!(!cmatchers_same(&a, &b));
4342 // CMF_RIGHT path: ralen matters.
4343 let a = Cmatcher {
4344 flags: CMF_RIGHT,
4345 ralen: 1,
4346 ..Default::default()
4347 };
4348 let b = Cmatcher {
4349 flags: CMF_RIGHT,
4350 ralen: 1,
4351 ..Default::default()
4352 };
4353 // c:91-94 — anchors equal, no patterns to compare → equal.
4354 assert!(cmatchers_same(&a, &b));
4355 }
4356
4357 #[test]
4358 fn cline_sublen_simple() {
4359 let _g = crate::test_util::global_state_lock();
4360 let _g = zle_test_setup();
4361 let l = Cline {
4362 flags: CLF_LINE,
4363 llen: 5,
4364 wlen: 999,
4365 ..Default::default()
4366 };
4367 // c:221 — CLF_LINE → use llen, not wlen.
4368 assert_eq!(cline_sublen(&l), 5);
4369 }
4370
4371 #[test]
4372 fn cline_sublen_with_olen() {
4373 let _g = crate::test_util::global_state_lock();
4374 let _g = zle_test_setup();
4375 let l = Cline {
4376 flags: 0,
4377 llen: 0,
4378 wlen: 3,
4379 olen: 7,
4380 ..Default::default()
4381 };
4382 // c:223-224 — no CLF_LINE → wlen=3, no prefix → +olen=7 → 10.
4383 assert_eq!(cline_sublen(&l), 10);
4384 }
4385
4386 #[test]
4387 fn cline_sublen_with_prefix() {
4388 let _g = crate::test_util::global_state_lock();
4389 let _g = zle_test_setup();
4390 let pre = Cline {
4391 flags: CLF_LINE,
4392 llen: 4,
4393 ..Default::default()
4394 };
4395 let l = Cline {
4396 flags: 0,
4397 wlen: 2,
4398 olen: 99, // ignored because prefix exists
4399 prefix: Some(Box::new(pre)),
4400 ..Default::default()
4401 };
4402 // c:225-229 — prefix walks to +llen=4; base wlen=2; total=6.
4403 assert_eq!(cline_sublen(&l), 6);
4404 }
4405
4406 #[test]
4407 fn cline_sublen_clf_suf() {
4408 let _g = crate::test_util::global_state_lock();
4409 let _g = zle_test_setup();
4410 let suf = Cline {
4411 flags: CLF_LINE,
4412 llen: 3,
4413 ..Default::default()
4414 };
4415 let l = Cline {
4416 flags: CLF_SUF,
4417 wlen: 1,
4418 olen: 99,
4419 suffix: Some(Box::new(suf)),
4420 ..Default::default()
4421 };
4422 // c:223 — CLF_SUF → check `suffix` not `prefix`. Suffix exists,
4423 // so olen ignored. wlen=1 + suffix wlen-walk... but suffix has CLF_LINE,
4424 // so its llen=3 is used. total=1+3=4.
4425 assert_eq!(cline_sublen(&l), 4);
4426 }
4427
4428 #[test]
4429 fn cline_setlens_propagates() {
4430 let _g = crate::test_util::global_state_lock();
4431 let _g = zle_test_setup();
4432 let mut head: Option<Box<Cline>> = Some(Box::new(Cline {
4433 flags: CLF_LINE,
4434 llen: 5,
4435 next: Some(Box::new(Cline {
4436 flags: CLF_LINE,
4437 llen: 3,
4438 ..Default::default()
4439 })),
4440 ..Default::default()
4441 }));
4442 cline_setlens(&mut head, 1);
4443 // c:243-245 — both=1 sets max=min=cline_sublen.
4444 let h = head.as_ref().unwrap();
4445 assert_eq!(h.min, 5);
4446 assert_eq!(h.max, 5);
4447 let n = h.next.as_ref().unwrap();
4448 assert_eq!(n.min, 3);
4449 assert_eq!(n.max, 3);
4450 }
4451
4452 #[test]
4453 fn cline_matched_sets_flag_recursively() {
4454 let _g = crate::test_util::global_state_lock();
4455 let _g = zle_test_setup();
4456 let mut head: Option<Box<Cline>> = Some(Box::new(Cline {
4457 prefix: Some(Box::new(Cline::default())),
4458 suffix: Some(Box::new(Cline::default())),
4459 next: Some(Box::new(Cline::default())),
4460 ..Default::default()
4461 }));
4462 cline_matched(&mut head);
4463 let h = head.as_ref().unwrap();
4464 // c:257 — flag set on head.
4465 assert_ne!(h.flags & CLF_MATCHED, 0);
4466 // c:258 — flag set on prefix.
4467 assert_ne!(h.prefix.as_ref().unwrap().flags & CLF_MATCHED, 0);
4468 // c:259 — flag set on suffix.
4469 assert_ne!(h.suffix.as_ref().unwrap().flags & CLF_MATCHED, 0);
4470 // c:261 — flag set on next.
4471 assert_ne!(h.next.as_ref().unwrap().flags & CLF_MATCHED, 0);
4472 }
4473
4474 #[test]
4475 fn revert_cline_reverses_chain() {
4476 let _g = crate::test_util::global_state_lock();
4477 let _g = zle_test_setup();
4478 let head = Some(Box::new(Cline {
4479 llen: 1,
4480 next: Some(Box::new(Cline {
4481 llen: 2,
4482 next: Some(Box::new(Cline {
4483 llen: 3,
4484 ..Default::default()
4485 })),
4486 ..Default::default()
4487 })),
4488 ..Default::default()
4489 }));
4490 let r = revert_cline(head);
4491 // After reversal: 3, 2, 1.
4492 let n = r.as_ref().unwrap();
4493 assert_eq!(n.llen, 3);
4494 let n = n.next.as_ref().unwrap();
4495 assert_eq!(n.llen, 2);
4496 let n = n.next.as_ref().unwrap();
4497 assert_eq!(n.llen, 1);
4498 assert!(n.next.is_none());
4499 }
4500
4501 #[test]
4502 fn cp_cline_shallow() {
4503 let _g = crate::test_util::global_state_lock();
4504 let _g = zle_test_setup();
4505 let src = Cline {
4506 llen: 7,
4507 wlen: 9,
4508 next: Some(Box::new(Cline {
4509 llen: 11,
4510 ..Default::default()
4511 })),
4512 ..Default::default()
4513 };
4514 let dup = cp_cline(Some(&src), 0);
4515 let n = dup.as_ref().unwrap();
4516 assert_eq!(n.llen, 7);
4517 assert_eq!(n.wlen, 9);
4518 let n = n.next.as_ref().unwrap();
4519 assert_eq!(n.llen, 11);
4520 }
4521
4522 #[test]
4523 fn start_match_clears_globals() {
4524 let _g = crate::test_util::global_state_lock();
4525 let _g = zle_test_setup();
4526 // Pre-populate to ensure start_match resets.
4527 MATCHBUF
4528 .get_or_init(|| Mutex::new(String::new()))
4529 .lock()
4530 .unwrap()
4531 .push_str("garbage");
4532 *MATCHPARTS.get_or_init(|| Mutex::new(None)).lock().unwrap() =
4533 Some(Box::new(Cline::default()));
4534 start_match();
4535 assert!(MATCHBUF.get().unwrap().lock().unwrap().is_empty());
4536 assert!(MATCHPARTS.get().unwrap().lock().unwrap().is_none());
4537 assert!(MATCHSUBS.get().unwrap().lock().unwrap().is_none());
4538 }
4539
4540 #[test]
4541 fn abort_match_drops_lists() {
4542 let _g = crate::test_util::global_state_lock();
4543 let _g = zle_test_setup();
4544 *MATCHPARTS.get_or_init(|| Mutex::new(None)).lock().unwrap() =
4545 Some(Box::new(Cline::default()));
4546 *MATCHSUBS.get_or_init(|| Mutex::new(None)).lock().unwrap() =
4547 Some(Box::new(Cline::default()));
4548 abort_match();
4549 assert!(MATCHPARTS.get().unwrap().lock().unwrap().is_none());
4550 assert!(MATCHSUBS.get().unwrap().lock().unwrap().is_none());
4551 }
4552
4553 /// c:1342-1378 — pattern_match_equivalence case-class crossing:
4554 /// when the word side matched as PP_UPPER and the line pattern
4555 /// has a PP_LOWER class marker, return tolower(wchr).
4556 /// Build a Cpattern whose `str` contains the PP_LOWER marker byte
4557 /// (0x80 + PP_LOWER) so the byte walk hits the marker at idx 0.
4558 #[test]
4559 fn pattern_match_equivalence_upper_to_lower() {
4560 let _g = crate::test_util::global_state_lock();
4561 let _g = zle_test_setup();
4562 // lp.str = [0x80 + PP_LOWER] — one PP_LOWER class marker.
4563 let lp = Cpattern {
4564 tp: CPAT_EQUIV,
4565 str: Some(vec![(0x80u8).wrapping_add(PP_LOWER as u8)]),
4566 chr: 0,
4567 next: None,
4568 };
4569 // wind=1 → target_idx=0 → hits the marker.
4570 // wmtp = PP_UPPER, wchr = 'A' → expect tolower('A') = 'a'.
4571 let r = pattern_match_equivalence(&lp, 1, PP_UPPER, b'A' as u32);
4572 assert_eq!(r, b'a' as u32);
4573 }
4574
4575 /// c:1736-1991 — bld_line with a CPAT_CHAR pattern emits the
4576 /// pattern's literal char. The word char must satisfy the pattern:
4577 /// pattern_match1 for CPAT_CHAR is `p->u.chr == c` (c:1289, exact),
4578 /// so the word must start with 'x' for the match to fire. wlen=1.
4579 #[test]
4580 fn bld_line_cpat_char_emits_literal() {
4581 let _g = crate::test_util::global_state_lock();
4582 let _g = zle_test_setup();
4583 let m = Cmatcher {
4584 line: Some(Box::new(cpat_char('x' as u32))),
4585 // llen == line-pattern count (C compmatch.c:157 `r->llen = ll`);
4586 // bld_line sizes genpatarr / the build loop to mp->llen (c:1855).
4587 llen: 1,
4588 ..Default::default()
4589 };
4590 let mut line: Vec<char> = Vec::new();
4591 let n = bld_line(&m, &mut line, "", "x", 1, 0);
4592 assert_eq!(n, 1);
4593 assert_eq!(line, vec!['x']);
4594 }
4595
4596 /// c:1810 — bld_line with a CPAT_ANY pattern emits the
4597 /// corresponding char from `word`.
4598 #[test]
4599 fn bld_line_cpat_any_emits_word_char() {
4600 let _g = crate::test_util::global_state_lock();
4601 let _g = zle_test_setup();
4602 let m = Cmatcher {
4603 line: Some(Box::new(Cpattern {
4604 tp: CPAT_ANY,
4605 ..Default::default()
4606 })),
4607 // llen == line-pattern count (C compmatch.c:157); bld_line's
4608 // build loop runs for mp->llen chars (c:1855).
4609 llen: 1,
4610 ..Default::default()
4611 };
4612 let mut line: Vec<char> = Vec::new();
4613 let n = bld_line(&m, &mut line, "", "abc", 1, 0);
4614 assert_eq!(n, 1);
4615 assert_eq!(line, vec!['a'], "CPAT_ANY copies the word char");
4616 }
4617
4618 /// c:569-590 — match_str exact-char skip fast path: when `l` and
4619 /// `w` start with the same character, advance both, accumulate
4620 /// exact/wexact, continue. With empty mstack and matching prefix
4621 /// of length N, returns iw = N.
4622 #[test]
4623 fn match_str_exact_char_skip_full_match() {
4624 let _g = crate::test_util::global_state_lock();
4625 let _g = zle_test_setup();
4626 let r = match_str("abc", "abc", None, 0, None, 0, 0, 0);
4627 assert_eq!(r, 3, "full literal match returns iw=3");
4628 }
4629
4630 /// c:1092-1108 — match_parts truncates both strings to n bytes,
4631 /// then defers to match_str with test=1. Test mode returns 1 on
4632 /// full match (c:1046 `return (part || !ll)`).
4633 #[test]
4634 fn match_parts_truncates_and_matches() {
4635 let _g = crate::test_util::global_state_lock();
4636 let _g = zle_test_setup();
4637 if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4638 *g = None;
4639 }
4640 let r = match_parts("abcXYZ", "abcdef", 3, 0);
4641 assert_eq!(r, 1, "first 3 chars match exactly (test=1 → 1)");
4642 }
4643
4644 /// c:1251 — comp_match with pfx=w (exact equal) sets *exact=1.
4645 /// Empty sfx, qu=0 (no quoting needed), no Patprog.
4646 #[test]
4647 fn comp_match_exact_prefix_match() {
4648 let _g = crate::test_util::global_state_lock();
4649 let _g = zle_test_setup();
4650 if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4651 *g = None;
4652 }
4653 let mut clp: Option<Box<Cline>> = None;
4654 let mut exact = 99i32;
4655 let r = comp_match(
4656 "hello",
4657 "",
4658 "hello",
4659 None,
4660 Some(&mut clp),
4661 0,
4662 None,
4663 0,
4664 None,
4665 0,
4666 &mut exact,
4667 );
4668 assert!(r.is_some(), "literal prefix match succeeds");
4669 assert_eq!(exact, 1, "pfx == w → exact=1");
4670 }
4671
4672 /// The LIVE option-completion shape: `-M 'r:|[_-]=* r:|=*'` active on
4673 /// mstack (what _describe passes for options), typed `-`, candidate
4674 /// `-a`. C matches; a matcher-branch regression rejecting this kills
4675 /// every `cmd -<TAB>` shell-wide.
4676 #[test]
4677 fn comp_match_dash_prefix_with_option_matcher() {
4678 let _g = crate::test_util::global_state_lock();
4679 let _g = zle_test_setup();
4680 let m = crate::ported::zle::complete::parse_cmatcher("test", "r:|[_-]=* r:|=*");
4681 assert!(m.is_some(), "matcher spec must parse");
4682 if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4683 *g = Some(Box::new(crate::ported::zle::comp_h::Cmlist {
4684 next: None,
4685 matcher: m.unwrap(),
4686 str: "r:|[_-]=* r:|=*".to_string(),
4687 }));
4688 }
4689 let mut clp: Option<Box<Cline>> = None;
4690 let mut exact = 99i32;
4691 let r = comp_match("-", "", "-a", None, Some(&mut clp), 1, None, 0, None, 0, &mut exact);
4692 if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4693 *g = None;
4694 }
4695 assert!(r.is_some(), "'-' + option matcher must match '-a', got None");
4696 }
4697
4698 /// Case-insensitive matcher `m:{a}={A}`, typed `a`, candidate `Apple`.
4699 /// The matcher substitutes the first char; the word tail must be
4700 /// appended so the reconstruction is the full `Apple`.
4701 #[test]
4702 fn comp_match_ci_single_char_reconstructs_full_word() {
4703 let _g = crate::test_util::global_state_lock();
4704 let _g = zle_test_setup();
4705 let m = crate::ported::zle::complete::parse_cmatcher("test", "m:{a}={A}");
4706 assert!(m.is_some(), "matcher spec must parse");
4707 if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4708 *g = Some(Box::new(crate::ported::zle::comp_h::Cmlist {
4709 next: None,
4710 matcher: m.unwrap(),
4711 str: "m:{a}={A}".to_string(),
4712 }));
4713 }
4714 let mut clp: Option<Box<Cline>> = None;
4715 let mut exact = 99i32;
4716 let r = comp_match("a", "", "Apple", None, Some(&mut clp), 1, None, 0, None, 0, &mut exact);
4717 if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4718 *g = None;
4719 }
4720 assert_eq!(r.as_deref(), Some("Apple"), "'a' + m:{{a}}={{A}} must reconstruct 'Apple'");
4721 }
4722
4723 /// Case-insensitive matcher `m:{a-z}={A-Z}`, typed `app`, candidate
4724 /// `Apple`. The matcher swaps `a`->`A`; the exactly-matched `pp` and the
4725 /// tail `le` must survive in the reconstruction. Regression: the exact
4726 /// fast-path advanced the word cursor without emitting `pp`, so the
4727 /// result came out `Ale`.
4728 #[test]
4729 fn comp_match_ci_multi_char_keeps_exact_run() {
4730 let _g = crate::test_util::global_state_lock();
4731 let _g = zle_test_setup();
4732 let m = crate::ported::zle::complete::parse_cmatcher("test", "m:{a-z}={A-Z}");
4733 assert!(m.is_some(), "matcher spec must parse");
4734 if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4735 *g = Some(Box::new(crate::ported::zle::comp_h::Cmlist {
4736 next: None,
4737 matcher: m.unwrap(),
4738 str: "m:{a-z}={A-Z}".to_string(),
4739 }));
4740 }
4741 let mut clp: Option<Box<Cline>> = None;
4742 let mut exact = 99i32;
4743 let r = comp_match("app", "", "Apple", None, Some(&mut clp), 1, None, 0, None, 0, &mut exact);
4744 if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4745 *g = None;
4746 }
4747 assert_eq!(r.as_deref(), Some("Apple"), "'app' + m:{{a-z}}={{A-Z}} must reconstruct 'Apple', not drop the exact 'pp'");
4748 }
4749
4750 /// The option-completion shape: typed word `-`, candidate `-a` —
4751 /// must prefix-match (this is every `cmd -<TAB>` in compsys).
4752 #[test]
4753 fn comp_match_dash_prefix_matches_option_word() {
4754 let _g = crate::test_util::global_state_lock();
4755 let _g = zle_test_setup();
4756 if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4757 *g = None;
4758 }
4759 let mut clp: Option<Box<Cline>> = None;
4760 let mut exact = 99i32;
4761 let r = comp_match("-", "", "-a", None, Some(&mut clp), 1, None, 0, None, 0, &mut exact);
4762 assert!(r.is_some(), "'-' must prefix-match '-a', got None");
4763 assert_eq!(r.as_deref(), Some("-a"));
4764 }
4765
4766 /// c:546-1080 — match_str with diverging prefix returns -1 when
4767 /// mstack is empty (no matcher to bridge the gap).
4768 #[test]
4769 fn match_str_diverging_returns_neg_one_with_empty_mstack() {
4770 let _g = crate::test_util::global_state_lock();
4771 let _g = zle_test_setup();
4772 // Clear mstack to guarantee the empty-stack code path.
4773 if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4774 *g = None;
4775 }
4776 let r = match_str("abc", "xyz", None, 0, None, 0, 0, 0);
4777 assert_eq!(r, -1, "no matcher can bridge `a` vs `x`");
4778 }
4779
4780 // ---------- update_bmatchers real-port tests (this session). ----------
4781
4782 /// c:121-139 — `update_bmatchers` walks bmatchers; entries whose
4783 /// matcher isn't in mstack (via cmatchers_same) get trimmed via the
4784 /// `bmatchers = p->next` reset. With mstack empty, every entry
4785 /// misses → bmatchers should end up None.
4786 #[test]
4787 fn update_bmatchers_with_empty_mstack_trims_all_entries() {
4788 let _g = crate::test_util::global_state_lock();
4789 let _g = zle_test_setup();
4790 // Seed bmatchers with one entry.
4791 let matcher = Cmatcher {
4792 refc: 1,
4793 next: None,
4794 flags: 0,
4795 line: None,
4796 llen: 1,
4797 word: None,
4798 wlen: 1,
4799 left: None,
4800 lalen: 0,
4801 right: None,
4802 ralen: 0,
4803 };
4804 let bm_cell = crate::ported::zle::compcore::bmatchers.get_or_init(|| Mutex::new(None));
4805 *bm_cell.lock().unwrap() = Some(Box::new(Cmlist {
4806 next: None,
4807 matcher: Box::new(matcher),
4808 str: String::new(),
4809 }));
4810 // Clear mstack so the entry must be trimmed.
4811 let ms_cell = mstack.get_or_init(|| Mutex::new(None));
4812 *ms_cell.lock().unwrap() = None;
4813
4814 update_bmatchers();
4815
4816 // After update with empty mstack: bmatchers is None — c:135-137.
4817 assert!(
4818 bm_cell.lock().unwrap().is_none(),
4819 "every bmatcher must be trimmed when mstack is empty"
4820 );
4821 }
4822
4823 /// c:84 — `cmatchers_same` short-circuits to true on POINTER
4824 /// IDENTITY (a == b). The Rust port uses `std::ptr::eq`. Without
4825 /// this, two large equivalent matchers would scan every field.
4826 /// Regression dropping the short-circuit would balloon the
4827 /// `update_bmatchers`-triggered O(N*M) scan into O(N*M*F).
4828 #[test]
4829 fn cmatchers_same_pointer_identity_short_circuits() {
4830 let _g = crate::test_util::global_state_lock();
4831 let m = Cmatcher {
4832 refc: 1,
4833 next: None,
4834 flags: 0,
4835 line: None,
4836 llen: 0,
4837 word: None,
4838 wlen: 0,
4839 left: None,
4840 lalen: 0,
4841 right: None,
4842 ralen: 0,
4843 };
4844 // Same pointer → equal.
4845 assert!(cmatchers_same(&m, &m));
4846 }
4847
4848 /// c:87 — different `flags` bits MUST cause inequality. Catches
4849 /// a regression where the flag check is dropped — would let
4850 /// CMF_LEFT and CMF_RIGHT matchers compare equal silently.
4851 #[test]
4852 fn cmatchers_same_different_flags_compare_unequal() {
4853 let _g = crate::test_util::global_state_lock();
4854 let a = Cmatcher {
4855 refc: 1,
4856 next: None,
4857 flags: 0,
4858 line: None,
4859 llen: 0,
4860 word: None,
4861 wlen: 0,
4862 left: None,
4863 lalen: 0,
4864 right: None,
4865 ralen: 0,
4866 };
4867 let b = Cmatcher {
4868 refc: 1,
4869 next: None,
4870 flags: CMF_LEFT,
4871 line: None,
4872 llen: 0,
4873 word: None,
4874 wlen: 0,
4875 left: None,
4876 lalen: 0,
4877 right: None,
4878 ralen: 0,
4879 };
4880 assert!(!cmatchers_same(&a, &b));
4881 }
4882
4883 /// c:87 — different `llen`/`wlen` MUST cause inequality. The
4884 /// length fields are part of the natural-key comparison; a
4885 /// regression dropping them would conflate distinct matchers.
4886 #[test]
4887 fn cmatchers_same_different_lengths_compare_unequal() {
4888 let _g = crate::test_util::global_state_lock();
4889 let a = Cmatcher {
4890 refc: 1,
4891 next: None,
4892 flags: 0,
4893 line: None,
4894 llen: 1,
4895 word: None,
4896 wlen: 1,
4897 left: None,
4898 lalen: 0,
4899 right: None,
4900 ralen: 0,
4901 };
4902 let b = Cmatcher {
4903 refc: 1,
4904 next: None,
4905 flags: 0,
4906 line: None,
4907 llen: 2,
4908 word: None,
4909 wlen: 1,
4910 left: None,
4911 lalen: 0,
4912 right: None,
4913 ralen: 0,
4914 };
4915 assert!(!cmatchers_same(&a, &b), "differing llen must NOT be equal");
4916 let c = Cmatcher {
4917 refc: 1,
4918 next: None,
4919 flags: 0,
4920 line: None,
4921 llen: 1,
4922 word: None,
4923 wlen: 5,
4924 left: None,
4925 lalen: 0,
4926 right: None,
4927 ralen: 0,
4928 };
4929 assert!(!cmatchers_same(&a, &c), "differing wlen must NOT be equal");
4930 }
4931
4932 // ═══════════════════════════════════════════════════════════════════
4933 // C-parity tests for cpatterns_same NULL/chain edge cases.
4934 // ═══════════════════════════════════════════════════════════════════
4935
4936 /// c:46-77 — `cpatterns_same(None, None)` returns true (both NULL
4937 /// is trivially equal — while-loop exits immediately + `!b` is true).
4938 #[test]
4939 fn cpatterns_same_both_none_returns_true() {
4940 let _g = crate::test_util::global_state_lock();
4941 let _g2 = zle_test_setup();
4942 assert!(cpatterns_same(None, None), "both None → trivially equal");
4943 }
4944
4945 /// c:48 — `cpatterns_same(Some, None)` returns false (a non-NULL,
4946 /// b NULL during walk → `if(!b) return 0`).
4947 #[test]
4948 fn cpatterns_same_a_some_b_none_returns_false() {
4949 let _g = crate::test_util::global_state_lock();
4950 let _g2 = zle_test_setup();
4951 let a = cpat_char('x' as u32);
4952 assert!(!cpatterns_same(Some(&a), None));
4953 }
4954
4955 /// c:77 — `cpatterns_same(None, Some)` returns false (loop doesn't
4956 /// run, returns `!b` = false).
4957 #[test]
4958 fn cpatterns_same_a_none_b_some_returns_false() {
4959 let _g = crate::test_util::global_state_lock();
4960 let _g2 = zle_test_setup();
4961 let b = cpat_char('x' as u32);
4962 assert!(!cpatterns_same(None, Some(&b)));
4963 }
4964
4965 /// c:60-61 — CCLASS pattern: same str → equal; differing str → not.
4966 #[test]
4967 fn cpatterns_same_class_str_differs_not_equal() {
4968 let _g = crate::test_util::global_state_lock();
4969 let _g2 = zle_test_setup();
4970 let a = cpat_class("abc");
4971 let b = cpat_class("xyz");
4972 assert!(!cpatterns_same(Some(&a), Some(&b)));
4973 }
4974
4975 /// c:60-61 — NCLASS pattern (negated class) also uses str compare.
4976 #[test]
4977 fn cpatterns_same_nclass_str_compare() {
4978 let _g = crate::test_util::global_state_lock();
4979 let _g2 = zle_test_setup();
4980 let a = Cpattern {
4981 tp: CPAT_NCLASS,
4982 str: Some(b"abc".to_vec()),
4983 ..Default::default()
4984 };
4985 let b = Cpattern {
4986 tp: CPAT_NCLASS,
4987 str: Some(b"abc".to_vec()),
4988 ..Default::default()
4989 };
4990 assert!(
4991 cpatterns_same(Some(&a), Some(&b)),
4992 "same NCLASS str → equal"
4993 );
4994 }
4995
4996 /// c:52-54 — EQUIV uses str compare as well (same dispatch as
4997 /// CCLASS/NCLASS per the `x if x == CPAT_*` arm).
4998 #[test]
4999 fn cpatterns_same_equiv_str_compare() {
5000 let _g = crate::test_util::global_state_lock();
5001 let _g2 = zle_test_setup();
5002 let a = Cpattern {
5003 tp: CPAT_EQUIV,
5004 str: Some(b"AaBb".to_vec()),
5005 ..Default::default()
5006 };
5007 let b = Cpattern {
5008 tp: CPAT_EQUIV,
5009 str: Some(b"AaBb".to_vec()),
5010 ..Default::default()
5011 };
5012 assert!(cpatterns_same(Some(&a), Some(&b)));
5013 let c = Cpattern {
5014 tp: CPAT_EQUIV,
5015 str: Some(b"XxYy".to_vec()),
5016 ..Default::default()
5017 };
5018 assert!(!cpatterns_same(Some(&a), Some(&c)));
5019 }
5020
5021 /// c:74-75 — chain walk: different chain lengths → not equal.
5022 /// `a` is 2-node chain, `b` is 1-node — second iter, b=None → false.
5023 #[test]
5024 fn cpatterns_same_different_chain_length_not_equal() {
5025 let _g = crate::test_util::global_state_lock();
5026 let _g2 = zle_test_setup();
5027 let a = Cpattern {
5028 tp: CPAT_CHAR,
5029 chr: 'a' as u32,
5030 next: Some(Box::new(cpat_char('b' as u32))),
5031 ..Default::default()
5032 };
5033 let b = cpat_char('a' as u32);
5034 assert!(
5035 !cpatterns_same(Some(&a), Some(&b)),
5036 "a=2-chain, b=1 → not equal"
5037 );
5038 assert!(
5039 !cpatterns_same(Some(&b), Some(&a)),
5040 "b=1, a=2-chain → not equal"
5041 );
5042 }
5043
5044 /// c:46-77 — chain walk: same multi-node chain returns true.
5045 #[test]
5046 fn cpatterns_same_matching_chain_returns_true() {
5047 let _g = crate::test_util::global_state_lock();
5048 let _g2 = zle_test_setup();
5049 let a = Cpattern {
5050 tp: CPAT_CHAR,
5051 chr: 'a' as u32,
5052 next: Some(Box::new(cpat_char('b' as u32))),
5053 ..Default::default()
5054 };
5055 let b = Cpattern {
5056 tp: CPAT_CHAR,
5057 chr: 'a' as u32,
5058 next: Some(Box::new(cpat_char('b' as u32))),
5059 ..Default::default()
5060 };
5061 assert!(cpatterns_same(Some(&a), Some(&b)));
5062 }
5063
5064 /// c:86 — `cmatchers_same` with pointer-identity (same pointer)
5065 /// returns true (short-circuit before any field comparison).
5066 #[test]
5067 fn cmatchers_same_pointer_identity_true() {
5068 let _g = crate::test_util::global_state_lock();
5069 let _g2 = zle_test_setup();
5070 let m = Cmatcher {
5071 refc: 1,
5072 next: None,
5073 flags: 0xFFFF, // any garbage
5074 line: None,
5075 llen: 999,
5076 word: None,
5077 wlen: -42,
5078 left: None,
5079 lalen: 7,
5080 right: None,
5081 ralen: 11,
5082 };
5083 assert!(
5084 cmatchers_same(&m, &m),
5085 "same pointer → true regardless of fields"
5086 );
5087 }
5088
5089 /// c:91 — anchor checks ONLY run when CMF_LEFT or CMF_RIGHT is
5090 /// set; bare flags=0 ignores lalen/ralen mismatch.
5091 #[test]
5092 fn cmatchers_same_no_anchor_flags_ignores_anchor_lens() {
5093 let _g = crate::test_util::global_state_lock();
5094 let _g2 = zle_test_setup();
5095 let a = Cmatcher {
5096 refc: 1,
5097 next: None,
5098 flags: 0,
5099 line: None,
5100 llen: 0,
5101 word: None,
5102 wlen: 0,
5103 left: None,
5104 lalen: 5, // mismatch
5105 right: None,
5106 ralen: 7, // mismatch
5107 };
5108 let b = Cmatcher {
5109 refc: 1,
5110 next: None,
5111 flags: 0,
5112 line: None,
5113 llen: 0,
5114 word: None,
5115 wlen: 0,
5116 left: None,
5117 lalen: 99, // mismatch
5118 right: None,
5119 ralen: 0, // mismatch
5120 };
5121 // Without CMF_LEFT/CMF_RIGHT, anchor lens shouldn't matter.
5122 assert!(
5123 cmatchers_same(&a, &b),
5124 "flags=0 must ignore lalen/ralen per c:91 gate"
5125 );
5126 }
5127
5128 // ═══════════════════════════════════════════════════════════════════
5129 // Additional C-parity tests for Src/Zle/compmatch.c
5130 // c:79 cpatterns_same / c:143 cmatchers_same / c:188 add_bmatchers
5131 // c:220 update_bmatchers / c:311 free_cline / c:562 start_match /
5132 // c:591 abort_match / c:1605 match_parts
5133 // ═══════════════════════════════════════════════════════════════════
5134
5135 /// c:79 — `cpatterns_same(None, None)` returns true (reflexive None).
5136 #[test]
5137 fn cpatterns_same_double_none_reflexive() {
5138 assert!(cpatterns_same(None, None));
5139 }
5140
5141 /// c:143 — `cmatchers_same(&a, &a)` always true (reflexive).
5142 #[test]
5143 fn cmatchers_same_self_reflexive() {
5144 let m = Cmatcher {
5145 refc: 1,
5146 next: None,
5147 flags: 0,
5148 line: None,
5149 llen: 0,
5150 word: None,
5151 wlen: 0,
5152 left: None,
5153 lalen: 0,
5154 right: None,
5155 ralen: 0,
5156 };
5157 assert!(cmatchers_same(&m, &m), "cmatchers_same(a, a) must be true");
5158 }
5159
5160 /// c:188 — `add_bmatchers(None)` is a safe no-op (idempotent).
5161 #[test]
5162 fn add_bmatchers_none_no_panic() {
5163 add_bmatchers(None);
5164 add_bmatchers(None);
5165 }
5166
5167 /// c:311 — `free_cline(None)` is a safe no-op.
5168 #[test]
5169 fn free_cline_none_no_panic() {
5170 free_cline(None);
5171 }
5172
5173 /// c:1605 — `match_parts` returns i32 (compile-time type pin).
5174 #[test]
5175 fn match_parts_returns_i32_type() {
5176 let _: i32 = match_parts("", "", 0, 0);
5177 }
5178
5179 /// c:1605 — `match_parts("", "", 0, 0)` returns 0/1 boolean.
5180 #[test]
5181 fn match_parts_empty_inputs_boolean_result() {
5182 let r = match_parts("", "", 0, 0);
5183 assert!(
5184 r == 0 || r == 1,
5185 "match_parts must return 0 or 1, got {}",
5186 r
5187 );
5188 }
5189
5190 /// c:1605 — `match_parts` deterministic for same input.
5191 #[test]
5192 fn match_parts_is_deterministic() {
5193 for (l, w) in [("a", "a"), ("abc", "abc"), ("", "")] {
5194 let first = match_parts(l, w, 0, 0);
5195 for _ in 0..3 {
5196 assert_eq!(
5197 match_parts(l, w, 0, 0),
5198 first,
5199 "match_parts({:?}, {:?}) must be deterministic",
5200 l,
5201 w
5202 );
5203 }
5204 }
5205 }
5206
5207 /// c:562 + c:591 — `start_match` then `abort_match` round-trip safe.
5208 #[test]
5209 fn start_then_abort_match_round_trip_safe() {
5210 start_match();
5211 abort_match();
5212 }
5213
5214 /// c:220 — `update_bmatchers` idempotent / no-panic.
5215 #[test]
5216 fn update_bmatchers_idempotent() {
5217 for _ in 0..5 {
5218 update_bmatchers();
5219 }
5220 }
5221
5222 /// c:413 — `cline_sublen` returns i32 (type pin).
5223 #[test]
5224 fn cline_sublen_returns_i32_type() {
5225 let cline = Cline {
5226 next: None,
5227 prefix: None,
5228 suffix: None,
5229 line: None,
5230 llen: 0,
5231 word: None,
5232 wlen: 0,
5233 orig: None,
5234 olen: 0,
5235 slen: 0,
5236 min: 0,
5237 max: 0,
5238 flags: 0,
5239 };
5240 let _: i32 = cline_sublen(&cline);
5241 }
5242
5243 // ═══════════════════════════════════════════════════════════════════
5244 // Additional C-parity tests for Src/Zle/compmatch.c
5245 // c:188 add_bmatchers / c:220 update_bmatchers / c:311 free_cline /
5246 // c:413 cline_sublen / c:562 start_match / c:591 abort_match /
5247 // c:1605 match_parts
5248 // ═══════════════════════════════════════════════════════════════════
5249
5250 /// c:188 — `add_bmatchers(None)` is safe / idempotent.
5251 #[test]
5252 fn add_bmatchers_none_idempotent() {
5253 for _ in 0..5 {
5254 add_bmatchers(None);
5255 }
5256 }
5257
5258 /// c:311 — `free_cline(None)` is safe (early-return on empty).
5259 #[test]
5260 fn free_cline_none_no_panic_pin2() {
5261 free_cline(None);
5262 }
5263
5264 /// c:413 — `cline_sublen` is pure (multiple calls = same result).
5265 #[test]
5266 fn cline_sublen_is_pure() {
5267 let cline = Cline {
5268 next: None,
5269 prefix: None,
5270 suffix: None,
5271 line: None,
5272 llen: 0,
5273 word: None,
5274 wlen: 0,
5275 orig: None,
5276 olen: 0,
5277 slen: 0,
5278 min: 0,
5279 max: 0,
5280 flags: 0,
5281 };
5282 let first = cline_sublen(&cline);
5283 for _ in 0..3 {
5284 assert_eq!(cline_sublen(&cline), first, "cline_sublen must be pure");
5285 }
5286 }
5287
5288 /// c:562 — `start_match` is idempotent / safe to call repeatedly.
5289 #[test]
5290 fn start_match_idempotent_safe() {
5291 let _g = crate::test_util::global_state_lock();
5292 for _ in 0..5 {
5293 start_match();
5294 }
5295 }
5296
5297 /// c:591 — `abort_match` is idempotent / safe to call repeatedly.
5298 #[test]
5299 fn abort_match_idempotent_safe() {
5300 let _g = crate::test_util::global_state_lock();
5301 for _ in 0..5 {
5302 abort_match();
5303 }
5304 }
5305
5306 /// c:562 + c:591 — `start_match` then `abort_match` round-trips
5307 /// without panic.
5308 #[test]
5309 fn start_match_then_abort_match_round_trip() {
5310 let _g = crate::test_util::global_state_lock();
5311 for _ in 0..3 {
5312 start_match();
5313 abort_match();
5314 }
5315 }
5316
5317 /// c:1605 — `match_parts("","",0,0)` returns boolean i32 (0 or 1).
5318 #[test]
5319 fn match_parts_empty_zero_args_returns_boolean() {
5320 let r = match_parts("", "", 0, 0);
5321 assert!(r == 0 || r == 1, "result must be 0 or 1, got {}", r);
5322 }
5323
5324 /// c:1605 — `match_parts` is pure for stable input.
5325 #[test]
5326 fn match_parts_is_pure_full_sweep() {
5327 for (l, w, n, p) in [
5328 ("", "", 0, 0),
5329 ("a", "a", 0, 0),
5330 ("abc", "abc", 1, 0),
5331 ("x", "y", 0, 1),
5332 ] {
5333 let first = match_parts(l, w, n, p);
5334 for _ in 0..3 {
5335 assert_eq!(
5336 match_parts(l, w, n, p),
5337 first,
5338 "match_parts({:?},{:?},{},{}) must be pure",
5339 l,
5340 w,
5341 n,
5342 p
5343 );
5344 }
5345 }
5346 }
5347
5348 /// c:79 — `cpatterns_same(None, None)` reflexive (true).
5349 #[test]
5350 fn cpatterns_same_double_none_true() {
5351 assert!(
5352 cpatterns_same(None, None),
5353 "double None is reflexive identity"
5354 );
5355 }
5356
5357 /// c:220 — `update_bmatchers` followed by `add_bmatchers(None)` safe.
5358 #[test]
5359 fn update_then_add_bmatchers_safe() {
5360 update_bmatchers();
5361 add_bmatchers(None);
5362 update_bmatchers();
5363 }
5364
5365 /// c:188 + c:220 — interleaved add/update is safe for many iters.
5366 #[test]
5367 fn interleaved_add_update_bmatchers_safe() {
5368 for _ in 0..10 {
5369 add_bmatchers(None);
5370 update_bmatchers();
5371 }
5372 }
5373}