use super::gid::Gid;
pub const ATTACHMENT_LEN: usize = 33;
pub const LEGACY_ATTACHMENT_LEN: usize = 77;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AttachmentFormat {
#[default]
Current,
Legacy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Attachment {
pub sequence_number: i64,
pub source_timestamp: i64,
pub source_gid: Gid,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum AttachmentError {
#[error("attachment truncated")]
Truncated,
#[error("attachment field {expected:?} not found")]
UnexpectedField {
expected: &'static str,
},
#[error("attachment gid has length {0}, expected 16")]
GidLength(u64),
#[error("attachment has {0} trailing bytes")]
Trailing(usize),
}
fn put_str(out: &mut Vec<u8>, s: &str) {
out.push(s.len() as u8);
out.extend_from_slice(s.as_bytes());
}
struct Reader<'a>(&'a [u8]);
impl Reader<'_> {
fn take(&mut self, n: usize) -> Result<&[u8], AttachmentError> {
if self.0.len() < n {
return Err(AttachmentError::Truncated);
}
let (head, tail) = self.0.split_at(n);
self.0 = tail;
Ok(head)
}
fn varint(&mut self) -> Result<u64, AttachmentError> {
let mut value = 0_u64;
for shift in (0..64).step_by(7) {
let byte = self.take(1)?[0];
value |= u64::from(byte & 0x7f) << shift;
if byte & 0x80 == 0 {
return Ok(value);
}
}
Err(AttachmentError::Truncated)
}
fn name(&mut self, expected: &'static str) -> Result<(), AttachmentError> {
let len = self.varint()?;
let bytes = self.take(usize::try_from(len).map_err(|_| AttachmentError::Truncated)?)?;
if bytes == expected.as_bytes() {
Ok(())
} else {
Err(AttachmentError::UnexpectedField { expected })
}
}
fn i64(&mut self) -> Result<i64, AttachmentError> {
let bytes = self.take(8)?;
let mut buf = [0_u8; 8];
buf.copy_from_slice(bytes);
Ok(i64::from_le_bytes(buf))
}
}
impl Attachment {
#[must_use]
pub fn now(sequence_number: i64, source_gid: Gid) -> Self {
Self {
sequence_number,
source_timestamp: super::msgs::builtin_interfaces::Time::now().as_nanos(),
source_gid,
}
}
#[must_use]
pub fn encode(&self) -> Vec<u8> {
self.encode_as(AttachmentFormat::Current)
}
#[must_use]
pub fn encode_as(&self, format: AttachmentFormat) -> Vec<u8> {
match format {
AttachmentFormat::Current => {
let mut out = Vec::with_capacity(ATTACHMENT_LEN);
out.extend_from_slice(&self.sequence_number.to_le_bytes());
out.extend_from_slice(&self.source_timestamp.to_le_bytes());
out.push(16);
out.extend_from_slice(&self.source_gid.0);
out
}
AttachmentFormat::Legacy => {
let mut out = Vec::with_capacity(LEGACY_ATTACHMENT_LEN);
put_str(&mut out, "sequence_number");
out.extend_from_slice(&self.sequence_number.to_le_bytes());
put_str(&mut out, "source_timestamp");
out.extend_from_slice(&self.source_timestamp.to_le_bytes());
put_str(&mut out, "source_gid");
out.push(16);
out.extend_from_slice(&self.source_gid.0);
out
}
}
}
pub fn decode(bytes: &[u8]) -> Result<Self, AttachmentError> {
Self::decode_format(bytes).map(|(a, _)| a)
}
pub fn decode_format(bytes: &[u8]) -> Result<(Self, AttachmentFormat), AttachmentError> {
let legacy = bytes.len() == LEGACY_ATTACHMENT_LEN
&& bytes.first() == Some(&15)
&& bytes.get(1..16) == Some(b"sequence_number".as_slice());
let mut r = Reader(bytes);
if legacy {
r.name("sequence_number")?;
}
let sequence_number = r.i64()?;
if legacy {
r.name("source_timestamp")?;
}
let source_timestamp = r.i64()?;
if legacy {
r.name("source_gid")?;
}
let len = r.varint()?;
if len != 16 {
return Err(AttachmentError::GidLength(len));
}
let mut gid = [0_u8; 16];
gid.copy_from_slice(r.take(16)?);
if !r.0.is_empty() {
return Err(AttachmentError::Trailing(r.0.len()));
}
let format = if legacy {
AttachmentFormat::Legacy
} else {
AttachmentFormat::Current
};
Ok((
Self {
sequence_number,
source_timestamp,
source_gid: Gid(gid),
},
format,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> Attachment {
Attachment {
sequence_number: 1,
source_timestamp: 0x0102_0304_0506_0708,
source_gid: Gid([0xab; 16]),
}
}
#[test]
fn current_layout_and_round_trip() {
let a = sample();
let bytes = a.encode();
assert_eq!(bytes.len(), ATTACHMENT_LEN);
assert_eq!(&bytes[0..8], &1_i64.to_le_bytes());
assert_eq!(&bytes[8..16], &0x0102_0304_0506_0708_i64.to_le_bytes());
assert_eq!(bytes[16], 16);
assert_eq!(&bytes[17..], &[0xab; 16]);
assert_eq!(
Attachment::decode_format(&bytes),
Ok((a, AttachmentFormat::Current))
);
}
#[test]
fn legacy_layout_and_round_trip() {
let a = sample();
let bytes = a.encode_as(AttachmentFormat::Legacy);
assert_eq!(bytes.len(), LEGACY_ATTACHMENT_LEN);
assert_eq!(bytes[0], 15);
assert_eq!(&bytes[1..16], b"sequence_number");
assert_eq!(&bytes[16..24], &1_i64.to_le_bytes());
assert_eq!(bytes[24], 16);
assert_eq!(&bytes[25..41], b"source_timestamp");
assert_eq!(bytes[49], 10);
assert_eq!(&bytes[50..60], b"source_gid");
assert_eq!(bytes[60], 16);
assert_eq!(
Attachment::decode_format(&bytes),
Ok((a, AttachmentFormat::Legacy))
);
}
#[test]
fn rejects_malformed() {
let bytes = Attachment::now(3, Gid::default()).encode();
assert_eq!(
Attachment::decode(&bytes[..20]),
Err(AttachmentError::Truncated)
);
let mut long = bytes.clone();
long.push(0);
assert_eq!(Attachment::decode(&long), Err(AttachmentError::Trailing(1)));
let mut bad = bytes;
bad[16] = 15;
assert_eq!(
Attachment::decode(&bad),
Err(AttachmentError::GidLength(15))
);
let legacy = Attachment::now(3, Gid::default()).encode_as(AttachmentFormat::Legacy);
let mut bad = legacy.clone();
bad[20 + 5] = b'x';
assert!(matches!(
Attachment::decode(&bad),
Err(AttachmentError::UnexpectedField { .. })
));
let mut bad = legacy;
bad[60] = 15;
assert_eq!(
Attachment::decode(&bad),
Err(AttachmentError::GidLength(15))
);
}
}