1use std::collections::BTreeMap;
7use std::io::Read;
8use std::path::{Path, PathBuf};
9
10use walkdir::WalkDir;
11
12const RAR4_SIGNATURE: &[u8] = b"Rar!\x1a\x07\x00";
14const RAR5_SIGNATURE: &[u8] = b"Rar!\x1a\x07\x01\x00";
16
17pub fn has_rar_signature(path: &Path) -> bool {
23 let Ok(mut file) = std::fs::File::open(path) else {
24 return false;
25 };
26 let mut header = [0u8; RAR5_SIGNATURE.len()];
28 let mut filled = 0;
29 while filled < header.len() {
30 match file.read(&mut header[filled..]) {
31 Ok(0) => break,
32 Ok(n) => filled += n,
33 Err(_) => return false,
34 }
35 }
36 let head = &header[..filled];
37 head.starts_with(RAR4_SIGNATURE) || head.starts_with(RAR5_SIGNATURE)
38}
39
40fn split_numeric_volume(filename: &str) -> Option<(&str, u32)> {
46 let dot = filename.rfind('.')?;
47 let ext = &filename[dot + 1..];
48 if ext.is_empty() || !ext.chars().all(|c| c.is_ascii_digit()) {
49 return None;
50 }
51 let stem = &filename[..dot];
52 if stem.to_ascii_lowercase().ends_with(".7z") {
53 return None;
54 }
55 ext.parse::<u32>().ok().map(|num| (stem, num))
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct RarVolumeInfo {
61 pub set_name: String,
63 pub volume_number: u32,
66}
67
68pub fn parse_rar_volume(filename: &str) -> Option<RarVolumeInfo> {
76 let name_lower = filename.to_ascii_lowercase();
77
78 if let Some(stem) = name_lower.strip_suffix(".rar") {
80 if let Some(dot_pos) = stem.rfind(".part") {
81 let part_num_str = &stem[dot_pos + 5..];
82 if !part_num_str.is_empty()
83 && part_num_str.chars().all(|c| c.is_ascii_digit())
84 && let Ok(part_num) = part_num_str.parse::<u32>()
85 {
86 let set_name = &filename[..dot_pos];
88 return Some(RarVolumeInfo {
89 set_name: set_name.to_string(),
90 volume_number: part_num.saturating_sub(1),
91 });
92 }
93 }
94 let set_name = &filename[..filename.len() - 4];
96 return Some(RarVolumeInfo {
97 set_name: set_name.to_string(),
98 volume_number: 0,
99 });
100 }
101
102 if name_lower.len() > 4
105 && let Some(last4) = name_lower.get(name_lower.len() - 4..)
106 && last4.starts_with('.')
107 && (b'r'..=b'z').contains(&last4.as_bytes()[1])
108 && last4.as_bytes()[2].is_ascii_digit()
109 && last4.as_bytes()[3].is_ascii_digit()
110 {
111 let letter = last4.as_bytes()[1];
112 let tens = (last4.as_bytes()[2] - b'0') as u32;
113 let ones = (last4.as_bytes()[3] - b'0') as u32;
114 let letter_offset = (letter - b'r') as u32 * 100;
117 let vol = letter_offset + tens * 10 + ones + 1;
118 let set_name = &filename[..filename.len() - 4];
119 return Some(RarVolumeInfo {
120 set_name: set_name.to_string(),
121 volume_number: vol,
122 });
123 }
124
125 None
126}
127
128pub fn parse_rar_volume_at(path: &Path) -> Option<RarVolumeInfo> {
140 let name = path.file_name().and_then(|n| n.to_str())?;
141 if let Some(info) = parse_rar_volume(name) {
142 return Some(info);
143 }
144 let (set_name, volume_number) = split_numeric_volume(name)?;
145 if !has_rar_signature(path) {
146 return None;
147 }
148 Some(RarVolumeInfo {
149 set_name: set_name.to_string(),
150 volume_number,
151 })
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum ArchiveType {
157 Rar,
158 SevenZip,
159 Tar,
160 Zip,
161}
162
163impl std::fmt::Display for ArchiveType {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 match self {
166 Self::Rar => write!(f, "RAR"),
167 Self::SevenZip => write!(f, "7z"),
168 Self::Tar => write!(f, "TAR"),
169 Self::Zip => write!(f, "ZIP"),
170 }
171 }
172}
173
174pub fn find_par2_files(dir: &Path) -> Vec<PathBuf> {
178 let mut index_files: Vec<PathBuf> = Vec::new();
179 let mut volume_files: Vec<PathBuf> = Vec::new();
180
181 for entry in WalkDir::new(dir).into_iter().flatten() {
182 let path = entry.path();
183 if !path.is_file() {
184 continue;
185 }
186 let name = match path.file_name().and_then(|n| n.to_str()) {
187 Some(n) => n.to_lowercase(),
188 None => continue,
189 };
190 if !name.ends_with(".par2") {
191 continue;
192 }
193 if is_par2_volume(&name) {
195 volume_files.push(path.to_path_buf());
196 } else {
197 index_files.push(path.to_path_buf());
198 }
199 }
200
201 index_files.sort();
202 volume_files.sort();
203
204 index_files.extend(volume_files);
206 index_files
207}
208
209pub(crate) fn is_par2_volume(name_lower: &str) -> bool {
212 let without_ext = name_lower.trim_end_matches(".par2");
215 if let Some(vol_pos) = without_ext.rfind(".vol") {
217 let after_vol = &without_ext[vol_pos + 4..];
218 if let Some(separator_pos) = after_vol.find(['+', '-']) {
220 let before_separator = &after_vol[..separator_pos];
221 let after_separator = &after_vol[separator_pos + 1..];
222 return !before_separator.is_empty()
223 && before_separator.chars().all(|c| c.is_ascii_digit())
224 && !after_separator.is_empty()
225 && after_separator.chars().all(|c| c.is_ascii_digit());
226 }
227 }
228 false
229}
230
231pub fn find_rar_files(dir: &Path) -> Vec<PathBuf> {
237 let mut first_volumes: Vec<PathBuf> = Vec::new();
238 let mut numeric_sets: BTreeMap<String, (u32, PathBuf)> = BTreeMap::new();
240
241 for entry in WalkDir::new(dir).into_iter().flatten() {
242 let path = entry.path();
243 if !path.is_file() {
244 continue;
245 }
246 let name = match path.file_name().and_then(|n| n.to_str()) {
247 Some(n) => n,
248 None => continue,
249 };
250 let name_lower = name.to_lowercase();
251
252 if name_lower.ends_with(".rar")
254 && let Some(stem) = name_lower.strip_suffix(".rar")
255 {
256 if let Some(dot_pos) = stem.rfind(".part") {
258 let part_num_str = &stem[dot_pos + 5..];
259 if !part_num_str.is_empty()
260 && part_num_str.chars().all(|c| c.is_ascii_digit())
261 && let Ok(part_num) = part_num_str.parse::<u32>()
262 {
263 if part_num == 1 {
264 first_volumes.push(path.to_path_buf());
265 }
266 continue;
268 }
269 }
270 first_volumes.push(path.to_path_buf());
272 continue;
273 }
274 if parse_rar_volume(&name_lower).is_some() {
277 continue;
278 }
279
280 if let Some((set_name, volume_number)) = split_numeric_volume(&name_lower)
285 && has_rar_signature(path)
286 {
287 numeric_sets
288 .entry(set_name.to_string())
289 .and_modify(|(lowest, lowest_path)| {
290 if volume_number < *lowest {
291 *lowest = volume_number;
292 *lowest_path = path.to_path_buf();
293 }
294 })
295 .or_insert_with(|| (volume_number, path.to_path_buf()));
296 }
297 }
298
299 first_volumes.extend(numeric_sets.into_values().map(|(_, path)| path));
300 first_volumes.sort();
301 first_volumes
302}
303
304pub fn find_archives(dir: &Path) -> Vec<(ArchiveType, PathBuf)> {
307 let mut archives: Vec<(ArchiveType, PathBuf)> = Vec::new();
308
309 for path in find_rar_files(dir) {
311 archives.push((ArchiveType::Rar, path));
312 }
313
314 for entry in WalkDir::new(dir).into_iter().flatten() {
316 let path = entry.path();
317 if !path.is_file() {
318 continue;
319 }
320 let name = match path.file_name().and_then(|n| n.to_str()) {
321 Some(n) => n.to_lowercase(),
322 None => continue,
323 };
324
325 if name.ends_with(".7z") || name.ends_with(".7z.enc") {
326 archives.push((ArchiveType::SevenZip, path.to_path_buf()));
327 } else if is_split_7z_first_volume(&name) {
328 archives.push((ArchiveType::SevenZip, path.to_path_buf()));
330 } else if name.ends_with(".tar") {
331 archives.push((ArchiveType::Tar, path.to_path_buf()));
332 } else if name.ends_with(".zip") {
333 archives.push((ArchiveType::Zip, path.to_path_buf()));
334 }
335 }
336
337 archives.sort_by(|a, b| a.1.cmp(&b.1));
338 archives
339}
340
341pub fn find_cleanup_files(dir: &Path) -> Vec<PathBuf> {
345 let mut cleanup: Vec<PathBuf> = Vec::new();
346
347 for entry in WalkDir::new(dir).into_iter().flatten() {
348 let path = entry.path();
349 if !path.is_file() {
350 continue;
351 }
352 let name = match path.file_name().and_then(|n| n.to_str()) {
353 Some(n) => n.to_lowercase(),
354 None => continue,
355 };
356
357 if is_cleanup_candidate_at(path, &name) {
358 cleanup.push(path.to_path_buf());
359 }
360 }
361
362 cleanup.sort();
363 cleanup
364}
365
366pub fn has_usable_output(dir: &Path) -> std::io::Result<bool> {
371 for entry in WalkDir::new(dir).into_iter().flatten() {
372 let path = entry.path();
373 if !path.is_file() {
374 continue;
375 }
376 let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
377 continue;
378 };
379 if !is_cleanup_candidate_at(path, &name.to_ascii_lowercase()) {
380 return Ok(true);
381 }
382 }
383
384 std::fs::read_dir(dir)?;
386 Ok(false)
387}
388
389fn is_split_7z_first_volume(name_lower: &str) -> bool {
392 if let Some(stem) = name_lower.strip_suffix(".001") {
394 return stem.ends_with(".7z");
395 }
396 false
397}
398
399fn is_split_7z_volume(name_lower: &str) -> bool {
402 if let Some(dot_pos) = name_lower.rfind('.') {
404 let ext = &name_lower[dot_pos + 1..];
405 if !ext.is_empty() && ext.chars().all(|c| c.is_ascii_digit()) {
406 let stem = &name_lower[..dot_pos];
407 return stem.ends_with(".7z");
408 }
409 }
410 false
411}
412
413fn is_cleanup_candidate_at(path: &Path, name_lower: &str) -> bool {
426 if is_cleanup_candidate(name_lower) {
427 return true;
428 }
429 if !name_lower.ends_with(".rar")
433 && let Some(info) = parse_rar_volume(name_lower)
434 {
435 return has_rar_first_volume(path, &info.set_name) || has_rar_signature(path);
436 }
437 split_numeric_volume(name_lower).is_some() && has_rar_signature(path)
438}
439
440fn has_rar_first_volume(path: &Path, set_lower: &str) -> bool {
444 let Some(parent) = path.parent() else {
445 return false;
446 };
447 let Ok(entries) = std::fs::read_dir(parent) else {
448 return false;
449 };
450 entries.flatten().any(|entry| {
451 let Some(name) = entry.file_name().to_str().map(str::to_lowercase) else {
452 return false;
453 };
454 name.ends_with(".rar")
455 && parse_rar_volume(&name).is_some_and(|info| info.set_name == set_lower)
456 && entry.file_type().is_ok_and(|t| t.is_file())
457 })
458}
459
460fn is_cleanup_candidate(name: &str) -> bool {
466 if name.ends_with(".par2")
468 || name.ends_with(".zip")
469 || name.ends_with(".7z")
470 || name.ends_with(".tar")
471 {
472 return true;
473 }
474
475 if name.ends_with(".rar") {
478 return true;
479 }
480
481 if is_split_7z_volume(name) {
487 return true;
488 }
489
490 if name.ends_with(".7z.enc") {
492 return true;
493 }
494
495 false
496}
497
498#[cfg(test)]
503mod tests {
504 use super::*;
505 use std::fs;
506
507 fn make_test_dir(files: &[&str]) -> tempfile::TempDir {
509 let dir = tempfile::tempdir().unwrap();
510 for name in files {
511 let path = dir.path().join(name);
512 if let Some(parent) = path.parent() {
513 fs::create_dir_all(parent).unwrap();
514 }
515 fs::write(&path, b"").unwrap();
516 }
517 dir
518 }
519
520 #[test]
521 fn test_find_par2_index_first() {
522 let dir = make_test_dir(&["movie.vol00+01.par2", "movie.vol01+02.par2", "movie.par2"]);
523 let results = find_par2_files(dir.path());
524 assert_eq!(results.len(), 3);
525 assert!(
527 results[0].file_name().unwrap().to_str().unwrap() == "movie.par2",
528 "Index par2 file should be first, got {:?}",
529 results[0]
530 );
531 }
532
533 #[test]
534 fn test_find_par2_hyphenated_volumes_after_index() {
535 let dir = make_test_dir(&["movie.vol63-67.par2", "movie.par2", "movie.vol00-01.par2"]);
536 let files = find_par2_files(dir.path());
537 assert_eq!(files[0].file_name().unwrap(), "movie.par2");
538 assert_eq!(files.len(), 3);
539 }
540
541 #[test]
542 fn test_find_par2_empty_dir() {
543 let dir = make_test_dir(&["readme.txt", "movie.mkv"]);
544 let results = find_par2_files(dir.path());
545 assert!(results.is_empty());
546 }
547
548 #[test]
549 fn test_find_rar_new_style() {
550 let dir = make_test_dir(&[
551 "archive.part001.rar",
552 "archive.part002.rar",
553 "archive.part003.rar",
554 ]);
555 let results = find_rar_files(dir.path());
556 assert_eq!(results.len(), 1);
558 assert!(
559 results[0]
560 .file_name()
561 .unwrap()
562 .to_str()
563 .unwrap()
564 .contains("part001"),
565 );
566 }
567
568 #[test]
569 fn test_find_rar_old_style() {
570 let dir = make_test_dir(&["archive.rar", "archive.r00", "archive.r01", "archive.r02"]);
571 let results = find_rar_files(dir.path());
572 assert_eq!(results.len(), 1);
574 assert!(
575 results[0]
576 .file_name()
577 .unwrap()
578 .to_str()
579 .unwrap()
580 .ends_with(".rar")
581 );
582 }
583
584 #[test]
585 fn test_find_archives_mixed() {
586 let dir = make_test_dir(&[
587 "movie.part001.rar",
588 "movie.part002.rar",
589 "subs.zip",
590 "extras.7z",
591 ]);
592 let results = find_archives(dir.path());
593 let types: Vec<ArchiveType> = results.iter().map(|(t, _)| *t).collect();
594 assert!(types.contains(&ArchiveType::Rar));
595 assert!(types.contains(&ArchiveType::Zip));
596 assert!(types.contains(&ArchiveType::SevenZip));
597 assert_eq!(types.iter().filter(|&&t| t == ArchiveType::Rar).count(), 1);
599 }
600
601 #[test]
602 fn usable_output_requires_a_non_artifact_file() {
603 let raw_only = make_test_dir(&[
604 "release.part001.rar",
605 "release.part002.rar",
606 "release.par2",
607 "release.vol00+01.par2",
608 "release.7z",
609 ]);
610 assert!(!has_usable_output(raw_only.path()).unwrap());
611
612 let payload = make_test_dir(&["release.part001.rar", "Movie.2024.mkv"]);
613 assert!(has_usable_output(payload.path()).unwrap());
614 }
615
616 #[test]
617 fn test_find_cleanup_files() {
618 let dir = make_test_dir(&[
619 "movie.par2",
620 "movie.vol00+01.par2",
621 "movie.part001.rar",
622 "movie.part002.rar",
623 "movie.r00",
624 "movie.r01",
625 "movie.mkv", "readme.txt", ]);
628 let results = find_cleanup_files(dir.path());
629 assert_eq!(
631 results.len(),
632 6,
633 "Expected 6 cleanup files, got: {results:?}"
634 );
635 for path in &results {
637 let name = path.file_name().unwrap().to_str().unwrap();
638 assert!(!name.ends_with(".mkv"));
639 assert!(!name.ends_with(".txt"));
640 }
641 }
642
643 #[test]
644 fn test_is_par2_volume() {
645 assert!(is_par2_volume("file.vol00+01.par2"));
646 assert!(is_par2_volume("file.vol123+456.par2"));
647 assert!(is_par2_volume("file.vol00-01.par2"));
648 assert!(is_par2_volume("file.vol63-67.par2"));
649 assert!(!is_par2_volume("file.par2"));
650 assert!(!is_par2_volume("file.volume.par2"));
651 }
652
653 #[test]
654 fn test_cleanup_old_style_volumes() {
655 let dir = make_test_dir(&[
656 "archive.rar",
657 "archive.r00",
658 "archive.r99",
659 "archive.s00",
660 "readme.txt",
661 "movie.mkv",
662 ]);
663 let candidate = |name: &str| {
664 is_cleanup_candidate_at(&dir.path().join(name), &name.to_ascii_lowercase())
665 };
666 assert!(candidate("archive.r00"));
667 assert!(candidate("archive.r99"));
668 assert!(candidate("archive.s00"));
669 assert!(!candidate("readme.txt"));
670 assert!(!candidate("movie.mkv"));
671 }
672
673 #[test]
674 fn test_cleanup_keeps_letter_digit_payload_without_rar_set() {
675 let dir = make_test_dir(&["Game.n64", "disk.d64", "notes.r00", "readme.txt"]);
678 let results = find_cleanup_files(dir.path());
679 assert!(
680 results.is_empty(),
681 "payload marked for cleanup: {results:?}"
682 );
683 assert!(has_usable_output(dir.path()).unwrap());
684 }
685
686 #[test]
687 fn test_cleanup_old_style_volume_matches_set_case_insensitively() {
688 let dir = make_test_dir(&["Movie.RAR", "Movie.R00", "movie.r01", "Game.n64"]);
689 let names: Vec<String> = find_cleanup_files(dir.path())
690 .iter()
691 .map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
692 .collect();
693 assert_eq!(names, ["Movie.R00", "Movie.RAR", "movie.r01"]);
694 }
695
696 #[test]
697 fn test_cleanup_orphan_old_style_volume_with_rar_signature() {
698 let dir = make_test_dir(&[]);
701 fs::write(dir.path().join("movie.r00"), b"Rar!\x1a\x07\x00rest").unwrap();
702 let results = find_cleanup_files(dir.path());
703 assert_eq!(results.len(), 1, "{results:?}");
704 }
705
706 #[test]
711 fn test_split_7z_first_volume() {
712 assert!(is_split_7z_first_volume("archive.7z.001"));
713 assert!(is_split_7z_first_volume("my.movie.7z.001"));
714 assert!(!is_split_7z_first_volume("archive.7z.002"));
715 assert!(!is_split_7z_first_volume("archive.7z.010"));
716 assert!(!is_split_7z_first_volume("archive.7z"));
717 assert!(!is_split_7z_first_volume("archive.rar.001"));
718 }
719
720 #[test]
721 fn test_split_7z_volume() {
722 assert!(is_split_7z_volume("archive.7z.001"));
723 assert!(is_split_7z_volume("archive.7z.002"));
724 assert!(is_split_7z_volume("archive.7z.099"));
725 assert!(!is_split_7z_volume("archive.7z"));
726 assert!(!is_split_7z_volume("archive.rar.001"));
727 assert!(!is_split_7z_volume("archive.7z.abc"));
728 }
729
730 #[test]
731 fn test_find_archives_split_7z() {
732 let dir = make_test_dir(&["movie.7z.001", "movie.7z.002", "movie.7z.003"]);
733 let results = find_archives(dir.path());
734 assert_eq!(results.len(), 1);
736 assert_eq!(results[0].0, ArchiveType::SevenZip);
737 assert!(
738 results[0]
739 .1
740 .file_name()
741 .unwrap()
742 .to_str()
743 .unwrap()
744 .ends_with(".7z.001"),
745 );
746 }
747
748 #[test]
749 fn test_cleanup_split_7z_volumes() {
750 assert!(is_cleanup_candidate("archive.7z"));
751 assert!(is_cleanup_candidate("archive.7z.001"));
752 assert!(is_cleanup_candidate("archive.7z.002"));
753 assert!(is_cleanup_candidate("archive.7z.099"));
754 assert!(!is_cleanup_candidate("movie.mkv"));
755 }
756
757 #[test]
758 fn test_find_cleanup_includes_split_7z() {
759 let dir = make_test_dir(&[
760 "movie.7z.001",
761 "movie.7z.002",
762 "movie.7z.003",
763 "movie.mkv", ]);
765 let results = find_cleanup_files(dir.path());
766 assert_eq!(
767 results.len(),
768 3,
769 "Expected 3 split 7z cleanup files, got: {results:?}"
770 );
771 for path in &results {
772 let name = path.file_name().unwrap().to_str().unwrap();
773 assert!(!name.ends_with(".mkv"));
774 }
775 }
776
777 #[test]
782 fn test_parse_rar_volume_new_style() {
783 let v = parse_rar_volume("movie.part001.rar").unwrap();
784 assert_eq!(v.set_name, "movie");
785 assert_eq!(v.volume_number, 0);
786
787 let v = parse_rar_volume("movie.part002.rar").unwrap();
788 assert_eq!(v.set_name, "movie");
789 assert_eq!(v.volume_number, 1);
790
791 let v = parse_rar_volume("My.Movie.2024.part015.rar").unwrap();
792 assert_eq!(v.set_name, "My.Movie.2024");
793 assert_eq!(v.volume_number, 14);
794 }
795
796 #[test]
797 fn test_parse_rar_volume_old_style() {
798 let v = parse_rar_volume("archive.rar").unwrap();
799 assert_eq!(v.set_name, "archive");
800 assert_eq!(v.volume_number, 0);
801
802 let v = parse_rar_volume("archive.r00").unwrap();
803 assert_eq!(v.set_name, "archive");
804 assert_eq!(v.volume_number, 1);
805
806 let v = parse_rar_volume("archive.r01").unwrap();
807 assert_eq!(v.set_name, "archive");
808 assert_eq!(v.volume_number, 2);
809
810 let v = parse_rar_volume("archive.r99").unwrap();
811 assert_eq!(v.set_name, "archive");
812 assert_eq!(v.volume_number, 100);
813
814 let v = parse_rar_volume("archive.s00").unwrap();
815 assert_eq!(v.set_name, "archive");
816 assert_eq!(v.volume_number, 101);
817 }
818
819 #[test]
820 fn test_parse_rar_volume_non_rar() {
821 assert!(parse_rar_volume("movie.mkv").is_none());
822 assert!(parse_rar_volume("movie.par2").is_none());
823 assert!(parse_rar_volume("movie.7z").is_none());
824 assert!(parse_rar_volume("movie.zip").is_none());
825 assert!(parse_rar_volume("readme.txt").is_none());
826 }
827
828 #[test]
829 fn parse_rar_volume_does_not_panic_on_multibyte_names() {
830 assert!(parse_rar_volume("テスト").is_none());
831 assert!(parse_rar_volume("file.日本").is_none());
832 assert!(parse_rar_volume(".r0").is_none());
833 assert!(parse_rar_volume(".r00").is_none());
834 assert!(parse_rar_volume("r0").is_none());
835 assert!(parse_rar_volume("").is_none());
836 let v = parse_rar_volume("İSTANBUL.part01.rar").unwrap();
837 assert_eq!(v.set_name, "İSTANBUL");
838 assert_eq!(v.volume_number, 0);
839 }
840
841 #[test]
842 fn test_parse_rar_volume_rejects_letters_before_r() {
843 for name in ["Game.n64", "clip.a52", "x.c01", "disk.d64", "tape.q99"] {
847 assert!(
848 parse_rar_volume(name).is_none(),
849 "{name} is not a RAR volume"
850 );
851 }
852
853 let v = parse_rar_volume("archive.t00").unwrap();
854 assert_eq!(v.volume_number, 201);
855 let v = parse_rar_volume("archive.z99").unwrap();
856 assert_eq!(v.volume_number, 900);
857 }
858
859 #[test]
860 fn test_find_archives_ignores_non_rar_letter_extensions() {
861 let dir = make_test_dir(&["Game.n64", "clip.a52", "x.c01", "movie.mkv"]);
862 assert!(find_archives(dir.path()).is_empty());
863 }
864
865 #[test]
866 fn test_parse_rar_volume_case_insensitive() {
867 let v = parse_rar_volume("Movie.Part003.RAR").unwrap();
868 assert_eq!(v.set_name, "Movie");
869 assert_eq!(v.volume_number, 2);
870
871 let v = parse_rar_volume("ARCHIVE.R05").unwrap();
872 assert_eq!(v.set_name, "ARCHIVE");
873 assert_eq!(v.volume_number, 6);
874 }
875
876 #[test]
877 fn test_parse_rar_volume_preserves_original_set_name() {
878 let v = parse_rar_volume("My.Movie.2024.1080p.part001.rar").unwrap();
879 assert_eq!(v.set_name, "My.Movie.2024.1080p");
880 }
881}