Skip to main content

rars_format/
detect.rs

1use crate::version::ArchiveFamily;
2
3pub const RAR13_SIGNATURE: &[u8; 4] = b"RE~^";
4pub const RAR15_SIGNATURE: &[u8; 7] = b"Rar!\x1a\x07\x00";
5pub const RAR50_SIGNATURE: &[u8; 8] = b"Rar!\x1a\x07\x01\x00";
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8#[non_exhaustive]
9pub struct ArchiveSignature {
10    pub family: ArchiveFamily,
11    pub offset: usize,
12    pub length: usize,
13}
14
15pub fn detect_archive_family(input: &[u8]) -> Option<ArchiveSignature> {
16    detect_at(input, 0)
17}
18
19pub fn find_archive_start(input: &[u8], max_scan: usize) -> Option<ArchiveSignature> {
20    let limit = input.len().min(max_scan);
21    (0..=limit).find_map(|offset| detect_at(input, offset))
22}
23
24fn detect_at(input: &[u8], offset: usize) -> Option<ArchiveSignature> {
25    let tail = input.get(offset..)?;
26
27    if tail.starts_with(RAR50_SIGNATURE) {
28        Some(ArchiveSignature {
29            family: ArchiveFamily::Rar50Plus,
30            offset,
31            length: RAR50_SIGNATURE.len(),
32        })
33    } else if tail.starts_with(RAR15_SIGNATURE) {
34        Some(ArchiveSignature {
35            family: ArchiveFamily::Rar15To40,
36            offset,
37            length: RAR15_SIGNATURE.len(),
38        })
39    } else if tail.starts_with(RAR13_SIGNATURE) {
40        Some(ArchiveSignature {
41            family: ArchiveFamily::Rar13,
42            offset,
43            length: RAR13_SIGNATURE.len(),
44        })
45    } else {
46        None
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn detects_all_known_signatures() {
56        assert_eq!(
57            detect_archive_family(b"RE~^").unwrap().family,
58            ArchiveFamily::Rar13
59        );
60        assert_eq!(
61            detect_archive_family(b"Rar!\x1a\x07\x00").unwrap().family,
62            ArchiveFamily::Rar15To40
63        );
64        assert_eq!(
65            detect_archive_family(b"Rar!\x1a\x07\x01\x00")
66                .unwrap()
67                .family,
68            ArchiveFamily::Rar50Plus
69        );
70    }
71
72    #[test]
73    fn finds_sfx_prefixed_archive() {
74        let sig = find_archive_start(b"stub bytes RE~^payload", 128).unwrap();
75        assert_eq!(sig.family, ArchiveFamily::Rar13);
76        assert_eq!(sig.offset, 11);
77    }
78
79    #[test]
80    fn rejects_unknown_and_truncated_signatures() {
81        assert_eq!(detect_archive_family(b""), None);
82        assert_eq!(detect_archive_family(b"RAR!"), None);
83        assert_eq!(detect_archive_family(b"Rar!\x1a\x07"), None);
84        assert_eq!(find_archive_start(b"not an archive", 128), None);
85    }
86
87    #[test]
88    fn scan_limit_bounds_sfx_detection() {
89        let input = b"stub bytes RE~^payload";
90
91        assert_eq!(find_archive_start(input, 10), None);
92
93        let sig = find_archive_start(input, 11).unwrap();
94        assert_eq!(sig.family, ArchiveFamily::Rar13);
95        assert_eq!(sig.offset, 11);
96        assert_eq!(sig.length, RAR13_SIGNATURE.len());
97    }
98}