Skip to main content

rar_stream/parsing/rar5/
file_header.rs

1//! RAR5 file header parser.
2//!
3//! The file header contains information about each file in the archive,
4//! including name, size, compression method, and timestamps.
5
6use super::{Rar5HeaderFlags, VintReader};
7use crate::crc32::crc32 as compute_crc32;
8use crate::error::{RarError, Result};
9
10/// Safely cast u64 to usize, returning an error on 32-bit overflow.
11#[inline]
12fn safe_usize(value: u64) -> Result<usize> {
13    usize::try_from(value).map_err(|_| RarError::InvalidHeader)
14}
15
16/// RAR5 file flags (specific to file header).
17#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
18pub struct Rar5FileFlags {
19    /// File is a directory
20    pub is_directory: bool,
21    /// File modification time is present
22    pub has_mtime: bool,
23    /// File CRC32 is present
24    pub has_crc32: bool,
25    /// Unpacked size is unknown
26    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/// RAR5 compression information.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct Rar5CompressionInfo {
43    /// Compression algorithm version
44    pub version: u8,
45    /// Solid flag
46    pub is_solid: bool,
47    /// Compression method (0 = store, 1-5 = compression levels)
48    pub method: u8,
49    /// Dictionary size as power of 2 (minimum 17 = 128KB)
50    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    /// Get dictionary size in bytes.
66    pub fn dict_size(&self) -> u64 {
67        1u64 << self.dict_size_log
68    }
69
70    /// Check if file is stored (not compressed).
71    pub fn is_stored(&self) -> bool {
72        self.method == 0
73    }
74}
75
76/// RAR5 host OS.
77#[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/// Parsed RAR5 file header.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct Rar5FileHeader {
99    /// Header CRC32
100    pub crc32: u32,
101    /// Total header size in bytes
102    pub header_size: u64,
103    /// Common header flags
104    pub header_flags: Rar5HeaderFlags,
105    /// File-specific flags
106    pub file_flags: Rar5FileFlags,
107    /// Unpacked (original) file size
108    pub unpacked_size: u64,
109    /// File attributes
110    pub attributes: u64,
111    /// Modification time (if present)
112    pub mtime: Option<u32>,
113    /// File CRC32 (if present)
114    pub file_crc32: Option<u32>,
115    /// Compression info
116    pub compression: Rar5CompressionInfo,
117    /// Host OS
118    pub host_os: Rar5HostOs,
119    /// File name (UTF-8)
120    pub name: String,
121    /// Packed (compressed) data size
122    pub packed_size: u64,
123    /// Extra area data (if present)
124    pub extra_area: Option<Vec<u8>>,
125}
126
127impl Rar5FileHeader {
128    /// Check if file continues from previous volume.
129    pub fn continues_from_previous(&self) -> bool {
130        self.header_flags.split_before
131    }
132
133    /// Check if file continues in next volume.
134    pub fn continues_in_next(&self) -> bool {
135        self.header_flags.split_after
136    }
137
138    /// Check if file is stored (not compressed).
139    pub fn is_stored(&self) -> bool {
140        self.compression.is_stored()
141    }
142
143    /// Check if file is a directory.
144    pub fn is_directory(&self) -> bool {
145        self.file_flags.is_directory
146    }
147
148    /// Check if file is encrypted.
149    pub fn is_encrypted(&self) -> bool {
150        self.header_flags.has_extra_area && self.encryption_info().is_some()
151    }
152
153    /// Get encryption info from extra area if present.
154    /// Returns (encryption_data, flags) where flags indicate password check presence.
155    pub fn encryption_info(&self) -> Option<&[u8]> {
156        let extra = self.extra_area.as_ref()?;
157        Self::find_extra_field(extra, 0x01) // FHEXTRA_CRYPT = 0x01
158    }
159
160    /// Find a specific extra field by type.
161    fn find_extra_field(extra: &[u8], field_type: u64) -> Option<&[u8]> {
162        let mut pos = 0;
163        while pos < extra.len() {
164            // Each extra field: size (vint), type (vint), data
165            // size = total size of type + data (does NOT include the size vint itself)
166            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                // Return the data after the type field
176                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    /// Parse RAR5 file header from buffer.
193    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        // Read CRC32 (4 bytes, not vint)
204        let crc32 = reader.read_u32_le().ok_or(RarError::InvalidHeader)?;
205
206        // Read header size (vint) - this is the size of header content AFTER this vint
207        let header_size = reader.read().ok_or(RarError::InvalidHeader)?;
208
209        // Record position after reading header_size vint
210        let header_content_start = reader.position();
211
212        // Read header type (vint) - should be 2 for file header
213        let header_type = reader.read().ok_or(RarError::InvalidHeader)?;
214        if header_type != 2 {
215            return Err(RarError::InvalidHeader);
216        }
217
218        // Read header flags (vint)
219        let header_flags_raw = reader.read().ok_or(RarError::InvalidHeader)?;
220        let header_flags = Rar5HeaderFlags::from(header_flags_raw);
221
222        // Read extra area size if present
223        let extra_area_size = if header_flags.has_extra_area {
224            reader.read().ok_or(RarError::InvalidHeader)?
225        } else {
226            0
227        };
228
229        // Read data size if present (packed size)
230        let packed_size = if header_flags.has_data_area {
231            reader.read().ok_or(RarError::InvalidHeader)?
232        } else {
233            0
234        };
235
236        // File-specific fields
237        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        // Modification time (if present)
244        let mtime = if file_flags.has_mtime {
245            Some(reader.read_u32_le().ok_or(RarError::InvalidHeader)?)
246        } else {
247            None
248        };
249
250        // File CRC32 (if present)
251        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        // Compression info
258        let compression_raw = reader.read().ok_or(RarError::InvalidHeader)?;
259        let compression = Rar5CompressionInfo::from(compression_raw);
260
261        // Host OS
262        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        // Name length and name
266        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        // Read extra area if present
273        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        // Calculate total bytes consumed
283        // header_size indicates bytes after the header_size vint itself
284        let total_consumed = header_content_start
285            .checked_add(safe_usize(header_size)?)
286            .ok_or(RarError::InvalidHeader)?;
287
288        // Validate CRC32 over header content (everything after the 4-byte CRC field)
289        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        // Layout: bits 0-5 = version, bit 6 = solid, bits 7-9 = method, bits 10-13 = dict_size
327        // value 0 = version 0, not solid, method 0, dict_size 0 (+17 = 17)
328        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        // method=3 at bits 7-9: 0b011 << 7 = 0x180
339        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}