Skip to main content

sqz_engine/
text_boundary.rs

1//! UTF-8 boundary-safe slicing helpers.
2//!
3//! sqz slices user-controlled text (command output, session content,
4//! function signatures) at computed byte offsets in many places. Direct
5//! `&s[..n]` panics when `n` lands inside a multi-byte character — with
6//! the shell hook in the pipeline, that panic swallows the user's entire
7//! command output (issue #34, `Привет мир` + a `password:` marker).
8//!
9//! Every truncation of arbitrary text must go through these helpers.
10//! Slicing is only safe to do directly when the surrounding code has
11//! already proven the offset is a boundary (an ASCII-only match, a value
12//! returned by `find()`, a hex hash).
13
14/// Largest byte index `<= max_bytes` that is a `char` boundary of `s`.
15///
16/// `O(1)`: a UTF-8 sequence is at most 4 bytes, so the nearest boundary
17/// is at most 3 bytes back.
18#[inline]
19pub fn floor_char_boundary(s: &str, max_bytes: usize) -> usize {
20    if max_bytes >= s.len() {
21        return s.len();
22    }
23    let mut i = max_bytes;
24    while i > 0 && !s.is_char_boundary(i) {
25        i -= 1;
26    }
27    i
28}
29
30/// Boundary-safe version of `&s[..max_bytes]`: truncates to the largest
31/// char boundary at or below `max_bytes`. Never panics.
32#[inline]
33pub fn truncate_str(s: &str, max_bytes: usize) -> &str {
34    &s[..floor_char_boundary(s, max_bytes)]
35}
36
37/// Boundary-safe version of `s.split_at(mid)`. `mid` is rounded DOWN to
38/// the nearest char boundary so the left side never exceeds `mid` bytes.
39#[inline]
40pub fn split_at_boundary_safe(s: &str, mid: usize) -> (&str, &str) {
41    s.split_at(floor_char_boundary(s, mid))
42}
43
44// ── Tests ─────────────────────────────────────────────────────────────────
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49    use proptest::prelude::*;
50
51    #[test]
52    fn ascii_passthrough() {
53        assert_eq!(truncate_str("hello world", 5), "hello");
54        assert_eq!(truncate_str("hello", 100), "hello");
55        assert_eq!(truncate_str("", 5), "");
56    }
57
58    #[test]
59    fn cyrillic_mid_char() {
60        // "Привет" — every char is 2 bytes. Byte 7 splits 'в' (bytes 6..8).
61        let s = "Привет мир";
62        assert_eq!(truncate_str(s, 7), "При"); // 6 bytes, 3 chars
63        assert_eq!(truncate_str(s, 6), "При");
64        assert_eq!(truncate_str(s, 8), "Прив");
65    }
66
67    #[test]
68    fn four_byte_emoji() {
69        let s = "ab🦀cd"; // 🦀 = 4 bytes at offset 2..6
70        assert_eq!(truncate_str(s, 3), "ab");
71        assert_eq!(truncate_str(s, 4), "ab");
72        assert_eq!(truncate_str(s, 5), "ab");
73        assert_eq!(truncate_str(s, 6), "ab🦀");
74    }
75
76    #[test]
77    fn split_never_panics_mid_char() {
78        let s = "Привет";
79        let (l, r) = split_at_boundary_safe(s, 7);
80        assert_eq!(l, "При");
81        assert_eq!(r, "вет");
82        assert_eq!(format!("{l}{r}"), s);
83    }
84
85    proptest! {
86        /// For ANY string and ANY cut point, truncate_str never panics,
87        /// returns a prefix, and never exceeds the requested byte length.
88        #[test]
89        fn prop_truncate_safe(s in "\\PC*", n in 0usize..512) {
90            let t = truncate_str(&s, n);
91            prop_assert!(t.len() <= n.min(s.len()));
92            prop_assert!(s.starts_with(t));
93        }
94
95        /// split_at_boundary_safe always reassembles to the original.
96        #[test]
97        fn prop_split_reassembles(s in "\\PC*", n in 0usize..512) {
98            let (l, r) = split_at_boundary_safe(&s, n);
99            let reassembled = format!("{l}{r}");
100            let left_len = l.len();
101            prop_assert_eq!(reassembled, s.clone());
102            prop_assert!(left_len <= n.min(s.len()));
103        }
104    }
105}