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}