fmtbuf/lib.rs
1//! # `fmtbuf`
2//! This library is intended to help write formatted text to fixed buffers.
3//!
4//! ```
5//! use fmtbuf::WriteBuf;
6//! use std::fmt::Write;
7//!
8//! let mut buf: [u8; 10] = [0; 10];
9//! let mut writer = WriteBuf::new(&mut buf);
10//! if let Err(e) = write!(&mut writer, "πππ") {
11//! println!("write error: {e:?}");
12//! }
13//! let written_len = match writer.finish_with_or("!", "β¦") {
14//! Ok(len) => len, // <- won't be hit since πππ is 12 bytes
15//! Err(len) => {
16//! println!("writing was truncated");
17//! len
18//! }
19//! };
20//! let written = &buf[..written_len];
21//! assert_eq!("πβ¦", std::str::from_utf8(written).unwrap());
22//! ```
23//!
24//! A few things happened in that example:
25//!
26//! 1. We stared with a 10 byte buffer
27//! 2. Tried to write `"πππ"` to it, which is encoded as 3 `b"\xf0\x9f\x9a\x80"`s (12 bytes)
28//! 3. This can't fit into 10 bytes, so only `"ππ"` is stored and the `writer` is noted as having truncated writes
29//! 4. We finish the buffer with `"!"` on success or `"β¦"` (a.k.a. `b"\xe2\x80\xa6"`) on truncation
30//! 5. Since we noted truncation in step #3, we try to write `"β¦"`, but this can not fit into the buffer either, since
31//! 8 (`"ππ".len()`) + 3 (`"β¦".len()`) > 12 (`buf.len()`)
32//! 6. Roll the buffer back to the end of the first π, then add β¦, leaving us with `"πβ¦"`
33
34#![cfg_attr(not(feature = "std"), no_std)]
35
36mod utf8;
37
38use core::fmt;
39
40#[deprecated]
41pub use utf8::rfind_utf8_end;
42
43/// A write buffer pointing to a `&mut [u8]`.
44///
45/// ```
46/// use fmtbuf::WriteBuf;
47/// use std::fmt::Write;
48///
49/// // The buffer to write into. The contents can be uninitialized, but using a
50/// // bogus `\xff` sigil for demonstration.
51/// let mut buf: [u8; 128] = [0xff; 128];
52/// let mut writer = WriteBuf::new(&mut buf);
53///
54/// // Write data to the buffer.
55/// write!(writer, "some data: {}", 0x01a4).unwrap();
56///
57/// // Finish writing:
58/// let write_len = writer.finish().unwrap();
59/// let written = std::str::from_utf8(&buf[..write_len]).unwrap();
60/// assert_eq!(written, "some data: 420");
61/// ```
62pub struct WriteBuf<'a> {
63 target: &'a mut [u8],
64 position: usize,
65 reserve: usize,
66 truncated: bool,
67}
68
69impl<'a> WriteBuf<'a> {
70 /// Create an instance that will write to the given `target`. The contents of the target do not need to have been
71 /// initialized before this, as they will be overwritten by writing.
72 pub fn new(target: &'a mut [u8]) -> Self {
73 Self {
74 target,
75 position: 0,
76 reserve: 0,
77 truncated: false,
78 }
79 }
80
81 /// Create an instance that will write to the given `target` and `reserve` bytes at the end that will not be written
82 /// be `write_str` operations.
83 ///
84 /// The use of this constructor is to note that `reserve` bytes will always be written at the end of the buffer (by
85 /// the [`WriteBuf::finish_with`] family of functions), so `write_str` should not bother writing to it. This is
86 /// useful when you know that you will always `finish_with` a null terminator or other character.
87 ///
88 /// It is allowed to have `target.len() < reserve`, but this can never be written to.
89 pub fn with_reserve(target: &'a mut [u8], reserve: usize) -> Self {
90 Self {
91 target,
92 position: 0,
93 reserve,
94 truncated: false,
95 }
96 }
97
98 /// Get the position in the target buffer. The value is one past the end of written content and the next position to
99 /// be written to.
100 pub fn position(&self) -> usize {
101 self.position
102 }
103
104 /// Get if a truncated write has happened.
105 pub fn truncated(&self) -> bool {
106 self.truncated
107 }
108
109 /// Get the count of reserved bytes.
110 pub fn reserve(&self) -> usize {
111 self.reserve
112 }
113
114 /// Set the reserve bytes to `count`. If the written section has already encroached on the reserve space, this has
115 /// no immediate effect, but it will prevent future writes. If [`WriteBuf::truncated`] has already been triggered,
116 /// it will not be reset.
117 pub fn set_reserve(&mut self, count: usize) {
118 self.reserve = count;
119 }
120
121 /// Get the contents that have been written so far.
122 pub fn written_bytes(&self) -> &[u8] {
123 &self.target[..self.position]
124 }
125
126 /// Get the contents that have been written so far.
127 pub fn written(&self) -> &str {
128 #[cfg(debug_assertions)]
129 return core::str::from_utf8(self.written_bytes()).expect("contents of buffer should have been UTF-8 encoded");
130
131 // safety: The only way to write into the buffer is with valid UTF-8, so there is no reason to check the
132 // contents for validity. They're still checked in debug builds just in case, though.
133 #[cfg(not(debug_assertions))]
134 unsafe {
135 core::str::from_utf8_unchecked(self.written_bytes())
136 }
137 }
138
139 /// Finish writing to the buffer. This returns control of the target buffer to the caller (it is no longer mutably
140 /// borrowed) and returns the number of bytes written.
141 ///
142 /// # Returns
143 ///
144 /// In both the `Ok` and `Err` cases, the [`WriteBuf::position`] is returned. The `Ok` case indicates the truncation
145 /// did not occur, while `Err` indicates that it did.
146 pub fn finish(self) -> Result<usize, usize> {
147 if self.truncated() {
148 Err(self.position())
149 } else {
150 Ok(self.position())
151 }
152 }
153
154 /// Finish the buffer, adding the `suffix` to the end. A common use case for this is to add a null terminator.
155 ///
156 /// This operates slightly differently than the normal format writing function `write_str` in that the `suffix` is
157 /// always put at the end. The only case where this will not happen is when `suffix.len()` is less than the size of
158 /// the buffer originally provided. In this case, the last bit of `suffix` will be copied (starting at a valid UTF-8
159 /// sequence start; e.g.: writing `"π..."` to a 5 byte buffer will leave you with just `"..."`, no matter what was
160 /// written before).
161 ///
162 /// ```
163 /// use fmtbuf::WriteBuf;
164 ///
165 /// let mut buf: [u8; 4] = [0xff; 4];
166 /// let mut writer = WriteBuf::new(&mut buf);
167 ///
168 /// // Finish writing with too many bytes:
169 /// let write_len = writer.finish_with("12345").unwrap_err();
170 /// assert_eq!(write_len, 4);
171 /// let buf_str = std::str::from_utf8(&buf).unwrap();
172 /// assert_eq!(buf_str, "2345");
173 /// ```
174 ///
175 /// # Returns
176 ///
177 /// The returned value has the same meaning as [`WriteBuf::finish`].
178 pub fn finish_with(self, suffix: impl AsRef<[u8]>) -> Result<usize, usize> {
179 let suffix = suffix.as_ref();
180 self._finish_with(suffix, suffix)
181 }
182
183 /// Finish the buffer by adding `normal_suffix` if not truncated or `truncated_suffix` if the buffer will be
184 /// truncated. This operates the same as [`WriteBuf::finish_with`] in every other way.
185 pub fn finish_with_or(
186 self,
187 normal_suffix: impl AsRef<[u8]>,
188 truncated_suffix: impl AsRef<[u8]>,
189 ) -> Result<usize, usize> {
190 self._finish_with(normal_suffix.as_ref(), truncated_suffix.as_ref())
191 }
192
193 fn _finish_with(mut self, normal: &[u8], truncated: &[u8]) -> Result<usize, usize> {
194 let remaining = self.target.len() - self.position();
195
196 // If the truncated case is shorter than the normal case, then writing it might still work
197 for (suffix, should_test) in [(normal, !self.truncated), (truncated, true)] {
198 if !should_test {
199 continue;
200 }
201
202 // enough room in the buffer to write entire suffix, so just write it
203 if suffix.len() <= remaining {
204 self.target[self.position..self.position + suffix.len()].copy_from_slice(suffix);
205 self.position += suffix.len();
206 return if self.truncated() {
207 Err(self.position())
208 } else {
209 Ok(self.position())
210 };
211 }
212
213 // we attempted to perform a write, but rejected it
214 self.truncated = true;
215 }
216
217 let suffix = truncated;
218
219 // if the suffix is larger than the entire target buffer, copy the last N
220 if self.target.len() < suffix.len() {
221 let copyable_suffix = &suffix[suffix.len() - self.target.len()..];
222 let Some(valid_utf8_idx) = copyable_suffix
223 .iter()
224 .enumerate()
225 .find(|(_, cu)| utf8::utf8_char_width(**cu).is_some())
226 .map(|(idx, _)| idx)
227 else {
228 return Err(0);
229 };
230 let copyable_suffix = ©able_suffix[valid_utf8_idx..];
231 self.target[..copyable_suffix.len()].copy_from_slice(copyable_suffix);
232 return Err(copyable_suffix.len());
233 }
234
235 // Scan backwards to find the position we should write to (can't interrupt a UTF-8 multibyte sequence)
236 let potential_end_idx = self.target.len() - suffix.len();
237 let write_idx = rfind_utf8_end(&self.target[..potential_end_idx]);
238 self.target[write_idx..write_idx + suffix.len()].copy_from_slice(suffix);
239 Err(write_idx + suffix.len())
240 }
241
242 fn _write(&mut self, input: &[u8]) -> fmt::Result {
243 if self.truncated() {
244 return Err(fmt::Error);
245 }
246
247 let remaining = self.target.len() - self.position();
248 if remaining < self.reserve() {
249 self.truncated = true;
250 return Err(fmt::Error);
251 }
252 let remaining = remaining - self.reserve();
253
254 let (input, result) = if remaining >= input.len() {
255 (input, Ok(()))
256 } else {
257 let to_write = &input[..remaining];
258 self.truncated = true;
259 (&input[..rfind_utf8_end(to_write)], Err(fmt::Error))
260 };
261
262 self.target[self.position..self.position + input.len()].copy_from_slice(input);
263 self.position += input.len();
264
265 result
266 }
267}
268
269impl<'a> fmt::Write for WriteBuf<'a> {
270 /// Append `s` to the target buffer.
271 ///
272 /// # Error
273 ///
274 /// An error is returned if the entirety of `s` can not fit in the target buffer or if a previous `write_str`
275 /// operation failed. If this occurs, as much as `s` that can fit into the buffer will be written up to the last
276 /// valid Unicode code point. In other words, if the target buffer have 6 writable bytes left and `s` is the two
277 /// code points `"β‘πΆ"` (a.k.a.: the 7 byte `b"\xe2\x99\xa1\xf0\x9f\x90\xb6"`), then only `β‘` will make it to the
278 /// output buffer, making the target of your β‘ ambiguous.
279 ///
280 /// Truncation marks this buffer as truncated, which can be observed with [`WriteBuf::truncated`]. Future write
281 /// attempts will immediately return in `Err`. This also affects the behavior of [`WriteBuf::finish`] family of
282 /// functions, which will always return the `Err` case to indicate truncation. For [`WriteBuf::finish_with_or`],
283 /// the `normal_suffix` will not be attempted.
284 fn write_str(&mut self, s: &str) -> fmt::Result {
285 self._write(s.as_bytes())
286 }
287}
288
289#[cfg(test)]
290mod test {
291 use super::*;
292 use core::fmt::Write;
293
294 /// * `.0`: Input string
295 /// * `.1`: The end position if the last byte was chopped off
296 static TEST_CASES: &[(&str, usize)] = &[
297 ("", 0),
298 ("James", 4),
299 ("_ΓΈ", 1),
300 ("磨", 0),
301 ("here: θ§/θ¦", 10),
302 ("π¨εγθΆ", 10),
303 ("π", 0),
304 ("πππ", 8),
305 ("rocket: π", 8),
306 ];
307
308 #[test]
309 fn rfind_utf8_end_test() {
310 for (input, last_valid_idx_after_cut) in TEST_CASES.iter() {
311 let result = rfind_utf8_end(input.as_bytes());
312 assert_eq!(result, input.len(), "input=\"{input}\"");
313 if input.len() == 0 {
314 continue;
315 }
316 let input_truncated = &input.as_bytes()[..input.len() - 1];
317 let result = rfind_utf8_end(input_truncated);
318 assert_eq!(
319 result, *last_valid_idx_after_cut,
320 "input=\"{input}\" truncated={input_truncated:?}"
321 );
322 }
323 }
324
325 #[test]
326 fn format_enough_space() {
327 for (input, _) in TEST_CASES.iter() {
328 let mut buf: [u8; 128] = [0xff; 128];
329 let mut writer = WriteBuf::new(&mut buf);
330
331 writer.write_str(input).unwrap();
332 assert_eq!(input.len(), writer.position());
333 let last_idx = writer.finish().unwrap();
334 assert_eq!(input.len(), last_idx);
335 }
336 }
337
338 #[test]
339 fn format_enough_space_just_enough_reserved() {
340 for (input, _) in TEST_CASES.iter() {
341 let mut buf: [u8; 128] = [0xff; 128];
342 let mut writer = WriteBuf::with_reserve(&mut buf[..input.len() + 1], 1);
343
344 writer.write_str(input).unwrap();
345 assert_eq!(input.len(), writer.position());
346 let last_idx = writer.finish().unwrap();
347 assert_eq!(input.len(), last_idx);
348 }
349 }
350
351 #[test]
352 fn format_truncation() {
353 for (input, last_valid_idx_after_cut) in TEST_CASES.iter() {
354 if input.len() == 0 {
355 continue;
356 }
357
358 let mut buf: [u8; 128] = [0xff; 128];
359 let mut writer = WriteBuf::new(&mut buf[..input.len() - 1]);
360
361 writer.write_str(input).unwrap_err();
362 assert_eq!(*last_valid_idx_after_cut, writer.position());
363 assert!(writer.truncated());
364 write!(writer, "!!!").expect_err("writes should fail here");
365
366 let last_idx = writer.finish().unwrap_err();
367 assert_eq!(*last_valid_idx_after_cut, last_idx);
368 }
369 }
370
371 struct SimpleString {
372 storage: [u8; 128],
373 size: usize,
374 }
375
376 impl SimpleString {
377 pub fn from_segments(segments: &[&str]) -> Self {
378 let mut out = Self {
379 storage: [0; 128],
380 size: 0,
381 };
382 for segment in segments {
383 out.append(segment);
384 }
385 out
386 }
387
388 pub fn append(&mut self, value: &str) {
389 let value = value.as_bytes();
390 self.storage[self.size..self.size + value.len()].copy_from_slice(value);
391 self.size += value.len();
392 }
393
394 pub fn as_str(&self) -> &str {
395 core::str::from_utf8(&self.storage[..self.size]).unwrap()
396 }
397 }
398
399 impl From<&str> for SimpleString {
400 fn from(value: &str) -> Self {
401 Self::from_segments(&[value])
402 }
403 }
404
405 #[test]
406 fn finish_with_enough_space() {
407 for (input, _) in TEST_CASES.iter() {
408 let mut buf: [u8; 128] = [0xff; 128];
409 let mut writer = WriteBuf::new(&mut buf);
410
411 writer.write_str(input).unwrap();
412 let position = writer.finish_with(b".123").unwrap();
413 assert_eq!(position, input.len() + 4);
414 let expected_written = SimpleString::from_segments(&[input, ".123"]);
415 let actually_wriiten = core::str::from_utf8(&buf[..position]).unwrap();
416 assert_eq!(expected_written.as_str(), actually_wriiten);
417 }
418 }
419
420 #[test]
421 fn finish_with_overwrite() {
422 for (input, last_valid_idx_after_cut) in TEST_CASES.iter() {
423 if input.len() == 0 {
424 continue;
425 }
426
427 let mut buf: [u8; 128] = [0xff; 128];
428 let mut writer = WriteBuf::new(&mut buf[..input.len()]);
429
430 writer.write_str(input).unwrap();
431 let position = writer.finish_with("?").unwrap_err();
432 assert_eq!(position, last_valid_idx_after_cut + 1);
433 let expected_written = SimpleString::from_segments(&[
434 core::str::from_utf8(&input.as_bytes()[..*last_valid_idx_after_cut]).unwrap(),
435 "?",
436 ]);
437 let actually_wriiten = core::str::from_utf8(&buf[..position]).unwrap();
438 assert_eq!(expected_written.as_str(), actually_wriiten);
439 }
440 }
441
442 #[test]
443 fn finish_with_or_with_longer_normal_closer() {
444 let mut buf: [u8; 4] = [0xff; 4];
445 let writer = WriteBuf::new(&mut buf);
446
447 let written = writer.finish_with_or("0123456789", "abc").unwrap_err();
448 assert_eq!(written, 3);
449 assert_eq!("abc", core::str::from_utf8(&buf[..written]).unwrap());
450 }
451
452 #[test]
453 fn finish_with_full_overwrite_utf8() {
454 let mut buf: [u8; 4] = [0xff; 4];
455 let writer = WriteBuf::new(&mut buf);
456
457 let written = writer.finish_with("π12").unwrap_err();
458 assert_eq!(written, 2);
459 assert_eq!("12", core::str::from_utf8(&buf[..written]).unwrap());
460 }
461
462 #[test]
463 fn set_reserve_should_not_change_written() {
464 let mut buf: [u8; 10] = [0xff; 10];
465 let mut writer = WriteBuf::new(&mut buf);
466
467 write!(writer, "0123456789").unwrap();
468 assert_eq!("0123456789", writer.written());
469
470 writer.set_reserve(4);
471 assert_eq!("0123456789", writer.written());
472
473 writer.finish_with_or("", "!").unwrap();
474 assert_eq!("0123456789", core::str::from_utf8(&buf).unwrap());
475 }
476}
477
478#[cfg(doctest)]
479mod test_readme {
480 macro_rules! external_doc_test {
481 ($x:expr) => {
482 #[doc = $x]
483 extern "C" {}
484 };
485 }
486
487 external_doc_test!(include_str!("../README.md"));
488}