Skip to main content

par2_rs/packet/
main.rs

1use crate::error::{Par2Error, Result};
2use crate::types::{FileId, MAX_FILES_PER_SET, RecoverySetId};
3
4/// Parsed Main packet.
5///
6/// Contains the slice size and lists of file IDs for recovery and non-recovery files.
7/// The recovery_set_id from the header equals the MD5 of this packet's body.
8#[derive(Debug, Clone)]
9pub struct MainPacket {
10    /// The recovery set ID (from the packet header).
11    pub recovery_set_id: RecoverySetId,
12    /// Block/slice size in bytes.
13    pub slice_size: u64,
14    /// File IDs of files in the recovery set.
15    pub recovery_file_ids: Vec<FileId>,
16    /// File IDs of files NOT in the recovery set.
17    pub non_recovery_file_ids: Vec<FileId>,
18}
19
20impl MainPacket {
21    /// Parse a Main packet from its body bytes (everything after the 64-byte header).
22    pub fn parse(body: &[u8], recovery_set_id: RecoverySetId) -> Result<Self> {
23        // Minimum body: 8 (slice_size) + 4 (count) = 12
24        if body.len() < 12 {
25            return Err(Par2Error::InvalidMainPacket {
26                reason: format!("body too short: {} bytes, need at least 12", body.len()),
27            });
28        }
29
30        let slice_size = u64::from_le_bytes(body[0..8].try_into().unwrap());
31        if slice_size == 0 {
32            return Err(Par2Error::InvalidMainPacket {
33                reason: "slice_size is 0".to_string(),
34            });
35        }
36        if slice_size % 4 != 0 {
37            return Err(Par2Error::InvalidMainPacket {
38                reason: format!("slice_size {slice_size} is not a multiple of 4"),
39            });
40        }
41
42        let recovery_file_count = u32::from_le_bytes(body[8..12].try_into().unwrap()) as usize;
43
44        let file_id_area = &body[12..];
45        if !file_id_area.len().is_multiple_of(16) {
46            return Err(Par2Error::InvalidMainPacket {
47                reason: format!(
48                    "file ID area length {} is not a multiple of 16",
49                    file_id_area.len()
50                ),
51            });
52        }
53
54        let total_ids = file_id_area.len() / 16;
55        if total_ids > MAX_FILES_PER_SET {
56            return Err(Par2Error::InvalidMainPacket {
57                reason: format!("file ID count {total_ids} exceeds maximum {MAX_FILES_PER_SET}"),
58            });
59        }
60        if recovery_file_count > total_ids {
61            return Err(Par2Error::InvalidMainPacket {
62                reason: format!(
63                    "recovery_file_count ({recovery_file_count}) exceeds total file IDs ({total_ids})"
64                ),
65            });
66        }
67
68        let mut recovery_file_ids = Vec::with_capacity(recovery_file_count);
69        for i in 0..recovery_file_count {
70            let offset = i * 16;
71            let id: [u8; 16] = file_id_area[offset..offset + 16].try_into().unwrap();
72            recovery_file_ids.push(FileId::from_bytes(id));
73        }
74
75        let non_recovery_count = total_ids - recovery_file_count;
76        let mut non_recovery_file_ids = Vec::with_capacity(non_recovery_count);
77        for i in recovery_file_count..total_ids {
78            let offset = i * 16;
79            let id: [u8; 16] = file_id_area[offset..offset + 16].try_into().unwrap();
80            non_recovery_file_ids.push(FileId::from_bytes(id));
81        }
82
83        Ok(MainPacket {
84            recovery_set_id,
85            slice_size,
86            recovery_file_ids,
87            non_recovery_file_ids,
88        })
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    fn make_main_body(
97        slice_size: u64,
98        recovery_ids: &[[u8; 16]],
99        non_recovery_ids: &[[u8; 16]],
100    ) -> Vec<u8> {
101        let mut body = Vec::new();
102        body.extend_from_slice(&slice_size.to_le_bytes());
103        body.extend_from_slice(&(recovery_ids.len() as u32).to_le_bytes());
104        for id in recovery_ids {
105            body.extend_from_slice(id);
106        }
107        for id in non_recovery_ids {
108            body.extend_from_slice(id);
109        }
110        body
111    }
112
113    #[test]
114    fn parse_valid_main() {
115        let file_a = [0x01u8; 16];
116        let file_b = [0x02u8; 16];
117        let file_c = [0x03u8; 16];
118        let body = make_main_body(65536, &[file_a, file_b], &[file_c]);
119        let rsid = RecoverySetId::from_bytes([0xFF; 16]);
120
121        let pkt = MainPacket::parse(&body, rsid).unwrap();
122        assert_eq!(pkt.slice_size, 65536);
123        assert_eq!(pkt.recovery_file_ids.len(), 2);
124        assert_eq!(pkt.non_recovery_file_ids.len(), 1);
125        assert_eq!(*pkt.recovery_file_ids[0].as_bytes(), file_a);
126        assert_eq!(*pkt.recovery_file_ids[1].as_bytes(), file_b);
127        assert_eq!(*pkt.non_recovery_file_ids[0].as_bytes(), file_c);
128    }
129
130    #[test]
131    fn parse_main_no_non_recovery() {
132        let file_a = [0x01u8; 16];
133        let body = make_main_body(1024, &[file_a], &[]);
134        let pkt = MainPacket::parse(&body, RecoverySetId::from_bytes([0; 16])).unwrap();
135        assert_eq!(pkt.recovery_file_ids.len(), 1);
136        assert_eq!(pkt.non_recovery_file_ids.len(), 0);
137    }
138
139    #[test]
140    fn parse_main_no_files() {
141        let body = make_main_body(4096, &[], &[]);
142        let pkt = MainPacket::parse(&body, RecoverySetId::from_bytes([0; 16])).unwrap();
143        assert_eq!(pkt.recovery_file_ids.len(), 0);
144        assert_eq!(pkt.non_recovery_file_ids.len(), 0);
145    }
146
147    #[test]
148    fn reject_zero_slice_size() {
149        let body = make_main_body(0, &[], &[]);
150        let err = MainPacket::parse(&body, RecoverySetId::from_bytes([0; 16])).unwrap_err();
151        assert!(matches!(err, Par2Error::InvalidMainPacket { .. }));
152    }
153
154    #[test]
155    fn reject_non_aligned_slice_size() {
156        let body = make_main_body(7, &[], &[]);
157        let err = MainPacket::parse(&body, RecoverySetId::from_bytes([0; 16])).unwrap_err();
158        assert!(matches!(err, Par2Error::InvalidMainPacket { .. }));
159    }
160
161    #[test]
162    fn reject_body_too_short() {
163        let body = [0u8; 8];
164        let err = MainPacket::parse(&body, RecoverySetId::from_bytes([0; 16])).unwrap_err();
165        assert!(matches!(err, Par2Error::InvalidMainPacket { .. }));
166    }
167
168    #[test]
169    fn reject_count_exceeds_ids() {
170        // Claims 5 recovery file IDs but only provides 2 total
171        let mut body = Vec::new();
172        body.extend_from_slice(&1024u64.to_le_bytes());
173        body.extend_from_slice(&5u32.to_le_bytes());
174        body.extend_from_slice(&[0u8; 32]); // 2 file IDs
175        let err = MainPacket::parse(&body, RecoverySetId::from_bytes([0; 16])).unwrap_err();
176        assert!(matches!(err, Par2Error::InvalidMainPacket { .. }));
177    }
178}