pub const MAX_FRAME_BYTES: usize = 1024 * 1024;
pub const MAX_HEADERS: usize = 1024;
const MAX_QUOTED_FRAGMENT: usize = 120;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StompFrame {
pub command: String,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
}
impl StompFrame {
pub fn new(command: impl Into<String>, headers: Vec<(String, String)>, body: Vec<u8>) -> Self {
Self {
command: command.into(),
headers,
body,
}
}
pub fn header(&self, name: &str) -> Option<&str> {
self.headers
.iter()
.find(|(k, _)| k == name)
.map(|(_, v)| v.as_str())
}
pub fn headers_except(&self, exclude: &[&str]) -> serde_json::Map<String, serde_json::Value> {
let mut out = serde_json::Map::new();
for (k, v) in &self.headers {
if exclude.contains(&k.as_str()) {
continue;
}
out.entry(k.clone())
.or_insert_with(|| serde_json::Value::String(v.clone()));
}
out
}
pub fn encode(&self) -> Vec<u8> {
let escape = should_escape(&self.command);
let mut out = Vec::with_capacity(64 + self.body.len());
out.extend_from_slice(self.command.as_bytes());
out.push(b'\n');
let mut has_content_length = false;
for (k, v) in &self.headers {
if k == "content-length" {
has_content_length = true;
}
let (k, v) = if escape {
(escape_header(k), escape_header(v))
} else {
(k.clone(), v.clone())
};
out.extend_from_slice(k.as_bytes());
out.push(b':');
out.extend_from_slice(v.as_bytes());
out.push(b'\n');
}
if !has_content_length && !self.body.is_empty() {
out.extend_from_slice(format!("content-length:{}\n", self.body.len()).as_bytes());
}
out.push(b'\n');
out.extend_from_slice(&self.body);
out.push(0);
out
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseOutcome {
Frame {
frame: StompFrame,
consumed: usize,
},
Heartbeat {
consumed: usize,
},
Incomplete,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FrameError {
MalformedHeader(String),
InvalidEscape(String),
BadContentLength(String),
MissingNulTerminator,
NotUtf8,
FrameTooLarge(usize),
TooManyHeaders,
}
impl std::fmt::Display for FrameError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FrameError::MalformedHeader(line) => {
write!(f, "header line has no ':' separator: {line:?}")
}
FrameError::InvalidEscape(seq) => write!(f, "undefined header escape sequence {seq:?}"),
FrameError::BadContentLength(v) => {
write!(f, "content-length is not a non-negative integer: {v:?}")
}
FrameError::MissingNulTerminator => write!(
f,
"content-length bytes were not followed by a NUL terminator"
),
FrameError::NotUtf8 => write!(f, "command line or header was not valid UTF-8"),
FrameError::FrameTooLarge(n) => write!(
f,
"frame exceeded {MAX_FRAME_BYTES} bytes without terminating ({n} buffered)"
),
FrameError::TooManyHeaders => {
write!(f, "frame declared more than {MAX_HEADERS} headers")
}
}
}
}
impl std::error::Error for FrameError {}
fn quote_fragment(s: &str) -> String {
crate::utils::truncate_for_log(s, MAX_QUOTED_FRAGMENT)
}
pub fn is_safe_unescaped_header(s: &str) -> bool {
!s.contains(['\r', '\n', ':', '\0'])
}
pub fn should_escape(command: &str) -> bool {
!matches!(command, "CONNECT" | "STOMP" | "CONNECTED")
}
pub fn escape_header(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\r' => out.push_str("\\r"),
'\n' => out.push_str("\\n"),
':' => out.push_str("\\c"),
'\\' => out.push_str("\\\\"),
other => out.push(other),
}
}
out
}
pub fn unescape_header(s: &str) -> Result<String, FrameError> {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c != '\\' {
out.push(c);
continue;
}
match chars.next() {
Some('r') => out.push('\r'),
Some('n') => out.push('\n'),
Some('c') => out.push(':'),
Some('\\') => out.push('\\'),
Some(other) => return Err(FrameError::InvalidEscape(format!("\\{other}"))),
None => return Err(FrameError::InvalidEscape("\\<end of value>".to_string())),
}
}
Ok(out)
}
pub fn parse_frame(buf: &[u8]) -> Result<ParseOutcome, FrameError> {
match parse_inner(buf) {
Ok(ParseOutcome::Incomplete) if buf.len() > MAX_FRAME_BYTES => {
Err(FrameError::FrameTooLarge(buf.len()))
}
other => other,
}
}
fn parse_inner(buf: &[u8]) -> Result<ParseOutcome, FrameError> {
let mut pos = 0usize;
loop {
match buf.get(pos) {
Some(b'\n') => pos += 1,
Some(b'\r') => match buf.get(pos + 1) {
Some(b'\n') => pos += 2,
Some(_) => break,
None => return Ok(ParseOutcome::Incomplete),
},
_ => break,
}
}
if pos > 0 && pos >= buf.len() {
return Ok(ParseOutcome::Heartbeat { consumed: pos });
}
if pos >= buf.len() {
return Ok(ParseOutcome::Incomplete);
}
let Some((command, next)) = read_line(buf, pos)? else {
return Ok(ParseOutcome::Incomplete);
};
pos = next;
let escape = should_escape(&command);
let mut headers: Vec<(String, String)> = Vec::new();
loop {
let Some((line, next)) = read_line(buf, pos)? else {
return Ok(ParseOutcome::Incomplete);
};
pos = next;
if line.is_empty() {
break;
}
if headers.len() >= MAX_HEADERS {
return Err(FrameError::TooManyHeaders);
}
let Some((raw_name, raw_value)) = line.split_once(':') else {
return Err(FrameError::MalformedHeader(quote_fragment(&line)));
};
let (name, value) = if escape {
(unescape_header(raw_name)?, unescape_header(raw_value)?)
} else {
(raw_name.to_string(), raw_value.to_string())
};
headers.push((name, value));
}
let declared_len = headers
.iter()
.find(|(k, _)| k == "content-length")
.map(|(_, v)| {
v.trim()
.parse::<usize>()
.map_err(|_| FrameError::BadContentLength(quote_fragment(v)))
})
.transpose()?;
let (body, consumed) = match declared_len {
Some(len) => {
if len > MAX_FRAME_BYTES {
return Err(FrameError::FrameTooLarge(len));
}
let end = pos + len;
if end >= buf.len() {
return Ok(ParseOutcome::Incomplete);
}
if buf[end] != 0 {
return Err(FrameError::MissingNulTerminator);
}
(buf[pos..end].to_vec(), end + 1)
}
None => {
let Some(offset) = buf[pos..].iter().position(|&b| b == 0) else {
return Ok(ParseOutcome::Incomplete);
};
(buf[pos..pos + offset].to_vec(), pos + offset + 1)
}
};
Ok(ParseOutcome::Frame {
frame: StompFrame {
command,
headers,
body,
},
consumed,
})
}
fn read_line(buf: &[u8], from: usize) -> Result<Option<(String, usize)>, FrameError> {
let Some(offset) = buf[from..].iter().position(|&b| b == b'\n') else {
return Ok(None);
};
let mut end = from + offset;
let next = end + 1;
if end > from && buf[end - 1] == b'\r' {
end -= 1;
}
let line = std::str::from_utf8(&buf[from..end])
.map_err(|_| FrameError::NotUtf8)?
.to_string();
Ok(Some((line, next)))
}
pub fn receipt_frame(receipt_id: &str) -> Vec<u8> {
StompFrame::new(
"RECEIPT",
vec![("receipt-id".to_string(), receipt_id.to_string())],
Vec::new(),
)
.encode()
}
pub fn error_frame(message: &str, body: &str) -> Vec<u8> {
StompFrame::new(
"ERROR",
vec![
("message".to_string(), message.to_string()),
("content-type".to_string(), "text/plain".to_string()),
],
body.as_bytes().to_vec(),
)
.encode()
}