Skip to main content

rar_stream/parsing/
file_header.rs

1//! File header parser.
2//!
3//! Each file in a RAR archive has a file header that describes
4//! the file's name, size, compression method, etc.
5
6use crate::error::{RarError, Result};
7
8/// File header type constant.
9pub const FILE_HEADER_TYPE: u8 = 0x74; // 116
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct FileHeader {
13    pub crc: u16,
14    pub header_type: u8,
15    pub flags: u16,
16    pub head_size: u16,
17    pub packed_size: u64,
18    pub unpacked_size: u64,
19    pub host_os: u8,
20    pub file_crc: u32,
21    pub timestamp: u32,
22    pub version: u8,
23    pub method: u8,
24    pub name_size: u16,
25    pub attributes: u32,
26    pub name: String,
27    // Parsed flags
28    pub continues_from_previous: bool,
29    pub continues_in_next: bool,
30    pub is_encrypted: bool,
31    pub has_comment: bool,
32    pub has_info_from_previous: bool,
33    pub has_high_size: bool,
34    pub has_special_name: bool,
35    pub has_salt: bool,
36    pub has_extended_time: bool,
37    /// 8-byte encryption salt (if has_salt is true)
38    pub salt: Option<[u8; 8]>,
39}
40
41pub struct FileHeaderParser;
42
43impl FileHeaderParser {
44    /// Maximum header size to read (includes variable-length filename).
45    pub const HEADER_SIZE: usize = 280;
46    /// Minimum fixed header size before filename.
47    const MIN_HEADER_SIZE: usize = 32;
48
49    pub fn parse(buffer: &[u8]) -> Result<FileHeader> {
50        if buffer.len() < Self::MIN_HEADER_SIZE {
51            return Err(RarError::BufferTooSmall {
52                needed: Self::MIN_HEADER_SIZE,
53                have: buffer.len(),
54            });
55        }
56
57        let mut offset = 0;
58
59        let crc = u16::from_le_bytes([buffer[offset], buffer[offset + 1]]);
60        offset += 2;
61
62        let header_type = buffer[offset];
63        offset += 1;
64
65        let flags = u16::from_le_bytes([buffer[offset], buffer[offset + 1]]);
66        offset += 2;
67
68        let head_size = u16::from_le_bytes([buffer[offset], buffer[offset + 1]]);
69        offset += 2;
70
71        let mut packed_size = u32::from_le_bytes([
72            buffer[offset],
73            buffer[offset + 1],
74            buffer[offset + 2],
75            buffer[offset + 3],
76        ]) as u64;
77        offset += 4;
78
79        let mut unpacked_size = u32::from_le_bytes([
80            buffer[offset],
81            buffer[offset + 1],
82            buffer[offset + 2],
83            buffer[offset + 3],
84        ]) as u64;
85        offset += 4;
86
87        let host_os = buffer[offset];
88        offset += 1;
89
90        let file_crc = u32::from_le_bytes([
91            buffer[offset],
92            buffer[offset + 1],
93            buffer[offset + 2],
94            buffer[offset + 3],
95        ]);
96        offset += 4;
97
98        let timestamp = u32::from_le_bytes([
99            buffer[offset],
100            buffer[offset + 1],
101            buffer[offset + 2],
102            buffer[offset + 3],
103        ]);
104        offset += 4;
105
106        let version = buffer[offset];
107        offset += 1;
108
109        let method = buffer[offset];
110        offset += 1;
111
112        let name_size = u16::from_le_bytes([buffer[offset], buffer[offset + 1]]);
113        offset += 2;
114
115        let attributes = u32::from_le_bytes([
116            buffer[offset],
117            buffer[offset + 1],
118            buffer[offset + 2],
119            buffer[offset + 3],
120        ]);
121        offset += 4;
122
123        // Parse flags - RAR4 file header flags
124        let continues_from_previous = (flags & 0x0001) != 0;
125        let continues_in_next = (flags & 0x0002) != 0;
126        let is_encrypted = (flags & 0x0004) != 0;
127        let has_comment = (flags & 0x0008) != 0;
128        let has_info_from_previous = (flags & 0x0010) != 0;
129        let has_high_size = (flags & 0x0100) != 0; // LHD_LARGE - 64-bit sizes follow
130        let has_special_name = (flags & 0x0040) != 0; // LHD_UNICODE
131        let has_salt = (flags & 0x0080) != 0;
132        let has_extended_time = (flags & 0x0200) != 0;
133
134        // Handle 64-bit sizes
135        if has_high_size && buffer.len() >= offset + 8 {
136            let high_packed = u32::from_le_bytes([
137                buffer[offset],
138                buffer[offset + 1],
139                buffer[offset + 2],
140                buffer[offset + 3],
141            ]) as u64;
142            offset += 4;
143            let high_unpacked = u32::from_le_bytes([
144                buffer[offset],
145                buffer[offset + 1],
146                buffer[offset + 2],
147                buffer[offset + 3],
148            ]) as u64;
149            offset += 4;
150
151            packed_size |= high_packed << 32;
152            unpacked_size |= high_unpacked << 32;
153        }
154
155        // Parse filename
156        let name_end = offset
157            .checked_add(name_size as usize)
158            .ok_or(RarError::InvalidHeader)?;
159        if buffer.len() < name_end {
160            return Err(RarError::BufferTooSmall {
161                needed: name_end,
162                have: buffer.len(),
163            });
164        }
165        let name = String::from_utf8_lossy(&buffer[offset..name_end]).to_string();
166        offset = name_end;
167
168        // Parse salt if present (8 bytes after filename).
169        // Salt is present when has_salt flag is set, OR when the file is
170        // encrypted and the header has room for it (some RAR versions
171        // store the salt without setting the has_salt flag).
172        let salt = if (has_salt || is_encrypted)
173            && buffer.len() >= offset + 8
174            && head_size as usize > offset
175        {
176            let mut s = [0u8; 8];
177            s.copy_from_slice(&buffer[offset..offset + 8]);
178            Some(s)
179        } else {
180            None
181        };
182
183        Ok(FileHeader {
184            crc,
185            header_type,
186            flags,
187            head_size,
188            packed_size,
189            unpacked_size,
190            host_os,
191            file_crc,
192            timestamp,
193            version,
194            method,
195            name_size,
196            attributes,
197            name,
198            continues_from_previous,
199            continues_in_next,
200            is_encrypted,
201            has_comment,
202            has_info_from_previous,
203            has_high_size,
204            has_special_name,
205            has_salt,
206            has_extended_time,
207            salt,
208        })
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_parse_file_header() {
218        // Minimal file header with a 4-byte filename "test"
219        let mut buffer = vec![0u8; 36];
220        buffer[2] = FILE_HEADER_TYPE; // type
221        buffer[5] = 36; // head_size low byte
222        buffer[26] = 4; // name_size = 4
223        buffer[32] = b't';
224        buffer[33] = b'e';
225        buffer[34] = b's';
226        buffer[35] = b't';
227
228        let header = FileHeaderParser::parse(&buffer).unwrap();
229        assert_eq!(header.header_type, FILE_HEADER_TYPE);
230        assert_eq!(header.name, "test");
231    }
232
233    #[test]
234    fn test_compression_method() {
235        let mut buffer = vec![0u8; 36];
236        buffer[2] = FILE_HEADER_TYPE;
237        buffer[5] = 36;
238        buffer[25] = 0x30; // method = Store (no compression) - at offset 25
239        buffer[26] = 4; // name_size low byte
240        buffer[32..36].copy_from_slice(b"test");
241
242        let header = FileHeaderParser::parse(&buffer).unwrap();
243        assert_eq!(header.method, 0x30); // Store method
244    }
245}