Crates.io | fmtbuf |
lib.rs | fmtbuf |
version | 0.1.2 |
source | src |
created_at | 2023-09-03 03:53:39.361294 |
updated_at | 2023-09-10 06:06:07.285267 |
description | Utilities for formatting to a fixed-size buffer |
homepage | |
repository | https://github.com/tgockel/fmtbuf |
max_upload_size | |
id | 962071 |
size | 60,885 |
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.
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_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));
}
ππ
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 unsafe 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
std::slice::from_raw_parts_mut(buf as *mut u8, buf_len)
};
// Reserve at least 1 byte at the end because we will always
// write '\0'
let mut writer = WriteBuf::with_reserve(buf, 1);
// 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
// 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);
}
!#[no_std]
Support for !#[no_std]
is enabled by disabling the default features and not re-enabling the "std"
feature.
fmtbuf = { version = "*", default_features = false }
&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 a 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.
This library only guarantees that the contents of the target buffer is valid UTF-8.
It does not make any guarantees of semantics resulting from truncation due to the Unicode format characters,
specifically U+200D
, U+200E
, and U+200F
.
What?
If you don't know what those are, that's okay. Suffice it to say that human language is complicated and Unicode has a set a features to make things possible, but when you run out of space to store that in your fixed-size buffer, things go awry. If you're looking for details, see the mini sections below.
U+200D
: Zero Width JoinerCertain graphemes like "πββ" (which you might see as two separate graphemes) are comprised of three code points:
So the single grapheme is the 10 byte sequence b"\xf0\x9f\x99\x87\xe2\x80\x8d\xe2\x99\x80"
.
The question arises: What should happen if the buffer size is only 9?
On truncation, this library will discard code points which are meant to be modifiers.
This library will truncate the last Unicode code point, leaving you with b"\xf0\x9f\x99\x87\xe2\x80\x8d"
--a person
bowing and a zero-width joiner joining with nothing, as the female modifier can not fit.
U+200E
and U+200F
: Direction MarkersConsider Arabic, which is a right-to-left language:
βΨ’Ω Ω Ψ£Ω ΩΨΩ βRustβ Ω ΨΩ βC++β ΩΩΩ ΩΨ§ Ω Ψ§.β
Depending on how compliant with right-to-left presentation your text editor or browser is, you might see that text any
number of ways (if "Ψ’Ω
Ω" on the right-hand side of the text, then the presentation is working).
But note the borrowed words "Rust" and "C++" are still spelled in a left-to-right manner within the right-to-left text
(or they should be).
This is done by encoding U+200E
left-to-right mark, then writing the borrowed
text, then U+200F
right-to-left mark to continue.
What happens if text is reversed, but there is not enough space in the buffer to flip it back? On truncation, this library might leave you in the middle of a text-reversed run.
The construction of Egyptian Hieroglyphs and other languages of this sort 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.