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