use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum SubjectError {
Framing,
EmptyToken,
TailNotLast,
}
impl fmt::Display for SubjectError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Framing => f.write_str("invalid subject: empty, or it contains whitespace"),
Self::EmptyToken => f.write_str("invalid subject: it contains an empty token"),
Self::TailNotLast => {
f.write_str("invalid subject: `>` is only allowed as the last token")
}
}
}
}
impl std::error::Error for SubjectError {}
fn frames(value: &str) -> Result<(), SubjectError> {
if value.is_empty()
|| value
.bytes()
.any(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
{
return Err(SubjectError::Framing);
}
Ok(())
}
pub(crate) fn check_publish(subject: &str) -> Result<(), SubjectError> {
frames(subject)
}
pub(crate) fn check_queue_group(name: &str) -> Result<(), SubjectError> {
frames(name)
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Token {
Literal(String),
One,
Tail,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SubjectPattern {
tokens: Vec<Token>,
}
impl SubjectPattern {
pub(crate) fn parse(pattern: &str) -> Result<Self, SubjectError> {
frames(pattern)?;
let raw: Vec<&str> = pattern.split('.').collect();
if raw.iter().any(|token| token.is_empty()) {
return Err(SubjectError::EmptyToken);
}
let last = raw.len() - 1;
let mut tokens = Vec::with_capacity(raw.len());
for (index, token) in raw.into_iter().enumerate() {
tokens.push(match token {
"*" => Token::One,
">" if index == last => Token::Tail,
">" => return Err(SubjectError::TailNotLast),
literal => Token::Literal(literal.to_owned()),
});
}
Ok(Self { tokens })
}
pub(crate) fn matches(&self, subject: &str) -> bool {
let mut parts = subject.split('.');
for token in &self.tokens {
match token {
Token::Tail => return parts.next().is_some(),
Token::One => {
if parts.next().is_none() {
return false;
}
}
Token::Literal(literal) => {
if parts.next() != Some(literal.as_str()) {
return false;
}
}
}
}
parts.next().is_none()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn match_table() {
let cases: &[(&str, &str, bool)] = &[
("foo", "foo", true),
("foo.bar", "foo.bar", true),
("foo", "bar", false),
("foo.bar", "foo", false),
("foo", "foo.bar", false),
("foo.*", "foo.bar", true),
("foo.*", "foo", false),
("foo.*", "foo.bar.baz", false),
("*.bar", "foo.bar", true),
("*", "foo", true),
("*", "foo.bar", false),
("foo.>", "foo.bar", true),
("foo.>", "foo.bar.baz", true),
("foo.>", "foo", false),
(">", "foo", true),
(">", "foo.bar.baz", true),
("foo.*.>", "foo.x.y", true),
("foo.*.>", "foo.x", false),
("Foo", "foo", false),
("orders:v1.*", "orders:v1.created", true),
("$SYS.>", "$SYS.server", true),
("foo*", "foo*", true),
("foo*", "foox", false),
("foo.*", "foo.*", true),
("foo.bar", "foo.*", false),
];
for (pattern, subject, expected) in cases {
let parsed = SubjectPattern::parse(pattern).expect("pattern parses");
assert_eq!(
parsed.matches(subject),
*expected,
"pattern={pattern} subject={subject}"
);
}
}
#[test]
fn a_subscription_refuses_what_the_client_or_the_server_refuses() {
assert_eq!(SubjectPattern::parse(""), Err(SubjectError::Framing));
assert_eq!(SubjectPattern::parse("foo bar"), Err(SubjectError::Framing));
assert_eq!(
SubjectPattern::parse("foo\tbar"),
Err(SubjectError::Framing)
);
assert_eq!(
SubjectPattern::parse("foo..bar"),
Err(SubjectError::EmptyToken)
);
assert_eq!(SubjectPattern::parse(".foo"), Err(SubjectError::EmptyToken));
assert_eq!(SubjectPattern::parse("foo."), Err(SubjectError::EmptyToken));
assert_eq!(
SubjectPattern::parse("foo.>.bar"),
Err(SubjectError::TailNotLast)
);
assert!(SubjectPattern::parse("foo*").is_ok());
assert!(SubjectPattern::parse("orders/eu").is_ok());
}
#[test]
fn a_publish_takes_whatever_the_frame_carries() {
for subject in [
"orders.created",
"orders:v1",
"a..b",
"foo.*",
"unicode.\u{e9}",
] {
assert!(check_publish(subject).is_ok(), "{subject}");
}
for subject in ["", "foo bar", "foo\r\n"] {
assert_eq!(
check_publish(subject),
Err(SubjectError::Framing),
"{subject:?}"
);
}
}
}