tacacs_plus_protocol/
text.rs

1//! Convenience type for enforcing valid ASCII printable strings.
2
3use core::fmt;
4
5mod inner;
6use inner::FieldTextInner;
7
8#[cfg(test)]
9mod tests;
10
11/// A wrapper for `&str` that is checked to be printable ASCII, which is
12/// defined as not containing control characters in [RFC8907 section 3.7].
13///
14/// This type implements `TryFrom<&str>` and `TryFrom<&[u8]>`; in both cases,
15/// an invalid argument will be returned as an `Err` variant.
16///
17/// # Examples
18///
19/// Conversions from `&str`:
20///
21/// ```
22/// use tacacs_plus_protocol::FieldText;
23///
24/// let valid_ascii = "a string";
25/// assert!(FieldText::try_from(valid_ascii).is_ok());
26///
27/// let beyond_ascii = "💀";
28/// assert!(FieldText::try_from(beyond_ascii).is_err());
29/// ```
30///
31/// Conversions from `&[u8]`:
32///
33/// ```
34/// # use tacacs_plus_protocol::FieldText;
35///
36/// let valid_slice = b"this is (almost) a string";
37/// assert!(FieldText::try_from(valid_slice.as_slice()).is_ok());
38///
39/// let not_printable = b"all ASCII characters with - oh no! - a\ttab";
40/// assert!(FieldText::try_from(not_printable.as_slice()).is_err());
41///
42/// let invalid_utf8 = [0x80]; // where'd the rest of the codepoint go?
43/// assert!(FieldText::try_from(invalid_utf8.as_slice()).is_err());
44/// ```
45///
46/// If the `std` feature is enabled, the `FieldText::from_string_lossy()` constructor
47/// is also available in case a `.try_into().unwrap()` is undesirable:
48///
49/// ```
50/// # use tacacs_plus_protocol::FieldText;
51/// # #[cfg(feature = "std")] {
52/// let already_valid = "all ASCII!";
53/// let valid_text = FieldText::from_string_lossy(String::from(already_valid));
54/// assert_eq!(valid_text, already_valid);
55///
56/// let unicode_fun = "\tsome chars and ✨emojis✨ (and a quote: ')";
57/// let escaped_text = FieldText::from_string_lossy(String::from(unicode_fun));
58/// assert_eq!(escaped_text, "\\tsome chars and \\u{2728}emojis\\u{2728} (and a quote: ')");
59///
60/// // now that escaped_text is valid ASCII, a .try_into().unwrap() should be guaranteed
61/// // not to panic with the escaped string
62/// let _: FieldText<'_> = escaped_text.as_ref().try_into().unwrap();
63/// # }
64/// ```
65///
66/// [RFC8907 section 3.7]: https://www.rfc-editor.org/rfc/rfc8907.html#section-3.7
67#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
68pub struct FieldText<'string>(FieldTextInner<'string>);
69
70impl FieldText<'_> {
71    /// Creates a [`FieldText`] from a `String`, escaping any non-printable-ASCII
72    /// characters as necessary.
73    #[cfg(feature = "std")]
74    pub fn from_string_lossy(string: std::string::String) -> FieldText<'static> {
75        use std::string::String;
76
77        // we don't just use String::escape_default() + ToString since that also escapes quotes,
78        // which we don't want since they're already valid ASCII
79        let escaped = string
80            .chars()
81            .fold(String::with_capacity(string.len()), |mut result, c| {
82                if char_is_printable_ascii(c) {
83                    result.push(c);
84                } else {
85                    result.extend(c.escape_default());
86                }
87
88                result
89            });
90
91        FieldText(FieldTextInner::Owned(escaped))
92    }
93
94    /// Converts this [`FieldText`] to one that owns its underlying data,
95    /// extending its lifetime to `'static`.
96    #[cfg(feature = "std")]
97    pub fn into_owned(self) -> FieldText<'static> {
98        FieldText(self.0.into_owned())
99    }
100}
101
102impl<'string> FieldText<'string> {
103    /// Gets the length of the underlying `&str`.
104    pub fn len(&self) -> usize {
105        self.0.len()
106    }
107
108    /// Gets the byte slice representation of the underlying `&str`.
109    pub fn as_bytes(&self) -> &[u8] {
110        self.0.as_bytes()
111    }
112
113    /// Returns true if the underlying `&str` is empty.
114    pub fn is_empty(&self) -> bool {
115        self.0.is_empty()
116    }
117
118    /// Returns `true` if the underlying `&str` contains any of the provided characters, or false otherwise.
119    pub fn contains_any(&self, characters: &[char]) -> bool {
120        self.0.contains(characters)
121    }
122
123    /// Asserts a string is ASCII, converting it to an [`FieldText`] or panicking if it is not actually ASCII.
124    #[cfg(test)]
125    pub(crate) fn assert(string: &str) -> FieldText<'_> {
126        if is_printable_ascii(string) {
127            FieldText(FieldTextInner::Borrowed(string))
128        } else {
129            panic!("non-ASCII string passed to `FieldText::assert()`");
130        }
131    }
132}
133
134fn is_printable_ascii(string: &str) -> bool {
135    // all characters must be ASCII printable (i.e., not control characers)
136    string.chars().all(char_is_printable_ascii)
137}
138
139fn char_is_printable_ascii(c: char) -> bool {
140    c.is_ascii() && !c.is_ascii_control()
141}
142
143impl AsRef<str> for FieldText<'_> {
144    fn as_ref(&self) -> &str {
145        &self.0
146    }
147}
148
149/// The error type returned by the [`TryFrom`] implementations for [`FieldText`].
150#[derive(Debug, Clone, PartialEq, Eq, Hash)]
151#[repr(transparent)]
152pub struct InvalidText<T>(T);
153
154impl<T> InvalidText<T> {
155    /// Returns a reference to the contained invalid argument.
156    pub fn inner(&self) -> &T {
157        &self.0
158    }
159
160    /// Consumes this [`InvalidText`] object and takes ownership of the inner value.
161    pub fn into_inner(self) -> T {
162        self.0
163    }
164}
165
166impl fmt::Display for InvalidText<&str> {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        write!(f, "string was not printable ASCII: {}", self.0)
169    }
170}
171
172impl fmt::Display for InvalidText<&[u8]> {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        write!(f, "bytes were not printable ASCII: {:?}", self.0)
175    }
176}
177
178#[cfg(feature = "std")]
179impl fmt::Display for InvalidText<std::string::String> {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        write!(f, "string was not printable ASCII: {}", self.0)
182    }
183}
184
185impl<'string> TryFrom<&'string str> for FieldText<'string> {
186    type Error = InvalidText<&'string str>;
187
188    fn try_from(value: &'string str) -> Result<Self, Self::Error> {
189        if is_printable_ascii(value) {
190            Ok(Self(FieldTextInner::Borrowed(value)))
191        } else {
192            Err(InvalidText(value))
193        }
194    }
195}
196
197// std-gated since we can't keep a reference to the &str internally without a lifetime parameter on FromStr
198#[cfg(feature = "std")]
199impl std::str::FromStr for FieldText<'static> {
200    type Err = InvalidText<std::string::String>;
201
202    fn from_str(s: &str) -> Result<Self, Self::Err> {
203        use std::borrow::ToOwned;
204        s.to_owned().try_into()
205    }
206}
207
208impl<'bytes> TryFrom<&'bytes [u8]> for FieldText<'bytes> {
209    type Error = InvalidText<&'bytes [u8]>;
210
211    fn try_from(value: &'bytes [u8]) -> Result<Self, Self::Error> {
212        if let Ok(value_str) = core::str::from_utf8(value) {
213            // defer to TryFrom<&str> impl for ASCII check consistency
214            value_str.try_into().map_err(|_| InvalidText(value))
215        } else {
216            Err(InvalidText(value))
217        }
218    }
219}
220
221#[cfg(feature = "std")]
222impl TryFrom<std::string::String> for FieldText<'_> {
223    type Error = InvalidText<std::string::String>;
224
225    fn try_from(value: std::string::String) -> Result<Self, Self::Error> {
226        if is_printable_ascii(&value) {
227            Ok(Self(FieldTextInner::Owned(value)))
228        } else {
229            Err(InvalidText(value))
230        }
231    }
232}
233
234impl PartialEq<&str> for FieldText<'_> {
235    fn eq(&self, other: &&str) -> bool {
236        self.0 == *other
237    }
238}
239
240impl PartialEq<FieldText<'_>> for &str {
241    fn eq(&self, other: &FieldText<'_>) -> bool {
242        *self == other.0
243    }
244}
245
246impl fmt::Display for FieldText<'_> {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        <_ as fmt::Display>::fmt(&self.0, f)
249    }
250}