Skip to main content

cadmpeg_core/
printable.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Nonempty printable ASCII values shared by parsed and retained records.
3
4mod sealed {
5    pub trait Sealed {}
6    impl Sealed for String {}
7    impl Sealed for &str {}
8}
9
10/// Immutable borrowed or owned text storage for a checked string.
11pub trait ImmutableText: sealed::Sealed + AsRef<str> {}
12impl ImmutableText for String {}
13impl ImmutableText for &str {}
14
15/// A nonempty value of printable ASCII bytes, space included.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct PrintableString<S>(S);
18
19const NOT_PRINTABLE: &str = "value: must be nonempty printable ASCII";
20
21fn printable(bytes: &[u8]) -> bool {
22    !bytes.is_empty()
23        && bytes
24            .iter()
25            .all(|byte| byte.is_ascii_graphic() || *byte == b' ')
26}
27
28impl<S: ImmutableText> PrintableString<S> {
29    /// Admit text that is nonempty printable ASCII.
30    ///
31    /// # Errors
32    ///
33    /// Returns a static message when the text is empty or holds another byte.
34    pub fn new(value: S) -> Result<Self, &'static str> {
35        if !printable(value.as_ref().as_bytes()) {
36            return Err(NOT_PRINTABLE);
37        }
38        Ok(Self(value))
39    }
40
41    /// The checked text.
42    #[must_use]
43    pub fn as_str(&self) -> &str {
44        self.0.as_ref()
45    }
46
47    /// The checked storage.
48    #[must_use]
49    pub fn into_inner(self) -> S {
50        self.0
51    }
52}
53
54impl<'a> PrintableString<&'a str> {
55    /// Admit raw bytes. The check stops at the first byte that is not
56    /// printable ASCII, so it never reads past the end of a printable run.
57    ///
58    /// # Errors
59    ///
60    /// Returns a static message when the bytes are empty or hold another byte.
61    pub fn from_bytes(bytes: &'a [u8]) -> Result<Self, &'static str> {
62        if !printable(bytes) {
63            return Err(NOT_PRINTABLE);
64        }
65        std::str::from_utf8(bytes)
66            .map(Self)
67            .map_err(|_| NOT_PRINTABLE)
68    }
69
70    /// Copy the checked text into owned storage.
71    #[must_use]
72    pub fn into_owned(self) -> PrintableString<String> {
73        PrintableString(self.0.to_owned())
74    }
75}
76
77impl<S: ImmutableText> serde::Serialize for PrintableString<S> {
78    fn serialize<T: serde::Serializer>(&self, serializer: T) -> Result<T::Ok, T::Error> {
79        serializer.serialize_str(self.as_str())
80    }
81}
82
83impl<'de> serde::Deserialize<'de> for PrintableString<String> {
84    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
85        let value = <String as serde::Deserialize>::deserialize(deserializer)?;
86        Self::new(value).map_err(serde::de::Error::custom)
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    #![allow(clippy::unwrap_used)]
93
94    use super::PrintableString;
95
96    #[test]
97    fn printable_value_preserves_wire_and_owned_transfer() {
98        let text = " ~Name42";
99        let value = PrintableString::new(text).unwrap().into_owned();
100        let json = serde_json::to_string(&value).unwrap();
101        assert_eq!(json, serde_json::to_string(text).unwrap());
102        assert_eq!(
103            serde_json::from_str::<PrintableString<String>>(&json).unwrap(),
104            value
105        );
106    }
107
108    #[test]
109    fn printable_value_rejects_empty_control_and_non_ascii() {
110        for text in ["", "\0", "\n", "\t", "\u{7f}", "μ"] {
111            assert!(PrintableString::new(text).is_err());
112            let json = serde_json::to_string(text).unwrap();
113            let error = serde_json::from_str::<PrintableString<String>>(&json).unwrap_err();
114            assert!(error.to_string().contains("value"));
115        }
116    }
117
118    #[test]
119    fn byte_admission_matches_decoding_then_admitting_text() {
120        let alphabet = [
121            0x00, 0x09, 0x1f, b' ', b'A', b'~', 0x7f, 0x80, 0xc2, 0xb5, 0xff,
122        ];
123        let mut inputs = vec![Vec::new()];
124        let mut frontier = vec![Vec::new()];
125        for _ in 0..3 {
126            frontier = frontier
127                .iter()
128                .flat_map(|prefix: &Vec<u8>| {
129                    alphabet
130                        .iter()
131                        .map(move |byte| [prefix.as_slice(), &[*byte]].concat())
132                })
133                .collect();
134            inputs.extend(frontier.iter().cloned());
135        }
136        for input in &inputs {
137            let decoded = std::str::from_utf8(input)
138                .ok()
139                .and_then(|text| PrintableString::new(text).ok());
140            assert_eq!(PrintableString::from_bytes(input).ok(), decoded);
141        }
142    }
143    #[test]
144    fn printablestring_serializes_the_checked_borrowed_and_owned_text() {
145        let text = "Name";
146        let borrowed = super::PrintableString::new(text).unwrap();
147        let owned = super::PrintableString::new(text.to_owned()).unwrap();
148        for _ in 0..3 {
149            assert_eq!(borrowed.as_str(), text);
150            assert_eq!(owned.as_str(), text);
151            assert_eq!(
152                serde_json::to_string(&borrowed).unwrap(),
153                serde_json::to_string(text).unwrap()
154            );
155            assert_eq!(
156                serde_json::to_string(&owned).unwrap(),
157                serde_json::to_string(text).unwrap()
158            );
159        }
160    }
161}