Skip to main content

zsh/ported/
string.rs

1//! String manipulation utilities for zshrs
2//!
3//! Direct port of `Src/string.c` (201 lines, 11 ported).
4//!
5//! Duplicate string on heap when length is known                            // c:44
6//! Append a string to an allocated string, reallocating to make room.      // c:182
7//!
8//! C zsh distinguishes two allocation lanes — `zalloc` (permanent
9//! storage, freed by `zsfree`) and `zhalloc` (heap-arena, bulk-
10//! freed at the end of the current dispatch). Rust's `String` always
11//! owns its allocation and `Drop`s when it falls out of scope, so the
12//! two lanes collapse into one. The function names below are kept
13//! verbatim for caller-side parity with the C source — passing
14//! through to a single owned `String` regardless of whether C would
15//! have used zalloc or zhalloc.
16//!
17//! Byte-faithfulness: C's `memcpy(r, s, len)` copies bytes without
18//! regard for UTF-8 boundaries. The Rust ports use `as_bytes` slicing
19//! plus `from_utf8_lossy` so a `len` that lands mid-codepoint doesn't
20//! panic — matching the C behavior of producing a possibly-truncated
21//! byte string.
22
23/// Port of `dupstring(const char *s)` from `Src/string.c:33`.
24///
25/// C body:
26/// ```c
27/// if (!s) return NULL;
28/// t = (char *) zhalloc(strlen(s) + 1);
29/// strcpy(t, s);
30/// return t;
31/// ```
32///
33/// Heap-arena duplicate. Rust takes `&str` (NULL is impossible);
34/// the heap-arena lane collapses to a regular `String`.
35pub fn dupstring(s: &str) -> String {
36    // c:33
37    s.to_string()
38}
39
40/// Port of `dupstring_wlen(const char *s, unsigned len)` from `Src/string.c:48`.
41///
42/// C body:
43/// ```c
44/// if (!s) return NULL;
45/// t = (char *) zhalloc(len + 1);
46/// memcpy(t, s, len);
47/// t[len] = '\0';
48/// return t;
49/// ```
50///
51/// Byte-counted heap-arena duplicate. The previous Rust port did
52/// `s[..len.min(s.len())]` which panics if `len` lands on a non-
53/// UTF-8 boundary. C just `memcpy`s the bytes; this port matches
54/// that semantic via `as_bytes` slicing + `from_utf8_lossy`.
55pub fn dupstring_wlen(s: &str, len: usize) -> String {
56    // c:48
57    let bytes = s.as_bytes();
58    let n = len.min(bytes.len());
59    String::from_utf8_lossy(&bytes[..n]).into_owned()
60}
61
62/// Port of `ztrdup(const char *s)` from `Src/string.c:62`.
63///
64/// C body:
65/// ```c
66/// if (!s) return NULL;
67/// t = (char *) zalloc(strlen(s) + 1);
68/// strcpy(t, s);
69/// return t;
70/// ```
71///
72/// Permanent-storage duplicate (C's strdup analog). Rust collapses
73/// to `to_string()` since there's no per-allocation lane choice.
74pub fn ztrdup(s: &str) -> String {
75    // c:62
76    s.to_string()
77}
78
79/// Port of `wcs_ztrdup(const wchar_t *s)` from `Src/string.c:77`.
80///
81/// C body (under `#ifdef MULTIBYTE_SUPPORT`):
82/// ```c
83/// if (!s) return NULL;
84/// t = (wchar_t *) zalloc(sizeof(wchar_t) * (wcslen(s) + 1));
85/// wcscpy(t, s);
86/// return t;
87/// ```
88///
89/// Wide-char duplicate. Rust `String` is UTF-8 which subsumes the
90/// wchar_t representation; the conversion is identity.
91pub fn wcs_ztrdup(s: &str) -> String {
92    // c:77
93    s.to_string()
94}
95
96/// Port of `tricat(char const *s1, char const *s2, char const *s3)` from `Src/string.c:98`.
97///
98/// C body uses three `strcpy` calls into a `zalloc(l1+l2+l3+1)`
99/// buffer. Rust port pre-sizes the `String` to avoid reallocation
100/// and pushes the three slices in order.
101///
102// To concatenate four or more strings, see zjoin().                       // c:98
103/// "Permanent" allocation lane in C; Rust's `String` is always
104/// owned so the lane choice is irrelevant.
105pub fn tricat(s1: &str, s2: &str, s3: &str) -> String {
106    // c:98
107    let mut result = String::with_capacity(s1.len() + s2.len() + s3.len());
108    result.push_str(s1);
109    result.push_str(s2);
110    result.push_str(s3);
111    result
112}
113
114/// Port of `zhtricat(char const *s1, char const *s2, char const *s3)` from `Src/string.c:114`.
115///
116/// Heap-arena variant of [`tricat`] in C. Same Rust impl since
117/// the lanes collapse.
118pub fn zhtricat(s1: &str, s2: &str, s3: &str) -> String {
119    // c:114
120    tricat(s1, s2, s3)
121}
122
123/// Port of `dyncat(const char *s1, const char *s2)` from `Src/string.c:131`.
124///
125/// C body:
126/// ```c
127/// ptr = (char *) zhalloc(l1 + strlen(s2) + 1);
128/// strcpy(ptr, s1);
129/// strcpy(ptr + l1, s2);
130/// return ptr;
131/// ```
132///
133// concatenate s1 and s2 in dynamically allocated buffer                    // c:131
134/// Heap-arena two-string concat.
135pub fn dyncat(s1: &str, s2: &str) -> String {
136    // c:131
137    let mut result = String::with_capacity(s1.len() + s2.len());
138    result.push_str(s1);
139    result.push_str(s2);
140    result
141}
142
143/// Port of `bicat(const char *s1, const char *s2)` from `Src/string.c:145`.
144///
145/// Same shape as [`dyncat`], but C uses the permanent-storage
146/// `zalloc` lane. Rust port: identical body.
147pub fn bicat(s1: &str, s2: &str) -> String {
148    // c:145
149    let mut result = String::with_capacity(s1.len() + s2.len());
150    result.push_str(s1);
151    result.push_str(s2);
152    result
153}
154
155/// Port of `dupstrpfx(const char *s, int len)` from `Src/string.c:161`.
156///
157/// C body:
158/// ```c
159/// char *r = zhalloc(len + 1);
160/// memcpy(r, s, len);
161/// r[len] = '\0';
162/// return r;
163/// ```
164///
165// like dupstring(), but with a specified length                             // c:161
166/// Byte-counted prefix copy. The previous Rust port used
167/// `s[..len]` which panics on non-UTF-8 boundary; this port
168/// matches C's `memcpy` semantics via byte slicing.
169pub fn dupstrpfx(s: &str, len: usize) -> String {
170    // c:161
171    let bytes = s.as_bytes();
172    let n = len.min(bytes.len());
173    String::from_utf8_lossy(&bytes[..n]).into_owned()
174}
175
176/// Port of `ztrduppfx(const char *s, int len)` from `Src/string.c:172`.
177///
178/// Same body as [`dupstrpfx`], but C uses the permanent-storage
179/// lane. Lanes collapse in Rust.
180pub fn ztrduppfx(s: &str, len: usize) -> String {
181    dupstrpfx(s, len)
182}
183
184/// Port of `appstr(char *base, char const *append)` from `Src/string.c:186`.
185///
186/// C body:
187/// ```c
188/// return strcat(realloc(base, strlen(base) + strlen(append) + 1),
189///               append);
190/// ```
191///
192/// C reallocates `base` (which may move) and returns the new
193/// pointer. Rust's `&mut String` mutates in place; the equivalent
194/// of C's "return the new pointer" is "the caller's reference is
195/// still valid after the push" — `String::push_str` reallocates
196/// transparently if needed.
197pub fn appstr(base: &mut String, append: &str) {
198    base.push_str(append);
199}
200
201/// Port of `strend(char *str)` from `Src/string.c:196`.
202///
203/// C body:
204/// ```c
205/// if (*str == '\0') return str;
206/// return str + strlen(str) - 1;
207/// ```
208///
209/// C returns a pointer into the input — to the last character if
210/// the string is non-empty, or to the NUL byte (i.e. the start)
211/// if empty. Rust port returns the trailing byte slice for the
212/// closest pointer-shape parity:
213/// - Empty input → empty `&str` (the "`*str == '\\0'`" branch).
214/// - Non-empty input → the trailing UTF-8 character as a `&str`
215///   slice.
216pub fn strend(str: &str) -> &str {
217    if str.is_empty() {
218        return str;
219    }
220    let bytes = str.as_bytes();
221    // Walk back to the start of the last UTF-8 codepoint.
222    let mut i = bytes.len();
223    while i > 0 {
224        i -= 1;
225        if bytes[i] & 0xC0 != 0x80 {
226            // Codepoint boundary (not a continuation byte).
227            return &str[i..];
228        }
229    }
230    str
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn test_dupstring() {
239        let _g = crate::test_util::global_state_lock();
240        assert_eq!(dupstring("hello"), "hello");
241        assert_eq!(dupstring(""), "");
242    }
243
244    #[test]
245    fn test_dupstring_wlen() {
246        let _g = crate::test_util::global_state_lock();
247        assert_eq!(dupstring_wlen("hello world", 5), "hello");
248        // len longer than string is clamped (matches Rust `min` —
249        // C would walk past the NUL which is UB; the safe analog
250        // here is to return the whole string).
251        assert_eq!(dupstring_wlen("hi", 50), "hi");
252        // len of 0 returns empty.
253        assert_eq!(dupstring_wlen("hello", 0), "");
254    }
255
256    #[test]
257    fn test_dupstring_wlen_byte_safe_at_codepoint_boundary() {
258        let _g = crate::test_util::global_state_lock();
259        // C: `memcpy(t, s, len)` copies bytes regardless of UTF-8
260        // boundary. The previous Rust port panicked on
261        // `s[..len.min(s.len())]` if `len` landed mid-codepoint.
262        // Use a 2-byte UTF-8 character: 'é' is 0xC3 0xA9.
263        let s = "café";
264        // bytes: c, a, f, 0xC3, 0xA9
265        // len=4 lands inside the 'é' — must not panic.
266        let r = dupstring_wlen(s, 4);
267        // Replacement char produced by from_utf8_lossy on the
268        // truncated 0xC3 byte.
269        assert!(r.starts_with("caf"));
270    }
271
272    #[test]
273    fn test_ztrdup() {
274        let _g = crate::test_util::global_state_lock();
275        assert_eq!(ztrdup("permanent"), "permanent");
276    }
277
278    #[test]
279    fn test_wcs_ztrdup() {
280        let _g = crate::test_util::global_state_lock();
281        assert_eq!(wcs_ztrdup("ünicode"), "ünicode");
282    }
283
284    #[test]
285    fn test_tricat() {
286        let _g = crate::test_util::global_state_lock();
287        assert_eq!(tricat("a", "b", "c"), "abc");
288        assert_eq!(tricat("", "", ""), "");
289        assert_eq!(tricat("foo", "", "bar"), "foobar");
290    }
291
292    #[test]
293    fn test_zhtricat() {
294        let _g = crate::test_util::global_state_lock();
295        assert_eq!(zhtricat("x", "y", "z"), "xyz");
296    }
297
298    #[test]
299    fn test_bicat() {
300        let _g = crate::test_util::global_state_lock();
301        assert_eq!(bicat("hello", " world"), "hello world");
302        assert_eq!(bicat("", ""), "");
303    }
304
305    #[test]
306    fn test_dyncat() {
307        let _g = crate::test_util::global_state_lock();
308        assert_eq!(dyncat("foo", "bar"), "foobar");
309    }
310
311    #[test]
312    fn test_appstr() {
313        let _g = crate::test_util::global_state_lock();
314        let mut s = "hello".to_string();
315        appstr(&mut s, " world");
316        assert_eq!(s, "hello world");
317    }
318
319    #[test]
320    fn test_dupstrpfx() {
321        let _g = crate::test_util::global_state_lock();
322        assert_eq!(dupstrpfx("hello world", 5), "hello");
323        assert_eq!(dupstrpfx("hi", 50), "hi");
324        assert_eq!(dupstrpfx("hi", 0), "");
325    }
326
327    #[test]
328    fn test_dupstrpfx_byte_safe() {
329        let _g = crate::test_util::global_state_lock();
330        // 'é' = 0xC3 0xA9. len=1 inside it must not panic.
331        let _ = dupstrpfx("é", 1);
332    }
333
334    #[test]
335    fn test_ztrduppfx() {
336        let _g = crate::test_util::global_state_lock();
337        assert_eq!(ztrduppfx("hello", 3), "hel");
338    }
339
340    #[test]
341    fn test_strend_returns_last_codepoint() {
342        let _g = crate::test_util::global_state_lock();
343        // C returns pointer to last char (or to NUL on empty).
344        // Rust returns the trailing &str slice for pointer-shape parity.
345        assert_eq!(strend("hello"), "o");
346        assert_eq!(strend(""), "");
347        // Multibyte: 'é' is 2 bytes; strend returns the whole codepoint.
348        assert_eq!(strend("café"), "é");
349        // Single ASCII char.
350        assert_eq!(strend("a"), "a");
351    }
352
353    /// c:98 — `tricat(s1,s2,s3)` is the canonical 3-string concat used
354    /// everywhere zsh builds `${prefix}${name}${suffix}`. Regression
355    /// dropping any segment silently corrupts every param-subst path.
356    #[test]
357    fn tricat_concatenates_three_segments_in_order() {
358        let _g = crate::test_util::global_state_lock();
359        assert_eq!(tricat("a", "b", "c"), "abc");
360        assert_eq!(tricat("", "b", "c"), "bc");
361        assert_eq!(tricat("a", "", "c"), "ac");
362        assert_eq!(tricat("a", "b", ""), "ab");
363    }
364
365    /// c:131 — `dyncat(s1,s2)` is the 2-string concat counterpart.
366    #[test]
367    fn dyncat_concatenates_two_segments() {
368        let _g = crate::test_util::global_state_lock();
369        assert_eq!(dyncat("hello", " world"), "hello world");
370        assert_eq!(dyncat("", "x"), "x");
371    }
372
373    /// c:62 — `ztrdup` is the owning copy. Verifies the duplicate
374    /// is independent of the source after a mutating clear.
375    #[test]
376    fn ztrdup_returns_independent_owned_copy() {
377        let _g = crate::test_util::global_state_lock();
378        let mut src = String::from("original");
379        let dup = ztrdup(&src);
380        src.clear();
381        assert_eq!(dup, "original", "dup must survive source-side clear");
382    }
383
384    /// c:161 — `dupstrpfx(s, len)` returns first `len` bytes; len > s.len()
385    /// must NOT panic — returns whole string. Critical for any
386    /// truncation path that doesn't pre-clamp.
387    #[test]
388    fn dupstrpfx_handles_len_larger_than_input() {
389        let _g = crate::test_util::global_state_lock();
390        assert_eq!(dupstrpfx("ab", 100), "ab");
391        assert_eq!(dupstrpfx("hello", 0), "");
392        assert_eq!(dupstrpfx("hello", 3), "hel");
393    }
394
395    /// c:131 — `dyncat` with both empty inputs returns empty (no
396    /// phantom delimiters).
397    #[test]
398    fn dyncat_empty_inputs_return_empty() {
399        let _g = crate::test_util::global_state_lock();
400        assert_eq!(dyncat("", ""), "");
401    }
402
403    /// `Src/string.c:144-155` — `bicat(s1, s2)` is the
404    /// permanent-storage variant of `dyncat`. C body computes
405    /// `zalloc(strlen(s1)+strlen(s2)+1)` then `strcpy(ptr, s1)` and
406    /// `strcpy(ptr+l1, s2)`. Two-segment concat, never reorders.
407    #[test]
408    fn bicat_concatenates_in_order_with_either_empty() {
409        let _g = crate::test_util::global_state_lock();
410        assert_eq!(bicat("foo", "bar"), "foobar");
411        assert_eq!(
412            bicat("", "bar"),
413            "bar",
414            "c:152 — strcpy(ptr, \"\") writes only the NUL, ptr+0 starts s2"
415        );
416        assert_eq!(
417            bicat("foo", ""),
418            "foo",
419            "c:153 — strcpy(ptr+3, \"\") writes only the NUL"
420        );
421        assert_eq!(bicat("", ""), "");
422    }
423
424    /// `Src/string.c:172-178` — `ztrduppfx(s, len)` body is identical
425    /// to `dupstrpfx` (same `memcpy`/NUL pattern at c:175-177); only
426    /// the allocator differs (`zalloc` vs `zhalloc`). Both lanes
427    /// collapse to `String` in the Rust port. Behaviour parity with
428    /// `dupstrpfx` is the contract — a regression that diverged the
429    /// two would silently leak storage-lane assumptions into callers.
430    #[test]
431    fn ztrduppfx_matches_dupstrpfx_byte_for_byte() {
432        let _g = crate::test_util::global_state_lock();
433        for (s, len) in [("hello", 3usize), ("ab", 100), ("hello", 0), ("", 5)] {
434            assert_eq!(
435                ztrduppfx(s, len),
436                dupstrpfx(s, len),
437                "ztrduppfx/dupstrpfx divergence at ({:?}, {})",
438                s,
439                len
440            );
441        }
442    }
443
444    /// `Src/string.c:186-189` — `appstr(base, append)` C body is
445    /// `strcat(realloc(base, strlen(base)+strlen(append)+1), append)`.
446    /// Append-in-place semantics: post-condition is `base == base ++ append`.
447    /// Empty append → base unchanged. Empty base → result equals append.
448    #[test]
449    fn appstr_appends_in_place() {
450        let _g = crate::test_util::global_state_lock();
451        let mut b = String::from("foo");
452        appstr(&mut b, "bar");
453        assert_eq!(b, "foobar");
454        // c:188 — strcat with empty s2 leaves base unchanged.
455        appstr(&mut b, "");
456        assert_eq!(b, "foobar", "appending empty must leave base unchanged");
457        // Empty base + nonempty append.
458        let mut e = String::new();
459        appstr(&mut e, "xyz");
460        assert_eq!(e, "xyz");
461    }
462
463    /// `Src/string.c:195-201` — `strend(str)`. C body:
464    /// `if (*str == '\0') return str; return str + strlen(str) - 1;`.
465    /// Single-char input → that char (no underflow on `len-1`).
466    /// Multi-char input → last char only.
467    #[test]
468    fn strend_returns_only_last_character_for_multichar_input() {
469        let _g = crate::test_util::global_state_lock();
470        // c:200 — `str + strlen(str) - 1` for "hello" (len=5) → 'o'.
471        assert_eq!(strend("hello"), "o");
472        // c:200 — len=2 → 'b'.
473        assert_eq!(strend("ab"), "b");
474        // c:198 — empty input falls through `*str == '\0'` branch and
475        // returns the empty string (the pointer-to-NUL in C).
476        assert_eq!(strend(""), "");
477    }
478
479    /// `Src/string.c:32-42` — `dupstring(s)`. C body:
480    /// `if (!s) return NULL; t = zhalloc(strlen(s)+1); strcpy(t,s); return t;`.
481    /// Empty string round-trips (no underflow on len=0).
482    #[test]
483    fn dupstring_returns_owned_copy_with_identity_content() {
484        let _g = crate::test_util::global_state_lock();
485        assert_eq!(dupstring("hello"), "hello");
486        assert_eq!(
487            dupstring(""),
488            "",
489            "c:39 — empty input → len 0+1, strcpy copies NUL"
490        );
491        // Non-ASCII (UTF-8) round-trips byte-identical.
492        assert_eq!(dupstring("café"), "café");
493        assert_eq!(dupstring("字"), "字");
494    }
495
496    /// `Src/string.c:47-58` — `dupstring_wlen(s, len)`. C body:
497    /// `memcpy(t, s, len); t[len] = '\\0';`. Byte-counted copy — len
498    /// can be less than, equal to, or greater than `strlen(s)`. The
499    /// Rust port via `as_bytes()` slicing must match `memcpy`
500    /// semantics, including the `len > s.len()` case which clamps
501    /// (C would read past the buffer — UB; Rust port clamps to
502    /// avoid panic per the impl note at c:50).
503    #[test]
504    fn dupstring_wlen_respects_byte_length_and_clamps_overflow() {
505        let _g = crate::test_util::global_state_lock();
506        // c:55 — memcpy(t, s, len) for len < strlen.
507        assert_eq!(dupstring_wlen("hello world", 5), "hello");
508        // len == 0 → empty.
509        assert_eq!(dupstring_wlen("hello", 0), "");
510        // Clamp: Rust port returns whole string rather than reading
511        // past the buffer (C would have been UB).
512        assert_eq!(dupstring_wlen("ab", 100), "ab");
513        // Exact-length boundary.
514        assert_eq!(dupstring_wlen("foo", 3), "foo");
515    }
516
517    /// `Src/string.c:76-85` — `wcs_ztrdup(const wchar_t *s)`. C body
518    /// is the wide-char version of `ztrdup`: copies the wchar_t string
519    /// into a zalloc'd buffer. Rust UTF-8 `String` subsumes the
520    /// wchar_t representation — identity copy.
521    #[test]
522    fn wcs_ztrdup_returns_independent_copy() {
523        let _g = crate::test_util::global_state_lock();
524        let mut src = String::from("widechar");
525        let dup = wcs_ztrdup(&src);
526        src.clear();
527        assert_eq!(
528            dup, "widechar",
529            "wide-char dup must survive source-side mutation"
530        );
531        // Non-ASCII paths.
532        assert_eq!(wcs_ztrdup("éàü字"), "éàü字");
533    }
534
535    /// `Src/string.c:113-128` — `zhtricat(s1, s2, s3)`. C body uses
536    /// heap-arena allocator (zhalloc) instead of permanent zalloc.
537    /// Both lanes collapse to `String` in Rust; behaviour must match
538    /// tricat exactly. Pin parity with tricat for the same three
539    /// inputs — a regression diverging the two would silently change
540    /// memory ownership in C but produce wrong content if anything
541    /// changed at the byte level.
542    #[test]
543    fn zhtricat_matches_tricat_byte_for_byte() {
544        let _g = crate::test_util::global_state_lock();
545        for (a, b, c) in [
546            ("foo", "bar", "baz"),
547            ("", "x", ""),
548            ("a", "", "z"),
549            ("", "", ""),
550        ] {
551            assert_eq!(
552                zhtricat(a, b, c),
553                tricat(a, b, c),
554                "lane divergence at ({:?}, {:?}, {:?})",
555                a,
556                b,
557                c
558            );
559        }
560    }
561
562    /// `Src/string.c:171-181` — `ztrduppfx(s, len)` is `dupstrpfx`
563    /// with permanent storage. We already pinned the body-identical
564    /// contract above; this test pins behaviour for `len > strlen`
565    /// specifically (the C source would `memcpy` past the source
566    /// buffer — UB; the Rust port clamps).
567    #[test]
568    fn ztrduppfx_clamps_oversize_len_safely() {
569        let _g = crate::test_util::global_state_lock();
570        assert_eq!(ztrduppfx("hi", 100), "hi");
571        assert_eq!(ztrduppfx("", 5), "");
572        assert_eq!(ztrduppfx("abc", 2), "ab");
573    }
574
575    // ═══════════════════════════════════════════════════════════════════
576    // Concatenation helpers — `tricat`, `bicat`, `dyncat`, `dupstring`,
577    // `appstr`. Each pinned against direct string equality. Empty-arg
578    // edge cases pinned explicitly so a regression that drops empties
579    // surfaces immediately.
580    // ═══════════════════════════════════════════════════════════════════
581
582    // ── tricat: 3-string concatenation ───────────────────────────────
583    /// Plain 3-string concat.
584    #[test]
585    fn tricat_three_non_empty_strings() {
586        assert_eq!(tricat("foo", "bar", "baz"), "foobarbaz");
587    }
588
589    /// First arg empty — others stay.
590    #[test]
591    fn tricat_empty_first_keeps_others() {
592        assert_eq!(tricat("", "bar", "baz"), "barbaz");
593    }
594
595    /// Middle arg empty.
596    #[test]
597    fn tricat_empty_middle_keeps_others() {
598        assert_eq!(tricat("foo", "", "baz"), "foobaz");
599    }
600
601    /// Last arg empty.
602    #[test]
603    fn tricat_empty_last_keeps_others() {
604        assert_eq!(tricat("foo", "bar", ""), "foobar");
605    }
606
607    /// All empty → empty.
608    #[test]
609    fn tricat_all_empty_yields_empty() {
610        assert_eq!(tricat("", "", ""), "");
611    }
612
613    /// Multi-byte UTF-8 across all three positions.
614    #[test]
615    fn tricat_multibyte_utf8_concatenates_correctly() {
616        assert_eq!(tricat("日", "本", "語"), "日本語");
617    }
618
619    // ── bicat: 2-string concatenation ────────────────────────────────
620    /// Plain bicat.
621    #[test]
622    fn bicat_two_non_empty_strings() {
623        assert_eq!(bicat("hello", " world"), "hello world");
624    }
625
626    /// First empty.
627    #[test]
628    fn bicat_empty_first_returns_second() {
629        assert_eq!(bicat("", "world"), "world");
630    }
631
632    /// Second empty.
633    #[test]
634    fn bicat_empty_second_returns_first() {
635        assert_eq!(bicat("hello", ""), "hello");
636    }
637
638    /// Both empty.
639    #[test]
640    fn bicat_both_empty_yields_empty() {
641        assert_eq!(bicat("", ""), "");
642    }
643
644    // ── dyncat: dynamic concat (same as bicat for our purposes) ─────
645    /// dyncat plain.
646    #[test]
647    fn dyncat_two_strings() {
648        assert_eq!(dyncat("abc", "xyz"), "abcxyz");
649    }
650
651    /// dyncat with empties.
652    #[test]
653    fn dyncat_empties() {
654        assert_eq!(dyncat("", "x"), "x");
655        assert_eq!(dyncat("x", ""), "x");
656        assert_eq!(dyncat("", ""), "");
657    }
658
659    // ── appstr: in-place append ─────────────────────────────────────
660    /// appstr appends to existing buffer.
661    #[test]
662    fn appstr_appends_to_existing_string() {
663        let mut s = String::from("hello");
664        appstr(&mut s, " world");
665        assert_eq!(s, "hello world");
666    }
667
668    /// appstr to empty buffer.
669    #[test]
670    fn appstr_to_empty_buffer_yields_argument() {
671        let mut s = String::new();
672        appstr(&mut s, "value");
673        assert_eq!(s, "value");
674    }
675
676    /// appstr of empty is no-op.
677    #[test]
678    fn appstr_empty_argument_is_noop() {
679        let mut s = String::from("preserved");
680        appstr(&mut s, "");
681        assert_eq!(s, "preserved");
682    }
683
684    /// appstr multiple times accumulates.
685    #[test]
686    fn appstr_repeated_calls_accumulate() {
687        let mut s = String::new();
688        appstr(&mut s, "a");
689        appstr(&mut s, "b");
690        appstr(&mut s, "c");
691        assert_eq!(s, "abc");
692    }
693
694    // ── strend: slice from last codepoint ───────────────────────────
695    // NOTE: zshrs's strend() returns the slice STARTING at the last
696    // codepoint, NOT a past-the-end pointer like C's strend.
697    // Pin the actual observed contract.
698
699    /// `strend("hello")` returns `"o"` (last codepoint).
700    #[test]
701    fn strend_returns_slice_starting_at_last_codepoint() {
702        assert_eq!(strend("hello"), "o");
703    }
704
705    /// `strend("")` returns empty.
706    #[test]
707    fn strend_empty_input_returns_empty() {
708        assert_eq!(strend(""), "");
709    }
710
711    /// `strend("a")` returns `"a"` (single char IS the last codepoint).
712    #[test]
713    fn strend_single_char_returns_self() {
714        assert_eq!(strend("a"), "a");
715    }
716
717    /// Multi-byte: `strend("日本")` returns just `"本"` (last codepoint
718    /// regardless of byte width).
719    #[test]
720    fn strend_multibyte_returns_last_codepoint_only() {
721        assert_eq!(strend("日本"), "本");
722    }
723
724    // ── ztrdup / dupstring: identity copy ───────────────────────────
725    /// dupstring is value-equal to input.
726    #[test]
727    fn dupstring_returns_identical_content() {
728        assert_eq!(dupstring("hello world"), "hello world");
729        assert_eq!(dupstring(""), "");
730        assert_eq!(dupstring("日本語"), "日本語");
731    }
732
733    /// ztrdup mirrors dupstring for ASCII (and UTF-8 in zshrs's port).
734    #[test]
735    fn ztrdup_identity_for_ascii_and_utf8() {
736        assert_eq!(ztrdup("hello"), "hello");
737        assert_eq!(ztrdup(""), "");
738    }
739
740    // ═══════════════════════════════════════════════════════════════════
741    // Additional C-parity tests for Src/string.c.
742    // ═══════════════════════════════════════════════════════════════════
743
744    /// c:91 — `wcs_ztrdup` round-trip on ASCII.
745    #[test]
746    fn wcs_ztrdup_ascii_round_trip() {
747        assert_eq!(wcs_ztrdup("hello"), "hello");
748        assert_eq!(wcs_ztrdup(""), "");
749    }
750
751    /// c:118 — `zhtricat("a", "b", "c")` returns "abc" (3-string concat
752    /// via heap arena).
753    #[test]
754    fn zhtricat_joins_three_strings() {
755        assert_eq!(zhtricat("a", "b", "c"), "abc");
756        assert_eq!(zhtricat("", "middle", ""), "middle");
757        assert_eq!(zhtricat("pre", "", "suf"), "presuf");
758    }
759
760    /// c:118 — `zhtricat` matches `tricat` (heap-arena vs permanent
761    /// distinction collapses in Rust).
762    #[test]
763    fn zhtricat_matches_tricat() {
764        for (a, b, c) in &[("x", "y", "z"), ("", "", ""), ("foo", "bar", "baz")] {
765            assert_eq!(zhtricat(a, b, c), tricat(a, b, c));
766        }
767    }
768
769    /// c:135/147 — `dyncat` matches `bicat` for any input pair.
770    #[test]
771    fn dyncat_matches_bicat() {
772        for (a, b) in &[("foo", "bar"), ("", ""), ("hello", "")] {
773            assert_eq!(dyncat(a, b), bicat(a, b));
774        }
775    }
776
777    /// c:161 — `dupstrpfx("hello", 3)` returns "hel".
778    #[test]
779    fn dupstrpfx_takes_byte_prefix() {
780        assert_eq!(dupstrpfx("hello", 3), "hel");
781        assert_eq!(dupstrpfx("hello", 0), "");
782        // Overflow clamps to len.
783        assert_eq!(dupstrpfx("hi", 100), "hi");
784    }
785
786    /// c:172 — `ztrduppfx` matches `dupstrpfx` (lanes collapse).
787    #[test]
788    fn ztrduppfx_matches_dupstrpfx() {
789        for (s, n) in &[("hello", 3), ("", 0), ("foo", 100)] {
790            assert_eq!(ztrduppfx(s, *n), dupstrpfx(s, *n));
791        }
792    }
793
794    /// c:186 — `appstr` accumulates across multiple calls.
795    #[test]
796    fn appstr_accumulates_multiple_pushes() {
797        let mut s = String::from("a");
798        appstr(&mut s, "b");
799        appstr(&mut s, "c");
800        appstr(&mut s, "d");
801        assert_eq!(s, "abcd");
802    }
803
804    /// c:186 — `appstr(_, "")` is no-op.
805    #[test]
806    fn appstr_empty_append_is_noop() {
807        let mut s = String::from("hello");
808        appstr(&mut s, "");
809        assert_eq!(s, "hello");
810    }
811
812    /// c:196 — `strend("")` returns empty &str.
813    #[test]
814    fn strend_empty_returns_empty() {
815        assert_eq!(strend(""), "");
816    }
817
818    /// c:196 — `strend("a")` returns "a" (single char).
819    #[test]
820    fn strend_single_char_returns_self_pin() {
821        assert_eq!(strend("a"), "a");
822    }
823
824    /// c:196 — `strend("abc")` returns "c" (last char).
825    #[test]
826    fn strend_returns_last_ascii_char() {
827        assert_eq!(strend("abc"), "c");
828        assert_eq!(strend("hello"), "o");
829    }
830
831    /// c:55 — `dupstring_wlen("hello", 100)` clamps to byte length.
832    #[test]
833    fn dupstring_wlen_overlong_clamps() {
834        assert_eq!(dupstring_wlen("hi", 100), "hi");
835        assert_eq!(dupstring_wlen("", 100), "");
836    }
837
838    /// c:35 — `dupstring` returns OWNED String (caller can mutate
839    /// without affecting source).
840    #[test]
841    fn dupstring_returns_owned_independent_copy() {
842        let src = "hello";
843        let mut dup = dupstring(src);
844        dup.push_str("_mut");
845        assert_eq!(src, "hello", "source unchanged");
846        assert_eq!(dup, "hello_mut");
847    }
848
849    // ═══════════════════════════════════════════════════════════════════
850    // Additional C-parity tests for Src/string.c
851    // c:35 dupstring / c:74 ztrdup / c:91 wcs_ztrdup / c:105 tricat /
852    // c:118 zhtricat / c:135 dyncat / c:147 bicat / c:169 dupstrpfx /
853    // c:197 appstr / c:216 strend
854    // ═══════════════════════════════════════════════════════════════════
855
856    /// c:35 — `dupstring("")` empty returns empty String.
857    #[test]
858    fn dupstring_empty_returns_empty_pin() {
859        assert_eq!(dupstring(""), "");
860    }
861
862    /// c:74 — `ztrdup("")` empty returns empty String.
863    #[test]
864    fn ztrdup_empty_returns_empty() {
865        assert_eq!(ztrdup(""), "");
866    }
867
868    /// c:74 — `ztrdup` is pure.
869    #[test]
870    fn ztrdup_is_pure() {
871        for s in ["", "abc", "hello world", "日本"] {
872            let first = ztrdup(s);
873            for _ in 0..3 {
874                assert_eq!(ztrdup(s), first, "ztrdup({:?}) must be pure", s);
875            }
876        }
877    }
878
879    /// c:91 — `wcs_ztrdup("")` empty returns empty String.
880    #[test]
881    fn wcs_ztrdup_empty_returns_empty() {
882        assert_eq!(wcs_ztrdup(""), "");
883    }
884
885    /// c:105 — `tricat("", "", "")` all empty returns empty.
886    #[test]
887    fn tricat_all_empty_returns_empty() {
888        assert_eq!(tricat("", "", ""), "");
889    }
890
891    /// c:105 — `tricat("a", "b", "c")` concatenates to "abc".
892    #[test]
893    fn tricat_concatenates_three_parts() {
894        assert_eq!(tricat("a", "b", "c"), "abc");
895    }
896
897    /// c:135 — `dyncat("", "")` both empty returns empty.
898    #[test]
899    fn dyncat_both_empty_returns_empty() {
900        assert_eq!(dyncat("", ""), "");
901    }
902
903    /// c:147 — `bicat("", "")` both empty returns empty.
904    #[test]
905    fn bicat_both_empty_returns_empty() {
906        assert_eq!(bicat("", ""), "");
907    }
908
909    /// c:147 — `bicat("a", "b")` concatenates to "ab".
910    #[test]
911    fn bicat_concatenates_two_parts() {
912        assert_eq!(bicat("a", "b"), "ab");
913    }
914
915    /// c:169 — `dupstrpfx("", 0)` empty returns empty.
916    #[test]
917    fn dupstrpfx_empty_zero_len_returns_empty() {
918        assert_eq!(dupstrpfx("", 0), "");
919    }
920
921    /// c:169 — `dupstrpfx("abc", 0)` zero len returns empty.
922    #[test]
923    fn dupstrpfx_zero_len_returns_empty() {
924        assert_eq!(dupstrpfx("abc", 0), "");
925    }
926
927    /// c:216 — `strend("abc")` returns the last character slice.
928    #[test]
929    fn strend_returns_last_char_slice() {
930        let s = "abc";
931        let e = strend(s);
932        assert_eq!(e.len(), 1, "strend returns 1-char str");
933        assert_eq!(e.chars().next(), Some('c'));
934    }
935
936    // ═══════════════════════════════════════════════════════════════════
937    // Additional C-parity tests for Src/string.c
938    // c:35 dupstring / c:74 ztrdup / c:105 tricat / c:135 dyncat
939    // c:147 bicat / c:169 dupstrpfx / c:216 strend + determinism pins
940    // ═══════════════════════════════════════════════════════════════════
941
942    /// c:35 — `dupstring` returns String (compile-time type pin).
943    #[test]
944    fn dupstring_returns_string_type() {
945        let _: String = dupstring("anything");
946    }
947
948    /// c:74 — `ztrdup` returns String (compile-time type pin).
949    #[test]
950    fn ztrdup_returns_string_type() {
951        let _: String = ztrdup("anything");
952    }
953
954    /// c:105 — `tricat` returns String (compile-time type pin).
955    #[test]
956    fn tricat_returns_string_type() {
957        let _: String = tricat("a", "b", "c");
958    }
959
960    /// c:147 — `bicat` returns String (compile-time type pin).
961    #[test]
962    fn bicat_returns_string_type() {
963        let _: String = bicat("a", "b");
964    }
965
966    /// c:216 — `strend` returns &str (compile-time type pin).
967    #[test]
968    fn strend_returns_str_type() {
969        let s = "abc";
970        let _: &str = strend(s);
971    }
972
973    /// c:35 — `dupstring` is deterministic (pure function).
974    #[test]
975    fn dupstring_is_deterministic() {
976        for s in ["", "a", "hello world", "café"] {
977            let first = dupstring(s);
978            for _ in 0..3 {
979                assert_eq!(
980                    dupstring(s),
981                    first,
982                    "dupstring({:?}) must be deterministic",
983                    s
984                );
985            }
986        }
987    }
988
989    /// c:105 — `tricat` is deterministic.
990    #[test]
991    fn tricat_is_deterministic() {
992        let first = tricat("foo", "bar", "baz");
993        for _ in 0..3 {
994            assert_eq!(
995                tricat("foo", "bar", "baz"),
996                first,
997                "tricat must be deterministic"
998            );
999        }
1000    }
1001
1002    /// c:118 — `zhtricat` is identical to `tricat` (same body, different
1003    /// storage lane). Verify byte-for-byte parity.
1004    #[test]
1005    fn zhtricat_matches_tricat_byte_for_byte_pin() {
1006        for (a, b, c) in [
1007            ("", "", ""),
1008            ("foo", "bar", "baz"),
1009            ("x", "", "y"),
1010            ("a", "b", ""),
1011            ("café", "日", "中"),
1012        ] {
1013            assert_eq!(
1014                zhtricat(a, b, c),
1015                tricat(a, b, c),
1016                "zhtricat({:?},{:?},{:?}) must match tricat",
1017                a,
1018                b,
1019                c
1020            );
1021        }
1022    }
1023
1024    /// c:55 — `dupstring_wlen(s, len)` length-bounded copy returns
1025    /// String (compile-time type pin).
1026    #[test]
1027    fn dupstring_wlen_returns_string_type() {
1028        let _: String = dupstring_wlen("hello", 3);
1029    }
1030
1031    /// c:55 — `dupstring_wlen` with len == s.len() returns whole string.
1032    #[test]
1033    fn dupstring_wlen_exact_len_returns_whole_string() {
1034        assert_eq!(
1035            dupstring_wlen("hello", 5),
1036            "hello",
1037            "len == s.len() must return whole string"
1038        );
1039    }
1040
1041    /// c:197 — `appstr(base, "")` empty append leaves base unchanged.
1042    #[test]
1043    fn appstr_empty_append_leaves_base_unchanged() {
1044        let mut s = "base".to_string();
1045        appstr(&mut s, "");
1046        assert_eq!(s, "base", "empty append is no-op");
1047    }
1048
1049    /// c:197 — `appstr("", x)` append-to-empty equals just x.
1050    #[test]
1051    fn appstr_to_empty_yields_appendage() {
1052        let mut s = String::new();
1053        appstr(&mut s, "added");
1054        assert_eq!(s, "added", "append to empty base = appendage");
1055    }
1056
1057    // ═══════════════════════════════════════════════════════════════════
1058    // Additional C-parity tests for Src/string.c
1059    // c:74 ztrdup / c:91 wcs_ztrdup / c:135 dyncat / c:147 bicat /
1060    // c:169 dupstrpfx / c:180 ztrduppfx / c:216 strend
1061    // ═══════════════════════════════════════════════════════════════════
1062
1063    /// c:74 — `ztrdup` returns String (compile-time pin, alt).
1064    #[test]
1065    fn ztrdup_returns_string_pin_alt() {
1066        let _: String = ztrdup("");
1067    }
1068
1069    /// c:74 — `ztrdup(s)` preserves content for ASCII + multibyte.
1070    #[test]
1071    fn ztrdup_preserves_content() {
1072        for s in ["", "x", "hello world", "日本語", "café"] {
1073            assert_eq!(ztrdup(s), s, "ztrdup must preserve {:?}", s);
1074        }
1075    }
1076
1077    /// c:91 — `wcs_ztrdup` returns String (compile-time pin).
1078    #[test]
1079    fn wcs_ztrdup_returns_string_type() {
1080        let _: String = wcs_ztrdup("");
1081    }
1082
1083    /// c:135 — `dyncat("", "")` returns empty (alt).
1084    #[test]
1085    fn dyncat_both_empty_returns_empty_alt() {
1086        assert_eq!(dyncat("", ""), "", "empty + empty → empty");
1087    }
1088
1089    /// c:135 — `dyncat(s, "")` equals s (right-identity).
1090    #[test]
1091    fn dyncat_right_identity_empty() {
1092        for s in ["", "x", "hello"] {
1093            assert_eq!(dyncat(s, ""), s, "dyncat({:?}, '') must equal {:?}", s, s);
1094        }
1095    }
1096
1097    /// c:135 — `dyncat("", s)` equals s (left-identity).
1098    #[test]
1099    fn dyncat_left_identity_empty() {
1100        for s in ["", "x", "hello"] {
1101            assert_eq!(dyncat("", s), s, "dyncat('', {:?}) must equal {:?}", s, s);
1102        }
1103    }
1104
1105    /// c:147 — `bicat("", "")` returns empty (alt).
1106    #[test]
1107    fn bicat_both_empty_returns_empty_alt() {
1108        assert_eq!(bicat("", ""), "");
1109    }
1110
1111    /// c:147 — `bicat` returns String (compile-time pin, alt).
1112    #[test]
1113    fn bicat_returns_string_pin_alt() {
1114        let _: String = bicat("a", "b");
1115    }
1116
1117    /// c:169 — `dupstrpfx(s, 0)` returns empty (zero-len prefix).
1118    #[test]
1119    fn dupstrpfx_zero_returns_empty() {
1120        for s in ["", "x", "hello"] {
1121            assert_eq!(dupstrpfx(s, 0), "", "dupstrpfx({:?}, 0) must be empty", s);
1122        }
1123    }
1124
1125    /// c:180 — `ztrduppfx(s, 0)` returns empty (zero-len prefix).
1126    #[test]
1127    fn ztrduppfx_zero_returns_empty() {
1128        for s in ["", "x", "hello"] {
1129            assert_eq!(ztrduppfx(s, 0), "");
1130        }
1131    }
1132
1133    /// c:216 — `strend("")` returns "" (empty input has empty end, alt).
1134    #[test]
1135    fn strend_empty_returns_empty_alt() {
1136        assert_eq!(strend(""), "", "empty → empty end");
1137    }
1138
1139    /// c:216 — `strend` is deterministic (pure).
1140    #[test]
1141    fn strend_is_deterministic() {
1142        for s in ["", "x", "abc", "hello"] {
1143            let first = strend(s);
1144            for _ in 0..3 {
1145                assert_eq!(strend(s), first, "strend({:?}) must be pure", s);
1146            }
1147        }
1148    }
1149}