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