use crate::checksum;
use crate::error::{Par2Error, Result};
use crate::types::RecoverySetId;
pub const MAGIC: &[u8; 8] = b"PAR2\x00PKT";
pub const HEADER_SIZE: usize = 64;
pub const TYPE_MAIN: &[u8; 16] = b"PAR 2.0\x00Main\x00\x00\x00\x00";
pub const TYPE_FILE_DESC: &[u8; 16] = b"PAR 2.0\x00FileDesc";
pub const TYPE_IFSC: &[u8; 16] = b"PAR 2.0\x00IFSC\x00\x00\x00\x00";
pub const TYPE_RECOVERY: &[u8; 16] = b"PAR 2.0\x00RecvSlic";
pub const TYPE_CREATOR: &[u8; 16] = b"PAR 2.0\x00Creator\x00";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PacketType {
Main,
FileDescription,
InputFileSliceChecksum,
RecoverySlice,
Creator,
Unknown([u8; 16]),
}
impl PacketType {
pub fn from_signature(sig: &[u8; 16]) -> Self {
match sig {
s if s == TYPE_MAIN => PacketType::Main,
s if s == TYPE_FILE_DESC => PacketType::FileDescription,
s if s == TYPE_IFSC => PacketType::InputFileSliceChecksum,
s if s == TYPE_RECOVERY => PacketType::RecoverySlice,
s if s == TYPE_CREATOR => PacketType::Creator,
other => PacketType::Unknown(*other),
}
}
}
#[derive(Debug, Clone)]
pub struct PacketHeader {
pub length: u64,
pub packet_hash: [u8; 16],
pub recovery_set_id: RecoverySetId,
pub packet_type: PacketType,
}
impl PacketHeader {
pub fn parse(data: &[u8], offset: u64) -> Result<Self> {
if data.len() < HEADER_SIZE {
return Err(Par2Error::PacketTooShort {
expected: HEADER_SIZE as u64,
actual: data.len() as u64,
});
}
if &data[0..8] != MAGIC {
return Err(Par2Error::InvalidMagic { offset });
}
let length = u64::from_le_bytes(data[8..16].try_into().unwrap());
if length < HEADER_SIZE as u64 {
return Err(Par2Error::PacketTooShort {
expected: HEADER_SIZE as u64,
actual: length,
});
}
if length % 4 != 0 {
return Err(Par2Error::InvalidPacketLength { length });
}
let packet_hash: [u8; 16] = data[16..32].try_into().unwrap();
let recovery_set_id = RecoverySetId::from_bytes(data[32..48].try_into().unwrap());
let type_sig: [u8; 16] = data[48..64].try_into().unwrap();
let packet_type = PacketType::from_signature(&type_sig);
Ok(PacketHeader {
length,
packet_hash,
recovery_set_id,
packet_type,
})
}
pub fn validate_hash(&self, full_packet: &[u8], offset: u64) -> Result<()> {
if (full_packet.len() as u64) < self.length {
return Err(Par2Error::PacketTooShort {
expected: self.length,
actual: full_packet.len() as u64,
});
}
let hash_input = &full_packet[32..self.length as usize];
let computed = checksum::md5(hash_input);
if computed != self.packet_hash {
return Err(Par2Error::PacketHashMismatch { offset });
}
Ok(())
}
pub fn body_length(&self) -> u64 {
self.length - HEADER_SIZE as u64
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_header(packet_type: &[u8; 16], body: &[u8]) -> Vec<u8> {
let length = (HEADER_SIZE + body.len()) as u64;
let recovery_set_id = [0xAAu8; 16];
let type_sig = packet_type;
let mut hash_input = Vec::new();
hash_input.extend_from_slice(&recovery_set_id);
hash_input.extend_from_slice(type_sig);
hash_input.extend_from_slice(body);
let packet_hash = checksum::md5(&hash_input);
let mut data = Vec::new();
data.extend_from_slice(MAGIC);
data.extend_from_slice(&length.to_le_bytes());
data.extend_from_slice(&packet_hash);
data.extend_from_slice(&recovery_set_id);
data.extend_from_slice(type_sig);
data.extend_from_slice(body);
data
}
#[test]
fn parse_valid_header() {
let data = make_header(TYPE_MAIN, &[0u8; 0]);
let header = PacketHeader::parse(&data, 0).unwrap();
assert_eq!(header.length, HEADER_SIZE as u64);
assert_eq!(header.packet_type, PacketType::Main);
assert_eq!(
header.recovery_set_id,
RecoverySetId::from_bytes([0xAA; 16])
);
}
#[test]
fn parse_all_packet_types() {
for (sig, expected) in [
(TYPE_MAIN, PacketType::Main),
(TYPE_FILE_DESC, PacketType::FileDescription),
(TYPE_IFSC, PacketType::InputFileSliceChecksum),
(TYPE_RECOVERY, PacketType::RecoverySlice),
(TYPE_CREATOR, PacketType::Creator),
] {
let data = make_header(sig, &[]);
let header = PacketHeader::parse(&data, 0).unwrap();
assert_eq!(header.packet_type, expected);
}
}
#[test]
fn unknown_packet_type() {
let custom = b"PAR 2.0\x00CustomXX";
let data = make_header(custom, &[]);
let header = PacketHeader::parse(&data, 0).unwrap();
assert_eq!(header.packet_type, PacketType::Unknown(*custom));
}
#[test]
fn validate_hash_succeeds() {
let data = make_header(TYPE_CREATOR, b"test body data!!");
let header = PacketHeader::parse(&data, 0).unwrap();
header.validate_hash(&data, 0).unwrap();
}
#[test]
fn validate_hash_fails_on_corruption() {
let mut data = make_header(TYPE_CREATOR, b"test body data!!");
let last = data.len() - 1;
data[last] ^= 0xFF;
let header = PacketHeader::parse(&data, 42).unwrap();
let err = header.validate_hash(&data, 42).unwrap_err();
assert!(matches!(err, Par2Error::PacketHashMismatch { offset: 42 }));
}
#[test]
fn reject_bad_magic() {
let mut data = make_header(TYPE_MAIN, &[]);
data[0] = b'X';
let err = PacketHeader::parse(&data, 100).unwrap_err();
assert!(matches!(err, Par2Error::InvalidMagic { offset: 100 }));
}
#[test]
fn reject_too_short() {
let data = [0u8; 32];
let err = PacketHeader::parse(&data, 0).unwrap_err();
assert!(matches!(err, Par2Error::PacketTooShort { .. }));
}
#[test]
fn reject_non_aligned_length() {
let mut data = make_header(TYPE_MAIN, &[]);
let bad_len = 65u64;
data[8..16].copy_from_slice(&bad_len.to_le_bytes());
let err = PacketHeader::parse(&data, 0).unwrap_err();
assert!(matches!(err, Par2Error::InvalidPacketLength { length: 65 }));
}
#[test]
fn reject_length_too_small() {
let mut data = make_header(TYPE_MAIN, &[]);
let bad_len = 60u64;
data[8..16].copy_from_slice(&bad_len.to_le_bytes());
let err = PacketHeader::parse(&data, 0).unwrap_err();
assert!(matches!(err, Par2Error::PacketTooShort { .. }));
}
#[test]
fn body_length_calculation() {
let body = [0u8; 32];
let data = make_header(TYPE_MAIN, &body);
let header = PacketHeader::parse(&data, 0).unwrap();
assert_eq!(header.body_length(), 32);
}
#[test]
fn accept_large_packet_length() {
let mut data = make_header(TYPE_MAIN, &[]);
let huge_len = (2u64 << 30) & !3; data[8..16].copy_from_slice(&huge_len.to_le_bytes());
let header = PacketHeader::parse(&data, 0).unwrap();
assert_eq!(header.length, huge_len);
}
}