pub(crate) const MAGIC: &[u8; 6] = b"ASAPv1";
pub(crate) const VERSION: u8 = 0x01;
pub(crate) fn encode(kind_id: &[u8], metadata: &[u8], payload: &[u8]) -> Vec<u8> {
let kind_id_len = u8::try_from(kind_id.len()).expect("ASAPv1 kind_id too long (>255 bytes)");
let metadata_len = u32::try_from(metadata.len()).expect("ASAPv1 metadata too large");
let payload_len = u32::try_from(payload.len()).expect("ASAPv1 payload too large");
let mut out = Vec::with_capacity(
MAGIC.len() + 1 + 1 + kind_id.len() + 4 + 4 + metadata.len() + payload.len(),
);
out.extend_from_slice(MAGIC);
out.push(VERSION);
out.push(kind_id_len);
out.extend_from_slice(kind_id);
out.extend_from_slice(&metadata_len.to_be_bytes());
out.extend_from_slice(&payload_len.to_be_bytes());
out.extend_from_slice(metadata);
out.extend_from_slice(payload);
out
}
pub(crate) type Parts<'a> = (&'a [u8], &'a [u8], &'a [u8]);
pub(crate) fn split(bytes: &[u8]) -> Result<Parts<'_>, String> {
let magic_len = MAGIC.len();
let kind_id_len_offset = magic_len + 1;
let kind_id_offset = magic_len + 2;
let header_min = kind_id_offset + 4 + 4;
if bytes.len() < header_min {
return Err(format!(
"ASAPv1 envelope: too short ({} bytes, need at least {header_min})",
bytes.len()
));
}
if &bytes[..magic_len] != MAGIC {
return Err("ASAPv1 envelope: bad magic".to_string());
}
if bytes[magic_len] != VERSION {
return Err(format!(
"ASAPv1 envelope: unsupported version 0x{:02x}",
bytes[magic_len]
));
}
let kind_id_len = bytes[kind_id_len_offset] as usize;
let lengths_offset = kind_id_offset + kind_id_len;
if bytes.len() < lengths_offset + 8 {
return Err("ASAPv1 envelope: truncated kind_id / length fields".to_string());
}
let kind_id = &bytes[kind_id_offset..lengths_offset];
let metadata_len = u32::from_be_bytes(
bytes[lengths_offset..lengths_offset + 4]
.try_into()
.expect("four-byte length slice"),
) as usize;
let payload_len = u32::from_be_bytes(
bytes[lengths_offset + 4..lengths_offset + 8]
.try_into()
.expect("four-byte length slice"),
) as usize;
let metadata_start = lengths_offset + 8;
let payload_start = metadata_start
.checked_add(metadata_len)
.ok_or_else(|| "ASAPv1 envelope: length overflow".to_string())?;
let payload_end = payload_start
.checked_add(payload_len)
.ok_or_else(|| "ASAPv1 envelope: length overflow".to_string())?;
if bytes.len() < payload_end {
return Err("ASAPv1 envelope: truncated metadata / payload".to_string());
}
Ok((
kind_id,
&bytes[metadata_start..payload_start],
&bytes[payload_start..payload_end],
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_frames_metadata_and_payload() {
let kind_id = [0x01u8, 0x03];
let metadata = b"meta-block";
let payload = b"payload-bytes";
let bytes = encode(&kind_id, metadata, payload);
assert!(bytes.starts_with(MAGIC));
assert_eq!(bytes[6], VERSION);
assert_eq!(bytes[7], 2);
let (k, m, p) = split(&bytes).expect("split");
assert_eq!(k, kind_id);
assert_eq!(m, metadata);
assert_eq!(p, payload);
}
#[test]
fn rejects_bad_magic_and_version() {
let mut bytes = encode(&[0x01, 0x01], b"m", b"p");
assert!(split(&bytes).is_ok());
bytes[0] = b'X';
assert!(split(&bytes).is_err());
let mut bytes = encode(&[0x01, 0x01], b"m", b"p");
bytes[6] = 0xFF; assert!(split(&bytes).is_err());
}
#[test]
fn rejects_truncation() {
let bytes = encode(&[0x01, 0x01], b"metadata", b"payload");
assert!(split(&bytes[..bytes.len() - 1]).is_err());
assert!(split(&bytes[..3]).is_err());
}
}