Skip to main content

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}