Skip to main content

watermelon_proto/headers/
name.rs

1use alloc::string::String;
2use core::{
3    fmt::{self, Display},
4    ops::Deref,
5    str::FromStr,
6};
7use unicase::UniCase;
8
9use bytestring::ByteString;
10
11/// A string that can be used to represent an header name
12///
13/// `HeaderName` contains a string that is guaranteed [^1] to
14/// contain a valid header name that meets the following requirements:
15///
16/// * The value is not empty
17/// * The value does not contain bytes ≥ 0x80, `\r`, `\n`, or `:`
18///
19/// `HeaderName` can be constructed from [`HeaderName::from_static`]
20/// or any of the `TryFrom` implementations.
21///
22/// [^1]: Because [`HeaderName::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 HeaderName(UniCase<ByteString>);
26
27impl HeaderName {
28    /// Client-defined unique identifier for a message that will be used by the server apply de-duplication within the configured Jetstream _Duplicate Window_
29    pub const MESSAGE_ID: Self = Self::new_internal("Nats-Msg-Id");
30    /// Have Jetstream assert that the published message is received by the expected stream
31    pub const EXPECTED_STREAM: Self = Self::new_internal("Nats-Expected-Stream");
32    /// Have Jetstream assert that the last expected [`HeaderName::MESSAGE_ID`] matches this ID
33    pub const EXPECTED_LAST_MESSAGE_ID: Self = Self::new_internal("Nats-Expected-Last-Msg-Id");
34    /// Have Jetstream assert that the last sequence ID matches this ID
35    pub const EXPECTED_LAST_SEQUENCE: Self = Self::new_internal("Nats-Expected-Last-Sequence");
36    /// Purge all prior messages in the stream (`all` value) or at the subject-level (`sub` value)
37    pub const ROLLUP: Self = Self::new_internal("Nats-Rollup");
38
39    /// Name of the stream the message was republished from
40    pub const STREAM: Self = Self::new_internal("Nats-Stream");
41    /// Original subject to which the message was republished from
42    pub const SUBJECT: Self = Self::new_internal("Nats-Subject");
43    /// Original sequence ID the message was republished from
44    pub const SEQUENCE: Self = Self::new_internal("Nats-Sequence");
45    /// Last sequence ID of the message having the same subject, or zero if this is the first message for the subject
46    pub const LAST_SEQUENCE: Self = Self::new_internal("Nats-Last-Sequence");
47    /// The original RFC3339 timestamp of the message
48    pub const TIMESTAMP: Self = Self::new_internal("Nats-Time-Stamp");
49
50    /// Origin stream name, subject, sequence number, subject filter and destination transform of the message being sourced
51    pub const STREAM_SOURCE: Self = Self::new_internal("Nats-Stream-Source");
52
53    /// Size of the message payload in bytes for an headers-only message
54    pub const MESSAGE_SIZE: Self = Self::new_internal("Nats-Msg-Size");
55
56    /// Construct `HeaderName` from a static string
57    ///
58    /// # Panics
59    ///
60    /// Will panic if `value` isn't a valid `HeaderName`
61    #[must_use]
62    pub fn from_static(value: &'static str) -> Self {
63        Self::try_from(ByteString::from_static(value)).expect("invalid HeaderName")
64    }
65
66    /// Construct a `HeaderName` from a string, without checking invariants
67    ///
68    /// This method bypasses invariants checks implemented by [`HeaderName::from_static`]
69    /// and all `TryFrom` implementations.
70    ///
71    /// # Security
72    ///
73    /// While calling this method can eliminate the runtime performance cost of
74    /// checking the string, constructing `HeaderName` with an invalid string and
75    /// then calling the NATS server with it can cause serious security issues.
76    /// When in doubt use the [`HeaderName::from_static`] or any of the `TryFrom`
77    /// implementations.
78    #[expect(
79        clippy::missing_panics_doc,
80        reason = "The header validation is only made in debug"
81    )]
82    #[must_use]
83    pub fn from_dangerous_value(value: ByteString) -> Self {
84        if cfg!(debug_assertions)
85            && let Err(err) = validate_header_name(&value)
86        {
87            panic!("HeaderName {value:?} isn't valid {err:?}");
88        }
89        Self(UniCase::new(value))
90    }
91
92    const fn new_internal(value: &'static str) -> Self {
93        if value.is_ascii() {
94            Self(UniCase::ascii(ByteString::from_static(value)))
95        } else {
96            Self(UniCase::unicode(ByteString::from_static(value)))
97        }
98    }
99
100    #[must_use]
101    pub fn as_str(&self) -> &str {
102        &self.0
103    }
104}
105
106impl Display for HeaderName {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        Display::fmt(&self.0, f)
109    }
110}
111
112impl TryFrom<ByteString> for HeaderName {
113    type Error = HeaderNameValidateError;
114
115    fn try_from(value: ByteString) -> Result<Self, Self::Error> {
116        validate_header_name(&value)?;
117        Ok(Self::from_dangerous_value(value))
118    }
119}
120
121impl FromStr for HeaderName {
122    type Err = HeaderNameValidateError;
123
124    fn from_str(value: &str) -> Result<Self, Self::Err> {
125        validate_header_name(value)?;
126        Ok(Self::from_dangerous_value(value.into()))
127    }
128}
129
130impl TryFrom<String> for HeaderName {
131    type Error = HeaderNameValidateError;
132
133    fn try_from(value: String) -> Result<Self, Self::Error> {
134        validate_header_name(&value)?;
135        Ok(Self::from_dangerous_value(value.into()))
136    }
137}
138
139impl From<HeaderName> for ByteString {
140    fn from(value: HeaderName) -> Self {
141        value.0.into_inner()
142    }
143}
144
145impl AsRef<[u8]> for HeaderName {
146    fn as_ref(&self) -> &[u8] {
147        self.as_str().as_bytes()
148    }
149}
150
151impl AsRef<str> for HeaderName {
152    fn as_ref(&self) -> &str {
153        self.as_str()
154    }
155}
156
157impl Deref for HeaderName {
158    type Target = str;
159
160    fn deref(&self) -> &Self::Target {
161        self.as_str()
162    }
163}
164
165/// An error encountered while validating [`HeaderName`]
166#[derive(Debug, thiserror::Error)]
167pub enum HeaderNameValidateError {
168    /// The value is empty
169    #[error("HeaderName is empty")]
170    Empty,
171    /// The value contains an illegal character
172    #[error("HeaderName contained an illegal character")]
173    IllegalCharacter,
174}
175
176fn validate_header_name(header_name: &str) -> Result<(), HeaderNameValidateError> {
177    if header_name.is_empty() {
178        return Err(HeaderNameValidateError::Empty);
179    }
180
181    for b in header_name.bytes() {
182        match b {
183            // The NATS server relays header names verbatim without validating
184            // them, but Go's `textproto.ReadMIMEHeader` (used by `nats.go` to
185            // parse received headers) errors out on bytes >= 0x80. Rejecting
186            // them also keeps `HeaderName` ASCII, making the case-insensitive
187            // comparison well defined.
188            b if b >= 0x80 => return Err(HeaderNameValidateError::IllegalCharacter),
189            // Wire protocol constraints: line terminators and name/value separator
190            b'\r' | b'\n' | b':' => return Err(HeaderNameValidateError::IllegalCharacter),
191            _ => {}
192        }
193    }
194
195    Ok(())
196}
197
198#[cfg(test)]
199mod tests {
200    use core::cmp::Ordering;
201    use core::str::FromStr;
202
203    use claims::assert_matches;
204
205    use super::{HeaderName, HeaderNameValidateError};
206
207    #[test]
208    fn eq() {
209        let cased = HeaderName::from_static("Nats-Message-Id");
210        let lowercase = HeaderName::from_static("nats-message-id");
211        assert_eq!(cased, lowercase);
212        assert_eq!(cased.cmp(&lowercase), Ordering::Equal);
213    }
214
215    #[test]
216    fn valid_header_names() {
217        let names = ["Nats-Msg-Id", "a", "X-Custom-Header", "Header Name"];
218        for name in names {
219            let header_name = HeaderName::from_str(name).unwrap();
220            assert_eq!(header_name.as_str(), name);
221        }
222    }
223
224    #[test]
225    fn invalid_header_names() {
226        assert_matches!(
227            HeaderName::from_str(""),
228            Err(HeaderNameValidateError::Empty)
229        );
230
231        let names = [":", "name:", "na:me", "name\r", "na\nme", "nàme"];
232        for name in names {
233            assert_matches!(
234                HeaderName::from_str(name),
235                Err(HeaderNameValidateError::IllegalCharacter),
236                "{name:?}"
237            );
238        }
239    }
240}