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/// Default upper bound for scanning past an SFX stub when looking for the RAR
8/// signature. Most installers in the wild place the archive within a few
9/// hundred KiB, but large SFX modules (notably WinRAR's own installer plus a
10/// bundled runtime) can push the offset past 1 MiB.
11pub const SFX_SCAN_LIMIT: usize = 8 * 1024 * 1024;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[non_exhaustive]
15pub struct ArchiveSignature {
16    pub family: ArchiveFamily,
17    pub offset: usize,
18    pub length: usize,
19}
20
21pub fn detect_archive_family(input: &[u8]) -> Option<ArchiveSignature> {
22    detect_at(input, 0)
23}
24
25pub fn find_archive_start(input: &[u8], max_scan: usize) -> Option<ArchiveSignature> {
26    let limit = input.len().min(max_scan);
27    let mut first_rar13 = None;
28    for offset in 0..=limit {
29        let tail = input.get(offset..)?;
30        if tail.starts_with(RAR50_SIGNATURE) {
31            return Some(ArchiveSignature {
32                family: ArchiveFamily::Rar50Plus,
33                offset,
34                length: RAR50_SIGNATURE.len(),
35            });
36        }
37        if tail.starts_with(RAR15_SIGNATURE) {
38            return Some(ArchiveSignature {
39                family: ArchiveFamily::Rar15To40,
40                offset,
41                length: RAR15_SIGNATURE.len(),
42            });
43        }
44        if first_rar13.is_none() && tail.starts_with(RAR13_SIGNATURE) {
45            first_rar13 = Some(ArchiveSignature {
46                family: ArchiveFamily::Rar13,
47                offset,
48                length: RAR13_SIGNATURE.len(),
49            });
50        }
51    }
52    first_rar13
53}
54
55fn detect_at(input: &[u8], offset: usize) -> Option<ArchiveSignature> {
56    let tail = input.get(offset..)?;
57
58    if tail.starts_with(RAR50_SIGNATURE) {
59        Some(ArchiveSignature {
60            family: ArchiveFamily::Rar50Plus,
61            offset,
62            length: RAR50_SIGNATURE.len(),
63        })
64    } else if tail.starts_with(RAR15_SIGNATURE) {
65        Some(ArchiveSignature {
66            family: ArchiveFamily::Rar15To40,
67            offset,
68            length: RAR15_SIGNATURE.len(),
69        })
70    } else if tail.starts_with(RAR13_SIGNATURE) {
71        Some(ArchiveSignature {
72            family: ArchiveFamily::Rar13,
73            offset,
74            length: RAR13_SIGNATURE.len(),
75        })
76    } else {
77        None
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn detects_all_known_signatures() {
87        assert_eq!(
88            detect_archive_family(b"RE~^").unwrap().family,
89            ArchiveFamily::Rar13
90        );
91        assert_eq!(
92            detect_archive_family(b"Rar!\x1a\x07\x00").unwrap().family,
93            ArchiveFamily::Rar15To40
94        );
95        assert_eq!(
96            detect_archive_family(b"Rar!\x1a\x07\x01\x00")
97                .unwrap()
98                .family,
99            ArchiveFamily::Rar50Plus
100        );
101    }
102
103    #[test]
104    fn finds_sfx_prefixed_archive() {
105        let sig = find_archive_start(b"stub bytes RE~^payload", 128).unwrap();
106        assert_eq!(sig.family, ArchiveFamily::Rar13);
107        assert_eq!(sig.offset, 11);
108    }
109
110    #[test]
111    fn sfx_scan_prefers_stronger_rar15_signature_over_earlier_rar13_bytes() {
112        let sig = find_archive_start(b"stub RE~^ bytes Rar!\x1a\x07\x00payload", 128).unwrap();
113        assert_eq!(sig.family, ArchiveFamily::Rar15To40);
114        assert_eq!(sig.offset, 16);
115    }
116
117    #[test]
118    fn rejects_unknown_and_truncated_signatures() {
119        assert_eq!(detect_archive_family(b""), None);
120        assert_eq!(detect_archive_family(b"RAR!"), None);
121        assert_eq!(detect_archive_family(b"Rar!\x1a\x07"), None);
122        assert_eq!(find_archive_start(b"not an archive", 128), None);
123    }
124
125    #[test]
126    fn scan_limit_bounds_sfx_detection() {
127        let input = b"stub bytes RE~^payload";
128
129        assert_eq!(find_archive_start(input, 10), None);
130
131        let sig = find_archive_start(input, 11).unwrap();
132        assert_eq!(sig.family, ArchiveFamily::Rar13);
133        assert_eq!(sig.offset, 11);
134        assert_eq!(sig.length, RAR13_SIGNATURE.len());
135    }
136
137    #[test]
138    fn sfx_scan_limit_finds_signature_past_128kib_stub() {
139        // Real SFX installers routinely place the RAR payload past 128 KiB
140        // (modern WinRAR-built SFXes, Nero, anti-virus installers, etc.).
141        let mut stub = vec![0u8; 300 * 1024];
142        stub.extend_from_slice(RAR15_SIGNATURE);
143        let sig = find_archive_start(&stub, SFX_SCAN_LIMIT).unwrap();
144        assert_eq!(sig.family, ArchiveFamily::Rar15To40);
145        assert_eq!(sig.offset, 300 * 1024);
146    }
147}