Viewing: cstrbuf.rs
// SPDX-License-Identifier: MIT
// Copyright (c) 2025. DDN. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
//! A buffer type for safe interaction with C string APIs.
//!
//! This module provides [`CStrBuf`], a wrapper around a byte vector designed for
//! passing string buffers to C functions that expect `char*` pointers. It handles
//! the common pattern of allocating a buffer, passing it to C code that writes a
//! null-terminated string into it, and then converting the result back to Rust
//! string types.
//!
//! Based on the approach described at:
//! <https://dean.serenevy.net/blog/2021/Feb/c-string-buffers/>
//!
//! # Example
//!
//! ```
//! use cstrbuf::CStrBuf;
//!
//! // Create a buffer for C functions to write into
//! let mut buf = CStrBuf::new(256);
//!
//! // Pass to C function: some_c_function(buf.as_mut_ptr(), buf.buffer_len());
//!
//! // Convert the result to a Rust string
//! let result = buf.to_string().expect("valid UTF-8");
//! ```
use memchr::memchr;
use std::{
ffi::{CString, NulError, OsStr, c_char},
os::unix::ffi::OsStrExt,
path::Path,
str::FromStr,
};
/// A buffer for C string interoperability.
///
/// `CStrBuf` provides a safe way to allocate buffers that can be passed to C
/// functions expecting `char*` pointers. The buffer tracks its total capacity
/// and can determine the length of any null-terminated string written into it.
pub struct CStrBuf {
vec: Vec<u8>,
}
impl CStrBuf {
/// Creates a new buffer of the specified length, initialized to zeros.
///
/// The buffer will contain `len` bytes, all set to 0.
pub fn new(len: usize) -> Self {
CStrBuf { vec: vec![0; len] }
}
/// Fills the entire buffer with the specified byte value.
///
/// Returns a mutable reference to self for method chaining.
pub fn fill(&mut self, fill: u8) -> &mut Self {
self.vec.fill(fill);
self
}
/// Returns a `const` pointer to the buffer for passing to C functions.
///
/// The pointer is valid for the lifetime of the `CStrBuf`.
pub fn as_ptr(&self) -> *const c_char {
self.vec.as_ptr() as *const c_char
}
/// Returns a mutable pointer to the buffer for passing to C functions.
///
/// The pointer is valid for the lifetime of the `CStrBuf`.
pub fn as_mut_ptr(&mut self) -> *mut c_char {
self.vec.as_mut_ptr() as *mut c_char
}
/// Returns the total capacity of the buffer in bytes.
pub fn buffer_len(&self) -> usize {
self.vec.len()
}
/// Returns the length of the null-terminated string in the buffer.
///
/// Scans for the first null byte and returns its index. If no null byte
/// is found, returns the total buffer length.
pub fn strlen(&self) -> usize {
match memchr(0, &self.vec) {
Some(n) => n,
None => self.vec.len(),
}
}
/// Consumes the buffer and returns its contents as an owned `String`.
///
/// The string is truncated at the first null byte. Returns an error if
/// the contents are not valid UTF-8.
pub fn into_string(mut self) -> Result<String, std::string::FromUtf8Error> {
let len = self.strlen();
self.vec.truncate(len);
String::from_utf8(self.vec)
}
/// Returns the buffer contents as an owned `String`.
///
/// The string is truncated at the first null byte. Returns an error if
/// the contents are not valid UTF-8. Unlike [`into_string`](Self::into_string),
/// this method clones the data.
pub fn to_string(&self) -> Result<String, std::string::FromUtf8Error> {
let len = self.strlen();
String::from_utf8(self.vec[0..len].to_vec())
}
/// Returns the buffer contents as a string slice.
///
/// The slice extends to the first null byte. Returns an error if the
/// contents are not valid UTF-8.
pub fn to_str(&self) -> Result<&str, std::str::Utf8Error> {
let len = self.strlen();
std::str::from_utf8(&self.vec[0..len])
}
/// Returns the buffer contents as an OS string slice.
///
/// The slice extends to the first null byte. On Unix, this is a zero-copy
/// operation that accepts any byte sequence.
pub fn to_os_str(&self) -> &OsStr {
let len = self.strlen();
OsStr::from_bytes(&self.vec[0..len])
}
/// Creates a `CStrBuf` from an OS string.
///
/// Returns an error if the string contains an interior null byte.
pub fn from_os_str(os_str: &OsStr) -> Result<Self, NulError> {
let bytes = os_str.as_bytes();
let cstring = CString::new(bytes.to_vec())?;
let vec = cstring.into_bytes();
Ok(Self { vec })
}
/// Creates a `CStrBuf` from an owned `String`.
///
/// Returns an error if the string contains an interior null byte.
pub fn from_string(s: String) -> Result<CStrBuf, NulError> {
Self::from_str(&s)
}
/// Creates a `CStrBuf` from a filesystem path.
///
/// Returns an error if the path contains an interior null byte.
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<CStrBuf, NulError> {
Self::from_os_str(path.as_ref().as_os_str())
}
}
impl FromStr for CStrBuf {
type Err = NulError;
/// Creates a `CStrBuf` from a string slice.
///
/// The buffer will contain the string bytes followed by a null terminator.
/// Returns an error if the string contains an interior null byte.
fn from_str(s: &str) -> Result<Self, NulError> {
let bytes = s.as_bytes();
let _validation = CString::new(bytes)?;
let mut vec = Vec::with_capacity(bytes.len() + 1);
vec.extend_from_slice(bytes);
vec.push(0); // null terminator
Ok(Self { vec })
}
}
impl AsRef<OsStr> for CStrBuf {
/// Returns the buffer contents as an OS string reference.
fn as_ref(&self) -> &OsStr {
self.to_os_str()
}
}
#[cfg(test)]
mod tests {
use super::CStrBuf;
use std::ffi::OsStr;
#[test]
fn test_cstrbuf_to_os_str() {
let mut cbuf = CStrBuf::new(256);
let s = "hello world";
let c = s.as_bytes();
cbuf.vec[0..c.len()].copy_from_slice(c);
let os_str: &OsStr = cbuf.as_ref();
assert_eq!(OsStr::new("hello world"), os_str);
}
#[test]
fn test_cstrbuf_to_str() {
let mut cbuf = CStrBuf::new(256);
let s = "hello world";
let c = s.as_bytes();
cbuf.vec[0..c.len()].copy_from_slice(c);
assert_eq!(cbuf.strlen(), c.len());
assert_eq!(cbuf.to_str().expect("String should be valid."), s);
}
#[test]
fn test_cstrbuf_to_string() {
let mut cbuf = CStrBuf::new(256);
let s = "hello world";
let c = s.as_bytes();
cbuf.vec[0..c.len()].copy_from_slice(c);
assert_eq!(cbuf.strlen(), c.len());
assert_eq!(cbuf.to_string().expect("String should be valid."), s);
}
#[test]
fn test_cstrbuf_into_string() {
let mut cbuf = CStrBuf::new(256);
let s = "hello world";
let c = s.as_bytes();
cbuf.vec[0..c.len()].copy_from_slice(c);
assert_eq!(cbuf.strlen(), c.len());
assert_eq!(cbuf.into_string().expect("String should be valid"), s);
}
#[test]
fn test_cstrbuf_as_ptr() {
let mut cbuf = CStrBuf::new(256);
let s = "hello world";
let c = s.as_bytes();
cbuf.vec[0..c.len()].copy_from_slice(c);
assert_eq!(cbuf.strlen(), c.len());
let ptr = cbuf.as_ptr();
let s2 = unsafe {
std::ffi::CStr::from_ptr(ptr)
.to_str()
.expect("String should be valid.")
};
assert_eq!(s2, s);
}
#[test]
fn test_cstrbuf_as_mut_ptr() {
let mut cbuf = CStrBuf::new(256);
let s = "hello world";
let c = s.as_bytes();
cbuf.vec[0..c.len()].copy_from_slice(c);
assert_eq!(cbuf.strlen(), c.len());
let ptr = cbuf.as_mut_ptr();
let s2 = unsafe {
std::ffi::CStr::from_ptr(ptr)
.to_str()
.expect("String should be valid.")
};
assert_eq!(s2, s);
}
#[test]
fn test_cstrbuf_buffer_len() {
let cbuf = CStrBuf::new(256);
assert_eq!(cbuf.buffer_len(), 256);
}
#[test]
fn test_cstrbuf_strlen() {
let mut cbuf = CStrBuf::new(256);
let s = "hello world";
let c = s.as_bytes();
cbuf.vec[0..c.len()].copy_from_slice(c);
assert_eq!(cbuf.strlen(), c.len());
}
#[test]
fn test_cstrbuf_new() {
let cbuf = CStrBuf::new(256);
assert_eq!(cbuf.buffer_len(), 256);
}
#[test]
fn test_cstrbuf_new_0() {
let cbuf = CStrBuf::new(0);
assert_eq!(cbuf.buffer_len(), 0);
}
}