Skip to main content

watermelon_proto/
queue_group.rs

1use alloc::string::String;
2use core::{
3    fmt::{self, Display},
4    ops::Deref,
5    str::FromStr,
6};
7use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
8
9use bytestring::ByteString;
10
11/// A string that can be used to represent an queue group
12///
13/// `QueueGroup` contains a string that is guaranteed [^1] to
14/// contain a valid queue group that meets the following requirements:
15///
16/// * The value is not empty
17/// * The value does not contain ` `, `\t`, `\r` or `\n`
18///
19/// `QueueGroup` can be constructed from [`QueueGroup::from_static`]
20/// or any of the `TryFrom` implementations.
21///
22/// [^1]: Because [`QueueGroup::from_dangerous_value`] is safe to call,
23///       unsafe code must not assume any of the above invariants.
24#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
25pub struct QueueGroup(ByteString);
26
27impl QueueGroup {
28    /// Construct `QueueGroup` from a static string
29    ///
30    /// # Panics
31    ///
32    /// Will panic if `value` isn't a valid `QueueGroup`
33    #[must_use]
34    pub fn from_static(value: &'static str) -> Self {
35        Self::try_from(ByteString::from_static(value)).expect("invalid QueueGroup")
36    }
37
38    /// Construct a `QueueGroup` from a string, without checking invariants
39    ///
40    /// This method bypasses invariants checks implemented by [`QueueGroup::from_static`]
41    /// and all `TryFrom` implementations.
42    ///
43    /// # Security
44    ///
45    /// While calling this method can eliminate the runtime performance cost of
46    /// checking the string, constructing `QueueGroup` with an invalid string and
47    /// then calling the NATS server with it can cause serious security issues.
48    /// When in doubt use the [`QueueGroup::from_static`] or any of the `TryFrom`
49    /// implementations.
50    #[must_use]
51    #[expect(
52        clippy::missing_panics_doc,
53        reason = "The queue group validation is only made in debug"
54    )]
55    pub fn from_dangerous_value(value: ByteString) -> Self {
56        if cfg!(debug_assertions)
57            && let Err(err) = validate_queue_group(&value)
58        {
59            panic!("QueueGroup {value:?} isn't valid {err:?}");
60        }
61        Self(value)
62    }
63
64    #[must_use]
65    pub fn as_str(&self) -> &str {
66        &self.0
67    }
68}
69
70impl Display for QueueGroup {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        Display::fmt(&self.0, f)
73    }
74}
75
76impl TryFrom<ByteString> for QueueGroup {
77    type Error = QueueGroupValidateError;
78
79    fn try_from(value: ByteString) -> Result<Self, Self::Error> {
80        validate_queue_group(&value)?;
81        Ok(Self::from_dangerous_value(value))
82    }
83}
84
85impl FromStr for QueueGroup {
86    type Err = QueueGroupValidateError;
87
88    fn from_str(value: &str) -> Result<Self, Self::Err> {
89        validate_queue_group(value)?;
90        Ok(Self::from_dangerous_value(value.into()))
91    }
92}
93
94impl TryFrom<String> for QueueGroup {
95    type Error = QueueGroupValidateError;
96
97    fn try_from(value: String) -> Result<Self, Self::Error> {
98        validate_queue_group(&value)?;
99        Ok(Self::from_dangerous_value(value.into()))
100    }
101}
102
103impl From<QueueGroup> for ByteString {
104    fn from(value: QueueGroup) -> Self {
105        value.0
106    }
107}
108
109impl AsRef<[u8]> for QueueGroup {
110    fn as_ref(&self) -> &[u8] {
111        self.as_str().as_bytes()
112    }
113}
114
115impl AsRef<str> for QueueGroup {
116    fn as_ref(&self) -> &str {
117        self.as_str()
118    }
119}
120
121impl Deref for QueueGroup {
122    type Target = str;
123
124    fn deref(&self) -> &Self::Target {
125        self.as_str()
126    }
127}
128
129impl Serialize for QueueGroup {
130    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
131        self.as_str().serialize(serializer)
132    }
133}
134
135impl<'de> Deserialize<'de> for QueueGroup {
136    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
137        let s = ByteString::deserialize(deserializer)?;
138        s.try_into().map_err(de::Error::custom)
139    }
140}
141
142/// An error encountered while validating [`QueueGroup`]
143#[derive(Debug, thiserror::Error)]
144#[cfg_attr(test, derive(PartialEq, Eq))]
145pub enum QueueGroupValidateError {
146    /// The value is empty
147    #[error("QueueGroup is empty")]
148    Empty,
149    /// The value contains ` `, `\t`, `\r` or `\n`
150    #[error("QueueGroup contained an illegal character")]
151    IllegalCharacter,
152}
153
154fn validate_queue_group(queue_group: &str) -> Result<(), QueueGroupValidateError> {
155    if queue_group.is_empty() {
156        return Err(QueueGroupValidateError::Empty);
157    }
158
159    for b in queue_group.bytes() {
160        // The server accepts almost any bytes and does not enforce a
161        // per-queue-group length limit. The queue group is however written
162        // to the wire inside the whitespace delimited `SUB` control line,
163        // so ` ` and `\t` would be interpreted as an argument separator and
164        // `\r`/`\n` would terminate the control line early.
165        if b == b' ' || b == b'\t' || b == b'\r' || b == b'\n' {
166            return Err(QueueGroupValidateError::IllegalCharacter);
167        }
168    }
169
170    Ok(())
171}
172
173#[cfg(test)]
174mod tests {
175    use bytestring::ByteString;
176
177    use super::{QueueGroup, QueueGroupValidateError};
178
179    #[test]
180    fn valid_queue_groups() {
181        let queue_groups = ["importer", "importer.thing", "blablabla:itworks"];
182        for queue_group in queue_groups {
183            let q = QueueGroup::try_from(ByteString::from_static(queue_group)).unwrap();
184            assert_eq!(queue_group, q.as_str());
185        }
186    }
187
188    #[test]
189    fn invalid_queue_groups() {
190        let queue_groups = [
191            ("", QueueGroupValidateError::Empty),
192            ("importer ", QueueGroupValidateError::IllegalCharacter),
193            ("importer .thing", QueueGroupValidateError::IllegalCharacter),
194            (" importer", QueueGroupValidateError::IllegalCharacter),
195            ("importer.thing ", QueueGroupValidateError::IllegalCharacter),
196            (
197                "importer.thing.works ",
198                QueueGroupValidateError::IllegalCharacter,
199            ),
200            (
201                "importer.thing.works\r",
202                QueueGroupValidateError::IllegalCharacter,
203            ),
204            (
205                "importer.thing.works\t",
206                QueueGroupValidateError::IllegalCharacter,
207            ),
208            (
209                "importer.thi ng.works",
210                QueueGroupValidateError::IllegalCharacter,
211            ),
212            (
213                "importer.thi\tng.works",
214                QueueGroupValidateError::IllegalCharacter,
215            ),
216            (
217                "importer.thing .works",
218                QueueGroupValidateError::IllegalCharacter,
219            ),
220            (
221                "importer.thing\t.works",
222                QueueGroupValidateError::IllegalCharacter,
223            ),
224            (" ", QueueGroupValidateError::IllegalCharacter),
225            ("\t", QueueGroupValidateError::IllegalCharacter),
226            (
227                "importer.thing.works\n",
228                QueueGroupValidateError::IllegalCharacter,
229            ),
230            (
231                "importer.thi\rng.works",
232                QueueGroupValidateError::IllegalCharacter,
233            ),
234            (
235                "importer.thi\nng.works",
236                QueueGroupValidateError::IllegalCharacter,
237            ),
238            (
239                "importer.thing\r.works",
240                QueueGroupValidateError::IllegalCharacter,
241            ),
242            (
243                "importer.thing\n.works",
244                QueueGroupValidateError::IllegalCharacter,
245            ),
246            ("\r", QueueGroupValidateError::IllegalCharacter),
247            ("\n", QueueGroupValidateError::IllegalCharacter),
248        ];
249        for (queue_group, expected_err) in queue_groups {
250            let err = QueueGroup::try_from(ByteString::from_static(queue_group)).unwrap_err();
251            assert_eq!(expected_err, err);
252        }
253    }
254}