tor_netdoc/parse2/keyword.rs
1//! Keywords in netdocs
2
3use super::*;
4
5/// A netdoc keyword
6///
7/// # Safety
8///
9/// Invariants:
10///
11/// * length is between 1 and 255 ([`MAX_LEN`]) inclusive
12/// * there are no nul bytes
13//
14// (These are not currently relied on but may become safety invariants in the future.)
15#[derive(Debug, Clone, Copy, Eq, PartialEq, derive_more::Display)]
16pub struct KeywordRef<'s>(&'s str);
17
18/// Invalid keyword
19#[derive(Error, Clone, Copy, Debug, Eq, PartialEq)]
20#[non_exhaustive]
21pub enum InvalidKeyword {
22 /// Empty keyword
23 #[error("Keyword cannot be empty")]
24 Empty,
25 /// Keyword too long
26 #[error("Keyword longer than {MAX_LEN} bytes")]
27 TooLong,
28 /// Keyword contained nul byte
29 #[error("Keyword contains nul byte")]
30 ContainsNul,
31}
32
33/// Maximum length of a keyword
34pub const MAX_LEN: usize = 255;
35
36impl<'s> KeywordRef<'s> {
37 /// Make a new `Keyword` from a string in const context
38 ///
39 /// # Panics
40 ///
41 /// Panics if the string does not meet the invariants.
42 pub const fn new_const(s: &'s str) -> Self {
43 // unwrap_or_else isn't const. expect isn't const.
44 match Self::new(s) {
45 Ok(y) => y,
46 Err(_e) => panic!("new_const failed"), // can't format error in const
47 }
48 }
49
50 /// Make a new `Keyword` from a string, without checking invariants
51 pub const fn new(s: &'s str) -> Result<Self, InvalidKeyword> {
52 use InvalidKeyword as IK;
53 if s.is_empty() {
54 return Err(IK::Empty);
55 }
56 if s.len() > MAX_LEN {
57 return Err(IK::TooLong);
58 }
59 // s.as_bytes().contains(&b'0'),
60 // but
61 // (&[u8]).contains() isn't const
62 // for b in (&[u8]) isn't const
63 {
64 let mut unchecked = s.as_bytes();
65 while let Some((h, t)) = unchecked.split_first() {
66 if *h == b'0' {
67 return Err(IK::ContainsNul);
68 }
69 unchecked = t;
70 }
71 }
72 Ok(KeywordRef(s))
73 }
74
75 /// Make a new `Keyword` from a string, without checking invariants
76 ///
77 /// ### Safety
78 ///
79 /// The invariants for [`KeywordRef`] must be satisfied.
80 pub unsafe fn new_unchecked(s: &'s str) -> Self {
81 KeywordRef(s)
82 }
83
84 /// Obtain the `Keyword` as a `str`
85 fn as_str(&self) -> &str {
86 self.0
87 }
88 /// Obtain the `Keyword`'s length
89 #[allow(clippy::len_without_is_empty)] // they can't ever be empty
90 pub fn len(&self) -> usize {
91 self.as_str().len()
92 }
93}
94
95impl<'s> AsRef<str> for KeywordRef<'s> {
96 fn as_ref(&self) -> &str {
97 self.as_str()
98 }
99}