smol_bytes/utf8_buf.rs
1use core::ops::RangeBounds;
2
3use super::error::{OutOfBounds, Utf8IndexError, normalize_range};
4
5/// Implementation detail that prevents downstream crates from implementing
6/// the UTF-8 buffer traits.
7pub(crate) mod sealed {
8 pub trait Sealed {}
9}
10
11/// Extension trait for UTF-8 validated buffer types.
12///
13/// This trait provides common functionality for buffer types that guarantee
14/// valid UTF-8 content, such as `Utf8Buffer`, `Utf8Bytes`, and `Utf8BytesMut`.
15pub trait Utf8Buf: sealed::Sealed + AsRef<str> + Sized {
16 /// Returns the contents as a string slice.
17 ///
18 /// # Examples
19 ///
20 /// ```
21 /// use smol_bytes::{Utf8Buf, Utf8Bytes};
22 ///
23 /// let buf = Utf8Bytes::from("hello");
24 /// assert_eq!(buf.as_str(), "hello");
25 /// ```
26 fn as_str(&self) -> &str {
27 self.as_ref()
28 }
29
30 /// Returns the length of the buffer in bytes.
31 ///
32 /// # Examples
33 ///
34 /// ```
35 /// use smol_bytes::{Utf8Buf, Utf8Bytes};
36 ///
37 /// let buf = Utf8Bytes::from("hello");
38 /// assert_eq!(buf.len(), 5);
39 /// ```
40 fn len(&self) -> usize {
41 self.as_str().len()
42 }
43
44 /// Returns `true` if the buffer has a length of 0.
45 ///
46 /// # Examples
47 ///
48 /// ```
49 /// use smol_bytes::{Utf8Buf, Utf8Bytes};
50 ///
51 /// let buf = Utf8Bytes::new();
52 /// assert!(buf.is_empty());
53 /// ```
54 fn is_empty(&self) -> bool {
55 self.len() == 0
56 }
57
58 /// Splits the buffer at the given index, returning the content before the split point.
59 ///
60 /// # Panics
61 ///
62 /// Panics if `at` does not lie on a UTF-8 character boundary or is out of bounds.
63 fn split_to(&mut self, at: usize) -> Self {
64 self.try_split_to(at).expect("split_to failed")
65 }
66
67 /// Tries to split the buffer at the given index.
68 ///
69 /// # Errors
70 ///
71 /// Returns an error if `at` is out of bounds or does not lie on a UTF-8 character boundary.
72 fn try_split_to(&mut self, at: usize) -> Result<Self, Utf8IndexError>;
73
74 /// Splits the buffer at the given index, returning the content after the split point.
75 ///
76 /// # Panics
77 ///
78 /// Panics if `at` does not lie on a UTF-8 character boundary or is out of bounds.
79 fn split_off(&mut self, at: usize) -> Self {
80 self.try_split_off(at).expect("split_off failed")
81 }
82
83 /// Tries to split the buffer at the given index, returning the tail.
84 ///
85 /// # Errors
86 ///
87 /// Returns an error if `at` is out of bounds or does not lie on a UTF-8 character boundary.
88 fn try_split_off(&mut self, at: usize) -> Result<Self, Utf8IndexError>;
89
90 /// Returns a sub-slice of the buffer.
91 ///
92 /// # Panics
93 ///
94 /// Panics if the range does not lie on UTF-8 character boundaries or is out of bounds.
95 fn slice(&self, range: impl RangeBounds<usize>) -> Self {
96 self.try_slice(range).expect("slice failed")
97 }
98
99 /// Tries to return a sub-slice of the buffer.
100 ///
101 /// # Errors
102 ///
103 /// Returns an error if the range is out of bounds or does not lie on UTF-8 character boundaries.
104 fn try_slice(&self, range: impl RangeBounds<usize>) -> Result<Self, Utf8IndexError>;
105}
106
107/// Extension trait for mutable UTF-8 validated buffer types.
108///
109/// This trait provides common mutable operations for buffer types that can be
110/// modified while maintaining UTF-8 validity.
111pub trait Utf8BufMut: Utf8Buf + AsMut<str> {
112 /// The error returned when an append cannot fit.
113 type CapacityError: core::error::Error;
114
115 /// Returns the contents as a mutable string slice.
116 fn as_mut_str(&mut self) -> &mut str {
117 self.as_mut()
118 }
119
120 /// Truncates at a UTF-8 character boundary.
121 ///
122 /// A length at or beyond the current byte length is a no-op. On failure the
123 /// buffer is unchanged.
124 fn try_truncate(&mut self, new_len: usize) -> Result<(), Utf8IndexError>;
125
126 /// Attempts to append a character.
127 fn try_push(&mut self, ch: char) -> Result<(), Self::CapacityError>;
128
129 /// Attempts to append a string slice.
130 fn try_push_str(&mut self, s: &str) -> Result<(), Self::CapacityError>;
131
132 /// Appends a character to the buffer.
133 ///
134 /// # Examples
135 ///
136 /// ```
137 /// use smol_bytes::{Utf8BufMut, Utf8BytesMut};
138 ///
139 /// let mut buf = Utf8BytesMut::new();
140 /// buf.push('a');
141 /// buf.push('b');
142 /// assert_eq!(buf.as_str(), "ab");
143 /// ```
144 fn push(&mut self, ch: char) {
145 if let Err(error) = self.try_push(ch) {
146 panic!("UTF-8 buffer capacity exceeded: {error}");
147 }
148 }
149
150 /// Appends a string slice to the buffer.
151 ///
152 /// # Examples
153 ///
154 /// ```
155 /// use smol_bytes::{Utf8BufMut, Utf8BytesMut};
156 ///
157 /// let mut buf = Utf8BytesMut::new();
158 /// buf.push_str("hello");
159 /// assert_eq!(buf.as_str(), "hello");
160 /// ```
161 fn push_str(&mut self, s: &str) {
162 if let Err(error) = self.try_push_str(s) {
163 panic!("UTF-8 buffer capacity exceeded: {error}");
164 }
165 }
166
167 /// Clears the buffer, removing all contents.
168 ///
169 /// # Examples
170 ///
171 /// ```
172 /// use smol_bytes::{Utf8BufMut, Utf8BytesMut};
173 ///
174 /// let mut buf = Utf8BytesMut::from("hello");
175 /// buf.clear();
176 /// assert!(buf.is_empty());
177 /// ```
178 fn clear(&mut self);
179}
180
181/// Validates that `at` is a visible UTF-8 character boundary.
182pub(crate) fn validate_char_boundary(s: &str, at: usize) -> Result<(), Utf8IndexError> {
183 let len = s.len();
184 if at > len {
185 return Err(OutOfBounds::new(at, len).into());
186 }
187 if !s.is_char_boundary(at) {
188 return Err(Utf8IndexError::InvalidCharBoundary { index: at });
189 }
190 Ok(())
191}
192
193/// Normalizes a range and validates both UTF-8 character boundaries.
194pub(crate) fn range_bounds_to_start_end(
195 s: &str,
196 range: impl RangeBounds<usize>,
197) -> Result<(usize, usize), Utf8IndexError> {
198 let (start, end) = normalize_range(range, s.len())?;
199 if !s.is_char_boundary(start) {
200 return Err(Utf8IndexError::InvalidCharBoundary { index: start });
201 }
202 if !s.is_char_boundary(end) {
203 return Err(Utf8IndexError::InvalidCharBoundary { index: end });
204 }
205 Ok((start, end))
206}
207
208/// Maps a Unicode-scalar offset to its byte offset in `s`.
209///
210/// `chars == number of chars` maps to `s.len()`; anything larger is out of range.
211///
212/// Kept crate-level (rather than inside the `pyo3`-gated bindings) so its unit
213/// tests run under the default test configuration. Only the Python bindings call
214/// it, so it is dead code in non-`pyo3`, non-test builds.
215#[allow(dead_code)]
216pub(crate) fn char_offset_to_byte(s: &str, chars: usize) -> Option<usize> {
217 if chars == s.chars().count() {
218 Some(s.len())
219 } else {
220 s.char_indices().nth(chars).map(|(index, _)| index)
221 }
222}
223
224#[cfg(test)]
225mod char_offset_tests {
226 use super::char_offset_to_byte;
227
228 #[test]
229 fn maps_scalar_offset_to_byte_offset() {
230 assert_eq!(char_offset_to_byte("abc", 1), Some(1));
231 assert_eq!(char_offset_to_byte("é", 1), Some(2));
232 assert_eq!(char_offset_to_byte("aé", 1), Some(1));
233 assert_eq!(char_offset_to_byte("aé", 2), Some(3));
234 assert_eq!(char_offset_to_byte("", 0), Some(0));
235 assert_eq!(char_offset_to_byte("a", 2), None);
236 assert_eq!(char_offset_to_byte("🦀x", 1), Some(4));
237 }
238}