string_types/
hex_str.rs

1//! Defines strings of either ASCII hex strings. There are three different `HexString` forms
2//! supported:
3//!
4//! - [`HexString`] may only contain the characters `[0-9a-fA-F]`
5//! - [`LowerHexString`] may only contain the characters `[0-9a-f]`
6//! - [`UpperHexString`] may only contain the characters `[0-9A-F]`
7//!
8//! In addition, all three `HexString` variants must be an even number of characters in length.
9use std::convert::TryFrom;
10
11use crate::{
12    EnsureValid,
13    ParseError,
14    TrimmedNonBlankString,
15    NonEmptyString,
16    StringType,
17    NonBlankString,
18    Display,
19    FromStr,
20    string_type
21};
22
23/// String guaranteed to have at least two characters, and all characters are hexadecimal digits
24#[derive(Display, FromStr)]
25#[cfg_attr(feature="serde", derive(serde::Serialize, serde::Deserialize))]
26#[string_type]
27pub struct HexString(TrimmedNonBlankString);
28
29impl HexString {
30    /// Convert to a `LowerHexString`
31    pub fn to_lower(self) -> LowerHexString {
32        let mut s: String = self.0.into();
33        s.make_ascii_lowercase();
34        s.parse().expect("Started as HexString")
35    }
36
37    /// Convert to an `UpperHexString`
38    pub fn to_upper(self) -> UpperHexString {
39        let mut s: String = self.0.into();
40        s.make_ascii_uppercase();
41        s.parse().expect("Started as HexString")
42    }
43}
44
45impl EnsureValid for HexString {
46    type ParseErr = ParseError;
47
48    /// Validate string slice
49    ///
50    /// # Errors
51    ///
52    /// - [`ParseError::EmptyString`] if the string is empty
53    /// - [`ParseError::NonHexDigit`] if a character in the string is a non-hexdigit
54    fn ensure_valid(s: &str) -> Result<(), Self::ParseErr> {
55        if s.is_empty() { return Err(ParseError::EmptyString); }
56
57        if let Some((i, c)) = s.chars().enumerate()
58            .find(|(_i, c)| !c.is_ascii_hexdigit()) {
59            return Err(ParseError::NonHexDigit(i, c));
60        }
61        if (s.chars().count() % 2) == 1 {
62            return Err(ParseError::OddLength);
63        }
64        Ok(())
65    }
66}
67
68/// Any `HexString` is automatically non-blank
69impl From<HexString> for NonBlankString {
70    fn from(value: HexString) -> Self { value.to_inner().into() }
71}
72
73/// Any `HexString` is automatically not empty
74impl From<HexString> for NonEmptyString {
75    fn from(value: HexString) -> Self { value.to_inner().into() }
76}
77
78impl TryFrom<NonEmptyString> for HexString {
79    type Error = <Self as EnsureValid>::ParseErr;
80
81    fn try_from(value: NonEmptyString) -> Result<Self, Self::Error> {
82        value.as_str().parse()
83    }
84}
85
86impl TryFrom<TrimmedNonBlankString> for HexString {
87    type Error = <Self as EnsureValid>::ParseErr;
88
89    fn try_from(value: TrimmedNonBlankString) -> Result<Self, Self::Error> {
90        value.as_str().parse()
91    }
92}
93
94/// String guaranteed to have at least two characters, and all characters are lowercase hexadecimal digits
95#[derive(Display, FromStr)]
96#[cfg_attr(feature="serde", derive(serde::Serialize, serde::Deserialize))]
97#[string_type]
98pub struct LowerHexString(HexString);
99
100impl EnsureValid for LowerHexString {
101    type ParseErr = ParseError;
102
103    /// Validate string slice
104    ///
105    /// # Errors
106    ///
107    /// - [`ParseError::EmptyString`] if the string is empty
108    /// - [`ParseError::NonHexDigit`] if a character in the string is not a lowercase hexdigit
109    fn ensure_valid(s: &str) -> Result<(), Self::ParseErr> {
110        if s.is_empty() { return Err(ParseError::EmptyString); }
111
112        if let Some((i, c)) =  s.chars().enumerate()
113            .find(|(_i, c)| !c.is_ascii_digit() && !c.is_ascii_lowercase()) {
114            return Err(ParseError::NonHexDigit(i, c));
115        }
116        if (s.chars().count() % 2) == 1 {
117            return Err(ParseError::OddLength);
118        }
119        Ok(())
120    }
121}
122
123/// Any `LowerHexString` is automatically not empty
124impl From<LowerHexString> for NonEmptyString {
125    fn from(value: LowerHexString) -> Self { value.to_inner().into() }
126}
127
128/// Any `LowerHexString` is automatically non-blank
129impl From<LowerHexString> for NonBlankString {
130    fn from(value: LowerHexString) -> Self { value.to_inner().into() }
131}
132
133impl TryFrom<NonEmptyString> for LowerHexString {
134    type Error = <Self as EnsureValid>::ParseErr;
135
136    fn try_from(value: NonEmptyString) -> Result<Self, Self::Error> {
137        value.as_str().parse()
138    }
139}
140
141impl TryFrom<TrimmedNonBlankString> for LowerHexString {
142    type Error = <Self as EnsureValid>::ParseErr;
143
144    fn try_from(value: TrimmedNonBlankString) -> Result<Self, Self::Error> {
145        value.as_str().parse()
146    }
147}
148
149/// String guaranteed to have at least two characters, and all characters are uppercase hexadecimal digits
150#[derive(Display, FromStr)]
151#[cfg_attr(feature="serde", derive(serde::Serialize, serde::Deserialize))]
152#[string_type]
153pub struct UpperHexString(HexString);
154
155impl EnsureValid for UpperHexString {
156    type ParseErr = ParseError;
157
158    /// Validate string slice
159    ///
160    /// # Errors
161    ///
162    /// - [`ParseError::EmptyString`] if the string is empty
163    /// - [`ParseError::NonHexDigit`] if a character in the string is not an uppercase hexdigit
164    fn ensure_valid(s: &str) -> Result<(), Self::ParseErr> {
165        if s.is_empty() { return Err(ParseError::EmptyString); }
166
167        if let Some((i, c)) = s.chars().enumerate()
168            .find(|(_i, c)| !c.is_ascii_digit() && !c.is_ascii_uppercase()) {
169            return Err(ParseError::NonHexDigit(i, c));
170        }
171        if (s.chars().count() % 2) == 1 {
172            return Err(ParseError::OddLength);
173        }
174        Ok(())
175    }
176}
177
178/// Any `UpperHexString` is automatically not empty
179impl From<UpperHexString> for NonEmptyString {
180    fn from(value: UpperHexString) -> Self { value.to_inner().into() }
181}
182
183/// Any `UpperHexString` is automatically non-blank
184impl From<UpperHexString> for NonBlankString {
185    fn from(value: UpperHexString) -> Self { value.to_inner().into() }
186}
187
188impl TryFrom<NonEmptyString> for UpperHexString {
189    type Error = <Self as EnsureValid>::ParseErr;
190
191    fn try_from(value: NonEmptyString) -> Result<Self, Self::Error> {
192        value.as_str().parse()
193    }
194}
195
196impl TryFrom<TrimmedNonBlankString> for UpperHexString {
197    type Error = <Self as EnsureValid>::ParseErr;
198
199    fn try_from(value: TrimmedNonBlankString) -> Result<Self, Self::Error> {
200        value.as_str().parse()
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::{
207        TrimmedNonBlankString,
208        HexString,
209        LowerHexString,
210        StringType,
211        UpperHexString
212    };
213    use crate::ParseError;
214
215    use assert2::{assert, let_assert};
216    use rstest::rstest;
217
218    #[rstest]
219    #[case("",        ParseError::EmptyString,         "empty string")]
220    #[case("ertyui",  ParseError::NonHexDigit(1, 'r'), "bad character")]
221    #[case("deadbef", ParseError::OddLength,           "odd length")]
222    fn hexstring_parse_unsuccess(#[case]val: &str, #[case]err: ParseError, #[case]msg: &str) {
223        let_assert!(Err(e) = val.parse::<HexString>());
224        assert!(e == err, "parse error ({})", msg);
225    }
226
227    #[rstest]
228    #[case("deadbeef",   "deadbeef", "lowercase hex string")]
229    #[case("DEADBEEF",   "DEADBEEF", "uppercase hex string")]
230    #[case("DeaDbeEF",   "DeaDbeEF", "mixed case hex string")]
231    #[case("  deaDbeAF", "deaDbeAF", "leading whitespace trimmed")]
232    #[case(" deaDbeAF ", "deaDbeAF", "surrounding whitespace trimmed")]
233    fn hexstring_parse_success(#[case]val: &str, #[case]slice: &str, #[case]msg: &str) {
234        let_assert!(Ok(s) = val.parse::<HexString>());
235        assert!(s.as_str() == slice, "success ({})", msg);
236    }
237
238    #[test]
239    fn hexstring_convert_to_string() {
240        let_assert!{Ok(s) = "deADBEef".parse::<HexString>()};
241        let st: String = s.into();
242        assert!(st == String::from("deADBEef"));
243    }
244
245    #[test]
246    fn hexstring_convert_to_inner() {
247        let_assert!{Ok(inner) = "deADBEef".parse::<TrimmedNonBlankString>()};
248        let_assert!{Ok(s) = "deADBEef".parse::<HexString>()};
249        assert!(inner == s.into());
250    }
251
252    #[test]
253    fn hexstring_to_lower() {
254        let_assert!{Ok(s) = "deADBEef".parse::<HexString>()};
255        let lhs = s.to_lower();
256        assert!(lhs == "deadbeef".parse::<LowerHexString>().expect("Hardcoded safe string"));
257    }
258
259    #[test]
260    fn hexstring_to_upper() {
261        let_assert!{Ok(s) = "deADBEef".parse::<HexString>()};
262        let lhs = s.to_upper();
263        assert!(lhs == "DEADBEEF".parse::<UpperHexString>().expect("Hardcoded safe string"));
264    }
265
266    #[rstest]
267    #[case("",         ParseError::EmptyString,         "empty string")]
268    #[case("ertyui",   ParseError::NonHexDigit(1, 'r'), "bad character")]
269    #[case("deadbef",  ParseError::OddLength,           "odd length")]
270    #[case("123DBEEF", ParseError::NonHexDigit(3, 'D'), "upper case")]
271    #[case("deADbeeF", ParseError::NonHexDigit(2, 'A'), "mixed case")]
272    fn lower_hexstring_parse_unsuccess(#[case]val: &str, #[case]err: ParseError, #[case]msg: &str) {
273        let_assert!(Err(e) = val.parse::<LowerHexString>());
274        assert!(e == err, "parse error ({})", msg);
275    }
276
277    #[rstest]
278    #[case("deadbeef",    "deadbeef", "lowercase")]
279    #[case("   deadbeef", "deadbeef", "leading whitespace trimmed")]
280    #[case(" deadbeef  ", "deadbeef", "surrounding whitespace trimmed")]
281    fn lower_hexstring_success(#[case]val: &str, #[case]slice: &str, #[case]msg: &str) {
282        let_assert!(Ok(s) = val.parse::<LowerHexString>());
283        assert!(s.as_str() == slice, "{msg}");
284    }
285
286    #[test]
287    fn lower_hexstring_convert_to_string() {
288        let_assert!{Ok(s) = "deadbeef".parse::<LowerHexString>()};
289        let st: String = s.into();
290        assert!(st == String::from("deadbeef"));
291    }
292
293    #[test]
294    fn lower_hexstring_convert_to_inner() {
295        let_assert!{Ok(inner) = "deadbeef".parse::<HexString>()};
296        let_assert!{Ok(s) = "deadbeef".parse::<LowerHexString>()};
297        assert!(inner == s.into());
298    }
299
300    #[rstest]
301    #[case("",         ParseError::EmptyString,         "empty string")]
302    #[case("ERTYUI",   ParseError::NonHexDigit(1, 'R'), "bad character")]
303    #[case("DEADBEF",  ParseError::OddLength,           "odd length")]
304    #[case("1eadbeef", ParseError::NonHexDigit(1, 'e'), "upper case")]
305    #[case("DeaDbeeF", ParseError::NonHexDigit(1, 'e'), "mixed case")]
306    fn upper_hexstring_parse_unsuccess(#[case]val: &str, #[case]err: ParseError, #[case]msg: &str) {
307        let_assert!(Err(e) = val.parse::<UpperHexString>());
308        assert!(e == err, "parse error ({})", msg);
309    }
310
311    #[rstest]
312    #[case("DEADBEEF",    "DEADBEEF", "lowercase")]
313    #[case("   DEADBEEF", "DEADBEEF", "leading whitespace trimmed")]
314    #[case(" DEADBEEF  ", "DEADBEEF", "surrounding whitespace trimmed")]
315    fn upper_hexstring_success(#[case]val: &str, #[case]slice: &str, #[case]msg: &str) {
316        let_assert!(Ok(s) = val.parse::<UpperHexString>());
317        assert!(s.as_str() == slice, "{msg}");
318    }
319
320    #[test]
321    fn upper_hexstring_convert_to_string() {
322        let_assert!{Ok(s) = "DEADBEEF".parse::<UpperHexString>()};
323        let st: String = s.into();
324        assert!(st == String::from("DEADBEEF"));
325    }
326
327    #[test]
328    fn upper_hexstring_convert_to_inner() {
329        let_assert!{Ok(inner) = "DEADBEEF".parse::<HexString>()};
330        let_assert!{Ok(s) = "DEADBEEF".parse::<UpperHexString>()};
331        assert!(inner == s.into());
332    }
333}