fmtbuf 0.1.0

Utilities for formatting to a fixed-size buffer
Documentation

fmtbuf

Format into a fixed buffer.

Usage

use fmtbuf::WriteBuf;
use std::fmt::Write;

fn main() {
    let mut buf: [u8; 10] = [0; 10];
    let mut writer = WriteBuf::new(&mut buf);
    if let Err(e) = write!(&mut writer, "πŸš€πŸš€πŸš€") {
        println!("write error: {e:?}");
    }
    let written_len = match writer.finish() {
        Ok(len) => len, // <- won't be hit since πŸš€πŸš€πŸš€ is 12 bytes
        Err(len) => {
            println!("writing was truncated");
            len
        }
    };
    let written = &buf[..written_len];
    println!("wrote {written_len} bytes: {written:?}");
    println!("result: {:?}", std::str::from_utf8(written));
}

πŸš€πŸš€

The primary use case is for implementing APIs like strerror_r, where the user provides the buffer.

use std::{ffi, fmt::Write, io::Error};
use fmtbuf::WriteBuf;

#[no_mangle]
pub extern "C" fn mylib_strerror(err: *mut Error, buf: *mut ffi::c_char, buf_len: usize) {
    let mut buf = unsafe {
        // Buffer provided by a users
        let mut buf = std::slice::from_raw_parts_mut(buf as *mut u8, buf_len);
    };
    let mut writer = WriteBuf::new(buf);

    // Use the standard `write!` macro (no error handling for brevity)
    write!(writer, "{}", err.as_ref().unwrap()).unwrap();

    let _ =
        if writer.truncated() {
            // the message was truncated, let the caller know by adding "..."
            writer.finish_with(b"...\0")
        } else {
            // just null-terminate the buffer
            writer.finish_with(b"\0")
        };
}

Why not write to &mut [u8]?

The Rust Standard Library trait std::io::Write is implemented for &mut [u8] which could be used instead of this library. The problem with this approach is the lack of UTF-8 encoding support (also, it is not available in #![no_std]).

use std::io::{Cursor, Write};

fn main() {
    let mut buf: [u8; 10] = [0; 10];
    let mut writer = Cursor::<&mut [u8]>::new(&mut buf);
    if let Err(e) = write!(&mut writer, "rocket: πŸš€") {
        println!("write error: {e:?}");
    }
    let written_len = writer.position() as usize;
    let written = &buf[..written_len];
    println!("wrote {written_len} bytes: {written:?}");
    println!("result: {:?}", std::str::from_utf8(written));
}

Running this program will show you the error:

write error: Error { kind: WriteZero, message: "failed to write whole buffer" }
wrote 10 bytes: [114, 111, 99, 107, 101, 116, 58, 32, 240, 159]
result: Err(Utf8Error { valid_up_to: 8, error_len: None })

The problem is that "rocket: πŸš€" is encoded as the 12 byte sequence -- the πŸš€ emoji is encoded in UTF-8 as the 4 bytes b"\xf0\x9f\x9a\x80" -- but our target buffer is only 10 bytes long. The write! to the cursor naΓ―vely cuts off the πŸš€ mid-encode, making the encoded string invalid UTF-8, even though it advanced the cursor the entire 10 bytes. This is expected, since std::io::Write comes from io and does not know anything about string encoding; it operates on the u8 level.

One could use the std::str::Utf8Error to properly cut off the buf. The only issue with this is performance. Since std::str::from_utf8 scans the whole string moving forward, it costs O(n) to test this, whereas fmtbuf will do this in O(1), since it only looks at the final few bytes.