Skip to main content

stream_unpack/zip/structures/
local_file_header.rs

1use std::io::Cursor;
2
3use byteorder::{ReadBytesExt, LittleEndian};
4
5use crate::{decrypt::{Decryptor, DecryptorCreationError}, zip::structures::file_header::{ENCRYPTED_FLAG, STRONG_ENCRYPTION_FLAG}};
6
7use super::{CompressionMethod, file_header::{FileHeaderExtraField, Zip64ProcessedData, Zip64OriginalData}};
8
9#[cfg(feature = "zipcrypto")]
10use crate::decrypt::zipcrypto::ZipCryptoDecryptor;
11
12pub const LFH_SIGNATURE: u32 = 0x04034b50;
13pub const LFH_CONSTANT_SIZE: usize = 26;
14
15/// Represents the result of reading a ZIP local file header (LFH)
16///
17/// The layout of this object does not follow the original ZIP LFH structure
18#[derive(Debug, Clone)]
19pub struct LocalFileHeader {
20    pub version: u16,
21
22    pub flag: u16,
23
24    pub compression_method: Option<CompressionMethod>,
25
26    pub mod_time: u16,
27    pub mod_date: u16,
28
29    pub crc32: u32,
30
31    pub compressed_size: u64,
32    pub uncompressed_size: u64,
33
34    pub filename: String,
35
36    pub extra_fields: Vec<FileHeaderExtraField>,
37
38    pub zipcrypto_header: Option<[u8; 12]>,
39
40    pub header_size: usize
41}
42
43impl LocalFileHeader {
44    /// Attempts to read a local file header from the provided
45    /// byte buffer. Returns None if there isn't enought data
46    pub fn from_bytes(data: impl AsRef<[u8]>) -> Option<Self> {
47        let data = data.as_ref();
48        if data.len() < LFH_CONSTANT_SIZE {
49            return None;
50        }
51
52        let mut cursor = Cursor::new(data);
53
54        let version = cursor.read_u16::<LittleEndian>().unwrap();
55        let flag = cursor.read_u16::<LittleEndian>().unwrap();
56        let compression_method = cursor.read_u16::<LittleEndian>().unwrap();
57        let mod_time = cursor.read_u16::<LittleEndian>().unwrap();
58        let mod_date = cursor.read_u16::<LittleEndian>().unwrap();
59        let crc32 = cursor.read_u32::<LittleEndian>().unwrap();
60        let compressed_size = cursor.read_u32::<LittleEndian>().unwrap();
61        let uncompressed_size = cursor.read_u32::<LittleEndian>().unwrap();
62        let filename_length = cursor.read_u16::<LittleEndian>().unwrap();
63        let extra_fields_length = cursor.read_u16::<LittleEndian>().unwrap();
64
65        let filename_length = filename_length as usize;
66        let extra_fields_length = extra_fields_length as usize;
67        if data.len() < LFH_CONSTANT_SIZE + filename_length + extra_fields_length {
68            return None;
69        }
70
71        let compression_method = CompressionMethod::from_id(compression_method);
72
73        let filename_start = LFH_CONSTANT_SIZE;
74        let filename_end = filename_start + filename_length;
75        let filename = String::from_utf8_lossy(&data[filename_start..filename_end]).to_string();
76
77        let extra_fields_start = filename_end;
78        let extra_fields_end = extra_fields_start + extra_fields_length;
79        let Some(extra_fields) = FileHeaderExtraField::read_extra_fields(&data[extra_fields_start..extra_fields_end]) else {
80            return None;
81        };
82
83        let original_zip64_data = Zip64OriginalData {
84            uncompressed_size,
85            compressed_size,
86            ..Default::default()
87        };
88
89        let Some(Zip64ProcessedData {
90            uncompressed_size,
91            compressed_size,
92            ..
93        }) = original_zip64_data.process(&extra_fields) else {
94            return None;
95        };
96
97        let (zipcrypto_header, header_size) = if flag & ENCRYPTED_FLAG != 0 && flag & STRONG_ENCRYPTION_FLAG == 0 {
98            let zipcrypto_header_start = extra_fields_end;
99            let zipcrypto_header_end = zipcrypto_header_start + 12;
100            if zipcrypto_header_end > data.len() {
101                return None;
102            }
103
104            (Some(data[zipcrypto_header_start..zipcrypto_header_end].try_into().unwrap()), zipcrypto_header_end)
105        } else { (None, extra_fields_end) };
106
107        let compressed_size = if zipcrypto_header.is_some() { compressed_size - 12 } else { compressed_size };
108
109        Some(Self {
110            version,
111            flag,
112            compression_method,
113            mod_time,
114            mod_date,
115            crc32,
116            compressed_size,
117            uncompressed_size,
118            filename,
119            extra_fields,
120
121            zipcrypto_header,
122
123            header_size
124        })
125    }
126
127    pub fn is_directory(&self) -> bool {
128        self.filename.ends_with('/')
129    }
130
131    pub fn is_encrypted(&self) -> bool {
132        return self.flag & ENCRYPTED_FLAG != 0;
133    }
134
135    /// Create a [Decryptor] for the file described by this LFH with the given password. Returns
136    /// an error if the file is not encrypted.
137    pub fn create_decryptor(&self, password: &[u8]) -> Result<Box<dyn Decryptor>, DecryptorCreationError> {
138        if !self.is_encrypted() {
139            return Err(DecryptorCreationError::NotEncrypted);
140        }
141
142        // ZipCrypto
143        #[cfg(feature = "zipcrypto")]
144        if let Some(zipcrypto_header) = self.zipcrypto_header {
145            return Ok(Box::new(
146                ZipCryptoDecryptor::new(password, zipcrypto_header, self.crc32)?
147            ));
148        }
149
150        Err(DecryptorCreationError::Generic("unsupported encryption method".to_string()))
151    }
152}