Skip to main content

async_zip/base/read/
cd.rs

1use futures_lite::io::{AsyncRead, AsyncReadExt};
2
3use crate::base::read::counting::Counting;
4use crate::base::read::io::CombinedCentralDirectoryRecord;
5use crate::base::read::{detect_comment, detect_filename, get_combined_sizes, get_zip64_extra_field, io};
6use crate::error::{Result, ZipError};
7use crate::spec::consts::{CDH_SIGNATURE, EOCDR_SIGNATURE, NON_ZIP64_MAX_SIZE, ZIP64_EOCDR_SIGNATURE};
8use crate::spec::header::{
9    CentralDirectoryRecord, EndOfCentralDirectoryHeader, Zip64EndOfCentralDirectoryLocator,
10    Zip64EndOfCentralDirectoryRecord,
11};
12use crate::spec::parse::parse_extra_fields;
13use crate::ZipString;
14
15/// An entry returned by the [`CentralDirectoryReader`].
16pub enum Entry {
17    CentralDirectoryEntry(CentralDirectoryEntry),
18    EndOfCentralDirectoryRecord {
19        /// The combined end-of-central-directory record, which may include ZIP64 information.
20        record: CombinedCentralDirectoryRecord,
21        /// The comment associated with the end-of-central-directory record.
22        comment: ZipString,
23        /// Whether the end-of-central-directory record contains extensible data.
24        extensible: bool,
25    },
26}
27
28/// An entry in the ZIP file's central directory.
29pub struct CentralDirectoryEntry {
30    /// The compressed size of the entry, taking into account ZIP64 if necessary.
31    pub(crate) compressed_size: u64,
32    /// The uncompressed size of the entry, taking into account ZIP64 if necessary.
33    pub(crate) uncompressed_size: u64,
34    /// The file offset of the entry in the ZIP file, taking into account ZIP64 if necessary.
35    pub(crate) lh_offset: u64,
36    /// The end-of-central-directory record header.
37    pub(crate) header: CentralDirectoryRecord,
38    /// The filename of the entry.
39    pub(crate) filename: ZipString,
40}
41
42impl CentralDirectoryEntry {
43    /// Returns the entry's filename.
44    ///
45    /// ## Note
46    /// This will return the raw filename stored during ZIP creation. If calling this method on entries retrieved from
47    /// untrusted ZIP files, the filename should be sanitised before being used as a path to prevent [directory
48    /// traversal attacks](https://en.wikipedia.org/wiki/Directory_traversal_attack).
49    pub fn filename(&self) -> &ZipString {
50        &self.filename
51    }
52
53    /// Returns whether or not the entry represents a directory.
54    pub fn dir(&self) -> Result<bool> {
55        Ok(self.filename.as_str()?.ends_with('/'))
56    }
57
58    /// Returns the entry's integer-based UNIX permissions.
59    pub fn unix_permissions(&self) -> Option<u32> {
60        Some((self.header.exter_attr) >> 16)
61    }
62
63    /// Returns the CRC32 checksum of the entry.
64    pub fn crc32(&self) -> u32 {
65        self.header.crc
66    }
67
68    /// Returns the file offset of the entry in the ZIP file.
69    pub fn file_offset(&self) -> u64 {
70        self.lh_offset
71    }
72
73    /// Returns the entry's compressed size.
74    pub fn compressed_size(&self) -> u64 {
75        self.compressed_size
76    }
77
78    /// Returns the entry's uncompressed size.
79    pub fn uncompressed_size(&self) -> u64 {
80        self.uncompressed_size
81    }
82}
83
84#[derive(Clone)]
85pub struct CentralDirectoryReader<R> {
86    reader: R,
87    initial: bool,
88    offset: u64,
89}
90
91impl<R> CentralDirectoryReader<Counting<R>>
92where
93    R: AsyncRead + Unpin,
94{
95    /// Constructs a new ZIP reader from a non-seekable source.
96    pub fn new(reader: R, offset: u64) -> Self {
97        Self { reader: Counting::new(reader), offset, initial: true }
98    }
99
100    /// Reads the next [`CentralDirectoryEntry`] from the underlying source, advancing the
101    /// reader to the next record.
102    ///
103    /// Returns `Ok(EndOfCentralDirectoryRecord)` if the end of the central directory record has
104    /// been reached and at most 4 KiB of NUL padding remains before EOF. The source must be finite
105    /// or bounded to a single archive; returning the end record consumes that padding. Zeros in
106    /// the declared comment do not count toward the padding limit.
107    /// Archive and entry comments containing bytes `0x01` through `0x08` are rejected as a conservative
108    /// defense against embedded ZIP records. This does not prohibit ZIP files stored as entries.
109    pub async fn next(&mut self) -> Result<Entry> {
110        // Skip the first `CDH_SIGNATURE`. The `CentralDirectoryReader` is assumed to pick up from
111        // where the streaming `ZipFileReader` left off, which means that the first record's
112        // signature has already been read.
113        if self.initial {
114            self.initial = false;
115        } else {
116            let signature = {
117                let mut buffer = [0; 4];
118                self.reader.read_exact(&mut buffer).await?;
119                u32::from_le_bytes(buffer)
120            };
121            let offset = self.offset + self.reader.bytes_read();
122            match signature {
123                CDH_SIGNATURE => (),
124                EOCDR_SIGNATURE => {
125                    // Read the end-of-central-directory header.
126                    let eocdr = EndOfCentralDirectoryHeader::from_reader(&mut self.reader).await?;
127
128                    let observed_directory_size = offset.saturating_sub(self.offset);
129                    if eocdr.size_cent_dir as u64 != observed_directory_size {
130                        return Err(ZipError::InvalidCentralDirectorySize {
131                            expected: eocdr.size_cent_dir as u64,
132                            actual: observed_directory_size,
133                        });
134                    }
135
136                    // Read the EOCDR comment.
137                    let comment =
138                        io::read_string(&mut self.reader, eocdr.file_comm_length.into(), crate::StringEncoding::Utf8)
139                            .await?;
140
141                    // Verify that the EOCDR offset matches the current reader offset.
142                    if eocdr.central_directory_offset() != self.offset {
143                        return Err(ZipError::InvalidEndOfCentralDirectoryOffset(
144                            eocdr.central_directory_offset(),
145                            offset,
146                        ));
147                    }
148
149                    let record = CombinedCentralDirectoryRecord::try_from(&eocdr)?;
150                    io::validate_comment(&comment)?;
151                    io::validate_trailing_contents(&mut self.reader).await?;
152                    return Ok(Entry::EndOfCentralDirectoryRecord { record, comment, extensible: false });
153                }
154                ZIP64_EOCDR_SIGNATURE => {
155                    // Read the ZIP64 EOCDR.
156                    let zip64_eocdr = Zip64EndOfCentralDirectoryRecord::from_reader(&mut self.reader).await?;
157
158                    // Skip the extensible data field.
159                    let extensible = if zip64_eocdr.size_of_zip64_end_of_cd_record > 44 {
160                        let extensible_data_size = zip64_eocdr.size_of_zip64_end_of_cd_record - 44;
161                        io::skip_bytes(&mut self.reader, extensible_data_size).await?;
162                        true
163                    } else {
164                        false
165                    };
166
167                    // Read the ZIP64 EOCDR locator.
168                    let Some(zip64_eocdl) =
169                        Zip64EndOfCentralDirectoryLocator::try_from_reader(&mut self.reader).await?
170                    else {
171                        return Err(ZipError::MissingZip64EndOfCentralDirectoryLocator);
172                    };
173
174                    // Verify that the ZIP64 EOCDR locator points to the correct offset.
175                    if zip64_eocdl.relative_offset != offset {
176                        return Err(ZipError::InvalidZip64EndOfCentralDirectoryLocatorOffset(
177                            zip64_eocdl.relative_offset,
178                            offset,
179                        ));
180                    }
181
182                    // Read the EOCDR signature.
183                    let signature = {
184                        let mut buffer = [0; 4];
185                        self.reader.read_exact(&mut buffer).await?;
186                        u32::from_le_bytes(buffer)
187                    };
188                    if signature != EOCDR_SIGNATURE {
189                        return Err(ZipError::UnexpectedHeaderError(signature, EOCDR_SIGNATURE));
190                    }
191
192                    // Read the end-of-central-directory header.
193                    let eocdr = EndOfCentralDirectoryHeader::from_reader(&mut self.reader).await?;
194
195                    // Read the EOCDR comment.
196                    let comment =
197                        io::read_string(&mut self.reader, eocdr.file_comm_length.into(), crate::StringEncoding::Utf8)
198                            .await?;
199
200                    // Combine the EOCDR and ZIP64 EOCDR.
201                    let zip64_directory_size = zip64_eocdr.directory_size;
202                    let combined = CombinedCentralDirectoryRecord::combine(eocdr, zip64_eocdr)?;
203
204                    let observed_directory_size = offset.saturating_sub(self.offset);
205                    if zip64_directory_size != observed_directory_size {
206                        return Err(ZipError::InvalidCentralDirectorySize {
207                            expected: zip64_directory_size,
208                            actual: observed_directory_size,
209                        });
210                    }
211
212                    // Verify that the EOCDR offset matches the current reader offset.
213                    if combined.central_directory_offset() != self.offset {
214                        return Err(ZipError::InvalidEndOfCentralDirectoryOffset(
215                            combined.central_directory_offset(),
216                            offset,
217                        ));
218                    }
219
220                    io::validate_comment(&comment)?;
221                    io::validate_trailing_contents(&mut self.reader).await?;
222                    return Ok(Entry::EndOfCentralDirectoryRecord { record: combined, comment, extensible });
223                }
224                actual => return Err(ZipError::UnexpectedHeaderError(actual, CDH_SIGNATURE)),
225            }
226        }
227
228        // Read the record.
229        let header = CentralDirectoryRecord::from_reader(&mut self.reader).await?;
230
231        // Read the file name, extra field, and comment, which also ensures that we advance the
232        // reader to the next record.
233        let filename_basic = io::read_bytes(&mut self.reader, header.file_name_length.into()).await?;
234        let extra_field = io::read_bytes(&mut self.reader, header.extra_field_length.into()).await?;
235        let extra_fields = parse_extra_fields(
236            extra_field,
237            header.uncompressed_size,
238            header.compressed_size,
239            Some(header.lh_offset),
240            Some(header.disk_start),
241        )?;
242        let zip64_extra_field = get_zip64_extra_field(&extra_fields);
243
244        // Validate the comment even though streaming readers do not return it.
245        let comment_basic = io::read_bytes(&mut self.reader, header.file_comment_length.into()).await?;
246        detect_comment(comment_basic, header.flags.filename_unicode, &extra_fields)?;
247
248        // Reconcile the compressed size, uncompressed size, and file offset, using ZIP64 if necessary.
249        let (uncompressed_size, compressed_size) =
250            get_combined_sizes(header.uncompressed_size, header.compressed_size, &zip64_extra_field)?;
251        let lh_offset = if let Some(lh_offset) = zip64_extra_field
252            .and_then(|zip64| zip64.relative_header_offset)
253            .filter(|_| header.lh_offset == NON_ZIP64_MAX_SIZE)
254        {
255            lh_offset
256        } else {
257            header.lh_offset as u64
258        };
259
260        // Parse out the filename.
261        let filename = detect_filename(filename_basic, header.flags.filename_unicode, extra_fields.as_ref())?;
262
263        Ok(Entry::CentralDirectoryEntry(CentralDirectoryEntry {
264            header,
265            compressed_size,
266            uncompressed_size,
267            lh_offset,
268            filename,
269        }))
270    }
271}