cadmpeg_core/
printable.rs1mod sealed {
5 pub trait Sealed {}
6 impl Sealed for String {}
7 impl Sealed for &str {}
8}
9
10pub trait ImmutableText: sealed::Sealed + AsRef<str> {}
12impl ImmutableText for String {}
13impl ImmutableText for &str {}
14
15#[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 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 #[must_use]
43 pub fn as_str(&self) -> &str {
44 self.0.as_ref()
45 }
46
47 #[must_use]
49 pub fn into_inner(self) -> S {
50 self.0
51 }
52}
53
54impl<'a> PrintableString<&'a str> {
55 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 #[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}