use crate::bytes::Bytes;
use crate::security::tag::TAG_SIZE;
pub const ATP_QUIC_SYMBOL_MAGIC: u32 = 0x4154_5153;
pub const ENVELOPE_HEADER_LEN: usize = 4 + 8 + 4 + 1 + 4 + 1 + 2;
pub const AUTH_ENVELOPE_HEADER_LEN: usize = ENVELOPE_HEADER_LEN + TAG_SIZE;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum QuicSymbolEnvelopeError {
#[error("symbol envelope too short: have {have} bytes, need at least {need}")]
TooShort {
have: usize,
need: usize,
},
#[error(
"symbol envelope bad magic: found {found:#010x}, expected {ATP_QUIC_SYMBOL_MAGIC:#010x}"
)]
BadMagic {
found: u32,
},
#[error("symbol envelope invalid repair flag byte: {byte}")]
InvalidRepairFlag {
byte: u8,
},
#[error("symbol envelope length mismatch: declared {declared}, available {available}")]
LengthMismatch {
declared: usize,
available: usize,
},
#[error("symbol payload too large to encode: {len} bytes (max {max})", max = u16::MAX)]
PayloadTooLarge {
len: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QuicSymbolEnvelope {
pub transfer_tag: u64,
pub entry: u32,
pub sbn: u8,
pub esi: u32,
pub is_repair: bool,
pub auth_tag: Option<[u8; TAG_SIZE]>,
pub payload: Bytes,
}
impl QuicSymbolEnvelope {
#[must_use]
pub fn header_len(&self) -> usize {
if self.auth_tag.is_some() {
AUTH_ENVELOPE_HEADER_LEN
} else {
ENVELOPE_HEADER_LEN
}
}
#[must_use]
pub fn encoded_len(&self) -> usize {
self.header_len() + self.payload.len()
}
pub fn encode(&self) -> Result<Bytes, QuicSymbolEnvelopeError> {
let payload_len = u16::try_from(self.payload.len()).map_err(|_| {
QuicSymbolEnvelopeError::PayloadTooLarge {
len: self.payload.len(),
}
})?;
let mut out = Vec::with_capacity(self.encoded_len());
out.extend_from_slice(&ATP_QUIC_SYMBOL_MAGIC.to_be_bytes());
out.extend_from_slice(&self.transfer_tag.to_be_bytes());
out.extend_from_slice(&self.entry.to_be_bytes());
out.push(self.sbn);
out.extend_from_slice(&self.esi.to_be_bytes());
out.push(u8::from(self.is_repair));
out.extend_from_slice(&payload_len.to_be_bytes());
if let Some(tag) = &self.auth_tag {
out.extend_from_slice(tag);
}
out.extend_from_slice(&self.payload);
Ok(Bytes::from(out))
}
pub fn decode(buf: &[u8], auth_required: bool) -> Result<Self, QuicSymbolEnvelopeError> {
Self::decode_bytes(Bytes::copy_from_slice(buf), auth_required)
}
pub fn decode_bytes(buf: Bytes, auth_required: bool) -> Result<Self, QuicSymbolEnvelopeError> {
let header_len = if auth_required {
AUTH_ENVELOPE_HEADER_LEN
} else {
ENVELOPE_HEADER_LEN
};
if buf.len() < header_len {
return Err(QuicSymbolEnvelopeError::TooShort {
have: buf.len(),
need: header_len,
});
}
let magic = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
if magic != ATP_QUIC_SYMBOL_MAGIC {
return Err(QuicSymbolEnvelopeError::BadMagic { found: magic });
}
let transfer_tag = u64::from_be_bytes([
buf[4], buf[5], buf[6], buf[7], buf[8], buf[9], buf[10], buf[11],
]);
let entry = u32::from_be_bytes([buf[12], buf[13], buf[14], buf[15]]);
let sbn = buf[16];
let esi = u32::from_be_bytes([buf[17], buf[18], buf[19], buf[20]]);
let is_repair = match buf[21] {
0 => false,
1 => true,
byte => return Err(QuicSymbolEnvelopeError::InvalidRepairFlag { byte }),
};
let declared = usize::from(u16::from_be_bytes([buf[22], buf[23]]));
let available = buf.len() - header_len;
if declared != available {
return Err(QuicSymbolEnvelopeError::LengthMismatch {
declared,
available,
});
}
let auth_tag = if auth_required {
let mut tag = [0u8; TAG_SIZE];
tag.copy_from_slice(&buf[ENVELOPE_HEADER_LEN..AUTH_ENVELOPE_HEADER_LEN]);
Some(tag)
} else {
None
};
let payload = buf.slice(header_len..);
Ok(Self {
transfer_tag,
entry,
sbn,
esi,
is_repair,
auth_tag,
payload,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample(auth: bool) -> QuicSymbolEnvelope {
QuicSymbolEnvelope {
transfer_tag: 0x0102_0304_0506_0708,
entry: 7,
sbn: 3,
esi: 12345,
is_repair: true,
auth_tag: auth.then_some([0xABu8; TAG_SIZE]),
payload: Bytes::from_static(b"the-raptorq-symbol-bytes"),
}
}
#[test]
fn roundtrip_without_auth() {
let env = sample(false);
let bytes = env.encode().unwrap();
assert_eq!(bytes.len(), env.encoded_len());
let back = QuicSymbolEnvelope::decode(&bytes, false).unwrap();
assert_eq!(env, back);
}
#[test]
fn roundtrip_with_auth() {
let env = sample(true);
let bytes = env.encode().unwrap();
assert_eq!(bytes.len(), AUTH_ENVELOPE_HEADER_LEN + env.payload.len());
let back = QuicSymbolEnvelope::decode(&bytes, true).unwrap();
assert_eq!(env, back);
assert_eq!(back.auth_tag, Some([0xABu8; TAG_SIZE]));
}
#[test]
fn header_layout_is_golden() {
let env = sample(false);
let b = env.encode().unwrap();
assert_eq!(&b[0..4], &ATP_QUIC_SYMBOL_MAGIC.to_be_bytes());
assert_eq!(&b[4..12], &0x0102_0304_0506_0708u64.to_be_bytes());
assert_eq!(&b[12..16], &7u32.to_be_bytes());
assert_eq!(b[16], 3); assert_eq!(&b[17..21], &12345u32.to_be_bytes());
assert_eq!(b[21], 1); assert_eq!(&b[22..24], &(env.payload.len() as u16).to_be_bytes());
assert_eq!(&b[ENVELOPE_HEADER_LEN..], env.payload.as_ref());
}
#[test]
fn empty_payload_roundtrips() {
let mut env = sample(false);
env.payload = Bytes::new();
let b = env.encode().unwrap();
assert_eq!(b.len(), ENVELOPE_HEADER_LEN);
assert_eq!(QuicSymbolEnvelope::decode(&b, false).unwrap(), env);
}
#[test]
fn decode_rejects_short_buffer() {
let err = QuicSymbolEnvelope::decode(&[0u8; 10], false).unwrap_err();
assert!(
matches!(err, QuicSymbolEnvelopeError::TooShort { need, .. } if need == ENVELOPE_HEADER_LEN)
);
}
#[test]
fn decode_rejects_bad_magic() {
let mut b = sample(false).encode().unwrap().to_vec();
b[0] ^= 0xFF;
assert!(matches!(
QuicSymbolEnvelope::decode(&b, false),
Err(QuicSymbolEnvelopeError::BadMagic { .. })
));
}
#[test]
fn decode_rejects_length_mismatch() {
let b = sample(false).encode().unwrap();
let truncated = &b[..b.len() - 1];
assert!(matches!(
QuicSymbolEnvelope::decode(truncated, false),
Err(QuicSymbolEnvelopeError::LengthMismatch { .. })
));
}
#[test]
fn decode_rejects_invalid_repair_flag() {
let mut b = sample(false).encode().unwrap().to_vec();
b[21] = 2;
assert!(matches!(
QuicSymbolEnvelope::decode(&b, false),
Err(QuicSymbolEnvelopeError::InvalidRepairFlag { byte: 2 })
));
}
#[test]
fn auth_posture_mismatch_fails_closed() {
let b = sample(true).encode().unwrap();
assert!(QuicSymbolEnvelope::decode(&b, false).is_err());
let b2 = sample(false).encode().unwrap();
assert!(QuicSymbolEnvelope::decode(&b2, true).is_err());
}
#[test]
fn metamorphic_field_change_changes_bytes() {
let base = sample(false).encode().unwrap();
let mut other = sample(false);
other.esi += 1;
assert_ne!(base, other.encode().unwrap());
}
}