Skip to main content

async_zip/entry/
mod.rs

1// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
2// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
3
4pub mod builder;
5
6use std::ops::Deref;
7
8use futures_lite::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, SeekFrom};
9
10use crate::base::read::{get_combined_sizes, get_zip64_extra_field};
11use crate::entry::builder::ZipEntryBuilder;
12use crate::error::{Result, ZipError};
13use crate::spec::{
14    attribute::AttributeCompatibility,
15    consts::{LFH_SIGNATURE, NON_ZIP64_MAX_SIZE},
16    header::{ExtraField, LocalFileHeader},
17    parse::parse_extra_fields,
18    Compression,
19};
20use crate::{string::ZipString, ZipDateTime};
21
22/// An immutable store of data about a ZIP entry.
23///
24/// This type cannot be directly constructed so instead, the [`ZipEntryBuilder`] must be used. Internally this builder
25/// stores a [`ZipEntry`] so conversions between these two types via the [`From`] implementations will be
26/// non-allocating.
27#[derive(Clone, Debug)]
28pub struct ZipEntry {
29    pub(crate) filename: ZipString,
30    pub(crate) compression: Compression,
31    #[cfg(any(
32        feature = "deflate",
33        feature = "bzip2",
34        feature = "zstd",
35        feature = "lzma",
36        feature = "xz",
37        feature = "deflate64"
38    ))]
39    pub(crate) compression_level: async_compression::Level,
40    pub(crate) crc32: u32,
41    pub(crate) uncompressed_size: u64,
42    pub(crate) compressed_size: u64,
43    pub(crate) attribute_compatibility: AttributeCompatibility,
44    pub(crate) last_modification_date: ZipDateTime,
45    pub(crate) internal_file_attribute: u16,
46    pub(crate) external_file_attribute: u32,
47    pub(crate) extra_fields: Vec<ExtraField>,
48    pub(crate) comment: ZipString,
49    pub(crate) data_descriptor: bool,
50    pub(crate) file_offset: u64,
51}
52
53impl From<ZipEntryBuilder> for ZipEntry {
54    fn from(builder: ZipEntryBuilder) -> Self {
55        builder.0
56    }
57}
58
59impl ZipEntry {
60    pub(crate) fn new(filename: ZipString, compression: Compression) -> Self {
61        ZipEntry {
62            filename,
63            compression,
64            #[cfg(any(
65                feature = "deflate",
66                feature = "bzip2",
67                feature = "zstd",
68                feature = "lzma",
69                feature = "xz",
70                feature = "deflate64"
71            ))]
72            compression_level: async_compression::Level::Default,
73            crc32: 0,
74            uncompressed_size: 0,
75            compressed_size: 0,
76            attribute_compatibility: AttributeCompatibility::Unix,
77            last_modification_date: ZipDateTime::default_for_write(),
78            internal_file_attribute: 0,
79            external_file_attribute: 0,
80            extra_fields: Vec::new(),
81            comment: String::new().into(),
82            data_descriptor: false,
83            file_offset: 0,
84        }
85    }
86
87    /// Returns the entry's filename.
88    ///
89    /// ## Note
90    /// This will return the raw filename stored during ZIP creation. If calling this method on entries retrieved from
91    /// untrusted ZIP files, the filename should be sanitised before being used as a path to prevent [directory
92    /// traversal attacks](https://en.wikipedia.org/wiki/Directory_traversal_attack).
93    pub fn filename(&self) -> &ZipString {
94        &self.filename
95    }
96
97    /// Returns the entry's compression method.
98    pub fn compression(&self) -> Compression {
99        self.compression
100    }
101
102    /// Returns the entry's CRC32 value.
103    pub fn crc32(&self) -> u32 {
104        self.crc32
105    }
106
107    /// Returns the entry's uncompressed size.
108    pub fn uncompressed_size(&self) -> u64 {
109        self.uncompressed_size
110    }
111
112    /// Returns the entry's compressed size.
113    pub fn compressed_size(&self) -> u64 {
114        self.compressed_size
115    }
116
117    /// Returns the entry's attribute's host compatibility.
118    pub fn attribute_compatibility(&self) -> AttributeCompatibility {
119        self.attribute_compatibility
120    }
121
122    /// Returns the entry's last modification time & date.
123    pub fn last_modification_date(&self) -> &ZipDateTime {
124        &self.last_modification_date
125    }
126
127    /// Returns the entry's internal file attribute.
128    pub fn internal_file_attribute(&self) -> u16 {
129        self.internal_file_attribute
130    }
131
132    /// Returns the entry's external file attribute
133    pub fn external_file_attribute(&self) -> u32 {
134        self.external_file_attribute
135    }
136
137    /// Returns the entry's extra field data.
138    pub fn extra_fields(&self) -> &[ExtraField] {
139        &self.extra_fields
140    }
141
142    /// Returns the entry's file comment.
143    pub fn comment(&self) -> &ZipString {
144        &self.comment
145    }
146
147    /// Returns the entry's integer-based UNIX permissions.
148    ///
149    /// # Note
150    /// This will return None if the attribute host compatibility is not listed as Unix.
151    pub fn unix_permissions(&self) -> Option<u16> {
152        if !matches!(self.attribute_compatibility, AttributeCompatibility::Unix) {
153            return None;
154        }
155
156        Some(((self.external_file_attribute) >> 16) as u16)
157    }
158
159    /// Returns whether or not the entry represents a directory.
160    pub fn dir(&self) -> Result<bool> {
161        Ok(self.filename.as_str()?.ends_with('/'))
162    }
163
164    /// Returns whether or not the entry has a data descriptor.
165    pub fn data_descriptor(&self) -> bool {
166        self.data_descriptor
167    }
168
169    /// Returns the file offset in bytes of the local file header for this entry.
170    pub fn file_offset(&self) -> u64 {
171        self.file_offset
172    }
173}
174
175/// An immutable store of data about how a ZIP entry is stored within a specific archive.
176///
177/// Besides storing archive independent information like the size and timestamp it can also be used to query
178/// information about how the entry is stored in an archive.
179#[derive(Clone)]
180pub struct StoredZipEntry {
181    pub(crate) entry: ZipEntry,
182    // pub(crate) general_purpose_flag: GeneralPurposeFlag,
183    pub(crate) file_offset: u64,
184    pub(crate) header_size: u64,
185}
186
187impl StoredZipEntry {
188    /// Returns the offset in bytes to where the header of the entry starts.
189    pub fn header_offset(&self) -> u64 {
190        self.file_offset
191    }
192
193    /// Returns the combined size in bytes of the header, the filename, and any extra fields.
194    ///
195    /// Note: This uses the extra field length stored in the central directory, which may differ from that stored in
196    /// the local file header. See specification: <https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#732>
197    pub fn header_size(&self) -> u64 {
198        self.header_size
199    }
200
201    /// Seek to the offset in bytes where the data of the entry starts.
202    pub(crate) async fn seek_to_data_offset<R: AsyncRead + AsyncSeek + Unpin>(&self, mut reader: &mut R) -> Result<()> {
203        // Seek to the header
204        reader.seek(SeekFrom::Start(self.file_offset)).await?;
205
206        // Check the signature
207        let signature = {
208            let mut buffer = [0; 4];
209            reader.read_exact(&mut buffer).await?;
210            u32::from_le_bytes(buffer)
211        };
212
213        match signature {
214            LFH_SIGNATURE => (),
215            actual => return Err(ZipError::UnexpectedHeaderError(actual, LFH_SIGNATURE)),
216        };
217
218        // Read and validate the local file header's trailing data.
219        let header = LocalFileHeader::from_reader(&mut reader).await?;
220        if header.flags.data_descriptor != self.entry.data_descriptor {
221            return Err(ZipError::LocalFileHeaderDataDescriptorMismatch);
222        }
223
224        let local_filename = crate::base::read::io::read_bytes(&mut reader, header.file_name_length.into()).await?;
225        if local_filename.as_slice() != self.entry.filename().as_bytes() {
226            return Err(ZipError::LocalFileHeaderNameMismatch);
227        }
228
229        if !header.flags.data_descriptor
230            && ((header.compressed_size != NON_ZIP64_MAX_SIZE
231                && header.compressed_size as u64 != self.entry.compressed_size)
232                || (header.uncompressed_size != NON_ZIP64_MAX_SIZE
233                    && header.uncompressed_size as u64 != self.entry.uncompressed_size))
234        {
235            return Err(ZipError::LocalFileHeaderSizeMismatch);
236        }
237
238        let mut extra_field = vec![0; usize::from(header.extra_field_length)];
239        reader.read_exact(&mut extra_field).await?;
240        let extra_fields =
241            parse_extra_fields(extra_field, header.uncompressed_size, header.compressed_size, None, None)?;
242        let zip64_extra_field = get_zip64_extra_field(&extra_fields);
243
244        if !header.flags.data_descriptor
245            && zip64_extra_field.is_none()
246            && extra_fields.is_empty()
247            && (header.compressed_size == NON_ZIP64_MAX_SIZE || header.uncompressed_size == NON_ZIP64_MAX_SIZE)
248        {
249            return Err(ZipError::LocalFileHeaderSizeMismatch);
250        }
251
252        let (local_uncompressed_size, local_compressed_size) =
253            get_combined_sizes(header.uncompressed_size, header.compressed_size, &zip64_extra_field)?;
254
255        if !header.flags.data_descriptor
256            && (local_compressed_size != self.entry.compressed_size
257                || local_uncompressed_size != self.entry.uncompressed_size)
258        {
259            return Err(ZipError::LocalFileHeaderSizeMismatch);
260        }
261
262        Ok(())
263    }
264}
265
266impl Deref for StoredZipEntry {
267    type Target = ZipEntry;
268
269    fn deref(&self) -> &Self::Target {
270        &self.entry
271    }
272}