watermelon_proto/headers/
name.rs

1use alloc::string::String;
2use core::{
3    fmt::{self, Display},
4    ops::Deref,
5};
6use unicase::UniCase;
7
8use bytestring::ByteString;
9
10/// A string that can be used to represent an header name
11///
12/// `HeaderName` contains a string that is guaranteed [^1] to
13/// contain a valid header name that meets the following requirements:
14///
15/// * The value is not empty
16/// * The value has a length less than or equal to 64 [^2]
17/// * The value does not contain any whitespace characters 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/// [^2]: Messages coming from the NATS server are allowed to violate this rule.
25#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
26pub struct HeaderName(UniCase<ByteString>);
27
28impl HeaderName {
29    /// Client-defined unique identifier for a message that will be used by the server apply de-duplication within the configured Jetstream _Duplicate Window_
30    pub const MESSAGE_ID: Self = Self::new_internal("Nats-Msg-Id");
31    /// Have Jetstream assert that the published message is received by the expected stream
32    pub const EXPECTED_STREAM: Self = Self::new_internal("Nats-Expected-Stream");
33    /// Have Jetstream assert that the last expected [`HeaderName::MESSAGE_ID`] matches this ID
34    pub const EXPECTED_LAST_MESSAGE_ID: Self = Self::new_internal("Nats-Expected-Last-Msg-Id");
35    /// Have Jetstream assert that the last sequence ID matches this ID
36    pub const EXPECTED_LAST_SEQUENCE: Self = Self::new_internal("Nats-Expected-Last-Sequence");
37    /// Purge all prior messages in the stream (`all` value) or at the subject-level (`sub` value)
38    pub const ROLLUP: Self = Self::new_internal("Nats-Rollup");
39
40    /// Name of the stream the message was republished from
41    pub const STREAM: Self = Self::new_internal("Nats-Stream");
42    /// Original subject to which the message was republished from
43    pub const SUBJECT: Self = Self::new_internal("Nats-Subject");
44    /// Original sequence ID the message was republished from
45    pub const SEQUENCE: Self = Self::new_internal("Nats-Sequence");
46    /// Last sequence ID of the message having the same subject, or zero if this is the first message for the subject
47    pub const LAST_SEQUENCE: Self = Self::new_internal("Nats-Last-Sequence");
48    /// The original RFC3339 timestamp of the message
49    pub const TIMESTAMP: Self = Self::new_internal("Nats-Time-Stamp");
50
51    /// Origin stream name, subject, sequence number, subject filter and destination transform of the message being sourced
52    pub const STREAM_SOURCE: Self = Self::new_internal("Nats-Stream-Source");
53
54    /// Size of the message payload in bytes for an headers-only message
55    pub const MESSAGE_SIZE: Self = Self::new_internal("Nats-Msg-Size");
56
57    /// Construct `HeaderName` from a static string
58    ///
59    /// # Panics
60    ///
61    /// Will panic if `value` isn't a valid `HeaderName`
62    #[must_use]
63    pub fn from_static(value: &'static str) -> Self {
64        Self::try_from(ByteString::from_static(value)).expect("invalid HeaderName")
65    }
66
67    /// Construct a `HeaderName` from a string, without checking invariants
68    ///
69    /// This method bypasses invariants checks implemented by [`HeaderName::from_static`]
70    /// and all `TryFrom` implementations.
71    ///
72    /// # Security
73    ///
74    /// While calling this method can eliminate the runtime performance cost of
75    /// checking the string, constructing `HeaderName` with an invalid string and
76    /// then calling the NATS server with it can cause serious security issues.
77    /// When in doubt use the [`HeaderName::from_static`] or any of the `TryFrom`
78    /// implementations.
79    #[expect(
80        clippy::missing_panics_doc,
81        reason = "The header validation is only made in debug"
82    )]
83    #[must_use]
84    pub fn from_dangerous_value(value: ByteString) -> Self {
85        if cfg!(debug_assertions) {
86            if let Err(err) = validate_header_name(&value) {
87                panic!("HeaderName {value:?} isn't valid {err:?}");
88            }
89        }
90        Self(UniCase::new(value))
91    }
92
93    const fn new_internal(value: &'static str) -> Self {
94        if value.is_ascii() {
95            Self(UniCase::ascii(ByteString::from_static(value)))
96        } else {
97            Self(UniCase::unicode(ByteString::from_static(value)))
98        }
99    }
100
101    #[must_use]
102    pub fn as_str(&self) -> &str {
103        &self.0
104    }
105}
106
107impl Display for HeaderName {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        Display::fmt(&self.0, f)
110    }
111}
112
113impl TryFrom<ByteString> for HeaderName {
114    type Error = HeaderNameValidateError;
115
116    fn try_from(value: ByteString) -> Result<Self, Self::Error> {
117        validate_header_name(&value)?;
118        Ok(Self::from_dangerous_value(value))
119    }
120}
121
122impl TryFrom<String> for HeaderName {
123    type Error = HeaderNameValidateError;
124
125    fn try_from(value: String) -> Result<Self, Self::Error> {
126        validate_header_name(&value)?;
127        Ok(Self::from_dangerous_value(value.into()))
128    }
129}
130
131impl From<HeaderName> for ByteString {
132    fn from(value: HeaderName) -> Self {
133        value.0.into_inner()
134    }
135}
136
137impl AsRef<[u8]> for HeaderName {
138    fn as_ref(&self) -> &[u8] {
139        self.as_str().as_bytes()
140    }
141}
142
143impl AsRef<str> for HeaderName {
144    fn as_ref(&self) -> &str {
145        self.as_str()
146    }
147}
148
149impl Deref for HeaderName {
150    type Target = str;
151
152    fn deref(&self) -> &Self::Target {
153        self.as_str()
154    }
155}
156
157/// An error encountered while validating [`HeaderName`]
158#[derive(Debug, thiserror::Error)]
159pub enum HeaderNameValidateError {
160    /// The value is empty
161    #[error("HeaderName is empty")]
162    Empty,
163    /// The value has a length greater than 64
164    #[error("HeaderName is too long")]
165    TooLong,
166    /// The value contains an Unicode whitespace character or `:`
167    #[error("HeaderName contained an illegal whitespace character")]
168    IllegalCharacter,
169}
170
171fn validate_header_name(header_name: &str) -> Result<(), HeaderNameValidateError> {
172    if header_name.is_empty() {
173        return Err(HeaderNameValidateError::Empty);
174    }
175
176    if header_name.len() > 64 {
177        // This is an arbitrary limit, but I guess the server must also have one
178        return Err(HeaderNameValidateError::TooLong);
179    }
180
181    if header_name.chars().any(|c| c.is_whitespace() || c == ':') {
182        // The theoretical security limit is just ` `, `\t`, `\r`, `\n` and `:`.
183        // Let's be more careful.
184        return Err(HeaderNameValidateError::IllegalCharacter);
185    }
186
187    Ok(())
188}
189
190#[cfg(test)]
191mod tests {
192    use core::cmp::Ordering;
193
194    use super::HeaderName;
195
196    #[test]
197    fn eq() {
198        let cased = HeaderName::from_static("Nats-Message-Id");
199        let lowercase = HeaderName::from_static("nats-message-id");
200        assert_eq!(cased, lowercase);
201        assert_eq!(cased.cmp(&lowercase), Ordering::Equal);
202    }
203}