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 + name_size as usize;
157        if buffer.len() < name_end {
158            return Err(RarError::BufferTooSmall {
159                needed: name_end,
160                have: buffer.len(),
161            });
162        }
163        let name = String::from_utf8_lossy(&buffer[offset..name_end]).to_string();
164        offset = name_end;
165
166        // Parse salt if present (8 bytes after filename)
167        let salt = if has_salt && buffer.len() >= offset + 8 {
168            let mut s = [0u8; 8];
169            s.copy_from_slice(&buffer[offset..offset + 8]);
170            Some(s)
171        } else {
172            None
173        };
174
175        Ok(FileHeader {
176            crc,
177            header_type,
178            flags,
179            head_size,
180            packed_size,
181            unpacked_size,
182            host_os,
183            file_crc,
184            timestamp,
185            version,
186            method,
187            name_size,
188            attributes,
189            name,
190            continues_from_previous,
191            continues_in_next,
192            is_encrypted,
193            has_comment,
194            has_info_from_previous,
195            has_high_size,
196            has_special_name,
197            has_salt,
198            has_extended_time,
199            salt,
200        })
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn test_parse_file_header() {
210        // Minimal file header with a 4-byte filename "test"
211        let mut buffer = vec![0u8; 36];
212        buffer[2] = FILE_HEADER_TYPE; // type
213        buffer[5] = 36; // head_size low byte
214        buffer[26] = 4; // name_size = 4
215        buffer[32] = b't';
216        buffer[33] = b'e';
217        buffer[34] = b's';
218        buffer[35] = b't';
219
220        let header = FileHeaderParser::parse(&buffer).unwrap();
221        assert_eq!(header.header_type, FILE_HEADER_TYPE);
222        assert_eq!(header.name, "test");
223    }
224
225    #[test]
226    fn test_compression_method() {
227        let mut buffer = vec![0u8; 36];
228        buffer[2] = FILE_HEADER_TYPE;
229        buffer[5] = 36;
230        buffer[25] = 0x30; // method = Store (no compression) - at offset 25
231        buffer[26] = 4; // name_size low byte
232        buffer[32..36].copy_from_slice(b"test");
233
234        let header = FileHeaderParser::parse(&buffer).unwrap();
235        assert_eq!(header.method, 0x30); // Store method
236    }
237}