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
7pub 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 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}