Skip to main content

par2_rs/packet/
header.rs

1use crate::checksum;
2use crate::error::{Par2Error, Result};
3use crate::types::RecoverySetId;
4
5/// The 8-byte magic sequence that begins every PAR2 packet.
6pub const MAGIC: &[u8; 8] = b"PAR2\x00PKT";
7
8/// Minimum packet size: 64-byte header.
9pub const HEADER_SIZE: usize = 64;
10
11/// 16-byte packet type signatures.
12pub const TYPE_MAIN: &[u8; 16] = b"PAR 2.0\x00Main\x00\x00\x00\x00";
13pub const TYPE_FILE_DESC: &[u8; 16] = b"PAR 2.0\x00FileDesc";
14pub const TYPE_IFSC: &[u8; 16] = b"PAR 2.0\x00IFSC\x00\x00\x00\x00";
15pub const TYPE_RECOVERY: &[u8; 16] = b"PAR 2.0\x00RecvSlic";
16pub const TYPE_CREATOR: &[u8; 16] = b"PAR 2.0\x00Creator\x00";
17
18/// The type of a PAR2 packet, determined by the 16-byte type signature.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum PacketType {
21    Main,
22    FileDescription,
23    InputFileSliceChecksum,
24    RecoverySlice,
25    Creator,
26    Unknown([u8; 16]),
27}
28
29impl PacketType {
30    pub fn from_signature(sig: &[u8; 16]) -> Self {
31        match sig {
32            s if s == TYPE_MAIN => PacketType::Main,
33            s if s == TYPE_FILE_DESC => PacketType::FileDescription,
34            s if s == TYPE_IFSC => PacketType::InputFileSliceChecksum,
35            s if s == TYPE_RECOVERY => PacketType::RecoverySlice,
36            s if s == TYPE_CREATOR => PacketType::Creator,
37            other => PacketType::Unknown(*other),
38        }
39    }
40}
41
42/// Parsed 64-byte packet header.
43#[derive(Debug, Clone)]
44pub struct PacketHeader {
45    /// Total packet length (header + body), always >= 64 and multiple of 4.
46    pub length: u64,
47    /// MD5 of bytes 32..length (recovery_set_id + type + body).
48    pub packet_hash: [u8; 16],
49    /// Recovery set this packet belongs to.
50    pub recovery_set_id: RecoverySetId,
51    /// The packet type.
52    pub packet_type: PacketType,
53}
54
55impl PacketHeader {
56    /// Parse a 64-byte header from raw bytes.
57    ///
58    /// `offset` is used only for error reporting (position in file/stream).
59    /// This does NOT validate the packet hash (needs the full packet body).
60    pub fn parse(data: &[u8], offset: u64) -> Result<Self> {
61        if data.len() < HEADER_SIZE {
62            return Err(Par2Error::PacketTooShort {
63                expected: HEADER_SIZE as u64,
64                actual: data.len() as u64,
65            });
66        }
67
68        // Validate magic
69        if &data[0..8] != MAGIC {
70            return Err(Par2Error::InvalidMagic { offset });
71        }
72
73        // Parse length
74        let length = u64::from_le_bytes(data[8..16].try_into().unwrap());
75        if length < HEADER_SIZE as u64 {
76            return Err(Par2Error::PacketTooShort {
77                expected: HEADER_SIZE as u64,
78                actual: length,
79            });
80        }
81        if length % 4 != 0 {
82            return Err(Par2Error::InvalidPacketLength { length });
83        }
84        // Extract fields
85        let packet_hash: [u8; 16] = data[16..32].try_into().unwrap();
86        let recovery_set_id = RecoverySetId::from_bytes(data[32..48].try_into().unwrap());
87        let type_sig: [u8; 16] = data[48..64].try_into().unwrap();
88        let packet_type = PacketType::from_signature(&type_sig);
89
90        Ok(PacketHeader {
91            length,
92            packet_hash,
93            recovery_set_id,
94            packet_type,
95        })
96    }
97
98    /// Validate the packet hash against the full packet data (bytes 0..length).
99    ///
100    /// The hash covers bytes 32..length (recovery_set_id + type + body).
101    pub fn validate_hash(&self, full_packet: &[u8], offset: u64) -> Result<()> {
102        if (full_packet.len() as u64) < self.length {
103            return Err(Par2Error::PacketTooShort {
104                expected: self.length,
105                actual: full_packet.len() as u64,
106            });
107        }
108
109        let hash_input = &full_packet[32..self.length as usize];
110        let computed = checksum::md5(hash_input);
111
112        if computed != self.packet_hash {
113            return Err(Par2Error::PacketHashMismatch { offset });
114        }
115
116        Ok(())
117    }
118
119    /// Size of the packet body (everything after the 64-byte header).
120    pub fn body_length(&self) -> u64 {
121        self.length - HEADER_SIZE as u64
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    /// Build a valid 64-byte header with a zero-length body.
130    fn make_header(packet_type: &[u8; 16], body: &[u8]) -> Vec<u8> {
131        let length = (HEADER_SIZE + body.len()) as u64;
132        let recovery_set_id = [0xAAu8; 16];
133        let type_sig = packet_type;
134
135        // Build bytes 32..length for hashing
136        let mut hash_input = Vec::new();
137        hash_input.extend_from_slice(&recovery_set_id);
138        hash_input.extend_from_slice(type_sig);
139        hash_input.extend_from_slice(body);
140
141        let packet_hash = checksum::md5(&hash_input);
142
143        let mut data = Vec::new();
144        data.extend_from_slice(MAGIC);
145        data.extend_from_slice(&length.to_le_bytes());
146        data.extend_from_slice(&packet_hash);
147        data.extend_from_slice(&recovery_set_id);
148        data.extend_from_slice(type_sig);
149        data.extend_from_slice(body);
150        data
151    }
152
153    #[test]
154    fn parse_valid_header() {
155        let data = make_header(TYPE_MAIN, &[0u8; 0]);
156        let header = PacketHeader::parse(&data, 0).unwrap();
157        assert_eq!(header.length, HEADER_SIZE as u64);
158        assert_eq!(header.packet_type, PacketType::Main);
159        assert_eq!(
160            header.recovery_set_id,
161            RecoverySetId::from_bytes([0xAA; 16])
162        );
163    }
164
165    #[test]
166    fn parse_all_packet_types() {
167        for (sig, expected) in [
168            (TYPE_MAIN, PacketType::Main),
169            (TYPE_FILE_DESC, PacketType::FileDescription),
170            (TYPE_IFSC, PacketType::InputFileSliceChecksum),
171            (TYPE_RECOVERY, PacketType::RecoverySlice),
172            (TYPE_CREATOR, PacketType::Creator),
173        ] {
174            let data = make_header(sig, &[]);
175            let header = PacketHeader::parse(&data, 0).unwrap();
176            assert_eq!(header.packet_type, expected);
177        }
178    }
179
180    #[test]
181    fn unknown_packet_type() {
182        let custom = b"PAR 2.0\x00CustomXX";
183        let data = make_header(custom, &[]);
184        let header = PacketHeader::parse(&data, 0).unwrap();
185        assert_eq!(header.packet_type, PacketType::Unknown(*custom));
186    }
187
188    #[test]
189    fn validate_hash_succeeds() {
190        let data = make_header(TYPE_CREATOR, b"test body data!!");
191        let header = PacketHeader::parse(&data, 0).unwrap();
192        header.validate_hash(&data, 0).unwrap();
193    }
194
195    #[test]
196    fn validate_hash_fails_on_corruption() {
197        let mut data = make_header(TYPE_CREATOR, b"test body data!!");
198        // Corrupt a body byte
199        let last = data.len() - 1;
200        data[last] ^= 0xFF;
201        let header = PacketHeader::parse(&data, 42).unwrap();
202        let err = header.validate_hash(&data, 42).unwrap_err();
203        assert!(matches!(err, Par2Error::PacketHashMismatch { offset: 42 }));
204    }
205
206    #[test]
207    fn reject_bad_magic() {
208        let mut data = make_header(TYPE_MAIN, &[]);
209        data[0] = b'X';
210        let err = PacketHeader::parse(&data, 100).unwrap_err();
211        assert!(matches!(err, Par2Error::InvalidMagic { offset: 100 }));
212    }
213
214    #[test]
215    fn reject_too_short() {
216        let data = [0u8; 32];
217        let err = PacketHeader::parse(&data, 0).unwrap_err();
218        assert!(matches!(err, Par2Error::PacketTooShort { .. }));
219    }
220
221    #[test]
222    fn reject_non_aligned_length() {
223        let mut data = make_header(TYPE_MAIN, &[]);
224        // Set length to 65 (not multiple of 4)
225        let bad_len = 65u64;
226        data[8..16].copy_from_slice(&bad_len.to_le_bytes());
227        let err = PacketHeader::parse(&data, 0).unwrap_err();
228        assert!(matches!(err, Par2Error::InvalidPacketLength { length: 65 }));
229    }
230
231    #[test]
232    fn reject_length_too_small() {
233        let mut data = make_header(TYPE_MAIN, &[]);
234        // Set length to 60 (< 64)
235        let bad_len = 60u64;
236        data[8..16].copy_from_slice(&bad_len.to_le_bytes());
237        let err = PacketHeader::parse(&data, 0).unwrap_err();
238        assert!(matches!(err, Par2Error::PacketTooShort { .. }));
239    }
240
241    #[test]
242    fn body_length_calculation() {
243        let body = [0u8; 32];
244        let data = make_header(TYPE_MAIN, &body);
245        let header = PacketHeader::parse(&data, 0).unwrap();
246        assert_eq!(header.body_length(), 32);
247    }
248
249    #[test]
250    fn accept_large_packet_length() {
251        let mut data = make_header(TYPE_MAIN, &[]);
252        let huge_len = (2u64 << 30) & !3; // aligned to 4
253        data[8..16].copy_from_slice(&huge_len.to_le_bytes());
254        let header = PacketHeader::parse(&data, 0).unwrap();
255        assert_eq!(header.length, huge_len);
256    }
257}