Skip to main content

smol_bytes/
utf8_buffer.rs

1use core::{borrow::Borrow, ops::RangeBounds, str};
2
3use super::{
4  buffer::Buffer,
5  error::*,
6  utf8_buf::{Utf8Buf, Utf8BufMut},
7};
8
9mod cmp;
10mod fmt;
11mod from;
12mod ops;
13
14#[cfg(feature = "arbitrary")]
15mod arbitrary;
16#[cfg(feature = "borsh")]
17mod borsh;
18#[cfg(all(feature = "quickcheck", any(feature = "std", feature = "alloc")))]
19mod quickcheck;
20#[cfg(feature = "serde")]
21mod serde;
22
23#[cfg(feature = "pyo3")]
24mod python;
25
26#[cfg(feature = "wasm")]
27mod wasm;
28
29#[doc = "UTF-8 validated wrapper around `Buffer` with a String-like interface."]
30#[doc = ""]
31#[doc = "Guarantees valid UTF-8. Split/slice operations check char boundaries."]
32#[doc = "Fixed inline capacity of 62 bytes."]
33#[cfg_attr(not(feature = "wasm"), doc = "")]
34#[cfg_attr(not(feature = "wasm"), doc = "# Examples")]
35#[cfg_attr(not(feature = "wasm"), doc = "")]
36#[cfg_attr(not(feature = "wasm"), doc = "```")]
37#[cfg_attr(not(feature = "wasm"), doc = "use smol_bytes::Utf8Buffer;")]
38#[cfg_attr(not(feature = "wasm"), doc = "")]
39#[cfg_attr(not(feature = "wasm"), doc = "let mut buf = Utf8Buffer::new();")]
40#[cfg_attr(not(feature = "wasm"), doc = "buf.push_str(\"Hello\");")]
41#[cfg_attr(not(feature = "wasm"), doc = "buf.push(' ');")]
42#[cfg_attr(not(feature = "wasm"), doc = "buf.push_str(\"world!\");")]
43#[cfg_attr(
44  not(feature = "wasm"),
45  doc = "assert_eq!(buf.as_str(), \"Hello world!\");"
46)]
47#[cfg_attr(not(feature = "wasm"), doc = "```")]
48#[cfg_attr(feature = "wasm", doc = "")]
49#[cfg_attr(feature = "wasm", doc = "@example")]
50#[cfg_attr(feature = "wasm", doc = "```typescript")]
51#[cfg_attr(feature = "wasm", doc = "import { Utf8Buffer } from 'smol-bytes';")]
52#[cfg_attr(feature = "wasm", doc = "const buf = new Utf8Buffer();")]
53#[cfg_attr(feature = "wasm", doc = "buf.pushStr('hello world');")]
54#[cfg_attr(
55  feature = "wasm",
56  doc = "console.log(buf.toString()); // 'hello world'"
57)]
58#[cfg_attr(feature = "wasm", doc = "```")]
59#[derive(Clone, Copy)]
60#[cfg_attr(feature = "pyo3", ::pyo3::prelude::pyclass(skip_from_py_object))]
61#[cfg_attr(feature = "wasm", wasm_bindgen::prelude::wasm_bindgen)]
62pub struct Utf8Buffer {
63  inner: Buffer,
64}
65
66impl Default for Utf8Buffer {
67  #[cfg_attr(not(coverage), inline(always))]
68  fn default() -> Self {
69    Self::new()
70  }
71}
72
73impl Utf8Buffer {
74  /// Creates a new, empty `Utf8Buffer`.
75  ///
76  /// # Examples
77  ///
78  /// ```
79  /// use smol_bytes::Utf8Buffer;
80  ///
81  /// let buf = Utf8Buffer::new();
82  /// assert!(buf.is_empty());
83  /// ```
84  #[cfg_attr(not(coverage), inline(always))]
85  pub const fn new() -> Self {
86    Self {
87      inner: Buffer::new(),
88    }
89  }
90
91  /// Creates a `Utf8Buffer` from a static string slice.
92  ///
93  /// Unlike the shared immutable UTF-8 wrapper's `from_static` constructor,
94  /// this method **copies** the bytes into the inline buffer — `Utf8Buffer`
95  /// has no heap variant. It exists as a const constructor for parity.
96  ///
97  /// # Panics
98  ///
99  /// Panics at compile time (via `const` assertion) if `s.len() > 62`.
100  ///
101  /// # Examples
102  ///
103  /// ```
104  /// use smol_bytes::Utf8Buffer;
105  ///
106  /// const GREETING: Utf8Buffer = Utf8Buffer::from_static("hello");
107  /// assert_eq!(GREETING.as_str(), "hello");
108  /// ```
109  #[cfg_attr(not(coverage), inline(always))]
110  pub const fn from_static(s: &'static str) -> Self {
111    let bytes = s.as_bytes();
112    assert!(
113      bytes.len() <= crate::INLINE_CAP,
114      "string too large for Utf8Buffer inline capacity"
115    );
116    // SAFETY: length checked against INLINE_CAP above; input is already valid UTF-8.
117    Self {
118      inner: unsafe { Buffer::copy_from_slice(bytes) },
119    }
120  }
121
122  /// Returns a reference to the inner `Buffer`.
123  #[cfg_attr(not(coverage), inline(always))]
124  pub const fn as_inner(&self) -> &Buffer {
125    &self.inner
126  }
127
128  /// Consumes `self` and returns the inner `Buffer`.
129  #[cfg_attr(not(coverage), inline(always))]
130  pub const fn into_inner(self) -> Buffer {
131    self.inner
132  }
133
134  /// Returns the contents as a string slice.
135  ///
136  /// # Examples
137  ///
138  /// ```
139  /// use smol_bytes::Utf8Buffer;
140  ///
141  /// let buf = Utf8Buffer::from("hello");
142  /// assert_eq!(buf.as_str(), "hello");
143  /// ```
144  #[cfg_attr(not(coverage), inline(always))]
145  pub fn as_str(&self) -> &str {
146    // SAFETY: Utf8Buffer guarantees valid UTF-8
147    unsafe { str::from_utf8_unchecked(self.inner.as_slice()) }
148  }
149
150  /// Returns the length of the buffer in bytes.
151  ///
152  /// # Examples
153  ///
154  /// ```
155  /// use smol_bytes::Utf8Buffer;
156  ///
157  /// let buf = Utf8Buffer::from("hello");
158  /// assert_eq!(buf.len(), 5);
159  /// ```
160  #[cfg_attr(not(coverage), inline(always))]
161  pub const fn len(&self) -> usize {
162    self.inner.len()
163  }
164
165  /// Returns `true` if the buffer has a length of 0.
166  ///
167  /// # Examples
168  ///
169  /// ```
170  /// use smol_bytes::Utf8Buffer;
171  ///
172  /// let buf = Utf8Buffer::new();
173  /// assert!(buf.is_empty());
174  /// ```
175  #[cfg_attr(not(coverage), inline(always))]
176  pub const fn is_empty(&self) -> bool {
177    self.inner.is_empty()
178  }
179
180  /// Returns the number of bytes stored in the buffer.
181  ///
182  /// This is equivalent to [`len`](Self::len).
183  ///
184  /// # Examples
185  ///
186  /// ```
187  /// use smol_bytes::Utf8Buffer;
188  ///
189  /// let buf = Utf8Buffer::from("hi");
190  /// assert_eq!(buf.remaining(), 2);
191  /// ```
192  #[cfg_attr(not(coverage), inline(always))]
193  pub const fn remaining(&self) -> usize {
194    self.inner.remaining()
195  }
196
197  /// Returns the capacity of the buffer.
198  #[cfg_attr(not(coverage), inline(always))]
199  pub const fn capacity(&self) -> usize {
200    self.inner.capacity()
201  }
202
203  /// Appends a character to the buffer.
204  ///
205  /// `Utf8Buffer` is backed by a fixed 62-byte inline [`Buffer`]; this
206  /// method panics when the character would not fit. Use [`try_push`]
207  /// for a fallible variant.
208  ///
209  /// [`try_push`]: Self::try_push
210  ///
211  /// # Panics
212  ///
213  /// Panics if the buffer's remaining capacity is less than `ch.len_utf8()`.
214  ///
215  /// # Examples
216  ///
217  /// ```
218  /// use smol_bytes::Utf8Buffer;
219  ///
220  /// let mut buf = Utf8Buffer::new();
221  /// buf.push('a');
222  /// buf.push('é'); // 2-byte char
223  /// assert_eq!(buf.as_str(), "aé");
224  /// ```
225  pub fn push(&mut self, ch: char) {
226    let mut buf = [0u8; 4];
227    let s = ch.encode_utf8(&mut buf);
228    self.inner.put_slice(s.as_bytes());
229  }
230
231  /// Appends a string slice to the buffer.
232  ///
233  /// `Utf8Buffer` is backed by a fixed 62-byte inline [`Buffer`]; this
234  /// method panics when the string would not fit. Use [`try_push_str`]
235  /// for a fallible variant.
236  ///
237  /// [`try_push_str`]: Self::try_push_str
238  ///
239  /// # Panics
240  ///
241  /// Panics if the buffer's remaining capacity is less than `s.len()`.
242  ///
243  /// # Examples
244  ///
245  /// ```
246  /// use smol_bytes::Utf8Buffer;
247  ///
248  /// let mut buf = Utf8Buffer::new();
249  /// buf.push_str("hello");
250  /// assert_eq!(buf.as_str(), "hello");
251  /// ```
252  pub fn push_str(&mut self, s: &str) {
253    self.inner.put_slice(s.as_bytes());
254  }
255
256  /// Tries to append a character to the buffer.
257  ///
258  /// # Errors
259  ///
260  /// Returns [`TryPutError`] if the buffer's remaining capacity is less
261  /// than `ch.len_utf8()`. On error the buffer is unchanged.
262  ///
263  /// # Examples
264  ///
265  /// ```
266  /// use smol_bytes::Utf8Buffer;
267  ///
268  /// let mut buf = Utf8Buffer::from("a".repeat(60).as_str());
269  /// assert!(buf.try_push('a').is_ok());        // 61 bytes
270  /// assert!(buf.try_push('a').is_ok());        // 62 bytes
271  /// assert!(buf.try_push('a').is_err());       // full
272  /// ```
273  pub fn try_push(&mut self, ch: char) -> Result<(), TryPutError> {
274    let mut buf = [0u8; 4];
275    let s = ch.encode_utf8(&mut buf);
276    self.inner.try_put_slice(s.as_bytes())
277  }
278
279  /// Tries to append a string slice to the buffer.
280  ///
281  /// # Errors
282  ///
283  /// Returns [`TryPutError`] if the buffer's remaining capacity is less
284  /// than `s.len()`. On error the buffer is unchanged.
285  ///
286  /// # Examples
287  ///
288  /// ```
289  /// use smol_bytes::Utf8Buffer;
290  ///
291  /// let mut buf = Utf8Buffer::new();
292  /// assert!(buf.try_push_str("hello").is_ok());
293  /// assert_eq!(buf.as_str(), "hello");
294  /// ```
295  pub fn try_push_str(&mut self, s: &str) -> Result<(), TryPutError> {
296    self.inner.try_put_slice(s.as_bytes())
297  }
298
299  /// Clears the buffer, removing all contents.
300  pub fn clear(&mut self) {
301    self.inner.clear();
302  }
303
304  /// Truncates the buffer to the specified length.
305  ///
306  /// # Panics
307  ///
308  /// Panics if `new_len` does not lie on a UTF-8 character boundary.
309  pub fn truncate(&mut self, new_len: usize) {
310    self
311      .try_truncate(new_len)
312      .expect("new_len must lie on a UTF-8 character boundary");
313  }
314
315  /// Truncates the buffer at a UTF-8 character boundary.
316  pub fn try_truncate(&mut self, new_len: usize) -> Result<(), Utf8Error> {
317    <Self as Utf8BufMut>::try_truncate(self, new_len)
318  }
319
320  /// Splits the buffer at `at`, returning the head `[0, at)` and leaving
321  /// `self` with `[at, len)`.
322  ///
323  /// # Panics
324  ///
325  /// Panics if `at` is out of bounds or not on a UTF-8 character boundary.
326  ///
327  /// # Examples
328  ///
329  /// ```
330  /// use smol_bytes::Utf8Buffer;
331  ///
332  /// let mut buf = Utf8Buffer::from("hello world");
333  /// let head = buf.split_to(5);
334  /// assert_eq!(head.as_str(), "hello");
335  /// assert_eq!(buf.as_str(), " world");
336  /// ```
337  pub fn split_to(&mut self, at: usize) -> Self {
338    <Self as Utf8Buf>::split_to(self, at)
339  }
340
341  /// Tries to split the buffer at `at`.
342  ///
343  /// # Errors
344  ///
345  /// Returns [`Utf8Error::InvalidCharBoundary`] if `at` is not on a
346  /// character boundary, or [`Utf8Error::OutOfBounds`] if `at > len`.
347  pub fn try_split_to(&mut self, at: usize) -> Result<Self, Utf8Error> {
348    <Self as Utf8Buf>::try_split_to(self, at)
349  }
350
351  /// Splits the buffer at `at`, returning the tail `[at, len)` and leaving
352  /// `self` with `[0, at)`.
353  ///
354  /// # Panics
355  ///
356  /// Panics if `at` is out of bounds or not on a UTF-8 character boundary.
357  ///
358  /// # Examples
359  ///
360  /// ```
361  /// use smol_bytes::Utf8Buffer;
362  ///
363  /// let mut buf = Utf8Buffer::from("hello world");
364  /// let tail = buf.split_off(6);
365  /// assert_eq!(buf.as_str(), "hello ");
366  /// assert_eq!(tail.as_str(), "world");
367  /// ```
368  pub fn split_off(&mut self, at: usize) -> Self {
369    <Self as Utf8Buf>::split_off(self, at)
370  }
371
372  /// Tries to split the buffer at `at`, returning the tail.
373  ///
374  /// # Errors
375  ///
376  /// Returns [`Utf8Error::InvalidCharBoundary`] if `at` is not on a
377  /// character boundary, or [`Utf8Error::OutOfBounds`] if `at > len`.
378  pub fn try_split_off(&mut self, at: usize) -> Result<Self, Utf8Error> {
379    <Self as Utf8Buf>::try_split_off(self, at)
380  }
381
382  /// Returns a copy of a sub-range of the buffer.
383  ///
384  /// # Panics
385  ///
386  /// Panics if the range is out of bounds or not on UTF-8 character boundaries.
387  ///
388  /// # Examples
389  ///
390  /// ```
391  /// use smol_bytes::Utf8Buffer;
392  ///
393  /// let buf = Utf8Buffer::from("hello world");
394  /// let slice = buf.slice(0..5);
395  /// assert_eq!(slice.as_str(), "hello");
396  /// ```
397  pub fn slice(&self, range: impl core::ops::RangeBounds<usize>) -> Self {
398    <Self as Utf8Buf>::slice(self, range)
399  }
400
401  /// Tries to return a copy of a sub-range of the buffer.
402  ///
403  /// # Errors
404  ///
405  /// Returns [`Utf8Error`] if the range is out of bounds or not on
406  /// character boundaries.
407  pub fn try_slice(&self, range: impl core::ops::RangeBounds<usize>) -> Result<Self, Utf8Error> {
408    <Self as Utf8Buf>::try_slice(self, range)
409  }
410}
411
412impl AsRef<str> for Utf8Buffer {
413  #[cfg_attr(not(coverage), inline(always))]
414  fn as_ref(&self) -> &str {
415    self.as_str()
416  }
417}
418
419impl AsRef<[u8]> for Utf8Buffer {
420  #[cfg_attr(not(coverage), inline(always))]
421  fn as_ref(&self) -> &[u8] {
422    self.as_str().as_bytes()
423  }
424}
425
426impl Borrow<str> for Utf8Buffer {
427  #[cfg_attr(not(coverage), inline(always))]
428  fn borrow(&self) -> &str {
429    self.as_str()
430  }
431}
432
433impl core::ops::Deref for Utf8Buffer {
434  type Target = str;
435
436  #[cfg_attr(not(coverage), inline(always))]
437  fn deref(&self) -> &Self::Target {
438    self.as_str()
439  }
440}
441
442impl Utf8Buf for Utf8Buffer {
443  fn try_split_to(&mut self, at: usize) -> Result<Self, Utf8Error> {
444    self.validate_char_boundary(at)?;
445
446    Ok(Self {
447      inner: self.inner.try_split_to(at).expect("already checked bounds"),
448    })
449  }
450
451  fn try_split_off(&mut self, at: usize) -> Result<Self, Utf8Error> {
452    self.validate_char_boundary(at)?;
453
454    Ok(Self {
455      inner: self
456        .inner
457        .try_split_off(at)
458        .expect("already checked bounds"),
459    })
460  }
461
462  fn try_slice(&self, range: impl RangeBounds<usize>) -> Result<Self, Utf8Error> {
463    let (start, end) = self.range_bounds_to_start_end(range)?;
464
465    Ok(Self {
466      inner: self
467        .inner
468        .try_slice(start..end)
469        .expect("already checked bounds"),
470    })
471  }
472}
473
474impl Utf8BufMut for Utf8Buffer {
475  fn try_truncate(&mut self, new_len: usize) -> Result<(), Utf8Error> {
476    if new_len >= self.len() {
477      return Ok(());
478    }
479    if !self.as_str().is_char_boundary(new_len) {
480      return Err(Utf8Error::InvalidCharBoundary { at: new_len });
481    }
482    self.inner.truncate(new_len);
483    Ok(())
484  }
485
486  fn push(&mut self, ch: char) {
487    let mut buf = [0u8; 4];
488    let s = ch.encode_utf8(&mut buf);
489    self.inner.put_slice(s.as_bytes());
490  }
491
492  fn push_str(&mut self, s: &str) {
493    self.inner.put_slice(s.as_bytes());
494  }
495
496  fn clear(&mut self) {
497    self.inner.clear();
498  }
499}