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::error::{RarError, Result};
8
9/// Safely cast u64 to usize, returning an error on 32-bit overflow.
10#[inline]
11fn safe_usize(value: u64) -> Result<usize> {
12    usize::try_from(value).map_err(|_| RarError::InvalidHeader)
13}
14
15/// RAR5 file flags (specific to file header).
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
17pub struct Rar5FileFlags {
18    /// File is a directory
19    pub is_directory: bool,
20    /// File modification time is present
21    pub has_mtime: bool,
22    /// File CRC32 is present
23    pub has_crc32: bool,
24    /// Unpacked size is unknown
25    pub unpacked_size_unknown: bool,
26}
27
28impl From<u64> for Rar5FileFlags {
29    fn from(flags: u64) -> Self {
30        Self {
31            is_directory: flags & 0x0001 != 0,
32            has_mtime: flags & 0x0002 != 0,
33            has_crc32: flags & 0x0004 != 0,
34            unpacked_size_unknown: flags & 0x0008 != 0,
35        }
36    }
37}
38
39/// RAR5 compression information.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct Rar5CompressionInfo {
42    /// Compression algorithm version
43    pub version: u8,
44    /// Solid flag
45    pub is_solid: bool,
46    /// Compression method (0 = store, 1-5 = compression levels)
47    pub method: u8,
48    /// Dictionary size as power of 2 (minimum 17 = 128KB)
49    pub dict_size_log: u8,
50}
51
52impl From<u64> for Rar5CompressionInfo {
53    fn from(info: u64) -> Self {
54        Self {
55            version: (info & 0x3F) as u8,
56            is_solid: (info >> 6) & 1 != 0,
57            method: ((info >> 7) & 0x07) as u8,
58            dict_size_log: ((info >> 10) & 0x0F) as u8 + 17,
59        }
60    }
61}
62
63impl Rar5CompressionInfo {
64    /// Get dictionary size in bytes.
65    pub fn dict_size(&self) -> u64 {
66        1u64 << self.dict_size_log
67    }
68
69    /// Check if file is stored (not compressed).
70    pub fn is_stored(&self) -> bool {
71        self.method == 0
72    }
73}
74
75/// RAR5 host OS.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77#[repr(u8)]
78pub enum Rar5HostOs {
79    Windows = 0,
80    Unix = 1,
81}
82
83impl TryFrom<u64> for Rar5HostOs {
84    type Error = ();
85
86    fn try_from(value: u64) -> std::result::Result<Self, Self::Error> {
87        match value {
88            0 => Ok(Self::Windows),
89            1 => Ok(Self::Unix),
90            _ => Err(()),
91        }
92    }
93}
94
95/// Parsed RAR5 file header.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct Rar5FileHeader {
98    /// Header CRC32
99    pub crc32: u32,
100    /// Total header size in bytes
101    pub header_size: u64,
102    /// Common header flags
103    pub header_flags: Rar5HeaderFlags,
104    /// File-specific flags
105    pub file_flags: Rar5FileFlags,
106    /// Unpacked (original) file size
107    pub unpacked_size: u64,
108    /// File attributes
109    pub attributes: u64,
110    /// Modification time (if present)
111    pub mtime: Option<u32>,
112    /// File CRC32 (if present)
113    pub file_crc32: Option<u32>,
114    /// Compression info
115    pub compression: Rar5CompressionInfo,
116    /// Host OS
117    pub host_os: Rar5HostOs,
118    /// File name (UTF-8)
119    pub name: String,
120    /// Packed (compressed) data size
121    pub packed_size: u64,
122    /// Extra area data (if present)
123    pub extra_area: Option<Vec<u8>>,
124}
125
126impl Rar5FileHeader {
127    /// Check if file continues from previous volume.
128    pub fn continues_from_previous(&self) -> bool {
129        self.header_flags.split_before
130    }
131
132    /// Check if file continues in next volume.
133    pub fn continues_in_next(&self) -> bool {
134        self.header_flags.split_after
135    }
136
137    /// Check if file is stored (not compressed).
138    pub fn is_stored(&self) -> bool {
139        self.compression.is_stored()
140    }
141
142    /// Check if file is a directory.
143    pub fn is_directory(&self) -> bool {
144        self.file_flags.is_directory
145    }
146
147    /// Check if file is encrypted.
148    pub fn is_encrypted(&self) -> bool {
149        self.header_flags.has_extra_area && self.encryption_info().is_some()
150    }
151
152    /// Get encryption info from extra area if present.
153    /// Returns (encryption_data, flags) where flags indicate password check presence.
154    pub fn encryption_info(&self) -> Option<&[u8]> {
155        let extra = self.extra_area.as_ref()?;
156        Self::find_extra_field(extra, 0x01) // FHEXTRA_CRYPT = 0x01
157    }
158
159    /// Find a specific extra field by type.
160    fn find_extra_field(extra: &[u8], field_type: u64) -> Option<&[u8]> {
161        let mut pos = 0;
162        while pos < extra.len() {
163            // Each extra field: size (vint), type (vint), data
164            // size = total size of type + data (does NOT include the size vint itself)
165            let mut reader = super::VintReader::new(&extra[pos..]);
166            let size = reader.read()?;
167            let size_vint_len = reader.position();
168            let ftype = reader.read()?;
169            let header_consumed = reader.position();
170
171            if ftype == field_type {
172                // Return the data after the type field
173                let data_start = pos + header_consumed;
174                let size_usize = size as usize;
175                if size_usize as u64 != size {
176                    return None; // Overflow on 32-bit
177                }
178                let data_end = pos + size_vint_len + size_usize;
179                if data_end <= extra.len() {
180                    return Some(&extra[data_start..data_end]);
181                }
182            }
183
184            let size_usize = size as usize;
185            if size_usize as u64 != size {
186                return None;
187            }
188            pos += size_vint_len + size_usize;
189        }
190        None
191    }
192}
193
194pub struct Rar5FileHeaderParser;
195
196impl Rar5FileHeaderParser {
197    /// Parse RAR5 file header from buffer.
198    pub fn parse(buffer: &[u8]) -> Result<(Rar5FileHeader, usize)> {
199        if buffer.len() < 12 {
200            return Err(RarError::BufferTooSmall {
201                needed: 12,
202                have: buffer.len(),
203            });
204        }
205
206        let mut reader = VintReader::new(buffer);
207
208        // Read CRC32 (4 bytes, not vint)
209        let crc32 = reader.read_u32_le().ok_or(RarError::InvalidHeader)?;
210
211        // Read header size (vint) - this is the size of header content AFTER this vint
212        let header_size = reader.read().ok_or(RarError::InvalidHeader)?;
213
214        // Record position after reading header_size vint
215        let header_content_start = reader.position();
216
217        // Read header type (vint) - should be 2 for file header
218        let header_type = reader.read().ok_or(RarError::InvalidHeader)?;
219        if header_type != 2 {
220            return Err(RarError::InvalidHeader);
221        }
222
223        // Read header flags (vint)
224        let header_flags_raw = reader.read().ok_or(RarError::InvalidHeader)?;
225        let header_flags = Rar5HeaderFlags::from(header_flags_raw);
226
227        // Read extra area size if present
228        let extra_area_size = if header_flags.has_extra_area {
229            reader.read().ok_or(RarError::InvalidHeader)?
230        } else {
231            0
232        };
233
234        // Read data size if present (packed size)
235        let packed_size = if header_flags.has_data_area {
236            reader.read().ok_or(RarError::InvalidHeader)?
237        } else {
238            0
239        };
240
241        // File-specific fields
242        let file_flags_raw = reader.read().ok_or(RarError::InvalidHeader)?;
243        let file_flags = Rar5FileFlags::from(file_flags_raw);
244
245        let unpacked_size = reader.read().ok_or(RarError::InvalidHeader)?;
246        let attributes = reader.read().ok_or(RarError::InvalidHeader)?;
247
248        // Modification time (if present)
249        let mtime = if file_flags.has_mtime {
250            Some(reader.read_u32_le().ok_or(RarError::InvalidHeader)?)
251        } else {
252            None
253        };
254
255        // File CRC32 (if present)
256        let file_crc32 = if file_flags.has_crc32 {
257            Some(reader.read_u32_le().ok_or(RarError::InvalidHeader)?)
258        } else {
259            None
260        };
261
262        // Compression info
263        let compression_raw = reader.read().ok_or(RarError::InvalidHeader)?;
264        let compression = Rar5CompressionInfo::from(compression_raw);
265
266        // Host OS
267        let host_os_raw = reader.read().ok_or(RarError::InvalidHeader)?;
268        let host_os = Rar5HostOs::try_from(host_os_raw).map_err(|()| RarError::InvalidHeader)?;
269
270        // Name length and name
271        let name_len = reader.read().ok_or(RarError::InvalidHeader)?;
272        let name_bytes = reader
273            .read_bytes(safe_usize(name_len)?)
274            .ok_or(RarError::InvalidHeader)?;
275        let name = String::from_utf8_lossy(name_bytes).into_owned();
276
277        // Read extra area if present
278        let extra_area = if extra_area_size > 0 {
279            let extra_bytes = reader
280                .read_bytes(safe_usize(extra_area_size)?)
281                .ok_or(RarError::InvalidHeader)?;
282            Some(extra_bytes.to_vec())
283        } else {
284            None
285        };
286
287        // Calculate total bytes consumed
288        // header_size indicates bytes after the header_size vint itself
289        let total_consumed = header_content_start + safe_usize(header_size)?;
290
291        Ok((
292            Rar5FileHeader {
293                crc32,
294                header_size,
295                header_flags,
296                file_flags,
297                unpacked_size,
298                attributes,
299                mtime,
300                file_crc32,
301                compression,
302                host_os,
303                name,
304                packed_size,
305                extra_area,
306            },
307            total_consumed,
308        ))
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315
316    #[test]
317    fn test_compression_info() {
318        // Layout: bits 0-5 = version, bit 6 = solid, bits 7-9 = method, bits 10-13 = dict_size
319        // value 0 = version 0, not solid, method 0, dict_size 0 (+17 = 17)
320        let info = Rar5CompressionInfo::from(0);
321        assert_eq!(info.version, 0);
322        assert_eq!(info.method, 0);
323        assert_eq!(info.dict_size_log, 17);
324        assert!(!info.is_solid);
325        assert!(info.is_stored());
326    }
327
328    #[test]
329    fn test_compression_with_method() {
330        // method=3 at bits 7-9: 0b011 << 7 = 0x180
331        let info = Rar5CompressionInfo::from(0x180);
332        assert_eq!(info.method, 3);
333    }
334
335    #[test]
336    fn test_stored_file() {
337        let info = Rar5CompressionInfo::from(0);
338        assert!(info.is_stored());
339    }
340}