use bytes::Bytes;
use ruststream::HeaderMap;
use zeromq::ZmqMessage as WireMessage;
use crate::error::ZmqError;
use crate::message::ZmqMessage;
fn encode_headers(headers: &HeaderMap) -> Result<Bytes, String> {
if headers.is_empty() {
return Ok(Bytes::new());
}
let mut text = String::new();
for (name, value) in headers.iter() {
let value = std::str::from_utf8(value).map_err(|_| {
format!("header '{name}' holds a value that is not UTF-8, and the header frame is text")
})?;
text.push_str(name);
text.push_str(": ");
text.push_str(value);
text.push('\n');
}
text.pop();
Ok(Bytes::from(text))
}
fn decode_headers(frame: &[u8]) -> Result<HeaderMap, ZmqError> {
let text = std::str::from_utf8(frame)
.map_err(|_| ZmqError::Wire("the header frame must be UTF-8".into()))?;
let mut headers = HeaderMap::new();
for line in text.lines() {
if line.trim().is_empty() {
continue;
}
let (name, value) = line.split_once(':').ok_or_else(|| {
ZmqError::Wire(format!(
"header line '{line}' has no ':' separating name from value"
))
})?;
headers.insert(name.trim().to_owned(), value.trim().to_owned());
}
Ok(headers)
}
fn frames(
front: Option<Bytes>,
name: &str,
headers: &HeaderMap,
payload: Bytes,
) -> Result<WireMessage, String> {
let headers = encode_headers(headers)?;
let name = Bytes::copy_from_slice(name.as_bytes());
let frames = match front {
Some(front) => vec![front, name, headers, payload],
None => vec![name, headers, payload],
};
Ok(WireMessage::try_from(frames).expect("a list built from named frames is never empty"))
}
pub(crate) fn encode(
name: &str,
headers: &HeaderMap,
payload: Bytes,
) -> Result<WireMessage, String> {
frames(None, name, headers, payload)
}
pub(crate) fn encode_addressed_to(
destination: &str,
identity: Bytes,
name: &str,
headers: &HeaderMap,
payload: Bytes,
) -> Result<WireMessage, ZmqError> {
frames(Some(identity), name, headers, payload).map_err(|reason| ZmqError::Send {
name: destination.to_owned(),
reason,
})
}
pub(crate) fn encode_to(
destination: &str,
name: &str,
headers: &HeaderMap,
payload: Bytes,
) -> Result<WireMessage, ZmqError> {
encode(name, headers, payload).map_err(|reason| ZmqError::Send {
name: destination.to_owned(),
reason,
})
}
#[cfg(feature = "testing")]
pub(crate) fn encode_foreign(name: &str, headers: &HeaderMap, payload: Bytes) -> WireMessage {
let mut text = Vec::new();
for (name, value) in headers.iter() {
if !text.is_empty() {
text.push(b'\n');
}
text.extend_from_slice(name.as_bytes());
text.extend_from_slice(b": ");
text.extend_from_slice(value);
}
let frames = vec![
Bytes::copy_from_slice(name.as_bytes()),
Bytes::from(text),
payload,
];
WireMessage::try_from(frames).expect("a list built from named frames is never empty")
}
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn read_delivery(message: WireMessage) -> Option<Result<ZmqMessage, ZmqError>> {
Some(decode(message).map(|(name, headers, payload)| ZmqMessage::new(name, headers, payload)))
}
pub(crate) fn decode(message: WireMessage) -> Result<(String, HeaderMap, Bytes), ZmqError> {
let mut frames = message.into_vecdeque();
let name = frames
.pop_front()
.ok_or_else(|| ZmqError::Wire("a message needs at least a name frame".into()))?;
let name = std::str::from_utf8(&name)
.map_err(|_| ZmqError::Wire("the name frame must be UTF-8".into()))?
.to_owned();
let (headers, payload) = match (frames.pop_front(), frames.pop_front()) {
(Some(headers), Some(payload)) => (decode_headers(&headers)?, payload),
(Some(payload), None) => (HeaderMap::new(), payload),
(None, None) => (HeaderMap::new(), Bytes::new()),
(None, Some(_)) => unreachable!("pop_front cannot skip"),
};
Ok((name, headers, payload))
}
#[cfg(test)]
mod tests {
use bytes::BytesMut;
use super::*;
#[test]
fn the_payload_frame_is_the_buffer_that_was_handed_in() {
let body = BytesMut::from(&br#"{"id":7}"#[..]);
let written_at = body.as_ptr();
let message = encode("orders", &HeaderMap::new(), body.freeze()).expect("encodes");
assert_eq!(
message.get(2).expect("the payload frame").as_ptr(),
written_at,
"the frame must carry the buffer the publish wrote, not a copy of it",
);
}
#[test]
fn the_frame_list_is_built_at_its_final_size() {
let message =
encode("orders", &HeaderMap::new(), Bytes::from_static(b"{}")).expect("encodes");
let frames = message.into_vecdeque();
assert_eq!(frames.len(), 3, "name, headers, payload");
assert_eq!(
frames.capacity(),
frames.len(),
"a list grown frame by frame ends up larger than the message it holds",
);
}
#[test]
fn a_reply_is_framed_behind_its_identity_at_its_final_size() {
let message = encode_addressed_to(
"orders",
Bytes::from_static(&[0xde, 0xad]),
"reply",
&HeaderMap::new(),
Bytes::from_static(b"{}"),
)
.expect("encodes");
let frames = message.into_vecdeque();
assert_eq!(frames.len(), 4, "identity, name, headers, payload");
assert_eq!(
frames.capacity(),
frames.len(),
"a list grown frame by frame ends up larger than the message it holds",
);
}
#[test]
fn three_frames_round_trip() {
let mut headers = HeaderMap::new();
headers.insert("content-type", "application/json");
headers.insert("x-tenant", "acme");
let message = encode("orders", &headers, Bytes::from_static(b"{}")).expect("encodes");
let (name, decoded, payload) = decode(message).expect("decodes");
assert_eq!(name, "orders");
assert_eq!(decoded.get_str("content-type"), Some("application/json"));
assert_eq!(decoded.get_str("x-tenant"), Some("acme"));
assert_eq!(payload.as_ref(), b"{}");
}
#[test]
fn a_two_frame_message_reads_as_headerless() {
let mut message = zeromq::ZmqMessage::from("orders");
message.push_back(Bytes::from_static(b"raw"));
let (name, headers, payload) = decode(message).expect("decodes");
assert_eq!(name, "orders");
assert_eq!(headers, HeaderMap::new());
assert_eq!(payload.as_ref(), b"raw");
}
#[test]
fn empty_headers_stay_an_empty_frame() {
let message =
encode("orders", &HeaderMap::new(), Bytes::from_static(b"x")).expect("encodes");
assert_eq!(message.get(1).map(Bytes::len), Some(0));
}
#[test]
fn a_value_that_is_not_text_is_refused_with_its_destination() {
let mut headers = HeaderMap::new();
headers.insert("x-binary", [0xff, 0xfe].as_slice());
let err = encode_to("orders", "reply", &headers, Bytes::from_static(b"{}"))
.expect_err("a binary value has no frame");
let message = err.to_string();
assert!(message.contains("x-binary"), "names the header: {message}");
assert!(
message.contains("orders"),
"names the destination: {message}"
);
}
#[test]
fn a_header_frame_that_is_not_text_is_refused() {
let mut message = zeromq::ZmqMessage::from("orders");
message.push_back(Bytes::from_static(&[0xff, 0xfe]));
message.push_back(Bytes::from_static(b"{}"));
assert!(decode(message).is_err());
}
#[test]
fn a_header_line_without_a_separator_is_refused() {
let mut message = zeromq::ZmqMessage::from("orders");
message.push_back(Bytes::from_static(b"content-type application/json"));
message.push_back(Bytes::from_static(b"{}"));
let err = decode(message).expect_err("a line with no ':' is malformed");
assert!(err.to_string().contains("content-type application/json"));
}
#[test]
fn blank_header_lines_are_skipped() {
let mut message = zeromq::ZmqMessage::from("orders");
message.push_back(Bytes::from_static(b"x-tenant: acme\n"));
message.push_back(Bytes::from_static(b"{}"));
let (_, headers, _) = decode(message).expect("decodes");
assert_eq!(headers.get_str("x-tenant"), Some("acme"));
}
}