Skip to main content

fmtbuf/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(not(feature = "std"), no_std)]
3#![allow(
4    clippy::doc_link_with_quotes,
5    reason = "README.md links use bracketed text containing quoted Unicode names like `\"U+200D\"`; these are real Markdown links, not malformed intra-doc links."
6)]
7
8mod truncated;
9mod utf8;
10
11use core::fmt;
12
13pub use truncated::{Truncated, TruncatedResultExt};
14
15use utf8::rfind_utf8_end;
16
17/// A write buffer pointing to a `&mut [u8]`.
18///
19/// ```
20/// use fmtbuf::WriteBuf;
21/// use core::fmt::Write;
22///
23/// // The buffer to write into. The contents can be uninitialized, but using a
24/// // bogus `\xff` sigil for demonstration.
25/// let mut buf: [u8; 128] = [0xff; 128];
26/// let mut writer = WriteBuf::new(&mut buf);
27///
28/// // Write data to the buffer.
29/// write!(writer, "some data: {}", 0x01a4).unwrap();
30///
31/// // Finish writing:
32/// let written = writer.finish().unwrap();
33/// assert_eq!(written, "some data: 420");
34/// ```
35pub struct WriteBuf<'a> {
36    target: &'a mut [u8],
37    position: usize,
38    reserve: usize,
39    truncated: bool,
40}
41
42impl<'a> WriteBuf<'a> {
43    /// Create an instance that will write to the given `target`. The contents of the target do not need to have been
44    /// initialized before this, as they will be overwritten by writing.
45    pub fn new(target: &'a mut [u8]) -> Self {
46        Self {
47            target,
48            position: 0,
49            reserve: 0,
50            truncated: false,
51        }
52    }
53
54    /// Create an instance that will write to the given `target` and `reserve` bytes at the end that will not be written
55    /// be `write_str` operations.
56    ///
57    /// The use of this constructor is to note that `reserve` bytes will always be written at the end of the buffer (by
58    /// the [`WriteBuf::finish_with`] family of functions), so `write_str` should not bother writing to it. This is
59    /// useful when you know that you will always `finish_with` a null terminator or other character.
60    ///
61    /// It is allowed to have `target.len() < reserve`, but this can never be written to.
62    ///
63    /// ```
64    /// use fmtbuf::WriteBuf;
65    /// use core::fmt::Write;
66    ///
67    /// // Reserve one byte at the end for a null terminator.
68    /// let mut buf: [u8; 8] = [0xff; 8];
69    /// let mut writer = WriteBuf::with_reserve(&mut buf, 1);
70    ///
71    /// // Only 7 bytes are writable; the rest is held for the suffix.
72    /// let _ = write!(writer, "abcdefgh");
73    /// let written = writer.finish_with("\0").unwrap_err().written();
74    /// assert_eq!(written, "abcdefg\0");
75    /// ```
76    pub fn with_reserve(target: &'a mut [u8], reserve: usize) -> Self {
77        Self {
78            target,
79            position: 0,
80            reserve,
81            truncated: false,
82        }
83    }
84
85    /// Get the position in the target buffer. The value is one past the end of written content and the next position to
86    /// be written to.
87    #[must_use]
88    pub fn position(&self) -> usize {
89        self.position
90    }
91
92    /// Get the total size of the target buffer in bytes. This is fixed for the lifetime of the [`WriteBuf`].
93    #[inline]
94    #[must_use]
95    pub const fn capacity(&self) -> usize {
96        self.target.len()
97    }
98
99    /// Get the number of bytes still available for `write_str` to consume.
100    ///
101    /// This is `capacity - position - reserve` (saturating on `reserve`), or `0` if [`WriteBuf::truncated`] is set,
102    /// since further writes via [`core::fmt::Write`] would immediately fail. Use this as a fast pre-check before a
103    /// `write!` call:
104    ///
105    /// ```
106    /// use fmtbuf::WriteBuf;
107    /// use core::fmt::Write;
108    ///
109    /// let mut buf: [u8; 16] = [0xff; 16];
110    /// let mut writer = WriteBuf::new(&mut buf);
111    /// write!(writer, "hello").unwrap();
112    /// assert_eq!(writer.remaining(), 11);
113    /// ```
114    ///
115    /// To query the raw arithmetic distance regardless of the truncated flag, use
116    /// `buf.capacity().saturating_sub(buf.position()).saturating_sub(buf.reserve())`.
117    #[inline]
118    #[must_use]
119    pub fn remaining(&self) -> usize {
120        if self.truncated {
121            0
122        } else {
123            // `position <= target.len()` by invariant; `reserve` is unconstrained by `with_reserve`'s contract,
124            // so saturate only the second subtraction.
125            (self.target.len() - self.position).saturating_sub(self.reserve)
126        }
127    }
128
129    /// Get if a truncated write has happened.
130    #[must_use]
131    pub fn truncated(&self) -> bool {
132        self.truncated
133    }
134
135    /// Get the count of reserved bytes.
136    #[must_use]
137    pub fn reserve(&self) -> usize {
138        self.reserve
139    }
140
141    /// Set the reserve bytes to `count`. If the written section has already encroached on the reserve space, this has
142    /// no immediate effect, but it will prevent future writes. If [`WriteBuf::truncated`] has already been triggered,
143    /// it will not be reset.
144    pub fn set_reserve(&mut self, count: usize) {
145        self.reserve = count;
146    }
147
148    /// Reset the buffer for reuse. Sets [`position`](Self::position) to zero and clears the
149    /// [`truncated`](Self::truncated) flag, so the buffer can be written to again. The configured
150    /// [`reserve`](Self::reserve) is preserved, and the bytes already in the target are not overwritten (they are
151    /// already considered uninitialized/sentinel).
152    ///
153    /// ```
154    /// use fmtbuf::WriteBuf;
155    /// use core::fmt::Write;
156    ///
157    /// let mut buf: [u8; 4] = [0xff; 4];
158    /// let mut writer = WriteBuf::new(&mut buf);
159    /// // First attempt overflows.
160    /// let _ = write!(writer, "too long");
161    /// assert!(writer.truncated());
162    ///
163    /// // Reset and try a shorter string.
164    /// writer.clear();
165    /// write!(writer, "ok").unwrap();
166    /// assert_eq!(writer.written(), "ok");
167    /// ```
168    #[inline]
169    pub fn clear(&mut self) {
170        self.position = 0;
171        self.truncated = false;
172    }
173
174    /// Get the contents that have been written so far.
175    #[must_use]
176    pub fn written_bytes(&self) -> &[u8] {
177        &self.target[..self.position]
178    }
179
180    /// Get the contents that have been written so far.
181    #[must_use]
182    pub fn written(&self) -> &str {
183        // safety: The only way to write into the buffer is with valid UTF-8, so there is no reason to check the
184        // contents for validity.
185        unsafe { from_utf8_expect(self.written_bytes()) }
186    }
187
188    /// Finish writing to the buffer. This returns control of the target buffer to the caller (it is no longer mutably
189    /// borrowed).
190    ///
191    /// # Returns
192    ///
193    /// On success, the successfully-written portion of the output as a `&str`.
194    ///
195    /// # Errors
196    ///
197    /// Returns `Err(Truncated)` if any prior write into this buffer was rejected by truncation. The `Truncated`
198    /// carries the same successfully-written `&str` that the `Ok` case would have returned, accessible via
199    /// [`Truncated::written`].
200    pub fn finish(self) -> Result<&'a str, Truncated<'a>> {
201        self.into_result()
202    }
203
204    fn into_result(self) -> Result<&'a str, Truncated<'a>> {
205        // safety: The only way to write into the buffer is with valid UTF-8
206        let written = unsafe { from_utf8_expect(&self.target[..self.position]) };
207        if self.truncated {
208            Err(Truncated(written))
209        } else {
210            Ok(written)
211        }
212    }
213
214    /// Finish the buffer, adding the `suffix` to the end. A common use case for this is to add a null terminator.
215    ///
216    /// This operates slightly differently than the normal format writing function `write_str` in that the `suffix` is
217    /// always put at the end. The only case where this will not happen is when `suffix.len()` exceeds the size of the
218    /// buffer originally provided. In this case, the last bit of `suffix` will be copied (starting at a valid UTF-8
219    /// sequence start; e.g.: writing `"🚀..."` to a 5 byte buffer will leave you with just `"..."`, no matter what was
220    /// written before).
221    ///
222    /// ```
223    /// use fmtbuf::WriteBuf;
224    ///
225    /// let mut buf: [u8; 4] = [0xff; 4];
226    /// let writer = WriteBuf::new(&mut buf);
227    ///
228    /// // Finish writing with too many bytes:
229    /// let written = writer.finish_with("12345").unwrap_err().written();
230    /// assert_eq!(written, "2345");
231    /// ```
232    ///
233    /// # Errors
234    ///
235    /// Returns `Err(Truncated)` if any prior write into this buffer was rejected by truncation, or if `suffix`
236    /// did not fit alongside the prior content. The `Truncated` carries the successfully-written `&str` (which
237    /// includes the suffix when the suffix could be placed). See [`WriteBuf::finish`] for the full semantics.
238    #[allow(
239        clippy::used_underscore_items,
240        reason = "`_finish_with` is the shared internal helper for both `finish_with` and `finish_with_or`; the leading underscore disambiguates it from this public method."
241    )]
242    pub fn finish_with(self, suffix: impl AsRef<str>) -> Result<&'a str, Truncated<'a>> {
243        let suffix = suffix.as_ref();
244        self._finish_with(suffix, suffix)
245    }
246
247    /// Finish the buffer by adding `normal_suffix` if not truncated or `truncated_suffix` if the buffer will be
248    /// truncated. This operates the same as [`WriteBuf::finish_with`] in every other way.
249    ///
250    /// ```
251    /// use fmtbuf::WriteBuf;
252    /// use core::fmt::Write;
253    ///
254    /// // Plenty of room: the normal suffix is appended.
255    /// let mut buf: [u8; 8] = [0xff; 8];
256    /// let mut writer = WriteBuf::new(&mut buf);
257    /// write!(writer, "abc").unwrap();
258    /// assert_eq!(writer.finish_with_or("!", "...").unwrap(), "abc!");
259    ///
260    /// // Doesn't fit: the truncated suffix is used instead.
261    /// let mut buf: [u8; 4] = [0xff; 4];
262    /// let mut writer = WriteBuf::new(&mut buf);
263    /// let _ = write!(writer, "abcdef");
264    /// assert_eq!(writer.finish_with_or("!", "...").unwrap_err().written(), "a...");
265    /// ```
266    ///
267    /// # Errors
268    ///
269    /// Returns `Err(Truncated)` when truncation occurred -- either because a prior write was rejected, or
270    /// because `normal_suffix` could not be placed and `truncated_suffix` was used instead. The `Truncated`
271    /// carries the successfully-written `&str`. See [`WriteBuf::finish`] for the full semantics.
272    #[allow(
273        clippy::used_underscore_items,
274        reason = "`_finish_with` is the shared internal helper for both `finish_with` and `finish_with_or`; the leading underscore disambiguates it from this public method."
275    )]
276    pub fn finish_with_or(
277        self,
278        normal_suffix: impl AsRef<str>,
279        truncated_suffix: impl AsRef<str>,
280    ) -> Result<&'a str, Truncated<'a>> {
281        self._finish_with(normal_suffix.as_ref(), truncated_suffix.as_ref())
282    }
283
284    fn _finish_with(mut self, normal: &str, truncated: &str) -> Result<&'a str, Truncated<'a>> {
285        let remaining = self.target.len() - self.position();
286
287        // If the truncated case is shorter than the normal case, then writing it might still work
288        for (suffix, should_test) in [(normal, !self.truncated), (truncated, true)] {
289            if !should_test {
290                continue;
291            }
292
293            // enough room in the buffer to write entire suffix, so just write it
294            if suffix.len() <= remaining {
295                self.target[self.position..self.position + suffix.len()].copy_from_slice(suffix.as_bytes());
296                self.position += suffix.len();
297                return self.into_result();
298            }
299
300            // we attempted to perform a write, but rejected it
301            self.truncated = true;
302        }
303
304        let suffix = truncated;
305
306        // if the suffix is larger than the entire target buffer, copy the last N
307        if self.target.len() < suffix.len() {
308            let suffix_bytes = suffix.as_bytes();
309            let copyable_suffix = &suffix_bytes[suffix.len() - self.target.len()..];
310            let Some(valid_utf8_idx) = copyable_suffix
311                .iter()
312                .enumerate()
313                .find(|(_, cu)| utf8::utf8_char_width(**cu).is_some())
314                .map(|(idx, _)| idx)
315            else {
316                self.position = 0;
317                self.truncated = true;
318                return self.into_result();
319            };
320            let copyable_suffix = &copyable_suffix[valid_utf8_idx..];
321            self.target[..copyable_suffix.len()].copy_from_slice(copyable_suffix);
322            self.position = copyable_suffix.len();
323            self.truncated = true;
324            return self.into_result();
325        }
326
327        // Scan backwards to find the position we should write to (can't interrupt a UTF-8 multibyte sequence)
328        let potential_end_idx = self.target.len() - suffix.len();
329        let write_idx = rfind_utf8_end(&self.target[..potential_end_idx]);
330        self.target[write_idx..write_idx + suffix.len()].copy_from_slice(suffix.as_bytes());
331        self.position = write_idx + suffix.len();
332        self.truncated = true;
333        self.into_result()
334    }
335
336    fn _write(&mut self, input: &[u8]) -> fmt::Result {
337        if self.truncated() {
338            return Err(fmt::Error);
339        }
340
341        let remaining = self.target.len() - self.position();
342        if remaining < self.reserve() {
343            self.truncated = true;
344            return Err(fmt::Error);
345        }
346        let remaining = remaining - self.reserve();
347
348        let (input, result) = if remaining >= input.len() {
349            (input, Ok(()))
350        } else {
351            let to_write = &input[..remaining];
352            self.truncated = true;
353            (&input[..rfind_utf8_end(to_write)], Err(fmt::Error))
354        };
355
356        self.target[self.position..self.position + input.len()].copy_from_slice(input);
357        self.position += input.len();
358
359        result
360    }
361}
362
363impl fmt::Debug for WriteBuf<'_> {
364    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
365        f.debug_struct("WriteBuf")
366            .field("position", &self.position)
367            .field("capacity", &self.target.len())
368            .field("reserve", &self.reserve)
369            .field("truncated", &self.truncated)
370            .field("written", &self.written())
371            .finish()
372    }
373}
374
375impl fmt::Write for WriteBuf<'_> {
376    /// Append `s` to the target buffer.
377    ///
378    /// # Errors
379    ///
380    /// Returns `Err(fmt::Error)` if the entirety of `s` can not fit in the target buffer or if a previous
381    /// `write_str` operation failed. When the input doesn't fit, as much of `s` as can fit into the buffer will
382    /// be written up to the last valid Unicode code point. In other words, if the target buffer has 6 writable
383    /// bytes left and `s` is the two code points `"♡🐶"` (a.k.a. the 7 byte `b"\xe2\x99\xa1\xf0\x9f\x90\xb6"`),
384    /// then only `♡` will make it to the output buffer, making the target of your ♡ ambiguous.
385    ///
386    /// Truncation marks this buffer as truncated, which can be observed with [`WriteBuf::truncated`]. Future write
387    /// attempts will immediately return in `Err`. This also affects the behavior of [`WriteBuf::finish`] family of
388    /// functions, which will always return the `Err` case to indicate truncation. For [`WriteBuf::finish_with_or`],
389    /// the `normal_suffix` will not be attempted.
390    #[allow(
391        clippy::used_underscore_items,
392        reason = "`_write` is the shared internal helper for `fmt::Write`; the leading underscore distinguishes it from the trait method."
393    )]
394    fn write_str(&mut self, s: &str) -> fmt::Result {
395        self._write(s.as_bytes())
396    }
397}
398
399/// Extract a `&str` from source `&[u8]`, expecting it to be valid UTF-8.
400///
401/// # Safety
402///
403/// Under the covers, this calls `str::from_utf8` if debug assertions are enabled, otherwise it calls
404/// `str::from_utf8_unchecked`. This means `src` should always be valid UTF-8.
405unsafe fn from_utf8_expect(src: &[u8]) -> &str {
406    #[cfg(debug_assertions)]
407    return core::str::from_utf8(src).expect("buffer should have been valid UTF-8");
408
409    // safety: The contents are valid UTF-8 by construction; debug builds verify the invariant via the
410    // `cfg(debug_assertions)` branch above.
411    #[cfg(not(debug_assertions))]
412    unsafe {
413        core::str::from_utf8_unchecked(src)
414    }
415}
416
417#[cfg(test)]
418mod test;