Skip to main content

watermelon_proto/
subject.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 a subject
12///
13/// `Subject` contains a string that is guaranteed [^1] to
14/// contain a valid subject that meets the following requirements:
15///
16/// * The value is not empty
17/// * The value does not contain ` `, `\t`, `\r` or `\n`
18/// * The value does not contain wrongly placed `*` or `>` characters
19///
20/// `Subject` can be constructed from [`Subject::from_static`]
21/// or any of the `TryFrom` implementations.
22///
23/// [^1]: Because [`Subject::from_dangerous_value`] is safe to call,
24///       unsafe code must not assume any of the above invariants.
25#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
26pub struct Subject(ByteString);
27
28impl Subject {
29    /// Construct `Subject` from a static string
30    ///
31    /// # Panics
32    ///
33    /// Will panic if `value` isn't a valid `Subject`
34    #[must_use]
35    pub fn from_static(value: &'static str) -> Self {
36        Self::try_from(ByteString::from_static(value)).expect("invalid Subject")
37    }
38
39    /// Construct a `Subject` from a string, without checking invariants
40    ///
41    /// This method bypasses invariants checks implemented by [`Subject::from_static`]
42    /// and all `TryFrom` implementations.
43    ///
44    /// # Security
45    ///
46    /// While calling this method can eliminate the runtime performance cost of
47    /// checking the string, constructing `Subject` with an invalid string and
48    /// then calling the NATS server with it can cause serious security issues.
49    /// When in doubt use the [`Subject::from_static`] or any of the `TryFrom`
50    /// implementations.
51    #[expect(
52        clippy::missing_panics_doc,
53        reason = "The subject validation is only made in debug"
54    )]
55    #[must_use]
56    pub fn from_dangerous_value(value: ByteString) -> Self {
57        if cfg!(debug_assertions)
58            && let Err(err) = validate_subject(&value)
59        {
60            panic!("Subject {value:?} isn't valid {err:?}");
61        }
62        Self(value)
63    }
64
65    #[must_use]
66    pub fn as_str(&self) -> &str {
67        &self.0
68    }
69}
70
71impl Display for Subject {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        Display::fmt(&self.0, f)
74    }
75}
76
77impl TryFrom<ByteString> for Subject {
78    type Error = SubjectValidateError;
79
80    fn try_from(value: ByteString) -> Result<Self, Self::Error> {
81        validate_subject(&value)?;
82        Ok(Self::from_dangerous_value(value))
83    }
84}
85
86impl FromStr for Subject {
87    type Err = SubjectValidateError;
88
89    fn from_str(value: &str) -> Result<Self, Self::Err> {
90        validate_subject(value)?;
91        Ok(Self::from_dangerous_value(value.into()))
92    }
93}
94
95impl TryFrom<String> for Subject {
96    type Error = SubjectValidateError;
97
98    fn try_from(value: String) -> Result<Self, Self::Error> {
99        validate_subject(&value)?;
100        Ok(Self::from_dangerous_value(value.into()))
101    }
102}
103
104impl From<Subject> for ByteString {
105    fn from(value: Subject) -> Self {
106        value.0
107    }
108}
109
110impl AsRef<[u8]> for Subject {
111    fn as_ref(&self) -> &[u8] {
112        self.as_str().as_bytes()
113    }
114}
115
116impl AsRef<str> for Subject {
117    fn as_ref(&self) -> &str {
118        self.as_str()
119    }
120}
121
122impl Deref for Subject {
123    type Target = str;
124
125    fn deref(&self) -> &Self::Target {
126        self.as_str()
127    }
128}
129
130impl Serialize for Subject {
131    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
132        self.as_str().serialize(serializer)
133    }
134}
135
136impl<'de> Deserialize<'de> for Subject {
137    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
138        let s = ByteString::deserialize(deserializer)?;
139        s.try_into().map_err(de::Error::custom)
140    }
141}
142
143/// An error encountered while validating [`Subject`]
144#[derive(Debug, thiserror::Error)]
145#[cfg_attr(test, derive(PartialEq, Eq))]
146pub enum SubjectValidateError {
147    /// The value is empty
148    #[error("Subject is empty")]
149    Empty,
150    /// The value contains ` `, `\t`, `\r` or `\n`
151    #[error("Subject contained an illegal character")]
152    IllegalCharacter,
153    /// The value contains consecutive `.` characters
154    #[error("Subject contained a broken token")]
155    BrokenToken,
156    /// The value contains `.` or `>` together with other characters
157    /// in the same token, or the `>` is in the non-last token
158    #[error("Subject contained a broken wildcard")]
159    BrokenWildcard,
160}
161
162fn validate_subject(subject: &str) -> Result<(), SubjectValidateError> {
163    if subject.is_empty() {
164        return Err(SubjectValidateError::Empty);
165    }
166
167    for b in subject.bytes() {
168        // The server accepts almost any bytes via IsValidSubject
169        // (checkRunes=false) and does not enforce a per-subject length limit
170        // beyond the maximum control line length. The subject is however
171        // written to the wire inside whitespace delimited control lines
172        // (e.g. `PUB <subject> <size>\r\n`), so ` ` and `\t` would be
173        // interpreted as an argument separator and `\r`/`\n` would terminate
174        // the control line early.
175        if b == b' ' || b == b'\t' || b == b'\r' || b == b'\n' {
176            return Err(SubjectValidateError::IllegalCharacter);
177        }
178    }
179
180    let mut tokens = subject.split('.').peekable();
181    while let Some(token) = tokens.next() {
182        if token.is_empty() || token.contains("..") {
183            return Err(SubjectValidateError::BrokenToken);
184        }
185
186        if token.len() > 1 && (token.contains(['*', '>'])) {
187            return Err(SubjectValidateError::BrokenWildcard);
188        }
189
190        if token == ">" && tokens.peek().is_some() {
191            return Err(SubjectValidateError::BrokenWildcard);
192        }
193    }
194
195    Ok(())
196}
197
198#[cfg(test)]
199mod tests {
200    use bytestring::ByteString;
201
202    use super::{Subject, SubjectValidateError};
203
204    #[test]
205    fn valid_subjects() {
206        let subjects = [
207            "cmd",
208            "cmd.endpoint",
209            "cmd.endpoint.detail",
210            "cmd.*.detail",
211            "cmd.*.*",
212            "cmd.endpoint.>",
213        ];
214        for subject in subjects {
215            let s = Subject::try_from(ByteString::from_static(subject)).unwrap();
216            assert_eq!(subject, s.as_str());
217        }
218    }
219
220    #[test]
221    fn invalid_subjects() {
222        let subjects = [
223            ("", SubjectValidateError::Empty),
224            ("cmd ", SubjectValidateError::IllegalCharacter),
225            ("cmd .endpoint", SubjectValidateError::IllegalCharacter),
226            (" cmd", SubjectValidateError::IllegalCharacter),
227            ("cmd.endpoint ", SubjectValidateError::IllegalCharacter),
228            (
229                "cmd.endpoint.detail ",
230                SubjectValidateError::IllegalCharacter,
231            ),
232            (
233                "cmd.endpoint.detail\r",
234                SubjectValidateError::IllegalCharacter,
235            ),
236            (
237                "cmd.endpoint.detail\t",
238                SubjectValidateError::IllegalCharacter,
239            ),
240            (
241                "cmd.endp oint.detail",
242                SubjectValidateError::IllegalCharacter,
243            ),
244            (
245                "cmd.endp\toint.detail",
246                SubjectValidateError::IllegalCharacter,
247            ),
248            (
249                "cmd.endpoint .detail",
250                SubjectValidateError::IllegalCharacter,
251            ),
252            (
253                "cmd.endpoint\t.detail",
254                SubjectValidateError::IllegalCharacter,
255            ),
256            (" ", SubjectValidateError::IllegalCharacter),
257            ("\t", SubjectValidateError::IllegalCharacter),
258            (
259                "cmd.endpoint.detail\n",
260                SubjectValidateError::IllegalCharacter,
261            ),
262            (
263                "cmd.endp\roint.detail",
264                SubjectValidateError::IllegalCharacter,
265            ),
266            (
267                "cmd.endp\noint.detail",
268                SubjectValidateError::IllegalCharacter,
269            ),
270            (
271                "cmd.endpoint\r.detail",
272                SubjectValidateError::IllegalCharacter,
273            ),
274            (
275                "cmd.endpoint\n.detail",
276                SubjectValidateError::IllegalCharacter,
277            ),
278            ("\r", SubjectValidateError::IllegalCharacter),
279            ("\n", SubjectValidateError::IllegalCharacter),
280            ("cmd..endpoint", SubjectValidateError::BrokenToken),
281            (".cmd.endpoint", SubjectValidateError::BrokenToken),
282            ("cmd.endpoint.", SubjectValidateError::BrokenToken),
283            ("cmd.**", SubjectValidateError::BrokenWildcard),
284            ("cmd.**.endpoint", SubjectValidateError::BrokenWildcard),
285            ("cmd.a*.endpoint", SubjectValidateError::BrokenWildcard),
286            ("cmd.*a.endpoint", SubjectValidateError::BrokenWildcard),
287            ("cmd.>.endpoint", SubjectValidateError::BrokenWildcard),
288            ("cmd.a>.endpoint", SubjectValidateError::BrokenWildcard),
289            ("cmd.endpoint.a>", SubjectValidateError::BrokenWildcard),
290            ("cmd.endpoint.>a", SubjectValidateError::BrokenWildcard),
291        ];
292        for (subject, expected_err) in subjects {
293            let err = Subject::try_from(ByteString::from_static(subject)).unwrap_err();
294            assert_eq!(expected_err, err);
295        }
296    }
297}