autocore-std 3.3.21

Standard library for AutoCore control programs - shared memory, IPC, and logging utilities
Documentation
//! Fixed-length string type for shared memory variables.
//!
//! [`FixedString<N>`] is a `Copy`-able, `repr(transparent)` wrapper around
//! `[u8; N]` that stores UTF-8 text zero-padded to a fixed capacity. It is
//! designed for use in `#[repr(C)]` structs that are shared via memory-mapped
//! regions between processes.
//!
//! # Example
//!
//! ```
//! use autocore_std::FixedString;
//!
//! let mut s = FixedString::<64>::new();
//! assert!(s.is_empty());
//!
//! s.set("Hello, world!");
//! assert_eq!(s.as_str(), "Hello, world!");
//! assert_eq!(s.len(), 13);
//!
//! // Strings longer than N are silently truncated at a UTF-8 boundary
//! let mut tiny = FixedString::<4>::new();
//! tiny.set("Hello");
//! assert_eq!(tiny.as_str(), "Hell");
//! ```

use core::fmt;

/// A fixed-capacity string stored as a zero-padded UTF-8 byte array.
///
/// `N` is the maximum number of bytes (not characters). Strings shorter than
/// `N` are zero-padded. Strings longer than `N` are truncated at the last
/// valid UTF-8 character boundary that fits.
///
/// This type is `Copy`, `#[repr(transparent)]`, and safe to embed in
/// `#[repr(C)]` shared memory structs.
#[derive(Clone, Copy, Eq, Hash)]
#[repr(transparent)]
pub struct FixedString<const N: usize>(pub [u8; N]);

impl<const N: usize> FixedString<N> {
    /// Create an empty (all-zero) fixed string.
    pub const fn new() -> Self {
        Self([0u8; N])
    }

    /// Set the string content, truncating at the last UTF-8 boundary that fits.
    pub fn set(&mut self, s: &str) {
        self.0 = [0u8; N];
        let bytes = s.as_bytes();
        let mut copy_len = bytes.len().min(N);
        // Walk back to a valid UTF-8 boundary if we truncated mid-character
        while copy_len > 0 && !s.is_char_boundary(copy_len) {
            copy_len -= 1;
        }
        self.0[..copy_len].copy_from_slice(&bytes[..copy_len]);
    }

    /// Return the string content as a `&str`.
    ///
    /// Trailing zeros are excluded. If the buffer contains invalid UTF-8
    /// (e.g. from external corruption), returns an empty string.
    pub fn as_str(&self) -> &str {
        let end = self.0.iter().position(|&b| b == 0).unwrap_or(N);
        core::str::from_utf8(&self.0[..end]).unwrap_or("")
    }

    /// Number of bytes in the string (excluding trailing zeros).
    pub fn len(&self) -> usize {
        self.0.iter().position(|&b| b == 0).unwrap_or(N)
    }

    /// Returns `true` if the string is empty.
    pub fn is_empty(&self) -> bool {
        self.0[0] == 0
    }

    /// Clear the string (fill with zeros).
    pub fn clear(&mut self) {
        self.0 = [0u8; N];
    }

    /// The fixed capacity in bytes.
    pub const fn capacity(&self) -> usize {
        N
    }
}

impl<const N: usize> Default for FixedString<N> {
    fn default() -> Self {
        Self::new()
    }
}

impl<const N: usize> PartialEq for FixedString<N> {
    fn eq(&self, other: &Self) -> bool {
        self.0 == other.0
    }
}

impl<const N: usize> fmt::Debug for FixedString<N> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "FixedString<{}>({:?})", N, self.as_str())
    }
}

impl<const N: usize> fmt::Display for FixedString<N> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

// ---------------------------------------------------------------------------
// Serde: serialize as a JSON string, deserialize from a JSON string
// ---------------------------------------------------------------------------

impl<const N: usize> serde::Serialize for FixedString<N> {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(self.as_str())
    }
}

impl<'de, const N: usize> serde::Deserialize<'de> for FixedString<N> {
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        struct Visitor<const M: usize>;

