Skip to main content

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}