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