#![doc = include_str!("../README.md")]
#![cfg_attr(not(feature = "std"), no_std)]
#![allow(
clippy::doc_link_with_quotes,
reason = "README.md links use bracketed text containing quoted Unicode names like `\"U+200D\"`; these are real Markdown links, not malformed intra-doc links."
)]
mod truncated;
mod utf8;
use core::fmt;
pub use truncated::{Truncated, TruncatedResultExt};
use utf8::rfind_utf8_end;
pub struct WriteBuf<'a> {
target: &'a mut [u8],
position: usize,
reserve: usize,
truncated: bool,
}
impl<'a> WriteBuf<'a> {
pub fn new(target: &'a mut [u8]) -> Self {
Self {
target,
position: 0,
reserve: 0,
truncated: false,
}
}
pub fn with_reserve(target: &'a mut [u8], reserve: usize) -> Self {
Self {
target,
position: 0,
reserve,
truncated: false,
}
}
#[must_use]
pub fn position(&self) -> usize {
self.position
}
#[inline]
#[must_use]
pub const fn capacity(&self) -> usize {
self.target.len()
}
#[inline]
#[must_use]
pub fn remaining(&self) -> usize {
if self.truncated {
0
} else {
(self.target.len() - self.position).saturating_sub(self.reserve)
}
}
#[must_use]
pub fn truncated(&self) -> bool {
self.truncated
}
#[must_use]
pub fn reserve(&self) -> usize {
self.reserve
}
pub fn set_reserve(&mut self, count: usize) {
self.reserve = count;
}
#[inline]
pub fn clear(&mut self) {
self.position = 0;
self.truncated = false;
}
#[must_use]
pub fn written_bytes(&self) -> &[u8] {
&self.target[..self.position]
}
#[must_use]
pub fn written(&self) -> &str {
unsafe { from_utf8_expect(self.written_bytes()) }
}
pub fn finish(self) -> Result<&'a str, Truncated<'a>> {
self.into_result()
}
fn into_result(self) -> Result<&'a str, Truncated<'a>> {
let written = unsafe { from_utf8_expect(&self.target[..self.position]) };
if self.truncated {
Err(Truncated(written))
} else {
Ok(written)
}
}
#[allow(
clippy::used_underscore_items,
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."
)]
pub fn finish_with(self, suffix: impl AsRef<str>) -> Result<&'a str, Truncated<'a>> {
let suffix = suffix.as_ref();
self._finish_with(suffix, suffix)
}
#[allow(
clippy::used_underscore_items,
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."
)]
pub fn finish_with_or(
self,
normal_suffix: impl AsRef<str>,
truncated_suffix: impl AsRef<str>,
) -> Result<&'a str, Truncated<'a>> {
self._finish_with(normal_suffix.as_ref(), truncated_suffix.as_ref())
}
fn _finish_with(mut self, normal: &str, truncated: &str) -> Result<&'a str, Truncated<'a>> {
let remaining = self.target.len() - self.position();
for (suffix, should_test) in [(normal, !self.truncated), (truncated, true)] {
if !should_test {
continue;
}
if suffix.len() <= remaining {
self.target[self.position..self.position + suffix.len()].copy_from_slice(suffix.as_bytes());
self.position += suffix.len();
return self.into_result();
}
self.truncated = true;
}
let suffix = truncated;
if self.target.len() < suffix.len() {
let suffix_bytes = suffix.as_bytes();
let copyable_suffix = &suffix_bytes[suffix.len() - self.target.len()..];
let Some(valid_utf8_idx) = copyable_suffix
.iter()
.enumerate()
.find(|(_, cu)| utf8::utf8_char_width(**cu).is_some())
.map(|(idx, _)| idx)
else {
self.position = 0;
self.truncated = true;
return self.into_result();
};
let copyable_suffix = ©able_suffix[valid_utf8_idx..];
self.target[..copyable_suffix.len()].copy_from_slice(copyable_suffix);
self.position = copyable_suffix.len();
self.truncated = true;
return self.into_result();
}
let potential_end_idx = self.target.len() - suffix.len();
let write_idx = rfind_utf8_end(&self.target[..potential_end_idx]);
self.target[write_idx..write_idx + suffix.len()].copy_from_slice(suffix.as_bytes());
self.position = write_idx + suffix.len();
self.truncated = true;
self.into_result()
}
fn _write(&mut self, input: &[u8]) -> fmt::Result {
if self.truncated() {
return Err(fmt::Error);
}
let remaining = self.target.len() - self.position();
if remaining < self.reserve() {
self.truncated = true;
return Err(fmt::Error);
}
let remaining = remaining - self.reserve();
let (input, result) = if remaining >= input.len() {
(input, Ok(()))
} else {
let to_write = &input[..remaining];
self.truncated = true;
(&input[..rfind_utf8_end(to_write)], Err(fmt::Error))
};
self.target[self.position..self.position + input.len()].copy_from_slice(input);
self.position += input.len();
result
}
}
impl fmt::Debug for WriteBuf<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WriteBuf")
.field("position", &self.position)
.field("capacity", &self.target.len())
.field("reserve", &self.reserve)
.field("truncated", &self.truncated)
.field("written", &self.written())
.finish()
}
}
impl fmt::Write for WriteBuf<'_> {
#[allow(
clippy::used_underscore_items,
reason = "`_write` is the shared internal helper for `fmt::Write`; the leading underscore distinguishes it from the trait method."
)]
fn write_str(&mut self, s: &str) -> fmt::Result {
self._write(s.as_bytes())
}
}
unsafe fn from_utf8_expect(src: &[u8]) -> &str {
#[cfg(debug_assertions)]
return core::str::from_utf8(src).expect("buffer should have been valid UTF-8");
#[cfg(not(debug_assertions))]
unsafe {
core::str::from_utf8_unchecked(src)
}
}
#[cfg(test)]
mod test;