zsh/ported/hist.rs
1//! hist.c - history mechanism
2//!
3//! Port of Src/hist.c
4//!
5//! The history lines are kept in a hash, and also doubly-linked in a ring. // c:98
6
7use crate::ported::glob::{getmatch, remnulargs};
8use crate::ported::hashtable::addhistnode;
9use crate::ported::input::{ingetc, inputsetline, inungetc};
10use crate::ported::lex::{
11 lexinit, parse_subst_string, untokenize, LEX_ISFIRSTCH, LEX_LEXSTOP,
12};
13use crate::ported::options::dosetopt;
14use crate::ported::parse::init_parse_status;
15use crate::ported::signals::unqueue_signals;
16use crate::ported::subst::{equalsubstr, singsub};
17use crate::ported::utils::{errflag, zerr, zmonotime, zsleep_random, ERRFLAG_ERROR};
18use crate::ported::zle::compcore::ZLEMETACS;
19use crate::ported::zsh_h::{
20 hashnode, hist_stack, histent, isset, Pound, BANGHIST, CASMOD_CAPS, CASMOD_LOWER, CASMOD_NONE,
21 CASMOD_UPPER, CSHJUNKIEHISTORY, ERRFLAG_INT, HFILE_FAST, HFILE_USE_OPTIONS,
22 HISTEXPIREDUPSFIRST, HISTFLAG_DONE, HISTFLAG_NOEXEC, HISTFLAG_RECALL, HISTFLAG_SETTY,
23 HISTIGNOREALLDUPS, HISTIGNOREDUPS, HISTIGNORESPACE, HISTNOFUNCTIONS, HISTNOSTORE,
24 HISTREDUCEBLANKS, HISTSUBSTPATTERN, HISTVERIFY, HIST_DUP, HIST_FOREIGN, HIST_NOWRITE, HIST_OLD,
25 HIST_TMPSTORE, INCAPPENDHISTORY, INCAPPENDHISTORYTIME, INP_ALIAS, INP_HIST, INTERACTIVE,
26 SHAREHISTORY, SHINSTDIN, SUB_END, SUB_GLOBAL, SUB_LONG, SUB_REST, SUB_RETFAIL, SUB_START,
27 SUB_SUBSTR,
28};
29use crate::ported::zsh_system_h::IS_DIRSEP;
30use crate::ported::ztype_h::itok;
31use crate::signals::queue_signals;
32use crate::{DPUTS, DPUTS1};
33use std::io::Write;
34use std::os::unix::io::AsRawFd;
35use std::path::Component::*;
36use std::sync::atomic::Ordering::SeqCst;
37use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, AtomicU32, AtomicUsize, Ordering};
38use std::sync::Mutex;
39
40// NOTE: `inbufflags` and `inbufct` are NOT imported because the
41// hist.rs body uses `let inbufflags = ...` shadowing — Rust treats
42// imported names as constant-patterns in `let` LHS, breaking that
43// idiom. They stay as `crate::ported::input::inbufflags` until
44// the shadowing pattern is refactored.
45
46// Bits of histactive variable // c:137
47/// Port of `HA_ACTIVE` from Src/hist.c:138. History mechanism is active.
48pub const HA_ACTIVE: u32 = 1 << 0; // c:138
49/// Port of `HA_NOINC` from Src/hist.c:139. Don't store, curhist not incremented.
50pub const HA_NOINC: u32 = 1 << 1; // c:139
51/// Port of `HA_INWORD` from Src/hist.c:140. We're inside a word.
52pub const HA_INWORD: u32 = 1 << 2; // c:140
53/// Port of `HA_UNGET` from Src/hist.c:142. Recursively ungetting.
54pub const HA_UNGET: u32 = 1 << 3; // c:142
55
56/// Port of `static zlong defev` from Src/hist.c:210.
57static defev: AtomicI64 = AtomicI64::new(0); // c:210
58
59/// Port of `static int hist_keep_comment` from Src/hist.c:217.
60static hist_keep_comment: AtomicI32 = AtomicI32::new(0); // c:217
61
62/// Port of `static int histsave_stack_size` from Src/hist.c:239.
63static histsave_stack_size: AtomicI32 = AtomicI32::new(0); // c:239
64
65/// Port of `static int histsave_stack_pos` from Src/hist.c:240.
66static histsave_stack_pos: AtomicI32 = AtomicI32::new(0); // c:240
67
68/// Port of `static zlong histfile_linect` from Src/hist.c:242.
69static histfile_linect: AtomicI64 = AtomicI64::new(0); // c:242
70
71// =========================================================================
72// Functions from hist.c
73// =========================================================================
74
75/// Port of `hist_context_save()` from `Src/hist.c:248`. — C decl `hist_context_save(struct hist_stack *hs, int toplevel)`.
76pub fn hist_context_save(hs: &mut hist_stack, toplevel: i32) {
77 // c:248
78 // !!! WARNING: RUST-ONLY GUARD — NO C COUNTERPART !!!
79 // The history line state is process-global. A worker-pool parse
80 // (zcontext_save in parse_isolated_at) must neither snapshot nor clear
81 // the main thread's live line; hbegin/hend leave it alone there too.
82 if crate::worker::in_worker_thread() {
83 return;
84 }
85 if toplevel != 0 {
86 // c:248
87 // top level, make this version visible to ZLE // c:251
88 *zle_chline.lock().unwrap() = Some(chline.lock().unwrap().clone()); // c:252
89 // ensure line stored is NULL-terminated — implicit in String // c:253-255
90 }
91 hs.histactive = histactive.load(SeqCst) as i32; // c:257
92 hs.histdone = histdone.load(SeqCst); // c:258
93 hs.stophist = stophist.load(SeqCst); // c:259
94 hs.hline = Some(chline.lock().unwrap().clone()); // c:260
95 hs.hptr = Some(hptr.load(SeqCst).to_string()); // c:261
96 hs.chwords = chwords.lock().unwrap().clone(); // c:262
97 hs.chwordlen = chwordlen.load(SeqCst); // c:263
98 hs.chwordpos = chwordpos.load(SeqCst); // c:264
99 // hs->hgetc / hungetc / hwaddc / hwbegin / hwabort / hwend / addtoline // c:265-271
100 // are runtime-mutable function-pointer globals in C; the Rust port
101 // dispatches statically via crate::ported::input.
102 hs.hlinesz = hlinesz.load(SeqCst); // c:272
103 hs.defev = defev.load(SeqCst); // c:273
104 hs.hist_keep_comment = hist_keep_comment.load(SeqCst); // c:274
105 // hs->cstack = cmdstack; hs->csp = cmdsp; // c:296-282
106 hs.csp = 0;
107
108 stophist.store(0, SeqCst); // c:296
109 chline.lock().unwrap().clear(); // c:296
110 hptr.store(0, SeqCst); // c:296
111 histactive.store(0, SeqCst); // c:296
112 // cmdstack = zalloc(CMDSTACKSZ); cmdsp = 0; // c:296-289
113}
114
115/// Port of `hist_context_restore()` from `Src/hist.c:296`. — C decl `hist_context_restore(const struct hist_stack *hs, int toplevel)`.
116pub fn hist_context_restore(hs: &hist_stack, toplevel: i32) {
117 // c:296
118 // !!! WARNING: RUST-ONLY GUARD — NO C COUNTERPART !!!
119 // Paired with hist_context_save: a pool thread saved nothing, and writing
120 // its snapshot back would overwrite the main thread's live histactive,
121 // chline and stophist.
122 if crate::worker::in_worker_thread() {
123 return;
124 }
125 if toplevel != 0 {
126 // c:296
127 // c:299 — Back to top level: don't need special ZLE value
128 // c:300 — DPUTS(hs->hline != zle_chline, "BUG: Ouch, wrong chline for ZLE")
129 DPUTS!(
130 // c:300
131 hs.hline != *zle_chline.lock().unwrap(), // c:300
132 "BUG: Ouch, wrong chline for ZLE" // c:300
133 );
134 *zle_chline.lock().unwrap() = None; // c:301
135 }
136 histactive.store(hs.histactive as u32, SeqCst); // c:303
137 histdone.store(hs.histdone, SeqCst); // c:304
138 stophist.store(hs.stophist, SeqCst); // c:305
139 *chline.lock().unwrap() = hs.hline.clone().unwrap_or_default(); // c:306
140 hptr.store(
141 hs.hptr.as_ref().and_then(|s| s.parse().ok()).unwrap_or(0), // c:307
142 SeqCst,
143 );
144 *chwords.lock().unwrap() = hs.chwords.clone(); // c:308
145 chwordlen.store(hs.chwordlen, SeqCst); // c:309
146 chwordpos.store(hs.chwordpos, SeqCst); // c:310
147 // hgetc / hungetc / hwaddc / hwbegin / hwabort / hwend / addtoline // c:311-317
148 hlinesz.store(hs.hlinesz, SeqCst); // c:318
149 defev.store(hs.defev, SeqCst); // c:339
150 hist_keep_comment.store(hs.hist_keep_comment, SeqCst); // c:339
151 // cmdstack = hs->cstack; cmdsp = hs->csp; // c:339-324
152}
153
154/// Port of `hist_in_word()` from `Src/hist.c:339`. — C decl `hist_in_word(int yesno)`.
155pub fn hist_in_word(yesno: i32) {
156 if yesno != 0 {
157 histactive.fetch_or(HA_INWORD, SeqCst);
158 } else {
159 histactive.fetch_and(!HA_INWORD, SeqCst);
160 }
161}
162
163/// Port of `hist_is_in_word()` from `Src/hist.c:349`. — C decl `hist_is_in_word(void)`.
164pub fn hist_is_in_word() -> i32 {
165 if (histactive.load(SeqCst) & HA_INWORD) != 0 {
166 1
167 } else {
168 0
169 }
170}
171
172/// Port of `ihwaddc()` from `Src/hist.c:357`. — C decl `ihwaddc(int c)`.
173///
174/// C body:
175/// ```c
176/// if (chline && !(errflag || lexstop) &&
177/// (inbufflags & (INP_ALIAS|INP_HIST)) != INP_ALIAS) {
178/// if (c == bangchar && stophist < 2 && qbang)
179/// hwaddc('\\');
180/// *hptr++ = c;
181/// if (hptr - chline >= hlinesz) {
182/// int oldsiz = hlinesz;
183/// chline = realloc(chline, hlinesz = oldsiz + 64);
184/// hptr = chline + oldsiz;
185/// }
186/// }
187/// ```
188pub fn ihwaddc(c: i32) {
189 // c:357
190 // Rust-only: pool threads run C's `hwaddc = nohw` arm (c:1141).
191 if crate::worker::in_worker_thread() {
192 return;
193 }
194 // c:360-361 — guard: history line must exist, no error/lex stop,
195 // and we're not strictly inside alias-expansion-only input.
196 if errflag.load(SeqCst) != 0 || lexstop.load(SeqCst) {
197 return;
198 }
199 let inbufflags = crate::ported::input::inbufflags.with(|f| f.get());
200 if (inbufflags & (INP_ALIAS | INP_HIST)) == INP_ALIAS {
201 return;
202 }
203 // C guard: `if (chline && ...)` — chline must be ALLOCATED (non-NULL),
204 // which is the active state hbegin() sets (`hlinesz = 64`), NOT "the
205 // buffer has bytes yet". The prior `chline.is_empty()` proxy was a
206 // chicken-and-egg: an active-but-empty chline (right after hbegin)
207 // rejected the FIRST char, so the line never started building and
208 // interactive history recorded nothing. Use the real allocated flag.
209 if hlinesz.load(SeqCst) == 0 {
210 return; // c:360 — inactive history (chline == NULL)
211 }
212 // c:362-368 — `*hptr++ = c;`. C writes the byte at the hptr
213 // cursor then advances; the qbang escape arm also writes via
214 // the recursive `hwaddc('\\');` call.
215 //
216 // The previous Rust port called `chline.push(c)` which only
217 // APPENDS — after `hwrep` rewinds hptr to mid-buffer, new
218 // pushes would land at chline.end instead of overwriting the
219 // word being replaced. Mirror C exactly: write each byte at
220 // the hptr position (growing only when hptr == chline.len()),
221 // then advance hptr.
222 let bc = bangchar.load(SeqCst);
223 let qbang_active = c == bc && stophist.load(SeqCst) < 2 && qbang.load(SeqCst);
224 {
225 let mut buf = chline.lock().expect("chline poisoned");
226 let bytes = unsafe { buf.as_mut_vec() };
227 let mut pos = hptr.load(SeqCst);
228 // Mirror the recursive `hwaddc('\\');` at c:366 — also writes
229 // at hptr + advances.
230 if qbang_active {
231 if pos < bytes.len() {
232 bytes[pos] = b'\\';
233 }
234 // c:366
235 else {
236 while bytes.len() < pos {
237 bytes.push(0);
238 }
239 bytes.push(b'\\');
240 }
241 pos += 1;
242 }
243 // c:368 — `*hptr++ = c;`. C's input stream is METAFIED BYTES so
244 // a single-byte store is right there; zshrs's ingetc yields full
245 // Unicode chars, so encode the char's UTF-8 bytes at the cursor.
246 // The previous `c as u8` TRUNCATED every non-ASCII char — ★
247 // (U+2605) stored as 0x05, é (U+00E9) as a lone invalid 0xE9 —
248 // corrupting the ring and $HISTFILE for any multibyte line.
249 let mut utf8 = [0u8; 4];
250 let enc: &[u8] = match char::from_u32(c as u32) {
251 Some(ch) => ch.encode_utf8(&mut utf8).as_bytes(),
252 None => {
253 utf8[0] = c as u8;
254 &utf8[..1]
255 }
256 };
257 for &b in enc {
258 if pos < bytes.len() {
259 bytes[pos] = b;
260 } else {
261 while bytes.len() < pos {
262 bytes.push(0);
263 }
264 bytes.push(b);
265 }
266 pos += 1;
267 }
268 hptr.store(pos, SeqCst);
269 }
270 // c:370-374 — resize tracking. Rust `String` grows on `push`
271 // automatically, but `hlinesz` mirrors C's allocation count
272 // for any caller that reads it (e.g. `hwend()`). C condition
273 // is `hptr - chline >= hlinesz`; mirror with the canonical
274 // `hptr` global (matches the c:1658 / c:1693 fixes).
275 let cur_off = hptr.load(SeqCst) as i32; // c:381 hptr - chline
276 let sz = hlinesz.load(SeqCst);
277 if cur_off >= sz {
278 // c:381
279 let new_sz = sz + 64;
280 hlinesz.store(new_sz, SeqCst); // c:384
281 }
282}
283
284/// Port of `iaddtoline()` from `Src/hist.c:397`. — C decl `iaddtoline(int c)`.
285///
286/// Adds a character to the ZLE input line while `doexpandhist` rewrites
287/// it, and tracks the new cursor position (`excs`) across the expansion.
288/// Called from `ihgetc` and from `gettok` for characters in comments.
289///
290/// ```c
291/// if (!expanding || lexstop) return;
292/// if (qbang && c == bangchar && stophist < 2) {
293/// exlast--;
294/// zleentry(ZLE_CMD_ADD_TO_LINE, '\\');
295/// }
296/// if (excs > zlemetacs) {
297/// excs += 1 + inbufct - exlast;
298/// if (excs < zlemetacs) excs = zlemetacs;
299/// }
300/// exlast = inbufct;
301/// zleentry(ZLE_CMD_ADD_TO_LINE, c);
302/// ```
303pub fn iaddtoline(c: i32) {
304 // c:397
305 // Rust-only: pool threads run C's `addtoline = nohw` arm (c:1145).
306 if crate::worker::in_worker_thread() {
307 return;
308 }
309 // c:399 — `if (!expanding || lexstop) return;`.
310 if expanding.load(SeqCst) == 0 || lexstop.load(SeqCst) {
311 return;
312 }
313 // c:401-404 — bang-escape under qbang.
314 let bc = bangchar.load(SeqCst);
315 if qbang.load(SeqCst) && c == bc && stophist.load(SeqCst) < 2 {
316 exlast.fetch_sub(1, SeqCst); // c:402
317 // c:403 — zleentry(ZLE_CMD_ADD_TO_LINE, '\\')
318 crate::ported::zle::zle_main::zle_main_entry(
319 crate::ported::zsh_h::ZLE_CMD_ADD_TO_LINE,
320 &mut crate::ported::zle::zle_main::zle_main_entry_args::AddToLine(b'\\' as i32),
321 );
322 }
323 // c:405-411 — `if (excs > zlemetacs) { excs += 1 + inbufct - exlast;
324 // if (excs < zlemetacs) excs = zlemetacs; }`.
325 //
326 // !!! UNIT NOTE !!! zshrs's `ingetc` yields whole chars and `inbufct`
327 // counts chars, while `excs`/`zlemetacs` are byte offsets into the
328 // metafied line. A char of `n` bytes is `n` byte-reads in C (each one
329 // contributing `1 + (-1) = 0`), so the `n - 1` extra bytes are added
330 // back to keep a plain read a net zero on `excs`.
331 let zlemetacs_v = ZLEMETACS.load(SeqCst);
332 let excs_v = excs.load(SeqCst);
333 if excs_v > zlemetacs_v {
334 // c:405
335 let inbufct_now = crate::ported::input::inbufct.with(|c| c.get());
336 let exlast_v = exlast.load(SeqCst);
337 let extra = char::from_u32(c as u32).map_or(0, |ch| ch.len_utf8() as i32 - 1);
338 let mut new_excs = excs_v + 1 + inbufct_now - exlast_v + extra; // c:406
339 if new_excs < zlemetacs_v {
340 // c:407 this case could be handled better but it is so rare
341 // that it does not worth it
342 new_excs = zlemetacs_v; // c:410
343 }
344 excs.store(new_excs, SeqCst);
345 }
346 // c:412 — `exlast = inbufct;`
347 let inbufct_v = crate::ported::input::inbufct.with(|cnt| cnt.get());
348 exlast.store(inbufct_v, SeqCst); // c:412
349 // c:413 — zleentry(ZLE_CMD_ADD_TO_LINE, c)
350 crate::ported::zle::zle_main::zle_main_entry(
351 crate::ported::zsh_h::ZLE_CMD_ADD_TO_LINE,
352 &mut crate::ported::zle::zle_main::zle_main_entry_args::AddToLine(c),
353 );
354}
355
356/// Port of `safeinungetc()` from `Src/hist.c:467`. — C decl `safeinungetc(int c)`.
357/// ```c
358/// static void
359/// safeinungetc(int c)
360/// {
361/// if (lexstop)
362/// lexstop = 0;
363/// else
364/// inungetc(c);
365/// }
366/// ```
367pub fn safeinungetc(c: i32) {
368 // c:467
369 if lexstop.load(SeqCst) {
370 // c:469
371 lexstop.store(false, SeqCst); // c:470
372 } else {
373 // c:471
374 if let Some(ch) = char::from_u32(c as u32) {
375 // c:472 inungetc(c)
376 inungetc(ch);
377 }
378 }
379}
380
381/// Port of `ihgetc()` from `Src/hist.c:418`. — C decl `ihgetc(void)`.
382/// Returns the next
383/// character from the input stream, optionally performing history
384/// expansion via `histsubchar`. Sets `lexstop`/`errflag` and bumps
385/// `qbang` per the bang-escape rules.
386/// ```c
387/// static int
388/// ihgetc(void)
389/// {
390/// int c = ingetc();
391/// if (exit_pending) { lexstop = 1; errflag |= ERRFLAG_ERROR; return ' '; }
392/// qbang = 0;
393/// if (!stophist && !(inbufflags & INP_ALIAS)) {
394/// c = histsubchar(c);
395/// if (c < 0) { lexstop = 1; errflag |= ERRFLAG_ERROR; return ' '; }
396/// }
397/// if ((inbufflags & INP_HIST) && !stophist) {
398/// qbang = 0;
399/// if (c == '\\' && !(qbang = (c = ingetc()) == bangchar))
400/// safeinungetc(c), c = '\\';
401/// } else if (stophist || (inbufflags & INP_ALIAS))
402/// qbang = c == bangchar && (stophist < 2);
403/// hwaddc(c);
404/// addtoline(c);
405/// return c;
406/// }
407/// ```
408pub fn ihgetc() -> i32 {
409 // c:418
410 // Rust-only: pool threads run C's `hgetc = ingetc` arm (c:1139).
411 if crate::worker::in_worker_thread() {
412 return ingetc().map(|ch| ch as i32).unwrap_or(b' ' as i32);
413 }
414 // c:420 — `int c = ingetc();`. C's ingetc returns the byte 32 (' ')
415 // WITH lexstop set at EOF (input.c:322). zshrs's ingetc signals EOF as
416 // `None`, so map None → ' ' (NOT -1): a negative `c` is reserved for
417 // the histsubchar bad-`!`-expansion path at c:432 (which sets errflag),
418 // and conflating EOF with it spuriously flags an error on a clean
419 // Ctrl-D / end-of-input.
420 let mut c: i32 = ingetc() // c:420 int c = ingetc();
421 .map(|ch| ch as i32)
422 .unwrap_or(b' ' as i32);
423 if exit_pending.load(SeqCst) {
424 // c:422
425 lexstop.store(true, SeqCst); // c:424
426 errflag.fetch_or(
427 // c:425 errflag |= ERRFLAG_ERROR
428 ERRFLAG_ERROR,
429 SeqCst,
430 );
431 return b' ' as i32; // c:426
432 }
433 qbang.store(false, SeqCst); // c:428 qbang = 0
434 let inbufflags_v = crate::ported::input::inbufflags.with(|f| f.get());
435 if stophist.load(SeqCst) == 0 // c:429 !stophist
436 && (inbufflags_v & INP_ALIAS) == 0
437 // c:429 !(inbufflags & INP_ALIAS)
438 {
439 c = histsubchar(c); // c:431 c = histsubchar(c)
440 if c < 0 {
441 // c:432
442 lexstop.store(true, SeqCst); // c:434
443 errflag.fetch_or(
444 // c:435
445 ERRFLAG_ERROR,
446 SeqCst,
447 );
448 return b' ' as i32; // c:436
449 }
450 }
451 let inbufflags_v = crate::ported::input::inbufflags.with(|f| f.get());
452 let bc = bangchar.load(SeqCst);
453 if (inbufflags_v & INP_HIST) != 0 && stophist.load(SeqCst) == 0 {
454 // c:439
455 // c:447 qbang = 0
456 qbang.store(false, SeqCst);
457 if c == b'\\' as i32 {
458 // c:448 c == '\\'
459 let g = ingetc() // c:448 c = ingetc()
460 .map(|ch| ch as i32)
461 .unwrap_or(-1);
462 if g == bc {
463 // c:448 qbang = (c == bangchar)
464 qbang.store(true, SeqCst);
465 c = g;
466 } else {
467 // c:449 safeinungetc(c), c = '\\';
468 safeinungetc(g);
469 c = b'\\' as i32;
470 }
471 }
472 } else if stophist.load(SeqCst) != 0 // c:450 stophist
473 || (inbufflags_v & INP_ALIAS) != 0
474 // c:450 (inbufflags & INP_ALIAS)
475 {
476 // c:458 qbang = c == bangchar && (stophist < 2)
477 let v = c == bc && stophist.load(SeqCst) < 2;
478 qbang.store(v, SeqCst);
479 }
480 ihwaddc(c); // c:459 hwaddc(c)
481 iaddtoline(c); // c:460 addtoline(c)
482 c // c:462 return c
483}
484
485/// Port of `unsigned char hatchar` from `Src/params.c:132`. Caret used
486/// as the substitution-shortcut lead character on first column; init'd
487/// to `'^'` at `Src/init.c:1102`. Read by `histsubchar` (c:618).
488/// NOTE on placement: canonical home per PORT.md Rule C would be
489/// `params.rs`, alongside `bangchar`/`hashchar`. Kept here to mirror
490/// the existing (pre-2026-05) placement of `bangchar` at hist.rs:1858;
491/// move-all-three is a follow-up cleanup.
492pub static hatchar: AtomicI32 = AtomicI32::new(b'^' as i32); // c:params.c:132
493
494/// Port of `unsigned char hashchar` from `Src/params.c:132`. Comment-
495/// start character; init'd to `'#'` at `Src/init.c:1101`. Read by
496/// `gettokstr` (`Src/lex.c:678`). Atomic so `histcharssetfn`
497/// (`Src/params.c:5097`) can update it dynamically when `$HISTCHARS`
498/// changes.
499pub static hashchar: AtomicI32 = AtomicI32::new(b'#' as i32); // c:params.c:132
500
501/// Port of `static int marg` from `Src/hist.c:599`. Argument index of the
502/// most-recent `!?str?` event match; `-1` when no match has happened.
503pub static marg: AtomicI32 = AtomicI32::new(-1); // c:599
504
505/// Port of `static zlong mev` from `Src/hist.c:600`. Event number of the
506/// most-recent `!?str?` event match; `-1` when no match has happened.
507pub static mev: AtomicI64 = AtomicI64::new(-1); // c:600
508
509/// Port of `histsubchar()` from `Src/hist.c:595`. — C decl `histsubchar(int c)`.
510/// Implements `^foo^bar` and full `!` history expansion: walks the input
511/// stream pulling event-spec, word-designator, and modifier characters,
512/// looks up the matching history entry, applies any modifiers, then
513/// pushes the resulting string back onto the input stack via `inpush`.
514/// Returns the first character of the expanded result or `-1` on error.
515///
516/// ```c
517/// static int
518/// histsubchar(int c)
519/// {
520/// int farg, evset = -1, larg, argc, cflag = 0, bflag = 0;
521/// zlong ev;
522/// static int marg = -1;
523/// static zlong mev = -1;
524/// char *buf, *ptr;
525/// char *sline;
526/// int lexraw_mark;
527/// Histent ehist;
528/// size_t buflen;
529/// /* ... see Src/hist.c:594-983 for full body ... */
530/// }
531/// ```
532pub fn histsubchar(c_in: i32) -> i32 {
533 // c:595
534 let mut c: i32 = c_in;
535 let mut farg: i32; // c:597
536 let mut evset: i32 = -1; // c:597
537 let mut larg: i32;
538 let mut argc: i32; // c:597
539 let mut cflag: i32 = 0; // c:597
540 let mut bflag: i32 = 0; // c:597
541 let mut ev: i64; // c:598
542 let mut buf: String; // c:601 char *buf, *ptr
543 let mut sline: String; // c:602
544 // c:615 — if accumulating raw input for use in command substitution, mark
545 // the history text (including the character being substituted) for removal.
546 let lexraw_mark: i64 = crate::ported::lex::zshlex_raw_mark(-1); // c:603,615
547
548 // c:618 — `^foo^bar` shortcut: only valid on first column of input.
549 let hat = hatchar.load(SeqCst);
550 if LEX_ISFIRSTCH.with(|f| f.get()) && c == hat {
551 // c:618
552 let mut gbal: i32 = 0; // c:619
553 // c:622 — clear isfirstch
554 LEX_ISFIRSTCH.with(|f| f.set(false)); // c:622
555 // c:623 — push hatchar back so getargs parses the leading ^.
556 if let Some(ch) = char::from_u32(hat as u32) {
557 inungetc(ch); // c:623
558 }
559 let ehist = match gethist(defev.load(SeqCst)) {
560 // c:624
561 Some(h) => h,
562 None => return -1, // c:626
563 };
564 let argc_local = getargc(&ehist) as usize;
565 sline = match getargs(&ehist, 0, argc_local.saturating_sub(0)) {
566 // c:625
567 Some(s) => s,
568 None => return -1, // c:626
569 };
570
571 if getsubsargs(&sline, &mut gbal, &mut cflag) != 0 {
572 // c:628
573 return substfailed(); // c:629
574 }
575 if hsubl.lock().unwrap().is_none() {
576 // c:630
577 return -1; // c:631
578 }
579 let in_pat = hsubl.lock().unwrap().clone().unwrap_or_default();
580 let out_pat = hsubr.lock().unwrap().clone().unwrap_or_default();
581 // c:632 — `if (subst(&sline, hsubl, hsubr, gbal, 0))`. The
582 // verdict is subst's STATUS, not whether `sline` changed:
583 // `^old^old` substitutes successfully and leaves the line
584 // looking exactly as it did. `forcepat` is 0 here — the
585 // `^a^b` form has no `:S` spelling.
586 if subst(&mut sline, &in_pat, &out_pat, gbal, 0) != 0 {
587 return substfailed(); // c:633
588 }
589 } else {
590 // c:636 — !c shortcut: first-column flag clears unless c==' '.
591 if c != b' ' as i32 {
592 // c:636
593 LEX_ISFIRSTCH.with(|f| f.set(false)); // c:637
594 }
595 let bc = bangchar.load(SeqCst);
596 if c == b'\\' as i32 {
597 // c:638
598 let g = ingetc() // c:639 ingetc()
599 .map(|ch| ch as i32)
600 .unwrap_or(-1);
601 if g != bc {
602 // c:641
603 safeinungetc(g); // c:642
604 } else {
605 // c:643
606 qbang.store(true, SeqCst); // c:644
607 return bc; // c:645 return bangchar
608 }
609 }
610 if c != bc {
611 // c:648
612 return c; // c:649
613 }
614 // c:650 — `*hptr = '\0'` truncates chline at the current position.
615 let pos = hptr.load(SeqCst); // c:650
616 {
617 let mut cl = chline.lock().unwrap();
618 if pos < cl.len() {
619 cl.truncate(pos);
620 }
621 }
622 c = ingetc() // c:651
623 .map(|ch| ch as i32)
624 .unwrap_or(-1);
625 if c == b'{' as i32 {
626 // c:651
627 bflag = 1; // c:652
628 cflag = 1;
629 c = ingetc() // c:653
630 .map(|ch| ch as i32)
631 .unwrap_or(-1);
632 }
633 if c == b'"' as i32 {
634 // c:655 c == '\"'
635 stophist.store(1, SeqCst); // c:656
636 return ingetc() // c:657 return ingetc()
637 .map(|ch| ch as i32)
638 .unwrap_or(-1);
639 }
640 // c:659 — (!cflag && inblank(c)) || c == '=' || c == '(' || lexstop
641 let is_blank = (c as u8 as char).is_ascii_whitespace();
642 if (cflag == 0 && is_blank) || c == b'=' as i32 || c == b'(' as i32 || lexstop.load(SeqCst)
643 // c:659
644 {
645 safeinungetc(c); // c:660
646 return bc; // c:661 return bangchar
647 }
648 cflag = 0; // c:663
649 let mut buflen: usize = 265; // c:664
650 buf = String::with_capacity(buflen); // c:664 zhalloc
651
652 // c:666-727 — read event-spec into buf.
653 queue_signals(); // c:668
654 if c == b'?' as i32 {
655 // c:669
656 loop {
657 // c:670
658 c = ingetc() // c:671 ingetc()
659 .map(|ch| ch as i32)
660 .unwrap_or(-1);
661 if c == b'?' as i32 || c == b'\n' as i32 || lexstop.load(SeqCst) {
662 // c:672
663 break; // c:673
664 } else {
665 buf.push(c as u8 as char); // c:675 *ptr++ = c
666 if buf.len() >= buflen {
667 // c:676
668 buflen *= 2; // c:679 buflen *= 2
669 buf.reserve(buflen);
670 }
671 }
672 }
673 if c != b'\n' as i32 && !lexstop.load(SeqCst) {
674 // c:683
675 c = ingetc() // c:684
676 .map(|ch| ch as i32)
677 .unwrap_or(-1);
678 }
679 // c:685 *ptr = '\0' — Rust String is already terminated.
680 *hsubl.lock().unwrap() = Some(buf.clone()); // c:686 hsubl = ztrdup(buf)
681 // c:686 — `mev = ev = hconsearch(hsubl = ztrdup(buf), &marg);`
682 // hconsearch returns Option<(histnum, marg)> per C's
683 // out-pointer pair.
684 let (ev_val, marg_val) = match hconsearch(&buf) {
685 // c:686
686 Some((e, m)) => (e, m),
687 None => (-1, -1),
688 };
689 ev = ev_val;
690 mev.store(ev, SeqCst); // c:686 mev = ev
691 marg.store(marg_val, SeqCst); // c:686 marg out-arg
692 evset = 0; // c:687
693 if ev == -1 {
694 // c:688
695 herrflush(); // c:689
696 unqueue_signals(); // c:690
697 zerr(&format!("no such event: {}", buf)); // c:691
698 return -1; // c:692
699 }
700 } else {
701 // c:694
702 // c:697 — collect event spec until terminator.
703 loop {
704 let is_term = (c as u8 as char).is_ascii_whitespace()
705 || c == b';' as i32
706 || c == b':' as i32
707 || c == b'^' as i32
708 || c == b'$' as i32
709 || c == b'*' as i32
710 || c == b'%' as i32
711 || c == b'}' as i32
712 || c == b'\'' as i32
713 || c == b'"' as i32
714 || c == b'`' as i32
715 || lexstop.load(SeqCst); // c:698-700
716 if is_term {
717 break;
718 }
719 if !buf.is_empty() {
720 // c:702
721 if c == b'-' as i32 {
722 break;
723 } // c:703-704
724 let first = buf.as_bytes()[0];
725 if (first.is_ascii_digit() || first == b'-') // c:705
726 && !(c as u8).is_ascii_digit()
727 // c:705 !idigit(c)
728 {
729 break; // c:706
730 }
731 }
732 buf.push(c as u8 as char); // c:708
733 if buf.len() >= buflen {
734 // c:709
735 buflen *= 2; // c:712
736 buf.reserve(buflen);
737 }
738 if c == b'#' as i32 || c == bc {
739 // c:714
740 c = ingetc() // c:715
741 .map(|ch| ch as i32)
742 .unwrap_or(-1);
743 break; // c:716
744 }
745 c = ingetc() // c:718
746 .map(|ch| ch as i32)
747 .unwrap_or(-1);
748 }
749 if buf.is_empty() // c:720
750 && (c == b'}' as i32 || c == b';' as i32 || c == b'\'' as i32
751 || c == b'"' as i32 || c == b'`' as i32)
752 {
753 safeinungetc(c); // c:723
754 unqueue_signals(); // c:724
755 return bc; // c:725
756 }
757 // c:727 *ptr = 0 — handled by Rust String
758 if buf.is_empty() {
759 // c:728
760 if c != b'%' as i32 {
761 // c:729
762 if isset(CSHJUNKIEHISTORY) {
763 // c:730
764 ev = addhistnum(curhist.load(SeqCst), -1, HIST_FOREIGN as i32);
765 } else {
766 // c:732
767 ev = defev.load(SeqCst); // c:733
768 }
769 if c == b':' as i32 && evset == -1 {
770 // c:734
771 evset = 0; // c:735
772 } else {
773 evset = 1; // c:737
774 }
775 } else {
776 // c:738
777 if marg.load(SeqCst) != -1 {
778 // c:739
779 ev = mev.load(SeqCst); // c:740
780 } else {
781 // c:741
782 ev = defev.load(SeqCst); // c:742
783 }
784 evset = 0; // c:743
785 }
786 } else if let Ok(t0) = buf.trim().parse::<i64>() {
787 // c:745 zstrtol(buf, NULL, 10)
788 if t0 != 0 {
789 ev = if t0 < 0 {
790 // c:746
791 addhistnum(curhist.load(SeqCst), t0 as i32, HIST_FOREIGN as i32)
792 } else {
793 t0
794 };
795 evset = 1; // c:747
796 } else if buf.as_bytes()[0] == bc as u8 {
797 // c:748 *buf == bangchar
798 ev = addhistnum(curhist.load(SeqCst), -1, HIST_FOREIGN as i32); // c:749
799 evset = 1; // c:750
800 } else if buf.as_bytes()[0] == b'#' {
801 // c:751
802 ev = curhist.load(SeqCst); // c:752
803 evset = 1; // c:753
804 } else {
805 // c:754
806 match hcomsearch(&buf) {
807 // c:754
808 Some(e) => {
809 ev = e;
810 evset = 1;
811 }
812 None => {
813 herrflush(); // c:755
814 unqueue_signals(); // c:756
815 zerr(&format!("event not found: {}", buf)); // c:757
816 return -1; // c:758
817 }
818 }
819 }
820 } else if buf.as_bytes()[0] == bc as u8 {
821 // c:748 *buf == bangchar
822 ev = addhistnum(curhist.load(SeqCst), -1, HIST_FOREIGN as i32);
823 evset = 1;
824 } else if buf.as_bytes()[0] == b'#' {
825 // c:751
826 ev = curhist.load(SeqCst);
827 evset = 1;
828 } else {
829 // c:754
830 match hcomsearch(&buf) {
831 Some(e) => {
832 ev = e;
833 evset = 1;
834 }
835 None => {
836 herrflush();
837 unqueue_signals();
838 zerr(&format!("event not found: {}", buf));
839 return -1;
840 }
841 }
842 }
843 }
844
845 // c:765 — fetch the resolved history entry.
846 defev.store(ev, SeqCst); // c:765 defev = ev
847 let mut ehist = match gethist(ev) {
848 // c:765
849 Some(h) => h,
850 None => {
851 unqueue_signals(); // c:766
852 return -1; // c:767
853 }
854 };
855 argc = getargc(&ehist) as i32; // c:771
856
857 // c:772 — word-designator parsing.
858 if c == b':' as i32 {
859 // c:772
860 cflag = 1; // c:773
861 c = ingetc() // c:774
862 .map(|ch| ch as i32)
863 .unwrap_or(-1);
864 if c == b'%' as i32 && marg.load(SeqCst) != -1 {
865 // c:775
866 if evset == 0 {
867 // c:776
868 ehist = match gethist(mev.load(SeqCst)) {
869 // c:777
870 Some(h) => {
871 defev.store(mev.load(SeqCst), SeqCst);
872 h
873 }
874 None => {
875 unqueue_signals();
876 return -1;
877 }
878 };
879 argc = getargc(&ehist) as i32; // c:778
880 } else {
881 // c:779
882 herrflush(); // c:780
883 unqueue_signals(); // c:781
884 zerr("ambiguous history reference"); // c:782
885 return -1; // c:783
886 }
887 }
888 }
889 // c:788
890 if c == b'*' as i32 {
891 // c:788
892 farg = 1; // c:789
893 larg = argc; // c:790
894 cflag = 0; // c:791
895 } else {
896 // c:792
897 if let Some(ch) = char::from_u32(c as u32) {
898 inungetc(ch); // c:793
899 }
900 let r = getargspec(argc, marg.load(SeqCst), evset); // c:794
901 larg = r;
902 farg = r;
903 if larg == -2 {
904 // c:795
905 unqueue_signals(); // c:796
906 return -1; // c:797
907 }
908 if farg != -1 {
909 // c:799
910 cflag = 0; // c:800
911 }
912 c = ingetc() // c:801
913 .map(|ch| ch as i32)
914 .unwrap_or(-1);
915 if c == b'*' as i32 {
916 // c:802
917 cflag = 0; // c:803
918 larg = argc; // c:804
919 } else if c == b'-' as i32 {
920 // c:805
921 cflag = 0; // c:806
922 larg = getargspec(argc, marg.load(SeqCst), evset); // c:807
923 if larg == -2 {
924 // c:808
925 unqueue_signals(); // c:809
926 return -1; // c:810
927 }
928 if larg == -1 {
929 // c:812
930 larg = argc - 1; // c:813
931 }
932 } else {
933 // c:814
934 if let Some(ch) = char::from_u32(c as u32) {
935 inungetc(ch); // c:815
936 }
937 }
938 }
939 if farg == -1 {
940 // c:817
941 farg = 0; // c:818
942 }
943 if larg == -1 {
944 // c:819
945 larg = argc; // c:820
946 }
947 sline = match getargs(&ehist, farg as usize, larg as usize) {
948 // c:821
949 Some(s) => s,
950 None => {
951 unqueue_signals(); // c:822
952 return -1; // c:823
953 }
954 };
955 unqueue_signals(); // c:825
956 }
957
958 // c:830 — modifier loop.
959 loop {
960 c = if cflag != 0 {
961 b':' as i32
962 } else {
963 // c:831
964 ingetc().map(|ch| ch as i32).unwrap_or(-1)
965 };
966 cflag = 0; // c:832
967 if c == b':' as i32 {
968 // c:833
969 let mut gbal: i32 = 0; // c:834
970 c = ingetc() // c:836
971 .map(|ch| ch as i32)
972 .unwrap_or(-1);
973 if c == b'g' as i32 {
974 // c:836
975 gbal = 1; // c:837
976 c = ingetc() // c:838
977 .map(|ch| ch as i32)
978 .unwrap_or(-1);
979 if c != b's' as i32 && c != b'S' as i32 && c != b'&' as i32 {
980 // c:839
981 zerr("'s' or '&' modifier expected after 'g'"); // c:840
982 return -1; // c:841
983 }
984 }
985 match c as u8 {
986 b'p' => {
987 // c:845
988 histdone.store(HISTFLAG_DONE | HISTFLAG_NOEXEC, SeqCst);
989 // c:846
990 }
991 b'a' => {
992 // c:848
993 match chabspath(&sline) {
994 // c:849
995 Some(new) => sline = new,
996 None => {
997 herrflush(); // c:850
998 zerr("modifier failed: a"); // c:851
999 return -1; // c:852
1000 }
1001 }
1002 }
1003 b'A' => {
1004 // c:856
1005 match chrealpath(&sline, b'A', false) {
1006 // c:857 chrealpath(&sline, 'A', 0)
1007 Some(new) => sline = new,
1008 None => {
1009 herrflush(); // c:858
1010 zerr("modifier failed: A"); // c:859
1011 return -1; // c:860
1012 }
1013 }
1014 }
1015 b'c' => {
1016 // c:863
1017 match equalsubstr(&sline, false, false) {
1018 // c:864
1019 Some(new) => sline = new,
1020 None => {
1021 herrflush(); // c:865
1022 zerr("modifier failed: c"); // c:866
1023 return -1; // c:867
1024 }
1025 }
1026 }
1027 b'h' => {
1028 // c:870
1029 let count = digitcount(); // c:871
1030 sline = remtpath(&sline, count);
1031 }
1032 b'e' => {
1033 // c:877
1034 sline = rembutext(&sline); // c:878
1035 }
1036 b'r' => {
1037 // c:884
1038 sline = remtext(&sline); // c:885
1039 }
1040 b't' => {
1041 // c:891
1042 let count = digitcount(); // c:892
1043 sline = remlpaths(&sline, count);
1044 }
1045 b's' | b'S' => {
1046 // c:898-899
1047 hsubpatopt.store((c == b'S' as i32) as i32, SeqCst); // c:900
1048 if getsubsargs(&sline, &mut gbal, &mut cflag) != 0 {
1049 // c:901
1050 return -1; // c:902
1051 }
1052 // fall through to '&' // c:902
1053 let (in_pat, out_pat) =
1054 (hsubl.lock().unwrap().clone(), hsubr.lock().unwrap().clone());
1055 if let (Some(ip), Some(op)) = (in_pat, out_pat) {
1056 // c:904
1057 // c:905 — branch on subst's STATUS. `hsubpatopt`
1058 // is the `forcepat` argument: `:S` forces the
1059 // pattern path even without HIST_SUBST_PATTERN.
1060 if subst(&mut sline, &ip, &op, gbal, hsubpatopt.load(SeqCst)) != 0 {
1061 return substfailed(); // c:906
1062 }
1063 } else {
1064 // c:907
1065 herrflush(); // c:908
1066 zerr("no previous substitution"); // c:909
1067 return -1; // c:910
1068 }
1069 }
1070 b'&' => {
1071 // c:903
1072 let (in_pat, out_pat) =
1073 (hsubl.lock().unwrap().clone(), hsubr.lock().unwrap().clone());
1074 if let (Some(ip), Some(op)) = (in_pat, out_pat) {
1075 // c:905 — the `:&` arm C falls into from `:s`,
1076 // so it repeats the same status branch.
1077 if subst(&mut sline, &ip, &op, gbal, hsubpatopt.load(SeqCst)) != 0 {
1078 return substfailed(); // c:906
1079 }
1080 } else {
1081 herrflush();
1082 zerr("no previous substitution");
1083 return -1;
1084 }
1085 }
1086 b'q' => {
1087 // c:913
1088 sline = quote(&sline); // c:914
1089 }
1090 b'Q' => {
1091 // c:916
1092 // c:918-923 — `int one = noerrs, oef = errflag; noerrs = 1;
1093 // parse_subst_string(sline); noerrs = one;
1094 // errflag = oef | (errflag & ERRFLAG_INT);`
1095 let one = *crate::ported::utils::noerrs_lock().lock().unwrap();
1096 let oef = errflag.load(SeqCst);
1097 *crate::ported::utils::noerrs_lock().lock().unwrap() = 1;
1098 if let Ok(t) = parse_subst_string(&sline) {
1099 sline = t; // c:921 (in-place in C)
1100 }
1101 *crate::ported::utils::noerrs_lock().lock().unwrap() = one;
1102 errflag.store(oef | (errflag.load(SeqCst) & ERRFLAG_INT), SeqCst); // c:923
1103 let mut s = sline.clone();
1104 remnulargs(&mut s); // c:924
1105 sline = untokenize(&s); // c:925
1106 }
1107 b'x' => {
1108 // c:928
1109 sline = quotebreak(&sline); // c:929
1110 }
1111 b'l' => {
1112 // c:931
1113 sline = casemodify(&sline, CASMOD_LOWER); // c:932
1114 }
1115 b'u' => {
1116 // c:934
1117 sline = casemodify(&sline, CASMOD_UPPER); // c:935
1118 }
1119 b'P' => {
1120 // c:937
1121 if !sline.starts_with('/') {
1122 // c:938
1123 let here = crate::ported::compat::zgetcwd(); // c:939
1124 sline = if here.ends_with('/') {
1125 // c:940
1126 crate::ported::string::dyncat(&here, &sline) // c:943
1127 } else {
1128 // c:941
1129 // c:941 zhtricat(metafy(here, -1, META_HEAPDUP), "/", sline)
1130 format!("{}/{}", here, sline) // c:941
1131 }; // c:944
1132 }
1133 match crate::ported::utils::xsymlink(&sline) {
1134 // c:945
1135 Some(new) => sline = new,
1136 None => {} // C ignores xsymlink failure (returns NULL → keep sline)
1137 }
1138 }
1139 _ => {
1140 // c:947 default
1141 herrflush(); // c:948
1142 zerr(&format!("invalid modifier: {}", c as u8 as char)); // c:949
1143 return -1; // c:950
1144 }
1145 }
1146 } else {
1147 // c:952
1148 if c != b'}' as i32 || bflag == 0 {
1149 // c:953
1150 if let Some(ch) = char::from_u32(c as u32) {
1151 inungetc(ch); // c:954
1152 }
1153 }
1154 if c != b'}' as i32 && bflag != 0 {
1155 // c:955
1156 zerr("'}' expected"); // c:956
1157 return -1; // c:957
1158 }
1159 break; // c:959
1160 }
1161 }
1162
1163 crate::ported::lex::zshlex_raw_back_to_mark(lexraw_mark); // c:963
1164
1165 // c:970-976 — push the expanded value onto the input stack as INP_HIST.
1166 lexstop.store(false, SeqCst); // c:970
1167 crate::ported::input::inpush(&sline, INP_HIST, None); // c:976
1168 histdone.fetch_or(HISTFLAG_DONE, SeqCst); // c:977
1169 if isset(HISTVERIFY) {
1170 // c:978
1171 histdone.fetch_or(HISTFLAG_NOEXEC | HISTFLAG_RECALL, SeqCst); // c:979
1172 }
1173
1174 // c:982 — return ingetc() so caller sees the first char of expansion.
1175 ingetc().map(|ch| ch as i32).unwrap_or(-1)
1176}
1177
1178/// Port of `herrflush()` from `Src/hist.c:477`. — C decl `herrflush(void)`.
1179///
1180/// C body drains the input buffer after a history-expansion error,
1181/// feeding the consumed chars into the history-build cursor
1182/// (`hwaddc` + `addtoline`) so the history line still records the
1183/// raw input that failed. Without this drain, the history entry
1184/// for a failed `!ev` would be truncated at the failure point.
1185///
1186/// The previous Rust port left the drain loop as a no-op comment,
1187/// claiming the deps weren't ported. They ARE: `ihwaddc` /
1188/// `iaddtoline` live at hist.rs above; `strin` / `LEX_LEX_ADD_RAW`
1189/// are file-statics on this module + lex.rs.
1190pub fn herrflush() {
1191 // c:477
1192 // c:479 — `inpopalias();`
1193 crate::ported::input::inpopalias();
1194
1195 // c:481-482 — `if (lexstop) return;`. Go through the `lexstop` handle
1196 // (hist.rs, below) rather than one half of it, so this reads the same
1197 // C variable that `ihgetc` writes.
1198 if lexstop.load(SeqCst) {
1199 return;
1200 }
1201
1202 // c:494-500 — drain the input buffer when expanding history for
1203 // ZLE (the `!strin || lex_add_raw` arm covers the two cases where
1204 // the input must be flushed into the history line:
1205 // - non-ZLE non-string input (`!strin` true);
1206 // - ZLE with raw-recording (`lex_add_raw != 0`).
1207 //
1208 // C:
1209 // while (inbufct && (!strin || lex_add_raw)) {
1210 // int c = ingetc();
1211 // if (!lexstop) { hwaddc(c); addtoline(c); }
1212 // }
1213 //
1214 // !!! WARNING: SPLIT INPUT BUFFERS !!! C has one input buffer: `hungetc`
1215 // puts characters back into `inbuf`, so this drain reads them first, and
1216 // `ingetc` records each character in the raw buffer while `lex_add_raw`
1217 // is set (c:Src/input.c:360-361). This port keeps the lexer's pushback in
1218 // `lex::LEX_UNGET_BUF` and does the raw recording in `lex::hgetc`, not in
1219 // `input::ingetc`. While skipcomm is recording a command substitution
1220 // (c:Src/lex.c:2217), read the pushback through `hgetc` (its pushback
1221 // arm records raw and does no history expansion) and record the rest
1222 // below. Otherwise the drained input vanished from the word:
1223 // `${(z)…}` of `echo $(|||) bar` gave `$(|||` for zsh's `$(|||) bar`.
1224 let recording_raw = crate::ported::lex::LEX_LEX_ADD_RAW.get() != 0;
1225 if recording_raw {
1226 while crate::ported::lex::LEX_UNGET_BUF.with_borrow(|b| !b.is_empty()) {
1227 crate::ported::lex::hgetc();
1228 }
1229 }
1230 loop {
1231 let inbufct = crate::ported::input::inbufct.with(|c| c.get());
1232 if inbufct <= 0 {
1233 break;
1234 }
1235 let strin_v = strin.load(SeqCst);
1236 let lex_add_raw = crate::ported::lex::LEX_LEX_ADD_RAW.get();
1237 if !(strin_v == 0 || lex_add_raw != 0) {
1238 // c:494 (!strin || lex_add_raw)
1239 break;
1240 }
1241 let ch = ingetc(); // c:495 ingetc()
1242 let c = ch.map(|ch| ch as i32).unwrap_or(-1);
1243 if !lexstop.load(SeqCst) {
1244 // c:496 if (!lexstop)
1245 if recording_raw {
1246 if let Some(ch) = ch {
1247 crate::ported::lex::zshlex_raw_add(ch); // c:Src/input.c:360-361
1248 }
1249 }
1250 ihwaddc(c); // c:497 hwaddc(c)
1251 iaddtoline(c); // c:498 addtoline(c)
1252 }
1253 }
1254}
1255
1256/// Port of `getargc()` from `Src/hist.c:556`. — C decl `getargc(Histent ehist)`.
1257/// C body: `return ehist->nwords ? ehist->nwords-1 : 0;`
1258/// Returns the number of word designators (nwords - 1, since word 0
1259/// is the command name and arguments start at word 1).
1260pub fn getargc(entry: &histent) -> usize {
1261 // c:558
1262 if entry.nwords > 0 {
1263 (entry.nwords - 1) as usize
1264 } else {
1265 0
1266 }
1267}
1268
1269/// Port of `substfailed()` from `Src/hist.c:563`. — C decl `substfailed(void)`.
1270/// ```c
1271/// static int
1272/// substfailed(void)
1273/// {
1274/// herrflush();
1275/// zerr("substitution failed");
1276/// return -1;
1277/// }
1278/// ```
1279pub fn substfailed() -> i32 {
1280 // c:563
1281 herrflush(); // c:565
1282 zerr("substitution failed"); // c:566
1283 -1 // c:567
1284}
1285
1286/// Port of `digitcount()` from `Src/hist.c:574`. — C decl `digitcount(void)`.
1287///
1288/// C body:
1289/// ```c
1290/// int c = ingetc(), count;
1291/// if (idigit(c)) {
1292/// count = 0;
1293/// do {
1294/// count = 10 * count + (c - '0');
1295/// c = ingetc();
1296/// } while (idigit(c));
1297/// } else
1298/// count = 0;
1299/// inungetc(c);
1300/// return count;
1301/// ```
1302///
1303/// "Return a count given by decimal digits after a modifier."
1304/// Pulls characters off the INPUT STREAM via `ingetc`/`inungetc`,
1305/// NOT from a passed-in string. Called from c:871 (`:h` modifier)
1306/// and c:892 (`:t` modifier) to parse the digit count after the
1307/// modifier letter.
1308///
1309/// The previous Rust port was a complete fabrication: signature was
1310/// `(s: &str) -> usize` counting leading digits in an argument
1311/// string. No real caller — the C function streams from input, not
1312/// a string. Pin the C signature exactly.
1313pub fn digitcount() -> i32 {
1314 // c:574
1315 let mut c: i32 = ingetc() // c:576 ingetc()
1316 .map(|ch| ch as i32)
1317 .unwrap_or(-1);
1318 let mut count: i32;
1319 if c >= 0 && (c as u8 as char).is_ascii_digit() {
1320 // c:578 idigit(c)
1321 count = 0; // c:579
1322 loop {
1323 count = 10 * count + (c - b'0' as i32); // c:581
1324 c = ingetc() // c:582 ingetc()
1325 .map(|ch| ch as i32)
1326 .unwrap_or(-1);
1327 if c < 0 || !(c as u8 as char).is_ascii_digit() {
1328 // c:583
1329 break;
1330 }
1331 }
1332 } else {
1333 count = 0; // c:586
1334 }
1335 if c >= 0 {
1336 if let Some(ch) = char::from_u32(c as u32) {
1337 // c:587 inungetc(c)
1338 inungetc(ch);
1339 }
1340 }
1341 count // c:588
1342}
1343
1344/// Port of `strinbeg()` from `Src/hist.c:1033`. — C decl `strinbeg(int dohist)`.
1345///
1346/// C body:
1347/// strin++;
1348/// hbegin(dohist);
1349/// lexinit();
1350/// init_parse_status();
1351pub fn strinbeg(dohist: i32) {
1352 // c:1033
1353 // !!! RUST-ONLY GUARD: the history-side `strin` is process-global and
1354 // the main thread's hbegin reads it (`&& !strin`, c:1163); a worker-pool
1355 // parse bumping it made a real command line look like string input.
1356 // Pool threads keep only the thread-local input-side copy below.
1357 if !crate::worker::in_worker_thread() {
1358 strin.fetch_add(1, SeqCst); // c:1035
1359 }
1360 // C has ONE `strin`; zshrs splits it into hist.rs `strin` (history
1361 // logic above/below) and input.rs `strin` (the copy `ingetc` checks
1362 // at input.rs:390 to decide "string input drained → EOF" vs "read
1363 // more SHIN"). The single C `strin++` must bump BOTH — same
1364 // paired-global rule as `lexstop`. Without the input-side bump, a
1365 // nested string parse (cmd-subst body via parse_isolated) that
1366 // drained its LEX_INPUT fell through to `inputline()` and STOLE the
1367 // outer reader's next SHIN line.
1368 crate::ported::input::strin.with(|s| s.set(s.get() + 1));
1369 hbegin(dohist); // c:1036
1370 lexinit(); // c:1037
1371 init_parse_status(); // c:1042
1372}
1373
1374/// Port of `strinend()` from `Src/hist.c:1049`. — C decl `strinend(void)`.
1375///
1376/// C body:
1377/// hend(NULL);
1378/// DPUTS(!strin, "BUG: strinend() called without strinbeg()");
1379/// strin--;
1380/// isfirstch = 1;
1381/// histdone = 0;
1382///
1383/// `isfirstch = 1` and `histdone = 0` resets are critical for the
1384/// `^foo^bar`-style histsubchar shortcut that keys on `isfirstch`
1385/// in the next strinbeg-driven parse.
1386pub fn strinend() {
1387 // c:1049
1388 hend(None); // c:1051
1389 // Pool threads never bumped the global `strin` or opened a history line
1390 // (see strinbeg / hbegin), so they leave both alone here too.
1391 let main_thread = !crate::worker::in_worker_thread();
1392 if main_thread {
1393 // c:1052 — DPUTS(!strin, "BUG: strinend() called without strinbeg()")
1394 DPUTS!(
1395 // c:1052
1396 strin.load(Ordering::SeqCst) == 0, // c:1052 !strin
1397 "BUG: strinend() called without strinbeg()" // c:1052
1398 );
1399 strin.fetch_sub(1, SeqCst); // c:1053
1400 }
1401 // Mirror the input-side `strin` decrement (see strinbeg note).
1402 crate::ported::input::strin.with(|s| s.set(s.get() - 1));
1403 LEX_ISFIRSTCH.with(|f| f.set(true)); // c:1054 isfirstch = 1
1404 if main_thread {
1405 histdone.store(0, SeqCst); // c:1055 histdone = 0
1406 }
1407}
1408
1409/// Port of `nohw()` from `Src/hist.c:1062`. — C decl `nohw(UNUSED(int c))`.
1410pub fn nohw(_c: i32) { /* do nothing */
1411} // c:1062
1412
1413/// Port of `nohwabort()` from `Src/hist.c:1067`. — C decl `nohwabort(void)`.
1414pub fn nohwabort() { /* do nothing */
1415} // c:1067
1416
1417/// Port of `nohwe()` from `Src/hist.c:1072`. — C decl `nohwe(void)`.
1418pub fn nohwe() { /* do nothing */
1419} // c:1072
1420
1421/// Port of `ihwbegin()` from `Src/hist.c:1656`. — C decl `ihwbegin(int offset)`.
1422///
1423/// C gate:
1424/// ```c
1425/// if (stophist == 2 || (histactive & HA_INWORD) ||
1426/// (inbufflags & (INP_ALIAS|INP_HIST)) == INP_ALIAS)
1427/// return;
1428/// ```
1429///
1430/// The previous Rust port checked only `stophist == 2 || HA_INWORD`,
1431/// missing the `INP_ALIAS-only` gate. Effect: word-position
1432/// recording fired during alias expansion (when `INP_ALIAS` is set
1433/// and `INP_HIST` is not), capturing alias-substituted bytes as
1434/// fresh words. The C source skips alias-only input from the
1435/// history-word table because aliases are pre-expansion and
1436/// shouldn't show up as user-typed words in `!:N` etc.
1437pub fn ihwbegin(offset: i32) {
1438 // c:1656
1439 // !!! WARNING: RUST-ONLY GUARD — NO C COUNTERPART !!!
1440 // C flips `hgetc`/`hungetc`/`hwaddc`/`hwbegin`/`hwabort`/`hwend`/
1441 // `addtoline` to their no-op twins (`ingetc`/`inungetc`/`nohw`/
1442 // `nohwabort`/`nohwe`) whenever `stophist == 2` (c:1134-1145) —
1443 // that is how a non-history parse avoids touching the history line
1444 // buffers. `stophist`, `chline`, `hptr`, `chwords` and `chwordpos`
1445 // are PROCESS globals in this port, so a worker-pool thread cannot
1446 // set `stophist = 2` without also disabling the interactive shell's
1447 // history. Background parses (compinit's bytecode backfill compiles
1448 // ~47k autoload bodies on the pool) are never history events, so a
1449 // pool thread behaves as if `stophist == 2`. Without this, the
1450 // worker's `gettok` → `ihwbegin` raced `chwordpos`/`chwords` with
1451 // the main thread's live command line and panicked in `Vec::resize`
1452 // ("capacity overflow"), poisoning the mutex and killing the shell.
1453 // Same guard on the other six hooks (c:1139-1145).
1454 if crate::worker::in_worker_thread() {
1455 return;
1456 }
1457 // c:1658 — `int pos = hptr - chline + offset;`. The C `hptr` is
1458 // the current write position into the chline buffer; `pos` is
1459 // the byte offset from buffer start + caller-supplied offset.
1460 // The previous Rust port used `chline.lock().unwrap().len()`
1461 // which is only equal to `hptr - chline` when hptr is at end —
1462 // any lexer rewind (e.g. backquote, comment-resume) shifts hptr
1463 // earlier and the chline.len() reading would record the WRONG
1464 // word start, producing off-by-many history word offsets.
1465 let hptr_val = hptr.load(SeqCst); // c:1658
1466 let stop = stophist.load(SeqCst);
1467 let active = histactive.load(SeqCst);
1468 let inflags = crate::ported::input::inbufflags.with(|f| f.get());
1469 // c:1659 — `(inbufflags & (INP_ALIAS|INP_HIST)) == INP_ALIAS`
1470 // means "alias-only input (no history layered above)".
1471 if stop == 2 || (active & HA_INWORD) != 0 || (inflags & (INP_ALIAS | INP_HIST)) == INP_ALIAS
1472 // c:1659
1473 {
1474 return;
1475 }
1476 let pos = chwordpos.load(SeqCst);
1477 // `pos > 0` is a RUST-ONLY floor on C's bare `if (chwordpos%2)`
1478 // (c:1662). C is single-threaded, so `chwordpos` is an invariantly
1479 // non-negative index into `chwords`; zshrs runs parallel lexers
1480 // against the same atomic (see the race note in ihwend below), and
1481 // an interleaving that drives it negative makes `-1 % 2 == -1`
1482 // "odd", so this decrement walks it further down. Everything
1483 // downstream then indexes `chwords[chwordpos]`, and a negative i32
1484 // `as usize` wraps to ~2^64 — `words.resize(idx + 1, 0)` panicked
1485 // with "attempt to add with overflow", poisoning `chwords` for the
1486 // rest of the process. Under C's invariant the guard never fires.
1487 if pos % 2 != 0 && pos > 0 {
1488 // c:1662 chwordpos%2
1489 chwordpos.fetch_sub(1, SeqCst); // c:1663
1490 }
1491 // c:1664-1665 — DPUTS1(pos < 0, "History word position < 0 in %s",
1492 // dupstrpfx(chline, hptr-chline))
1493 let word_pos = (hptr_val as i32) + offset; // c:1664 pos
1494 DPUTS1!(
1495 // c:1664
1496 word_pos < 0, // c:1664
1497 "History word position < 0 in {}", // c:1664
1498 {
1499 // c:1665 dupstrpfx(chline, hptr-chline)
1500 let line = chline.lock().unwrap(); // c:1665
1501 line.chars().take(hptr_val).collect::<String>() // c:1665
1502 }
1503 );
1504 // c:1666 — `if (pos < 0) pos = 0;`. The .max(0) clamp.
1505 let start = word_pos.max(0) as i16; // c:1658
1506 let mut words = chwords.lock().unwrap();
1507 // `.max(0)` before the cast — a negative i32 `as usize` wraps to a
1508 // near-`usize::MAX` index and the `idx + 1` below then overflows.
1509 // See the floor comment above.
1510 let idx = chwordpos.load(SeqCst).max(0) as usize;
1511 if words.len() <= idx {
1512 words.resize(idx + 1, 0);
1513 }
1514 words[idx] = start; // c:1668
1515 chwordpos.fetch_add(1, SeqCst); // c:1668 chwordpos++
1516}
1517
1518/// Port of `linkcurline()` from `Src/hist.c:1079`. — C decl `linkcurline(void)`.
1519pub fn linkcurline() {
1520 // c:1079
1521 let new_hist = curhist.fetch_add(1, SeqCst) + 1; // c:1093 ++curhist
1522 let mut cur = curline.lock().unwrap();
1523 *cur = Some(make_histent(new_hist, String::new())); // c:1093 curline.histnum
1524 // Splicing into the ring (c:1081-1088) is encoded by the Vec::insert
1525 // at hist_ring index 0 done by hend() on commit. The sentinel itself
1526 // lives in `curline` until then.
1527}
1528
1529/// Port of `unlinkcurline()` from `Src/hist.c:1093`. — C decl `unlinkcurline(void)`.
1530pub fn unlinkcurline() {
1531 // c:1093-1102 — C splices the STATIC `curline` struct out of the
1532 // ring; its fields (histnum included) survive. Two readers depend
1533 // on that during command execution: bin_fc's default-range check
1534 // `curline.histnum == curhist` (builtin.c:1581 — excludes the
1535 // in-flight fc command from `fc -l`) and loop()'s preexec-hook
1536 // same-event check (init.c:195). zshrs's ring never physically
1537 // holds the curline sentinel, so the splice itself is a no-op —
1538 // the previous `*curline = None` DROPPED histnum and broke both.
1539 curhist.fetch_sub(1, SeqCst); // c:1103
1540}
1541
1542/// Port of `hbegin()` from `Src/hist.c:1110`. — C decl `hbegin(int dohist)`.
1543pub fn hbegin(dohist: i32) {
1544 // c:1110
1545 // c:1114 — `isfirstln = isfirstch = 1;`. These live in lex.rs LEX_*
1546 // thread_locals. hbegin runs at the top of every loop() iteration, so
1547 // resetting them here makes the NEXT command start on its first line —
1548 // the PS1 prompt, not the PS2 continuation prompt. The prior port left
1549 // this to "the caller", but no caller did it, so every interactive
1550 // prompt after the first rendered as `>` (PS2).
1551 crate::ported::lex::LEX_ISFIRSTLN.with(|c| c.set(true));
1552 crate::ported::lex::LEX_ISFIRSTCH.with(|c| c.set(true));
1553
1554 // !!! WARNING: RUST-ONLY GUARD — NO C COUNTERPART !!!
1555 // Everything below writes the PROCESS-global history state
1556 // (histactive, histdone, stophist, chline, curhist via linkcurline),
1557 // which C, having one thread, owns outright. A worker-pool parse
1558 // (async precmd hooks, autoload backfill) runs this concurrently with
1559 // the main thread's command line: its HA_NOINC hend cleared histactive
1560 // between the main line's hbegin and hend, so the main hend took the
1561 // unlinkcurline arm and the next command reused an event number —
1562 // `fc -l` then looped forever on the duplicate. A pool parse records
1563 // no history, so it leaves that state alone (same rule as ihwaddc).
1564 if crate::worker::in_worker_thread() {
1565 return;
1566 }
1567
1568 errflag.fetch_and(
1569 // c:1115
1570 !ERRFLAG_ERROR,
1571 Ordering::Relaxed,
1572 );
1573 histdone.store(0, SeqCst); // c:1116
1574 // c:1117 — `isset(INTERACTIVE)` / `isset(SHINSTDIN)`.
1575 let interact = isset(INTERACTIVE);
1576 let shinstdin = isset(SHINSTDIN);
1577 if dohist == 0 {
1578 // c:1117
1579 stophist.store(2, SeqCst); // c:1118
1580 } else if dohist != 2 {
1581 // c:1119
1582 stophist.store(
1583 if !interact || !shinstdin { 2 } else { 0 }, // c:1120
1584 SeqCst,
1585 );
1586 } else {
1587 // c:1121
1588 stophist.store(0, SeqCst); // c:1122
1589 }
1590
1591 if stophist.load(SeqCst) == 2 {
1592 // c:1134
1593 chline.lock().unwrap().clear(); // c:1135 chline = NULL
1594 hptr.store(0, SeqCst); // c:1135 hptr = NULL
1595 hlinesz.store(0, SeqCst); // c:1136
1596 chwords.lock().unwrap().clear(); // c:1137
1597 chwordlen.store(0, SeqCst); // c:1138
1598 // hgetc/hungetc/hwaddc/hwbegin/hwabort/hwend/addtoline are c:1139-1145
1599 // function-pointer slots in C; Rust dispatches statically.
1600 } else {
1601 // c:1146
1602 let mut buf = chline.lock().unwrap(); // c:1147
1603 buf.clear();
1604 buf.reserve(64);
1605 hlinesz.store(64, SeqCst); // c:1147
1606 drop(buf);
1607 let mut w = chwords.lock().unwrap(); // c:1148
1608 w.clear();
1609 w.reserve(64);
1610 chwordlen.store(64, SeqCst);
1611 drop(w);
1612 // hgetc/hungetc/hwaddc/hwbegin/hwabort/hwend/addtoline c:1149-1155 — see c:1139.
1613 if !isset(BANGHIST) {
1614 // c:1156
1615 stophist.store(4, SeqCst); // c:1157
1616 }
1617 }
1618 chwordpos.store(0, SeqCst); // c:1159
1619
1620 {
1621 // c:1161
1622 let mut ring = hist_ring.lock().unwrap();
1623 if let Some(top) = ring.first_mut() {
1624 if top.ftim == 0 && strin.load(SeqCst) == 0 {
1625 top.ftim = std::time::SystemTime::now()
1626 .duration_since(std::time::UNIX_EPOCH)
1627 .map(|d| d.as_secs() as i64)
1628 .unwrap_or(0); // c:1162
1629 }
1630 }
1631 }
1632 if (dohist == 2 || (interact && shinstdin)) // c:1163
1633 && strin.load(SeqCst) == 0
1634 {
1635 histactive.store(HA_ACTIVE, SeqCst); // c:1164
1636 // c:1165 — `attachtty(mypgrp);` reclaims the controlling
1637 // terminal for the shell's pgrp at the start of a fresh
1638 // history-recording line.
1639 let mypgrp = *crate::ported::jobs::MYPGRP
1640 .get_or_init(|| Mutex::new(0))
1641 .lock()
1642 .expect("mypgrp poisoned");
1643 crate::ported::utils::attachtty(mypgrp); // c:1165
1644 linkcurline(); // c:1166
1645 defev.store(
1646 addhistnum(
1647 curhist.load(SeqCst), // c:1167
1648 -1,
1649 HIST_FOREIGN as i32,
1650 ),
1651 SeqCst,
1652 );
1653 } else {
1654 histactive.store(HA_ACTIVE | HA_NOINC, SeqCst); // c:1169
1655 }
1656
1657 if isset(INCAPPENDHISTORYTIME) // c:1189
1658 && !isset(SHAREHISTORY)
1659 && !isset(INCAPPENDHISTORY)
1660 && (histactive.load(SeqCst) & HA_NOINC) == 0
1661 && strin.load(SeqCst) == 0
1662 && histsave_stack_pos.load(SeqCst) == 0
1663 {
1664 let hf = resolve_histfile(); // c:1192
1665 savehistfile(
1666 hf.as_deref(),
1667 0 // c:1193
1668 | HFILE_USE_OPTIONS as i32
1669 | HFILE_FAST as i32,
1670 );
1671 }
1672}
1673
1674/// Port of `histreduceblanks()` from `Src/hist.c:1199`. — C decl `histreduceblanks(void)`.
1675///
1676/// Operates on the global `chline` / `chwords` / `chwordpos` exactly as C
1677/// does: each recorded word is moved left to close the gap before it,
1678/// carrying the ONE separator byte that followed it, so quoted text inside a
1679/// word (`"a b"`) is never touched and a tab separator stays a tab. Leading
1680/// spaces survive only under HIST_IGNORE_SPACE (c:1204-1205), which is what
1681/// keeps an ignore-space line recognisable as one.
1682///
1683/// The previous port collapsed every blank run in the raw string and trimmed
1684/// both ends, so ` print "a b"` was stored — and handed to preexec as
1685/// `$1` — as `print "a b"`.
1686pub fn histreduceblanks() {
1687 // c:1199
1688 let mut line = chline.lock().unwrap();
1689 let mut words = chwords.lock().unwrap();
1690 let wpos = (chwordpos.load(SeqCst).max(0) as usize).min(words.len());
1691 let mut buf: Vec<u8> = std::mem::take(&mut *line).into_bytes();
1692 // C reads a NUL-terminated buffer; the Rust String ends at its length.
1693 let at = |b: &[u8], i: usize| b.get(i).copied().unwrap_or(0);
1694
1695 let mut spacecount = 0usize; // c:1201
1696 if isset(HISTIGNORESPACE) {
1697 // c:1204
1698 while at(&buf, spacecount) == b' ' {
1699 spacecount += 1; // c:1205
1700 }
1701 }
1702
1703 let mut len = spacecount; // c:1207
1704 let mut i = 0usize;
1705 while i + 1 < wpos {
1706 len += (words[i + 1] - words[i]).max(0) as usize // c:1208
1707 + (i > 0 && words[i] > words[i - 1]) as usize; // c:1209
1708 i += 2;
1709 }
1710 if at(&buf, len) == 0 {
1711 // c:1211
1712 *line = String::from_utf8(buf).unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
1713 return; // c:1212
1714 }
1715
1716 /* Remember where the delimited words end */
1717 let lastptr = if wpos != 0 {
1718 // c:1215
1719 words[wpos - 1].max(0) as usize // c:1216
1720 } else {
1721 0 // c:1218
1722 };
1723
1724 let mut pos = spacecount; // c:1220
1725 i = 0;
1726 while i + 1 < wpos {
1727 let len = (words[i + 1] - words[i]).max(0) as usize; // c:1221
1728 let needblank = (i + 2 < wpos && words[i + 2] > words[i + 1]) as usize; // c:1222
1729 let from = words[i].max(0) as usize;
1730 if pos != from {
1731 // c:1223
1732 let end = (from + len + needblank).min(buf.len());
1733 if from < end {
1734 buf.copy_within(from..end, pos); // c:1224 memmove
1735 }
1736 words[i] = pos as i16; // c:1225
1737 words[i + 1] = (pos + len) as i16; // c:1226
1738 }
1739 pos += len + needblank; // c:1228
1740 i += 2;
1741 }
1742
1743 /*
1744 * A terminating comment isn't recorded as a word.
1745 * Only truncate the line if just whitespace remains.
1746 */
1747 let tail = buf.get(lastptr..).unwrap_or(&[]);
1748 let trunc_ok = tail
1749 .iter()
1750 .take_while(|&&b| b != 0)
1751 .all(|&b| b == b' ' || b == b'\t'); // c:1235-1241 inblank
1752 if trunc_ok {
1753 buf.truncate(pos); // c:1243 chline[pos] = '\0'
1754 } else if pos < lastptr {
1755 // c:1245-1248 — copy the tail from lastptr down to pos.
1756 buf.drain(pos..lastptr.min(buf.len()));
1757 }
1758 *line = String::from_utf8(buf).unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
1759}
1760
1761#[cfg(test)]
1762use reduce_blanks_test_util::{reduce_blanks, reduce_line};
1763
1764/// Test-only drivers for `histreduceblanks`, shared by the test modules below.
1765#[cfg(test)]
1766mod reduce_blanks_test_util {
1767 use super::*;
1768
1769 /// Run `histreduceblanks` over `line` whose recorded words are the
1770 /// `[start, end)` byte pairs in `words`, restoring the history globals.
1771 /// The caller must hold `test_util::global_state_lock`.
1772 pub(crate) fn reduce_blanks(line: &str, words: &[i16], ignorespace: bool) -> String {
1773 let saved = (
1774 chline.lock().unwrap().clone(),
1775 chwords.lock().unwrap().clone(),
1776 chwordpos.load(SeqCst),
1777 isset(HISTIGNORESPACE),
1778 );
1779 *chline.lock().unwrap() = line.to_string();
1780 *chwords.lock().unwrap() = words.to_vec();
1781 chwordpos.store(words.len() as i32, SeqCst);
1782 dosetopt(HISTIGNORESPACE, ignorespace as i32, 0);
1783 histreduceblanks();
1784 let out = chline.lock().unwrap().clone();
1785 *chline.lock().unwrap() = saved.0;
1786 *chwords.lock().unwrap() = saved.1;
1787 chwordpos.store(saved.2, SeqCst);
1788 dosetopt(HISTIGNORESPACE, saved.3 as i32, 0);
1789 out
1790 }
1791
1792 /// [`reduce_blanks`] with the words `histsplitwords` finds in `line`
1793 /// (blank-separated spans), HIST_IGNORE_SPACE off.
1794 pub(crate) fn reduce_line(line: &str) -> String {
1795 let words: Vec<i16> = histsplitwords(line, false)
1796 .iter()
1797 .flat_map(|&(s, e)| [s as i16, e as i16])
1798 .collect();
1799 reduce_blanks(line, &words, false)
1800 }
1801}
1802
1803/// Port of `histremovedups()` from `Src/hist.c:1254`. — C decl `histremovedups(void)`.
1804///
1805/// C body:
1806/// ```c
1807/// Histent he, next;
1808/// for (he = hist_ring; he; he = next) {
1809/// next = up_histent(he);
1810/// if (he->node.flags & HIST_DUP)
1811/// freehistnode(&he->node);
1812/// }
1813/// ```
1814///
1815/// The previous Rust port did NAME-based deduplication using a
1816/// HashSet — totally different semantic. C removes entries that
1817/// have the HIST_DUP flag set (which the history-add path sets
1818/// only when HIST_IGNORE_ALL_DUPS / HIST_IGNORE_DUPS marked the
1819/// entry as a duplicate). With HIST_IGNORE_ALL_DUPS off, identical
1820/// commands stay in the ring intentionally; the Rust port would
1821/// have aggressively pruned them anyway.
1822pub fn histremovedups() {
1823 // c:1254
1824 let mut ring = hist_ring.lock().unwrap();
1825 ring.retain(|h| (h.node.flags as u32 & HIST_DUP) == 0); // c:1259-1260
1826 let new_ct = ring.len() as i64;
1827 drop(ring);
1828 histlinect.store(new_ct, SeqCst);
1829}
1830
1831/// Port of `addhistnum()` from `Src/hist.c:1266`. — C decl `addhistnum(zlong hl, int n, int xflags)`.
1832pub fn addhistnum(hl: i64, mut n: i32, xflags: i32) -> i64 {
1833 // c:1266
1834 let dir: i32 = if n < 0 {
1835 -1
1836 } else if n > 0 {
1837 1
1838 } else {
1839 0
1840 }; // c:1266
1841 let he = gethistent(hl, dir); // c:1269
1842 let he = match he {
1843 None => return 0, // c:1271-1272
1844 Some(h) => h,
1845 };
1846 if he != hl {
1847 // c:1273
1848 n -= dir; // c:1274
1849 }
1850 let final_he = if n != 0 {
1851 // c:1275
1852 movehistent(he, n, xflags as u32) // c:1276
1853 } else {
1854 Some(he)
1855 };
1856 match final_he {
1857 // c:1277
1858 None => {
1859 if dir < 0 {
1860 // c:1278
1861 firsthist() - 1
1862 } else {
1863 curhist.load(SeqCst) + 1
1864 }
1865 }
1866 Some(h) => h, // c:1279
1867 }
1868}
1869
1870/// Port of `movehistent()` from `Src/hist.c:1284`. — C decl `movehistent(Histent he, int n, int xflags)`.
1871///
1872/// The previous Rust port omitted the `checkcurline(he)` call at
1873/// c:1298 — when the walk lands on the in-flight history entry,
1874/// C flushes the current chline/chwordpos/chwords build state into
1875/// `curline` so the caller sees fresh word data. Without it, code
1876/// that walks back to the current entry and then reads `curline`
1877/// got stale word data.
1878pub fn movehistent(start: i64, mut n: i32, xflags: u32) -> Option<i64> {
1879 // c:1284
1880 let mut cur = start;
1881 while n < 0 {
1882 // c:1286
1883 cur = up_histent(cur)?; // c:1287
1884 if let Some(e) = ring_get(cur) {
1885 if (e.node.flags as u32 & xflags) == 0 {
1886 // c:1289
1887 n += 1; // c:1290
1888 }
1889 }
1890 }
1891 while n > 0 {
1892 // c:1292
1893 cur = down_histent(cur)?; // c:1293
1894 if let Some(e) = ring_get(cur) {
1895 if (e.node.flags as u32 & xflags) == 0 {
1896 // c:1295
1897 n -= 1; // c:1296
1898 }
1899 }
1900 }
1901 // c:1298 — `checkcurline(he);` flushes in-flight build state
1902 // into `curline` if the walk landed on the active history entry.
1903 if let Some(e) = ring_get(cur) {
1904 checkcurline(&e); // c:1298
1905 }
1906 Some(cur) // c:1299
1907}
1908
1909/// Port of `up_histent()` from `Src/hist.c:1304`. — C decl `up_histent(Histent he)`.
1910pub fn up_histent(current: i64) -> Option<i64> {
1911 // c:1304
1912 let pos = ring_position(current)?; // c:1306 !he
1913 (pos + 1 < ring_len()).then(|| ring_at(pos + 1)) // c:1306 he->up == hist_ring? NULL : he->up
1914}
1915
1916/// Port of `down_histent()` from `Src/hist.c:1311`. — C decl `down_histent(Histent he)`.
1917pub fn down_histent(current: i64) -> Option<i64> {
1918 // c:1311
1919 let pos = ring_position(current)?;
1920 (pos > 0).then(|| ring_at(pos - 1)) // c:1313 he == hist_ring? NULL : he->down
1921}
1922
1923/// Port of `gethistent()` from `Src/hist.c:1318`. — C decl `gethistent(zlong ev, int nearmatch)`.
1924pub fn gethistent(ev: i64, nearmatch: i32) -> Option<i64> {
1925 // c:1318
1926 if ring_len() == 0 {
1927 return None;
1928 }
1929 if ring_get(ev).is_some() {
1930 return Some(ev);
1931 }
1932 if nearmatch == 0 {
1933 return None;
1934 }
1935 let mut best_older: Option<i64> = None;
1936 let mut best_newer: Option<i64> = None;
1937 for i in 0..ring_len() {
1938 let n = ring_at(i);
1939 if n < ev && best_older.map_or(true, |b| n > b) {
1940 best_older = Some(n);
1941 } else if n > ev && best_newer.map_or(true, |b| n < b) {
1942 best_newer = Some(n);
1943 }
1944 }
1945 if nearmatch < 0 {
1946 best_older
1947 } else {
1948 best_newer
1949 }
1950}
1951
1952/// Port of `putoldhistentryontop()` from `Src/hist.c:1347`. — C decl `putoldhistentryontop(short keep_going)`.
1953/// Rotate the next-to-expire entry to the head of the ring so that
1954/// the subsequent expire/save pass evicts it. When
1955/// `HISTEXPIREDUPSFIRST` is set and the candidate is not already a
1956/// duplicate, walk forward up to `savehistsiz` slots looking for the
1957/// next entry that IS a duplicate (so dup entries get evicted first).
1958/// `keep_going` advances the per-call cursor instead of resetting
1959/// it — used by save loops that expire multiple entries in succession.
1960pub fn putoldhistentryontop(keep_going: i32) -> i32 {
1961 // c:1347
1962
1963 thread_local! {
1964 // c:1349 — `static Histent next = NULL;` (per-evaluator cursor).
1965 static NEXT_IDX: std::cell::Cell<Option<usize>> = const { std::cell::Cell::new(None) };
1966 // c:1356 — `static zlong max_unique_ct = 0;`
1967 static MAX_UNIQUE_CT: std::cell::Cell<i64> = const { std::cell::Cell::new(0) };
1968 }
1969
1970 let mut ring = hist_ring.lock().unwrap();
1971 if ring.is_empty() {
1972 return 0;
1973 }
1974
1975 // c:1350 — `he = (keep_going || !hist_ring) ? next : hist_ring->down;`
1976 // In the Vec model the oldest entry is at the tail.
1977 let mut idx: Option<usize> = if keep_going != 0 {
1978 NEXT_IDX.with(|c| c.get())
1979 } else {
1980 Some(ring.len() - 1) // oldest = last element
1981 };
1982
1983 // c:1352-1354 — `if (he) next = he->down; else return;`
1984 let mut cur_idx = match idx {
1985 Some(i) if i < ring.len() => i,
1986 _ => return 0,
1987 };
1988 // Advance `next` to the slot one step further into the past — in the
1989 // ring that's one position closer to the head (newer).
1990 idx = if cur_idx == 0 {
1991 None
1992 } else {
1993 Some(cur_idx - 1)
1994 };
1995 NEXT_IDX.with(|c| c.set(idx));
1996
1997 // c:1355-1370 — HISTEXPIREDUPSFIRST: skip non-dups until we find one.
1998 let exp_dups_first = isset(HISTEXPIREDUPSFIRST);
1999 if exp_dups_first && (ring[cur_idx].node.flags as u32 & HIST_DUP) == 0 {
2000 if keep_going == 0 {
2001 // c:1357-1358 — `if (!keep_going) max_unique_ct = savehistsiz;`
2002 MAX_UNIQUE_CT.with(|c| c.set(savehistsiz.load(SeqCst) as i64));
2003 }
2004 loop {
2005 let cur = MAX_UNIQUE_CT.with(|c| {
2006 let v = c.get();
2007 c.set(v - 1);
2008 v
2009 });
2010 if cur <= 0 {
2011 // c:1360-1365 — give up: reset to ring head (oldest).
2012 MAX_UNIQUE_CT.with(|c| c.set(0));
2013 cur_idx = ring.len() - 1;
2014 NEXT_IDX.with(|c| {
2015 c.set(if cur_idx == 0 {
2016 None
2017 } else {
2018 Some(cur_idx - 1)
2019 })
2020 });
2021 break;
2022 }
2023 // c:1367-1368 — `he = next; next = he->down;`
2024 cur_idx = match NEXT_IDX.with(|c| c.get()) {
2025 Some(i) if i < ring.len() => i,
2026 _ => return 0,
2027 };
2028 let nxt = if cur_idx == 0 {
2029 None
2030 } else {
2031 Some(cur_idx - 1)
2032 };
2033 NEXT_IDX.with(|c| c.set(nxt));
2034 // c:1369 — `} while (!(he->node.flags & HIST_DUP));`
2035 if (ring[cur_idx].node.flags as u32 & HIST_DUP) != 0 {
2036 break;
2037 }
2038 }
2039 }
2040
2041 // c:1372-1382 — splice the chosen entry to ring head (position 0).
2042 if cur_idx < ring.len() && cur_idx != 0 {
2043 let entry = ring.remove(cur_idx);
2044 ring.insert(0, entry);
2045 }
2046 1
2047}
2048
2049/// Port of `prepnexthistent()` from `Src/hist.c:1387`. — C decl `prepnexthistent(void)`.
2050pub fn prepnexthistent() -> i64 {
2051 // c:1387
2052 let cap = histsiz.load(SeqCst);
2053 if cap > 0 && histlinect.load(SeqCst) >= cap {
2054 if let Some(oldest) = ring_oldest() {
2055 // c:1410 — `freehistdata(hist_ring, 0)`: drop the evicted
2056 // entry's histtab key. Without it every unique command text
2057 // stayed in histtab for the life of the shell.
2058 let idx = hist_ring.lock().unwrap().iter().position(|h| h.histnum == oldest);
2059 if let Some(idx) = idx {
2060 crate::ported::hashtable::freehistdata(idx, 0);
2061 }
2062 // Drop oldest from ring
2063 let mut ring = hist_ring.lock().unwrap();
2064 ring.retain(|h| h.histnum != oldest);
2065 histlinect.fetch_sub(1, SeqCst);
2066 }
2067 }
2068 let n = curhist.fetch_add(1, SeqCst) + 1;
2069 n
2070}
2071
2072/// Port of `should_ignore_line()` from `Src/hist.c:1425`. — C decl `should_ignore_line(Eprog prog)`.
2073fn should_ignore_line(prog: Option<&[u8]>) -> i32 {
2074 // c:1425
2075 let line = chline.lock().unwrap().clone();
2076 if isset(HISTIGNORESPACE) {
2077 // c:1427
2078 // c:1428 — `if (*chline == ' ' || aliasspaceflag)`. The
2079 // aliasspaceflag arm fires when the lexer expanded a
2080 // non-global alias whose body starts with a space (set at
2081 // lex.c:1930 / lex.rs LEX_ALIAS_SPACE_FLAG).
2082 let alias_space = crate::ported::lex::LEX_ALIAS_SPACE_FLAG.with(|c| c.get()) != 0;
2083 if line.starts_with(' ') || alias_space {
2084 return 1; // c:1429
2085 }
2086 }
2087 if prog.is_none() {
2088 // c:1432
2089 return 0; // c:1433
2090 }
2091 if isset(HISTNOFUNCTIONS) {
2092 // c:1435
2093 // Inspecting an Eprog requires the wordcode VM port — leave the
2094 // funcdef detection to the executor; conservatively return 0. // c:1436-1440
2095 return 0;
2096 }
2097 if isset(HISTNOSTORE) {
2098 // c:1443
2099 // getjobtext(prog, NULL) — text reconstruction also needs the
2100 // wordcode VM. Apply the simpler text-based filters on chline
2101 // for the cases the C code carves out.
2102 let mut b: &str = &line;
2103 let mut saw_builtin = false;
2104 if let Some(rest) = b.strip_prefix("builtin ") {
2105 // c:1446
2106 b = rest;
2107 saw_builtin = true;
2108 }
2109 if (b == "history" || b.starts_with("history ")) // c:1451
2110 && (saw_builtin /* || shfunctab.getnode("history").is_none() */)
2111 {
2112 return 1; // c:1453
2113 }
2114 if (b == "r" || b.starts_with("r ")) // c:1454
2115 && (saw_builtin /* || shfunctab.getnode("r").is_none() */)
2116 {
2117 return 1;
2118 }
2119 if let Some(rest) = b.strip_prefix("fc -") {
2120 // c:1457
2121 if (saw_builtin/* || shfunctab.getnode("fc").is_none() */)
2122 && rest
2123 .chars()
2124 .take_while(|c| c.is_ascii_alphabetic())
2125 .any(|c| c == 'l')
2126 {
2127 return 1; // c:1474
2128 }
2129 }
2130 }
2131 0 // c:1474
2132}
2133
2134/// Port of `hend()` from `Src/hist.c:1474`. — C decl `hend(Eprog prog)`.
2135pub fn hend(prog: Option<&[u8]>) -> i32 {
2136 // c:1474
2137 // !!! WARNING: RUST-ONLY GUARD — NO C COUNTERPART !!!
2138 // A worker-pool parse never opened a history line (hbegin returns early
2139 // on pool threads), so there is nothing to end; running the body would
2140 // read and clear the main thread's histactive and could unlinkcurline
2141 // its curhist. Same answer as the c:1494-1501 HA_NOINC exit.
2142 if crate::worker::in_worker_thread() {
2143 return 1;
2144 }
2145 let stack_pos = histsave_stack_pos.load(SeqCst); // c:1474
2146 let mut save: i32 = 1; // c:1484
2147 let mut hookret: i32 = 0;
2148
2149 // DPUTS(stophist != 2 && !(inbufflags & INP_ALIAS) && !chline, c:1487
2150 // "BUG: chline is NULL in hend()");
2151 crate::ported::signals::queue_signals(); // c:1489
2152 if (histdone.load(SeqCst) & HISTFLAG_SETTY) != 0 {
2153 // c:1491 settyinfo(&shttyinfo) — restore the cooked terminal
2154 // baseline captured by zsetterm before ZLE went raw. The
2155 // interactive read uses ZLRF_NOSETTY (input.c:418), so ZLE's own
2156 // trashzle deliberately does NOT restore the tty; the restore is
2157 // deferred to here, when the input line's history finalizes, right
2158 // before the command executes. Without it the tty stayed in ZLE
2159 // raw mode across command execution — `cat` never saw EOF on `^D`,
2160 // interactive programs got raw single-byte input. shttyinfo now
2161 // lands as utils::SHTTYINFO.
2162 if let Some(ti) = crate::ported::utils::SHTTYINFO.lock().ok().and_then(|g| *g) {
2163 crate::ported::utils::settyinfo(&ti);
2164 }
2165 }
2166 let active = histactive.load(SeqCst);
2167 if (active & HA_NOINC) == 0 {
2168 // c:1492
2169 unlinkcurline(); // c:1493
2170 }
2171 if (active & HA_NOINC) != 0 {
2172 // c:1494
2173 chline.lock().unwrap().clear(); // c:1495 zfree(chline)
2174 chwords.lock().unwrap().clear(); // c:1496 zfree(chwords)
2175 hptr.store(0, SeqCst); // c:1497
2176 histactive.store(0, SeqCst); // c:1499
2177 unqueue_signals(); // c:1500
2178 return 1; // c:1501
2179 }
2180 let cur_ignore_all = if isset(HISTIGNOREALLDUPS) { 1 } else { 0 }; // c:1503
2181 let prev_ignore_all = hist_ignore_all_dups.load(SeqCst);
2182 if prev_ignore_all != cur_ignore_all // c:1503
2183 && {
2184 hist_ignore_all_dups.store(cur_ignore_all, SeqCst); // c:1504
2185 cur_ignore_all != 0
2186 }
2187 {
2188 histremovedups(); // c:1505
2189 }
2190 // c:1507-1514 — `if (hptr) { DPUTS(hptr < chline, "History end
2191 // pointer off start of line"); *hptr = '\0'; }` — C terminates the
2192 // history line AT the cursor, so the stored line is `chline[0..hptr]`.
2193 //
2194 // NOT applied here, deliberately. zshrs's `hptr` is only a faithful
2195 // end-of-line marker on the plain lexing path; on the history-
2196 // EXPANSION path it is not. C feeds the expansion back through
2197 // `inpush(sline, INP_HIST, NULL)` + `return ingetc()` (c:976-983) so
2198 // every expanded character re-enters `ihgetc` and is re-`hwaddc`'d
2199 // (c:459), keeping `hptr` at the true end. zshrs splits lexer input
2200 // across `input::inbuf` and the Rust-only `LEX_INPUT`/`LEX_UNGET_BUF`
2201 // window, and on the INP_HIST re-read the two disagree: `hptr` ends up
2202 // BEHIND the expanded text that `ihwaddc` already wrote into `chline`.
2203 // Truncating at it therefore drops the expansion — `echo M !:6` stored
2204 // as `echo M ` instead of `echo M E` (verified: `fc -l` after
2205 // `zshrs --zsh -fis` with the truncation applied). Plain lines were
2206 // unaffected, which is what pins the gap to the INP_HIST re-read.
2207 //
2208 // Until that path keeps `hptr` honest, taking the whole buffer is the
2209 // strictly safer divergence: it can only ever include MORE than C
2210 // would, and today `hptr == chline.len()` on every non-expansion line.
2211 let chline_text = chline.lock().unwrap().clone();
2212 if !chline_text.is_empty() {
2213 // c:1515
2214 let save_errflag = errflag // c:1517
2215 .load(Ordering::Relaxed);
2216 errflag.store(0, Ordering::Relaxed); // c:1518
2217 let args = vec!["zshaddhistory".to_string(), chline_text.clone()]; // c:1520-1521
2218 // c:1522 — `callhookfunc("zshaddhistory", hookargs, 1, &hookret);`.
2219 // `hookret` is the HOOK's return value (the 4th out-param `*retval =
2220 // ret`, 0 when no hook ran), NOT callhookfunc's return (`stat`,
2221 // which is 1 whenever no hook exists). The prior port used the
2222 // `stat` return and passed NULL for retval, so with no zshaddhistory
2223 // hook (e.g. `zsh -f`) hookret was 1 → hend's `else if (hookret)`
2224 // set save=-1 (HIST_TMPSTORE) → every line was dropped by the next
2225 // command's tmpstore-purge. Pass &hookret and read the out-param.
2226 crate::ported::utils::callhookfunc(
2227 "zshaddhistory",
2228 Some(&args),
2229 1,
2230 &mut hookret as *mut i32,
2231 );
2232 let new_errflag = (errflag // c:1524-1525
2233 .load(Ordering::Relaxed)
2234 & !ERRFLAG_ERROR)
2235 | save_errflag;
2236 errflag.store(new_errflag, Ordering::Relaxed);
2237 }
2238 let hf = resolve_histfile(); // c:1528
2239 if isset(SHAREHISTORY) // c:1529
2240 && lockhistfile(hf.as_deref(), 0) == 0
2241 {
2242 readhistfile(
2243 hf.as_deref(),
2244 0, // c:1530
2245 HFILE_USE_OPTIONS as i32 | HFILE_FAST as i32,
2246 );
2247 // c:1531 — `curline.histnum = curhist+1;`. readhistfile just
2248 // appended other sessions' lines and advanced curhist; without this
2249 // the next prepnexthistent numbers this line past curline.histnum,
2250 // and loop()'s preexec check (c:Src/init.c:206) passes `$1` empty.
2251 if let Some(cl) = curline.lock().unwrap().as_mut() {
2252 cl.histnum = curhist.load(SeqCst) + 1; // c:1531
2253 }
2254 }
2255 let flag = histdone.load(SeqCst); // c:1533
2256 histdone.store(0, SeqCst); // c:1534
2257 let hptr_pos = hptr.load(SeqCst);
2258 let mut text = chline_text;
2259 if hptr_pos < 1 {
2260 // c:1535 hptr < chline + 1
2261 save = 0; // c:1536
2262 } else {
2263 if text.ends_with('\n') {
2264 // c:1538 hptr[-1] == '\n'
2265 if text.len() > 1 {
2266 // c:1539 chline[1]
2267 text.pop(); // c:1540 *--hptr = '\0'
2268 if hptr.load(SeqCst) > 0 {
2269 hptr.fetch_sub(1, SeqCst);
2270 }
2271 } else {
2272 save = 0; // c:1542
2273 }
2274 }
2275 if chwordpos.load(SeqCst) <= 2 // c:1544
2276 && hist_keep_comment.load(SeqCst) == 0
2277 {
2278 save = 0; // c:1545
2279 } else if should_ignore_line(prog) != 0 {
2280 // c:1546
2281 save = -1; // c:1547
2282 } else if hookret == 2 {
2283 // c:1548
2284 save = -2; // c:1549
2285 } else if hookret != 0 {
2286 // c:1550
2287 save = -1; // c:1551
2288 }
2289 }
2290 if (flag & (HISTFLAG_DONE | HISTFLAG_RECALL)) != 0 {
2291 // c:1553
2292 let ptr = text.clone(); // c:1556 ztrdup(chline)
2293 if (flag & (HISTFLAG_DONE | HISTFLAG_RECALL)) == HISTFLAG_DONE {
2294 // c:1557
2295 // c:1558-1560 — `zputs(ptr, shout); fputc('\n', shout);
2296 // fflush(shout);`. The echo of a `!`-expanded line goes to
2297 // `shout` — the TERMINAL (`fdopen(SHTTY)`, c:Src/init.c:748),
2298 // or stderr when there is no tty (c:738) — NEVER to stdout.
2299 // The previous port used `print!`, so every history expansion
2300 // injected an extra line into the script's captured stdout:
2301 // `zsh -fis <<<'print foo\nprint !!'` printed the expanded
2302 // `print foo` line ahead of its output where zsh prints
2303 // nothing on stdout at all.
2304 //
2305 // `shout` is resolved the way `init_shout` does it: the tty fd
2306 // when the shell has one (c:Src/init.c:748 `shout =
2307 // fdopen(SHTTY, "w")`), stderr when it does not (c:738 `shout
2308 // = stderr`). Resolved here rather than through
2309 // `extensions::shout::write` because that helper falls back to
2310 // fd 1, which is the very thing this fix is removing.
2311 let shout_fd = {
2312 let t = crate::ported::init::SHTTY.load(SeqCst);
2313 if t >= 0 {
2314 t // c:Src/init.c:748
2315 } else {
2316 2 // c:Src/init.c:738 — shout = stderr
2317 }
2318 };
2319 let mut out = crate::ported::utils::unmetafy_str(&ptr); // c:1558 zputs
2320 out.push(b'\n'); // c:1559 fputc('\n', shout)
2321 let _ = crate::ported::utils::write_loop(shout_fd, &out); // c:1560 fflush
2322 }
2323 if (flag & HISTFLAG_RECALL) != 0 {
2324 // c:1562
2325 // c:1563 — `zpushnode(bufstack, ptr)` — push the expanded
2326 // history line onto the buf-stack so ZLE recalls it on
2327 // the next prompt.
2328 crate::ported::zle::zle_main::BUFSTACK
2329 .lock()
2330 .unwrap()
2331 .insert(0, ptr.clone()); // c:1563
2332 save = 0; // c:1564
2333 }
2334 }
2335 if save != 0 || text.starts_with(' ') {
2336 // c:1568
2337 // Walk up the ring skipping HIST_FOREIGN entries; if the topmost
2338 // non-foreign entry is HIST_TMPSTORE, drop it. // c:1569-1576
2339 let mut ring = hist_ring.lock().unwrap();
2340 let mut idx: usize = 0;
2341 while idx < ring.len() && (ring[idx].node.flags as u32 & HIST_FOREIGN) != 0 {
2342 idx += 1;
2343 }
2344 if idx < ring.len() && (ring[idx].node.flags as u32 & HIST_TMPSTORE) != 0 {
2345 if idx == 0 {
2346 // c:1573 he == hist_ring
2347 // c:1574 — `curline.histnum = curhist--;`
2348 let old = curhist.fetch_sub(1, SeqCst);
2349 if let Some(cl) = curline.lock().unwrap().as_mut() {
2350 cl.histnum = old; // c:1574
2351 }
2352 }
2353 ring.remove(idx); // c:1575 freehistnode
2354 histlinect.fetch_sub(1, SeqCst);
2355 }
2356 }
2357 if save != 0 {
2358 // c:1578
2359 // chwordpos parity guard — if odd, hwend() to close. // c:1583-1587
2360 if chwordpos.load(SeqCst) % 2 != 0 {
2361 ihwend();
2362 }
2363 // Strip trailing \n which we already nulled out. // c:1589-1595
2364 let cwp = chwordpos.load(SeqCst);
2365 if cwp > 1 {
2366 let words = chwords.lock().unwrap();
2367 let last = words.get((cwp - 2) as usize).copied().unwrap_or(0);
2368 // C: !chline[chwords[chwordpos-2]] — index past end after NUL.
2369 if (last as usize) >= text.len() {
2370 // c:1590
2371 drop(words);
2372 chwordpos.fetch_sub(2, SeqCst);
2373 } else {
2374 drop(words);
2375 }
2376 if isset(HISTREDUCEBLANKS) {
2377 // c:1593
2378 // C edits `chline` in place; `text` is this port's copy of
2379 // it (with the trailing newline already dropped, c:1540),
2380 // so hand it to the buffer histreduceblanks works on and
2381 // take the result back.
2382 *chline.lock().unwrap() = text.clone();
2383 histreduceblanks(); // c:1594
2384 text = chline.lock().unwrap().clone();
2385 }
2386 }
2387 let newflags: u32 = if save == -1 {
2388 HIST_TMPSTORE
2389 }
2390 // c:1596-1601
2391 else if save == -2 {
2392 HIST_NOWRITE
2393 } else {
2394 0
2395 };
2396 let mut he_idx: Option<usize> = None;
2397 let mut overwrite_old: u32 = 0;
2398 if (isset(HISTIGNOREDUPS) || isset(HISTIGNOREALLDUPS)) // c:1602
2399 && save > 0
2400 {
2401 let ring = hist_ring.lock().unwrap();
2402 if let Some(top) = ring.first() {
2403 if top.node.nam == text {
2404 // c:1603 histstrcmp
2405 overwrite_old = top.node.flags as u32 & HIST_OLD; // c:1610
2406 he_idx = Some(0);
2407 // c:1612 — `curline.histnum = curhist;`. The line reuses
2408 // the top entry instead of taking a new number, so
2409 // curline must name that entry or preexec's `$1`
2410 // (c:Src/init.c:206) goes empty for an ignored duplicate.
2411 if let Some(cl) = curline.lock().unwrap().as_mut() {
2412 cl.histnum = curhist.load(SeqCst); // c:1612
2413 }
2414 }
2415 }
2416 }
2417 let now = std::time::SystemTime::now()
2418 .duration_since(std::time::UNIX_EPOCH)
2419 .map(|d| d.as_secs() as i64)
2420 .unwrap_or(0);
2421 let cwp = chwordpos.load(SeqCst);
2422 let chwords_snapshot: Vec<i16> = chwords.lock().unwrap().clone();
2423 let nwords = (cwp / 2) as i32;
2424 if let Some(0) = he_idx {
2425 // c:1609 reuse top
2426 let mut ring = hist_ring.lock().unwrap();
2427 if let Some(top) = ring.first_mut() {
2428 top.node.nam = text.clone(); // c:1616
2429 top.stim = now; // c:1617
2430 top.ftim = 0; // c:1618
2431 top.node.flags = (newflags | overwrite_old) as i32; // c:1619
2432 top.nwords = nwords; // c:1621
2433 top.words = if cwp > 0 {
2434 chwords_snapshot[..cwp as usize].to_vec() // c:1622-1623
2435 } else {
2436 Vec::new()
2437 };
2438 }
2439 } else {
2440 let n = prepnexthistent(); // c:1614
2441 let mut he = make_histent(n, text.clone());
2442 he.stim = now;
2443 he.ftim = 0;
2444 he.node.flags = newflags as i32;
2445 he.nwords = nwords;
2446 if cwp > 0 {
2447 he.words = chwords_snapshot[..cwp as usize].to_vec();
2448 }
2449 let mut ring = hist_ring.lock().unwrap();
2450 ring.insert(0, he);
2451 histlinect.fetch_add(1, SeqCst);
2452 drop(ring); // release ring: addhistnode re-locks hist_ring (non-reentrant)
2453 if (newflags & HIST_TMPSTORE) == 0 {
2454 // c:1625
2455 // addhistnode(histtab, he->node.nam, he) — hashtable wiring c:1626
2456 // routes through addhistnode.
2457 addhistnode(&text, n as i32);
2458 }
2459 }
2460 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
2461 // zshrs's DEFAULT history store is the SQLite index
2462 // (src/extensions/history.rs); write every accepted interactive
2463 // line. `newflags == 0` excludes HIST_TMPSTORE (leading space /
2464 // zshaddhistory reject) and HIST_NOWRITE — the same policy the
2465 // flat-$HISTFILE write applies. The $HISTFILE path elsewhere in
2466 // this fn is untouched (user HISTFILE/SAVEHIST overrides keep
2467 // full zsh-compat behavior alongside the index). Duration/exit
2468 // land later via history_sqlite_finish (preprompt).
2469 // SAVEHIST=0 means "save nothing" (zshparam), so a session that
2470 // sets it — every PTY harness does — stays out of the store.
2471 if newflags == 0
2472 && crate::ported::zsh_h::interact()
2473 && savehistsiz.load(SeqCst) > 0
2474 {
2475 crate::history::history_sqlite_add(&text);
2476 }
2477 }
2478 chline.lock().unwrap().clear(); // c:1628 zfree(chline)
2479 chwords.lock().unwrap().clear(); // c:1629 zfree(chwords)
2480 hptr.store(0, SeqCst); // c:1630
2481 histactive.store(0, SeqCst); // c:1632
2482
2483 let share = isset(SHAREHISTORY);
2484 let do_inc = if share {
2485 histfileIsLocked() != 0 // c:1636
2486 } else {
2487 isset(INCAPPENDHISTORY) // c:1637
2488 || (isset(INCAPPENDHISTORYTIME) // c:1637
2489 && histsave_stack_pos.load(SeqCst) != 0) // c:1638
2490 };
2491 if do_inc {
2492 savehistfile(
2493 hf.as_deref(),
2494 0 // c:1639
2495 | HFILE_USE_OPTIONS as i32
2496 | HFILE_FAST as i32,
2497 );
2498 }
2499 unlockhistfile(hf.as_deref().unwrap_or("")); // c:1640
2500
2501 while histsave_stack_pos.load(SeqCst) > stack_pos {
2502 // c:1645
2503 pophiststack(); // c:1646
2504 }
2505 hist_keep_comment.store(0, SeqCst); // c:1647
2506 unqueue_signals(); // c:1648
2507 if (flag & HISTFLAG_NOEXEC) != 0 || errflag.load(Ordering::Relaxed) != 0 {
2508 0 // c:1649
2509 } else {
2510 1
2511 }
2512}
2513
2514/// Port of `ihwabort()` from `Src/hist.c:1675`. — C decl `ihwabort(void)`.
2515pub fn ihwabort() {
2516 // c:1675
2517 // Rust-only: pool threads run C's `hwabort = nohwabort` arm (c:1143).
2518 if crate::worker::in_worker_thread() {
2519 return;
2520 }
2521 let pos = chwordpos.load(SeqCst);
2522 // `pos > 0` floor — see the ihwbegin note (c:1662).
2523 if pos % 2 != 0 && pos > 0 {
2524 chwordpos.fetch_sub(1, SeqCst);
2525 }
2526 hist_keep_comment.store(1, SeqCst);
2527}
2528
2529/// Port of `ihwend()` from `Src/hist.c:1686`. — C decl `ihwend(void)`.
2530///
2531/// Same gate as `ihwbegin` at c:1688:
2532/// stophist == 2 || (histactive & HA_INWORD) ||
2533/// (inbufflags & (INP_ALIAS|INP_HIST)) == INP_ALIAS
2534///
2535/// The previous Rust port missed the INP_ALIAS-only arm
2536/// (matching the `ihwbegin` divergence). Words started during
2537/// alias expansion got closed too — corrupting the chwords
2538/// table when an alias expansion ended.
2539pub fn ihwend() {
2540 // c:1686
2541 // Rust-only: pool threads run C's `hwend = nohwe` arm (c:1144).
2542 if crate::worker::in_worker_thread() {
2543 return;
2544 }
2545 let stop = stophist.load(SeqCst);
2546 let active = histactive.load(SeqCst);
2547 let inflags = crate::ported::input::inbufflags.with(|f| f.get());
2548 if stop == 2 || (active & HA_INWORD) != 0 || (inflags & (INP_ALIAS | INP_HIST)) == INP_ALIAS
2549 // c:1688
2550 {
2551 return;
2552 }
2553 // c:1691 — `if (chwordpos%2 && chline)`. Even chwordpos means we're between
2554 // words (no in-flight word to close); a NULL `chline` (empty buffer here)
2555 // means the history line has been freed. The previous port dropped the
2556 // `&& chline` half of this guard, so after `hend()` cleared the buffer a
2557 // fall-through still indexed `chwords[chwordpos-1]` out of bounds and
2558 // panicked (worker-thread OOB → poisoned the chwords mutex → main-thread
2559 // cascade in hend). Restore both halves.
2560 let pos = chwordpos.load(SeqCst);
2561 if pos % 2 == 0 || chline.lock().unwrap().is_empty() {
2562 return;
2563 }
2564 // c:1693 — `if (hptr > chline + chwords[chwordpos-1])`. The
2565 // previous Rust port used `chline.lock().unwrap().len()` for
2566 // the cursor position; that only equals `hptr - chline` when
2567 // hptr is at end of the buffer. Any lexer rewind would mis-
2568 // close the word boundary. Read the canonical `hptr` global
2569 // matching the `ihwbegin` fix at c:1658.
2570 let cur = hptr.load(SeqCst) as i16; // c:1693
2571 let mut words = chwords.lock().unwrap();
2572 let start_idx = (pos - 1) as usize;
2573 // chwordpos (atomic) and chwords (mutex) are separate primitives; parallel
2574 // lexers can leave chwordpos ahead of a chwords that another thread just
2575 // cleared. Single-threaded C indexes `chwords[chwordpos-1]` directly; guard
2576 // the bound here so the race degrades to "scrub that word" instead of an OOB.
2577 if start_idx < words.len() && cur > words[start_idx] {
2578 // c:1693
2579 let end_idx = pos as usize;
2580 if words.len() <= end_idx {
2581 words.resize(end_idx + 1, 0);
2582 }
2583 words[end_idx] = cur; // c:1694
2584 chwordpos.fetch_add(1, SeqCst);
2585 } else if chwordpos.load(SeqCst) > 0 {
2586 // c:1700 — `chwordpos--;` "scrub that last word, doesn't exist".
2587 // Guarded against a racing decrement having already reached 0 —
2588 // see the ihwbegin note (c:1662).
2589 chwordpos.fetch_sub(1, SeqCst);
2590 }
2591}
2592
2593/// Port of `histbackword()` from `Src/hist.c:1711`. — C decl `histbackword(void)`.
2594///
2595/// C body:
2596/// ```c
2597/// if (!(chwordpos%2) && chwordpos)
2598/// hptr = chline + chwords[chwordpos-1];
2599/// ```
2600///
2601/// Go back to immediately after the last word, skipping space.
2602/// Operates on the globals `chwordpos`, `chwords`, `chline`, `hptr`.
2603///
2604/// The previous Rust port had a completely different signature
2605/// (`(line: &str, pos: usize) -> usize`) and walked back through
2606/// ASCII whitespace — a stand-alone scan helper unrelated to the
2607/// C function. C operates on the GLOBAL history-line cursor via
2608/// `chwords[chwordpos-1]`, not an arbitrary text scan.
2609///
2610/// No call sites in src/ used the Rust signature, so renaming is
2611/// safe. Pin the C semantic: rewind `hptr` to the start of the
2612/// previous word when we're at a word-boundary.
2613pub fn histbackword() {
2614 // c:1711
2615 let pos = chwordpos.load(SeqCst);
2616 // c:1714 — `if (!(chwordpos%2) && chwordpos)`. Both conditions
2617 // — even (word boundary) AND non-zero.
2618 if pos % 2 == 0 && pos != 0 {
2619 // c:1714
2620 let words = chwords.lock().unwrap();
2621 let idx = (pos - 1) as usize;
2622 if idx < words.len() {
2623 // c:1715 — `hptr = chline + chwords[chwordpos-1]`. Rust's
2624 // hptr is an offset into chline; assign the cached
2625 // word-start offset (clamp negative to 0 since hptr
2626 // is AtomicUsize, matching C's pointer-arithmetic
2627 // semantic where chline + negative-offset is UB and
2628 // would have been clamped by the parser earlier).
2629 let off = (words[idx] as i32).max(0) as usize;
2630 hptr.store(off, SeqCst); // c:1715
2631 }
2632 }
2633}
2634
2635/// Port of `hwget()` from `Src/hist.c:1721`. — C decl `hwget(char **startptr)`.
2636pub fn hwget() -> Option<(i32, String)> {
2637 let pos = chwordpos.load(SeqCst);
2638 // c:1729 — DPUTS(1, "BUG: hwget() called with no words"); arm fires
2639 // when chwordpos == 0 (the C `if (!chwordpos)` branch at c:1728).
2640 if pos == 0 {
2641 DPUTS!(true, "BUG: hwget() called with no words"); // c:1729
2642 return None;
2643 }
2644 // c:1734 — DPUTS(1, "BUG: hwget() called in middle of word")
2645 if pos % 2 != 0 {
2646 DPUTS!(true, "BUG: hwget() called in middle of word"); // c:1734
2647 return None;
2648 }
2649 let words = chwords.lock().unwrap();
2650 let start_idx = (pos - 2) as usize;
2651 let end_idx = (pos - 1) as usize;
2652 if end_idx >= words.len() {
2653 return None;
2654 }
2655 let start = words[start_idx];
2656 let end = words[end_idx];
2657 let line = chline.lock().unwrap();
2658 let s = (start.max(0)) as usize;
2659 let e = (end.max(0) as usize).min(line.len());
2660 if s > e || s >= line.len() {
2661 return None;
2662 }
2663 Some((start as i32, line[s..e].to_string()))
2664}
2665
2666/// Port of `hwrep()` from `Src/hist.c:1748`. — C decl `hwrep(char *rep)`.
2667///
2668/// C body:
2669/// ```c
2670/// char *start;
2671/// hwget(&start);
2672/// if (!strcmp(rep, start)) return;
2673/// hptr = start;
2674/// chwordpos = chwordpos - 2;
2675/// hwbegin(0);
2676/// qbang = 1;
2677/// while (*rep) hwaddc(*rep++);
2678/// hwend();
2679/// ```
2680///
2681/// Replace the current history word with `rep` (the LAST recorded
2682/// word — `chwordpos - 2` indexes its start). The previous Rust
2683/// port had a completely different signature `(entry, replacement,
2684/// word_idx)` operating on a completed `histent` struct with
2685/// whitespace-split replacement. C operates on the IN-FLIGHT
2686/// `chline` / `chwordpos` / `hptr` globals — a fundamentally
2687/// different abstraction.
2688///
2689/// No call sites in src/ used the Rust signature, so renaming is
2690/// safe. Port faithfully to operate on the globals.
2691pub fn hwrep(rep: &str) {
2692 // c:1748
2693 // c:1752 — `hwget(&start)` — get the current word's start offset.
2694 let (start_off, start_text) = match hwget() {
2695 Some(v) => v,
2696 None => return,
2697 };
2698 // c:1754 — `if (!strcmp(rep, start)) return;` — no change, skip.
2699 if rep == start_text {
2700 return;
2701 }
2702 // c:1756 — `hptr = start; chwordpos -= 2;`. Rewind to the start
2703 // of the word we're rewriting; the open word slot is conceptually
2704 // re-opened by the chwordpos decrement.
2705 hptr.store(start_off.max(0) as usize, SeqCst); // c:1756
2706 chwordpos.fetch_sub(2, SeqCst); // c:1757
2707 // c:1758 — `hwbegin(0);` re-open at current hptr (no offset).
2708 ihwbegin(0);
2709 // c:1759 — `qbang = 1;` mark word as bang-bearing so subsequent
2710 // ihwaddc bang-escapes correctly.
2711 qbang.store(true, SeqCst); // c:1759
2712 // c:1760 — `while (*rep) hwaddc(*rep++);` — push each byte.
2713 for b in rep.bytes() {
2714 ihwaddc(b as i32);
2715 }
2716 // c:1761 — `hwend();` close the word slot.
2717 ihwend();
2718}
2719
2720/// Port of `hgetline()` from `Src/hist.c:1769`. — C decl `hgetline(void)`.
2721///
2722/// C body:
2723/// ```c
2724/// if (!chline || hptr == chline) return NULL;
2725/// *hptr = '\0';
2726/// ret = dupstring(chline);
2727/// hptr = chline;
2728/// chwordpos = 0;
2729/// return ret;
2730/// ```
2731///
2732/// "Get the entire current line, deleting it in the history."
2733/// Used by `pushlineoredit()` (zle_hist.c:856) to grab the
2734/// in-flight line for ZLE editing without committing it to
2735/// the history ring.
2736///
2737/// The previous Rust port took a `&histent` and returned its
2738/// name — fundamentally different from the global chline
2739/// truncation C performs. Operate on the canonical globals
2740/// (`chline`, `hptr`, `chwordpos`) and return `Option<String>`
2741/// (None for the C NULL case at c:1777).
2742///
2743/// No Rust callers used the old (entry) signature.
2744pub fn hgetline() -> Option<String> {
2745 // c:1769
2746 let hp = hptr.load(SeqCst);
2747 let line = chline.lock().unwrap();
2748 // c:1777 — `if (!chline || hptr == chline) return NULL;`
2749 if line.is_empty() || hp == 0 {
2750 return None;
2751 }
2752 // c:1779 — `*hptr = '\0';` truncate at hptr. In Rust, slice
2753 // the substring [0..hp].
2754 let truncated = if hp <= line.len() {
2755 line[..hp].to_string()
2756 } else {
2757 line.clone()
2758 };
2759 drop(line);
2760 // c:1780 — `ret = dupstring(chline);` (already a copy via
2761 // Rust .to_string()).
2762 // c:1783-1784 — reset line: hptr = 0, chwordpos = 0.
2763 hptr.store(0, SeqCst);
2764 chwordpos.store(0, SeqCst);
2765 Some(truncated) // c:1786
2766}
2767
2768/// Port of `getargspec()` from `Src/hist.c:1793`. — C decl `getargspec(int argc, int marg, int evset)`.
2769/// Reads a word-designator off the input stream via `ingetc`, returning
2770/// the resolved word index, `-1` for "no designator present" (caller
2771/// must default), or `-2` for hard error.
2772/// ```c
2773/// static int
2774/// getargspec(int argc, int marg, int evset)
2775/// {
2776/// int c, ret = -1;
2777/// if ((c = ingetc()) == '0') return 0;
2778/// if (idigit(c)) {
2779/// ret = 0;
2780/// while (idigit(c)) {
2781/// ret = ret * 10 + c - '0';
2782/// if (ret < 0) { herrflush(); zerr("no such word in event"); return -2; }
2783/// c = ingetc();
2784/// }
2785/// inungetc(c);
2786/// } else if (c == '^') ret = 1;
2787/// else if (c == '$') ret = argc;
2788/// else if (c == '%') {
2789/// if (evset) { herrflush(); zerr("Ambiguous history reference"); return -2; }
2790/// if (marg == -1) { herrflush(); zerr("%% with no previous word matched"); return -2; }
2791/// ret = marg;
2792/// } else inungetc(c);
2793/// return ret;
2794/// }
2795/// ```
2796pub fn getargspec(argc: i32, marg_arg: i32, evset: i32) -> i32 {
2797 // c:1793
2798 let mut c: i32 = ingetc() // c:1797 ingetc()
2799 .map(|ch| ch as i32)
2800 .unwrap_or(-1);
2801 let mut ret: i32 = -1; // c:1795
2802 if c == b'0' as i32 {
2803 // c:1797
2804 return 0; // c:1798
2805 }
2806 if (c as u8 as char).is_ascii_digit() {
2807 // c:1799 idigit(c)
2808 ret = 0; // c:1800
2809 while (c as u8 as char).is_ascii_digit() {
2810 // c:1801
2811 ret = ret * 10 + c - b'0' as i32; // c:1802
2812 if ret < 0 {
2813 // c:1803
2814 herrflush(); // c:1804
2815 zerr("no such word in event"); // c:1805
2816 return -2; // c:1806
2817 }
2818 c = ingetc() // c:1808 ingetc()
2819 .map(|ch| ch as i32)
2820 .unwrap_or(-1);
2821 }
2822 if let Some(ch) = char::from_u32(c as u32) {
2823 // c:1810 inungetc(c)
2824 inungetc(ch);
2825 }
2826 } else if c == b'^' as i32 {
2827 // c:1811
2828 ret = 1; // c:1812
2829 } else if c == b'$' as i32 {
2830 // c:1813
2831 ret = argc; // c:1814
2832 } else if c == b'%' as i32 {
2833 // c:1815
2834 if evset != 0 {
2835 // c:1816
2836 herrflush(); // c:1817
2837 zerr("Ambiguous history reference"); // c:1818
2838 return -2; // c:1819
2839 }
2840 if marg_arg == -1 {
2841 // c:1821
2842 herrflush(); // c:1822
2843 zerr("%% with no previous word matched"); // c:1823
2844 return -2; // c:1824
2845 }
2846 ret = marg_arg; // c:1826
2847 } else {
2848 // c:1827
2849 if let Some(ch) = char::from_u32(c as u32) {
2850 // c:1828 inungetc(c)
2851 inungetc(ch);
2852 }
2853 }
2854 ret // c:1829
2855}
2856
2857/// Port of `hconsearch()` from `Src/hist.c:1836`. — C decl `hconsearch(char *str, int *marg)`.
2858///
2859/// C body (c:1836-1853):
2860/// ```c
2861/// for (he = up_histent(hist_ring); he; he = up_histent(he)) {
2862/// if (he->node.flags & HIST_FOREIGN) continue;
2863/// if ((s = strstr(he->node.nam, str))) {
2864/// int pos = s - he->node.nam;
2865/// while (t1 < he->nwords && he->words[2*t1] <= pos)
2866/// t1++;
2867/// *marg = t1 - 1;
2868/// return he->histnum;
2869/// }
2870/// }
2871/// return -1;
2872/// ```
2873///
2874/// Two divergences in the previous Rust port:
2875/// 1. Took an extra `start: Option<i64>` parameter that has no
2876/// C counterpart — C always starts from `up_histent(hist_ring)`.
2877/// 2. Dropped the `*marg` output — caller code at c:686 needs
2878/// the matching word index to update `marg`/`hsubl` state.
2879/// Without it, the caller hardcoded `margbox=0` losing the
2880/// actual word position.
2881///
2882/// Rust adaptation: returns `Option<(histnum, marg)>` instead of
2883/// the C in/out pair; None mirrors the C `return -1` miss path.
2884/// WARNING: Rust returns a tuple — C uses an out-pointer for marg.
2885pub fn hconsearch(needle: &str) -> Option<(i64, i32)> {
2886 // c:1836
2887 // c:1842 — `for (he = up_histent(hist_ring); he; he = up_histent(he))`.
2888 // The C `hist_ring` is the doubly-linked-list sentinel; iterating
2889 // `up_histent(hist_ring)` walks from the newest real entry toward
2890 // older ones. Rust storage is a Vec with newest at position 0;
2891 // walk positions 0..ring_len for the same effect.
2892 let ring = hist_ring.lock().expect("hist_ring poisoned");
2893 for entry in ring.iter() {
2894 if (entry.node.flags as u32 & HIST_FOREIGN) != 0 {
2895 // c:1843
2896 continue; // c:1844
2897 }
2898 if let Some(pos) = entry.node.nam.find(needle) {
2899 // c:1845 strstr
2900 // c:1846 — `int pos = s - he->node.nam;`
2901 let mut t1: i32 = 0; // c:1838
2902 while t1 < entry.nwords {
2903 // c:1847
2904 let slot_pos = entry.words.get((2 * t1) as usize).copied().unwrap_or(0) as usize;
2905 if slot_pos > pos {
2906 // c:1847 he->words[2*t1] <= pos
2907 break;
2908 }
2909 t1 += 1; // c:1848
2910 }
2911 return Some((entry.histnum, t1 - 1)); // c:1849-1850
2912 }
2913 }
2914 None // c:1853
2915}
2916
2917/// Port of `hcomsearch()` from `Src/hist.c:1860`. — C decl `hcomsearch(char *str)`.
2918pub fn hcomsearch(prefix: &str) -> Option<i64> {
2919 let mut cur = curhist.load(SeqCst);
2920 while let Some(prev) = up_histent(cur) {
2921 cur = prev;
2922 if let Some(entry) = ring_get(cur) {
2923 if (entry.node.flags as u32 & HIST_FOREIGN) != 0 {
2924 continue;
2925 }
2926 if entry.node.nam.starts_with(prefix) {
2927 return Some(cur);
2928 }
2929 }
2930 }
2931 None
2932}
2933
2934/// Port of `chabspath()` from `Src/hist.c:1878`. — C decl `chabspath(char **junkptr)`.
2935pub fn chabspath(input: &str) -> Option<String> {
2936 if input.is_empty() {
2937 return Some(String::new());
2938 }
2939 let mut path = if !input.starts_with('/') {
2940 let cwd = std::env::current_dir().ok()?;
2941 let cwd_s = cwd.to_string_lossy().into_owned();
2942 if cwd_s.ends_with('/') {
2943 format!("{}{}", cwd_s, input)
2944 } else {
2945 format!("{}/{}", cwd_s, input)
2946 }
2947 } else {
2948 input.to_string()
2949 };
2950 let chars: Vec<char> = path.chars().collect();
2951 let mut out: Vec<char> = Vec::with_capacity(chars.len());
2952 let mut i = 0;
2953 while i < chars.len() {
2954 let c = chars[i];
2955 if c == '/' {
2956 out.push('/');
2957 i += 1;
2958 while i < chars.len() && chars[i] == '/' {
2959 i += 1;
2960 }
2961 } else if c == '.'
2962 && i + 1 < chars.len()
2963 && chars[i + 1] == '.'
2964 && (i + 2 == chars.len() || chars[i + 2] == '/')
2965 {
2966 if out.len() <= 1 {
2967 // c:1945 — `else return 0;` is reached ONLY when dest is still
2968 // at the very start (nothing accumulated at all).
2969 if out.is_empty() {
2970 return None;
2971 }
2972 // c:1941-1944 — `else if (dest == *junkptr + 1) { current += 2; }`
2973 // At the ROOT, `..` is SKIPPED: it is consumed and nothing is
2974 // written, so `/..` collapses to `/` (POSIX: the parent of / is
2975 // /). Returning an error here instead made chabspath fail, and
2976 // the modifier dispatch reports a failed chabspath as
2977 // `unrecognized modifier 'a'` — so `${f:a}` on `/..` errored
2978 // out where zsh prints `/`. `:A` was unaffected because it
2979 // reaches realpath(3), which resolves `/..` to `/` on its own,
2980 // which is why only `:a` showed it.
2981 if out == ['/'] {
2982 // c:1943 — `current += 2;` and NOTHING else: the `..` is
2983 // consumed but the '/' that follows it is NOT. That slash
2984 // is then copied by the '/' arm on top of the root already
2985 // in `out`, so zsh really does yield a DOUBLE slash here:
2986 // /../x :a → //x (not /x)
2987 // /.. :a → /
2988 // C's own comment flags it ("This might break with
2989 // Cygwin's leading double slashes?"). Consuming the slash
2990 // as well would be the tidier-looking answer and the wrong
2991 // one.
2992 i += 2;
2993 continue;
2994 }
2995 out.push('.');
2996 out.push('.');
2997 } else if out.len() >= 3 && &out[out.len() - 3..] == &['.', '.', '/'] {
2998 out.push('.');
2999 out.push('.');
3000 } else {
3001 if out.last() == Some(&'/') && out.len() > 1 {
3002 out.pop();
3003 }
3004 while out.last().map(|c| *c != '/').unwrap_or(false) {
3005 out.pop();
3006 }
3007 }
3008 i += 2;
3009 if i < chars.len() && chars[i] == '/' {
3010 i += 1;
3011 }
3012 } else if c == '.' && (i + 1 == chars.len() || chars[i + 1] == '/') {
3013 i += 1;
3014 while i < chars.len() && chars[i] == '/' {
3015 i += 1;
3016 }
3017 } else {
3018 out.push(c);
3019 i += 1;
3020 }
3021 }
3022 while out.len() > 1 && out.last() == Some(&'/') {
3023 out.pop();
3024 }
3025 path = out.into_iter().collect();
3026 if path.is_empty() {
3027 Some("/".to_string())
3028 } else {
3029 Some(path)
3030 }
3031}
3032
3033/// Port of `chrealpath()` from `Src/hist.c:1971`. — C decl `chrealpath(char **junkptr, char mode, int use_heap)`.
3034/// Mode 'A' → chabspath then realpath; mode 'P' → realpath only.
3035/// Handles non-existent paths by walking parent prefixes until one
3036/// resolves, then re-appending the remaining tail (matches the C
3037/// fallback at c:2027-2030).
3038///
3039/// Signature mirrors C (Rule B): `path`, `mode` ('A' or 'P'),
3040/// `use_heap` (ignored — Rust strings are heap by default).
3041pub fn chrealpath(path: &str, mode: u8, _use_heap: bool) -> Option<String> {
3042 // c:1971
3043 // c:1983 — DPUTS1(mode != 'A' && mode != 'P', "chrealpath: mode='%c' is invalid", mode)
3044 DPUTS1!(
3045 // c:1983
3046 mode != b'A' && mode != b'P', // c:1983
3047 "chrealpath: mode='{}' is invalid",
3048 mode as char // c:1983
3049 );
3050 // c:1985-1986 — if (!**junkptr) return 1; (empty input is success)
3051 if path.is_empty() {
3052 return Some(String::new());
3053 }
3054
3055 // c:1988-1990 — if (mode == 'A') chabspath; here we accept that
3056 // callers wanting absolutize-first do it themselves.
3057 // Caller may pass either mode; the partial-realpath
3058 // body is the only mode-independent semantic.
3059
3060 // c:1999-2000 — if (**junkptr != '/') return 0;
3061 // Only absolute paths are valid input.
3062 if !path.starts_with('/') {
3063 return None;
3064 }
3065
3066 // c:2002-2003 — untokenize + unmetafy. Rust strings already UTF-8.
3067
3068 // c:2008-2030 — loop: try realpath(p); on failure walk `nonreal`
3069 // backward to the previous '/' and retry the shorter
3070 // prefix. The bytes after each '/' boundary are
3071 // recorded as the non-real tail to re-splice later.
3072 let bytes = path.as_bytes();
3073 let mut prefix_end = bytes.len();
3074 let mut real: Option<String> = None;
3075 loop {
3076 let trial = &path[..prefix_end];
3077 if let Ok(canonical) = std::fs::canonicalize(trial) {
3078 real = Some(canonical.to_string_lossy().into_owned());
3079 break;
3080 }
3081 // walk `prefix_end` back to the previous '/'
3082 let mut i = prefix_end.saturating_sub(1);
3083 while i > 0 && bytes[i] != b'/' {
3084 i -= 1;
3085 }
3086 if i == 0 {
3087 // c:2020-2024 — `if (nonreal == *junkptr) { real = NULL; break; }`
3088 // Nothing along the path resolved. C's `nonreal` has walked back to
3089 // the START of the string, so after the '/'-restoration loop below
3090 // it spans the WHOLE input and c:2047 hands that back untouched.
3091 // Leaving prefix_end where it was instead returned a TRUNCATED
3092 // suffix: `/a/b/../c` came back as `/b/../c`.
3093 prefix_end = 0;
3094 break;
3095 }
3096 prefix_end = i;
3097 }
3098
3099 // c:2032-2037 — the nul-bytes inside `nonreal` get rewritten back to
3100 // '/'. In Rust we never overwrite, so the tail is just
3101 // the suffix from `prefix_end..`.
3102 let tail = &path[prefix_end..];
3103
3104 // c:2040-2048 — splice real + tail (or just tail when realpath fails
3105 // on every prefix).
3106 match real {
3107 Some(r) => Some(format!("{}{}", r, tail)),
3108 None => Some(tail.to_string()),
3109 }
3110}
3111
3112/// Port of `remtpath()` from `Src/hist.c:2056`. — C decl `remtpath(char **junkptr, int count)`.
3113/// Implements the `:h` modifier (and `:hN` via `digitcount()`).
3114///
3115/// The C body walks one cursor (`str`) backwards from `strend(*junkptr)`
3116/// and only ever NUL-terminates the buffer in the two branches that
3117/// actually cut (c:2090 and c:2115). Every other exit returns the buffer
3118/// UNCHANGED — which is why `${x:h4}` on `/a/b/c/` keeps its trailing
3119/// slash: the trailing-slash trim at c:2060-2062 moves the cursor but
3120/// writes nothing. This port keeps that shape (cursor + late cut) rather
3121/// than pre-trimming the string, because pre-trimming leaks into the
3122/// "full string needed" exits.
3123///
3124/// The Rust signature returns the resulting string instead of mutating
3125/// `*junkptr`, so C's in-place `*str = '\0'` becomes a `s[..idx]` slice.
3126///
3127/// !!! RUST-ONLY DETAIL !!! C reads `str[1]` / `**junkptr` freely because
3128/// the buffer is NUL-terminated; `byte()` below reproduces that by
3129/// yielding `'\0'` for any index at/after the end. `strend()`
3130/// (Src/string.c:196) returns the string itself when empty, so the
3131/// cursor starts at index 0 — i.e. on that implicit NUL — for `""`.
3132pub fn remtpath(s: &str, count: i32) -> String {
3133 // c:2056
3134 let b = s.as_bytes();
3135 // Nul-aware byte accessor: mirrors C reading through the terminator.
3136 let byte = |i: isize| -> char {
3137 if i >= 0 && (i as usize) < b.len() {
3138 b[i as usize] as char
3139 } else {
3140 '\0'
3141 }
3142 };
3143 // c:2058 — `char *str = strend(*junkptr);` (last char, or the string
3144 // itself when empty — Src/string.c:196).
3145 let mut str_: isize = if b.is_empty() {
3146 0
3147 } else {
3148 b.len() as isize - 1
3149 };
3150
3151 // c:2060 — /* ignore trailing slashes */
3152 while str_ >= 0 && IS_DIRSEP(byte(str_)) {
3153 str_ -= 1; // c:2062
3154 }
3155 if count == 0 {
3156 // c:2063
3157 // c:2064 — /* skip filename */
3158 while str_ >= 0 && !IS_DIRSEP(byte(str_)) {
3159 str_ -= 1; // c:2066
3160 }
3161 }
3162 if str_ < 0 {
3163 // c:2068
3164 // c:2069-2072 — `/` vs `.` decided by the FIRST byte of the
3165 // ORIGINAL buffer. Empty input's first byte is the NUL, not a
3166 // dirsep, so C yields `.`.
3167 return if IS_DIRSEP(byte(0)) { "/" } else { "." }.to_string();
3168 }
3169
3170 if count != 0 {
3171 // c:2076
3172 // c:2078-2082 — /* Return this many components, so start from the
3173 // front. Leading slash counts as one component, consistent with
3174 // behaviour of repeated applications of :h. */
3175 let mut count = count;
3176 let mut strp: isize = 0; // c:2083 `char *strp = *junkptr;`
3177 while strp < str_ {
3178 // c:2084
3179 if IS_DIRSEP(byte(strp)) {
3180 // c:2085
3181 count -= 1;
3182 if count <= 0 {
3183 // c:2086
3184 if strp == 0 {
3185 // c:2087 `if (strp == *junkptr)`
3186 strp += 1; // c:2088
3187 }
3188 return s[..strp as usize].to_string(); // c:2089 `*strp = '\0'`
3189 }
3190 // c:2093 — /* Count consecutive separators as one */
3191 while IS_DIRSEP(byte(strp + 1)) {
3192 strp += 1; // c:2095
3193 }
3194 }
3195 strp += 1; // c:2097
3196 }
3197
3198 // c:2100 — /* Full string needed */ — nothing was NUL'd, so the
3199 // caller sees the ORIGINAL buffer, trailing slashes included.
3200 return s.to_string(); // c:2101
3201 }
3202
3203 // c:2104 — /* repeated slashes are considered like a single slash */
3204 while str_ > 0 && IS_DIRSEP(byte(str_ - 1)) {
3205 str_ -= 1; // c:2106
3206 }
3207 // c:2107 — /* never erase the root slash */
3208 if str_ == 0 {
3209 // c:2108
3210 str_ += 1; // c:2109
3211 // c:2110-2112 — /* Leading doubled slashes (`//') have a special
3212 // meaning on cygwin and some old flavor of UNIX, so we do not
3213 // assimilate them to a single slash. However a greater number
3214 // is ok to squeeze. */
3215 if IS_DIRSEP(byte(str_)) && !IS_DIRSEP(byte(str_ + 1)) {
3216 str_ += 1; // c:2114
3217 }
3218 }
3219 s[..str_ as usize].to_string() // c:2115 `*str = '\0'`
3220}
3221
3222/// Port of `remtext()` from `Src/hist.c:2122`. — C decl `remtext(char **junkptr)`.
3223///
3224/// C body (c:2122-2132):
3225/// ```c
3226/// for (str = strend(*junkptr); str >= *junkptr && !IS_DIRSEP(*str); --str)
3227/// if (*str == '.') {
3228/// *str = '\0';
3229/// return 1;
3230/// }
3231/// return 0;
3232/// ```
3233///
3234/// Walks backward from the end of the string looking for `.`,
3235/// stopping at the rightmost `/` or the start of the string. When
3236/// found, truncates the string at the `.` (drops `.` AND the
3237/// extension). The Rust signature returns the truncated string
3238/// rather than mutating + returning int.
3239///
3240/// Edge cases pinned by C: `.hidden` → `""`, `foo/.bashrc` → `foo/`.
3241/// The previous Rust port guarded with `dot_pos > 0` which kept the
3242/// leading-dot files unchanged — a divergence from C's behavior.
3243pub fn remtext(s: &str) -> String {
3244 // c:2122
3245 // c:2126 — find the rightmost `/` to bound the scan (C walks back
3246 // from end until it hits a dirsep). Everything after that slash
3247 // (or the whole string if no slash) is the basename region.
3248 let (prefix, basename) = match s.rfind('/') {
3249 Some(i) => (&s[..=i], &s[i + 1..]),
3250 None => ("", s),
3251 };
3252 // c:2127 — `if (*str == '.')`. Find rightmost `.` in basename;
3253 // truncate there. C makes no exception for leading dot.
3254 if let Some(dot_pos) = basename.rfind('.') {
3255 // c:2127
3256 return format!("{}{}", prefix, &basename[..dot_pos]); // c:2128
3257 }
3258 s.to_string() // c:2131
3259}
3260
3261/// Port of `rembutext()` from `Src/hist.c:2136`. — C decl `rembutext(char **junkptr)`.
3262pub fn rembutext(s: &str) -> String {
3263 // c:2136
3264 if let Some(slash_pos) = s.rfind('/') {
3265 let after_slash = &s[slash_pos + 1..];
3266 if let Some(dot_pos) = after_slash.rfind('.') {
3267 return after_slash[dot_pos + 1..].to_string();
3268 }
3269 return String::new();
3270 }
3271 if let Some(dot_pos) = s.rfind('.') {
3272 return s[dot_pos + 1..].to_string();
3273 }
3274 String::new()
3275}
3276
3277/// Port of `remlpaths()` from `Src/hist.c:2152`. — C decl `remlpaths(char **junkptr, int count)`.
3278/// Keeps the last `count` path components (the `:t` modifier family).
3279///
3280/// The C body walks a single cursor backwards from `strend(*junkptr)`
3281/// and, on the separator that ends the wanted range, cuts in place
3282/// (`*str = '\0'; *junkptr = dupstring(str + 1)`). This port keeps that
3283/// shape — one backward byte scan, one allocation for the result — so
3284/// the modifier costs a scan of the string instead of a split + Vec +
3285/// join per element.
3286///
3287/// C behavior (c:2165-2172): when `--count > 0` and the cursor has
3288/// already reached the start of the string (`str > *junkptr` fails),
3289/// the function returns 1 ("whole string needed") WITHOUT cutting, so
3290/// the caller observes the input (leading slash included) apart from
3291/// the trailing-separator trim c:2156-2161 already applied in place.
3292/// c:2182-2185 (`str <= *junkptr` → `return 0`) reaches the same
3293/// result by a different route.
3294///
3295/// `count == 0` needs no special case: C tests `--count > 0`, which is
3296/// false for both 0 and 1, so both cut at the first separator from the
3297/// right — c:574's `digitcount()` default of 0 means "one component".
3298pub fn remlpaths(s: &str, count: i32) -> String {
3299 // c:2152
3300 let b = s.as_bytes();
3301 // c:2154 — `char *str = strend(*junkptr);` — the LAST character
3302 // (string.c:196), or the string itself when empty. The cursor is an
3303 // isize because C lets it walk one step before `*junkptr`.
3304 if b.is_empty() {
3305 return String::new();
3306 }
3307 let mut count = count;
3308 let mut str_ = b.len() as isize - 1;
3309 // `end` is where C's in-place NUL lands: everything from `end` on is
3310 // already cut off the buffer the caller sees.
3311 let mut end = b.len();
3312
3313 // c:2156-2161 — trailing separators are trimmed off the input first.
3314 if IS_DIRSEP(b[str_ as usize] as char) {
3315 while str_ >= 0 && IS_DIRSEP(b[str_ as usize] as char) {
3316 str_ -= 1; // c:2159
3317 }
3318 end = (str_ + 1) as usize; // c:2160 `str[1] = '\0'`
3319 }
3320
3321 loop {
3322 // c:2162-2163
3323 while str_ >= 0 {
3324 // c:2164
3325 if IS_DIRSEP(b[str_ as usize] as char) {
3326 count -= 1;
3327 if count > 0 {
3328 // c:2165 `if (--count > 0)`
3329 if str_ > 0 {
3330 // c:2166-2168 — more components wanted; keep scanning
3331 // left from before this separator.
3332 str_ -= 1;
3333 break;
3334 }
3335 // c:2169-2172 — whole string needed.
3336 return s[..end].to_string();
3337 }
3338 // c:2174-2176 — cut here; the result starts after the
3339 // separator, which is why the leading slash is dropped.
3340 return s[str_ as usize + 1..end].to_string();
3341 }
3342 str_ -= 1;
3343 }
3344 // c:2179-2181 — count consecutive separators as 1.
3345 while str_ >= 0 && IS_DIRSEP(b[str_ as usize] as char) {
3346 str_ -= 1;
3347 }
3348 if str_ <= 0 {
3349 break; // c:2182-2183
3350 }
3351 }
3352 // c:2185 — `return 0`: no cut, the caller keeps the (trimmed) input.
3353 s[..end].to_string()
3354}
3355
3356/// Port of `casemodify()` from `Src/hist.c:2196`. — C decl `casemodify(char *str, int how)`.
3357///
3358/// Both arms call the C library's classification and mapping functions,
3359/// as C does: the wide-character `iswupper`/`iswlower`/`iswalnum`/
3360/// `towupper`/`towlower` with MULTIBYTE set, the single-byte
3361/// `isupper`/`islower`/`toupper`/`tolower` without it. Which letters fold,
3362/// and to what, is locale state (`LC_CTYPE`), not a property of Unicode the
3363/// port could re-derive: titlecase U+01C5 `Dž` folds both ways under glibc
3364/// and not at all under macOS libc, matching each platform's zsh.
3365pub fn casemodify(s: &str, how: i32) -> String {
3366 // c:2196
3367 // c:2200 — `int nextupper = 1;`. Start expecting a leading uppercase.
3368 let mut nextupper = true;
3369 if !crate::ported::options::opt_state_get("multibyte").unwrap_or(true) {
3370 // c:2276-2320 — the byte loop. C calls the C library's single-byte
3371 // `isupper`/`tolower`/`islower`/`toupper` directly, so this calls the
3372 // same functions: they are locale state, not a property anything here
3373 // could re-derive, and zshrs already runs C's `setlocale(LC_ALL, "")`
3374 // (c:Src/init.c:1208) at startup, so both shells read one table.
3375 //
3376 // Those calls are why byte mode is not just "the same fold on smaller
3377 // units". Under en_US.UTF-8 on macOS `toupper(0xb5)` answers 0x39c —
3378 // wider than the `char` C stores it in — so `${(U)}` on a value ending
3379 // `ce b5` emits `ce 9c`. That truncation is upstream behavior, and
3380 // storing the low byte reproduces it rather than papering over it.
3381 let bytes = crate::ported::utils::unmetafy_str(s);
3382 let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
3383 // The result so far: byte runs between bare tokens, in the port's
3384 // `String` form (valid text verbatim, other bytes as `Meta` pairs).
3385 let mut result = String::with_capacity(bytes.len());
3386 let mut pending_meta = false;
3387 // c:2276 — `while (*str)`. The stream is already demetafied, which is
3388 // C's c:2279-2281 `if (*str == Meta) { c = str[1] ^ 32; str += 2; }`.
3389 for &b in &bytes {
3390 let mut c = b as i32;
3391 let mut modified = false; // c:2278 `int mod = 0;`
3392 match how {
3393 x if x == CASMOD_LOWER => {
3394 // c:2285-2290 — `if (isupper(c)) { c = tolower(c); mod = 1; }`
3395 if unsafe { libc::isupper(c) } != 0 {
3396 c = unsafe { libc::tolower(c) };
3397 modified = true;
3398 }
3399 }
3400 x if x == CASMOD_UPPER => {
3401 // c:2292-2297 — `if (islower(c)) { c = toupper(c); mod = 1; }`
3402 if unsafe { libc::islower(c) } != 0 {
3403 c = unsafe { libc::toupper(c) };
3404 modified = true;
3405 }
3406 }
3407 x if x == CASMOD_CAPS => {
3408 // c:2299-2313 — word-boundary tracking on zsh's own
3409 // `ialnum` typtab (c:2301), not the libc class.
3410 if !crate::ported::ztype_h::ialnum(b) {
3411 nextupper = true;
3412 } else if nextupper {
3413 if unsafe { libc::islower(c) } != 0 {
3414 c = unsafe { libc::toupper(c) };
3415 modified = true;
3416 }
3417 nextupper = false;
3418 } else if unsafe { libc::isupper(c) } != 0 {
3419 c = unsafe { libc::tolower(c) };
3420 modified = true;
3421 }
3422 }
3423 _ /* CASMOD_NONE */ => {}
3424 }
3425 // c:2315-2319 — `if (mod && imeta(c)) { *ptr2++ = Meta; *ptr2++ =
3426 // c ^ 32; } else *ptr2++ = c;`. C is re-establishing METAFICATION
3427 // on the byte it just folded, because its output buffer is a
3428 // metafied `char *`. This buffer is the LOGICAL byte stream, so
3429 // the escape is applied once, at the conversion below — emitting
3430 // the pair here as well would metafy it twice and leak `83 bc`
3431 // where zsh prints `9c`.
3432 //
3433 // C stores the int into a `char`, so a mapping that answers wider
3434 // than a byte keeps only its low byte (`toupper(0xb5)` → 0x39c →
3435 // 0x9c); the mask reproduces that.
3436 //
3437 // An UNMODIFIED byte goes out bare (`else *ptr2++ = c`), even when
3438 // it is an `imeta` data byte the input carried as `Meta` +
3439 // `byte ^ 32`. In C's metafied buffer a bare `itok` byte (0x84..0xa1) IS
3440 // a token (`Marker`, 0xa2, is `imeta` but no token and prints as
3441 // itself), so `${(L)v}` on a value holding one hands later stages a
3442 // token where the input held data (zsh: `unsetopt multibyte;
3443 // v=一二三; print ${(L)v}` prints `一\xe4\xba$\xe4\xb8(`: the
3444 // String and Outpar tokens untokenize). A bare `Meta` makes the
3445 // next stored byte read as escaped. Both are kept so the result
3446 // reads as zsh's does.
3447 let byte = (c & 0xff) as u8;
3448 if pending_meta {
3449 // c:Src/utils.c unmetafy — `Meta` + y reads as y ^ 32. A bare
3450 // token y is untokenized first (c:Src/exec.c untokenize, via
3451 // `ztokens`), so the pair reads as that glyph ^ 32: zsh turns
3452 // the data bytes 83 95 into 1e (`>` ^ 32), not b5.
3453 pending_meta = false;
3454 let glyph = (!modified && crate::ported::ztype_h::itok(byte))
3455 .then(|| crate::ported::glob::ZTOKENS.as_bytes().get((byte - 0x84) as usize))
3456 .flatten();
3457 out.push(glyph.copied().unwrap_or(byte) ^ 32);
3458 } else if !modified && byte == crate::ported::zsh_h::Meta {
3459 pending_meta = true;
3460 } else if let Some(tok) =
3461 (!modified && crate::ported::ztype_h::itok(byte))
3462 .then(|| crate::token_char::token_char_from_byte(byte))
3463 .flatten()
3464 {
3465 result.push_str(&crate::script_bytes::decode_script_bytes(&out));
3466 out.clear();
3467 result.push(tok);
3468 } else {
3469 out.push(byte);
3470 }
3471 }
3472 // c:2321-2322 — `*ptr2 = '\0'; return str2;`. Back to the port's
3473 // `String` form: well-formed text verbatim, anything else escaped as
3474 // `Meta` + `byte ^ 32`, the representation `unmetafy_str` decodes.
3475 // This is a whole-buffer conversion, not a per-unit one, so it cannot
3476 // come from `mb_metacharlenconv` — C has the same split, building the
3477 // buffer byte-by-byte in the loop and returning it once here.
3478 result.push_str(&crate::script_bytes::decode_script_bytes(&out));
3479 return result;
3480 }
3481 // c:2204-2275 — the wide-character loop.
3482 let bytes = crate::ported::utils::unmetafy_str(s);
3483 let mut out: Vec<u8> = Vec::with_capacity(2 * bytes.len());
3484 let mut pos = 0;
3485 while pos < bytes.len() {
3486 // c:2212 — `int len = mb_metacharlenconv(str, &wc), mod = 0, len2;`
3487 let (len, wc, _) = crate::ported::utils::mb_metacharlenconv(&bytes[pos..]);
3488 let len = len.max(1);
3489 let unit = &bytes[pos..pos + len];
3490 pos += len;
3491 // c:2218-2224 — `if (wc == WEOF)`: copy the bytes, not alphanumeric.
3492 let Some(c) = wc else {
3493 out.extend_from_slice(unit);
3494 nextupper = true;
3495 continue;
3496 };
3497 let mut w = c as u32 as WintT;
3498 let mut modified = false; // c:2212 `mod = 0`
3499 // SAFETY: the wide-character class/mapping functions take any
3500 // `wint_t` and only read the current locale.
3501 unsafe {
3502 match how {
3503 x if x == CASMOD_LOWER => {
3504 // c:2226-2229 — `if (iswupper(wc)) { wc = towlower(wc); mod = 1; }`
3505 if iswupper(w) != 0 {
3506 w = towlower(w);
3507 modified = true;
3508 }
3509 }
3510 x if x == CASMOD_UPPER => {
3511 // c:2232-2236 — `if (iswlower(wc)) { wc = towupper(wc); mod = 1; }`
3512 if iswlower(w) != 0 {
3513 w = towupper(w);
3514 modified = true;
3515 }
3516 }
3517 x if x == CASMOD_CAPS => {
3518 // c:2241-2242 — `if (IS_COMBINING(wc)) break;`
3519 // (`wc != 0 && WCWIDTH(wc) == 0`, Src/zsh.h:3343).
3520 let combining = (c as u32) != 0
3521 && unicode_width::UnicodeWidthChar::width(c).unwrap_or(1) == 0;
3522 if combining {
3523 } else if iswalnum(w) == 0 {
3524 // c:2243-2244
3525 nextupper = true;
3526 } else if nextupper {
3527 // c:2245-2250
3528 if iswlower(w) != 0 {
3529 w = towupper(w);
3530 modified = true;
3531 }
3532 nextupper = false;
3533 } else if iswupper(w) != 0 {
3534 // c:2251-2254
3535 w = towlower(w);
3536 modified = true;
3537 }
3538 }
3539 _ /* CASMOD_NONE */ => {}
3540 }
3541 }
3542 // c:2258-2272 — `if (mod && (len2 = wcrtomb(mbstr, wc, &ps)) > 0)`
3543 // emits the mapped character's bytes; otherwise the original bytes.
3544 let mut mb = [0 as libc::c_char; 16];
3545 let mut ps = crate::ported::utils::MBSTATE_ZERO;
3546 let len2 = if modified {
3547 unsafe {
3548 crate::ported::utils::wcrtomb(
3549 mb.as_mut_ptr(),
3550 w as libc::wchar_t,
3551 &mut ps as *mut _ as *mut libc::c_void,
3552 )
3553 }
3554 } else {
3555 0
3556 };
3557 if modified && len2 > 0 && len2 != usize::MAX {
3558 out.extend(mb[..len2].iter().map(|&b| b as u8));
3559 } else {
3560 out.extend_from_slice(unit);
3561 }
3562 }
3563 // c:2321-2322 — `*ptr2 = '\0'; return str2;`. Valid text stays text; a
3564 // byte that decodes as nothing (a WEOF unit copied through) goes back to
3565 // its `Meta` + `byte ^ 32` form, which `unmetafy_str` reverses.
3566 crate::script_bytes::decode_script_bytes(&out)
3567}
3568
3569
3570/// libc `wint_t`: `unsigned int` on glibc/musl, `__int32_t` on Darwin.
3571#[cfg(target_vendor = "apple")]
3572type WintT = libc::c_int;
3573#[cfg(not(target_vendor = "apple"))]
3574type WintT = libc::c_uint;
3575
3576extern "C" {
3577 // <wctype.h> — the libc crate does not bind these on unix targets.
3578 fn iswupper(wc: WintT) -> libc::c_int;
3579 fn iswlower(wc: WintT) -> libc::c_int;
3580 fn iswalnum(wc: WintT) -> libc::c_int;
3581 fn towupper(wc: WintT) -> WintT;
3582 fn towlower(wc: WintT) -> WintT;
3583}
3584
3585/*
3586 * Substitute "in" for "out" in "*strptr" and update "*strptr".
3587 * If "gbal", do global substitution.
3588 *
3589 * This returns a result from the heap. There seems to have
3590 * been some confusion on this point.
3591 */
3592/// Port of `subst()` from `Src/hist.c:2336`. — C decl
3593/// `int subst(char **strptr, char *in, char *out, int gbal, int forcepat)`.
3594///
3595/// !!! WARNING: RUST-ONLY ADAPTATION OF THE SIGNATURE !!!
3596///
3597/// C carries the result out through `char **strptr` and RETURNS A
3598/// STATUS: 0 when the substitution was performed, 1 when it was not.
3599/// The previous Rust port returned the resulting `String` and no
3600/// status, so all three callers had to guess at the status by asking
3601/// whether the string had CHANGED — which is a different question. A
3602/// substitution whose replacement equals what it replaced (`^old^old`,
3603/// `!!:s/x/x/`) succeeds in C and reports `substitution failed` under
3604/// that guess. `strptr` is therefore `&mut String` here (C's out-
3605/// pointer) and the return value is C's own 0/1 status.
3606///
3607/// Two behaviours the previous port did not have:
3608///
3609/// * `#` / `%` are anchors ONLY on the pattern path — i.e. only when
3610/// `HIST_SUBST_PATTERN` is set or `forcepat` is on (`:S`). Under
3611/// plain `:s` they are ordinary characters, and C never looks at
3612/// them (c:2344 gates the whole block). The old port applied them
3613/// unconditionally, so `!!:s/#print/echo/` substituted where zsh
3614/// reports `substitution failed`.
3615/// * the pattern path itself did not exist: the match was always
3616/// `strstr`, so a glob in the pattern never matched, and `&` in
3617/// the replacement was expanded by `convamps` even on the pattern
3618/// path, where C leaves it literal.
3619pub fn subst(strptr: &mut String, in_: &str, out: &str, gbal: i32, forcepat: i32) -> i32 {
3620 // c:2338 char *str = *strptr, *substcut, *sptr;
3621 let str_ = strptr.clone();
3622 let mut gbal = gbal; // c:2336
3623 let substcut: Option<usize>; // c:2338
3624 let mut off: usize; // c:2339 int off, inlen, outlen
3625 let inlen: usize;
3626 let outlen: usize;
3627
3628 // c:2341 — `if (!*in) in = str, gbal = 0;` An empty pattern means
3629 // the whole line is the pattern, and a whole-line match cannot
3630 // repeat, so global is turned off with it.
3631 let in_owned: String = if in_.is_empty() {
3632 gbal = 0; // c:2342
3633 str_.clone() // c:2342
3634 } else {
3635 in_.to_string()
3636 };
3637 let mut in_: &str = &in_owned;
3638
3639 if isset(HISTSUBSTPATTERN) || forcepat != 0 {
3640 // c:2344
3641 let mut fl = SUB_LONG | SUB_REST | SUB_RETFAIL; // c:2345
3642 let oldin = in_; // c:2346
3643 if gbal != 0 {
3644 // c:2347
3645 fl |= SUB_GLOBAL; // c:2348
3646 }
3647 // c:2349 — `if (*in == '#' || *in == Pound)`. The check covers
3648 // BOTH the literal `#` and the tokenized `Pound` (0x84) the
3649 // lexer emits for a `#` inside a parsed substitution body.
3650 if let Some(rest) = in_.strip_prefix('#').or_else(|| in_.strip_prefix(Pound)) {
3651 // c:2350 — anchor at head, flag needed if SUB_END is also set
3652 fl |= SUB_START; // c:2351
3653 in_ = rest; // c:2352 in++
3654 }
3655 if let Some(rest) = in_.strip_prefix('%') {
3656 // c:2354
3657 // c:2355 — anchor at tail
3658 in_ = rest; // c:2356 in++
3659 fl |= SUB_END; // c:2357
3660 }
3661 if in_ == oldin {
3662 // c:2359
3663 // c:2360 — no anchor, substring match
3664 fl |= SUB_SUBSTR; // c:2361
3665 }
3666 // c:2363-2364 — `if (in == str) in = dupstring(in);` C has to
3667 // copy because `in` may alias the buffer `getmatch` is about to
3668 // rewrite through `strptr`; `in_owned` is already a separate
3669 // Rust String, so the aliasing cannot happen.
3670 let in_parsed = match parse_subst_string(in_) {
3671 // c:2365
3672 Ok(s) => s,
3673 Err(_) => return 1, // c:2366
3674 };
3675 if errflag.load(SeqCst) != 0 {
3676 // c:2365
3677 return 1; // c:2366
3678 }
3679 let out_parsed = match parse_subst_string(out) {
3680 // c:2367
3681 Ok(s) => s,
3682 Err(_) => return 1, // c:2368
3683 };
3684 if errflag.load(SeqCst) != 0 {
3685 // c:2367
3686 return 1; // c:2368
3687 }
3688 let in_subbed = singsub(&in_parsed); // c:2369
3689 if getmatch(strptr, &in_subbed, fl, 1, Some(&out_parsed)) != 0 {
3690 // c:2370
3691 return 0; // c:2371
3692 }
3693 } else {
3694 substcut = str_.find(in_); // c:2373
3695 if let Some(pos) = substcut {
3696 inlen = in_.len(); // c:2374
3697 let sptr = convamps(out, in_); // c:2375
3698 outlen = sptr.len(); // c:2376
3699
3700 // c:2378-2384 — replace, then (when global) keep looking
3701 // from just past the text that was written in, so the
3702 // replacement is never re-scanned.
3703 let mut cur = str_; // c:2382 *strptr
3704 let mut substcut = pos; // c:2379
3705 loop {
3706 off = substcut + outlen; // c:2380
3707 cur = format!("{}{}{}", &cur[..substcut], sptr, &cur[substcut + inlen..]); // c:2382
3708 // c:2383 str = *strptr + off
3709 if gbal == 0 {
3710 // c:2384
3711 break;
3712 }
3713 match cur[off..].find(in_) {
3714 // c:2384
3715 Some(p) => substcut = off + p,
3716 None => break,
3717 }
3718 }
3719 *strptr = cur;
3720
3721 return 0; // c:2386
3722 }
3723 }
3724
3725 1 // c:2390
3726}
3727
3728/// Port of `convamps()` from `Src/hist.c:2395`. — C decl `convamps(char *out, char *in, int inlen)`.
3729fn convamps(out: &str, in_pattern: &str) -> String {
3730 let mut result = String::with_capacity(out.len());
3731 let mut chars = out.chars().peekable();
3732 while let Some(c) = chars.next() {
3733 if c == '\\' {
3734 if let Some(&next) = chars.peek() {
3735 result.push(next);
3736 chars.next();
3737 }
3738 } else if c == '&' {
3739 result.push_str(in_pattern);
3740 } else {
3741 result.push(c);
3742 }
3743 }
3744 result
3745}
3746
3747/// Port of `checkcurline()` from `Src/hist.c:2422`. — C decl `checkcurline(Histent he)`.
3748///
3749/// C body (c:2421-2429):
3750/// ```c
3751/// static void checkcurline(Histent he)
3752/// {
3753/// if (he->histnum == curhist && (histactive & HA_ACTIVE)) {
3754/// curline.node.nam = chline;
3755/// curline.nwords = chwordpos/2;
3756/// curline.words = chwords;
3757/// }
3758/// }
3759/// ```
3760///
3761/// The previous Rust port was a complete fabrication: signature was
3762/// `(line: &str) -> i32` returning whether the head of the ring's
3763/// name matched the argument — none of which appears in the C body.
3764/// No caller existed because the bogus signature fit nothing.
3765///
3766/// Real C behaviour: when `he` is the current in-flight history
3767/// entry (matches `curhist` AND the history machinery is active),
3768/// flush the in-progress chline/chwordpos/chwords build state into
3769/// the `curline` placeholder so the caller sees the latest words.
3770/// Called from `movehistent` per c:1298 before returning.
3771///
3772/// WARNING: Rust adaptation diverges in storage shape — C uses
3773/// pointer-aliasing (curline.node.nam = chline) whereas Rust
3774/// stores cloned values into the `curline` Mutex. The observable
3775/// effect is the same after the function returns: a caller reading
3776/// `curline` sees the latest chline/chwords snapshot.
3777pub fn checkcurline(he: &histent) {
3778 // c:2421
3779 let curhist_val = curhist.load(SeqCst); // c:2424
3780 let active = histactive.load(SeqCst); // c:2424
3781 if he.histnum == curhist_val && (active & HA_ACTIVE) != 0 {
3782 // c:2424
3783 let chline_val = chline.lock().expect("chline poisoned").clone(); // c:2425
3784 let chwordpos_val = chwordpos.load(SeqCst); // c:2426
3785 let chwords_val = chwords.lock().expect("chwords poisoned").clone(); // c:2427
3786 let mut cl = curline.lock().expect("curline poisoned");
3787 // Build a fresh histent snapshot mirroring the C field
3788 // aliasing — name = chline, nwords = chwordpos/2,
3789 // words = chwords.
3790 *cl = Some(histent {
3791 node: hashnode {
3792 next: None,
3793 nam: chline_val, // c:2425
3794 flags: 0,
3795 },
3796 up: None,
3797 down: None,
3798 zle_text: None,
3799 stim: 0,
3800 ftim: 0,
3801 words: chwords_val, // c:2427
3802 nwords: chwordpos_val / 2, // c:2426
3803 histnum: he.histnum,
3804 });
3805 }
3806}
3807
3808/// Port of `quietgethist()` from `Src/hist.c:2433`. — C decl `quietgethist(int ev)`.
3809pub fn quietgethist(ev: i64) -> Option<histent> {
3810 // c:2433
3811 // Lazy-history chokepoint: a miss BELOW the loaded floor pages the
3812 // older part of the HISTFILE in on demand (extensions/history_lazy
3813 // — the file is never slurped whole at startup).
3814 let hit = ring_get(ev);
3815 if hit.is_none()
3816 && ev > 0
3817 && crate::history_lazy::has_older()
3818 && ev < crate::history_lazy::floor_num()
3819 {
3820 crate::history_lazy::page_older_until(ev);
3821 return ring_get(ev);
3822 }
3823 hit
3824}
3825
3826/// Port of `gethist()` from `Src/hist.c:2440`. — C decl `gethist(int ev)`.
3827pub fn gethist(ev: i64) -> Option<histent> {
3828 // c:2440
3829 let mut ret = quietgethist(ev);
3830 // c:1318-1343 — C's ring also holds the `curline` sentinel that
3831 // linkcurline (c:1079-1089) splices in while a line is being read,
3832 // and gethistent's `checkcurline(he)` (c:1342) points it at the text
3833 // read so far; checkcurline (c:2424) gates on the same `histnum ==
3834 // curhist && HA_ACTIVE` pair. zshrs keeps the sentinel outside its
3835 // ring, so `curhist` missed and `!#` (c:751-753, `ev = curhist`)
3836 // reported "no such event" instead of the current line.
3837 //
3838 // !!! Resolved here, not in quietgethist: the ZLE navigation-list
3839 // sync (zle_main.rs) walks quietgethist up to curhist and relies on
3840 // the in-flight line being absent, or a PS2 read would record the
3841 // partial text as that event.
3842 if ret.is_none() && ev == curhist.load(SeqCst) && (histactive.load(SeqCst) & HA_ACTIVE) != 0 {
3843 let sentinel = curline.lock().unwrap().as_ref().map(clone_histent);
3844 if let Some(he) = sentinel {
3845 checkcurline(&he); // c:1342
3846 ret = curline.lock().unwrap().as_ref().map(clone_histent);
3847 }
3848 }
3849 if ret.is_none() {
3850 herrflush();
3851 zerr(&format!("no such event: {}", ev));
3852 }
3853 ret
3854}
3855
3856/// Port of `getargs()` from `Src/hist.c:2454`. — C decl `getargs(Histent elist, int arg1, int arg2)`.
3857///
3858/// C body (c:2456-2482):
3859/// ```c
3860/// short *words = elist->words;
3861/// int pos1, pos2, nwords = elist->nwords;
3862///
3863/// if (arg2 < arg1 || arg1 >= nwords || arg2 >= nwords) {
3864/// herrflush();
3865/// zerr("no such word in event");
3866/// return NULL;
3867/// }
3868/// if (arg1 == 0 && arg2 == nwords - 1)
3869/// return dupstring(elist->node.nam);
3870///
3871/// pos1 = words[2*arg1];
3872/// pos2 = words[2*arg2+1];
3873///
3874/// /* a word has to be at least one character long, so if the position
3875/// * of a word is less than its index, we've overflowed our signed
3876/// * short integer word range and the recorded position is garbage. */
3877/// if (pos1 < 0 || pos1 < arg1 || pos2 < 0 || pos2 < arg2) {
3878/// herrflush();
3879/// zerr("history event too long, can't index requested words");
3880/// return NULL;
3881/// }
3882/// return dupstrpfx(elist->node.nam + pos1, pos2 - pos1);
3883/// ```
3884///
3885/// Notes:
3886/// - `nwords` reads `elist->nwords` directly (authoritative).
3887/// - Overflow check `pos1 < arg1 || pos2 < arg2` detects signed-
3888/// short overflow: since each word must be ≥1 char long, the
3889/// start-byte index must be ≥ the 0-based word index; a
3890/// too-small stored position signals i16 overflow on history
3891/// lines >32KB.
3892pub fn getargs(entry: &histent, arg1: usize, arg2: usize) -> Option<String> {
3893 // c:2454
3894 let nwords = entry.nwords as usize; // c:2457 nwords = elist->nwords
3895 if arg2 < arg1 || arg1 >= nwords || arg2 >= nwords {
3896 // c:2459
3897 herrflush(); // c:2461
3898 zerr("no such word in event"); // c:2462
3899 return None; // c:2463
3900 }
3901 // c:2466-2467 — `if (arg1 == 0 && arg2 == nwords - 1) return dupstring(nam);`
3902 if arg1 == 0 && arg2 == nwords - 1 {
3903 return Some(entry.node.nam.clone()); // c:2467
3904 }
3905 let pos1_raw = entry.words.get(2 * arg1).copied().unwrap_or(-1); // c:2469 pos1 = words[2*arg1]
3906 let pos2_raw = entry.words.get(2 * arg2 + 1).copied().unwrap_or(-1); // c:2470 pos2 = words[2*arg2+1]
3907 // c:2476 — C signed-short overflow detection: any negative
3908 // position OR a position less than its corresponding word
3909 // index means the i16 storage wrapped on a >32KB history line.
3910 if pos1_raw < 0
3911 || (pos1_raw as i64) < (arg1 as i64)
3912 || pos2_raw < 0
3913 || (pos2_raw as i64) < (arg2 as i64)
3914 {
3915 // c:2476
3916 herrflush(); // c:2477
3917 zerr(
3918 "history event too long, can't index requested words", // c:2478
3919 );
3920 return None; // c:2479
3921 }
3922 let pos1 = pos1_raw as usize;
3923 let pos2 = pos2_raw as usize;
3924 // c:2481 — `dupstrpfx(elist->node.nam + pos1, pos2 - pos1)`.
3925 // Rust slice indexing requires pos1 <= pos2 <= len; both are
3926 // satisfied since both passed the c:2476 overflow check and
3927 // are bounded by the underlying string length per insert
3928 // contract. Guard with .get() so a malformed entry doesn't
3929 // panic.
3930 entry.node.nam.get(pos1..pos2).map(|s| s.to_string()) // c:2481
3931}
3932
3933/// Port of `quote()` from `Src/hist.c:2486`. — C decl `quote(char **tr)`.
3934/// Wraps `*tr` in single quotes; `'` inside becomes `'\''` and any
3935/// `inblank(c)` (space/tab/newline) outside quotes that wasn't
3936/// preceded by `\` becomes `'<c>'`.
3937///
3938/// Previous Rust port used `c.is_whitespace()` — broader than C's
3939/// `inblank()` which is the narrow typtab INBLANK class (space, tab,
3940/// newline ONLY). Drift would silently quote CR/FF/VT/NBSP/etc.
3941/// chars that C leaves alone.
3942pub fn quote(s: &str) -> String {
3943 // c:2486
3944 let bytes: Vec<char> = s.chars().collect();
3945 let mut out = String::with_capacity(bytes.len() + 3);
3946 out.push('\'');
3947 let mut inquotes = false;
3948 let mut prev: char = '\0';
3949 for &c in bytes.iter() {
3950 // c:2499 — `inblank(*ptr)` is narrow space/tab/newline.
3951 let is_inblank = matches!(c, ' ' | '\t' | '\n');
3952 if c == '\'' {
3953 inquotes = !inquotes;
3954 out.push('\'');
3955 out.push('\\');
3956 out.push('\'');
3957 out.push('\'');
3958 } else if is_inblank && !inquotes && prev != '\\' {
3959 // c:2514
3960 out.push('\'');
3961 out.push(c);
3962 out.push('\'');
3963 } else {
3964 out.push(c);
3965 }
3966 prev = c;
3967 }
3968 out.push('\'');
3969 out
3970}
3971
3972/// Port of `quotebreak()` from `Src/hist.c:2527`. — C decl `quotebreak(char **tr)`.
3973/// Same
3974/// shape as `quote` but `inblank` chars get the `'<c>'` break-out
3975/// treatment whether or not they're inside quotes.
3976///
3977/// `c.is_whitespace()` → `inblank(c)` fix per the same divergence
3978/// pattern as `quote` above.
3979pub fn quotebreak(s: &str) -> String {
3980 // c:2527
3981 let mut result = String::with_capacity(s.len() + 10);
3982 result.push('\'');
3983 for c in s.chars() {
3984 // c:2548 — `inblank(*ptr)` narrow set.
3985 let is_inblank = matches!(c, ' ' | '\t' | '\n');
3986 if c == '\'' {
3987 result.push_str("'\\''");
3988 } else if is_inblank {
3989 result.push('\'');
3990 result.push(c);
3991 result.push('\'');
3992 } else {
3993 result.push(c);
3994 }
3995 }
3996 result.push('\'');
3997 result
3998}
3999
4000/// Port of `hdynread()` from `Src/hist.c:2562`. — C decl `hdynread(int stop)`.
4001/// C body is
4002/// inside `#if 0` (the in-tree `hdynread2` variant is the live one), but
4003/// the name-parity port mirrors the disabled body: read input chars via
4004/// `ingetc()` until `stop`/newline/lexstop, honoring `\\`-escape, and
4005/// emit a `delimiter expected` error if newline is hit first.
4006pub fn hdynread(stop: i32) -> Option<String> {
4007 // c:2562
4008 let stop_c = stop as u8 as char; // c:2562 int stop
4009 let mut buf = String::with_capacity(256); // c:2564 bsiz=256
4010 let mut c: Option<char>; // c:2564 int c
4011 loop {
4012 c = ingetc(); // c:2568
4013 match c {
4014 None => break,
4015 Some(ch) if ch == stop_c => break, // c:2568
4016 Some('\n') => break, // c:2568
4017 Some(ch) => {
4018 if lexstop.load(SeqCst) {
4019 break;
4020 } // c:2568
4021 let mut written = ch;
4022 if ch == '\\' {
4023 // c:2569
4024 if let Some(nxt) = ingetc() {
4025 // c:2570
4026 written = nxt;
4027 } else {
4028 break;
4029 }
4030 }
4031 buf.push(written); // c:2571
4032 }
4033 }
4034 }
4035 if let Some('\n') = c {
4036 // c:2578
4037 inungetc('\n'); // c:2579
4038 zerr("delimiter expected"); // c:2580
4039 return None; // c:2582
4040 }
4041 Some(buf) // c:2584
4042}
4043
4044/// Port of `ihungetc()` from `Src/hist.c:989`. — C decl `ihungetc(int c)`.
4045/// Push back `c` into the lexer input stream while history-rewriting
4046/// is in progress: also rewinds chline (`hptr--`), undoes the
4047/// `expanding`-driven `zlemetacs`/`zlemetall` advance, and tracks the
4048/// `qbang` state for `\!` re-escape on the next pass. Loops while
4049/// `qbang` keeps firing (which can re-trigger via the `c='\\'` step
4050/// at the bottom).
4051pub fn ihungetc(c: i32) {
4052 // c:989
4053 // Rust-only: pool threads run C's `hungetc = inungetc` arm (c:1140).
4054 if crate::worker::in_worker_thread() {
4055 if let Some(ch) = char::from_u32(c as u32) {
4056 crate::ported::input::inungetc(ch);
4057 }
4058 return;
4059 }
4060 let mut c = char::from_u32(c as u32).unwrap_or(c as u8 as char); // c:991 int c
4061 let mut doit = 1; // c:991 doit = 1
4062 while !lexstop.load(SeqCst) // c:993 while (!lexstop && !errflag)
4063 && errflag.load(SeqCst) == 0
4064 {
4065 let hp = hptr.load(SeqCst);
4066 let line = chline.lock().unwrap().clone();
4067 let line_b = line.as_bytes();
4068 let stop = stophist.load(SeqCst);
4069 let inflags = crate::ported::input::inbufflags.with(|f| f.get());
4070 let active = histactive.load(SeqCst);
4071 if hp >= 2 && hp <= line_b.len() // c:994-997
4072 && line_b[hp - 1] != c as u8 && stop < 4
4073 && line_b[hp - 1] == b'\n' && line_b[hp - 2] == b'\\'
4074 && (active & HA_UNGET) == 0
4075 && (inflags & (INP_ALIAS | INP_HIST)) != INP_ALIAS
4076 {
4077 histactive.fetch_or(HA_UNGET, SeqCst); // c:998
4078 inungetc('\n'); // c:999 hungetc('\n') — default = inungetc (c:1140)
4079 inungetc('\\'); // c:1000
4080 histactive.fetch_and(!HA_UNGET, SeqCst);
4081 // c:1001
4082 }
4083 if expanding.load(SeqCst) != 0 {
4084 // c:1004 if (expanding)
4085 // c:1005-1006 — `zlemetacs--; zlemetall--;`. C ungets one BYTE; zshrs
4086 // ungets a whole char, which `zleaddtoline` added as `len_utf8` bytes.
4087 let w = c.len_utf8() as i32;
4088 ZLEMETACS.fetch_sub(w, SeqCst); // c:1005 zlemetacs--
4089 crate::ported::zle::compcore::ZLEMETALL.fetch_sub(w, SeqCst); // c:1006 zlemetall--
4090 exlast.fetch_add(1, SeqCst); // c:1007 exlast++
4091 }
4092 if (inflags & (INP_ALIAS | INP_HIST)) != INP_ALIAS {
4093 // c:1009
4094 // c:1010 — DPUTS(hptr <= chline, "BUG: hungetc attempted at buffer start")
4095 DPUTS!(hp <= 0, "BUG: hungetc attempted at buffer start"); // c:1010
4096 // c:1012 — DPUTS(*hptr != (char) c, "BUG: wrong character in hungetc() ")
4097 DPUTS!(
4098 // c:1012
4099 hp > 0 && line_b.get(hp - 1).copied() != Some(c as u8), // c:1012
4100 "BUG: wrong character in hungetc() " // c:1012
4101 );
4102 let new_hp = hp.saturating_sub(c.len_utf8()); // ihwaddc advanced hptr by len_utf8
4103 hptr.store(new_hp, SeqCst); // c:1011 hptr--
4104 let bangchar_v = bangchar.load(SeqCst) as u8;
4105 let qb = c == bangchar_v as char && stop < 2 // c:1014-1015
4106 && new_hp > 0 && line_b.get(new_hp - 1).copied() == Some(b'\\');
4107 qbang.store(qb, SeqCst);
4108 } else {
4109 qbang.store(false, SeqCst); // c:1018 No active bangs in aliases
4110 }
4111 if doit != 0 {
4112 // c:1020
4113 inungetc(c); // c:1021
4114 }
4115 if !qbang.load(SeqCst) {
4116 return;
4117 } // c:1022
4118 let inflags2 = crate::ported::input::inbufflags.with(|f| f.get());
4119 doit = if stophist.load(SeqCst) == 0 // c:1023-1024
4120 && ((inflags2 & INP_HIST) != 0 || (inflags2 & INP_ALIAS) == 0)
4121 {
4122 1
4123 } else {
4124 0
4125 };
4126 c = '\\'; // c:1025
4127 }
4128}
4129
4130/// Port of `getsubsargs()` from `Src/hist.c:518`. — C decl `getsubsargs(UNUSED(char *subline), int *gbalp, int *cflagp)`.
4131/// Parses the substitution arguments of a
4132/// `!:s/old/new/`-style history modifier: reads the delimiter via
4133/// ingetc, slurps `old` and `new` chunks, stores them in `hsubl`/`hsubr`
4134/// globals, then peeks the trailing `:G` (global) or fall-through char.
4135/// Returns 0 on success, 1 on a bad-expansion (empty old chunk).
4136/// WARNING: param names don't match C — Rust=(_subline, gbalp, cflagp) vs C=(subline, gbalp, cflagp)
4137pub fn getsubsargs(_subline: &str, gbalp: &mut i32, cflagp: &mut i32) -> i32 {
4138 // c:518
4139 let del = match ingetc() {
4140 // c:524 del = ingetc()
4141 Some(c) => c,
4142 None => return 1,
4143 };
4144 let ptr1 = hdynread2(del); // c:524
4145 // c:525-526 — `if (!ptr1) return 1;` cannot fire: hdynread2 always
4146 // returns the buffer it zalloc's at c:2593, never NULL.
4147 let ptr2 = hdynread2(del); // c:527
4148 if !ptr1.is_empty() {
4149 // c:530
4150 *hsubl.lock().unwrap() = Some(ptr1); // c:531-532 zsfree(hsubl); hsubl = ptr1
4151 } else if hsubl.lock().unwrap().is_none() {
4152 // c:533 fail silently
4153 return 0; // c:536
4154 }
4155 *hsubr.lock().unwrap() = Some(ptr2); // c:539-540 zsfree(hsubr); hsubr = ptr2
4156 let follow = ingetc(); // c:541 follow = ingetc()
4157 if follow == Some(':') {
4158 // c:542
4159 let next = ingetc(); // c:543
4160 if next == Some('G') {
4161 *gbalp = 1;
4162 }
4163 // c:544-545
4164 else {
4165 if let Some(c) = next {
4166 inungetc(c);
4167 } // c:547 inungetc
4168 *cflagp = 1; // c:548
4169 }
4170 } else if let Some(c) = follow {
4171 inungetc(c); // c:551 inungetc(follow)
4172 }
4173 0 // c:553
4174}
4175
4176/// Port of `hdynread2()` from `Src/hist.c:2590`. — C decl `hdynread2(int stop)`.
4177///
4178/// Read one `:s` / `^foo^bar` argument from the INPUT STREAM, up to the
4179/// delimiter `stop`, a newline, or end of input.
4180///
4181/// A newline ends the argument but is NOT consumed: c:2606-2607 puts it
4182/// back. That is what lets the closing delimiter be omitted —
4183/// `^old^new<Return>` reads `new` up to the newline, and that newline
4184/// still has to be in the stream afterwards to end the command. The
4185/// reader `getsubsargs` used to carry inline swallowed it, so the
4186/// expanded line had no terminator and the lexer read the NEXT line as
4187/// more of the same command: at a prompt `^old^new` echoed nothing, ran
4188/// nothing and sat in a continuation read; under `-s` it glued
4189/// `print new xx` onto the following line.
4190pub fn hdynread2(stop: char) -> String {
4191 // c:2590
4192 let mut buf = String::with_capacity(256); // c:2592-2593 bsiz = 256, zalloc
4193 let mut c: Option<char>; // c:2592 int c
4194 loop {
4195 // c:2596 — `while ((c = ingetc()) != stop && c != '\n' && !lexstop)`.
4196 // The port's `ingetc` reports lexstop as None (c:Src/input.c:322).
4197 c = ingetc(); // c:2596
4198 let Some(mut ch) = c else { break };
4199 if ch == stop || ch == '\n' {
4200 break;
4201 }
4202 if ch == '\\' {
4203 // c:2597
4204 // c:2598 — `c = ingetc();` The backslash is ALWAYS dropped and
4205 // only the character after it is stored — C does not care
4206 // whether that character is the delimiter.
4207 match ingetc() {
4208 Some(n) => ch = n, // c:2598
4209 None => break,
4210 }
4211 }
4212 buf.push(ch); // c:2599 *ptr++ = c (c:2600-2603 realloc = String growth)
4213 }
4214 // c:2605 *ptr = 0 — a Rust String needs no terminator.
4215 if c == Some('\n') {
4216 // c:2606
4217 inungetc('\n'); // c:2607
4218 }
4219 buf // c:2608
4220}
4221
4222/// Port of `inithist()` from `Src/hist.c:2613`. — C decl `inithist(void)`.
4223pub fn inithist() {
4224 // c:2613
4225 histsiz.store(1000, SeqCst);
4226 savehistsiz.store(1000, SeqCst);
4227 curhist.store(0, SeqCst);
4228 histlinect.store(0, SeqCst);
4229}
4230
4231/// Port of `resizehistents()` from `Src/hist.c:2620`. — C decl `resizehistents(void)`.
4232/// free the
4233/// oldest entries until `histlinect <= histsiz` (C loops
4234/// `putoldhistentryontop(1); freehistnode(&hist_ring->node)`).
4235/// `hist_ring` is strictly DESCENDING by histnum (see ring_get), so
4236/// the oldest entries are the Vec TAIL — one `truncate`, not the
4237/// previous per-entry `ring_oldest()` scan + `retain()` rebuild,
4238/// which was O(n²): a 558k-entry HISTFILE read under the default
4239/// histsiz spun for minutes (`fc -R` looked hung).
4240pub fn resizehistents() {
4241 let cap = histsiz.load(SeqCst);
4242 let ct = histlinect.load(SeqCst);
4243 if ct <= cap {
4244 return;
4245 }
4246 let excess = (ct - cap) as usize;
4247 let len = hist_ring.lock().unwrap().len();
4248 let keep = len.saturating_sub(excess);
4249 // c:2626-2629 — each `freehistnode` runs freehistdata, which drops
4250 // the entry's histtab key; do that before cutting the tail.
4251 for idx in keep..len {
4252 crate::ported::hashtable::freehistdata(idx, 0);
4253 }
4254 let mut ring = hist_ring.lock().unwrap();
4255 ring.truncate(keep);
4256 histlinect.store(cap, SeqCst);
4257}
4258
4259/// Port of `readhistline()` from `Src/hist.c:2635`. — C decl `readhistline(int start, char **bufp, int *bufsiz, FILE *in, int *readbytes)`.
4260///
4261/// Returns one LOGICAL history line: physical lines ending in a
4262/// backslash are joined with the backslash turned into the newline it
4263/// escaped, and the single space `savehistfile` appends after a
4264/// trailing backslash run (c:3063-3068) is removed again. `lines` are
4265/// the file's physical lines without their `\n`; `ends_with_nl` says
4266/// whether the LAST one had one (fgets' `buf[len - 1] != '\n'` test).
4267/// `None` is C's `return 0` — end of file.
4268pub fn readhistline<'a, I: Iterator<Item = &'a str>>(
4269 lines: &mut std::iter::Peekable<I>,
4270 ends_with_nl: bool,
4271) -> Option<String> {
4272 let mut buf = String::new();
4273 loop {
4274 let raw = lines.next()?; // c:2638 fgets; NULL → return 0
4275 buf.push_str(raw);
4276 // c:2644-2651 — an unterminated last line is returned as is.
4277 if lines.peek().is_none() && !ends_with_nl {
4278 return Some(buf);
4279 }
4280 // c:2654 buf[len - 1] = '\0'; c:2655-2659 — a trailing
4281 // backslash escaped a newline: turn it back into one and read on.
4282 // fgets stopped at the '\n', so feof() is still false here and
4283 // C always recurses; a continuation on the last line therefore
4284 // meets fgets == NULL and the whole entry is dropped (c:2638).
4285 if buf.ends_with('\\') {
4286 buf.pop();
4287 buf.push('\n');
4288 continue;
4289 }
4290 // c:2661-2665 — backslashes followed by spaces: drop the one
4291 // space the writer added so the line did not read as continued.
4292 let trimmed_len = buf.trim_end_matches(' ').len();
4293 if trimmed_len != buf.len() && buf[..trimmed_len].ends_with('\\') {
4294 buf.pop();
4295 }
4296 return Some(buf);
4297 }
4298}
4299
4300/// Port of `readhistfile()` from `Src/hist.c:2675`. — C decl `readhistfile(char *fn, int err, int readflags)`.
4301pub fn readhistfile(fn_path: Option<&str>, _err: i32, readflags: i32) {
4302 // c:2675
4303 let path: String = match fn_path {
4304 Some(p) => p.to_string(),
4305 None => match resolve_histfile() {
4306 Some(p) => p,
4307 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
4308 // zshrs's DEFAULT history store is the SQLite index; its
4309 // extended-history text mirror ($ZSHRS_HOME/zshrs_history,
4310 // extensions/history.rs::text_path) is kept current by the
4311 // per-command interactive sink (history_sqlite_add). Outside
4312 // --zsh mode (or with no $HISTFILE), the startup read (init.c:1573)
4313 // hydrates the ring from that mirror so up-arrow recalls
4314 // previous sessions. READ-side only: savehistfile stays
4315 // HISTFILE-gated — the engine owns all mirror writes, so a
4316 // rewrite-at-exit would duplicate every row.
4317 None => {
4318 let p = crate::history::HistoryEngine::text_path();
4319 if !p.exists() {
4320 return;
4321 }
4322 p.to_string_lossy().to_string()
4323 }
4324 },
4325 };
4326 // c:2675 — HFILE_FAST is the incremental (share / append) read: resume at
4327 // lasthist.fpos and consume only what other shells appended, rather than
4328 // re-parsing the whole file. Under SHARE_HISTORY hend() re-reads after
4329 // EVERY command (c:1529); a full re-read there is O(file) per prompt (and,
4330 // with the Vec ring, was an O(n²) memmove hang on a large HISTFILE).
4331 let fast = readflags & HFILE_FAST as i32 != 0;
4332 use std::os::unix::fs::MetadataExt;
4333 let meta = std::fs::metadata(&path).ok();
4334 let cur_size = meta.as_ref().map(|m| m.len() as i64).unwrap_or(0);
4335 let cur_mtim = meta.as_ref().map(|m| m.mtime()).unwrap_or(0);
4336 if cur_size == 0 {
4337 return;
4338 }
4339 if fast {
4340 // Nothing changed since our last read/write — savehistfile keeps
4341 // lasthist.{fpos,fsiz,mtim} current (c:3049/3079/3080) — so there is
4342 // nothing new to pull in. Skip without opening the file.
4343 let lh = lasthist.lock().unwrap();
4344 if lh.fsiz == cur_size && lh.mtim == cur_mtim {
4345 return;
4346 }
4347 }
4348 // A fast read resumes at lasthist.fpos when the file only grew; a rewrite
4349 // or truncation (file now shorter than fpos) or a cold/full read starts at
4350 // 0. `read_end` is where we actually stopped, recorded into lasthist below.
4351 let start_pos: i64 = if fast {
4352 let fp = lasthist.lock().unwrap().fpos;
4353 if fp > 0 && fp <= cur_size {
4354 fp
4355 } else {
4356 0
4357 }
4358 } else {
4359 0
4360 };
4361 // LAZY cold read — the history is never slurped whole (user-directed
4362 // contract; see extensions/history_lazy.rs). A cold read loads ONE
4363 // newest page; everything older stays on disk and pages in on demand
4364 // (navigation below the floor, whole-history scans). Exact zsh event
4365 // numbering comes from a zero-heap streaming entry count so `fc -l`,
4366 // `!N` and HISTNO match a full eager load. The old cold path
4367 // `read_to_end`'d the entire HISTFILE (635MB / 566k entries) into a
4368 // String + one histent per line, per shell — multi-second first
4369 // prompts and hundreds of MB × N shells at fleet start.
4370 //
4371 // Paging serves the interactive ring only. Inside a pushed history
4372 // stack (histsave_stack_pos > 0) the read is savehistfile's SAVEHIST
4373 // trim (c:3113-3121): it re-reads the file and REWRITES it from the
4374 // ring, so a one-page read there truncated $HISTFILE to its newest
4375 // page on every exit, and `arm` repointed the interactive ring's
4376 // floor at the temporary stack. Read the whole file, as C does.
4377 let page_bytes = crate::history_lazy::PAGE_BYTES as i64;
4378 let tail_start: i64 = if start_pos == 0
4379 && cur_size > page_bytes
4380 && histsave_stack_pos.load(SeqCst) == 0
4381 {
4382 cur_size - page_bytes
4383 } else {
4384 start_pos
4385 };
4386 let tail_bounded = tail_start > start_pos;
4387 let total_entries: Option<u64> = if tail_bounded {
4388 crate::history_lazy::count_entries(&path)
4389 } else {
4390 None
4391 };
4392 let start_pos = tail_start;
4393 let (contents, raw_len) = {
4394 use std::io::{Read, Seek, SeekFrom};
4395 let mut f = match std::fs::File::open(&path) {
4396 Ok(f) => f,
4397 Err(_) => return,
4398 };
4399 if start_pos > 0 && f.seek(SeekFrom::Start(start_pos as u64)).is_err() {
4400 return;
4401 }
4402 // c:2718 readhistline — C reads RAW BYTES and unmetafies each
4403 // line: savehistfile writes the file METAFIED (c:3030), and
4404 // decades-old real-world HISTFILEs also carry non-UTF-8 bytes
4405 // (latin1 args, binary paste). The previous `read_to_string`
4406 // errored on the first invalid byte and silently skipped the
4407 // ENTIRE file — a 26MB / 558k-line zpwr history loaded zero
4408 // entries (empty ring, no up-arrow across sessions) while small
4409 // clean-ASCII files worked. Unmetafy, then lossy-decode.
4410 // `raw_len` keeps the ON-DISK byte count: lasthist.fpos tracks
4411 // raw file offsets, and unmetafy/lossy-decode shrink the buffer
4412 // — recording the shrunk length would make the next HFILE_FAST
4413 // share-read resume short and re-read (duplicate) the tail.
4414 let mut bytes = Vec::new();
4415 if f.read_to_end(&mut bytes).is_err() {
4416 return;
4417 }
4418 let raw_len = bytes.len() as i64;
4419 crate::ported::utils::unmetafy(&mut bytes); // c:2731 unmetafy
4420 (String::from_utf8_lossy(&bytes).into_owned(), raw_len)
4421 };
4422 let read_end = start_pos + raw_len;
4423 if contents.is_empty() {
4424 // Nothing appended past fpos, but the file was touched (mtime/size
4425 // differed). Refresh lasthist so the next fast read early-outs.
4426 let mut lh = lasthist.lock().unwrap();
4427 lh.fpos = read_end;
4428 lh.fsiz = cur_size;
4429 lh.mtim = cur_mtim;
4430 return;
4431 }
4432 // c:2700-2706 — lockhistfile return codes:
4433 // 0 — lock acquired, proceed
4434 // 2 — locking failed but "reading anyway"; warn + continue
4435 // else — locking failed hard; zerr + bail
4436 let lock_ret = lockhistfile(Some(&path), 1);
4437 if lock_ret != 0 {
4438 if lock_ret == 2 {
4439 crate::ported::utils::zwarn(&format!(
4440 "locking failed for {}: {}: reading anyway",
4441 path,
4442 std::io::Error::last_os_error()
4443 ));
4444 } else {
4445 crate::ported::utils::zerr(&format!(
4446 "locking failed for {}: {}",
4447 path,
4448 std::io::Error::last_os_error()
4449 ));
4450 return;
4451 }
4452 }
4453
4454 // c:2675 — zsh's hist_ring is a doubly-linked list, so its per-entry
4455 // "insert at head" (addhistnode) is O(1). This port stores the ring in a
4456 // Vec, where `insert(0, ..)` shifts every existing element — O(n) per line,
4457 // O(n²) over the whole file. A large HISTFILE (hundreds of thousands of
4458 // lines) plus SHARE_HISTORY (which re-reads the file in hend() after every
4459 // command, c:1529) turned that into a multi-second memmove spin per prompt.
4460 // Collect into a local batch in file order (push = O(1)), then splice the
4461 // whole run onto the front of the ring ONCE below. Repeated `insert(0)` of
4462 // e1..eN (oldest..newest) yields ring == [eN..e1, existing..]; pushing
4463 // e1..eN then reversing gives the same [eN..e1] prefix. One lock, not one
4464 // per line.
4465 let mut batch: Vec<histent> = Vec::new();
4466 let mut current: Option<(i64, i64, String)> = None;
4467 // Tail-bounded reads land mid-entry: align to the first ENTRY
4468 // boundary. Skip the (guaranteed partial) first line, then — for
4469 // extended-history files — skip continuation lines until a `: N:M;`
4470 // header starts a fresh entry. Plain-format files align at the
4471 // first full line.
4472 let mut lines_iter = contents.lines();
4473 let mut align_consumed: usize = 0;
4474 if tail_bounded {
4475 if let Some(first) = lines_iter.next() {
4476 align_consumed += first.len() + 1; // partial first line
4477 }
4478 let looks_extended = contents.contains("\n: ");
4479 if looks_extended {
4480 let mut peeked = lines_iter.clone();
4481 while let Some(l) = peeked.next() {
4482 if l.starts_with(": ") && l.contains(';') {
4483 break;
4484 }
4485 align_consumed += l.len() + 1;
4486 let _ = lines_iter.next();
4487 }
4488 }
4489 }
4490 let mut lines_iter = lines_iter.peekable();
4491 let ends_with_nl = contents.ends_with('\n');
4492 while let Some(logical) = readhistline(&mut lines_iter, ends_with_nl) {
4493 let raw_line = logical.as_str();
4494 if let Some((stim, ftim, ref text)) = current {
4495 let n = curhist.fetch_add(1, SeqCst) + 1;
4496 let mut entry = make_histent(n, text.clone());
4497 entry.stim = stim;
4498 entry.ftim = ftim;
4499 entry.node.flags |= HIST_OLD as i32;
4500 batch.push(entry);
4501 histlinect.fetch_add(1, SeqCst);
4502 current = None;
4503 }
4504 // c:2771-2785 — `: STIM:ELAPSED;TEXT` extended header. C only
4505 // looks at the leading `:` and scans with zstrtol, so a header
4506 // missing its `:ELAPSED` part leaves the text EMPTY (pt ran to
4507 // the NUL looking for the `:`) and ftim = stim.
4508 let (stim, ftim, text): (i64, i64, &str) = if let Some(pt) = raw_line.strip_prefix(':') {
4509 let (stim, _) = crate::ported::utils::zstrtol(pt, 0); // c:2773
4510 match pt.find(':') {
4511 // c:2775-2779
4512 Some(c) => {
4513 let pt = &pt[c + 1..];
4514 let (ftim, _) = crate::ported::utils::zstrtol(pt, 0); // c:2776
4515 let text = match pt.find(';') {
4516 Some(s) => &pt[s + 1..],
4517 None => "",
4518 };
4519 (stim, ftim, text)
4520 }
4521 None => (stim, stim, ""), // c:2780-2781
4522 }
4523 } else {
4524 // c:2783-2784 — a plain-format line starting with `:` was
4525 // written as `\:` (c:3057-3058) so it does not read as a header.
4526 let text = match raw_line.strip_prefix("\\:") {
4527 Some(_) => &raw_line[1..],
4528 None => raw_line,
4529 };
4530 (0, 0, text)
4531 };
4532 // c:2816-2821 — no timestamp means "now"; an ELAPSED smaller than
4533 // the start is a duration (what savehistfile writes, c:3055-3056).
4534 let (stim, ftim) = if stim == 0 {
4535 let now = std::time::SystemTime::now()
4536 .duration_since(std::time::UNIX_EPOCH)
4537 .map(|d| d.as_secs() as i64)
4538 .unwrap_or(0);
4539 (now, now)
4540 } else if ftim < stim {
4541 (stim, stim + ftim)
4542 } else {
4543 (stim, ftim)
4544 };
4545 current = Some((stim, ftim, text.to_string()));
4546 }
4547 if let Some((stim, ftim, text)) = current {
4548 let n = curhist.fetch_add(1, SeqCst) + 1;
4549 let mut entry = make_histent(n, text);
4550 entry.stim = stim;
4551 entry.ftim = ftim;
4552 entry.node.flags |= HIST_OLD as i32;
4553 batch.push(entry);
4554 histlinect.fetch_add(1, SeqCst);
4555 }
4556 // Lazy numbering: the newest page's entries carry the TAIL of the
4557 // exact event range (…, TOTAL-1, TOTAL) so numbering is identical
4558 // to a full eager load; older pages fill downward on demand.
4559 if tail_bounded {
4560 if let Some(total) = total_entries {
4561 let k = batch.len() as i64;
4562 let shift = (total as i64) - k - (curhist.load(SeqCst) - k);
4563 if shift != 0 {
4564 for e in batch.iter_mut() {
4565 e.histnum += shift;
4566 }
4567 curhist.fetch_add(shift, SeqCst);
4568 }
4569 }
4570 let floor_num = batch.first().map(|e| e.histnum).unwrap_or(0);
4571 crate::history_lazy::arm(&path, tail_start + align_consumed as i64, floor_num);
4572 }
4573 if !batch.is_empty() {
4574 // Prepend the batch (newest-first) ahead of anything already in the
4575 // ring, matching the per-line `insert(0)` order. Single O(n) splice.
4576 batch.reverse();
4577 let mut ring = hist_ring.lock().unwrap();
4578 batch.append(&mut ring);
4579 *ring = batch;
4580 }
4581 // c:2675 — record where this read stopped. A non-fast (startup) read primes
4582 // lasthist too, so the first HFILE_FAST share-read early-outs instead of
4583 // re-reading from offset 0 and DUPLICATING every entry already in the ring.
4584 {
4585 let mut lh = lasthist.lock().unwrap();
4586 lh.fpos = read_end;
4587 lh.fsiz = cur_size;
4588 lh.mtim = cur_mtim;
4589 }
4590 unlockhistfile(&path);
4591 resizehistents();
4592}
4593
4594/// Port of `flockhistfile()` from `Src/hist.c:2881`. — C decl `flockhistfile(char *fn, int keep_trying)`.
4595pub fn flockhistfile(path: &str) -> i32 {
4596 #[cfg(unix)]
4597 {
4598 if let Ok(file) = std::fs::OpenOptions::new()
4599 .write(true)
4600 .create(true)
4601 .open(format!("{}.lock", path))
4602 {
4603 let fd = file.as_raw_fd();
4604 return unsafe {
4605 if libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) == 0 {
4606 1
4607 } else {
4608 0
4609 }
4610 };
4611 }
4612 0
4613 }
4614 #[cfg(not(unix))]
4615 {
4616 let _ = path;
4617 1
4618 }
4619}
4620
4621/// Port of `savehistfile()` from `Src/hist.c:2922`. — C decl `savehistfile(char *fn, int err, int writeflags)`.
4622///
4623/// The public Rust signature omits the C `err` argument; its value is
4624/// recovered from the write flags. Every silent (`err == 0`) C call
4625/// site also sets `HFILE_FAST` (the incremental / share saves at
4626/// c:1193 and c:1639), while every loud (`err == 1`) site — shell exit
4627/// (c:3961), `fc -W` / `fc -A` (c:1511/1517) and the trim recursion
4628/// (c:3121) — leaves `HFILE_FAST` clear. So `err == !(writeflags &
4629/// HFILE_FAST)` reproduces the C value at all call sites without
4630/// changing the signature seen by callers.
4631pub fn savehistfile(fn_path: Option<&str>, writeflags: i32) {
4632 // c:2922
4633 use crate::ported::zsh_h::{
4634 APPENDHISTORY, EXTENDEDHISTORY, GETHIST_DOWNWARD, HFILE_APPEND, HFILE_NO_REWRITE,
4635 HFILE_SKIPDUPS, HFILE_SKIPFOREIGN, HFILE_SKIPOLD, HISTSAVEBYCOPY, HISTSAVENODUPS,
4636 };
4637 use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
4638
4639 let mut writeflags = writeflags;
4640 // c:2927 — zlong xcurhist = curhist - !!(histactive & HA_ACTIVE);
4641 let xcurhist = curhist.load(SeqCst)
4642 - if (histactive.load(SeqCst) & HA_ACTIVE) != 0 {
4643 1
4644 } else {
4645 0
4646 };
4647 // c:2928 — int extended_history = isset(EXTENDEDHISTORY);
4648 let mut extended_history = isset(EXTENDEDHISTORY);
4649 // Recovered C `err` (see fn doc): silent when HFILE_FAST is set.
4650 let mut err = writeflags & HFILE_FAST as i32 == 0;
4651
4652 // c:2931-2933 — `if (!interact || savehistsiz <= 0 || !hist_ring
4653 // || (!fn && !(fn = getsparam("HISTFILE")))) return;`
4654 //
4655 // `!interact` is test-pinned (non-interactive shells must never
4656 // write the user's HISTFILE). An explicit `fc -W path` is gated the
4657 // same way: with SAVEHIST unset or 0 zsh writes nothing, not even an
4658 // empty file.
4659 if !isset(INTERACTIVE) {
4660 return;
4661 }
4662 let savehistsiz_v = savehistsiz.load(SeqCst); // c:2932 savehistsiz
4663 if savehistsiz_v <= 0 {
4664 return; // c:2932 savehistsiz <= 0
4665 }
4666 if ring_len() == 0 {
4667 return; // c:2931 !hist_ring
4668 }
4669 let path: String = match fn_path {
4670 // c:2932 fn / getsparam("HISTFILE")
4671 Some(p) => p.to_string(),
4672 None => match resolve_histfile() {
4673 Some(p) => p,
4674 None => return,
4675 },
4676 };
4677 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
4678 // An explicit `fc -W/-A $HISTFILE` outside `--zsh` mode would still
4679 // write zsh's file; see resolve_histfile.
4680 if !crate::history::histfile_writable() && crate::history::is_histfile(&path) {
4681 crate::ported::utils::zwarn(&format!("{}: not writing zsh history file", path));
4682 return;
4683 }
4684
4685 // c:2934-2951 — pick the first entry to write and take the lock.
4686 let he: Option<i64>;
4687 if writeflags & HFILE_FAST as i32 != 0 {
4688 // c:2935 — he = gethistent(lasthist.next_write_ev, GETHIST_DOWNWARD);
4689 let start_ev = lasthist.lock().unwrap().next_write_ev;
4690 let mut cur = gethistent(start_ev, GETHIST_DOWNWARD);
4691 // c:2936-2939 — advance past entries already written (HIST_OLD).
4692 while let Some(h) = cur {
4693 let flags = ring_get(h).map(|e| e.node.flags).unwrap_or(0);
4694 if flags & HIST_OLD as i32 == 0 {
4695 break;
4696 }
4697 lasthist.lock().unwrap().next_write_ev = h + 1; // c:2937
4698 cur = down_histent(h); // c:2938
4699 }
4700 he = cur;
4701 // c:2940 — if (!he || lockhistfile(fn, 0)) return;
4702 if he.is_none() || lockhistfile(Some(&path), 0) != 0 {
4703 return;
4704 }
4705 // c:2942-2943 — too many lines already: drop to a full rewrite.
4706 if histfile_linect.load(SeqCst) > savehistsiz_v + savehistsiz_v / 5 {
4707 writeflags &= !(HFILE_FAST as i32);
4708 }
4709 } else {
4710 // c:2946-2949 — if (lockhistfile(fn, 1)) { zerr(...); return; }
4711 // (ret == 2 means "couldn't lock but proceed anyway".)
4712 let lret = lockhistfile(Some(&path), 1);
4713 if lret != 0 && lret != 2 {
4714 crate::ported::utils::zerr(&format!(
4715 "locking failed for {}: {}",
4716 path,
4717 std::io::Error::last_os_error()
4718 ));
4719 return;
4720 }
4721 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
4722 // The interactive ring is paged (extensions/history_lazy.rs): only
4723 // the newest page is loaded at startup. C's ring always holds the
4724 // whole file, so a full write from a partial ring would cut the
4725 // file down to that page. Page everything in first. A pushed stack
4726 // (histsave_stack_pos > 0) is read whole and never paged.
4727 if histsave_stack_pos.load(SeqCst) == 0 && crate::history_lazy::has_older() {
4728 crate::history_lazy::page_older_until(0);
4729 }
4730 he = ring_oldest(); // c:2950 he = hist_ring->down;
4731 }
4732
4733 // c:2952-2962 — HFILE_USE_OPTIONS derives append/skip flags from options.
4734 if writeflags & HFILE_USE_OPTIONS as i32 != 0 {
4735 if isset(APPENDHISTORY)
4736 || isset(INCAPPENDHISTORY)
4737 || isset(INCAPPENDHISTORYTIME)
4738 || isset(SHAREHISTORY)
4739 {
4740 writeflags |= (HFILE_APPEND | HFILE_SKIPOLD) as i32; // c:2955
4741 } else {
4742 histfile_linect.store(0, SeqCst); // c:2957
4743 }
4744 if isset(HISTSAVENODUPS) {
4745 writeflags |= HFILE_SKIPDUPS as i32; // c:2959
4746 }
4747 if isset(SHAREHISTORY) {
4748 extended_history = true; // c:2961
4749 }
4750 }
4751
4752 // c:2963-3016 — open the destination: append, plain truncate, or
4753 // HISTSAVEBYCOPY (write `<fn>.new` then rename over `fn`).
4754 let umpath = crate::ported::utils::unmeta(&path);
4755 let append_mode = writeflags & HFILE_APPEND as i32 != 0;
4756 let mut tmpfile: Option<String> = None;
4757 let out: Option<std::fs::File> = if append_mode {
4758 // c:2964-2967 — open(fn, O_CREAT|O_WRONLY|O_APPEND, 0600)
4759 std::fs::OpenOptions::new()
4760 .create(true)
4761 .append(true)
4762 .mode(0o600)
4763 .open(&umpath)
4764 .ok()
4765 } else if !isset(HISTSAVEBYCOPY) {
4766 // c:2968-2971 — open(fn, O_CREAT|O_WRONLY|O_TRUNC, 0600)
4767 std::fs::OpenOptions::new()
4768 .write(true)
4769 .create(true)
4770 .truncate(true)
4771 .mode(0o600)
4772 .open(&umpath)
4773 .ok()
4774 } else {
4775 // c:2972-3015 — safe write through a sibling `.new` file.
4776 let tf = format!("{}.new", umpath); // c:2973 bicat(fn, ".new")
4777 // c:2974 — unlink(tmpfile); tolerate ENOENT.
4778 let unlink_ok = match std::fs::remove_file(&tf) {
4779 Ok(()) => true,
4780 Err(e) => e.kind() == std::io::ErrorKind::NotFound,
4781 };
4782 if !unlink_ok {
4783 None // c:2975 out = NULL;
4784 } else {
4785 let old_meta = std::fs::metadata(&umpath).ok(); // c:2978 stat(fn)
4786 let euid = unsafe { libc::geteuid() };
4787 // c:2981-2985 — rewriting must not change ownership (root exempt).
4788 let owner_change = old_meta
4789 .as_ref()
4790 .map(|m| euid != 0 && m.uid() != euid)
4791 .unwrap_or(false);
4792 if owner_change {
4793 // c:2986-2996 — skip; report only when err is set.
4794 if err {
4795 if isset(APPENDHISTORY)
4796 || isset(INCAPPENDHISTORY)
4797 || isset(INCAPPENDHISTORYTIME)
4798 || isset(SHAREHISTORY)
4799 {
4800 crate::ported::utils::zerr(&format!(
4801 "rewriting {} would change its ownership -- skipped",
4802 path
4803 ));
4804 } else {
4805 crate::ported::utils::zerr(&format!(
4806 "rewriting {} would change its ownership -- history not saved",
4807 path
4808 ));
4809 }
4810 err = false; // c:2994 err = 0; — don't also report below.
4811 }
4812 None
4813 } else {
4814 // c:2998 — open(tmpfile, O_CREAT|O_WRONLY|O_EXCL, 0600)
4815 match std::fs::OpenOptions::new()
4816 .write(true)
4817 .create_new(true)
4818 .mode(0o600)
4819 .open(&tf)
4820 {
4821 Ok(f) => {
4822 // c:3007-3013 — match the original owner and mode.
4823 if let Some(ref m) = old_meta {
4824 let fd = f.as_raw_fd();
4825 unsafe {
4826 let _ = libc::fchown(fd, m.uid(), m.gid()); // c:3010
4827 let _ = libc::fchmod(fd, m.mode() as libc::mode_t);
4828 // c:3012
4829 }
4830 }
4831 tmpfile = Some(tf);
4832 Some(f)
4833 }
4834 Err(_) => None, // c:3003 out = NULL;
4835 }
4836 }
4837 }
4838 };
4839
4840 let mut ret: i32 = 0;
4841 if let Some(out) = out {
4842 // c:2965/2969/2998 — C hands the descriptor to `fdopen(..., "a"/"w")`,
4843 // so `out` is a stdio `FILE *`. Every `fprintf`/`fputc` in the
4844 // c:3030-3074 write loop therefore lands in stdio's buffer and reaches
4845 // the kernel one BUFSIZ block at a time; C's only explicit boundaries
4846 // are the `fflush` at c:3077 and the `fclose` at c:3086.
4847 //
4848 // The port wrote each entry straight to the `File`, i.e. one `write(2)`
4849 // per history line. With `share_history` (or `inc_append_history`)
4850 // `hend` calls this on EVERY accepted command (c:1193, c:1639), so a
4851 // large `$HISTFILE` turned each prompt into one syscall per saved line.
4852 // Profiled with a 100k-entry ring, `repeat 40 { fc -AI }`:
4853 // `savehistfile` was 10447 samples, of which 8826 — 85% — were inside
4854 // `write(2)` itself. `BufWriter` is the stdio buffer C already had.
4855 let mut out = std::io::BufWriter::new(out);
4856 crate::ported::mem::pushheap(); // c:3021
4857
4858 // c:3018-3027 — compile the $HISTORY_IGNORE pattern once.
4859 let histpat = crate::ported::params::getsparam("HISTORY_IGNORE").and_then(|hi| {
4860 let mut s = crate::ported::string::dupstring(&hi); // c:3024 dupstring
4861 crate::ported::glob::tokenize(&mut s); // c:3024 tokenize
4862 crate::ported::glob::remnulargs(&mut s); // c:3025 remnulargs
4863 crate::ported::pattern::patcompile(&s, 0, None) // c:3026 patcompile
4864 });
4865
4866 // Running file offset for lasthist.fpos (ftell parity). Append
4867 // continues from the current size; truncate/copy start at 0.
4868 let mut fpos: i64 = if append_mode {
4869 std::fs::metadata(&umpath)
4870 .map(|m| m.len() as i64)
4871 .unwrap_or(0)
4872 } else {
4873 0
4874 };
4875 let mut start: Option<String> = None;
4876
4877 // c:3030 — for (; he && he->histnum <= xcurhist; he = down_histent(he))
4878 let mut cur = he;
4879 while let Some(h) = cur {
4880 let entry = match ring_get(h) {
4881 Some(e) => e,
4882 None => break,
4883 };
4884 if entry.histnum > xcurhist {
4885 break; // c:3030
4886 }
4887 let flags = entry.node.flags;
4888 // c:3033-3036 — skip dup/foreign/tmpstore per the write flags.
4889 if (writeflags & HFILE_SKIPDUPS as i32 != 0 && flags & HIST_DUP as i32 != 0)
4890 || (writeflags & HFILE_SKIPFOREIGN as i32 != 0 && flags & HIST_FOREIGN as i32 != 0)
4891 || flags & HIST_TMPSTORE as i32 != 0
4892 {
4893 cur = down_histent(h);
4894 continue;
4895 }
4896 // c:3037-3040 — skip entries matching $HISTORY_IGNORE.
4897 if let Some(ref pat) = histpat {
4898 if crate::ported::pattern::pattry(
4899 pat,
4900 &crate::ported::utils::metafy(&entry.node.nam),
4901 ) {
4902 cur = down_histent(h);
4903 continue;
4904 }
4905 }
4906 // c:3041-3047 — HFILE_SKIPOLD: skip old/nowrite, else mark old.
4907 if writeflags & HFILE_SKIPOLD as i32 != 0 {
4908 if flags & (HIST_OLD | HIST_NOWRITE) as i32 != 0 {
4909 cur = down_histent(h);
4910 continue;
4911 }
4912 if let Ok(mut ring) = hist_ring.lock() {
4913 if let Some(e) = ring.iter_mut().find(|e| e.histnum == h) {
4914 e.node.flags |= HIST_OLD as i32; // c:3044
4915 }
4916 }
4917 if writeflags & HFILE_USE_OPTIONS as i32 != 0 {
4918 lasthist.lock().unwrap().next_write_ev = entry.histnum + 1; // c:3046
4919 }
4920 }
4921 // c:3048-3052 — record write bookkeeping under USE_OPTIONS.
4922 if writeflags & HFILE_USE_OPTIONS as i32 != 0 {
4923 let mut lh = lasthist.lock().unwrap();
4924 lh.fpos = fpos; // c:3049 lasthist.fpos = ftell(out)
4925 lh.stim = entry.stim; // c:3050
4926 drop(lh);
4927 histfile_linect.fetch_add(1, SeqCst); // c:3051
4928 }
4929
4930 // c:3053-3073 — emit the command text with escaping.
4931 start = Some(entry.node.nam.clone()); // c:3053 start = he->node.nam
4932 let text = entry.node.nam.as_bytes();
4933 let mut buf: Vec<u8> = Vec::with_capacity(text.len() + 24);
4934 if extended_history {
4935 // c:3054-3056 — ": %ld:%ld;" prefix (only in extended mode).
4936 let dur = if entry.ftim != 0 {
4937 entry.ftim - entry.stim
4938 } else {
4939 0
4940 };
4941 let _ = write!(buf, ": {}:{};", entry.stim, dur);
4942 } else if text.first() == Some(&b':') {
4943 buf.push(b'\\'); // c:3057-3058 escape a leading ':'
4944 }
4945 // c:3060-3067 — escape embedded newlines; track trailing '\'.
4946 let mut end_backslashes = false;
4947 for &c in text {
4948 if c == b'\n' {
4949 buf.push(b'\\'); // c:3062
4950 }
4951 end_backslashes = c == b'\\' || (end_backslashes && c == b' '); // c:3064
4952 // C writes the in-memory METAFIED text verbatim here
4953 // (metafication happened at input time, utils.c:4856);
4954 // zshrs's ring holds clean UTF-8, so metafy at write
4955 // time for byte parity with zsh's HISTFILE: IMETA bytes
4956 // ({0x00, 0x83..=0xA2}, typtab utils.c:4195-4201) are
4957 // escaped as Meta + (byte ^ 32). readhistfile's
4958 // unmetafy is the exact inverse.
4959 if crate::ported::utils::imeta_byte(c) {
4960 buf.push(0x83); // Meta
4961 buf.push(c ^ 32);
4962 } else {
4963 buf.push(c); // c:3065
4964 }
4965 }
4966 if end_backslashes {
4967 buf.push(b' '); // c:3070-3071
4968 }
4969 buf.push(b'\n'); // c:3072
4970 if out.write_all(&buf).is_err() {
4971 ret = -1; // c:3065/3072 — fputc returned < 0.
4972 break;
4973 }
4974 fpos += buf.len() as i64;
4975 cur = down_histent(h);
4976 }
4977
4978 // c:3075-3085 — final size/mtime + last-written text (USE_OPTIONS).
4979 if ret >= 0 && start.is_some() && writeflags & HFILE_USE_OPTIONS as i32 != 0 {
4980 let _ = out.flush(); // c:3077 fflush(out)
4981 if let Ok(md) = out.get_ref().metadata() {
4982 let mut lh = lasthist.lock().unwrap();
4983 lh.fsiz = md.len() as i64; // c:3079
4984 lh.mtim = md.mtime(); // c:3080
4985 }
4986 lasthist.lock().unwrap().text = start.clone(); // c:3082-3083
4987 }
4988
4989 // c:3086 — fclose(out). C caught a failed write at the `fputc` that hit
4990 // it (c:3065/3072); buffering defers the error to the flush, so surface
4991 // it here instead, and only when the loop itself had not already
4992 // failed. `BufWriter`'s own drop-flush discards errors, so the flush is
4993 // explicit.
4994 if let Err(_e) = out.flush() {
4995 if ret >= 0 {
4996 ret = -1;
4997 }
4998 }
4999 drop(out);
5000
5001 if ret >= 0 {
5002 if let Some(ref tf) = tmpfile {
5003 // c:3089-3103 — rename the temp copy over the real file.
5004 if std::fs::rename(tf, &umpath).is_err() {
5005 crate::ported::utils::zerr(&format!("can't rename {}.new to $HISTFILE", path));
5006 ret = -1; // c:3092
5007 err = false; // c:3093 err = 0;
5008 }
5009 }
5010
5011 // c:3106-3125 — SKIPOLD (and not FAST/NO_REWRITE): re-read the
5012 // just-written file capped to savehistsiz, then rewrite it
5013 // trimmed. This enforces SAVEHIST on the append/share paths.
5014 if ret >= 0
5015 && writeflags & HFILE_SKIPOLD as i32 != 0
5016 && writeflags & (HFILE_FAST | HFILE_NO_REWRITE) as i32 == 0
5017 {
5018 let remember_histactive = histactive.load(SeqCst); // c:3108
5019 histactive.store(0, SeqCst); // c:3111
5020 pushhiststack(None, savehistsiz_v, savehistsiz_v, -1); // c:3113
5021 if isset(HISTSAVENODUPS) {
5022 hist_ignore_all_dups.store(1, SeqCst); // c:3115
5023 }
5024 readhistfile(Some(&path), if err { 1 } else { 0 }, 0); // c:3116
5025 hist_ignore_all_dups.store(if isset(HISTIGNOREALLDUPS) { 1 } else { 0 }, SeqCst); // c:3117
5026 if errflag.load(SeqCst) & ERRFLAG_INT != 0 {
5027 ret = -1; // c:3119
5028 } else if histlinect.load(SeqCst) != 0 {
5029 savehistfile(Some(&path), 0); // c:3121
5030 }
5031 pophiststack(); // c:3123
5032 histactive.store(remember_histactive, SeqCst); // c:3124
5033 }
5034 }
5035
5036 crate::ported::mem::popheap(); // c:3128
5037 } else {
5038 ret = -1; // c:3130
5039 }
5040
5041 // c:3132-3137 — report a write failure on stderr when err is set.
5042 if ret < 0 && err {
5043 if tmpfile.is_some() {
5044 crate::ported::utils::zerr(&format!(
5045 "failed to write history file {}.new: {}",
5046 path,
5047 std::io::Error::last_os_error()
5048 ));
5049 } else {
5050 crate::ported::utils::zerr(&format!(
5051 "failed to write history file {}: {}",
5052 path,
5053 std::io::Error::last_os_error()
5054 ));
5055 }
5056 }
5057 // c:3138-3139 — free(tmpfile): the owned String drops automatically.
5058
5059 unlockhistfile(&path); // c:3141
5060}
5061
5062/// Port of `int lockhistct` from Src/hist.c. Re-entrant lock counter.
5063static lockhistct: AtomicI32 = AtomicI32::new(0);
5064
5065/// Port of `checklocktime()` from `Src/hist.c:3147`. — C decl `checklocktime(char *lockfile, long *sleep_usp, time_t then)`.
5066///
5067/// Decides what to do when a history lock file already exists, given
5068/// its mtime (`then`). Returns `-1` (give up) when the lock file's
5069/// timestamp is implausibly far in the future; otherwise either sleeps
5070/// a randomised, exponentially-increasing backoff (recent lock — owner
5071/// likely still alive) or unlinks the stale lock file, returning `0`.
5072/// `sleep_usp` is doubled on each backoff so repeated calls ramp up.
5073///
5074/// Replaces the prior ad-hoc `(path, max_age_secs) -> 1|0` orphan,
5075/// which used a `.lock` suffix, an age threshold, and no backoff —
5076/// none of which match the C contract.
5077pub fn checklocktime(lockfile: &str, sleep_usp: &mut i64, then: i64) -> i32 {
5078 // c:3147
5079 let now = zmonotime(None); // c:3149
5080
5081 if now + 10 < then {
5082 // c:3151 — File is more than 10 seconds in the future?
5083 // c:3153 — `errno = EEXIST;` set for C caller-contract parity.
5084 unsafe {
5085 #[cfg(target_os = "linux")]
5086 {
5087 *libc::__errno_location() = libc::EEXIST;
5088 }
5089 #[cfg(not(target_os = "linux"))]
5090 {
5091 *libc::__error() = libc::EEXIST;
5092 }
5093 }
5094 return -1; // c:3154
5095 }
5096
5097 if now - then < 10 {
5098 // c:3157-3168 — gradually increasing backoff: sleep based on
5099 // the time spent so far, randomised to minimise clashes with
5100 // shells exiting at the same time.
5101 DPUTS!(now < then, "time flowing backwards through history"); // c:3162
5102 zsleep_random(*sleep_usp, then + 10); // c:3167
5103 *sleep_usp <<= 1; // c:3168 `*sleep_usp <<= 1;`
5104 } else {
5105 // c:3170 — `unlink(lockfile);` — the lock is stale, remove it.
5106 let _ = std::fs::remove_file(lockfile);
5107 }
5108
5109 0 // c:3172
5110}
5111
5112/// Port of `lockhistfile()` from `Src/hist.c:3182`. — C decl `lockhistfile(char *fn, int keep_trying)`.
5113/// Rust idiom replacement: `fs2::FileExt::try_lock_exclusive` covers
5114/// the C `flock` + `link`-symlink retry loop; the `keep_trying`
5115/// arg controls retry budget rather than mode flags.
5116pub fn lockhistfile(fn_path: Option<&str>, keep_trying: i32) -> i32 {
5117 // c:3182
5118 let path: String = match fn_path {
5119 // c:3182
5120 Some(p) => p.to_string(),
5121 None => match resolve_histfile() {
5122 Some(p) => p,
5123 None => return 1, // c:3189
5124 },
5125 };
5126 if lockhistct.fetch_add(1, SeqCst) > 0 {
5127 return 0;
5128 }
5129 let max_tries = if keep_trying != 0 { 30 } else { 1 };
5130 for attempt in 0..max_tries {
5131 if flockhistfile(&path) != 0 {
5132 return 0;
5133 }
5134 if attempt + 1 < max_tries {
5135 std::thread::sleep(std::time::Duration::from_millis(100));
5136 }
5137 }
5138 lockhistct.fetch_sub(1, SeqCst);
5139 if keep_trying != 0 {
5140 2
5141 } else {
5142 1
5143 }
5144}
5145
5146/// Port of `unlockhistfile()` from `Src/hist.c:3324`. — C decl `unlockhistfile(char *fn)`.
5147pub fn unlockhistfile(path: &str) {
5148 let prev = lockhistct.fetch_sub(1, SeqCst);
5149 if prev <= 0 {
5150 lockhistct.store(0, SeqCst);
5151 return;
5152 }
5153 if prev == 1 {
5154 let lockpath = format!("{}.lock", path);
5155 let _ = std::fs::remove_file(&lockpath);
5156 }
5157}
5158
5159/// Port of `histfileIsLocked()` from `Src/hist.c:3350`. — C decl `histfileIsLocked(void)`.
5160#[allow(non_snake_case)]
5161pub fn histfileIsLocked() -> i32 {
5162 if lockhistct.load(SeqCst) > 0 {
5163 1
5164 } else {
5165 0
5166 }
5167}
5168
5169/// Port of `bufferwords()` from `Src/hist.c:3385` — C decl `bufferwords(LinkList list, char *buf, int *index, int flags)`.
5170///
5171/// Runs the real lexer over a line and returns its words, as `(z)`/`(Z)`,
5172/// history word splitting and `$historywords` need. `cursor` is None for C's
5173/// `buf != NULL` arm (c:3412-3429: the cursor sits past the end) and Some for
5174/// the `buf == NULL` editor-line arm (c:3430-3462), whose line the Rust
5175/// callers pass in; the `chline` prefix of a continuation line (c:3438-3452)
5176/// is not modelled. Returns the words and C's `*index`, the word the cursor
5177/// is in.
5178pub fn bufferwords(buf: &str, cursor: Option<usize>, flags: i32) -> (Vec<String>, usize) {
5179 use crate::ported::lex::{
5180 ctxtlex, incond, noaliases, set_incmdpos, set_incond, set_noaliases, tok, tokfd, tokstr,
5181 tokstrings, untokenize_ztokens, LEX_INPUT, LEX_LEXFLAGS, LEX_NOCOMMENTS, LEX_POS,
5182 };
5183 use crate::ported::zle::compcore::{ADDEDX, WB, WE, ZLEMETALL};
5184 use crate::ported::zsh_h::{
5185 BANG_TOK, DAMPER, DBAR, DINBRACK, DINPAR, DOUTPAR, ENDINPUT, ENVARRAY, FOR, INPAR_TOK,
5186 IS_REDIROP, LEXERR, LEXFLAGS_ACTIVE, LEXFLAGS_COMMENTS_KEEP, LEXFLAGS_COMMENTS_STRIP,
5187 NEWLIN, OUTPAR_TOK, RCQUOTES,
5188 };
5189 let mut list: Vec<String> = Vec::new();
5190 // c:3387-3391
5191 let (mut num, mut cur, mut got) = (0i32, -1i32, false);
5192 let ne = *crate::ported::utils::noerrs_lock().lock().unwrap();
5193 let (owb, owe, oadx) = (WB.load(SeqCst), WE.load(SeqCst), ADDEDX.load(SeqCst));
5194 let (onc, ona) = (LEX_NOCOMMENTS.get(), noaliases());
5195 let (ocs, oll) = (ZLEMETACS.load(SeqCst), ZLEMETALL.load(SeqCst));
5196 let mut forloop = 0i32;
5197 let rcquotes = crate::ported::zsh_h::isset(RCQUOTES);
5198 let mut envarray = false;
5199
5200 // c:3397-3402 — "With RC_QUOTES, 'foo '' bar' comes back as 'foo ' bar'.
5201 // That's not very useful." — the option is off for the duration.
5202 crate::ported::options::opt_state_set("rcquotes", false);
5203 ADDEDX.store(0, SeqCst); // c:3403
5204 crate::ported::utils::set_noerrs(1); // c:3404
5205 crate::ported::context::zcontext_save(); // c:3405
5206 LEX_LEXFLAGS.set(flags | LEXFLAGS_ACTIVE); // c:3406
5207 // c:3410-3411
5208 LEX_NOCOMMENTS.set(flags & (LEXFLAGS_COMMENTS_KEEP | LEXFLAGS_COMMENTS_STRIP) == 0);
5209 // c:3415-3426 — the copy with a space appended (`addedspaceptr`).
5210 let mut p = String::with_capacity(buf.len() + 1);
5211 p.push_str(buf);
5212 p.push(' ');
5213 // The lexer reads LEX_INPUT ahead of the input stack: park it so hgetc
5214 // takes the pushed frame, exactly as parsestrnoerr does (lex.rs).
5215 let saved_lex_input = LEX_INPUT.with_borrow_mut(std::mem::take);
5216 let saved_lex_pos = LEX_POS.replace(0);
5217 LEX_LEXSTOP.set(false);
5218 crate::ported::input::inpush(&p, 0, None); // c:3427 / c:3459
5219 match cursor {
5220 None => {
5221 // c:3428-3429 — `zlemetall = strlen(p); zlemetacs = zlemetall + 1;`
5222 let ll = p.chars().count() as i32;
5223 ZLEMETALL.store(ll, SeqCst);
5224 ZLEMETACS.store(ll + 1, SeqCst);
5225 }
5226 Some(cs) => {
5227 // c:3435-3436 — `zlemetall = ll + 1; zlemetacs = cs;`
5228 ZLEMETALL.store(buf.chars().count() as i32 + 1, SeqCst);
5229 ZLEMETACS.store(cs as i32, SeqCst);
5230 }
5231 }
5232 // c:3463-3464
5233 if ZLEMETACS.load(SeqCst) != 0 {
5234 ZLEMETACS.fetch_sub(1, SeqCst);
5235 }
5236 strinbeg(0); // c:3465
5237 set_noaliases(true); // c:3466
5238 loop {
5239 // c:3468-3471
5240 if incond() != 0 {
5241 let t = tok();
5242 let rest = t != DINBRACK && t != INPAR_TOK && t != DBAR && t != DAMPER && t != BANG_TOK;
5243 set_incond(1 + rest as i32);
5244 }
5245 ctxtlex(); // c:3472
5246 let t = tok();
5247 if t == ENDINPUT || t == LEXERR {
5248 break; // c:3473-3474
5249 }
5250 // c:3479-3482 — after an array assignment, back to start-of-command.
5251 if t == OUTPAR_TOK && envarray {
5252 set_incmdpos(true);
5253 envarray = false;
5254 }
5255 // c:3483-3515 — `for (( a ; b ; c ))` arrives as FOR, DINPAR, DINPAR,
5256 // DINPAR, DOUTPAR; `forloop` counts the stages down.
5257 if t == FOR {
5258 forloop = 5;
5259 } else {
5260 match forloop {
5261 1 => {
5262 if t != DOUTPAR {
5263 forloop = 0;
5264 }
5265 }
5266 2 | 3 | 4 => {
5267 if t != DINPAR {
5268 forloop = 0;
5269 }
5270 }
5271 _ => {}
5272 }
5273 }
5274 if let Some(ts) = tokstr() {
5275 // c:3516-3541
5276 let raw = match t {
5277 ENVARRAY => {
5278 envarray = true;
5279 format!("{}=(", ts)
5280 }
5281 DINPAR if forloop != 0 => format!("{};", ts),
5282 DINPAR => format!("(({}))", ts),
5283 _ => ts,
5284 };
5285 if !raw.is_empty() {
5286 // c:3543 `untokenize(p);` — every token back to its source char.
5287 let mut w = untokenize_ztokens(&raw);
5288 // c:3544-3558 — read past the added space: drop it again.
5289 // `ingetptr() == addedspaceptr + 1` holds only when the
5290 // added space was CONSUMED. C's `hungetc` returns a
5291 // word-terminating space to `inbuf`, so an un-got space
5292 // leaves `ingetptr()` at `addedspaceptr`; this port queues it
5293 // in `LEX_UNGET_BUF` instead, so a pending pushback means the
5294 // space is still unread. Without that, `a\ ` lost its
5295 // escaped space: the terminator was the added one, un-got.
5296 if crate::ported::input::ingetptr().is_empty()
5297 && crate::ported::lex::LEX_UNGET_BUF.with_borrow(|b| b.is_empty())
5298 && w.ends_with(' ')
5299 {
5300 w.pop();
5301 }
5302 list.push(w); // c:3559
5303 num += 1; // c:3560
5304 }
5305 } else if cursor.is_none() {
5306 // c:3562-3572 — `else if (buf)`: punctuation words by their text.
5307 if IS_REDIROP(t) && tokfd() >= 0 {
5308 let text = tokstrings.get(t as usize).copied().flatten().unwrap_or("");
5309 list.push(format!("{}{}", tokfd(), text)); // c:3563-3567
5310 num += 1;
5311 } else if t != NEWLIN {
5312 if let Some(text) = tokstrings.get(t as usize).copied().flatten() {
5313 list.push(text.to_string()); // c:3568-3571
5314 num += 1;
5315 }
5316 }
5317 }
5318 // c:3573-3582
5319 if forloop != 0 {
5320 if forloop == 1 {
5321 list.push("))".to_string());
5322 }
5323 forloop -= 1;
5324 }
5325 // c:3583-3586
5326 if !got && LEX_LEXFLAGS.get() == 0 {
5327 got = true;
5328 cur = num - 1;
5329 }
5330 if errflag.load(SeqCst) & ERRFLAG_INT != 0 {
5331 break; // c:3587
5332 }
5333 }
5334 // c:3588-3601 — a lexer error keeps the unfinished rest as one word.
5335 if cursor.is_none() && tok() == LEXERR {
5336 if let Some(ts) = tokstr().filter(|s| !s.is_empty()) {
5337 let mut w = untokenize_ztokens(&ts);
5338 if w.ends_with(' ') {
5339 w.pop();
5340 }
5341 list.push(w);
5342 num += 1;
5343 }
5344 }
5345 // c:3602-3603
5346 if cur < 0 && num > 0 {
5347 cur = num - 1;
5348 }
5349 set_noaliases(ona); // c:3604
5350 strinend(); // c:3605
5351 crate::ported::input::inpop(); // c:3606
5352 errflag.fetch_and(!ERRFLAG_ERROR, SeqCst); // c:3607
5353 LEX_NOCOMMENTS.set(onc); // c:3608
5354 crate::ported::utils::set_noerrs(ne); // c:3609
5355 LEX_INPUT.with_borrow_mut(|b| *b = saved_lex_input);
5356 LEX_POS.set(saved_lex_pos);
5357 crate::ported::context::zcontext_restore(); // c:3610
5358 ZLEMETACS.store(ocs, SeqCst); // c:3611
5359 ZLEMETALL.store(oll, SeqCst); // c:3612
5360 WB.store(owb, SeqCst); // c:3613
5361 WE.store(owe, SeqCst); // c:3614
5362 ADDEDX.store(oadx, SeqCst); // c:3615
5363 crate::ported::options::opt_state_set("rcquotes", rcquotes); // c:3616
5364 (list, cur.max(0) as usize) // c:3618-3621
5365}
5366
5367/// Port of `histsplitwords()` from `Src/hist.c:3650`. — C decl `histsplitwords(char *lineptr, short **wordsp, int *nwordsp, int *nwordposp, int uselex)`.
5368///
5369/// Returns word (start, end) byte-offset pairs. When `uselex == true`,
5370/// runs the lex tokenizer (`bufferwords`) and matches each lexed
5371/// token's text against `line` to recover its position — same shape
5372/// as the C body's outer for-loop over the wordlist (c:3671-3800).
5373/// On lex mismatch (the `bad = 1` branch at c:3776), falls back to
5374/// the simple whitespace tokenizer (matches C's `lineptr = start;
5375/// uselex = 0;` retry).
5376pub fn histsplitwords(line: &str, uselex: bool) -> Vec<(usize, usize)> {
5377 // c:3650
5378 if uselex {
5379 // c:3662-3663 — wordlist = bufferwords(NULL, lineptr, NULL, LEXFLAGS_COMMENTS_KEEP);
5380 let (lexed, _) = bufferwords(line, None, crate::ported::zsh_h::LEXFLAGS_COMMENTS_KEEP);
5381
5382 let bytes = line.as_bytes();
5383 let mut lptr: usize = 0;
5384 let mut words: Vec<(usize, usize)> = Vec::with_capacity(lexed.len());
5385 let mut bad = false;
5386
5387 for word in &lexed {
5388 // c:3679-3694 — skip blanks + `\\\n` at start.
5389 while lptr < bytes.len() {
5390 let b = bytes[lptr];
5391 if b == b' ' || b == b'\t' {
5392 lptr += 1;
5393 } else if b == b'\\' && lptr + 1 < bytes.len() && bytes[lptr + 1] == b'\n' {
5394 lptr += 2;
5395 } else {
5396 break;
5397 }
5398 }
5399 let word_start = lptr;
5400 let wbytes = word.as_bytes();
5401
5402 // c:3707-3787 — match-with-backslash-newline-mid-word loop.
5403 let mut wptr: usize = 0;
5404 loop {
5405 // c:3715-3722 — semicolon vs ";;" disambiguation: a single
5406 // `;` lex token shouldn't consume both chars of `;;`.
5407 if word == ";"
5408 && lptr + 1 < bytes.len()
5409 && bytes[lptr] == b';'
5410 && bytes[lptr + 1] == b';'
5411 {
5412 // Treat this as a synthetic newline-→-`;` token: no
5413 // line consumption (c:3721 loop_next=2, continue).
5414 break;
5415 }
5416 // c:3709-3726 — strpfx fast path.
5417 if lptr + wbytes.len() - wptr <= bytes.len()
5418 && bytes[lptr..lptr + wbytes.len() - wptr] == wbytes[wptr..]
5419 {
5420 lptr += wbytes.len() - wptr;
5421 wptr = wbytes.len();
5422 break;
5423 }
5424 // c:3727-3787 — slow path: match char-by-char allowing
5425 // `\\\n` mid-word and the `!`→`|` substitution.
5426 let mut skipping = false;
5427 if word == ";" {
5428 // c:3736-3740 — newline-→-`;` synthetic token.
5429 break;
5430 }
5431 while lptr < bytes.len() {
5432 if wptr >= wbytes.len() {
5433 // c:3742-3750 — word ended before line: bail.
5434 bad = true;
5435 break;
5436 }
5437 if bytes[lptr] == wbytes[wptr] || (bytes[lptr] == b'!' && wbytes[wptr] == b'|')
5438 // c:3754-3755
5439 {
5440 lptr += 1;
5441 wptr += 1;
5442 if wptr >= wbytes.len() {
5443 break;
5444 }
5445 } else if bytes[lptr] == b'\\'
5446 && lptr + 1 < bytes.len()
5447 && bytes[lptr + 1] == b'\n'
5448 {
5449 // c:3759-3769 — \\\n mid-word, skip line break.
5450 lptr += 2;
5451 skipping = true;
5452 break;
5453 } else {
5454 bad = true;
5455 break;
5456 }
5457 }
5458 if bad || !skipping {
5459 break;
5460 }
5461 }
5462
5463 if bad {
5464 // c:3782-3786 — `lineptr = start; nwordpos = 0; uselex = 0;`
5465 // Restart with non-lex fallback.
5466 return histsplitwords(line, false);
5467 }
5468 // c:3795-3796 — words[nwordpos++] = start; words[nwordpos++] = lptr;
5469 words.push((word_start, lptr));
5470 }
5471 return words;
5472 }
5473
5474 // c:3802-3830 — non-lex path: simple whitespace + `\\\n` tokenizer.
5475 let mut words = Vec::new();
5476 let bytes = line.as_bytes();
5477 let mut lptr: usize = 0;
5478 loop {
5479 // c:3804-3811 — skip leading blanks + `\\\n`.
5480 while lptr < bytes.len() {
5481 let b = bytes[lptr];
5482 if b == b' ' || b == b'\t' || b == b'\n' {
5483 lptr += 1;
5484 } else if b == b'\\' && lptr + 1 < bytes.len() && bytes[lptr + 1] == b'\n' {
5485 lptr += 2;
5486 } else {
5487 break;
5488 }
5489 }
5490 if lptr >= bytes.len() {
5491 break;
5492 }
5493 let word_start = lptr; // c:3818
5494 // c:3819-3826 — walk until next blank; Meta-byte advances by 2.
5495 while lptr < bytes.len() {
5496 let b = bytes[lptr];
5497 if b == 0x83 /* Meta */ && lptr + 1 < bytes.len() {
5498 lptr += 2;
5499 } else if b == b' ' || b == b'\t' || b == b'\n' {
5500 break;
5501 } else {
5502 lptr += 1;
5503 }
5504 }
5505 words.push((word_start, lptr)); // c:3827
5506 }
5507 words
5508}
5509
5510/// Port of `pushhiststack()` from `Src/hist.c:3845`. — C decl `pushhiststack(char *hf, zlong hs, zlong shs, int level)`.
5511///
5512/// Saves the current history state (ring, sizes, counters, HISTFILE,
5513/// lasthist) onto the save stack, then switches to a fresh empty
5514/// history optionally backed by the new HISTFILE `hf`. Mirrored by
5515/// `pophiststack`, which restores the saved state. Returns the new
5516/// stack depth (`histsave_stack_pos`).
5517///
5518/// `inithist`'s C role (`createhisttable`, c:3890) is met by moving `histtab`
5519/// into the saved frame, which leaves a fresh empty table. C's `hist_ring == &curline` test is
5520/// `HA_ACTIVE && !HA_NOINC` here: `hbegin` links the current line only
5521/// when it does not set `HA_NOINC` (c:1163-1169), and the Vec ring never
5522/// holds the sentinel itself.
5523pub fn pushhiststack(hf: Option<&str>, hs: i64, shs: i64, level: i32) -> i32 {
5524 // c:3845
5525 // c:3848 — `int curline_in_ring = (histactive & HA_ACTIVE) && hist_ring == &curline;`
5526 let ha = histactive.load(SeqCst);
5527 let curline_in_ring = (ha & HA_ACTIVE) != 0 && (ha & HA_NOINC) == 0;
5528 if curline_in_ring {
5529 unlinkcurline(); // c:3856
5530 }
5531 // c:3862-3868 — save the OLD HISTFILE so pop can restore it. With
5532 // `hf` set, record the current HISTFILE ("" when empty/unset); with
5533 // `hf` None, record None (C `h->histfile = NULL`).
5534 let old_histfile: Option<String> = if hf.is_some() {
5535 match crate::ported::params::getsparam("HISTFILE") {
5536 Some(v) if !v.is_empty() => Some(v), // c:3863 ztrdup(HISTFILE)
5537 _ => Some(String::new()), // c:3866 h->histfile = ""
5538 }
5539 } else {
5540 None // c:3868 h->histfile = NULL
5541 };
5542 let snap = histsave {
5543 lasthist: lasthist.lock().unwrap().clone(), // c:3861 h->lasthist = lasthist
5544 histfile: old_histfile, // c:3862-3868 OLD HISTFILE
5545 histtab: std::mem::take(&mut *crate::ported::hashtable::histtab_lock().write().unwrap()), // c:3869 h->histtab = histtab (inithist then starts an empty one)
5546 hist_ring: std::mem::take(&mut *hist_ring.lock().unwrap()), // c:3870 h->hist_ring = hist_ring
5547 curhist: curhist.load(SeqCst), // c:3871 h->curhist = curhist
5548 histlinect: histlinect.load(SeqCst), // c:3872
5549 histsiz: histsiz.load(SeqCst), // c:3873
5550 savehistsiz: savehistsiz.load(SeqCst), // c:3874
5551 locallevel: level, // c:3875
5552 };
5553 histsave_stack.lock().unwrap().push(snap); // c:3859 histsave_stack[pos++] = *h
5554 histsave_stack_size.fetch_add(1, SeqCst);
5555 histsave_stack_pos.fetch_add(1, SeqCst);
5556 // c:3877 — `memset(&lasthist, 0, sizeof lasthist);`
5557 *lasthist.lock().unwrap() = histfile_stats {
5558 text: None,
5559 stim: 0,
5560 mtim: 0,
5561 fpos: 0,
5562 fsiz: 0,
5563 interrupted: 0,
5564 next_write_ev: 0,
5565 };
5566 // c:3878-3883 — switch HISTFILE to the new file (or unset it).
5567 if let Some(h) = hf {
5568 if !h.is_empty() {
5569 crate::ported::params::setsparam("HISTFILE", h); // c:3880 setsparam(HISTFILE, hf)
5570 } else {
5571 // c:3882 unsetparam("HISTFILE")
5572 let _ = crate::ported::params::paramtab()
5573 .write()
5574 .unwrap()
5575 .remove("HISTFILE");
5576 }
5577 }
5578 // c:3884 — `hist_ring = NULL`: already emptied by the `mem::take` above.
5579 curhist.store(0, SeqCst); // c:3885 curhist = histlinect = 0
5580 histlinect.store(0, SeqCst);
5581 // c:3886 — `if (zleactive) zleentry(ZLE_CMD_SET_HIST_LINE, curhist);`
5582 if crate::ported::builtins::sched::zleactive.load(SeqCst) != 0 {
5583 crate::ported::zle::zle_main::zle_main_entry(
5584 crate::ported::zsh_h::ZLE_CMD_SET_HIST_LINE,
5585 &mut crate::ported::zle::zle_main::zle_main_entry_args::SetHistLine(
5586 curhist.load(SeqCst),
5587 ),
5588 );
5589 }
5590 histsiz.store(hs, SeqCst); // c:3888 histsiz = hs
5591 savehistsiz.store(shs, SeqCst); // c:3889 savehistsiz = shs
5592 if curline_in_ring {
5593 linkcurline(); // c:3892
5594 }
5595 // c:3895 — return histsave_stack_pos.
5596 histsave_stack_pos.load(SeqCst)
5597}
5598
5599/// Port of `pophiststack()` from `Src/hist.c:3901`. — C decl `pophiststack(void)`.
5600///
5601/// C body:
5602/// ```c
5603/// if (histsave_stack_pos == 0) return 0;
5604/// if (curline_in_ring) unlinkcurline();
5605/// deletehashtable(histtab); zsfree(lasthist.text);
5606/// h = &histsave_stack[--histsave_stack_pos];
5607/// lasthist = h->lasthist;
5608/// if (h->histfile) {
5609/// if (*h->histfile) setsparam("HISTFILE", h->histfile);
5610/// else unsetparam("HISTFILE");
5611/// }
5612/// histtab = h->histtab;
5613/// hist_ring = h->hist_ring;
5614/// curhist = h->curhist;
5615/// if (zleactive) zleentry(ZLE_CMD_SET_HIST_LINE, curhist);
5616/// histlinect = h->histlinect;
5617/// histsiz = h->histsiz;
5618/// savehistsiz = h->savehistsiz;
5619/// if (curline_in_ring) linkcurline();
5620/// return histsave_stack_pos + 1;
5621/// ```
5622///
5623/// The previous Rust port skipped:
5624/// - The `histfile` paramtab restore (c:3920-3924) — HISTFILE
5625/// wasn't reverted when popping back out of a pushed stack
5626/// frame, so the user's HISTFILE could end up pointing at
5627/// the wrong file after `fc -p` / `fc -P`.
5628/// - Returning 0 on empty stack (c:3907) — the previous
5629/// port returned `()` and didn't signal "nothing to pop".
5630///
5631/// Return: 0 when nothing was popped, else `histsave_stack_pos + 1`
5632/// (the depth that WAS popped).
5633pub fn pophiststack() -> i32 {
5634 // c:3901
5635 // c:3904 — `int curline_in_ring = (histactive & HA_ACTIVE) && hist_ring == &curline;`
5636 // (see `pushhiststack` for the Vec-ring spelling of the test).
5637 let ha = histactive.load(SeqCst);
5638 let curline_in_ring = (ha & HA_ACTIVE) != 0 && (ha & HA_NOINC) == 0;
5639 if histsave_stack.lock().unwrap().is_empty() {
5640 return 0; // c:3907
5641 }
5642 if curline_in_ring {
5643 unlinkcurline(); // c:3910
5644 }
5645 let snap = match histsave_stack.lock().unwrap().pop() {
5646 Some(s) => s,
5647 None => return 0, // c:3907
5648 };
5649 // c:3913-3916 — `zsfree(lasthist.text); lasthist = h->lasthist;`
5650 *lasthist.lock().unwrap() = snap.lasthist.clone();
5651 // c:3920-3924 — restore HISTFILE via setsparam / unsetparam.
5652 if let Some(ref hf) = snap.histfile {
5653 if !hf.is_empty() {
5654 // c:3922 *h->histfile
5655 crate::ported::params::setsparam("HISTFILE", hf); // c:3922
5656 } else {
5657 // c:3923
5658 // Unset HISTFILE — Rust paramtab remove.
5659 let _ = crate::ported::params::paramtab()
5660 .write()
5661 .unwrap()
5662 .remove("HISTFILE"); // c:3923 unsetparam
5663 }
5664 }
5665 *crate::ported::hashtable::histtab_lock().write().unwrap() = snap.histtab; // c:3924 histtab = h->histtab (the pushed table was dropped, c:3912)
5666 *hist_ring.lock().unwrap() = snap.hist_ring; // c:3925
5667 curhist.store(snap.curhist, SeqCst); // c:3926
5668 // c:3927 — `if (zleactive) zleentry(ZLE_CMD_SET_HIST_LINE, curhist);`
5669 if crate::ported::builtins::sched::zleactive.load(SeqCst) != 0 {
5670 crate::ported::zle::zle_main::zle_main_entry(
5671 crate::ported::zsh_h::ZLE_CMD_SET_HIST_LINE,
5672 &mut crate::ported::zle::zle_main::zle_main_entry_args::SetHistLine(snap.curhist),
5673 );
5674 }
5675 histlinect.store(snap.histlinect, SeqCst); // c:3929
5676 histsiz.store(snap.histsiz, SeqCst); // c:3930
5677 savehistsiz.store(snap.savehistsiz, SeqCst); // c:3931
5678 histsave_stack_size.fetch_sub(1, SeqCst);
5679 histsave_stack_pos.fetch_sub(1, SeqCst);
5680 if curline_in_ring {
5681 linkcurline(); // c:3933
5682 }
5683 // c:3934 — `return histsave_stack_pos + 1;` (new pos after
5684 // decrement, plus 1 for the just-popped depth).
5685 histsave_stack_pos.load(SeqCst) + 1
5686}
5687
5688/// Port of `saveandpophiststack()` from `Src/hist.c:3947`. — C decl `saveandpophiststack(int pop_through, int writeflags)`.
5689///
5690/// C body:
5691/// ```c
5692/// if (pop_through <= 0) {
5693/// pop_through += histsave_stack_pos + 1;
5694/// if (pop_through <= 0) pop_through = 1;
5695/// }
5696/// while (pop_through > 1
5697/// && histsave_stack[pop_through-2].locallevel > locallevel)
5698/// pop_through--;
5699/// if (histsave_stack_pos < pop_through) return 0;
5700/// do {
5701/// if (!nohistsave) savehistfile(NULL, 1, writeflags);
5702/// pophiststack();
5703/// } while (histsave_stack_pos >= pop_through);
5704/// return 1;
5705/// ```
5706///
5707/// The previous Rust port had a `(writeflags)`-only signature and
5708/// just called `savehistfile + pophiststack` ONCE — dropping the
5709/// pop_through arg entirely. Callers from `endparamscope` (c:5863
5710/// passes pop_through=0) and `bin_fc -P` (builtin.c:1486 passes -1)
5711/// expect the C "pop ALL stack entries with locallevel > current"
5712/// semantic; the truncated port only popped one entry, leaving
5713/// outer-scope history-stack frames intact when a multi-level
5714/// scope exit happened.
5715///
5716/// Return: 1 if anything was popped, 0 if the stack was already
5717/// empty at the requested depth. The previous Rust signature
5718/// was `void`; callers (builtin.rs:1539) used `!saveandpophiststack(...)`
5719/// for the C-style truthy check.
5720///
5721/// WARNING: param names don't match C — Rust=(pop_through, writeflags)
5722/// matches C=(pop_through, writeflags) shape; callers updated.
5723pub fn saveandpophiststack(mut pop_through: i32, writeflags: i32) -> i32 {
5724 // c:3947
5725 let stack_pos = histsave_stack_pos.load(SeqCst);
5726 // c:3949-3953 — non-positive pop_through means "pop relative
5727 // to current pos": fold to an absolute index.
5728 if pop_through <= 0 {
5729 // c:3949
5730 pop_through += stack_pos + 1; // c:3950
5731 if pop_through <= 0 {
5732 // c:3951
5733 pop_through = 1;
5734 }
5735 }
5736 // c:3954-3956 — walk back while the entry at pop_through-2 was
5737 // saved at a deeper locallevel than the current scope.
5738 let cur_level = crate::ported::params::locallevel.load(Ordering::Relaxed) as i32;
5739 while pop_through > 1
5740 && histsave_stack
5741 .lock()
5742 .unwrap()
5743 .get((pop_through - 2) as usize)
5744 .is_some_and(|h| h.locallevel > cur_level)
5745 {
5746 pop_through -= 1; // c:3956
5747 }
5748 if stack_pos < pop_through {
5749 // c:3957
5750 return 0;
5751 }
5752 // c:3959-3962 — loop pop until we reach pop_through.
5753 loop {
5754 // c:3960-3961 — `if (!nohistsave) savehistfile(NULL, 1, writeflags);`.
5755 if crate::ported::exec::nohistsave.load(SeqCst) == 0 {
5756 savehistfile(None, writeflags);
5757 }
5758 pophiststack(); // c:3962
5759 if histsave_stack_pos.load(SeqCst) < pop_through {
5760 // c:3963
5761 break;
5762 }
5763 }
5764 1
5765}
5766// =========================================================================
5767// File-scope globals from hist.c
5768// =========================================================================
5769
5770// != 0 means history substitution is turned off // c:57
5771// (stophist is in zsh.h as an extern; we own it here.)
5772
5773/// Port of `HashTable histtab` from Src/hist.c:101.
5774/// Unused name-keeper: the live `histtab` is `hashtable::histtab_lock()`. // c:101
5775pub static histtab: Mutex<Vec<usize>> = Mutex::new(Vec::new()); // c:101
5776
5777/// Port of `mod_export Histent hist_ring` from Src/hist.c:103.
5778/// Doubly-linked ring of history entries; modelled here as a `Vec<histent>`
5779/// since each histent already has up/down pointers in the C struct.
5780pub static hist_ring: Mutex<Vec<histent>> = Mutex::new(Vec::new()); // c:103
5781
5782/// Port of `struct histent curline` from Src/hist.c:91. Sentinel
5783/// histent for the in-progress edit; spliced into the ring head by
5784/// linkcurline() and removed by unlinkcurline().
5785pub static curline: Mutex<Option<histent>> = Mutex::new(None); // c:91
5786
5787/// Port of `zlong histsiz` from Src/hist.c:108. Starts at
5788/// DEFAULT_HISTSIZE, the value setupvals assigns before anything reads it
5789/// (c:Src/init.c:1113 `histsiz = DEFAULT_HISTSIZE;`), and agrees with the
5790/// $HISTSIZE the params layer reports (params.rs histsiz_lock). At 0, the
5791/// first `fc -R` of a shell that never assigned HISTSIZE kept no entry.
5792pub static histsiz: AtomicI64 =
5793 AtomicI64::new(crate::ported::config_h::DEFAULT_HISTSIZE as i64); // c:108
5794
5795/// Port of `zlong savehistsiz` from Src/hist.c:113.
5796pub static savehistsiz: AtomicI64 = AtomicI64::new(0); // c:113
5797
5798/// Port of `int histdone` from Src/hist.c:119.
5799pub static histdone: AtomicI32 = AtomicI32::new(0); // c:119
5800
5801/// Port of `int histactive` from Src/hist.c:124.
5802pub static histactive: AtomicU32 = AtomicU32::new(0); // c:124
5803
5804/// Port of `int hist_ignore_all_dups` from Src/hist.c:130.
5805pub static hist_ignore_all_dups: AtomicI32 = AtomicI32::new(0); // c:130
5806
5807/// Port of `mod_export int hist_skip_flags` from Src/hist.c:135.
5808pub static hist_skip_flags: AtomicI32 = AtomicI32::new(0); // c:135
5809
5810/// Port of `short *chwords` from Src/hist.c:147.
5811/// Word beginning/end offsets in current history line.
5812pub static chwords: Mutex<Vec<i16>> = Mutex::new(Vec::new()); // c:147
5813
5814/// Port of `int chwordlen` from Src/hist.c:154.
5815pub static chwordlen: AtomicI32 = AtomicI32::new(0); // c:154
5816
5817/// Port of `int chwordpos` from Src/hist.c:154.
5818pub static chwordpos: AtomicI32 = AtomicI32::new(0); // c:154
5819
5820/// Port of `char *hsubl` from Src/hist.c:159.
5821/// Last `l` for `s/l/r/` history substitution.
5822pub static hsubl: Mutex<Option<String>> = Mutex::new(None); // c:159
5823
5824/// Port of `char *hsubr` from Src/hist.c:164.
5825pub static hsubr: Mutex<Option<String>> = Mutex::new(None); // c:164
5826
5827/// Port of `int hsubpatopt` from Src/hist.c:169.
5828pub static hsubpatopt: AtomicI32 = AtomicI32::new(0); // c:169
5829
5830/// Port of `mod_export char *hptr` from Src/hist.c:174.
5831/// Pointer into the history line; tracked as the byte length of `chline`.
5832pub static hptr: AtomicUsize = AtomicUsize::new(0); // c:174
5833
5834/// Port of `mod_export char *chline` from Src/hist.c:179.
5835pub static chline: Mutex<String> = Mutex::new(String::new()); // c:179
5836
5837/// Port of `mod_export char *zle_chline` from Src/hist.c:195.
5838pub static zle_chline: Mutex<Option<String>> = Mutex::new(None); // c:195
5839
5840/// Port of `int qbang` from Src/hist.c:201.
5841pub static qbang: AtomicBool = AtomicBool::new(false); // c:201
5842
5843/// Port of `int hlinesz` from Src/hist.c:206.
5844pub static hlinesz: AtomicI32 = AtomicI32::new(0); // c:206
5845
5846/// Port of `mod_export int expanding;` from Src/hist.c:65.
5847/// Non-zero while history-expansion is rewriting the current line.
5848pub static expanding: AtomicI32 = AtomicI32::new(0); // c:65
5849
5850/// Port of `mod_export int excs;` from Src/hist.c:70.
5851/// Cursor position offset accumulator used while history-expanding.
5852pub static excs: AtomicI32 = AtomicI32::new(0); // c:70
5853
5854/// Port of `mod_export int exlast;` from Src/hist.c:70.
5855/// Last `inbufct` snapshot taken at expansion start; the difference
5856/// drives the `excs` cursor advance through the rewritten line.
5857pub static exlast: AtomicI32 = AtomicI32::new(0); // c:70
5858
5859/// Port of `static struct histfile_stats lasthist` from Src/hist.c:220-226.
5860#[allow(non_camel_case_types)]
5861#[derive(Clone)]
5862pub struct histfile_stats {
5863 // c:220
5864 pub text: Option<String>, // c:221
5865 pub stim: i64, // c:222 time_t
5866 pub mtim: i64, // c:222
5867 pub fpos: i64, // c:223 off_t
5868 pub fsiz: i64, // c:223
5869 pub interrupted: i32, // c:224
5870 pub next_write_ev: i64, // c:225 zlong
5871}
5872static lasthist: Mutex<histfile_stats> = Mutex::new(histfile_stats {
5873 // c:226
5874 text: None,
5875 stim: 0,
5876 mtim: 0,
5877 fpos: 0,
5878 fsiz: 0,
5879 interrupted: 0,
5880 next_write_ev: 0,
5881});
5882
5883/// Port of `static struct histsave` from Src/hist.c:228-238.
5884#[allow(non_camel_case_types)]
5885pub struct histsave {
5886 // c:228
5887 pub lasthist: histfile_stats, // c:229
5888 pub histfile: Option<String>, // c:230
5889 pub histtab: std::collections::HashMap<String, i32>, // c:231
5890 pub hist_ring: Vec<histent>, // c:232
5891 pub curhist: i64, // c:233 zlong
5892 pub histlinect: i64, // c:234
5893 pub histsiz: i64, // c:235
5894 pub savehistsiz: i64, // c:236
5895 pub locallevel: i32, // c:237
5896}
5897
5898/// Port of `static struct histsave *histsave_stack` from Src/hist.c:238.
5899#[allow(clippy::vec_init_then_push)]
5900static histsave_stack: Mutex<Vec<histsave>> = Mutex::new(Vec::new()); // c:238
5901
5902// =========================================================================
5903// Externs from other C files used in hist.c
5904// =========================================================================
5905
5906/// Port of `int stophist` (extern from zsh.h, owned by other C files).
5907/// Track history-stop depth here so the hist module's save/restore work.
5908pub static stophist: AtomicI32 = AtomicI32::new(0);
5909
5910/// Port of `zlong curhist` (extern). Current history event number.
5911pub static curhist: AtomicI64 = AtomicI64::new(0);
5912
5913/// Port of `zlong histlinect` (extern). Number of entries currently in ring.
5914pub static histlinect: AtomicI64 = AtomicI64::new(0);
5915
5916/// Port of `char bangchar` from `Src/params.c:130`. History expansion
5917/// lead character (`!` by default).
5918pub static bangchar: AtomicI32 = AtomicI32::new(b'!' as i32);
5919
5920/// Port of `int lexstop` (declared `Src/lex.c:175`, extern in `zsh.h`) —
5921/// used by `ihgetc`/`histsubchar`/`herrflush`/`ihungetc`.
5922///
5923/// C has exactly ONE `lexstop`. zshrs splits it into two halves that are
5924/// reset in pairs (see the paired-global notes at `lex.rs:493-500` and
5925/// `input.rs:993-996`):
5926/// - `input::lexstop` — the input-side gate, `Src/input.c:322`
5927/// `if (lexstop) return ' ';`
5928/// - `lex::LEX_LEXSTOP` — the lexer-side gate on `gettok`.
5929///
5930/// `hist.rs` used to own a THIRD, private `AtomicBool` copy that nothing
5931/// outside this file ever read. That made `ihgetc`'s bad-`!`-expansion
5932/// stop (`Src/hist.c:434` `lexstop = 1;`) a write into a dead variable:
5933/// the `hgetc` bridge at `lex.rs:5069` kept seeing `input::lexstop ==
5934/// false`, accepted the `' '` that `Src/hist.c:436` returns as a real
5935/// character, and on the next refill fell through to `inputline()`
5936/// (`Src/input.c:354`) — so an interactive `print -r -- "a!b"` printed
5937/// `event not found: b` and then sat at a `dquote>` PS2 continuation
5938/// instead of discarding the line and reprompting PS1.
5939///
5940/// This zero-sized handle keeps the C spelling (`lexstop.load(…)` /
5941/// `lexstop.store(…, …)`) at every call site while addressing the real
5942/// pair: reads take the input-side half (every `lexstop` read in this
5943/// file asks "is input exhausted?"), writes set both, exactly as C's
5944/// single assignment does.
5945pub struct Lexstop;
5946
5947impl Lexstop {
5948 /// Read C's `lexstop`. The `Ordering` is accepted (and ignored) so
5949 /// call sites read like the `AtomicBool` they replace.
5950 pub fn load(&self, _order: Ordering) -> bool {
5951 crate::ported::input::lexstop.with(|c| c.get())
5952 }
5953
5954 /// Assign C's `lexstop`. One C assignment == both zshrs halves.
5955 pub fn store(&self, v: bool, _order: Ordering) {
5956 crate::ported::input::lexstop.with(|c| c.set(v));
5957 LEX_LEXSTOP.with(|c| c.set(v));
5958 }
5959}
5960
5961/// The single `lexstop` handle for this module.
5962pub static lexstop: Lexstop = Lexstop;
5963
5964/// Port of `int exit_pending` (extern). Set by SIGINT/`exit` builtin.
5965pub static exit_pending: AtomicBool = AtomicBool::new(false);
5966
5967/// Port of `int strin` from Src/hist.c — counts nested string-input
5968/// scopes (eval/source/here-string).
5969static strin: AtomicI32 = AtomicI32::new(0);
5970
5971// =========================================================================
5972// HIST_* flags (from zsh.h)
5973// =========================================================================
5974
5975// Re-export the canonical HIST_* flag bits from zsh_h.rs (which has
5976// the C-faithful values per `Src/zsh.h:2252-2258`).
5977//
5978// The previous hist.rs duplicates declared:
5979// HIST_OLD = 1 << 0 // = 0x01 (C: 0x02)
5980// HIST_DUP = 1 << 1 // = 0x02 (C: 0x08)
5981// HIST_FOREIGN = 1 << 2 // = 0x04 (C: 0x10)
5982// HIST_TMPSTORE= 1 << 3 // = 0x08 (C: 0x20 — overlapped with Rust HIST_DUP!)
5983// HIST_NOWRITE = 1 << 4 // = 0x10 (C: 0x40 — overlapped with Rust HIST_FOREIGN!)
5984//
5985// Every flag was off-by-bit-shift AND the values overlapped each
5986// other in confusing ways (Rust HIST_TMPSTORE = C HIST_DUP). Any
5987// caller importing from `crate::ported::hist::HIST_DUP` was reading
5988// or writing the wrong bit; history-file write paths would skip
5989// entries the user wanted preserved, and HIST_FOREIGN/HIST_NOWRITE
5990// gates fired against unrelated bit patterns.
5991
5992// =========================================================================
5993// CASMOD_ enum (port of zsh.h:3122-3127)
5994// =========================================================================
5995
5996/// Port of `enum { CASMOD_NONE, CASMOD_UPPER, CASMOD_LOWER, CASMOD_CAPS }`
5997/// from Src/zsh.h:3122.
5998// =========================================================================
5999// HISTFLAG_* (port of zsh.h)
6000// =========================================================================
6001
6002// HISTFLAG_* moved to canonical home at zsh_h.rs:2598-2601 (port of
6003// `Src/zsh.h:2270-2273`). Re-export here so existing callers using
6004// `crate::ported::hist::HISTFLAG_*` keep compiling, and the values
6005// can never diverge from zsh_h.rs.
6006//
6007// This is the same consolidation pattern applied to the prior HIST_*
6008// flag-value drift fix and the BINF/CONDF/MFF duplicates in
6009// module.rs — single source of truth for C-pinned bit values.
6010
6011/// !!! WARNING: RUST-ONLY HELPER !!!
6012///
6013/// No upstream C counterpart — this name exists nowhere in `Src/`.
6014/// C inlines `getsparam("HISTFILE")` at each site that needs the
6015/// history file and its `fn` argument is NULL: `hbegin` (`Src/hist.c:1192`),
6016/// `hend` (`Src/hist.c:1528`), `readhistfile` (`Src/hist.c:2687`),
6017/// `savehistfile` (`Src/hist.c:2932`), `lockhistfile` (`Src/hist.c:3188`),
6018/// `unlockhistfile` (`Src/hist.c:3326`) and `pushhiststack`
6019/// (`Src/hist.c:3863`). Approved Rust-only helper:
6020/// `tests/data/fake_fn_allowlist.txt:831`.
6021/// Deliberately left uncited so the port audit keeps reporting it.
6022/// Direct port of C's `getsparam("HISTFILE")` lookup used inside
6023/// `lockhistfile()` (c:3188) and `readhistfile()` / `savehistfile()`
6024/// when their `fn` arg is NULL. C reads from paramtab; was reading
6025/// the OS env which never carries the shell-private HISTFILE param.
6026///
6027/// Outside `--zsh` mode this returns None: `$HISTFILE` is zsh's file and
6028/// zshrs's history lives in the SQLite store
6029/// (`extensions/history.rs::histfile_writable`). Every implicit
6030/// read/write/lock then takes its HISTFILE-unset path — the startup read
6031/// hydrates from the store's text mirror, and nothing writes the file.
6032fn resolve_histfile() -> Option<String> {
6033 if !crate::history::histfile_writable() {
6034 return None;
6035 }
6036 crate::ported::params::getsparam("HISTFILE")
6037}
6038
6039// =========================================================================
6040// Helper inline accessors for the ring (private — match C internal use)
6041// =========================================================================
6042
6043/// !!! WARNING: RUST-ONLY HELPER !!!
6044///
6045/// No upstream C counterpart — this name exists nowhere in `Src/`.
6046/// C's ring is the circular doubly-linked `Histent` list rooted at
6047/// `mod_export Histent hist_ring;` (`Src/hist.c:103`), so C walks it
6048/// with `he->up` / `he->down` pointer chases inline at every call site
6049/// (e.g. `gethistent`, `Src/hist.c:1318`). zshrs stores the ring as a
6050/// `Vec<histent>` under a `Mutex`, so the pointer chase becomes this
6051/// lookup. Approved Rust-only helper:
6052/// `tests/data/fake_fn_allowlist.txt:832`.
6053/// Deliberately left uncited so the port audit keeps reporting it.
6054fn ring_get(ev: i64) -> Option<histent> {
6055 let ring = hist_ring.lock().unwrap();
6056 // hist_ring is strictly DESCENDING by histnum: entries are only ever added
6057 // via `insert(0, ...)` with the monotonically increasing `curhist` counter,
6058 // so index 0 holds the newest (highest) histnum. A linear scan here walked
6059 // all ~40k entries on every lookup — and the full ring on every miss — which
6060 // made each prompt slower as history grew (profiled hot path: ring_get →
6061 // Iter::next). Binary search is O(log n); the comparator is `ev.cmp(elem)`
6062 // to match the descending order.
6063 match ring.binary_search_by(|h| ev.cmp(&h.histnum)) {
6064 Ok(idx) => Some(clone_histent(&ring[idx])),
6065 Err(_) => None,
6066 }
6067}
6068
6069/// !!! WARNING: RUST-ONLY HELPER !!!
6070///
6071/// No upstream C counterpart — this name exists nowhere in `Src/`.
6072/// C hands out a borrowed `Histent` pointer straight into the ring
6073/// (`struct histent`, `Src/zsh.h:2234`); Rust's `Mutex` guard cannot
6074/// outlive the lock, so readers get an owned copy instead. Approved
6075/// Rust-only helper: `tests/data/fake_fn_allowlist.txt:833`.
6076/// Deliberately left uncited so the port audit keeps reporting it.
6077fn clone_histent(h: &histent) -> histent {
6078 histent {
6079 node: hashnode {
6080 next: None,
6081 nam: h.node.nam.clone(),
6082 flags: h.node.flags,
6083 },
6084 up: None,
6085 down: None,
6086 zle_text: h.zle_text.clone(),
6087 stim: h.stim,
6088 ftim: h.ftim,
6089 words: h.words.clone(),
6090 nwords: h.nwords,
6091 histnum: h.histnum,
6092 }
6093}
6094
6095/// !!! WARNING: RUST-ONLY HELPER !!!
6096///
6097/// No upstream C counterpart — this name exists nowhere in `Src/`.
6098/// C has no index into `hist_ring` (`Src/hist.c:103`) at all: it holds
6099/// the `Histent` pointer itself and moves with `up`/`down`
6100/// (`up_histent`, `Src/hist.c:1304`). Approved Rust-only helper:
6101/// `tests/data/fake_fn_allowlist.txt:834`.
6102/// Deliberately left uncited so the port audit keeps reporting it.
6103fn ring_position(ev: i64) -> Option<usize> {
6104 // hist_ring is strictly descending by histnum (see ring_get); O(log n)
6105 // binary search instead of an O(n) linear `position` scan.
6106 hist_ring
6107 .lock()
6108 .unwrap()
6109 .binary_search_by(|h| ev.cmp(&h.histnum))
6110 .ok()
6111}
6112
6113/// !!! WARNING: RUST-ONLY HELPER !!!
6114///
6115/// No upstream C counterpart — this name exists nowhere in `Src/`.
6116/// C reads `he->histnum` off the `Histent` it already holds
6117/// (`struct histent`, `Src/zsh.h:2234`); the Vec-backed ring needs an
6118/// index read under the lock. Approved Rust-only helper:
6119/// `tests/data/fake_fn_allowlist.txt:835`.
6120/// Deliberately left uncited so the port audit keeps reporting it.
6121fn ring_at(idx: usize) -> i64 {
6122 hist_ring.lock().unwrap()[idx].histnum
6123}
6124
6125/// !!! WARNING: RUST-ONLY HELPER !!!
6126///
6127/// No upstream C counterpart — this name exists nowhere in `Src/`.
6128/// C tracks the ring size in the global `zlong histlinect`
6129/// (`Src/hist.c` extern, declared in `Src/zsh.h`) rather than measuring
6130/// the list. Approved Rust-only helper:
6131/// `tests/data/fake_fn_allowlist.txt:836`.
6132/// Deliberately left uncited so the port audit keeps reporting it.
6133fn ring_len() -> usize {
6134 hist_ring.lock().unwrap().len()
6135}
6136
6137/// !!! WARNING: RUST-ONLY HELPER !!!
6138///
6139/// No upstream C counterpart — this name exists nowhere in `Src/`.
6140/// C spells the oldest entry `hist_ring->down` inline, as the
6141/// `firsthist()` macro does (`Src/zsh.h:594`). Approved Rust-only
6142/// helper: `tests/data/fake_fn_allowlist.txt:837`.
6143/// Deliberately left uncited so the port audit keeps reporting it.
6144fn ring_oldest() -> Option<i64> {
6145 hist_ring.lock().unwrap().last().map(|h| h.histnum)
6146}
6147
6148/// !!! WARNING: RUST-ONLY HELPER !!!
6149///
6150/// No upstream C counterpart — this name exists nowhere in `Src/`.
6151/// C spells the newest entry as `hist_ring` itself
6152/// (`mod_export Histent hist_ring;`, `Src/hist.c:103`) and dereferences
6153/// it in place. Approved Rust-only helper:
6154/// `tests/data/fake_fn_allowlist.txt:838`.
6155/// Deliberately left uncited so the port audit keeps reporting it.
6156fn ring_latest() -> Option<histent> {
6157 hist_ring.lock().unwrap().first().map(clone_histent)
6158}
6159
6160/// !!! WARNING: RUST-ONLY HELPER !!!
6161///
6162/// No upstream C counterpart — this name exists nowhere in `Src/`.
6163/// C allocates a ring entry inline in `prepnexthistent` with
6164/// `he = (Histent)zshcalloc(sizeof *he);` (`Src/hist.c:1400`) and then
6165/// assigns the fields one at a time; there is no constructor function
6166/// to port. Approved Rust-only helper:
6167/// `tests/data/fake_fn_allowlist.txt:830`.
6168/// Deliberately left uncited so the port audit keeps reporting it.
6169/// Construct a fresh `histent` for the ring. Rust-port helper —
6170/// in C every call site inlines `zshcalloc(sizeof(struct histent))`
6171/// plus field-by-field assignment (hist.c:1614/2098/...) so there
6172/// is no C function to mirror. Justified in
6173/// `tests/data/fake_fn_allowlist.txt:676`.
6174pub(crate) fn make_histent(num: i64, text: String) -> histent {
6175 let now = std::time::SystemTime::now()
6176 .duration_since(std::time::UNIX_EPOCH)
6177 .map(|d| d.as_secs() as i64)
6178 .unwrap_or(0);
6179 histent {
6180 node: hashnode {
6181 next: None,
6182 nam: text,
6183 flags: 0,
6184 },
6185 up: None,
6186 down: None,
6187 zle_text: None,
6188 stim: now,
6189 ftim: now,
6190 words: Vec::new(),
6191 nwords: 0,
6192 histnum: num,
6193 }
6194}
6195
6196/// Port of `firsthist()` from `Src/zsh.h:594`. — C decl `#define firsthist() (hist_ring? hist_ring->down->histnum : curhist)`.
6197pub fn firsthist() -> i64 {
6198 let ring = hist_ring.lock().unwrap();
6199 ring.last().map(|h| h.histnum).unwrap_or(1)
6200}
6201
6202/// !!! WARNING: RUST-ONLY HELPER !!!
6203///
6204/// No upstream C counterpart — this name exists nowhere in `Src/`.
6205/// C has no standalone modifier applier: the `:h`/`:t`/`:r`/`:e`/`:s`/
6206/// `:&`/`:q`/`:x`/`:l`/`:u`/`:A`/`:a`/`:P` chain is the `for (;;)` switch
6207/// inlined in `histsubchar` (`Src/hist.c:830-961`), which dispatches to
6208/// `chabspath` (`Src/hist.c:1878`), `chrealpath` (`Src/hist.c:1971`),
6209/// `remtpath` (`Src/hist.c:2056`), `remtext` (`Src/hist.c:2122`),
6210/// `rembutext` (`Src/hist.c:2136`), `remlpaths` (`Src/hist.c:2152`),
6211/// `subst` (`Src/hist.c:2336`), `quote` (`Src/hist.c:2486`),
6212/// `quotebreak` (`Src/hist.c:2527`) and `casemodify` (`Src/hist.c:2196`)
6213/// — all of which ARE ported above. This wrapper drives the same
6214/// dispatch for the parameter-expansion `${var:h}` path, which reads
6215/// its modifier string from an already-expanded value rather than from
6216/// `ingetc()`. Approved Rust-only helper:
6217/// `tests/data/fake_fn_allowlist.txt:1045`.
6218/// Deliberately left uncited so the port audit keeps reporting it.
6219/// Apply chained history modifiers `:X:Y...` to `val`.
6220/// Direct port of the modifier-loop body in `Src/hist.c:830-961`
6221/// (the `for (;;)` switch on `:`-prefixed mod chars). Each branch
6222/// dispatches via `chabspath`/`chrealpath`/`equalsubstr`/`remtpath`/
6223/// `rembutext`/`remtext`/`remlpaths`/`subst`/`quote`/`casemodify`/
6224/// `xsymlink`. Free fn — no executor state needed.
6225/// Apply zsh history-style modifiers to a value
6226/// Modifiers can be chained: :A:h:h
6227pub fn apply_history_modifiers(val: &str, modifiers: &str) -> String {
6228 let mut result = val.to_string();
6229 let mut chars = modifiers.chars().peekable();
6230
6231 while let Some(c) = chars.next() {
6232 match c {
6233 ':' => continue,
6234 'A' => {
6235 if let Ok(abs) = std::fs::canonicalize(&result) {
6236 result = abs.to_string_lossy().to_string();
6237 } else {
6238 // canonicalize() requires the path to exist. For
6239 // non-existent paths zsh still removes `./` and
6240 // resolves `..` lexically — `./foo` → `<cwd>/foo`,
6241 // not `<cwd>/./foo`. Without this normalization,
6242 // `${a:A}` for `a=./foo` left the `./` segment in
6243 // the output even after the cwd-prefix.
6244 let joined = if result.starts_with('/') {
6245 std::path::PathBuf::from(&result)
6246 } else if let Ok(cwd) = std::env::current_dir() {
6247 cwd.join(&result)
6248 } else {
6249 std::path::PathBuf::from(&result)
6250 };
6251 let mut parts: Vec<String> = Vec::new();
6252 for comp in joined.components() {
6253 match comp {
6254 CurDir => {}
6255 ParentDir => {
6256 parts.pop();
6257 }
6258 Normal(s) => parts.push(s.to_string_lossy().to_string()),
6259 RootDir => parts.insert(0, String::new()),
6260 Prefix(p) => {
6261 parts.insert(0, p.as_os_str().to_string_lossy().to_string())
6262 }
6263 }
6264 }
6265 result = parts.join("/");
6266 if result.is_empty() {
6267 result = "/".to_string();
6268 }
6269 }
6270 }
6271 'a' => {
6272 if !result.starts_with('/') {
6273 if let Ok(cwd) = std::env::current_dir() {
6274 result = cwd.join(&result).to_string_lossy().to_string();
6275 }
6276 }
6277 }
6278 'h' => {
6279 // zsh strips trailing slashes BEFORE applying head:
6280 // `/tmp/` :h is `/`, not `/tmp`. Repeatedly trim
6281 // trailing `/` first, then drop the last segment.
6282 let trimmed = result.trim_end_matches('/');
6283 if trimmed.is_empty() {
6284 // Pure-slash input (`/`, `//`, …) — head is `/`.
6285 result = "/".to_string();
6286 } else if let Some(pos) = trimmed.rfind('/') {
6287 if pos == 0 {
6288 result = "/".to_string();
6289 } else {
6290 result = trimmed[..pos].to_string();
6291 }
6292 } else {
6293 result = ".".to_string();
6294 }
6295 }
6296 't' => {
6297 // Mirror zsh: strip trailing slashes before tail
6298 // extraction so `foo/` :t is `foo`, not the empty
6299 // segment after the slash.
6300 let trimmed = result.trim_end_matches('/');
6301 if let Some(pos) = trimmed.rfind('/') {
6302 result = trimmed[pos + 1..].to_string();
6303 } else {
6304 result = trimmed.to_string();
6305 }
6306 }
6307 'r' => {
6308 if let Some(dot_pos) = result.rfind('.') {
6309 let slash_pos = result.rfind('/').map(|p| p + 1).unwrap_or(0);
6310 if dot_pos > slash_pos {
6311 result = result[..dot_pos].to_string();
6312 }
6313 }
6314 }
6315 'e' => {
6316 if let Some(dot_pos) = result.rfind('.') {
6317 let slash_pos = result.rfind('/').map(|p| p + 1).unwrap_or(0);
6318 if dot_pos > slash_pos {
6319 result = result[dot_pos + 1..].to_string();
6320 } else {
6321 result = String::new();
6322 }
6323 } else {
6324 result = String::new();
6325 }
6326 }
6327 'l' => {
6328 // `:l` lowercase. Direct port of
6329 // src/zsh/Src/hist.c:931-933 — calls casemodify
6330 // with CASMOD_LOWER. Use the faithful
6331 // casemodify port instead of plain to_lowercase
6332 // for Unicode-correct multibyte handling.
6333 result = casemodify(&result, CASMOD_LOWER);
6334 }
6335 'u' => {
6336 // `:u` uppercase. Port of src/zsh/Src/hist.c:934-936.
6337 result = casemodify(&result, CASMOD_UPPER);
6338 }
6339 'C' => {
6340 // `:C` capitalize. zsh-only modifier per
6341 // hist.c (see CASMOD_CAPS dispatch via
6342 // casemodify). The history-modifier loop's
6343 // legacy path didn't recognize `:C` — only the
6344 // `(C)` parameter flag did. Same semantics:
6345 // word-aware capitalization with mid-word
6346 // lowercase enforcement.
6347 result = casemodify(&result, CASMOD_CAPS);
6348 }
6349 'q' => {
6350 // zsh `:q` uses backslash quoting, not single-bslashquote
6351 // wrapping. Each shell-meta char gets a `\` prefix.
6352 let mut out = String::with_capacity(result.len() + 8);
6353 for ch in result.chars() {
6354 if " \t\n'\"\\$`;|&<>()[]{}*?#~!".contains(ch) {
6355 out.push('\\');
6356 }
6357 out.push(ch);
6358 }
6359 result = out;
6360 }
6361 'x' => {
6362 // `:x` bslashquote with word breaks. Direct port of
6363 // src/zsh/Src/hist.c:2527-2556 quotebreak —
6364 // wraps the value in single quotes, escapes
6365 // internal `'` as `'\''`, AND closes-then-reopens
6366 // SQ around each whitespace char (so `hello world`
6367 // becomes `'hello' 'world'`). Already ported as a
6368 // standalone helper in zle_hist.
6369 result = quotebreak(&result);
6370 }
6371 'Q' => {
6372 // Same shell-bslashquote-remove as the other :Q path
6373 // (hist.c remquote): strips matching `'`/`"` pairs
6374 // AND backslash escapes inside or unquoted.
6375 let bytes: Vec<char> = result.chars().collect();
6376 let mut out = String::with_capacity(result.len());
6377 let mut j = 0;
6378 let mut in_dq = false;
6379 let mut in_sq = false;
6380 while j < bytes.len() {
6381 let c = bytes[j];
6382 if in_sq {
6383 if c == '\'' {
6384 in_sq = false;
6385 } else {
6386 out.push(c);
6387 }
6388 j += 1;
6389 continue;
6390 }
6391 if in_dq {
6392 if c == '"' {
6393 in_dq = false;
6394 } else if c == '\\' && j + 1 < bytes.len() {
6395 j += 1;
6396 out.push(bytes[j]);
6397 } else {
6398 out.push(c);
6399 }
6400 j += 1;
6401 continue;
6402 }
6403 match c {
6404 '\'' => in_sq = true,
6405 '"' => in_dq = true,
6406 '\\' if j + 1 < bytes.len() => {
6407 j += 1;
6408 out.push(bytes[j]);
6409 }
6410 _ => out.push(c),
6411 }
6412 j += 1;
6413 }
6414 result = out;
6415 }
6416 'P' => {
6417 if let Ok(real) = std::fs::canonicalize(&result) {
6418 result = real.to_string_lossy().to_string();
6419 }
6420 }
6421 'f' | 'F' | 'g' | 's' | '&' => {
6422 // c:3743 — `modify()` `:s/:g/:&/:f/:F` arm inlined per
6423 // build.rs invariant. `f` = repeat until no
6424 // more changes (fixed-point); `F N` = repeat
6425 // up to N times. zsh treats both as flags that
6426 // PREFIX the actual `s`/`&`/`g` modifier.
6427 let mut fixed_point = false;
6428 let mut max_iters: Option<u32> = None;
6429 let mut c = c;
6430 if c == 'f' {
6431 fixed_point = true;
6432 c = chars.next().unwrap_or(' ');
6433 } else if c == 'F' {
6434 // F takes a numeric argument: F N
6435 let mut num = String::new();
6436 while let Some(&ch) = chars.peek() {
6437 if ch.is_ascii_digit() {
6438 num.push(ch);
6439 chars.next();
6440 } else {
6441 break;
6442 }
6443 }
6444 max_iters = num.parse().ok();
6445 c = chars.next().unwrap_or(' ');
6446 }
6447 let (global, do_parse) = match c {
6448 's' => (false, true),
6449 '&' => (false, false),
6450 'g' => {
6451 // 'g'
6452 match chars.next() {
6453 Some('s') => (true, true),
6454 Some('&') => (true, false),
6455 _ => break,
6456 }
6457 }
6458 _ => break,
6459 };
6460 // c:3760 — read delimiter, parse old/new bracketed by it,
6461 // backslash-escapes for embedded delimiters.
6462 let (pat, rep) = if do_parse {
6463 let delim = chars.next().unwrap_or('/');
6464 let mut old = String::new();
6465 while let Some(&ch) = chars.peek() {
6466 if ch == delim {
6467 chars.next();
6468 break;
6469 }
6470 chars.next();
6471 if ch == '\\' {
6472 if let Some(&n) = chars.peek() {
6473 if n == delim {
6474 chars.next();
6475 old.push(delim);
6476 continue;
6477 }
6478 }
6479 }
6480 old.push(ch);
6481 }
6482 let mut new = String::new();
6483 while let Some(&ch) = chars.peek() {
6484 if ch == delim {
6485 chars.next();
6486 break;
6487 }
6488 chars.next();
6489 if ch == '\\' {
6490 if let Some(&n) = chars.peek() {
6491 if n == delim {
6492 chars.next();
6493 new.push(delim);
6494 continue;
6495 }
6496 }
6497 }
6498 new.push(ch);
6499 }
6500 // c:3811 — cache for `:&`/`:g&`. Empty `old` re-uses
6501 // the previously cached value.
6502 if !old.is_empty() {
6503 LAST_SUBST_OLD.with(|c| *c.borrow_mut() = old.clone());
6504 LAST_SUBST_NEW.with(|c| *c.borrow_mut() = new.clone());
6505 }
6506 if old.is_empty() {
6507 let lo = LAST_SUBST_OLD.with(|c| c.borrow().clone());
6508 let ln = LAST_SUBST_NEW.with(|c| c.borrow().clone());
6509 (lo, ln)
6510 } else {
6511 (old, new)
6512 }
6513 } else {
6514 // c:3784 — `:&`/`:g&` reads cached last_str/last_rep.
6515 (
6516 LAST_SUBST_OLD.with(|c| c.borrow().clone()),
6517 LAST_SUBST_NEW.with(|c| c.borrow().clone()),
6518 )
6519 };
6520 if !pat.is_empty() {
6521 // c:3830 — `subststr(s, &pat, &rep, gbal)`.
6522 let apply = |s: &str| -> String {
6523 if global {
6524 s.replace(&pat, &rep)
6525 } else {
6526 s.replacen(&pat, &rep, 1)
6527 }
6528 };
6529 if fixed_point {
6530 // c:Src/hist.c — `:f` repeats until no more
6531 // changes. Cap at a generous safety bound to
6532 // avoid pathological growth on a replacement
6533 // that re-introduces the pattern.
6534 let cap: u32 = 10_000;
6535 for _ in 0..cap {
6536 let next = apply(&result);
6537 if next == result {
6538 break;
6539 }
6540 result = next;
6541 }
6542 } else if let Some(n) = max_iters {
6543 for _ in 0..n {
6544 let next = apply(&result);
6545 if next == result {
6546 break;
6547 }
6548 result = next;
6549 }
6550 } else {
6551 result = apply(&result);
6552 }
6553 }
6554 }
6555 // Bash-only modifiers — zsh rejects with "unrecognized
6556 // modifier". Match that error format. Without these arms,
6557 // unknown modifiers silently terminated the loop and the
6558 // caller saw the previous-stage value (often empty).
6559 // `F` is NOT in this set — zsh accepts `F N` as a
6560 // bounded-iteration prefix for `:s` (handled above).
6561 'U' | 'L' | 'V' | 'X' => {
6562 zerr(&format!("unrecognized modifier `{}'", c));
6563 result = String::new();
6564 break;
6565 }
6566 _ => break,
6567 }
6568 }
6569 result
6570}
6571
6572thread_local! {
6573 /// Port of file-static `last_str`/`last_rep` from
6574 /// `Src/subst.c::modify()` — the last `:s/old/new/` pair so `:&`
6575 /// and `:g&` can repeat it.
6576 static LAST_SUBST_OLD: std::cell::RefCell<String> =
6577 const { std::cell::RefCell::new(String::new()) };
6578 static LAST_SUBST_NEW: std::cell::RefCell<String> =
6579 const { std::cell::RefCell::new(String::new()) };
6580}
6581
6582#[cfg(test)]
6583mod chrealpath_tests {
6584 use super::*;
6585
6586 /// `chrealpath` MUST refuse a relative path (c:1999-2000 explicit
6587 /// `if (**junkptr != '/') return 0`). The C body has a hard
6588 /// comment at c:1997 that callers must pass absolute paths.
6589 /// A regression that resolved relative paths via the working
6590 /// directory would silently change history-modifier semantics
6591 /// (`!:A` against `foo.txt` would yield `/cwd/foo.txt` instead
6592 /// of the C-faithful "leave it alone").
6593 #[test]
6594 fn chrealpath_rejects_relative_path() {
6595 let _g = crate::test_util::global_state_lock();
6596 let r = chrealpath("relative/path", b'P', false); // c:1971
6597 assert!(
6598 r.is_none(),
6599 "c:1999-2000 — relative path MUST return None; got {:?}",
6600 r
6601 );
6602 }
6603
6604 /// `chrealpath` empty input returns Some(empty) — the c:1985-1986
6605 /// guard `if (!**junkptr) return 1` returns success (1) for the
6606 /// empty-string case, leaving `*junkptr` unchanged. Pin so a
6607 /// regression that returns None for empty doesn't silently break
6608 /// the caller's "this modifier didn't change anything" path.
6609 #[test]
6610 fn chrealpath_empty_returns_empty() {
6611 let _g = crate::test_util::global_state_lock();
6612 let r = chrealpath("", b'P', false); // c:1971
6613 assert_eq!(
6614 r.as_deref(),
6615 Some(""),
6616 "c:1985-1986 — empty input returns Some(empty), not None"
6617 );
6618 }
6619
6620 /// `chrealpath` MUST fall back to walking parent prefixes when
6621 /// the full path doesn't exist (c:2027-2030 + c:2046-2048).
6622 /// Implements `realpath` against a not-yet-created file:
6623 /// strip components until a parent resolves, then re-splice the
6624 /// unresolvable tail.
6625 ///
6626 /// Pin: `/tmp/<real>/nonexistent/tail` where `/tmp/<real>` exists
6627 /// resolves to `/tmp/<real>/nonexistent/tail` (the unresolvable
6628 /// tail is appended verbatim). On macOS `/tmp` is a symlink to
6629 /// `/private/tmp` so the resolved prefix carries the canonical
6630 /// form. The test creates a real subdir in tmp to anchor the
6631 /// resolution and verifies the unresolvable tail is preserved.
6632 #[test]
6633 fn chrealpath_partial_prefix_fallback() {
6634 let _g = crate::test_util::global_state_lock();
6635 let dir = tempfile::tempdir().expect("tempdir");
6636 // dir.path() is an absolute, canonicalized path.
6637 let probe = dir.path().join("nonexistent_sub/nonexistent_tail");
6638 let probe_str = probe.to_str().unwrap();
6639 let r = chrealpath(probe_str, b'P', false) // c:1971
6640 .expect("partial-prefix walk → Some");
6641 // The C semantics: prefix walks back to dir.path() (which
6642 // exists), and the tail "nonexistent_sub/nonexistent_tail"
6643 // gets re-spliced. Result must end with the unresolvable tail.
6644 assert!(
6645 r.ends_with("/nonexistent_sub/nonexistent_tail"),
6646 "c:2046-2048 — unresolvable tail must be re-spliced; got {:?}",
6647 r
6648 );
6649 // Result must START at a real absolute path (the existing prefix).
6650 // On macOS `dir.path()` might be `/var/folders/...` (already canonical).
6651 assert!(r.starts_with('/'), "result must be absolute; got {:?}", r);
6652 }
6653}
6654
6655#[cfg(test)]
6656mod histsplitwords_uselex_tests {
6657 use super::*;
6658
6659 /// `uselex=true` runs `bufferwords` and recovers byte offsets in
6660 /// `line`. `echo hi` → two words with the expected spans.
6661 #[test]
6662 fn uselex_matches_simple_words() {
6663 // bufferwords runs the real lexer, which needs the initialised
6664 // type table and options a shell sets up at startup.
6665 let _g = crate::test_util::global_state_lock();
6666 let line = "echo hi";
6667 let words = histsplitwords(line, true);
6668 assert_eq!(words, vec![(0, 4), (5, 7)]);
6669 }
6670
6671 /// `uselex=false` is the no-lex fast path: whitespace tokenizer.
6672 #[test]
6673 fn no_uselex_matches_simple_words() {
6674 let line = "echo hi";
6675 let words = histsplitwords(line, false);
6676 assert_eq!(words, vec![(0, 4), (5, 7)]);
6677 }
6678
6679 /// Pin the c:3782-3786 fallback: if the lexer disagrees with the
6680 /// raw line (forced bad case via shell-meta-only input the simple
6681 /// lex can't fully reconstruct), histsplitwords retries with
6682 /// uselex=false and returns a non-empty wordset.
6683 #[test]
6684 fn uselex_falls_back_on_lex_disagreement() {
6685 // bufferwords runs the real lexer, which needs the initialised
6686 // type table and options a shell sets up at startup.
6687 let _g = crate::test_util::global_state_lock();
6688 // bufferwords splits `a;b` into `["a", ";", "b"]`; raw line
6689 // matches each char-by-char, so this should succeed without
6690 // falling back. Pin success path.
6691 let line = "a;b";
6692 let words = histsplitwords(line, true);
6693 assert!(!words.is_empty(), "must produce at least one word");
6694 assert!(words.iter().all(|(s, e)| s < e && *e <= line.len()));
6695 }
6696
6697 /// Multi-char ops `&&`/`||`/`;;` survive the lex round-trip with
6698 /// their original byte spans.
6699 #[test]
6700 fn uselex_handles_compound_operators() {
6701 // bufferwords runs the real lexer, which needs the initialised
6702 // type table and options a shell sets up at startup.
6703 let _g = crate::test_util::global_state_lock();
6704 let line = "a && b";
6705 let words = histsplitwords(line, true);
6706 // Each word's span must lie within the line.
6707 for (s, e) in &words {
6708 assert!(*e <= line.len() && s < e);
6709 }
6710 }
6711
6712 /// c:3406 — bufferwords lexes with `lexflags = flags | LEXFLAGS_ACTIVE`,
6713 /// so the zshrs-only `intercept … { body }` capture (execution lexing
6714 /// only, src/extensions/intercepts.rs `wants_block`) must not fire:
6715 /// `(z)` / `(Z+c+)` split the body into zsh's words. Regression: the
6716 /// capture returned the body as ONE quoted word, and an unclosed body
6717 /// dropped `{` and everything after it. Expected lists are
6718 /// `zsh -f` output for `print -rl -- ${(z)s}`.
6719 #[test]
6720 fn bufferwords_splits_intercept_block_like_zsh() {
6721 let _g = crate::test_util::global_state_lock();
6722 use crate::ported::zsh_h::LEXFLAGS_COMMENTS_KEEP;
6723 let cases: &[(&str, &[&str])] = &[
6724 (
6725 "intercept before ls { echo hi }",
6726 &["intercept", "before", "ls", "{", "echo", "hi", "}"],
6727 ),
6728 (
6729 "intercept before ls { ech",
6730 &["intercept", "before", "ls", "{", "ech"],
6731 ),
6732 (
6733 "intercept around git { if x; then { y; }; fi }",
6734 &[
6735 "intercept", "around", "git", "{", "if", "x", ";", "then", "{", "y", ";",
6736 "}", ";", "fi", "}",
6737 ],
6738 ),
6739 (
6740 "intercept after * { echo $? >> log }",
6741 &["intercept", "after", "*", "{", "echo", "$?", ">>", "log", "}"],
6742 ),
6743 ];
6744 for &(line, want) in cases {
6745 for flags in [0, LEXFLAGS_COMMENTS_KEEP] {
6746 let (got, _) = bufferwords(line, None, flags);
6747 assert_eq!(got, want, "bufferwords({line:?}, flags={flags})");
6748 }
6749 }
6750 }
6751
6752 /// Trailing whitespace doesn't produce a phantom word.
6753 #[test]
6754 fn no_uselex_trailing_whitespace_no_phantom() {
6755 let words = histsplitwords("hi ", false);
6756 assert_eq!(words, vec![(0, 2)]);
6757 }
6758
6759 /// `histsplitwords(uselex=true)` MUST distinguish a lone `;` from
6760 /// the `;;` case-terminator. The c:3715-3722 special case stops
6761 /// the strpfx fast-path when the lex token is `";"` but the line
6762 /// has `";;"` — otherwise the lex token would greedily consume
6763 /// both bytes and corrupt the case-statement's word offsets.
6764 ///
6765 /// Pin the contract: `foo ;; bar` produces words pointing at
6766 /// `foo`, `;;`, and `bar` (not `foo`, `;`, mid-stream garbage).
6767 /// A regression that skips the c:3715 disambiguation would
6768 /// produce truncated spans for the `bar` word.
6769 #[test]
6770 fn uselex_distinguishes_semicolon_from_double() {
6771 // bufferwords runs the real lexer, which needs the initialised
6772 // type table and options a shell sets up at startup.
6773 let _g = crate::test_util::global_state_lock();
6774 let line = "foo ;; bar";
6775 let words = histsplitwords(line, true);
6776 // Every word span must lie within the line.
6777 for (s, e) in &words {
6778 assert!(
6779 *e <= line.len(),
6780 "word ({},{}) overflows line len {}",
6781 s,
6782 e,
6783 line.len()
6784 );
6785 assert!(s < e, "empty span ({},{})", s, e);
6786 }
6787 // The last word must be "bar" (offsets 7..10).
6788 let last = words.last().expect("at least one word");
6789 assert_eq!(
6790 &line[last.0..last.1],
6791 "bar",
6792 "c:3715-3722 — last word must be 'bar' regardless of ;; handling, got {:?}",
6793 &line[last.0..last.1]
6794 );
6795 }
6796}
6797
6798#[cfg(test)]
6799mod subst_modifier_tests {
6800 use super::*;
6801
6802 /// Tests that touch the shared chline/hptr/chwords globals
6803 /// must serialize through this Mutex — cargo's parallel test
6804 /// runner races on these atomics otherwise.
6805 fn hist_test_lock() -> &'static Mutex<()> {
6806 static L: std::sync::OnceLock<Mutex<()>> = std::sync::OnceLock::new();
6807 L.get_or_init(|| Mutex::new(()))
6808 }
6809
6810 #[test]
6811 fn s_replaces_first_occurrence() {
6812 let _g = crate::test_util::global_state_lock();
6813 // c:3743 — `:s/old/new/` single substitution.
6814 assert_eq!(
6815 apply_history_modifiers("foo bar foo", ":s/foo/baz/"),
6816 "baz bar foo"
6817 );
6818 }
6819
6820 #[test]
6821 fn gs_replaces_all_occurrences() {
6822 let _g = crate::test_util::global_state_lock();
6823 // c:3743 — `:gs/old/new/` global substitution.
6824 assert_eq!(
6825 apply_history_modifiers("foo bar foo", ":gs/foo/baz/"),
6826 "baz bar baz"
6827 );
6828 }
6829
6830 #[test]
6831 fn ampersand_repeats_last_subst() {
6832 let _g = crate::test_util::global_state_lock();
6833 // c:3784 — `:&` repeats the cached last_str/last_rep pair.
6834 // First call caches old="x" new="y"; second `:&` reuses it.
6835 let first = apply_history_modifiers("xxx", ":s/x/y/");
6836 let second = apply_history_modifiers("xxxx", ":&");
6837 assert_eq!(first, "yxx");
6838 assert_eq!(second, "yxxx");
6839 }
6840
6841 #[test]
6842 fn g_ampersand_repeats_last_subst_globally() {
6843 let _g = crate::test_util::global_state_lock();
6844 // c:3784 — `:g&` global form of `:&`.
6845 let _ = apply_history_modifiers("init", ":s/i/X/");
6846 // Now LAST_SUBST_OLD="i", LAST_SUBST_NEW="X". Global re-apply:
6847 assert_eq!(apply_history_modifiers("aiibii", ":g&"), "aXXbXX");
6848 }
6849
6850 #[test]
6851 fn s_alternate_delimiter() {
6852 let _g = crate::test_util::global_state_lock();
6853 // c:3760 — first char after `s` is the delimiter; not bound
6854 // to `/`.
6855 assert_eq!(apply_history_modifiers("a-b-c", ":s|-|+|"), "a+b-c");
6856 }
6857
6858 #[test]
6859 fn s_escaped_delimiter_in_pattern() {
6860 let _g = crate::test_util::global_state_lock();
6861 // c:3768 — `\/` inside the pattern emits a literal `/`.
6862 assert_eq!(apply_history_modifiers("a/b", r":s/\//#/"), "a#b");
6863 }
6864
6865 /// c:1304/1311 — `up_histent` walks the hist_ring toward newer
6866 /// entries; on an empty ring there's no walk possible. None is
6867 /// the well-defined empty state. Regression where it returns
6868 /// Some(0) would make the up-history widget enter a phantom entry.
6869 #[test]
6870 fn up_histent_on_empty_ring_is_none() {
6871 let _g = crate::test_util::global_state_lock();
6872 let snapshot: Vec<_> = hist_ring.lock().unwrap().drain(..).collect();
6873 assert!(up_histent(1).is_none());
6874 assert!(down_histent(1).is_none());
6875 hist_ring.lock().unwrap().extend(snapshot);
6876 }
6877
6878 /// c:518 (getsubsargs port) — the substitution-argument parser
6879 /// returns 1 when the input stream produces no delimiter char.
6880 /// Without any input buffered, the very first ingetc() call yields
6881 /// None → return 1 BEFORE we try to read ptr1/ptr2.
6882 #[test]
6883 fn getsubsargs_returns_one_when_no_delimiter_available() {
6884 let _g = crate::test_util::global_state_lock();
6885 let mut gbal = 0i32;
6886 let mut cflag = 0i32;
6887 // No input pre-seeded; ingetc returns None on the very first
6888 // call → ptr1 is None → return 1.
6889 let r = getsubsargs("", &mut gbal, &mut cflag);
6890 assert_eq!(r, 1, "no delimiter byte → fail-fast 1");
6891 assert_eq!(gbal, 0, "no :G suffix observed");
6892 assert_eq!(cflag, 0, "no cflag set");
6893 }
6894
6895 /// `Src/hist.c:1199` — words move left carrying the ONE byte that
6896 /// followed them, so the gaps between words shrink but nothing inside a
6897 /// word changes. zsh stores `print "a b" c` as `print "a b" c` and
6898 /// `a<TAB><TAB>b c` as `a<TAB>b c` (measured with `fc -ln -1`).
6899 #[test]
6900 fn histreduceblanks_closes_gaps_between_recorded_words_only() {
6901 let _g = crate::test_util::global_state_lock();
6902 assert_eq!(
6903 reduce_blanks("print \"a b\" c", &[0, 5, 7, 13, 16, 17], false),
6904 "print \"a b\" c",
6905 "c:1224 — a quoted word keeps its inner blanks"
6906 );
6907 assert_eq!(
6908 reduce_blanks("a\t\tb c", &[0, 1, 3, 4, 7, 8], false),
6909 "a\tb c",
6910 "c:1224 — the separator byte that follows a word is carried, tab included"
6911 );
6912 assert_eq!(
6913 reduce_blanks("a b", &[0, 1, 2, 3], false),
6914 "a b",
6915 "c:1211 — an already-reduced line returns untouched"
6916 );
6917 assert_eq!(
6918 reduce_blanks("é x", &[0, 2, 4, 5], false),
6919 "é x",
6920 "offsets are bytes; a multibyte word survives the move"
6921 );
6922 }
6923
6924 /// `Src/hist.c:1204-1205` / `:1235-1248` — the ends of the line.
6925 #[test]
6926 fn histreduceblanks_line_ends_follow_ignorespace_and_tail() {
6927 let _g = crate::test_util::global_state_lock();
6928 assert_eq!(
6929 reduce_blanks(" print lead", &[1, 6, 8, 12], false),
6930 "print lead",
6931 "c:1204 — without HIST_IGNORE_SPACE the leading space goes"
6932 );
6933 assert_eq!(
6934 reduce_blanks(" print lead", &[1, 6, 8, 12], true),
6935 " print lead",
6936 "c:1205 — HIST_IGNORE_SPACE keeps the leading spaces"
6937 );
6938 assert_eq!(
6939 reduce_blanks("x ", &[0, 1], false),
6940 "x",
6941 "c:1243 — an all-inblank tail is truncated"
6942 );
6943 assert_eq!(
6944 reduce_blanks("print a # c", &[0, 5, 6, 7], false),
6945 "print a # c",
6946 "c:1245 — a comment tail is not a word and is kept as typed"
6947 );
6948 assert_eq!(
6949 reduce_blanks("print a \r", &[0, 5, 7, 8], false),
6950 "print a \r",
6951 "c:1237 — CR is not inblank, so the tail is copied down, not cut"
6952 );
6953 }
6954
6955 /// Pin `digitcount` to its canonical C body at `Src/hist.c:573-589`.
6956 /// C reads digits FROM THE INPUT STREAM via ingetc/inungetc, NOT
6957 /// from a string argument. The previous Rust port's `(s: &str)`
6958 /// signature was a fabrication with no real caller; the C version
6959 /// is used by `:h` and `:t` modifiers at c:871/c:892 to parse
6960 /// the digit count after the modifier letter.
6961 #[test]
6962 fn digitcount_streams_from_ingetc() {
6963 let _g = crate::test_util::global_state_lock();
6964 let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
6965 // Push "42abc" into the input stream; digitcount() should
6966 // parse 42 and inungetc() the 'a'. Use inputsetline to
6967 // seed the input buffer.
6968 inputsetline("42abc", 0);
6969 let n = digitcount();
6970 assert_eq!(n, 42, "c:581 — decimal digit accumulation");
6971
6972 // Next ingetc must yield 'a' (the inungetc'd terminator).
6973 let nxt = ingetc().unwrap_or('\0');
6974 assert_eq!(nxt, 'a', "c:587 — non-digit terminator was inungetc'd");
6975
6976 // No digits at all → returns 0, inungetc's the first char.
6977 inputsetline("xyz", 0);
6978 let n = digitcount();
6979 assert_eq!(n, 0, "c:586 — non-digit first char returns 0");
6980 let nxt = ingetc().unwrap_or('\0');
6981 assert_eq!(
6982 nxt, 'x',
6983 "c:587 — even the non-digit first char is inungetc'd"
6984 );
6985 }
6986
6987 /// `hist_in_word` / `hist_is_in_word` round-trip — the state flag
6988 /// the lexer flips while accumulating a word for history. C uses
6989 /// a single int; the Rust port preserves the bit-perfect contract.
6990 #[test]
6991 fn hist_in_word_round_trips() {
6992 let _g = crate::test_util::global_state_lock();
6993 hist_in_word(1);
6994 assert_eq!(hist_is_in_word(), 1);
6995 hist_in_word(0);
6996 assert_eq!(hist_is_in_word(), 0);
6997 }
6998
6999 /// c:2122 — `remtext("path/file.ext")` returns `"path/file"` —
7000 /// strips the file extension. Used by `${var:r}`. Regression
7001 /// dropping the dirname or keeping the dot would silently corrupt
7002 /// every filename-manipulation script.
7003 #[test]
7004 fn remtext_strips_extension_keeping_dirname() {
7005 let _g = crate::test_util::global_state_lock();
7006 assert_eq!(remtext("path/file.ext"), "path/file");
7007 assert_eq!(remtext("file.ext"), "file");
7008 assert_eq!(remtext("file"), "file");
7009 }
7010
7011 /// c:2122 + Doc/Zsh/expn.yo:303-310 — the `:r` modifier strips
7012 /// "a `.` followed by any number of characters (including zero)
7013 /// that are neither `.` nor `/` and that continue to the end of
7014 /// the string." Leading dot IS an extension separator: `.bashrc`
7015 /// is wholly an extension (`.` + `bashrc`), so `:r` strips it
7016 /// entirely. The C body walks backward from end stopping at `/`
7017 /// and truncates at the first `.` found — no exception for dot
7018 /// at position 0.
7019 /// Regression target: a previous Rust port guarded with
7020 /// `dot_pos > 0` keeping leading-dot files unchanged, which
7021 /// diverges from `${.bashrc:r}` in real zsh.
7022 #[test]
7023 fn remtext_strips_leading_dot_per_zsh_doc() {
7024 let _g = crate::test_util::global_state_lock();
7025 assert_eq!(
7026 remtext(".bashrc"),
7027 "",
7028 "$.bashrc:r is an extension per Doc/Zsh/expn.yo:303"
7029 );
7030 assert_eq!(
7031 remtext("path/.bashrc"),
7032 "path/",
7033 "extension scan stops at `/`, then strips at first `.`"
7034 );
7035 }
7036
7037 /// c:2136 — `rembutext("path/file.ext")` returns `"ext"` (the
7038 /// extension, dropping the body). Counterpart to `remtext`.
7039 /// Regression returning the wrong slice would break `${file:e}`.
7040 #[test]
7041 fn rembutext_returns_extension_only() {
7042 let _g = crate::test_util::global_state_lock();
7043 assert_eq!(rembutext("path/file.ext"), "ext");
7044 assert_eq!(
7045 rembutext("file.tar.gz"),
7046 "gz",
7047 "last `.` wins (extension-only is post-LAST-dot)"
7048 );
7049 assert_eq!(rembutext("file"), "");
7050 }
7051
7052 /// c:2056 — `remtpath(path, 0)` (the `${PWD:h}` no-count case)
7053 /// removes the LAST component. `remtpath("/a/b/c", 0)` → `"/a/b"`.
7054 /// This is the canonical `:h` modifier path used by every theme
7055 /// that displays `${PWD:h}`.
7056 #[test]
7057 fn remtpath_count_zero_strips_last_component() {
7058 let _g = crate::test_util::global_state_lock();
7059 assert_eq!(remtpath("/a/b/c", 0), "/a/b");
7060 assert_eq!(remtpath("/a", 0), "/");
7061 assert_eq!(remtpath("foo", 0), ".", "no slash → returns '.'");
7062 }
7063
7064 /// c:2152 — `remlpaths(path, count)` keeps the LAST `count`
7065 /// components — counterpart to remtpath. `remlpaths("/a/b/c", 2)`
7066 /// → `"b/c"`. Drives `${PWD:t}` family.
7067 #[test]
7068 fn remlpaths_keeps_last_n_components() {
7069 let _g = crate::test_util::global_state_lock();
7070 assert_eq!(remlpaths("/a/b/c", 1), "c");
7071 assert_eq!(remlpaths("/a/b/c", 2), "b/c");
7072 assert_eq!(remlpaths("/a/b/c", 3), "a/b/c");
7073 }
7074
7075 /// c:2196 — `casemodify(s, CASMOD_LOWER)` lowercases every char.
7076 /// Regression that flips the case direction would break every
7077 /// `${(L)var}` user has.
7078 #[test]
7079 fn casemodify_lower_lowercases() {
7080 let _g = crate::test_util::global_state_lock();
7081 assert_eq!(casemodify("HELLO World", CASMOD_LOWER), "hello world");
7082 }
7083
7084 /// c:2196 — `casemodify(s, CASMOD_UPPER)` uppercases.
7085 #[test]
7086 fn casemodify_upper_uppercases() {
7087 let _g = crate::test_util::global_state_lock();
7088 assert_eq!(casemodify("hello world", CASMOD_UPPER), "HELLO WORLD");
7089 }
7090
7091 /// c:2196 — `CASMOD_CAPS` capitalises FIRST letter of each word
7092 /// (word-boundary determined by `nextupper` flag flips on
7093 /// non-alpha chars). `"hello world"` → `"Hello World"`.
7094 #[test]
7095 fn casemodify_caps_capitalises_word_starts() {
7096 let _g = crate::test_util::global_state_lock();
7097 assert_eq!(casemodify("hello world", CASMOD_CAPS), "Hello World");
7098 assert_eq!(
7099 casemodify("FOO BAR", CASMOD_CAPS),
7100 "Foo Bar",
7101 "non-first letters lowercased"
7102 );
7103 }
7104
7105 /// `Src/hist.c:2486-2523` — `quote(s)` wraps in `'...'` and
7106 /// breaks out `inblank` chars (NARROW space/tab/newline ONLY)
7107 /// by closing the current quote span, emitting the blank in its
7108 /// own `'<c>'` pair, then opening a fresh quote span. CR/FF/VT/
7109 /// NBSP should NOT be broken out — they're not inblank.
7110 #[test]
7111 fn quote_breaks_only_narrow_inblank_chars() {
7112 let _g = crate::test_util::global_state_lock();
7113 // c:2514 — close current quote, emit ` ` in its own pair, reopen.
7114 assert_eq!(
7115 quote("a b"),
7116 "'a' 'b'",
7117 "c:2514 — space broken out of single-quote span"
7118 );
7119 assert_eq!(quote("a\tb"), "'a'\t'b'");
7120 assert_eq!(quote("a\nb"), "'a'\n'b'");
7121 // CR (\r) is NOT inblank per C → must stay inside the quotes.
7122 assert_eq!(
7123 quote("a\rb"),
7124 "'a\rb'",
7125 "c:2499 — CR is NOT in C's inblank set; stays inside quotes"
7126 );
7127 // NBSP (0xA0) is NOT in narrow inblank either.
7128 assert_eq!(
7129 quote("a\u{00A0}b"),
7130 "'a\u{00A0}b'",
7131 "NBSP is not inblank; must remain inside the quote span"
7132 );
7133 }
7134
7135 /// `Src/hist.c:2527-2560` — `quotebreak(s)` same as quote but
7136 /// breaks out inblank chars regardless of inquotes state. Pin
7137 /// narrow-inblank behavior (no CR/FF/NBSP breaking).
7138 #[test]
7139 fn quotebreak_uses_narrow_inblank_set() {
7140 let _g = crate::test_util::global_state_lock();
7141 assert_eq!(quotebreak("a b"), "'a' 'b'");
7142 assert_eq!(quotebreak("a\tb"), "'a'\t'b'");
7143 assert_eq!(quotebreak("a\nb"), "'a'\n'b'");
7144 // CR — NOT inblank → stays inside.
7145 assert_eq!(
7146 quotebreak("a\rb"),
7147 "'a\rb'",
7148 "CR not in inblank set, must not be broken out"
7149 );
7150 // NBSP — NOT inblank.
7151 assert_eq!(
7152 quotebreak("a\u{00A0}b"),
7153 "'a\u{00A0}b'",
7154 "NBSP not in inblank, must not be broken out"
7155 );
7156 // Form-feed (\x0C) — NOT inblank.
7157 assert_eq!(
7158 quotebreak("a\u{000C}b"),
7159 "'a\u{000C}b'",
7160 "FF not in inblank, must not be broken out"
7161 );
7162 }
7163
7164 /// Pin: `savehistfile` short-circuits when `!interact` per
7165 /// `Src/hist.c:2932`. Non-interactive shells must not write
7166 /// to the user's HISTFILE — a script running with INTERACTIVE
7167 /// off should leave the user's history untouched even when
7168 /// passed an explicit fn_path.
7169 ///
7170 /// Also pins the `savehistsiz <= 0` short-circuit. Either
7171 /// gate firing must leave the file untouched.
7172 #[test]
7173 fn savehistfile_short_circuits_on_non_interactive() {
7174 let _g = crate::test_util::global_state_lock();
7175 let dir = tempfile::tempdir().expect("tempdir");
7176 let path = dir.path().join("hist_test");
7177 let path_str = path.to_str().unwrap();
7178 // Pre-populate with content so we can detect "untouched".
7179 std::fs::write(&path, b"PRESERVED").expect("seed write");
7180 // Force INTERACTIVE off; savehistfile must NOT touch the
7181 // file regardless of fn_path.
7182 let saved = isset(INTERACTIVE);
7183 dosetopt(INTERACTIVE, 0, 0);
7184 savehistfile(Some(path_str), 0);
7185 let after = std::fs::read(&path).expect("read after");
7186 assert_eq!(
7187 after, b"PRESERVED",
7188 "c:2932 — !interact must skip write; original content preserved"
7189 );
7190 // Restore.
7191 dosetopt(INTERACTIVE, if saved { 1 } else { 0 }, 0);
7192 }
7193
7194 /// Pin: `hgetline` per `Src/hist.c:1769-1786` truncates the
7195 /// in-flight `chline` at `hptr`, resets the globals, and
7196 /// returns the captured snippet. Returns None when chline is
7197 /// empty or hptr is at the start (C returns NULL).
7198 #[test]
7199 fn hgetline_truncates_chline_and_resets_globals() {
7200 let _g = crate::test_util::global_state_lock();
7201 let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
7202 // Save state.
7203 let saved_chline = std::mem::take(&mut *chline.lock().unwrap());
7204 let saved_hptr = hptr.swap(0, SeqCst);
7205 let saved_chwordpos = chwordpos.swap(0, SeqCst);
7206
7207 // Empty chline → None (c:1777).
7208 assert_eq!(hgetline(), None, "c:1777 — empty chline returns None");
7209
7210 // chline = "abcdef", hptr = 0 → None (c:1777 hp == 0).
7211 *chline.lock().unwrap() = "abcdef".to_string();
7212 hptr.store(0, SeqCst);
7213 assert_eq!(hgetline(), None, "c:1777 — hptr == 0 returns None");
7214
7215 // chline = "abcdef", hptr = 3 → Some("abc"), reset hptr/pos.
7216 *chline.lock().unwrap() = "abcdef".to_string();
7217 hptr.store(3, SeqCst);
7218 chwordpos.store(2, SeqCst);
7219 let result = hgetline();
7220 assert_eq!(
7221 result,
7222 Some("abc".to_string()),
7223 "c:1779 — truncate chline at hptr=3 returns 'abc'"
7224 );
7225 assert_eq!(hptr.load(SeqCst), 0, "c:1783 — hptr reset to 0");
7226 assert_eq!(chwordpos.load(SeqCst), 0, "c:1784 — chwordpos reset to 0");
7227
7228 // Restore state.
7229 *chline.lock().unwrap() = saved_chline;
7230 hptr.store(saved_hptr, SeqCst);
7231 chwordpos.store(saved_chwordpos, SeqCst);
7232 }
7233
7234 /// Pin: `histbackword` per `Src/hist.c:1711-1715` rewinds `hptr`
7235 /// to the start of the previous word ONLY when:
7236 /// 1. `chwordpos % 2 == 0` (even position — at a word
7237 /// boundary, not mid-word), AND
7238 /// 2. `chwordpos != 0` (at least one full word recorded).
7239 /// Otherwise no-op.
7240 #[test]
7241 fn histbackword_rewinds_hptr_on_even_boundary() {
7242 let _g = crate::test_util::global_state_lock();
7243 let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
7244 // Capture and reset state.
7245 let saved_pos = chwordpos.swap(0, SeqCst);
7246 let saved_hptr = hptr.swap(0, SeqCst);
7247 let saved_words = {
7248 let mut w = chwords.lock().unwrap();
7249 std::mem::take(&mut *w)
7250 };
7251 // Seed: two words at offsets [0..3] "abc" and [4..7] "def".
7252 // chwords layout: [start1, end1, start2, end2].
7253 {
7254 let mut w = chwords.lock().unwrap();
7255 *w = vec![0i16, 3, 4, 7];
7256 }
7257 // chwordpos at 4 = even = word boundary. histbackword
7258 // should set hptr = chwords[chwordpos-1] = chwords[3] = 7.
7259 chwordpos.store(4, SeqCst);
7260 hptr.store(999, SeqCst);
7261 histbackword();
7262 assert_eq!(
7263 hptr.load(SeqCst),
7264 7,
7265 "c:1715 — even chwordpos must rewind hptr to chwords[pos-1]"
7266 );
7267
7268 // chwordpos at 0 (no words recorded) — no-op.
7269 chwordpos.store(0, SeqCst);
7270 hptr.store(123, SeqCst);
7271 histbackword();
7272 assert_eq!(
7273 hptr.load(SeqCst),
7274 123,
7275 "c:1714 — chwordpos == 0 means no-op (hptr untouched)"
7276 );
7277
7278 // chwordpos at 3 (odd, mid-word) — no-op.
7279 chwordpos.store(3, SeqCst);
7280 hptr.store(456, SeqCst);
7281 histbackword();
7282 assert_eq!(
7283 hptr.load(SeqCst),
7284 456,
7285 "c:1714 — odd chwordpos means mid-word, no-op"
7286 );
7287
7288 // Restore state.
7289 chwordpos.store(saved_pos, SeqCst);
7290 hptr.store(saved_hptr, SeqCst);
7291 *chwords.lock().unwrap() = saved_words;
7292 }
7293
7294 /// Pin `ihwaddc` to the C `*hptr++ = c;` OVERWRITE semantic.
7295 /// When hptr < chline.len() (e.g. after hwrep rewinds), the byte
7296 /// goes AT the cursor, not appended. The previous Rust port used
7297 /// `String::push` which appends only — pushing past the rewound
7298 /// hptr would leave the old word bytes intact and stash new bytes
7299 /// at the chline tail.
7300 #[test]
7301 fn ihwaddc_overwrites_at_hptr_not_append() {
7302 let _g = crate::test_util::global_state_lock();
7303 let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
7304 // Save state.
7305 let saved_chline = chline.lock().unwrap().clone();
7306 let saved_hptr = hptr.load(SeqCst);
7307 let saved_errflag = errflag.load(SeqCst);
7308 let saved_lexstop = lexstop.load(SeqCst);
7309 let saved_inflags = crate::ported::input::inbufflags.with(|f| f.get());
7310 let saved_qbang = qbang.load(SeqCst);
7311 let saved_stophist = stophist.load(SeqCst);
7312 let saved_hlinesz = hlinesz.load(SeqCst);
7313
7314 // Set up chline="echo oldword extra", rewind hptr to 5
7315 // (start of "oldword"). Push "NEW" → chline must become
7316 // "echo NEWword extra" with hptr advanced to 8.
7317 *chline.lock().unwrap() = "echo oldword extra".to_string();
7318 hptr.store(5, SeqCst);
7319 errflag.store(0, SeqCst);
7320 lexstop.store(false, SeqCst);
7321 crate::ported::input::inbufflags.with(|f| f.set(0));
7322 qbang.store(false, SeqCst);
7323 stophist.store(0, SeqCst);
7324 hlinesz.store(64, SeqCst);
7325
7326 ihwaddc(b'N' as i32);
7327 ihwaddc(b'E' as i32);
7328 ihwaddc(b'W' as i32);
7329
7330 let cl = chline.lock().unwrap().clone();
7331 assert_eq!(
7332 cl.as_str(),
7333 "echo NEWword extra",
7334 "c:368 — *hptr++ = c writes AT cursor (NOT appends to end)"
7335 );
7336 assert_eq!(
7337 hptr.load(SeqCst),
7338 8,
7339 "c:368 — hptr advances over the three overwrites"
7340 );
7341
7342 // Restore.
7343 *chline.lock().unwrap() = saved_chline;
7344 hptr.store(saved_hptr, SeqCst);
7345 errflag.store(saved_errflag, SeqCst);
7346 lexstop.store(saved_lexstop, SeqCst);
7347 crate::ported::input::inbufflags.with(|f| f.set(saved_inflags));
7348 qbang.store(saved_qbang, SeqCst);
7349 stophist.store(saved_stophist, SeqCst);
7350 hlinesz.store(saved_hlinesz, SeqCst);
7351 }
7352
7353 /// Pin `ihwaddc` to its canonical C body at `Src/hist.c:355-389`.
7354 /// C: `*hptr++ = c;` writes the byte and advances the cursor.
7355 /// The previous Rust port pushed to chline but never updated
7356 /// the `hptr` global — every subsequent ihwbegin/ihwend read a
7357 /// stale hptr=0, so chwords boundary positions stayed pinned
7358 /// to 0 no matter how many chars were appended.
7359 #[test]
7360 fn ihwaddc_advances_hptr_on_each_push() {
7361 let _g = crate::test_util::global_state_lock();
7362 let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
7363 // Save state.
7364 let saved_chline = chline.lock().unwrap().clone();
7365 let saved_hptr = hptr.load(SeqCst);
7366 let saved_errflag = errflag.load(SeqCst);
7367 let saved_lexstop = lexstop.load(SeqCst);
7368 let saved_inflags = crate::ported::input::inbufflags.with(|f| f.get());
7369 let saved_qbang = qbang.load(SeqCst);
7370 let saved_bangchar = bangchar.load(SeqCst);
7371 let saved_stophist = stophist.load(SeqCst);
7372 let saved_hlinesz = hlinesz.load(SeqCst);
7373
7374 // Set up: history active, no errors, no alias-only.
7375 *chline.lock().unwrap() = "AB".to_string(); // chline must be non-empty
7376 hptr.store(2, SeqCst);
7377 errflag.store(0, SeqCst);
7378 lexstop.store(false, SeqCst);
7379 crate::ported::input::inbufflags.with(|f| f.set(0));
7380 qbang.store(false, SeqCst);
7381 stophist.store(0, SeqCst);
7382 bangchar.store(b'!' as i32, SeqCst);
7383 hlinesz.store(64, SeqCst);
7384
7385 // Push 'x' → chline grows AND hptr advances.
7386 ihwaddc(b'x' as i32);
7387 assert_eq!(
7388 chline.lock().unwrap().as_str(),
7389 "ABx",
7390 "c:368 — chline grows"
7391 );
7392 assert_eq!(hptr.load(SeqCst), 3, "c:368 — hptr advances by 1");
7393
7394 // Push 'y' → second advance.
7395 ihwaddc(b'y' as i32);
7396 assert_eq!(hptr.load(SeqCst), 4, "c:368 — hptr advances on each push");
7397
7398 // Bang-escape with qbang: qbang=true, c=bangchar → '\\' AND c
7399 // get pushed, hptr advances by 2.
7400 qbang.store(true, SeqCst);
7401 ihwaddc(b'!' as i32);
7402 assert_eq!(
7403 chline.lock().unwrap().as_str(),
7404 "ABxy\\!",
7405 "c:366 — qbang escape pushes '\\\\' before bangchar"
7406 );
7407 assert_eq!(
7408 hptr.load(SeqCst),
7409 6,
7410 "c:366+c:368 — both pushes advance hptr"
7411 );
7412
7413 // errflag set → no-op.
7414 errflag.store(1, SeqCst);
7415 let hptr_before = hptr.load(SeqCst);
7416 ihwaddc(b'z' as i32);
7417 assert_eq!(
7418 hptr.load(SeqCst),
7419 hptr_before,
7420 "c:359 — errflag short-circuits, hptr unchanged"
7421 );
7422
7423 // Restore.
7424 *chline.lock().unwrap() = saved_chline;
7425 hptr.store(saved_hptr, SeqCst);
7426 errflag.store(saved_errflag, SeqCst);
7427 lexstop.store(saved_lexstop, SeqCst);
7428 crate::ported::input::inbufflags.with(|f| f.set(saved_inflags));
7429 qbang.store(saved_qbang, SeqCst);
7430 bangchar.store(saved_bangchar, SeqCst);
7431 stophist.store(saved_stophist, SeqCst);
7432 hlinesz.store(saved_hlinesz, SeqCst);
7433 }
7434
7435 /// Pin `ihwend` to its canonical C body at `Src/hist.c:1686-1705`.
7436 /// Same family of fix as `ihwbegin`: use the `hptr` global to
7437 /// compute the cursor position, NOT `chline.len()`. C: `if (hptr
7438 /// > chline + chwords[chwordpos-1])` writes `chwords[chwordpos++]
7439 /// = hptr - chline`; the previous Rust port used chline.len()
7440 /// for the cursor.
7441 #[test]
7442 fn ihwend_uses_hptr_not_chline_len() {
7443 let _g = crate::test_util::global_state_lock();
7444 let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
7445 // Save state.
7446 let saved_chline = chline.lock().unwrap().clone();
7447 let saved_chwords = chwords.lock().unwrap().clone();
7448 let saved_chwordpos = chwordpos.load(SeqCst);
7449 let saved_hptr = hptr.load(SeqCst);
7450 let saved_stop = stophist.load(SeqCst);
7451 let saved_active = histactive.load(SeqCst);
7452 let saved_inflags = crate::ported::input::inbufflags.with(|f| f.get());
7453
7454 // Set up: chline is "ABCDEFGHIJ" (10 bytes), hptr at 7
7455 // (lexer rewound). chwordpos=1 (in-flight word that started
7456 // at chwords[0]=2). C expects chwords[1] = hptr-chline = 7,
7457 // not chline.len() = 10.
7458 *chline.lock().unwrap() = "ABCDEFGHIJ".to_string();
7459 *chwords.lock().unwrap() = vec![2, 0];
7460 chwordpos.store(1, SeqCst);
7461 hptr.store(7, SeqCst);
7462 stophist.store(0, SeqCst);
7463 histactive.store(0, SeqCst);
7464 crate::ported::input::inbufflags.with(|f| f.set(0));
7465
7466 ihwend();
7467
7468 assert_eq!(
7469 chwords.lock().unwrap().get(1).copied(),
7470 Some(7),
7471 "c:1694 — chwords[chwordpos] = hptr - chline = 7 (NOT chline.len()=10)"
7472 );
7473 assert_eq!(
7474 chwordpos.load(SeqCst),
7475 2,
7476 "c:1694 — chwordpos++ on successful close"
7477 );
7478
7479 // c:1700 scrub branch — hptr<=chwords[start] → chwordpos--.
7480 *chwords.lock().unwrap() = vec![5, 0];
7481 chwordpos.store(1, SeqCst);
7482 hptr.store(3, SeqCst); // hptr(3) <= chwords[0]=5
7483 ihwend();
7484 assert_eq!(
7485 chwordpos.load(SeqCst),
7486 0,
7487 "c:1700 — chwordpos-- when hptr <= chwords[chwordpos-1]"
7488 );
7489
7490 // Even chwordpos short-circuits (between-words, no in-flight).
7491 chwordpos.store(2, SeqCst);
7492 hptr.store(9, SeqCst);
7493 *chwords.lock().unwrap() = vec![0, 4, 5, 8];
7494 ihwend();
7495 assert_eq!(
7496 chwordpos.load(SeqCst),
7497 2,
7498 "c:1691 — even chwordpos short-circuits"
7499 );
7500
7501 // Restore.
7502 *chline.lock().unwrap() = saved_chline;
7503 *chwords.lock().unwrap() = saved_chwords;
7504 chwordpos.store(saved_chwordpos, SeqCst);
7505 hptr.store(saved_hptr, SeqCst);
7506 stophist.store(saved_stop, SeqCst);
7507 histactive.store(saved_active, SeqCst);
7508 crate::ported::input::inbufflags.with(|f| f.set(saved_inflags));
7509 }
7510
7511 /// Pin `histremovedups` to its canonical C body at
7512 /// `Src/hist.c:1252-1262`: removes entries with HIST_DUP flag
7513 /// SET, not name-based dedup. Also pins the HIST_DUP bit value
7514 /// to the canonical C value (0x08 per `Src/zsh.h:2255`) — the
7515 /// hist.rs duplicate had `1 << 1 = 0x02` which both overlapped
7516 /// HIST_OLD and missed real HIST_DUP entries.
7517 #[test]
7518 fn histremovedups_removes_flagged_entries_only() {
7519 let _g = crate::test_util::global_state_lock();
7520 let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
7521 let saved_ring = {
7522 let r = hist_ring.lock().unwrap();
7523 r.iter()
7524 .map(|h| (h.node.nam.clone(), h.histnum, h.node.flags))
7525 .collect::<Vec<_>>()
7526 };
7527 let saved_histlinect = histlinect.load(SeqCst);
7528
7529 // HIST_DUP must equal C value 0x08 (Src/zsh.h:2255).
7530 assert_eq!(
7531 HIST_DUP, 0x08,
7532 "HIST_DUP bit value must match C (0x08), got {:#x}",
7533 HIST_DUP
7534 );
7535
7536 // Build a ring with three entries, only the middle one flagged HIST_DUP.
7537 {
7538 let mut ring = hist_ring.lock().unwrap();
7539 ring.clear();
7540 for (nam, num, flags) in [
7541 ("entry1", 1i64, 0i32),
7542 ("entry2", 2i64, HIST_DUP as i32), // flagged
7543 ("entry3", 3i64, 0i32),
7544 ] {
7545 ring.push(histent {
7546 node: hashnode {
7547 next: None,
7548 nam: nam.to_string(),
7549 flags,
7550 },
7551 up: None,
7552 down: None,
7553 zle_text: None,
7554 stim: 0,
7555 ftim: 0,
7556 words: vec![],
7557 nwords: 0,
7558 histnum: num,
7559 });
7560 }
7561 }
7562 histlinect.store(3, SeqCst);
7563
7564 histremovedups();
7565
7566 {
7567 let ring = hist_ring.lock().unwrap();
7568 assert_eq!(
7569 ring.len(),
7570 2,
7571 "c:1259-1260 — only the HIST_DUP-flagged entry is removed"
7572 );
7573 assert!(ring.iter().any(|h| h.node.nam == "entry1"));
7574 assert!(ring.iter().any(|h| h.node.nam == "entry3"));
7575 assert!(!ring.iter().any(|h| h.node.nam == "entry2"));
7576 }
7577 assert_eq!(
7578 histlinect.load(SeqCst),
7579 2,
7580 "histlinect updated after removal"
7581 );
7582
7583 // Restore ring.
7584 {
7585 let mut ring = hist_ring.lock().unwrap();
7586 ring.clear();
7587 for (nam, num, flags) in saved_ring {
7588 ring.push(histent {
7589 node: hashnode {
7590 next: None,
7591 nam,
7592 flags,
7593 },
7594 up: None,
7595 down: None,
7596 zle_text: None,
7597 stim: 0,
7598 ftim: 0,
7599 words: vec![],
7600 nwords: 0,
7601 histnum: num,
7602 });
7603 }
7604 }
7605 histlinect.store(saved_histlinect, SeqCst);
7606 }
7607
7608 /// Pin `iaddtoline` to its canonical C body at
7609 /// `Src/hist.c:397-413`, specifically the c:405-411 ZLE cursor
7610 /// adjustment block: `excs > zlemetacs` → `excs += 1 + inbufct -
7611 /// exlast` with a clamp to zlemetacs.
7612 #[test]
7613 fn iaddtoline_adjusts_excs_relative_to_zlemetacs() {
7614 let _g = crate::test_util::global_state_lock();
7615 let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
7616 // Save state.
7617 let saved_chline = chline.lock().unwrap().clone();
7618 let saved_excs = excs.load(SeqCst);
7619 let saved_zlemetacs = ZLEMETACS.load(SeqCst);
7620 let saved_expanding = expanding.load(SeqCst);
7621 let saved_lexstop = lexstop.load(SeqCst);
7622 let saved_qbang = qbang.load(SeqCst);
7623 let saved_exlast = exlast.load(SeqCst);
7624 // iaddtoline now reaches zleaddtoline, which edits the metafied line.
7625 let saved_metaline = crate::ported::zle::compcore::ZLEMETALINE
7626 .get_or_init(|| Mutex::new(String::new()))
7627 .lock()
7628 .unwrap()
7629 .clone();
7630 let saved_metall = crate::ported::zle::compcore::ZLEMETALL.load(SeqCst);
7631
7632 // Set up: expanding=1, lexstop=false, qbang=0 (no bang
7633 // escape path), excs > zlemetacs so the adjustment fires.
7634 *chline.lock().unwrap() = String::new();
7635 expanding.store(1, SeqCst);
7636 lexstop.store(false, SeqCst);
7637 qbang.store(false, SeqCst);
7638 excs.store(10, SeqCst); // > zlemetacs
7639 ZLEMETACS.store(5, SeqCst);
7640 crate::ported::input::inbufct.with(|c| c.set(3));
7641 exlast.store(2, SeqCst);
7642
7643 // c:405-407 — excs(10) > zlemetacs(5), so
7644 // new_excs = 10 + 1 + 3 - 2 = 12; 12 > 5 → no clamp.
7645 iaddtoline(b'x' as i32);
7646 assert_eq!(
7647 excs.load(SeqCst),
7648 12,
7649 "c:406 — excs += 1 + inbufct - exlast (10+1+3-2=12)"
7650 );
7651
7652 // c:407-410 — clamp branch: post-add excs < zlemetacs.
7653 // Set excs=6, zlemetacs=20, inbufct=1, exlast=10.
7654 // new_excs = 6 + 1 + 1 - 10 = -2; -2 < 20 → clamp to 20.
7655 excs.store(6, SeqCst);
7656 // But guard: excs(6) > zlemetacs(20) is FALSE, so block
7657 // wouldn't fire. Need excs > zlemetacs for entry: use
7658 // excs=25, zlemetacs=20, inbufct=1, exlast=10 →
7659 // new_excs = 25+1+1-10 = 17; 17 < 20 → clamp to 20.
7660 excs.store(25, SeqCst);
7661 ZLEMETACS.store(20, SeqCst);
7662 crate::ported::input::inbufct.with(|c| c.set(1));
7663 exlast.store(10, SeqCst);
7664 iaddtoline(b'y' as i32);
7665 assert_eq!(
7666 excs.load(SeqCst),
7667 20,
7668 "c:407-410 — clamp to zlemetacs(20) when post-add excs<zlemetacs"
7669 );
7670
7671 // No adjustment when excs <= zlemetacs.
7672 excs.store(3, SeqCst);
7673 ZLEMETACS.store(10, SeqCst);
7674 crate::ported::input::inbufct.with(|c| c.set(1));
7675 exlast.store(0, SeqCst);
7676 iaddtoline(b'z' as i32);
7677 assert_eq!(
7678 excs.load(SeqCst),
7679 3,
7680 "c:405 — excs<=zlemetacs leaves excs unchanged"
7681 );
7682
7683 // Restore.
7684 *chline.lock().unwrap() = saved_chline;
7685 excs.store(saved_excs, SeqCst);
7686 ZLEMETACS.store(saved_zlemetacs, SeqCst);
7687 expanding.store(saved_expanding, SeqCst);
7688 lexstop.store(saved_lexstop, SeqCst);
7689 qbang.store(saved_qbang, SeqCst);
7690 exlast.store(saved_exlast, SeqCst);
7691 *crate::ported::zle::compcore::ZLEMETALINE
7692 .get_or_init(|| Mutex::new(String::new()))
7693 .lock()
7694 .unwrap() = saved_metaline;
7695 crate::ported::zle::compcore::ZLEMETALL.store(saved_metall, SeqCst);
7696 }
7697
7698 /// Pin `ihwbegin` to its canonical C body at `Src/hist.c:1656-1670`.
7699 /// The C computes `pos = hptr - chline + offset` — the byte
7700 /// offset of the current write head in chline plus a caller-
7701 /// supplied offset. The previous Rust port used chline.len()
7702 /// which is only equal when hptr is at end-of-buffer; any
7703 /// lexer rewind (backquote, comment, here-doc) would record
7704 /// wrong word offsets.
7705 #[test]
7706 fn ihwbegin_records_hptr_not_chline_len() {
7707 let _g = crate::test_util::global_state_lock();
7708 let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
7709 // Save state.
7710 let saved_chline = chline.lock().unwrap().clone();
7711 let saved_chwords = chwords.lock().unwrap().clone();
7712 let saved_chwordpos = chwordpos.load(SeqCst);
7713 let saved_hptr = hptr.load(SeqCst);
7714 let saved_stop = stophist.load(SeqCst);
7715 let saved_active = histactive.load(SeqCst);
7716 let saved_inflags = crate::ported::input::inbufflags.with(|f| f.get());
7717
7718 // Set up: chline is "ABCDEFGHIJ" (10 bytes), but hptr was
7719 // rewound to position 4 — simulating a lexer that backtracked.
7720 // C would record pos=4 (hptr-chline=4 + offset=0); the buggy
7721 // Rust port would record pos=10 (chline.len()).
7722 *chline.lock().unwrap() = "ABCDEFGHIJ".to_string();
7723 chwords.lock().unwrap().clear();
7724 chwordpos.store(0, SeqCst);
7725 hptr.store(4, SeqCst);
7726 stophist.store(0, SeqCst);
7727 histactive.store(0, SeqCst); // !HA_INWORD
7728 crate::ported::input::inbufflags.with(|f| f.set(0)); // not alias-only
7729
7730 ihwbegin(0);
7731
7732 let recorded = chwords.lock().unwrap().first().copied().unwrap_or(-1);
7733 assert_eq!(
7734 recorded, 4,
7735 "c:1658 — pos = hptr - chline + offset = 4 + 0 = 4 \
7736 (NOT chline.len()=10)"
7737 );
7738
7739 // Negative offset clamps to 0 (c:1666).
7740 chwords.lock().unwrap().clear();
7741 chwordpos.store(0, SeqCst);
7742 hptr.store(3, SeqCst);
7743 ihwbegin(-10); // pos = 3-10 = -7 → clamp to 0
7744 let recorded = chwords.lock().unwrap().first().copied().unwrap_or(-1);
7745 assert_eq!(recorded, 0, "c:1666 — pos<0 clamps to 0");
7746
7747 // Stop guard (c:1659) — `stophist == 2` short-circuits.
7748 chwords.lock().unwrap().clear();
7749 chwordpos.store(0, SeqCst);
7750 hptr.store(5, SeqCst);
7751 stophist.store(2, SeqCst);
7752 ihwbegin(0);
7753 assert!(
7754 chwords.lock().unwrap().is_empty(),
7755 "c:1659 — stophist==2 short-circuits, no record"
7756 );
7757 stophist.store(0, SeqCst);
7758
7759 // Alias-only guard (c:1659).
7760 chwords.lock().unwrap().clear();
7761 chwordpos.store(0, SeqCst);
7762 hptr.store(5, SeqCst);
7763 crate::ported::input::inbufflags.with(|f| f.set(INP_ALIAS));
7764 ihwbegin(0);
7765 assert!(
7766 chwords.lock().unwrap().is_empty(),
7767 "c:1659 — alias-only (INP_ALIAS without INP_HIST) short-circuits"
7768 );
7769
7770 // INP_ALIAS|INP_HIST does NOT short-circuit (mixed input).
7771 chwords.lock().unwrap().clear();
7772 chwordpos.store(0, SeqCst);
7773 hptr.store(7, SeqCst);
7774 crate::ported::input::inbufflags.with(|f| f.set(INP_ALIAS | INP_HIST));
7775 ihwbegin(0);
7776 let recorded = chwords.lock().unwrap().first().copied().unwrap_or(-1);
7777 assert_eq!(recorded, 7, "c:1659 — alias+hist mixed still records");
7778
7779 // Restore.
7780 *chline.lock().unwrap() = saved_chline;
7781 *chwords.lock().unwrap() = saved_chwords;
7782 chwordpos.store(saved_chwordpos, SeqCst);
7783 hptr.store(saved_hptr, SeqCst);
7784 stophist.store(saved_stop, SeqCst);
7785 histactive.store(saved_active, SeqCst);
7786 crate::ported::input::inbufflags.with(|f| f.set(saved_inflags));
7787 }
7788
7789 /// Pin `getargs` to its canonical C body at `Src/hist.c:2454-2482`.
7790 /// Covers: nwords-derived-from-field (not words.len()/2),
7791 /// arg1>arg2 reject, arg≥nwords reject, full-event fast path,
7792 /// per-word slicing, and signed-short overflow detection.
7793 #[test]
7794 fn getargs_handles_field_indexing_and_overflow() {
7795 let _g = crate::test_util::global_state_lock();
7796 // Build a histent for "echo hello world" with 3 words.
7797 // C nwords=3, words=[0,4,5,10,11,16] (start/end pairs).
7798 let he = histent {
7799 node: hashnode {
7800 next: None,
7801 nam: "echo hello world".to_string(),
7802 flags: 0,
7803 },
7804 up: None,
7805 down: None,
7806 zle_text: None,
7807 stim: 0,
7808 ftim: 0,
7809 words: vec![0, 4, 5, 10, 11, 16],
7810 nwords: 3, // c:2457 source of truth
7811 histnum: 1,
7812 };
7813
7814 // c:2459 — `arg2 < arg1` → reject.
7815 assert_eq!(getargs(&he, 2, 1), None, "c:2459 — arg2 < arg1 rejects");
7816 // c:2459 — `arg1 >= nwords` → reject.
7817 assert_eq!(
7818 getargs(&he, 3, 3),
7819 None,
7820 "c:2459 — arg1 >= nwords (3>=3) rejects"
7821 );
7822 // c:2459 — `arg2 >= nwords` → reject.
7823 assert_eq!(
7824 getargs(&he, 0, 3),
7825 None,
7826 "c:2459 — arg2 >= nwords (3>=3) rejects"
7827 );
7828
7829 // c:2466 — `arg1==0 && arg2==nwords-1` → full event fast path.
7830 assert_eq!(
7831 getargs(&he, 0, 2).as_deref(),
7832 Some("echo hello world"),
7833 "c:2467 — full-event fast path returns dupstring(nam)"
7834 );
7835
7836 // c:2469-2481 — per-word slice. word[0] = "echo" (pos 0..4).
7837 assert_eq!(
7838 getargs(&he, 0, 0).as_deref(),
7839 Some("echo"),
7840 "c:2481 — word[0] = nam[0..4]"
7841 );
7842 // word[1] = "hello" (pos 5..10).
7843 assert_eq!(
7844 getargs(&he, 1, 1).as_deref(),
7845 Some("hello"),
7846 "c:2481 — word[1] = nam[5..10]"
7847 );
7848 // word[2] = "world" (pos 11..16).
7849 assert_eq!(
7850 getargs(&he, 2, 2).as_deref(),
7851 Some("world"),
7852 "c:2481 — word[2] = nam[11..16]"
7853 );
7854 // Multi-word span: word[1..=2] = "hello world".
7855 assert_eq!(
7856 getargs(&he, 1, 2).as_deref(),
7857 Some("hello world"),
7858 "c:2481 — words[1..=2] = nam[5..16]"
7859 );
7860
7861 // c:2476 — signed-short overflow detection. Build a histent
7862 // whose stored pos[0] is negative (simulating i16 wrap on a
7863 // >32KB history line). Use nwords=2 with arg1=0,arg2=0 so the
7864 // c:2466 full-event fast path doesn't trigger.
7865 let overflow = histent {
7866 node: hashnode {
7867 next: None,
7868 nam: "ab cd".to_string(),
7869 flags: 0,
7870 },
7871 up: None,
7872 down: None,
7873 zle_text: None,
7874 stim: 0,
7875 ftim: 0,
7876 words: vec![-1, 5, 3, 5], // word[0]: pos1 < 0
7877 nwords: 2,
7878 histnum: 1,
7879 };
7880 assert_eq!(
7881 getargs(&overflow, 0, 0),
7882 None,
7883 "c:2476 — pos1 < 0 (i16 overflow) rejects"
7884 );
7885
7886 // c:2476 — `pos1 < arg1` detection (pos must be ≥ word index).
7887 let underflow = histent {
7888 node: hashnode {
7889 next: None,
7890 nam: "a b c d".to_string(),
7891 flags: 0,
7892 },
7893 up: None,
7894 down: None,
7895 zle_text: None,
7896 stim: 0,
7897 ftim: 0,
7898 // arg1=2 but pos1=1 means recorded pos < word index.
7899 // Each word must be ≥1 char so word[2] must start at pos ≥ 2.
7900 words: vec![0, 1, 2, 3, 1, 5, 6, 7],
7901 nwords: 4,
7902 histnum: 1,
7903 };
7904 assert_eq!(
7905 getargs(&underflow, 2, 2),
7906 None,
7907 "c:2476 — pos1 < arg1 (i16 overflow signal) rejects"
7908 );
7909 }
7910
7911 /// Pin `hconsearch` to its canonical C body at
7912 /// `Src/hist.c:1834-1854`. Searches up from the ring for the
7913 /// most recent entry whose text contains `needle` as a substring,
7914 /// returning `(histnum, marg)` where marg is the word index of
7915 /// the match. The previous Rust port dropped marg entirely.
7916 #[test]
7917 fn hconsearch_returns_histnum_and_word_index() {
7918 let _g = crate::test_util::global_state_lock();
7919 let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
7920 // Save ring state.
7921 let saved_ring = {
7922 let r = hist_ring.lock().unwrap();
7923 r.iter()
7924 .map(|h| {
7925 (
7926 h.node.nam.clone(),
7927 h.histnum,
7928 h.words.clone(),
7929 h.nwords,
7930 h.node.flags,
7931 )
7932 })
7933 .collect::<Vec<_>>()
7934 };
7935 let saved_curhist = curhist.load(SeqCst);
7936
7937 // Build a single-entry ring with "echo hello world" — 3 words.
7938 // words = [start1,end1, start2,end2, start3,end3]
7939 // 0 4 5 10 11 16
7940 {
7941 let mut ring = hist_ring.lock().unwrap();
7942 ring.clear();
7943 ring.push(histent {
7944 node: hashnode {
7945 next: None,
7946 nam: "echo hello world".to_string(),
7947 flags: 0,
7948 },
7949 up: None,
7950 down: None,
7951 zle_text: None,
7952 stim: 0,
7953 ftim: 0,
7954 words: vec![0, 4, 5, 10, 11, 16],
7955 nwords: 3,
7956 histnum: 7,
7957 });
7958 }
7959 curhist.store(8, SeqCst); // up_histent will walk back to 7
7960
7961 // "hello" found at pos 5 → word index 1 (0-based).
7962 let got = hconsearch("hello");
7963 assert_eq!(
7964 got,
7965 Some((7, 1)),
7966 "c:1846-1850 — strstr at pos 5 lands in word[1] (start=5)"
7967 );
7968
7969 // "world" found at pos 11 → word index 2.
7970 let got = hconsearch("world");
7971 assert_eq!(
7972 got,
7973 Some((7, 2)),
7974 "c:1846-1850 — strstr at pos 11 lands in word[2] (start=11)"
7975 );
7976
7977 // "echo" found at pos 0 → word index 0.
7978 let got = hconsearch("echo");
7979 assert_eq!(
7980 got,
7981 Some((7, 0)),
7982 "c:1846-1850 — strstr at pos 0 lands in word[0]"
7983 );
7984
7985 // Miss → None (c:1853 return -1).
7986 let got = hconsearch("notthere");
7987 assert_eq!(got, None, "c:1853 — miss returns -1 / None");
7988
7989 // HIST_FOREIGN entries are skipped (c:1843).
7990 {
7991 let mut ring = hist_ring.lock().unwrap();
7992 ring.clear();
7993 ring.push(histent {
7994 node: hashnode {
7995 next: None,
7996 nam: "skip me".to_string(),
7997 flags: HIST_FOREIGN as i32,
7998 },
7999 up: None,
8000 down: None,
8001 zle_text: None,
8002 stim: 0,
8003 ftim: 0,
8004 words: vec![0, 4, 5, 7],
8005 nwords: 2,
8006 histnum: 3,
8007 });
8008 }
8009 let got = hconsearch("skip");
8010 assert_eq!(
8011 got, None,
8012 "c:1843-1844 — HIST_FOREIGN entries continue past, miss → None"
8013 );
8014
8015 // Restore ring.
8016 {
8017 let mut ring = hist_ring.lock().unwrap();
8018 ring.clear();
8019 for (nam, histnum, words, nwords, flags) in saved_ring {
8020 ring.push(histent {
8021 node: hashnode {
8022 next: None,
8023 nam,
8024 flags,
8025 },
8026 up: None,
8027 down: None,
8028 zle_text: None,
8029 stim: 0,
8030 ftim: 0,
8031 words,
8032 nwords,
8033 histnum,
8034 });
8035 }
8036 }
8037 curhist.store(saved_curhist, SeqCst);
8038 }
8039
8040 /// Pin `checkcurline` to the canonical C body at
8041 /// `Src/hist.c:2421-2429`: when `he.histnum == curhist` AND
8042 /// `histactive & HA_ACTIVE`, flush chline/chwordpos/chwords
8043 /// into `curline`. Both gates MUST be true; otherwise leave
8044 /// `curline` untouched.
8045 #[test]
8046 fn checkcurline_flushes_to_curline_only_when_active_and_matching() {
8047 let _g = crate::test_util::global_state_lock();
8048 let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
8049 let saved_curhist = curhist.load(SeqCst);
8050 let saved_active = histactive.load(SeqCst);
8051 let saved_chline = chline.lock().unwrap().clone();
8052 let saved_chwordpos = chwordpos.load(SeqCst);
8053 let saved_chwords = chwords.lock().unwrap().clone();
8054 let saved_curline = curline.lock().unwrap().take();
8055
8056 // Set up in-flight build state.
8057 curhist.store(42, SeqCst);
8058 histactive.store(HA_ACTIVE, SeqCst);
8059 *chline.lock().unwrap() = "echo hello".to_string();
8060 chwordpos.store(4, SeqCst); // 2 words
8061 *chwords.lock().unwrap() = vec![0, 4, 5, 10];
8062 *curline.lock().unwrap() = None;
8063
8064 // Case 1: matching histnum + active → flushes.
8065 let he = histent {
8066 node: hashnode {
8067 next: None,
8068 nam: "ignored-by-checkcurline".to_string(),
8069 flags: 0,
8070 },
8071 up: None,
8072 down: None,
8073 zle_text: None,
8074 stim: 0,
8075 ftim: 0,
8076 words: vec![],
8077 nwords: 0,
8078 histnum: 42,
8079 };
8080 checkcurline(&he);
8081 {
8082 let cl = curline.lock().unwrap();
8083 let snap = cl
8084 .as_ref()
8085 .expect("c:2425-2427 — matching+active must flush a snapshot");
8086 assert_eq!(
8087 snap.node.nam, "echo hello",
8088 "c:2425 — curline.node.nam = chline"
8089 );
8090 assert_eq!(
8091 snap.nwords, 2,
8092 "c:2426 — curline.nwords = chwordpos/2 (4/2=2)"
8093 );
8094 assert_eq!(
8095 snap.words,
8096 vec![0, 4, 5, 10],
8097 "c:2427 — curline.words = chwords"
8098 );
8099 }
8100
8101 // Case 2: matching histnum but NOT active → no flush.
8102 histactive.store(0, SeqCst); // c:2424 HA_ACTIVE off
8103 *curline.lock().unwrap() = None;
8104 checkcurline(&he);
8105 assert!(
8106 curline.lock().unwrap().is_none(),
8107 "c:2424 — HA_ACTIVE cleared, no flush"
8108 );
8109
8110 // Case 3: active but mismatched histnum → no flush.
8111 histactive.store(HA_ACTIVE, SeqCst);
8112 let he2 = histent {
8113 histnum: 99,
8114 ..histent {
8115 node: hashnode {
8116 next: None,
8117 nam: String::new(),
8118 flags: 0,
8119 },
8120 up: None,
8121 down: None,
8122 zle_text: None,
8123 stim: 0,
8124 ftim: 0,
8125 words: vec![],
8126 nwords: 0,
8127 histnum: 0,
8128 }
8129 };
8130 checkcurline(&he2);
8131 assert!(
8132 curline.lock().unwrap().is_none(),
8133 "c:2424 — histnum mismatch, no flush"
8134 );
8135
8136 // Restore.
8137 curhist.store(saved_curhist, SeqCst);
8138 histactive.store(saved_active, SeqCst);
8139 *chline.lock().unwrap() = saved_chline;
8140 chwordpos.store(saved_chwordpos, SeqCst);
8141 *chwords.lock().unwrap() = saved_chwords;
8142 *curline.lock().unwrap() = saved_curline;
8143 }
8144
8145 /// `Src/hist.c:2172-2175` — `remlpaths` when count exceeds the
8146 /// number of path components returns the input UNMODIFIED
8147 /// (preserving the leading slash for absolute paths). The previous
8148 /// Rust port silently stripped the leading slash via the
8149 /// `parts.iter().rev().take(min(n, parts.len()))` clamp, so `:t4`
8150 /// on `/a/b/c` returned `"a/b/c"` instead of `"/a/b/c"`. Catches
8151 /// this divergence — the C `return 1` early-exit at the leftmost
8152 /// `/` does NOT modify `*junkptr`.
8153 #[test]
8154 fn remlpaths_count_exceeds_components_preserves_leading_slash() {
8155 let _g = crate::test_util::global_state_lock();
8156 // 4 > 3 components in "/a/b/c" → return whole string verbatim.
8157 assert_eq!(
8158 remlpaths("/a/b/c", 4),
8159 "/a/b/c",
8160 "c:2172-2175 — count > components → preserve original (leading slash)"
8161 );
8162 assert_eq!(remlpaths("/a/b/c", 10), "/a/b/c");
8163 // Relative path: no leading slash, but still preserved.
8164 assert_eq!(remlpaths("a/b/c", 99), "a/b/c");
8165 }
8166
8167 /// `Src/hist.c:2156-2161` — `remlpaths` trims trailing slashes off
8168 /// the input before walking. Pin so `:t1` on `/a/b/c/` (trailing
8169 /// slash) still returns `"c"`, not `""`.
8170 #[test]
8171 fn remlpaths_trims_trailing_slashes() {
8172 let _g = crate::test_util::global_state_lock();
8173 assert_eq!(
8174 remlpaths("/a/b/c/", 1),
8175 "c",
8176 "c:2156-2161 — trailing slash trimmed before scan"
8177 );
8178 assert_eq!(remlpaths("/a/b/c///", 1), "c");
8179 assert_eq!(
8180 remlpaths("/", 1),
8181 "",
8182 "all-slashes input → empty after trim → empty result"
8183 );
8184 assert_eq!(remlpaths("", 1), "");
8185 }
8186
8187 /// `Src/hist.c:2152` — `remlpaths` with `count == 0` is a special
8188 /// case: per `digitcount()` at c:574, bare `:t` (no number) passes
8189 /// 0 which means "default to 1". The Rust port preserves that
8190 /// alias so `${PWD:t}` keeps the last component.
8191 #[test]
8192 fn remlpaths_count_zero_defaults_to_one() {
8193 let _g = crate::test_util::global_state_lock();
8194 assert_eq!(
8195 remlpaths("/a/b/c", 0),
8196 "c",
8197 "c:574 — count=0 from digitcount aliases to default 1"
8198 );
8199 // Same as explicit count=1.
8200 assert_eq!(remlpaths("/a/b/c", 0), remlpaths("/a/b/c", 1));
8201 }
8202
8203 /// `Src/hist.c:2225-2228` — CASMOD_LOWER converts uppercase chars
8204 /// to lowercase, leaves everything else unchanged.
8205 #[test]
8206 fn casemodify_lower_lowercases_uppercase_only() {
8207 let _g = crate::test_util::global_state_lock();
8208 assert_eq!(
8209 casemodify("HELLO", CASMOD_LOWER),
8210 "hello",
8211 "c:2226-2228 — uppercase → lowercase"
8212 );
8213 // Already lowercase: no change.
8214 assert_eq!(casemodify("hello", CASMOD_LOWER), "hello");
8215 // Digits + punct: untouched.
8216 assert_eq!(casemodify("123 !? abc", CASMOD_LOWER), "123 !? abc");
8217 // Unicode: 'É' (U+00C9) lowercases to 'é'.
8218 assert_eq!(casemodify("ÉLITE", CASMOD_LOWER), "élite");
8219 }
8220
8221 /// `Src/hist.c:2232-2236` — CASMOD_UPPER converts lowercase chars
8222 /// to uppercase, leaves everything else unchanged.
8223 #[test]
8224 fn casemodify_upper_uppercases_lowercase_only() {
8225 let _g = crate::test_util::global_state_lock();
8226 assert_eq!(
8227 casemodify("hello", CASMOD_UPPER),
8228 "HELLO",
8229 "c:2233-2236 — lowercase → uppercase"
8230 );
8231 assert_eq!(casemodify("HELLO", CASMOD_UPPER), "HELLO");
8232 assert_eq!(casemodify("élite", CASMOD_UPPER), "ÉLITE");
8233 }
8234
8235 /// `Src/hist.c:2239-2254` — CASMOD_CAPS title-cases at word
8236 /// boundaries (defined by `!iswalnum` chars).
8237 #[test]
8238 fn casemodify_caps_title_cases_at_word_boundaries() {
8239 let _g = crate::test_util::global_state_lock();
8240 // c:2245-2250 — first letter and post-space letter uppercase;
8241 // mid-word letters lowercase (c:2251-2253).
8242 assert_eq!(casemodify("hello world", CASMOD_CAPS), "Hello World");
8243 // c:2243-2244 — non-alphanumeric resets nextupper.
8244 assert_eq!(casemodify("foo-bar.baz", CASMOD_CAPS), "Foo-Bar.Baz");
8245 // Already capped: mid-word `LO` → `lo`.
8246 assert_eq!(casemodify("HELLO", CASMOD_CAPS), "Hello");
8247 }
8248
8249 /// `Src/hist.c:2241-2242` — `IS_COMBINING(wc)` (WCWIDTH == 0)
8250 /// makes CASMOD_CAPS short-circuit BEFORE touching nextupper.
8251 /// The previous Rust port omitted this guard — combining marks
8252 /// were classified as non-alphanumeric and reset nextupper,
8253 /// breaking word-boundary detection on accented words written
8254 /// as base char + combining mark.
8255 ///
8256 /// Test input: `"a" + COMBINING ACUTE (U+0301) + "b"`. With the
8257 /// guard: combining mark passes through, b stays lowercase
8258 /// (still inside the word). Without the guard: the combiner
8259 /// resets nextupper, so `b` would be uppercased.
8260 /// A titlecase letter folds as the platform libc says, like zsh: glibc
8261 /// under a UTF-8 codeset has `iswupper`/`iswlower` both true for `Dž`
8262 /// (U+01C5), so it folds in either direction (fuzz seeds 7/97/102/242/244);
8263 /// macOS libc, and any C/ASCII codeset, leave it unchanged.
8264 #[test]
8265 fn casemodify_folds_titlecase_both_ways() {
8266 let _ = *crate::ported::utils::MB_LOCALE_READY;
8267 // SAFETY: nl_langinfo returns a pointer to a static NUL-terminated string.
8268 let codeset = unsafe { std::ffi::CStr::from_ptr(libc::nl_langinfo(libc::CODESET)) }
8269 .to_string_lossy()
8270 .to_ascii_uppercase();
8271 let glibc_utf8 =
8272 cfg!(all(target_os = "linux", target_env = "gnu")) && codeset.replace('-', "") == "UTF8";
8273 let (upper, lower) = if glibc_utf8 {
8274 ("\u{1c4}", "\u{1c6}")
8275 } else {
8276 ("\u{1c5}", "\u{1c5}")
8277 };
8278 assert_eq!(casemodify("\u{1c5}", CASMOD_UPPER), upper);
8279 assert_eq!(casemodify("\u{1c5}", CASMOD_LOWER), lower);
8280 }
8281
8282 #[test]
8283 fn casemodify_caps_skips_combining_chars() {
8284 let _g = crate::test_util::global_state_lock();
8285 // a + combining acute (U+0301) + b — under CAPS, the
8286 // combiner must NOT reset nextupper. Expected output:
8287 // `A` + combining acute + `b` (b stays lowercase because
8288 // it's still mid-word).
8289 let input = "a\u{0301}b";
8290 let got = casemodify(input, CASMOD_CAPS);
8291 let expected = "A\u{0301}b";
8292 assert_eq!(
8293 got, expected,
8294 "c:2241-2242 — IS_COMBINING short-circuit must not break word boundary"
8295 );
8296 }
8297
8298 /// `Src/hist.c:2349` — anchor-head matcher checks BOTH ASCII `#`
8299 /// AND the tokenized `Pound` byte (0x84), and c:2344 gates the
8300 /// whole block behind `HIST_SUBST_PATTERN || forcepat`. These run
8301 /// with `forcepat = 1`, which is what `:S` passes.
8302 #[test]
8303 fn subst_anchor_head_recognises_pound_token() {
8304 let _g = crate::test_util::global_state_lock();
8305 // c:2349 — ASCII '#' anchor at head: matches only prefix.
8306 let mut s = String::from("foobar");
8307 assert_eq!(subst(&mut s, "#foo", "baz", 0, 1), 0);
8308 assert_eq!(s, "bazbar", "c:2349 ASCII '#' anchor — match at head");
8309 // c:2349 — Pound token (0x84) anchor at head: same effect.
8310 let pat = format!("{}foo", Pound);
8311 let mut s = String::from("foobar");
8312 assert_eq!(subst(&mut s, &pat, "baz", 0, 1), 0);
8313 assert_eq!(
8314 s, "bazbar",
8315 "c:2349 Pound token (0x84) anchor — match at head"
8316 );
8317 // Negative: anchored at head MUST NOT match mid-string, and
8318 // c:2390 reports that as status 1 rather than a quiet no-op.
8319 let pat = format!("{}foo", Pound);
8320 let mut s = String::from("xfoo");
8321 assert_eq!(
8322 subst(&mut s, &pat, "baz", 0, 1),
8323 1,
8324 "c:2390 anchor-head rejects non-prefix"
8325 );
8326 assert_eq!(s, "xfoo");
8327 }
8328
8329 /// `Src/hist.c:2354` — anchor-tail matcher checks `%` only
8330 /// (no tokenized counterpart in C).
8331 #[test]
8332 fn subst_anchor_tail_matches_only_suffix() {
8333 let _g = crate::test_util::global_state_lock();
8334 // c:2354 — `%foo` anchored at tail.
8335 let mut s = String::from("xxfoo");
8336 assert_eq!(subst(&mut s, "%foo", "bar", 0, 1), 0);
8337 assert_eq!(s, "xxbar", "c:2354 — '%' anchors at end of string");
8338 // Non-suffix → no match, status 1.
8339 let mut s = String::from("foox");
8340 assert_eq!(
8341 subst(&mut s, "%foo", "bar", 0, 1),
8342 1,
8343 "c:2354 — '%foo' must not match unless `foo` is at end"
8344 );
8345 assert_eq!(s, "foox");
8346 }
8347
8348 /// c:2344 — with neither `HIST_SUBST_PATTERN` nor `forcepat`, C
8349 /// never reads `#` or `%` as anchors: the `strstr` arm at c:2373
8350 /// takes the pattern literally. The previous port stripped them
8351 /// unconditionally, so `!!:s/#print/echo/` substituted where zsh
8352 /// reports `substitution failed`.
8353 #[test]
8354 fn subst_anchors_are_literal_without_pattern_mode() {
8355 let _g = crate::test_util::global_state_lock();
8356 let mut s = String::from("foobar");
8357 assert_eq!(
8358 subst(&mut s, "#foo", "baz", 0, 0),
8359 1,
8360 "c:2373 — `#foo` is the literal four characters under plain :s"
8361 );
8362 assert_eq!(s, "foobar");
8363
8364 let mut s = String::from("a#foob");
8365 assert_eq!(subst(&mut s, "#foo", "X", 0, 0), 0);
8366 assert_eq!(s, "aXb", "c:2373 — and it matches where it literally is");
8367 }
8368
8369 /// c:2386 vs c:2390 — the status says whether a substitution
8370 /// HAPPENED, which is not the same question as whether the text
8371 /// changed. `^old^old` replaces `old` with `old`: C returns 0 and
8372 /// the line runs. Deciding by comparing the strings turns that
8373 /// into `substitution failed`.
8374 #[test]
8375 fn subst_reports_success_when_the_replacement_equals_the_pattern() {
8376 let _g = crate::test_util::global_state_lock();
8377 let mut s = String::from("print old xx");
8378 assert_eq!(
8379 subst(&mut s, "old", "old", 0, 0),
8380 0,
8381 "c:2386 — a match is a success even with an identical replacement"
8382 );
8383 assert_eq!(s, "print old xx");
8384
8385 // The genuine no-match still fails.
8386 let mut s = String::from("print old xx");
8387 assert_eq!(
8388 subst(&mut s, "zzz", "q", 0, 0),
8389 1,
8390 "c:2390 — no match is the only failure"
8391 );
8392 assert_eq!(s, "print old xx");
8393 }
8394
8395 /// c:2378-2384 — `gbal` keeps scanning from just past the text it
8396 /// wrote in, so a replacement containing the pattern is not
8397 /// re-scanned and cannot loop.
8398 #[test]
8399 fn subst_global_does_not_rescan_its_own_replacement() {
8400 let _g = crate::test_util::global_state_lock();
8401 let mut s = String::from("aXbXc");
8402 assert_eq!(subst(&mut s, "X", "XX", 1, 0), 0);
8403 assert_eq!(s, "aXXbXXc", "c:2383 — resume after the replacement");
8404 }
8405
8406 /// c:2375 — `&` in the replacement stands for the pattern, but
8407 /// only on the `strstr` arm. c:2367 passes `out` straight through
8408 /// on the pattern arm, where `&` is an ordinary character.
8409 #[test]
8410 fn subst_ampersand_is_literal_in_pattern_mode() {
8411 let _g = crate::test_util::global_state_lock();
8412 let mut s = String::from("print old xx");
8413 assert_eq!(subst(&mut s, "old", "&X", 0, 0), 0);
8414 assert_eq!(s, "print oldX xx", "c:2375 — convamps expands `&`");
8415
8416 let mut s = String::from("print old xx");
8417 assert_eq!(subst(&mut s, "old", "&X", 0, 1), 0);
8418 assert_eq!(
8419 s, "print &X xx",
8420 "c:2367 — no convamps on the pattern arm, `&` stays literal"
8421 );
8422 }
8423
8424 // ═══════════════════════════════════════════════════════════════════
8425 // histreduceblanks edge cases — pure function, anchored to C semantics
8426 // (c:50, c:1240): collapse runs of space/tab to single space; trim
8427 // leading and trailing space; PRESERVE embedded newlines/CRs.
8428 // ═══════════════════════════════════════════════════════════════════
8429
8430 /// Single space stays single.
8431 #[test]
8432 fn histreduceblanks_single_space_stays_one() {
8433 let _g = crate::test_util::global_state_lock();
8434 assert_eq!(reduce_line("a b"), "a b");
8435 }
8436
8437 /// Run of spaces collapses to one.
8438 #[test]
8439 fn histreduceblanks_multi_space_collapses_to_one() {
8440 let _g = crate::test_util::global_state_lock();
8441 assert_eq!(reduce_line("a b"), "a b");
8442 }
8443
8444 /// Tab counts as inblank — collapses to single space.
8445 #[test]
8446 fn histreduceblanks_tab_collapses_with_spaces() {
8447 let _g = crate::test_util::global_state_lock();
8448 assert_eq!(reduce_line("a \t b"), "a b");
8449 }
8450
8451 /// Leading whitespace trimmed.
8452 #[test]
8453 fn histreduceblanks_leading_whitespace_trimmed() {
8454 let _g = crate::test_util::global_state_lock();
8455 assert_eq!(reduce_line(" hello"), "hello");
8456 }
8457
8458 /// Trailing whitespace trimmed.
8459 #[test]
8460 fn histreduceblanks_trailing_whitespace_trimmed() {
8461 let _g = crate::test_util::global_state_lock();
8462 assert_eq!(reduce_line("hello "), "hello");
8463 }
8464
8465 /// Both leading AND trailing trimmed.
8466 #[test]
8467 fn histreduceblanks_both_ends_trimmed() {
8468 let _g = crate::test_util::global_state_lock();
8469 assert_eq!(reduce_line(" hi "), "hi");
8470 }
8471
8472 /// Empty input → empty output.
8473 #[test]
8474 fn histreduceblanks_empty_input_returns_empty() {
8475 let _g = crate::test_util::global_state_lock();
8476 assert_eq!(reduce_line(""), "");
8477 }
8478
8479 /// All-whitespace input → empty after trim.
8480 #[test]
8481 fn histreduceblanks_all_whitespace_becomes_empty() {
8482 let _g = crate::test_util::global_state_lock();
8483 assert_eq!(reduce_line(" "), "");
8484 assert_eq!(reduce_line("\t\t \t"), "");
8485 }
8486
8487 /// Embedded newline is NOT treated as inblank — preserved as-is.
8488 /// (c:50 — `inblank` is space/tab ONLY; newline is preserved.)
8489 #[test]
8490 fn histreduceblanks_newline_preserved_not_collapsed() {
8491 let _g = crate::test_util::global_state_lock();
8492 // Newlines stay; surrounding spaces collapse normally.
8493 let r = reduce_line("a\nb");
8494 assert_eq!(r, "a\nb", "newline must be preserved");
8495 }
8496
8497 /// Multiple newlines stay; flanking spaces don't get consumed.
8498 #[test]
8499 fn histreduceblanks_multiple_newlines_preserved() {
8500 let _g = crate::test_util::global_state_lock();
8501 // Each newline is a recorded word; nothing between words to close.
8502 let r = reduce_blanks("a\n\nb", &[0, 1, 1, 2, 2, 3, 3, 4], false);
8503 assert_eq!(r, "a\n\nb");
8504 }
8505
8506 /// Space-newline-space — the spaces flanking the newline are not
8507 /// treated as a "run" because newline breaks continuity.
8508 #[test]
8509 fn histreduceblanks_space_around_newline_preserved() {
8510 let _g = crate::test_util::global_state_lock();
8511 // Words `a`, `\n`, `b` with one-byte gaps: already minimal (c:1211).
8512 let r = reduce_blanks("a \n b", &[0, 1, 2, 3, 4, 5], false);
8513 assert_eq!(r, "a \n b");
8514 }
8515
8516 /// Mixed: leading spaces dropped (no HIST_IGNORE_SPACE), each gap reduced
8517 /// to its first byte (c:1224), all-blank tail truncated (c:1243).
8518 #[test]
8519 fn histreduceblanks_complex_input_normalizes() {
8520 let _g = crate::test_util::global_state_lock();
8521 assert_eq!(reduce_line(" a b\t\tc "), "a b\tc");
8522 }
8523
8524 // ═══════════════════════════════════════════════════════════════════
8525 // histsplitwords edge cases — pure tokenizer pinning byte spans.
8526 // ═══════════════════════════════════════════════════════════════════
8527
8528 /// Single word, no whitespace.
8529 #[test]
8530 fn histsplitwords_single_word_returns_one_span() {
8531 let words = histsplitwords("echo", false);
8532 assert_eq!(words, vec![(0, 4)]);
8533 }
8534
8535 /// Empty input → no words.
8536 #[test]
8537 fn histsplitwords_empty_input_returns_no_words() {
8538 let words = histsplitwords("", false);
8539 assert!(words.is_empty(), "empty line has no words; got {words:?}");
8540 }
8541
8542 /// Only whitespace → no words.
8543 #[test]
8544 fn histsplitwords_only_whitespace_returns_no_words() {
8545 let words = histsplitwords(" ", false);
8546 assert!(
8547 words.is_empty(),
8548 "whitespace-only has no words; got {words:?}"
8549 );
8550 }
8551
8552 /// Leading whitespace doesn't create phantom first word at offset 0.
8553 #[test]
8554 fn histsplitwords_leading_whitespace_skipped_no_uselex() {
8555 // " hi" — first word starts at offset 3.
8556 let words = histsplitwords(" hi", false);
8557 assert_eq!(words, vec![(3, 5)]);
8558 }
8559
8560 /// Tab-separated words.
8561 #[test]
8562 fn histsplitwords_tab_separator_works_like_space() {
8563 let words = histsplitwords("a\tb\tc", false);
8564 // 3 words, each one char long.
8565 assert_eq!(words.len(), 3);
8566 for (s, e) in &words {
8567 assert_eq!(e - s, 1, "each word is one char; got ({s},{e})");
8568 }
8569 }
8570
8571 /// Spans never overlap and never overflow the input length.
8572 #[test]
8573 fn histsplitwords_spans_well_formed() {
8574 let line = "alpha beta gamma";
8575 for use_lex in [false, true] {
8576 let words = histsplitwords(line, use_lex);
8577 for (s, e) in &words {
8578 assert!(
8579 s < e && *e <= line.len(),
8580 "bad span ({s},{e}) for line len {} use_lex={}",
8581 line.len(),
8582 use_lex
8583 );
8584 }
8585 // Spans must be monotonically advancing.
8586 for i in 1..words.len() {
8587 assert!(
8588 words[i].0 >= words[i - 1].1,
8589 "spans overlap: {:?} then {:?}",
8590 words[i - 1],
8591 words[i]
8592 );
8593 }
8594 }
8595 }
8596
8597 // ─── zsh-corpus pins for histreduceblanks ─────────────────────
8598
8599 /// Empty input → empty output.
8600 #[test]
8601 fn hist_corpus_histreduceblanks_empty_is_empty() {
8602 let _g = crate::test_util::global_state_lock();
8603 assert_eq!(reduce_line(""), "");
8604 }
8605
8606 /// All-spaces input collapses to nothing (trimmed).
8607 #[test]
8608 fn hist_corpus_histreduceblanks_all_spaces_to_empty() {
8609 let _g = crate::test_util::global_state_lock();
8610 assert_eq!(reduce_line(" "), "");
8611 assert_eq!(reduce_line("\t\t\t"), "");
8612 assert_eq!(reduce_line(" \t \t "), "");
8613 }
8614
8615 /// Single non-space char passes through.
8616 #[test]
8617 fn hist_corpus_histreduceblanks_single_char() {
8618 let _g = crate::test_util::global_state_lock();
8619 assert_eq!(reduce_line("x"), "x");
8620 }
8621
8622 /// Tab counts as inblank; mixed space+tab runs collapse to one space.
8623 #[test]
8624 fn hist_corpus_histreduceblanks_mixed_space_tab() {
8625 let _g = crate::test_util::global_state_lock();
8626 assert_eq!(reduce_line("a \t \t b"), "a b");
8627 }
8628
8629 /// Multibyte characters pass through unchanged.
8630 #[test]
8631 fn hist_corpus_histreduceblanks_multibyte_passthrough() {
8632 let _g = crate::test_util::global_state_lock();
8633 assert_eq!(reduce_line("日 本"), "日 本");
8634 assert_eq!(reduce_line("日 本"), "日 本");
8635 }
8636
8637 /// Newlines preserved with exact count. The lexer records each newline
8638 /// as a word of its own, so adjacent words leave no gap to close (zsh
8639 /// stores `{ print a<NL><NL><NL>print b }` as
8640 /// `{ print a<NL><NL><NL>print b }`).
8641 #[test]
8642 fn hist_corpus_histreduceblanks_newlines_exact_count() {
8643 let _g = crate::test_util::global_state_lock();
8644 assert_eq!(
8645 reduce_blanks("a\n\n\nb", &[0, 1, 1, 2, 2, 3, 3, 4, 4, 5], false),
8646 "a\n\n\nb"
8647 );
8648 }
8649
8650 /// `hist_is_in_word` round-trips with `hist_in_word`.
8651 #[test]
8652 fn hist_corpus_in_word_round_trip() {
8653 let _g = crate::test_util::global_state_lock();
8654 let saved = hist_is_in_word();
8655 hist_in_word(1);
8656 assert_eq!(hist_is_in_word(), 1);
8657 hist_in_word(0);
8658 assert_eq!(hist_is_in_word(), 0);
8659 hist_in_word(saved);
8660 }
8661
8662 /// `substfailed` returns an i32 (zsh stores -1 when no subst
8663 /// has happened yet, 0 on success, 1 on fail). Pin: returns
8664 /// without panic.
8665 #[test]
8666 fn hist_corpus_substfailed_does_not_panic() {
8667 let _g = crate::test_util::global_state_lock();
8668 let _ = substfailed();
8669 }
8670
8671 // ═══════════════════════════════════════════════════════════════════
8672 // C-parity tests pinning Src/hist.c. Tests that capture KNOWN
8673 // ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"].
8674 // ═══════════════════════════════════════════════════════════════════
8675
8676 /// `getargc(entry)` returns `entry.nwords - 1` (or 0 if nwords=0).
8677 /// C `Src/hist.c:556`:
8678 /// `return ehist->nwords ? ehist->nwords-1 : 0;`
8679 /// Fixed: previously Rust returned plain `entry.nwords` (off-by-one).
8680 #[test]
8681 fn getargc_returns_nwords_minus_one() {
8682 let _g = crate::test_util::global_state_lock();
8683 // Build a fake histent with nwords=3. C returns 2; current
8684 // Rust returns 3.
8685 let entry = histent {
8686 node: crate::ported::zsh_h::hashnode {
8687 next: None,
8688 nam: String::new(),
8689 flags: 0,
8690 },
8691 down: None,
8692 up: None,
8693 zle_text: None,
8694 stim: 0,
8695 ftim: 0,
8696 words: Vec::new(),
8697 nwords: 3,
8698 histnum: 0,
8699 };
8700 assert_eq!(
8701 getargc(&entry),
8702 2,
8703 "C returns nwords-1=2 for nwords=3; Rust off-by-one bug returns 3"
8704 );
8705 }
8706
8707 /// `getargc(entry)` with nwords=0 returns 0 (per C ternary
8708 /// guard). Rust's off-by-one bug happens to return 0 here too
8709 /// (correct value); this test pins the corner case where Rust
8710 /// and C agree by accident.
8711 #[test]
8712 fn getargc_nwords_zero_returns_zero() {
8713 let _g = crate::test_util::global_state_lock();
8714 let entry = histent {
8715 node: crate::ported::zsh_h::hashnode {
8716 next: None,
8717 nam: String::new(),
8718 flags: 0,
8719 },
8720 down: None,
8721 up: None,
8722 zle_text: None,
8723 stim: 0,
8724 ftim: 0,
8725 words: Vec::new(),
8726 nwords: 0,
8727 histnum: 0,
8728 };
8729 assert_eq!(getargc(&entry), 0, "nwords=0 → 0 (both C and Rust)");
8730 }
8731
8732 /// `getargc(entry)` with nwords=1 — C returns 0 (1-1).
8733 /// Fixed: matches C semantics now.
8734 #[test]
8735 fn getargc_nwords_one_returns_zero() {
8736 let _g = crate::test_util::global_state_lock();
8737 let entry = histent {
8738 node: crate::ported::zsh_h::hashnode {
8739 next: None,
8740 nam: String::new(),
8741 flags: 0,
8742 },
8743 down: None,
8744 up: None,
8745 zle_text: None,
8746 stim: 0,
8747 ftim: 0,
8748 words: Vec::new(),
8749 nwords: 1,
8750 histnum: 0,
8751 };
8752 assert_eq!(getargc(&entry), 0, "nwords=1 → 0 (1-1) per C");
8753 }
8754
8755 // ═══════════════════════════════════════════════════════════════════
8756 // Additional C-parity tests for Src/hist.c histreduceblanks.
8757 // ═══════════════════════════════════════════════════════════════════
8758
8759 /// c:1240 — `histreduceblanks("")` returns empty.
8760 #[test]
8761 fn histreduceblanks_empty_returns_empty() {
8762 let _g = crate::test_util::global_state_lock();
8763 assert_eq!(reduce_line(""), "");
8764 }
8765
8766 /// c:1240 — `histreduceblanks` collapses multiple spaces to one.
8767 #[test]
8768 fn histreduceblanks_collapses_runs_of_spaces() {
8769 let _g = crate::test_util::global_state_lock();
8770 assert_eq!(reduce_line("a b"), "a b");
8771 assert_eq!(reduce_line("a b c"), "a b c");
8772 }
8773
8774 /// c:1224 — a gap shrinks to the ONE byte that followed the word, so a
8775 /// tab separator stays a tab (zsh stores `a<TAB><TAB>b` as `a<TAB>b`).
8776 #[test]
8777 fn histreduceblanks_collapses_tabs_to_space() {
8778 let _g = crate::test_util::global_state_lock();
8779 assert_eq!(reduce_line("a\tb"), "a\tb", "single tab gap is already minimal");
8780 assert_eq!(reduce_line("a\t\t\tb"), "a\tb", "tab run → the first tab");
8781 assert_eq!(reduce_line("a \t \tb"), "a b", "mixed run → its first byte, a space");
8782 }
8783
8784 /// c:1240 — strips leading whitespace.
8785 #[test]
8786 fn histreduceblanks_strips_leading_whitespace() {
8787 let _g = crate::test_util::global_state_lock();
8788 assert_eq!(reduce_line(" abc"), "abc");
8789 assert_eq!(reduce_line("\t\tabc"), "abc");
8790 }
8791
8792 /// c:1240 — strips trailing whitespace.
8793 #[test]
8794 fn histreduceblanks_strips_trailing_whitespace() {
8795 let _g = crate::test_util::global_state_lock();
8796 assert_eq!(reduce_line("abc "), "abc");
8797 assert_eq!(reduce_line("abc\t\t"), "abc");
8798 }
8799
8800 /// c:1240 — pure whitespace input returns empty.
8801 #[test]
8802 fn histreduceblanks_only_whitespace_returns_empty() {
8803 let _g = crate::test_util::global_state_lock();
8804 assert_eq!(reduce_line(" "), "");
8805 assert_eq!(reduce_line("\t\t\t"), "");
8806 assert_eq!(reduce_line(" \t \t "), "");
8807 }
8808
8809 /// c:1240 — `histreduceblanks` is idempotent.
8810 #[test]
8811 fn histreduceblanks_is_idempotent() {
8812 let _g = crate::test_util::global_state_lock();
8813 for input in &["a b", " foo", "x\ty\tz", " hello world "] {
8814 let once = reduce_line(input);
8815 let twice = reduce_line(&once);
8816 assert_eq!(once, twice, "must be idempotent on {:?}", input);
8817 }
8818 }
8819
8820 /// c:1240 — single space between words is preserved (already minimal).
8821 #[test]
8822 fn histreduceblanks_single_space_preserved() {
8823 let _g = crate::test_util::global_state_lock();
8824 assert_eq!(reduce_line("a b"), "a b");
8825 assert_eq!(reduce_line("hello world"), "hello world");
8826 }
8827
8828 /// c:50 — inblank is space/tab ONLY; newline is NOT collapsed.
8829 #[test]
8830 fn histreduceblanks_preserves_newlines() {
8831 let _g = crate::test_util::global_state_lock();
8832 // Newline is not inblank → preserved (and bordering chars too).
8833 let r = reduce_line("a\nb");
8834 assert!(r.contains('\n'), "newline preserved in {:?}", r);
8835 }
8836
8837 /// `histremovedups()` is safe on an empty hist_ring.
8838 #[test]
8839 fn histremovedups_empty_ring_is_safe() {
8840 let _g = crate::test_util::global_state_lock();
8841 hist_ring.lock().unwrap().clear();
8842 histremovedups();
8843 // No panic = pass; ring stays empty.
8844 assert!(hist_ring.lock().unwrap().is_empty());
8845 }
8846
8847 /// `getargc(entry)` for nwords=5 returns 4 per C `nwords - 1`.
8848 /// Pins post-fix C-correct semantics.
8849 #[test]
8850 fn getargc_returns_nwords_minus_one_for_five() {
8851 let _g = crate::test_util::global_state_lock();
8852 let entry = histent {
8853 node: crate::ported::zsh_h::hashnode {
8854 next: None,
8855 nam: String::new(),
8856 flags: 0,
8857 },
8858 down: None,
8859 up: None,
8860 zle_text: None,
8861 stim: 0,
8862 ftim: 0,
8863 words: Vec::new(),
8864 nwords: 5,
8865 histnum: 0,
8866 };
8867 assert_eq!(getargc(&entry), 4, "C: 5-1 = 4");
8868 }
8869
8870 // ═══════════════════════════════════════════════════════════════════
8871 // Additional C-parity tests for Src/hist.c
8872 // c:141 hist_in_word / c:150 hist_is_in_word / c:1155 herrflush /
8873 // c:1221 substfailed / c:1255 digitcount / c:1549 histreduceblanks /
8874 // c:1611 addhistnum / c:1293 strinbeg / c:1313 strinend
8875 // ═══════════════════════════════════════════════════════════════════
8876
8877 /// c:141 + c:150 — `hist_in_word(N)` then `hist_is_in_word()` round-trips.
8878 #[test]
8879 fn hist_in_word_set_get_round_trip() {
8880 let _g = crate::test_util::global_state_lock();
8881 let saved = hist_is_in_word();
8882 hist_in_word(1);
8883 assert_eq!(hist_is_in_word(), 1, "set/get round-trips");
8884 hist_in_word(0);
8885 assert_eq!(hist_is_in_word(), 0, "set/get round-trips back");
8886 hist_in_word(saved);
8887 }
8888
8889 /// c:1155 — `herrflush` is idempotent.
8890 #[test]
8891 fn herrflush_idempotent() {
8892 let _g = crate::test_util::global_state_lock();
8893 for _ in 0..5 {
8894 herrflush();
8895 }
8896 }
8897
8898 /// c:1221 — `substfailed` returns -1 per C body `return -1`.
8899 #[test]
8900 fn substfailed_returns_minus_one() {
8901 let _g = crate::test_util::global_state_lock();
8902 assert_eq!(substfailed(), -1, "C body: return -1");
8903 }
8904
8905 /// c:1255 — `digitcount` returns i32 (compile-time type pin).
8906 #[test]
8907 fn digitcount_returns_i32_type() {
8908 let _g = crate::test_util::global_state_lock();
8909 let _: i32 = digitcount();
8910 }
8911
8912 /// c:1549 — `histreduceblanks("")` empty returns empty String.
8913 #[test]
8914 fn histreduceblanks_empty_returns_empty_pin() {
8915 let _g = crate::test_util::global_state_lock();
8916 assert_eq!(reduce_line(""), "");
8917 }
8918
8919 /// c:1549 — `histreduceblanks` is pure.
8920 #[test]
8921 fn histreduceblanks_is_pure() {
8922 let _g = crate::test_util::global_state_lock();
8923 for s in ["", "abc", " spaces ", "\ttabs\t", "no\nnewlines\nhere"] {
8924 let first = reduce_line(s);
8925 for _ in 0..3 {
8926 assert_eq!(
8927 reduce_line(s),
8928 first,
8929 "reduce_line({:?}) must be pure",
8930 s
8931 );
8932 }
8933 }
8934 }
8935
8936 /// c:1293 + c:1313 — `strinbeg(0)` + `strinend` round-trip safe.
8937 #[test]
8938 fn strinbeg_strinend_round_trip_safe() {
8939 let _g = crate::test_util::global_state_lock();
8940 strinbeg(0);
8941 strinend();
8942 }
8943
8944 /// c:1611 — `addhistnum(0, 0, 0)` zero delta returns i64 (type pin).
8945 #[test]
8946 fn addhistnum_returns_i64_type() {
8947 let _g = crate::test_util::global_state_lock();
8948 let _: i64 = addhistnum(0, 0, 0);
8949 }
8950
8951 /// c:1611 — `addhistnum(N, 0, _)` zero increment preserves N.
8952 #[test]
8953 fn addhistnum_zero_increment_returns_input() {
8954 let _g = crate::test_util::global_state_lock();
8955 // Note: behavior depends on history ring state — pin only that
8956 // result is deterministic for same input.
8957 let first = addhistnum(0, 0, 0);
8958 for _ in 0..3 {
8959 assert_eq!(
8960 addhistnum(0, 0, 0),
8961 first,
8962 "addhistnum(0, 0, 0) must be deterministic"
8963 );
8964 }
8965 }
8966
8967 /// c:1328 — `nohw(0)` no-op accepts any i32.
8968 #[test]
8969 fn nohw_full_i32_range_no_panic() {
8970 for c in [i32::MIN, -1, 0, 1, 42, i32::MAX] {
8971 nohw(c);
8972 }
8973 }
8974
8975 /// c:1332 + c:1336 — `nohwabort` + `nohwe` are no-op idempotent.
8976 #[test]
8977 fn nohwabort_nohwe_idempotent() {
8978 for _ in 0..5 {
8979 nohwabort();
8980 nohwe();
8981 }
8982 }
8983
8984 // ═══════════════════════════════════════════════════════════════════
8985 // Additional C-parity tests for Src/hist.c
8986 // c:141 hist_in_word / c:150 hist_is_in_word / c:1417 unlinkcurline /
8987 // c:3582 flockhistfile / c:3745 histfileIsLocked / c:4449 firsthist
8988 // ═══════════════════════════════════════════════════════════════════
8989
8990 /// c:150 — `hist_is_in_word` returns i32 (compile-time type pin).
8991 #[test]
8992 fn hist_is_in_word_returns_i32_type() {
8993 let _g = crate::test_util::global_state_lock();
8994 let _: i32 = hist_is_in_word();
8995 }
8996
8997 /// c:150 — `hist_is_in_word` returns 0 or 1 only.
8998 #[test]
8999 fn hist_is_in_word_returns_zero_or_one() {
9000 let _g = crate::test_util::global_state_lock();
9001 let r = hist_is_in_word();
9002 assert!(r == 0 || r == 1, "hist_is_in_word ∈ {{0,1}}, got {}", r);
9003 }
9004
9005 /// c:141 + c:150 — `hist_in_word(1)` then `hist_is_in_word() == 1`
9006 /// round-trip.
9007 #[test]
9008 fn hist_in_word_set_clear_round_trip() {
9009 let _g = crate::test_util::global_state_lock();
9010 let saved = hist_is_in_word();
9011 hist_in_word(1);
9012 assert_eq!(hist_is_in_word(), 1, "set → is_in_word=1");
9013 hist_in_word(0);
9014 assert_eq!(hist_is_in_word(), 0, "clear → is_in_word=0");
9015 hist_in_word(saved);
9016 }
9017
9018 /// c:141 — `hist_in_word(0)` idempotent on a clear state.
9019 #[test]
9020 fn hist_in_word_clear_idempotent() {
9021 let _g = crate::test_util::global_state_lock();
9022 let saved = hist_is_in_word();
9023 hist_in_word(0);
9024 for _ in 0..5 {
9025 hist_in_word(0);
9026 assert_eq!(hist_is_in_word(), 0, "still clear");
9027 }
9028 hist_in_word(saved);
9029 }
9030
9031 /// c:1417 — `unlinkcurline` is safe on empty / fresh state.
9032 #[test]
9033 fn unlinkcurline_idempotent_safe() {
9034 let _g = crate::test_util::global_state_lock();
9035 for _ in 0..5 {
9036 unlinkcurline();
9037 }
9038 }
9039
9040 /// c:3582 — `flockhistfile` returns i32 (compile-time type pin).
9041 #[test]
9042 fn flockhistfile_returns_i32_type() {
9043 let _g = crate::test_util::global_state_lock();
9044 let _: i32 = flockhistfile("/__never_exists_zshrs_xyz__");
9045 }
9046
9047 /// c:3582 — `flockhistfile(nonexistent)` returns nonzero / determ.
9048 #[test]
9049 fn flockhistfile_nonexistent_path_deterministic() {
9050 let _g = crate::test_util::global_state_lock();
9051 let first = flockhistfile("/__never_exists_zshrs_flock__");
9052 for _ in 0..3 {
9053 assert_eq!(
9054 flockhistfile("/__never_exists_zshrs_flock__"),
9055 first,
9056 "flockhistfile must be deterministic"
9057 );
9058 }
9059 }
9060
9061 /// c:3745 — `histfileIsLocked` returns i32 (compile-time type pin).
9062 #[test]
9063 fn histfileIsLocked_returns_i32_type() {
9064 let _g = crate::test_util::global_state_lock();
9065 let _: i32 = histfileIsLocked();
9066 }
9067
9068 /// c:3745 — `histfileIsLocked` returns 0 or 1 only.
9069 #[test]
9070 fn histfileIsLocked_returns_zero_or_one() {
9071 let _g = crate::test_util::global_state_lock();
9072 let r = histfileIsLocked();
9073 assert!(r == 0 || r == 1, "histfileIsLocked ∈ {{0,1}}, got {}", r);
9074 }
9075
9076 /// c:4449 — `firsthist` returns i64 (compile-time type pin).
9077 #[test]
9078 fn firsthist_returns_i64_type() {
9079 let _g = crate::test_util::global_state_lock();
9080 let _: i64 = firsthist();
9081 }
9082
9083 /// c:4449 — `firsthist` empty ring returns 1 (per c:4451 default).
9084 #[test]
9085 fn firsthist_default_when_ring_empty() {
9086 let _g = crate::test_util::global_state_lock();
9087 // The ring is process-wide and earlier tests leave entries in it, so
9088 // empty it for the assertion and put it back.
9089 let saved = std::mem::take(&mut *hist_ring.lock().unwrap());
9090 let r = firsthist();
9091 *hist_ring.lock().unwrap() = saved;
9092 assert_eq!(r, 1, "an empty ring reports the default first event");
9093 }
9094
9095 /// c:4449 — `firsthist` is deterministic on stable ring.
9096 #[test]
9097 fn firsthist_is_deterministic() {
9098 let _g = crate::test_util::global_state_lock();
9099 let first = firsthist();
9100 for _ in 0..3 {
9101 assert_eq!(
9102 firsthist(),
9103 first,
9104 "firsthist must be deterministic on unchanged ring"
9105 );
9106 }
9107 }
9108
9109 /// c:1221 — `substfailed` returns i32 (compile-time type pin).
9110 #[test]
9111 fn substfailed_returns_i32_type() {
9112 let _g = crate::test_util::global_state_lock();
9113 let _: i32 = substfailed();
9114 }
9115
9116 /// Ring eviction must drop the evicted entry's histtab key
9117 /// (c:1410 `freehistdata(hist_ring, 0)`, c:2626-2629 `freehistnode`),
9118 /// and must leave a key that belongs to a newer entry alone.
9119 #[test]
9120 fn eviction_unhashes_evicted_entry_only() {
9121 use crate::ported::hashtable::{addhistnode, histtab_lock};
9122 let _g = crate::test_util::global_state_lock();
9123 let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
9124 let saved_ring = std::mem::take(&mut *hist_ring.lock().unwrap());
9125 let saved_tab = std::mem::take(&mut *histtab_lock().write().unwrap());
9126 let saved = (histsiz.load(SeqCst), histlinect.load(SeqCst), curhist.load(SeqCst));
9127
9128 // prepnexthistent: ring full at histsiz=2, oldest "a" is evicted.
9129 *hist_ring.lock().unwrap() = vec![make_histent(2, "b".into()), make_histent(1, "a".into())];
9130 addhistnode("a", 1);
9131 addhistnode("b", 2);
9132 histsiz.store(2, SeqCst);
9133 histlinect.store(2, SeqCst);
9134 curhist.store(2, SeqCst);
9135 prepnexthistent();
9136 assert!(histtab_lock().read().unwrap().get("a").is_none(), "evicted key kept");
9137 assert_eq!(histtab_lock().read().unwrap().get("b"), Some(&2));
9138
9139 // resizehistents: evicted file entry 4 shares text with hashed entry 5.
9140 histtab_lock().write().unwrap().clear();
9141 *hist_ring.lock().unwrap() = vec![make_histent(5, "x".into()), make_histent(4, "x".into())];
9142 addhistnode("x", 5);
9143 histsiz.store(1, SeqCst);
9144 histlinect.store(2, SeqCst);
9145 resizehistents();
9146 assert_eq!(hist_ring.lock().unwrap().len(), 1);
9147 assert_eq!(histtab_lock().read().unwrap().get("x"), Some(&5), "newer entry's key dropped");
9148
9149 *hist_ring.lock().unwrap() = saved_ring;
9150 *histtab_lock().write().unwrap() = saved_tab;
9151 histsiz.store(saved.0, SeqCst);
9152 histlinect.store(saved.1, SeqCst);
9153 curhist.store(saved.2, SeqCst);
9154 }
9155
9156 /// c:3869/c:3890/c:3924 — a pushed history stack starts with an empty
9157 /// `histtab` and the pop gives back the saved one. `saveandpophiststack`
9158 /// (c:3960) must not save the history file while `nohistsave` is set.
9159 #[test]
9160 fn histstack_push_pop_swaps_histtab() {
9161 use crate::ported::hashtable::{addhistnode, histtab_lock};
9162 let _g = crate::test_util::global_state_lock();
9163 let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
9164 let saved_ring = std::mem::take(&mut *hist_ring.lock().unwrap());
9165 let saved_tab = std::mem::take(&mut *histtab_lock().write().unwrap());
9166 let saved_active = histactive.swap(0, SeqCst);
9167
9168 addhistnode("keep", 7);
9169 pushhiststack(None, 10, 10, 0);
9170 assert!(histtab_lock().read().unwrap().is_empty(), "pushed stack must start empty");
9171 addhistnode("scratch", 8);
9172
9173 crate::ported::exec::nohistsave.store(1, SeqCst);
9174 assert_eq!(saveandpophiststack(1, 0), 1);
9175 crate::ported::exec::nohistsave.store(0, SeqCst);
9176
9177 let tab = histtab_lock().read().unwrap();
9178 assert_eq!(tab.get("keep"), Some(&7), "pop must restore the saved table");
9179 assert!(tab.get("scratch").is_none(), "pop must drop the pushed table");
9180 drop(tab);
9181
9182 histactive.store(saved_active, SeqCst);
9183 *hist_ring.lock().unwrap() = saved_ring;
9184 *histtab_lock().write().unwrap() = saved_tab;
9185 }
9186}