1use std::collections::HashMap;
18use std::io::{self, Read, Seek, SeekFrom};
19use std::path::Path;
20
21use md5::{Digest, Md5};
22use tracing::{debug, trace, warn};
23
24use crate::types::{Id16, Md5Hash, Par2File, Par2FileSet, SliceChecksum};
25
26const PAR2_MAGIC: &[u8; 8] = b"PAR2\x00PKT";
28
29pub const MAGIC: &[u8; 8] = PAR2_MAGIC;
31pub const HEADER_SIZE: usize = 64;
32
33const MIN_PACKET_LEN: u64 = 64;
35
36const TYPE_MAIN: &[u8; 16] = b"PAR 2.0\x00Main\x00\x00\x00\x00";
38const TYPE_FILE_DESC: &[u8; 16] = b"PAR 2.0\x00FileDesc";
39const TYPE_IFSC: &[u8; 16] = b"PAR 2.0\x00IFSC\x00\x00\x00\x00";
40const TYPE_RECOVERY: &[u8; 16] = b"PAR 2.0\x00RecvSlic";
41const TYPE_CREATOR: &[u8; 16] = b"PAR 2.0\x00Creator\x00";
42
43#[derive(Debug, thiserror::Error)]
45pub enum ParseError {
46 #[error("I/O error: {0}")]
47 Io(#[from] io::Error),
48 #[error("no PAR2 packets found in file")]
49 NoPar2Packets,
50 #[error("missing Main packet — cannot determine slice size")]
51 NoMainPacket,
52}
53
54struct ParseState {
56 recovery_set_id: Option<Id16>,
57 slice_size: Option<u64>,
58 nr_files: Option<u32>,
59 file_order: Vec<Id16>,
60 file_descs: HashMap<Id16, FileDescData>,
62 ifsc_data: HashMap<Id16, Vec<SliceChecksum>>,
64 recovery_count: u32,
66 creator: Option<String>,
68}
69
70struct FileDescData {
71 hash: Md5Hash,
72 hash_16k: Md5Hash,
73 size: u64,
74 filename: String,
75}
76
77pub fn parse_par2_file(path: &Path) -> Result<Par2FileSet, ParseError> {
83 let file = std::fs::File::open(path)?;
84 let file_size = file.metadata()?.len();
85 let mut reader = io::BufReader::new(file);
86
87 parse_par2_reader(&mut reader, file_size)
88}
89
90pub fn parse_par2_reader<R: Read + Seek>(
92 reader: &mut R,
93 file_size: u64,
94) -> Result<Par2FileSet, ParseError> {
95 let mut state = ParseState {
96 recovery_set_id: None,
97 slice_size: None,
98 nr_files: None,
99 file_order: Vec::new(),
100 file_descs: HashMap::new(),
101 ifsc_data: HashMap::new(),
102 recovery_count: 0,
103 creator: None,
104 };
105
106 let mut magic_buf = [0u8; 8];
107 let mut packets_parsed = 0u32;
108
109 loop {
110 let pos = reader.stream_position()?;
111 if pos >= file_size {
112 break;
113 }
114
115 if reader.read_exact(&mut magic_buf).is_err() {
117 break;
118 }
119
120 if magic_buf != *PAR2_MAGIC {
121 if let Some(next_pos) = scan_for_magic(reader, file_size)? {
124 reader.seek(SeekFrom::Start(next_pos))?;
125 continue;
126 }
127 break;
128 }
129
130 let mut len_buf = [0u8; 8];
132 if reader.read_exact(&mut len_buf).is_err() {
133 break;
134 }
135 let packet_len = u64::from_le_bytes(len_buf);
136
137 if packet_len < MIN_PACKET_LEN || packet_len % 4 != 0 {
139 warn!(packet_len, pos, "invalid PAR2 packet length, skipping");
140 continue;
141 }
142
143 let body_len = packet_len - 16; if body_len > 10 * 1024 * 1024 {
147 let mut md5_buf = [0u8; 16];
149 reader.read_exact(&mut md5_buf)?;
150
151 let mut type_header = [0u8; 32]; reader.read_exact(&mut type_header)?;
153 let packet_type = &type_header[16..32];
154
155 if packet_type == TYPE_RECOVERY {
156 state.recovery_count += 1;
157 if state.recovery_set_id.is_none() {
158 let mut id = [0u8; 16];
159 id.copy_from_slice(&type_header[..16]);
160 state.recovery_set_id = Some(id);
161 }
162 }
163
164 let remaining = packet_len - 64;
166 reader.seek(SeekFrom::Current(remaining as i64))?;
167 packets_parsed += 1;
168 continue;
169 }
170
171 let mut stored_md5 = [0u8; 16];
173 reader.read_exact(&mut stored_md5)?;
174
175 let data_len = (packet_len - 32) as usize;
177 let mut data = vec![0u8; data_len];
178 if reader.read_exact(&mut data).is_err() {
179 break;
180 }
181
182 let computed_md5: [u8; 16] = Md5::digest(&data).into();
184 if computed_md5 != stored_md5 {
185 warn!(pos, "PAR2 packet MD5 mismatch, skipping");
186 continue;
187 }
188
189 let mut set_id = [0u8; 16];
191 set_id.copy_from_slice(&data[..16]);
192 if state.recovery_set_id.is_none() {
193 state.recovery_set_id = Some(set_id);
194 }
195
196 let packet_type = &data[16..32];
197
198 if packet_type == TYPE_FILE_DESC {
200 parse_file_desc(&data, &mut state);
201 } else if packet_type == TYPE_IFSC {
202 parse_ifsc(&data, packet_len, &mut state);
203 } else if packet_type == TYPE_MAIN {
204 parse_main(&data, &mut state);
205 } else if packet_type == TYPE_RECOVERY {
206 state.recovery_count += 1;
207 } else if packet_type == TYPE_CREATOR {
208 parse_creator(&data, &mut state);
209 }
210
211 packets_parsed += 1;
212 }
213
214 if packets_parsed == 0 {
215 return Err(ParseError::NoPar2Packets);
216 }
217
218 let slice_size = state.slice_size.ok_or(ParseError::NoMainPacket)?;
219 let recovery_set_id = state.recovery_set_id.unwrap_or([0u8; 16]);
220
221 let mut files = HashMap::new();
223 for (file_id, desc) in state.file_descs {
224 let slices = state.ifsc_data.remove(&file_id).unwrap_or_default();
225 files.insert(
226 file_id,
227 Par2File {
228 file_id,
229 hash: desc.hash,
230 hash_16k: desc.hash_16k,
231 size: desc.size,
232 filename: desc.filename,
233 slices,
234 },
235 );
236 }
237
238 debug!(
239 files = files.len(),
240 recovery_blocks = state.recovery_count,
241 slice_size,
242 creator = state.creator.as_deref().unwrap_or("unknown"),
243 "PAR2 file parsed"
244 );
245
246 Ok(Par2FileSet {
247 recovery_set_id,
248 slice_size,
249 file_order: state.file_order,
250 files,
251 recovery_block_count: state.recovery_count,
252 creator: state.creator,
253 })
254}
255
256fn parse_file_desc(data: &[u8], state: &mut ParseState) {
273 if data.len() < 88 {
274 warn!("FileDesc packet too short ({} bytes)", data.len());
275 return;
276 }
277
278 let mut file_id = [0u8; 16];
279 file_id.copy_from_slice(&data[32..48]);
280
281 if state.file_descs.contains_key(&file_id) {
283 return;
284 }
285
286 let mut hash = [0u8; 16];
287 hash.copy_from_slice(&data[48..64]);
288
289 let mut hash_16k = [0u8; 16];
290 hash_16k.copy_from_slice(&data[64..80]);
291
292 let size = u64::from_le_bytes(data[80..88].try_into().unwrap());
293
294 let name_bytes = &data[88..];
296 let name_end = name_bytes
297 .iter()
298 .position(|&b| b == 0)
299 .unwrap_or(name_bytes.len());
300 let filename = String::from_utf8_lossy(&name_bytes[..name_end]).into_owned();
301
302 trace!(filename, size, "parsed FileDesc");
303
304 state.file_descs.insert(
305 file_id,
306 FileDescData {
307 hash,
308 hash_16k,
309 size,
310 filename,
311 },
312 );
313}
314
315fn parse_ifsc(data: &[u8], packet_len: u64, state: &mut ParseState) {
325 if data.len() < 48 {
326 warn!("IFSC packet too short ({} bytes)", data.len());
327 return;
328 }
329
330 let mut file_id = [0u8; 16];
331 file_id.copy_from_slice(&data[32..48]);
332
333 if state.ifsc_data.contains_key(&file_id) {
335 return;
336 }
337
338 let body_len = (packet_len - 64) as usize; let checksum_data = &data[48..];
340 let num_slices = (body_len - 16) / 20; let mut slices = Vec::with_capacity(num_slices);
343 for i in 0..num_slices {
344 let offset = i * 20;
345 if offset + 20 > checksum_data.len() {
346 break;
347 }
348
349 let mut md5 = [0u8; 16];
350 md5.copy_from_slice(&checksum_data[offset..offset + 16]);
351 let crc32 = u32::from_le_bytes(checksum_data[offset + 16..offset + 20].try_into().unwrap());
352
353 slices.push(SliceChecksum { md5, crc32 });
354 }
355
356 trace!(slices = slices.len(), "parsed IFSC");
357
358 state.ifsc_data.insert(file_id, slices);
359}
360
361fn parse_main(data: &[u8], state: &mut ParseState) {
372 if data.len() < 44 {
373 warn!("Main packet too short ({} bytes)", data.len());
374 return;
375 }
376
377 let slice_size = u64::from_le_bytes(data[32..40].try_into().unwrap());
378 let nr_files = u32::from_le_bytes(data[40..44].try_into().unwrap());
379 let ids = &data[44..];
380 let count = (nr_files as usize).min(ids.len() / 16);
381 let mut file_order = Vec::with_capacity(count);
382 for i in 0..count {
383 let start = i * 16;
384 let mut id = [0u8; 16];
385 id.copy_from_slice(&ids[start..start + 16]);
386 file_order.push(id);
387 }
388
389 trace!(
390 slice_size,
391 nr_files,
392 main_file_ids = file_order.len(),
393 "parsed Main"
394 );
395
396 state.slice_size = Some(slice_size);
397 state.nr_files = Some(nr_files);
398 state.file_order = file_order;
399}
400
401fn parse_creator(data: &[u8], state: &mut ParseState) {
403 if data.len() <= 32 {
404 return;
405 }
406 let creator_bytes = &data[32..];
407 let end = creator_bytes
408 .iter()
409 .position(|&b| b == 0)
410 .unwrap_or(creator_bytes.len());
411 let creator = String::from_utf8_lossy(&creator_bytes[..end]).into_owned();
412 debug!(creator, "PAR2 creator");
413 state.creator = Some(creator);
414}
415
416fn scan_for_magic<R: Read + Seek>(reader: &mut R, file_size: u64) -> io::Result<Option<u64>> {
422 let start = reader.stream_position()?;
423 let mut buf = [0u8; 4096];
425 let mut search_pos = start;
426
427 while search_pos < file_size {
428 reader.seek(SeekFrom::Start(search_pos))?;
429 let n = reader.read(&mut buf)?;
430 if n < 8 {
431 return Ok(None);
432 }
433 for i in 0..n.saturating_sub(7) {
434 if &buf[i..i + 8] == PAR2_MAGIC {
435 return Ok(Some(search_pos + i as u64));
436 }
437 }
438 search_pos += (n - 7) as u64;
440 }
441 Ok(None)
442}
443
444#[cfg(test)]
449mod tests {
450 use super::*;
451 use std::io::Cursor;
452
453 fn build_packet(set_id: Id16, packet_type: &[u8; 16], body: &[u8]) -> Vec<u8> {
454 let mut data = Vec::with_capacity(32 + body.len());
455 data.extend_from_slice(&set_id);
456 data.extend_from_slice(packet_type);
457 data.extend_from_slice(body);
458
459 let mut packet = Vec::with_capacity(32 + data.len());
460 packet.extend_from_slice(PAR2_MAGIC);
461 packet.extend_from_slice(&((HEADER_SIZE + body.len()) as u64).to_le_bytes());
462 packet.extend_from_slice(&Md5::digest(&data));
463 packet.extend_from_slice(&data);
464 packet
465 }
466
467 #[test]
469 fn test_parse_par2test() {
470 let path = Path::new("/home/sprooty/sabnzbd/tests/data/par2repair/basic/par2test.par2");
471 if !path.exists() {
472 eprintln!("Skipping test: {path:?} not found");
473 return;
474 }
475
476 let set = parse_par2_file(path).unwrap();
477
478 assert_eq!(set.files.len(), 6, "expected 6 files in par2 set");
480
481 assert_eq!(set.slice_size, 100000, "expected slice_size = 100000");
483
484 assert_eq!(
486 set.creator.as_deref(),
487 Some("QuickPar 0.9"),
488 "expected creator = QuickPar 0.9"
489 );
490
491 assert_eq!(set.recovery_block_count, 0);
493
494 let filenames: Vec<&str> = set.files.values().map(|f| f.filename.as_str()).collect();
496 for i in 1..=6 {
497 let expected = format!("par2test.part{i}.rar");
498 assert!(
499 filenames.contains(&expected.as_str()),
500 "missing file: {expected}"
501 );
502 }
503
504 for f in set.files.values() {
506 if f.filename == "par2test.part6.rar" {
507 assert!(f.size < 100000, "part6 should be smaller than slice_size");
509 } else {
510 assert_eq!(f.size, 102400, "{} should be 102400 bytes", f.filename);
511 }
512 }
513
514 for f in set.files.values() {
516 assert!(
517 !f.slices.is_empty(),
518 "{} should have slice checksums",
519 f.filename
520 );
521 }
522 }
523
524 #[test]
526 fn test_parse_basic_16k() {
527 let path = Path::new("/home/sprooty/sabnzbd/tests/data/par2file/basic_16k.par2");
528 if !path.exists() {
529 eprintln!("Skipping test: {path:?} not found");
530 return;
531 }
532
533 let set = parse_par2_file(path).unwrap();
534 assert!(!set.files.is_empty(), "should parse at least one file");
535 assert!(set.slice_size > 0, "slice_size should be > 0");
536 }
537
538 #[test]
540 fn test_parse_non_par2() {
541 let path =
542 Path::new("/home/sprooty/sabnzbd/tests/data/par2repair/basic/par2test.part2.rar");
543 if !path.exists() {
544 eprintln!("Skipping test: {path:?} not found");
545 return;
546 }
547
548 let result = parse_par2_file(path);
549 assert!(result.is_err(), "parsing a RAR file should fail");
550 }
551
552 #[test]
554 fn test_parse_recovery_volume() {
555 let path =
556 Path::new("/home/sprooty/sabnzbd/tests/data/par2repair/basic/par2test.vol0+1.par2");
557 if !path.exists() {
558 eprintln!("Skipping test: {path:?} not found");
559 return;
560 }
561
562 let set = parse_par2_file(path).unwrap();
563 assert!(
564 set.recovery_block_count >= 1,
565 "recovery volume should have at least 1 recovery block"
566 );
567 }
568
569 #[test]
570 fn test_parse_main_preserves_file_order() {
571 let set_id = [0xAB; 16];
572 let file_a = [0x10; 16];
573 let file_b = [0x01; 16];
574
575 let mut main_body = Vec::new();
576 main_body.extend_from_slice(&4096u64.to_le_bytes());
577 main_body.extend_from_slice(&2u32.to_le_bytes());
578 main_body.extend_from_slice(&file_a);
579 main_body.extend_from_slice(&file_b);
580
581 let packet = build_packet(set_id, TYPE_MAIN, &main_body);
582
583 let mut state = ParseState {
584 recovery_set_id: None,
585 slice_size: None,
586 nr_files: None,
587 file_order: Vec::new(),
588 file_descs: HashMap::new(),
589 ifsc_data: HashMap::new(),
590 recovery_count: 0,
591 creator: None,
592 };
593 parse_main(&packet[32..], &mut state);
594 assert_eq!(state.file_order, vec![file_a, file_b]);
595 }
596
597 #[test]
598 fn test_large_recovery_volume_counts_all_blocks() {
599 let set_id = [0x42; 16];
600 let file_id = [0x24; 16];
601 let mut bytes = Vec::new();
602
603 let mut main_body = Vec::new();
604 main_body.extend_from_slice(&524_288u64.to_le_bytes());
605 main_body.extend_from_slice(&1u32.to_le_bytes());
606 main_body.extend_from_slice(&file_id);
607 bytes.extend_from_slice(&build_packet(set_id, TYPE_MAIN, &main_body));
608
609 let mut file_desc_body = Vec::new();
610 file_desc_body.extend_from_slice(&file_id);
611 file_desc_body.extend_from_slice(&[0x11; 16]);
612 file_desc_body.extend_from_slice(&[0x22; 16]);
613 file_desc_body.extend_from_slice(&524_288u64.to_le_bytes());
614 file_desc_body.extend_from_slice(b"payload.bin\0");
615 while file_desc_body.len() % 4 != 0 {
616 file_desc_body.push(0);
617 }
618 bytes.extend_from_slice(&build_packet(set_id, TYPE_FILE_DESC, &file_desc_body));
619
620 let mut ifsc_body = Vec::new();
621 ifsc_body.extend_from_slice(&file_id);
622 ifsc_body.extend_from_slice(&[0x33; 16]);
623 ifsc_body.extend_from_slice(&0u32.to_le_bytes());
624 bytes.extend_from_slice(&build_packet(set_id, TYPE_IFSC, &ifsc_body));
625
626 for exp in 0..20u32 {
627 let mut rec_body = Vec::with_capacity(4 + 524_288);
628 rec_body.extend_from_slice(&exp.to_le_bytes());
629 rec_body.extend(std::iter::repeat_n(exp as u8, 524_288));
630 bytes.extend_from_slice(&build_packet(set_id, TYPE_RECOVERY, &rec_body));
631 }
632
633 let mut cursor = Cursor::new(bytes);
634 let file_size = cursor.get_ref().len() as u64;
635 let parsed = parse_par2_reader(&mut cursor, file_size).unwrap();
636 assert_eq!(parsed.recovery_block_count, 20);
637 assert_eq!(parsed.file_order, vec![file_id]);
638 }
639}