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