rar_stream/parsing/rar5/
file_header.rs1use super::{Rar5HeaderFlags, VintReader};
7use crate::crc32::crc32 as compute_crc32;
8use crate::error::{RarError, Result};
9
10#[inline]
12fn safe_usize(value: u64) -> Result<usize> {
13 usize::try_from(value).map_err(|_| RarError::InvalidHeader)
14}
15
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
18pub struct Rar5FileFlags {
19 pub is_directory: bool,
21 pub has_mtime: bool,
23 pub has_crc32: bool,
25 pub unpacked_size_unknown: bool,
27}
28
29impl From<u64> for Rar5FileFlags {
30 fn from(flags: u64) -> Self {
31 Self {
32 is_directory: flags & 0x0001 != 0,
33 has_mtime: flags & 0x0002 != 0,
34 has_crc32: flags & 0x0004 != 0,
35 unpacked_size_unknown: flags & 0x0008 != 0,
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct Rar5CompressionInfo {
43 pub version: u8,
45 pub is_solid: bool,
47 pub method: u8,
49 pub dict_size_log: u8,
51}
52
53impl From<u64> for Rar5CompressionInfo {
54 fn from(info: u64) -> Self {
55 Self {
56 version: (info & 0x3F) as u8,
57 is_solid: (info >> 6) & 1 != 0,
58 method: ((info >> 7) & 0x07) as u8,
59 dict_size_log: ((info >> 10) & 0x0F) as u8 + 17,
60 }
61 }
62}
63
64impl Rar5CompressionInfo {
65 pub fn dict_size(&self) -> u64 {
67 1u64 << self.dict_size_log
68 }
69
70 pub fn is_stored(&self) -> bool {
72 self.method == 0
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78#[repr(u8)]
79pub enum Rar5HostOs {
80 Windows = 0,
81 Unix = 1,
82}
83
84impl TryFrom<u64> for Rar5HostOs {
85 type Error = ();
86
87 fn try_from(value: u64) -> std::result::Result<Self, Self::Error> {
88 match value {
89 0 => Ok(Self::Windows),
90 1 => Ok(Self::Unix),
91 _ => Err(()),
92 }
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct Rar5FileHeader {
99 pub crc32: u32,
101 pub header_size: u64,
103 pub header_flags: Rar5HeaderFlags,
105 pub file_flags: Rar5FileFlags,
107 pub unpacked_size: u64,
109 pub attributes: u64,
111 pub mtime: Option<u32>,
113 pub file_crc32: Option<u32>,
115 pub compression: Rar5CompressionInfo,
117 pub host_os: Rar5HostOs,
119 pub name: String,
121 pub packed_size: u64,
123 pub extra_area: Option<Vec<u8>>,
125}
126
127impl Rar5FileHeader {
128 pub fn continues_from_previous(&self) -> bool {
130 self.header_flags.split_before
131 }
132
133 pub fn continues_in_next(&self) -> bool {
135 self.header_flags.split_after
136 }
137
138 pub fn is_stored(&self) -> bool {
140 self.compression.is_stored()
141 }
142
143 pub fn is_directory(&self) -> bool {
145 self.file_flags.is_directory
146 }
147
148 pub fn is_encrypted(&self) -> bool {
150 self.header_flags.has_extra_area && self.encryption_info().is_some()
151 }
152
153 pub fn encryption_info(&self) -> Option<&[u8]> {
156 let extra = self.extra_area.as_ref()?;
157 Self::find_extra_field(extra, 0x01) }
159
160 fn find_extra_field(extra: &[u8], field_type: u64) -> Option<&[u8]> {
162 let mut pos = 0;
163 while pos < extra.len() {
164 let mut reader = super::VintReader::new(&extra[pos..]);
167 let size = reader.read()?;
168 let size_vint_len = reader.position();
169 let ftype = reader.read()?;
170 let header_consumed = reader.position();
171
172 let size_usize = usize::try_from(size).ok()?;
173
174 if ftype == field_type {
175 let data_start = pos.checked_add(header_consumed)?;
177 let data_end = pos.checked_add(size_vint_len)?.checked_add(size_usize)?;
178 if data_end <= extra.len() {
179 return Some(&extra[data_start..data_end]);
180 }
181 }
182
183 pos = pos.checked_add(size_vint_len)?.checked_add(size_usize)?;
184 }
185 None
186 }
187}
188
189pub struct Rar5FileHeaderParser;
190
191impl Rar5FileHeaderParser {
192 pub fn parse(buffer: &[u8]) -> Result<(Rar5FileHeader, usize)> {
194 if buffer.len() < 12 {
195 return Err(RarError::BufferTooSmall {
196 needed: 12,
197 have: buffer.len(),
198 });
199 }
200
201 let mut reader = VintReader::new(buffer);
202
203 let crc32 = reader.read_u32_le().ok_or(RarError::InvalidHeader)?;
205
206 let header_size = reader.read().ok_or(RarError::InvalidHeader)?;
208
209 let header_content_start = reader.position();
211
212 let header_type = reader.read().ok_or(RarError::InvalidHeader)?;
214 if header_type != 2 {
215 return Err(RarError::InvalidHeader);
216 }
217
218 let header_flags_raw = reader.read().ok_or(RarError::InvalidHeader)?;
220 let header_flags = Rar5HeaderFlags::from(header_flags_raw);
221
222 let extra_area_size = if header_flags.has_extra_area {
224 reader.read().ok_or(RarError::InvalidHeader)?
225 } else {
226 0
227 };
228
229 let packed_size = if header_flags.has_data_area {
231 reader.read().ok_or(RarError::InvalidHeader)?
232 } else {
233 0
234 };
235
236 let file_flags_raw = reader.read().ok_or(RarError::InvalidHeader)?;
238 let file_flags = Rar5FileFlags::from(file_flags_raw);
239
240 let unpacked_size = reader.read().ok_or(RarError::InvalidHeader)?;
241 let attributes = reader.read().ok_or(RarError::InvalidHeader)?;
242
243 let mtime = if file_flags.has_mtime {
245 Some(reader.read_u32_le().ok_or(RarError::InvalidHeader)?)
246 } else {
247 None
248 };
249
250 let file_crc32 = if file_flags.has_crc32 {
252 Some(reader.read_u32_le().ok_or(RarError::InvalidHeader)?)
253 } else {
254 None
255 };
256
257 let compression_raw = reader.read().ok_or(RarError::InvalidHeader)?;
259 let compression = Rar5CompressionInfo::from(compression_raw);
260
261 let host_os_raw = reader.read().ok_or(RarError::InvalidHeader)?;
263 let host_os = Rar5HostOs::try_from(host_os_raw).map_err(|()| RarError::InvalidHeader)?;
264
265 let name_len = reader.read().ok_or(RarError::InvalidHeader)?;
267 let name_bytes = reader
268 .read_bytes(safe_usize(name_len)?)
269 .ok_or(RarError::InvalidHeader)?;
270 let name = String::from_utf8_lossy(name_bytes).into_owned();
271
272 let extra_area = if extra_area_size > 0 {
274 let extra_bytes = reader
275 .read_bytes(safe_usize(extra_area_size)?)
276 .ok_or(RarError::InvalidHeader)?;
277 Some(extra_bytes.to_vec())
278 } else {
279 None
280 };
281
282 let total_consumed = header_content_start
285 .checked_add(safe_usize(header_size)?)
286 .ok_or(RarError::InvalidHeader)?;
287
288 if total_consumed > 4 && total_consumed <= buffer.len() {
290 let actual_crc = compute_crc32(&buffer[4..total_consumed]);
291 if actual_crc != crc32 {
292 return Err(RarError::CrcMismatch {
293 expected: crc32,
294 actual: actual_crc,
295 });
296 }
297 }
298
299 Ok((
300 Rar5FileHeader {
301 crc32,
302 header_size,
303 header_flags,
304 file_flags,
305 unpacked_size,
306 attributes,
307 mtime,
308 file_crc32,
309 compression,
310 host_os,
311 name,
312 packed_size,
313 extra_area,
314 },
315 total_consumed,
316 ))
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn test_compression_info() {
326 let info = Rar5CompressionInfo::from(0);
329 assert_eq!(info.version, 0);
330 assert_eq!(info.method, 0);
331 assert_eq!(info.dict_size_log, 17);
332 assert!(!info.is_solid);
333 assert!(info.is_stored());
334 }
335
336 #[test]
337 fn test_compression_with_method() {
338 let info = Rar5CompressionInfo::from(0x180);
340 assert_eq!(info.method, 3);
341 }
342
343 #[test]
344 fn test_stored_file() {
345 let info = Rar5CompressionInfo::from(0);
346 assert!(info.is_stored());
347 }
348}