Skip to content
Merged
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
Changelog
=========

All notable changes to this project will be documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0]

### Added

- `Truncated<'_>` error type, returned by the `WriteBuf::finish*` family. Carries the portion of the output that was
successfully written so callers don't have to track it separately.
- `WriteBuf::written()` returns the written portion as a `&str` (companion to the existing `written_bytes()` accessor).

### Changed

- **Breaking:** `WriteBuf::finish`, `WriteBuf::finish_with`, and `WriteBuf::finish_with_or` now return
`Result<&str, Truncated<'_>>` instead of `Result<usize, usize>`. The new shape gives callers the validated
string slice directly without a separate `from_utf8` step on the underlying buffer.
- **Breaking:** `finish_with` and `finish_with_or` now require `impl AsRef<str>` for the suffix arguments
(previously `impl AsRef<[u8]>`). This keeps the UTF-8 invariant at the type level rather than relying on a
runtime check.
- **Breaking:** Minimum supported Rust version raised to 1.81 (for stable `core::error::Error`); edition is now 2021.

### Removed

- **Breaking:** `rfind_utf8_end` is no longer re-exported from the crate root. It remains as a crate-internal helper.

### Migration from 0.1.x

- Replace `let n = writer.finish().unwrap();` with `let s = writer.finish().unwrap();` -- the success value is now
`&str` rather than the byte count.
- For the error path, use `e.get()` to access the partially-written `&str`:
`writer.finish().unwrap_or_else(|e| e.get())`.
- If a `finish_with*` call passed a non-`str` buffer, convert via `core::str::from_utf8` first.

## [0.1.2] — 2025-09-24

- Documentation additions describing the lack of semantic Unicode support on truncation.

## [0.1.1]

- Added `WriteBuf` accessors to query and alter the reserve byte count.

## [0.1.0]

- Initial release.
10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[package]
name = "fmtbuf"
version = "0.1.2"
edition = "2018"
rust-version = "1.31"
version = "0.2.0"
edition = "2021"
rust-version = "1.81"
authors = ["Travis Gockel <travis@gockelhut.com>"]
license = "Apache-2.0"
description = "Utilities for formatting to a fixed-size buffer"
keywords = ["string", "format"]
keywords = ["buffer", "truncation", "utf-8", "ffi", "no-std"]
categories = ["development-tools::ffi", "no-std"]
readme = "README.md"
repository = "https://github.com/tgockel/fmtbuf"
Expand All @@ -21,4 +21,4 @@ std = []
clap = { version = "4.0.0", features = ["derive"] }

[badges]
maintenance = { status = "actively-developed" }
maintenance = { status = "experimental" } # this line is currently experimental
72 changes: 43 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,44 @@ Write a formatted string into a fixed buffer.
This is useful when you have a user-provided buffer you want to write into, which frequently arises when writing foreign
function interfaces for C, where strings are expected to have a null terminator.

Usage
-----

```rust
use fmtbuf::WriteBuf;
use std::fmt::Write;
use core::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_with("\0") {
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));
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 = match writer.finish_with_or("!", "…") {
Ok(s) => s, // <- won't be hit since 🚀🚀🚀 is 12 bytes
Err(e) => {
println!("writing was truncated");
e.get()
}
};
assert_eq!("🚀…", written);
```

🚀🚀
A few things happened in that example:

1. We started with a 10 byte buffer
2. Tried to write `"🚀🚀🚀"` to it, which is encoded as 3 `b"\xf0\x9f\x9a\x80"`s (12 bytes)
3. This can't fit into 10 bytes, so only `"🚀🚀"` is stored and the `writer` is noted as having truncated writes
4. We finish the buffer with `"!"` on success or `"…"` (a.k.a. `b"\xe2\x80\xa6"`) on truncation
5. Since we noted truncation in step #3, we try to write `"…"`, but this can not fit into the buffer either, since
8 (`"🚀🚀".len()`) + 3 (`"…".len()`) > 10 (`buf.len()`)
6. Roll the buffer back to the end of the first 🚀, then add …, leaving us with `"🚀…"`

Usage
-----

The primary use case is for implementing APIs like [`strerror_r`](https://linux.die.net/man/3/strerror_r), where the
user provides the buffer.

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

#[no_mangle]
pub unsafe extern "C" fn mylib_strerror(
Expand All @@ -47,7 +51,7 @@ pub unsafe extern "C" fn mylib_strerror(
buf_len: usize
) {
let mut buf = unsafe {
// Buffer provided by a users
// Buffer provided by user
std::slice::from_raw_parts_mut(buf as *mut u8, buf_len)
};
// Reserve at least 1 byte at the end because we will always
Expand All @@ -56,14 +60,17 @@ pub unsafe extern "C" fn mylib_strerror(

// Use the standard `write!` macro (no error handling for
// brevity) -- note that an error here might only indicate
// write truncation, which is handled gracefully be this
// write truncation, which is handled gracefully by this
// library's finish___ functions
let _ = write!(writer, "{}", err.as_ref().unwrap());

// null-terminate buffer or add "..." if it was truncated
let _written_len = writer.finish_with_or(b"\0", b"...\0")
// Err value is also number of bytes written
.unwrap_or_else(|e| e);
// null-terminate buffer or add "..." if it was truncated.
// `TruncatedResultExt` extracts the written `&str` (and its
// byte length) regardless of whether truncation occurred --
// useful for setting an FFI out-param.
let result = writer.finish_with_or("\0", "...\0");
let _written = result.written();
let _written_len = result.written_len();
}
```

Expand All @@ -75,7 +82,7 @@ Features
Support for `!#[no_std]` is enabled by disabling the default features and not re-enabling the `"std"` feature.

```toml
fmtbuf = { version = "*", default_features = false }
fmtbuf = { version = "*", default-features = false }
```

F.A.Q.
Expand Down Expand Up @@ -173,3 +180,10 @@ face a similar issue.
Where should the cutoff be?
This library does not know the difference between "𓁪𓌍𓃻" and "𓁪𓌍".
Figuring that out is the responsibility of a higher-level construct.

#### �

This library implements [`core::fmt::Write`](https://doc.rust-lang.org/stable/core/fmt/trait.Write.html), which only
accepts UTF-8-encoded data.
There is no place for � in this library.
However, the result of a truncated run might be replaced by � for presentation at a higher level.
21 changes: 6 additions & 15 deletions examples/writebuf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,26 +62,17 @@ fn main() {
(None, Some(truncate)) => writer.finish_with_or("", truncate),
(Some(finish), Some(truncate)) => writer.finish_with_or(finish, truncate),
};
let (written_len, truncated) = match result {
Ok(len) => (len, false),
Err(len) => (len, true),
let (contents, truncated) = match result {
Ok(s) => (s, false),
Err(e) => (e.get(), true),
};
println!("{contents}");

let contents = match std::str::from_utf8(&buf[..written_len]) {
Ok(contents) => {
println!("{contents}");
contents.as_bytes()
},
Err(e) => {
println!("! error: {e:?}");
&buf[..written_len]
},
};
if !cli.quiet {
println!("+ version: {}", env!("CARGO_PKG_VERSION"));
println!("+ written_len: {written_len}");
println!("+ written_len: {}", contents.len());
println!("+ truncated: {truncated}");
println!("+ output_bytes: {contents:?}");
println!("+ output_bytes: {:?}", contents.as_bytes());
println!("+ input: {}", cli.input);
println!("+ input_bytes: {:?}", cli.input.as_bytes());
}
Expand Down
Loading
Loading