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 = ©able_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;