        impl<'de, const M: usize> serde::de::Visitor<'de> for Visitor<M> {
            type Value = FixedString<M>;

            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                write!(f, "a string of at most {} bytes", M)
            }

            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
                let mut fs = FixedString::<M>::new();
                fs.set(v);
                Ok(fs)
            }
        }

        deserializer.deserialize_str(Visitor::<N>)
    }
}

// ---------------------------------------------------------------------------
// From / Into conversions
// ---------------------------------------------------------------------------

impl<const N: usize> From<&str> for FixedString<N> {
    fn from(s: &str) -> Self {
        let mut fs = Self::new();
        fs.set(s);
        fs
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn new_is_empty() {
        let s = FixedString::<64>::new();
        assert!(s.is_empty());
        assert_eq!(s.len(), 0);
        assert_eq!(s.as_str(), "");
    }

    #[test]
    fn set_and_read() {
        let mut s = FixedString::<64>::new();
        s.set("Hello, world!");
        assert_eq!(s.as_str(), "Hello, world!");
        assert_eq!(s.len(), 13);
        assert!(!s.is_empty());
    }

    #[test]
    fn truncation_at_capacity() {
        let mut s = FixedString::<4>::new();
        s.set("Hello");
        assert_eq!(s.as_str(), "Hell");
        assert_eq!(s.len(), 4);
    }

    #[test]
    fn truncation_at_utf8_boundary() {
        // '€' is 3 bytes in UTF-8 (0xE2 0x82 0xAC)
        let mut s = FixedString::<5>::new();
        s.set("ab€x");
        // "ab€" = 2 + 3 = 5 bytes, fits exactly
        assert_eq!(s.as_str(), "ab€");

        let mut s = FixedString::<4>::new();
        s.set("ab€");
        // "ab€" = 5 bytes, doesn't fit. Truncate to "ab" (2 bytes)
        assert_eq!(s.as_str(), "ab");
    }

    #[test]
    fn clear() {
        let mut s = FixedString::<64>::new();
        s.set("test");
        assert!(!s.is_empty());
        s.clear();
        assert!(s.is_empty());
        assert_eq!(s.as_str(), "");
    }

    #[test]
    fn exact_capacity_fill() {
        let mut s = FixedString::<5>::new();
        s.set("abcde");
        assert_eq!(s.as_str(), "abcde");
        assert_eq!(s.len(), 5);
    }

    #[test]
    fn overwrite_shorter() {
        let mut s = FixedString::<64>::new();
        s.set("Hello, world!");
        s.set("Hi");
        assert_eq!(s.as_str(), "Hi");
        assert_eq!(s.len(), 2);
    }

    #[test]
    fn equality() {
        let a = FixedString::<64>::from("test");
        let b = FixedString::<64>::from("test");
        let c = FixedString::<64>::from("other");
        assert_eq!(a, b);
        assert_ne!(a, c);
    }

    #[test]
    fn display() {
        let s = FixedString::<64>::from("display me");
        assert_eq!(format!("{}", s), "display me");
    }

    #[test]
    fn debug() {
        let s = FixedString::<32>::from("debug me");
        let dbg = format!("{:?}", s);
        assert!(dbg.contains("FixedString<32>"));
        assert!(dbg.contains("debug me"));
    }

    #[test]
    fn serde_roundtrip() {
        let original = FixedString::<64>::from("serde test");
        let json = serde_json::to_string(&original).unwrap();
        assert_eq!(json, "\"serde test\"");

        let restored: FixedString<64> = serde_json::from_str(&json).unwrap();
        assert_eq!(original, restored);
    }

    #[test]
    fn serde_truncation() {
        let json = "\"this string is longer than eight bytes\"";
        let s: FixedString<8> = serde_json::from_str(json).unwrap();
        assert_eq!(s.as_str(), "this str");
    }

    #[test]
    fn default_is_empty() {
        let s = FixedString::<64>::default();
        assert!(s.is_empty());
    }

    #[test]
    fn copy_semantics() {
        let a = FixedString::<64>::from("copy me");
        let b = a; // Copy, not move
        assert_eq!(a.as_str(), "copy me");
        assert_eq!(b.as_str(), "copy me");
    }

    #[test]
    fn capacity() {
        let s = FixedString::<128>::new();
        assert_eq!(s.capacity(), 128);
    }
}