storage_string/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Defines a fixed-size string format that's used by SCSI and NVMe
5//! specifications.
6
7#![forbid(unsafe_code)]
8
9use inspect::Inspect;
10use mesh_protobuf::Protobuf;
11use std::str::FromStr;
12use thiserror::Error;
13use zerocopy::FromBytes;
14use zerocopy::Immutable;
15use zerocopy::IntoBytes;
16use zerocopy::KnownLayout;
17
18/// A fixed-size string that is padded out with ASCII spaces.
19#[derive(Copy, Clone, Protobuf, IntoBytes, Immutable, KnownLayout, FromBytes)]
20#[repr(transparent)]
21#[mesh(transparent)]
22pub struct AsciiString<const N: usize>([u8; N]);
23
24impl<const N: usize> std::fmt::Debug for AsciiString<N> {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        if let Some(s) = self.as_str() {
27            s.fmt(f)
28        } else {
29            self.as_bytes().fmt(f)
30        }
31    }
32}
33
34impl<const N: usize> Inspect for AsciiString<N> {
35    fn inspect(&self, req: inspect::Request<'_>) {
36        if let Some(s) = self.as_str() {
37            req.value(s)
38        } else {
39            req.value(self.as_bytes().to_vec())
40        }
41    }
42}
43
44/// An error returned by [`AsciiString::new`].
45#[derive(Debug, Error)]
46pub enum InvalidAsciiString {
47    /// The string exceeds the length of the buffer.
48    #[error("string is too long")]
49    TooLong,
50    /// The string contains a character outside of `0x20..=0x7f`.
51    #[error("string contains non-ascii character")]
52    NonAscii,
53}
54
55impl<const N: usize> AsciiString<N> {
56    /// Returns a new string by padding `name` with spaces.
57    ///
58    /// Returns `None` if `name` is longer than `N`.
59    pub fn new(s: &str) -> Result<Self, InvalidAsciiString> {
60        // Pad with spaces.
61        let mut b = [b' '; N];
62        if !s.bytes().all(|c| matches!(c, 0x20..=0x7f)) {
63            return Err(InvalidAsciiString::NonAscii);
64        }
65        b.get_mut(..s.len())
66            .ok_or(InvalidAsciiString::TooLong)?
67            .copy_from_slice(s.as_bytes());
68
69        Ok(Self(b))
70    }
71
72    /// Gets the string, trimming trailing ASCII spaces.
73    ///
74    /// Returns `None` if the string is not valid UTF-8.
75    pub fn as_str(&self) -> Option<&str> {
76        Some(std::str::from_utf8(&self.0).ok()?.trim_end_matches(' '))
77    }
78
79    /// Gets the string as bytes, including the trailing spaces.
80    pub fn as_bytes(&self) -> &[u8; N] {
81        &self.0
82    }
83}
84
85impl<const N: usize> From<[u8; N]> for AsciiString<N> {
86    fn from(value: [u8; N]) -> Self {
87        Self(value)
88    }
89}
90
91impl<const N: usize> From<AsciiString<N>> for [u8; N] {
92    fn from(value: AsciiString<N>) -> Self {
93        value.0
94    }
95}
96
97impl<const N: usize> FromStr for AsciiString<N> {
98    type Err = InvalidAsciiString;
99
100    fn from_str(s: &str) -> Result<Self, Self::Err> {
101        Self::new(s)
102    }
103}