Skip to main content

zip/read/
stream.rs

1//! Code related to stream reading
2
3use crate::ZipReadOptions;
4use crate::extra_fields::ExtraFields;
5use crate::format::blocks::{FixedSizeBlock, Pod, ZipCentralEntryBlock, ZipLocalEntryBlock};
6use crate::format::magic::Magic;
7use crate::read::extract::{MAX_SYMLINK_TARGET_LEN, make_symlink_as_file};
8use crate::read::extract::{make_symlink, make_symlink_as_file_utf8};
9use crate::read::{
10    SymlinkExtractAction, ZipFile, ZipFileData, ZipFileEntry, ZipResult,
11    central_header_to_zip_file_inner,
12};
13use crate::result::{ZipError, invalid};
14
15use indexmap::IndexMap;
16use std::borrow::Cow;
17use std::io::{self, Read};
18use std::path::{Path, PathBuf};
19
20/// Stream decoder for zip.
21#[derive(Debug)]
22pub struct ZipStreamReader<R>(R);
23
24impl<R> ZipStreamReader<R> {
25    /// Create a new `ZipStreamReader`
26    pub const fn new(reader: R) -> Self {
27        Self(reader)
28    }
29}
30
31fn parse_central_directory<R: Read>(reader: &mut R) -> ZipResult<ZipFileEntry<'static>> {
32    // Give archive_offset and central_header_start dummy value 0, since
33    // they are not used in the output.
34    let archive_offset = 0;
35    let central_header_start = 0;
36
37    // Parse central header
38    let block = ZipCentralEntryBlock::parse(reader)?;
39    let (file, file_name_raw) =
40        central_header_to_zip_file_inner(reader, archive_offset, central_header_start, block)?;
41    Ok(ZipFileEntry {
42        file_name_raw: Cow::Owned(file_name_raw),
43        data: Cow::Owned(file),
44    })
45}
46
47impl<R: Read> ZipStreamReader<R> {
48    /// Iterate over the stream and extract all file and their
49    /// metadata.
50    pub fn visit<V: ZipStreamVisitor>(mut self, visitor: &mut V) -> ZipResult<()> {
51        while let Some(mut file) = read_zipfile_from_stream(&mut self.0)? {
52            visitor.visit_file(&mut file)?;
53        }
54
55        // `read_zipfile_from_stream` returned `None` because it read the signature of the
56        // first central directory header, so the stream is now 4 bytes into that header. Put
57        // the signature back in front so every header, the first one included, parses whole.
58        let signature = Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE.to_le_bytes();
59        let mut central_directory = (&signature[..]).chain(&mut self.0);
60        while let Ok(metadata) = parse_central_directory(&mut central_directory) {
61            visitor.visit_additional_metadata(&metadata)?;
62        }
63
64        Ok(())
65    }
66
67    /// Extract a Zip archive into a directory, overwriting files if they
68    /// already exist. Paths are sanitized with [`ZipFile::enclosed_name`].
69    ///
70    /// Extraction is not atomic; If an error is encountered, some of the files
71    /// may be left on disk. The file(s) and dir(s) are first created, then the
72    /// permissions are applied to them
73    ///
74    /// Extraction of symlink is not possible since we don't have access to the
75    /// external attributes in the local headers of the entries
76    pub fn extract<P: AsRef<Path>>(self, directory: P) -> ZipResult<()> {
77        /// Destination, every entry written in this pass (raw local name -> the checked output
78        /// path `visit_file` wrote it to), and the modes the central directory gives them.
79        struct Extractor {
80            outpath: PathBuf,
81            resolved_paths: IndexMap<Box<[u8]>, PathBuf>,
82            #[cfg(unix)]
83            file_permissions: std::collections::BTreeMap<PathBuf, u32>,
84        }
85        impl ZipStreamVisitor for Extractor {
86            fn visit_file<R: Read>(&mut self, file: &mut ZipFile<'_, R>) -> ZipResult<()> {
87                let extract_options = crate::read::extract::ExtractOptions::default();
88                let mut outpath = self.outpath.clone();
89                file.safe_prepare_path(
90                    &self.outpath,
91                    &mut outpath,
92                    None::<&(_, fn(&Path) -> bool)>,
93                )?;
94                self.resolved_paths
95                    .insert(file.name_raw().into(), outpath.clone());
96
97                if file.is_symlink() {
98                    // Same bound as `ZipArchive::extract`: the declared
99                    // uncompressed size is attacker-controlled, so reject
100                    // oversized entries rather than pre-allocating them.
101                    //
102                    // `is_symlink()` is currently always false on this path
103                    // (`from_local_block` has no external attributes to read),
104                    // so this is defence in depth -- see
105                    // `stream_does_not_expose_the_symlink_bit`.
106                    let declared_len = file.size();
107                    if declared_len > MAX_SYMLINK_TARGET_LEN {
108                        return Err(invalid!(
109                            "symlink target declares {} bytes, more than the {} byte limit",
110                            declared_len,
111                            MAX_SYMLINK_TARGET_LEN
112                        ));
113                    }
114
115                    let mut target = Vec::with_capacity(declared_len as usize);
116                    file.read_to_end(&mut target)?;
117                    match extract_options.symlink_action {
118                        SymlinkExtractAction::ExtractAsFile => {
119                            make_symlink_as_file(&outpath, &target)?;
120                        }
121                        SymlinkExtractAction::ExtractAsFileUtf8 => {
122                            make_symlink_as_file_utf8(&outpath, &target)?;
123                        }
124                        _ => {
125                            make_symlink(
126                                &self.outpath,
127                                &outpath,
128                                &target,
129                                &self.resolved_paths,
130                                extract_options.symlink_action,
131                            )?;
132                        }
133                    }
134                    return Ok(());
135                }
136                if file.is_dir() {
137                    fs::create_dir_all(&outpath)?;
138                } else {
139                    let mut outfile = fs::File::create(&outpath)?;
140                    io::copy(file, &mut outfile)?;
141                }
142
143                Ok(())
144            }
145
146            #[allow(unused)]
147            fn visit_additional_metadata(&mut self, metadata: &ZipFileEntry<'_>) -> ZipResult<()> {
148                // The central directory is not checked against the local headers, so its
149                // names only select entries this pass already wrote: a name that wasn't
150                // written gets no mode, and the mode goes to the path `visit_file` checked,
151                // never to a path rebuilt from the central directory.
152                #[cfg(unix)]
153                if let (Some(outpath), Some(mode)) = (
154                    self.resolved_paths.get(metadata.name_raw()),
155                    metadata.unix_mode(),
156                ) {
157                    self.file_permissions.insert(outpath.clone(), mode);
158                }
159                Ok(())
160            }
161        }
162        use std::fs;
163        fs::create_dir_all(&directory)?;
164        let directory = directory.as_ref().canonicalize()?;
165
166        let mut extractor = Extractor {
167            outpath: directory,
168            resolved_paths: IndexMap::new(),
169            #[cfg(unix)]
170            file_permissions: Default::default(),
171        };
172        self.visit(&mut extractor)?;
173
174        // As `ZipArchive::extract`: children before parents, so a read-only directory does not
175        // block setting its contents. Never through a symlink: `set_permissions` follows them.
176        #[cfg(unix)]
177        {
178            for (path, mode) in extractor.file_permissions.into_iter().rev() {
179                use std::os::unix::fs::PermissionsExt;
180                crate::read::extract::set_permissions_no_follow_symlink(
181                    &path,
182                    std::fs::Permissions::from_mode(mode),
183                )?;
184            }
185        }
186        Ok(())
187    }
188}
189
190/// Visitor for `ZipStreamReader`
191pub trait ZipStreamVisitor {
192    ///  * `file` - contains the content of the file and most of the metadata,
193    ///    except:
194    ///     - `comment`: set to an empty string
195    ///     - `data_start`: set to 0
196    ///     - `external_attributes`: `unix_mode()`: will return None
197    fn visit_file<R: Read>(&mut self, file: &mut ZipFile<'_, R>) -> ZipResult<()>;
198
199    /// This function is guaranteed to be called after all `visit_file`s.
200    ///
201    ///  * `metadata` - Provides missing metadata in `visit_file`.
202    fn visit_additional_metadata(&mut self, metadata: &ZipFileEntry<'_>) -> ZipResult<()>;
203}
204
205/// Read `ZipFile` structures from a non-seekable reader.
206///
207/// This is an alternative method to read a zip file. If possible, use the `ZipArchive` functions
208/// as some information will be missing when reading this manner.
209///
210/// Reads a file header from the start of the stream. Will return `Ok(Some(..))` if a file is
211/// present at the start of the stream. Returns `Ok(None)` if the start of the central directory
212/// is encountered. No more files should be read after this.
213///
214/// The Drop implementation of `ZipFile` ensures that the reader will be correctly positioned after
215/// the structure is done.
216///
217/// Missing fields are:
218/// * `comment`: set to an empty string
219/// * `data_start`: set to 0
220/// * `external_attributes`: `unix_mode()`: will return None
221pub fn read_zipfile_from_stream<R: Read>(reader: &mut R) -> ZipResult<Option<ZipFile<'_, R>>> {
222    read_zipfile_from_stream_with_options(reader, ZipReadOptions::default())
223}
224
225/// Read `ZipFile` from a non-seekable reader like [`read_zipfile_from_stream`] does, but assume the
226/// given compressed size and don't read any further ahead than that.
227pub fn read_zipfile_from_stream_with_compressed_size<R: Read>(
228    reader: &mut R,
229    compressed_size: u64,
230) -> ZipResult<Option<ZipFile<'_, R>>> {
231    let options = ZipReadOptions::default().override_compressed_size(compressed_size);
232    read_zipfile_from_stream_with_options(reader, options)
233}
234
235/// Same as `read_zipfile_from_stream` but with `ZipReadOptions`
236/// Since LZMA decoding requires the uncompressed length, you will need to override it
237pub fn read_zipfile_from_stream_with_options<'a, R: Read>(
238    reader: &'a mut R,
239    mut options: ZipReadOptions<'a>,
240) -> ZipResult<Option<ZipFile<'a, R>>> {
241    // We can't use the typical [`ZipLocalEntryBlock::parse`] method, as we follow separate code paths depending on the
242    // "magic" value (since the magic value will be from the central directory header if we've
243    // finished iterating over all the actual files).
244    let mut magic_buf = [0; size_of::<u32>()];
245    reader.read_exact(&mut magic_buf)?;
246
247    match Magic::from_le_bytes(magic_buf) {
248        Magic::LOCAL_FILE_HEADER_SIGNATURE => (),
249        Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE => return Ok(None),
250        _ => return Err(ZipLocalEntryBlock::WRONG_MAGIC_ERROR),
251    }
252
253    let mut block = ZipLocalEntryBlock::zeroed();
254    reader.read_exact(block.as_bytes_mut())?;
255
256    let block = block.from_le();
257    // parse file_name_raw
258    let file_name_length: usize = block.file_name_length.into();
259    let mut file_name_raw = vec![0u8; file_name_length];
260    if let Err(e) = reader.read_exact(&mut file_name_raw) {
261        if e.kind() == std::io::ErrorKind::UnexpectedEof {
262            return Err(invalid!("File name extends beyond file boundary"));
263        }
264        return Err(e.into());
265    }
266
267    // parse extra fields raw
268    let extra_field_length: usize = block.extra_field_length.into();
269    let mut extra_fields_raw = vec![0u8; extra_field_length];
270    if let Err(e) = reader.read_exact(&mut extra_fields_raw) {
271        if e.kind() == std::io::ErrorKind::UnexpectedEof {
272            return Err(invalid!("Extra field extends beyond file boundary"));
273        }
274        return Err(e.into());
275    }
276    // parse extra fields
277    let extra_fields = ExtraFields::parse(&extra_fields_raw, &block, true)?;
278    let mut data = ZipFileData::from_local_block(block, extra_fields)?;
279    data.apply_extra_fields(&mut file_name_raw)?;
280    if data.is_using_data_descriptor() {
281        if let Some(comp_size) = options.force_compressed_size {
282            data.compressed_size = comp_size;
283        } else {
284            return Err(ZipError::UnsupportedArchive(
285                "The file length is not available in the local header",
286            ));
287        }
288    }
289    if let Some(uncomp_size) = options.force_uncompressed_size {
290        data.uncompressed_size = uncomp_size;
291    } else if data.is_using_data_descriptor() {
292        // The local header doesn't carry the size; it follows the data.
293        options.size_unknown = true;
294    }
295    if let Some(crc) = options.force_crc {
296        data.crc32 = crc;
297    }
298
299    let limit_reader = reader.take(data.compressed_size);
300    let reader = data.make_reader(limit_reader, options)?;
301    Ok(Some(ZipFile {
302        file_name_raw: Cow::Owned(file_name_raw),
303        data: Cow::Owned(data),
304        reader,
305    }))
306}
307
308#[cfg(test)]
309mod tests {
310    use crate::read::ZipFile;
311    use crate::read::ZipFileEntry;
312    use crate::read::stream::{ZipStreamReader, ZipStreamVisitor};
313    use crate::result::ZipResult;
314    use std::collections::BTreeSet;
315    use std::io::{Cursor, Read};
316
317    #[test]
318    fn zip_read_streaming_visitor() {
319        let reader =
320            ZipStreamReader::new(Cursor::new(include_bytes!("../../tests/data/mimetype.zip")));
321
322        #[derive(Default)]
323        struct V {
324            filenames: BTreeSet<Box<str>>,
325        }
326        impl ZipStreamVisitor for V {
327            fn visit_file<R: Read>(&mut self, file: &mut ZipFile<'_, R>) -> ZipResult<()> {
328                if file.is_file() {
329                    let file_name = file.name().unwrap();
330                    self.filenames.insert(file_name.into());
331                }
332
333                Ok(())
334            }
335            fn visit_additional_metadata(
336                &mut self,
337                zip_file_metadata: &ZipFileEntry<'_>,
338            ) -> ZipResult<()> {
339                if zip_file_metadata.is_file() {
340                    let file_name = zip_file_metadata.name().unwrap();
341                    assert!(
342                        self.filenames.contains::<str>(file_name.as_ref()),
343                        "{} is missing its file content",
344                        file_name
345                    );
346                }
347
348                Ok(())
349            }
350        }
351
352        reader.visit(&mut V::default()).unwrap();
353    }
354
355    #[test]
356    fn file_and_dir_predicates() {
357        let reader = ZipStreamReader::new(Cursor::new(include_bytes!(
358            "../../tests/data/files_and_dirs.zip"
359        )));
360
361        #[derive(Default)]
362        struct V {
363            filenames: BTreeSet<Box<str>>,
364        }
365        impl ZipStreamVisitor for V {
366            fn visit_file<R: Read>(&mut self, file: &mut ZipFile<'_, R>) -> ZipResult<()> {
367                let full_name = file.enclosed_name().unwrap();
368                let file_name = full_name.file_name().unwrap().to_str().unwrap();
369                assert!(
370                    (file_name.starts_with("dir") && file.is_dir())
371                        || (file_name.starts_with("file") && file.is_file())
372                );
373
374                if file.is_file() {
375                    let file_name = file.name().unwrap();
376                    self.filenames.insert(file_name.into());
377                }
378
379                Ok(())
380            }
381            fn visit_additional_metadata(
382                &mut self,
383                zip_file_metadata: &ZipFileEntry<'_>,
384            ) -> ZipResult<()> {
385                if zip_file_metadata.is_file() {
386                    let file_name = zip_file_metadata.name().unwrap();
387                    assert!(
388                        self.filenames.contains::<str>(file_name.as_ref()),
389                        "{} is missing its file content",
390                        file_name
391                    );
392                }
393
394                Ok(())
395            }
396        }
397
398        reader.visit(&mut V::default()).unwrap();
399    }
400
401    /// Symlinks being extracted shouldn't be followed out of the destination directory.
402    /// Cannot use fs with miri CI
403    #[cfg(not(miri))]
404    #[test]
405    fn test_cannot_symlink_outside_destination() -> ZipResult<()> {
406        use crate::ZipWriter;
407        use crate::write::SimpleFileOptions;
408        use std::fs::create_dir;
409        use tempfile::TempDir;
410
411        let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
412        writer.add_symlink("symlink/", "../dest-sibling/", SimpleFileOptions::default())?;
413        writer.start_file("symlink/dest-file", SimpleFileOptions::default())?;
414        let reader = ZipStreamReader::new(writer.finish()?);
415        let dest_parent = TempDir::with_prefix("stream__cannot_symlink_outside_destination")?;
416        let dest_sibling = dest_parent.path().join("dest-sibling");
417        create_dir(&dest_sibling)?;
418        let dest = dest_parent.path().join("dest");
419        create_dir(&dest)?;
420        assert!(reader.extract(dest).is_err());
421        assert!(!dest_sibling.join("dest-file").exists());
422        Ok(())
423    }
424
425    /// The stream reader builds each entry from the *local* file header, which
426    /// has no external-attributes field (`ZipFileData::from_local_block` sets it
427    /// to 0), so `is_symlink()` is always false on this path and the symlink
428    /// branch in `extract` is currently unreachable.
429    ///
430    /// The `MAX_SYMLINK_TARGET_LEN` guard is still kept there, for consistency
431    /// with `ZipArchive::extract` and so it is already in place if that ever
432    /// changes. This test pins the current behaviour: if the local header starts
433    /// carrying the attribute, the assertion fails and the guard needs a real
434    /// regression test.
435    #[test]
436    fn stream_does_not_expose_the_symlink_bit() -> ZipResult<()> {
437        use crate::ZipWriter;
438        use crate::write::SimpleFileOptions;
439
440        let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
441        writer.add_symlink("link", "/target", SimpleFileOptions::default())?;
442        let mut cur = writer.finish()?;
443        cur.set_position(0);
444
445        let file = crate::read::read_zipfile_from_stream(&mut cur)?.unwrap();
446        assert!(
447            !file.is_symlink(),
448            "the stream path now sees the symlink bit -- the MAX_SYMLINK_TARGET_LEN \
449             guard in `stream.rs` is reachable and needs a regression test"
450        );
451
452        Ok(())
453    }
454
455    /// Cannot use fs with miri CI
456    #[cfg(not(miri))]
457    #[test]
458    fn test_can_create_destination() -> ZipResult<()> {
459        use tempfile::TempDir;
460
461        let v = include_bytes!("../../tests/data/mimetype.zip");
462        let reader = ZipStreamReader::new(v.as_ref());
463        let dest = TempDir::with_prefix("stream_test_can_create_destination").unwrap();
464        reader.extract(&dest)?;
465        assert!(dest.path().join("mimetype").exists());
466        Ok(())
467    }
468
469    #[test]
470    fn zip_read_streaming() {
471        use super::read_zipfile_from_stream;
472
473        let mut reader = Cursor::new(include_bytes!("../../tests/data/mimetype.zip"));
474        loop {
475            if read_zipfile_from_stream(&mut reader).unwrap().is_none() {
476                break;
477            }
478        }
479    }
480
481    #[test]
482    #[cfg(feature = "deflate")]
483    fn zip_read_streaming_compressed() {
484        use super::read_zipfile_from_stream_with_compressed_size;
485        use crate::ZipWriter;
486        use crate::write::SimpleFileOptions;
487        use std::io::Write;
488
489        let compression_method = crate::CompressionMethod::Deflated;
490        let options = SimpleFileOptions::default()
491            .compression_method(compression_method)
492            .unix_permissions(0o755);
493
494        let mut bytes = Vec::new();
495        let mut writer = ZipWriter::new(std::io::Cursor::new(&mut bytes));
496        writer.start_file("file.txt", options).unwrap();
497        write!(&mut writer, "{}", "test-".repeat(100)).unwrap();
498        writer.finish().unwrap();
499
500        let compressed_size = u32::from_le_bytes(bytes[18..22].try_into().unwrap());
501        let uncompressed_size = u32::from_le_bytes(bytes[22..26].try_into().unwrap());
502
503        assert_eq!(compressed_size, 14);
504        assert_eq!(uncompressed_size as usize, "test-".len() * 100);
505
506        let mut reader = Cursor::new(bytes);
507        loop {
508            if read_zipfile_from_stream_with_compressed_size(
509                &mut reader,
510                u64::from(compressed_size),
511            )
512            .unwrap()
513            .is_none()
514            {
515                break;
516            }
517        }
518    }
519
520    #[test]
521    #[cfg(feature = "aes-crypto")]
522    fn zip_read_streaming_compressed_and_aes() {
523        use super::read_zipfile_from_stream_with_options;
524        use crate::ZipReadOptions;
525
526        let bytes = include_bytes!("../../tests/data/aes_archive.zip");
527        let compressed_size = 46;
528
529        let mut reader = Cursor::new(bytes);
530        const PASSWORD: &[u8] = b"helloworld";
531        let options = ZipReadOptions::new()
532            .password(Some(PASSWORD))
533            .override_compressed_size(compressed_size);
534
535        // we simulate the fact that we need the compressed size like a streamed zip
536        let result = read_zipfile_from_stream_with_options(&mut reader, options);
537        let optional_file = result.unwrap();
538        let mut file = optional_file.unwrap();
539
540        let file_name = file.name().unwrap();
541        assert_eq!(file_name, "secret_data_128");
542
543        const SECRET_CONTENT: &str = "Lorem ipsum dolor sit amet";
544        let mut decrypted_content = String::new();
545        file.read_to_string(&mut decrypted_content)
546            .expect("couldn't read encrypted file");
547        assert_eq!(SECRET_CONTENT, decrypted_content);
548    }
549
550    #[test]
551    #[cfg(feature = "aes-crypto")]
552    fn zip_read_streaming_compressed_and_aes_without_size() {
553        use super::read_zipfile_from_stream_with_options;
554        use crate::ZipReadOptions;
555
556        let bytes = include_bytes!("../../tests/data/aes_archive.zip");
557
558        let mut reader = Cursor::new(bytes);
559        const PASSWORD: &[u8] = b"helloworld";
560        let options = ZipReadOptions::new().password(Some(PASSWORD));
561
562        // the zip already has the compressed size (it's not a streamed zip)
563        let result = read_zipfile_from_stream_with_options(&mut reader, options);
564        let optional_file = result.unwrap();
565        let mut file = optional_file.unwrap();
566
567        let file_name = file.name().unwrap();
568        assert_eq!(file_name, "secret_data_128");
569
570        const SECRET_CONTENT: &str = "Lorem ipsum dolor sit amet";
571        let mut decrypted_content = String::new();
572        file.read_to_string(&mut decrypted_content)
573            .expect("couldn't read encrypted file");
574        assert_eq!(SECRET_CONTENT, decrypted_content);
575    }
576
577    #[test]
578    fn zip_read_streaming_zipwriter() {
579        use crate::CompressionMethod;
580        use crate::ZipReadOptions;
581        use crate::ZipWriter;
582        use crate::read::read_zipfile_from_stream;
583        use crate::read::read_zipfile_from_stream_with_compressed_size;
584        use crate::read::read_zipfile_from_stream_with_options;
585        use crate::write::SimpleFileOptions;
586        use std::io::Write;
587
588        let mut buffer = Vec::new();
589        let options = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
590        let mut archive = ZipWriter::new_stream(Cursor::new(&mut buffer));
591        archive.start_file("name", options).unwrap();
592        archive.write_all(b"test").unwrap();
593        let compressed_size = b"test".len() as u64;
594        archive.finish().unwrap();
595        {
596            // reading will fail because it's a streamed zipfile and we don't know the size
597            let mut reader = Cursor::new(buffer.clone());
598            let result = read_zipfile_from_stream(&mut reader);
599            assert!(result.is_err());
600        }
601        {
602            // reading the file will fail because of the invalid checksum
603            let mut reader = Cursor::new(buffer.clone());
604            let result =
605                read_zipfile_from_stream_with_compressed_size(&mut reader, compressed_size);
606            let optional_file = result.unwrap();
607            let mut file = optional_file.unwrap();
608            let file_name = file.name().unwrap();
609            assert_eq!(file_name, "name");
610            // test reading
611            let mut content = Vec::new();
612            let read_result = file.read_to_end(&mut content);
613            assert!(read_result.is_err()); // invalid checksum
614
615            let error_str = read_result.unwrap_err().to_string();
616            // eprintln!("{error_str}");
617            assert!(error_str.contains("Invalid checksum"));
618        }
619        {
620            // reading the file will work because we override the checksum
621            let mut reader = Cursor::new(buffer.clone());
622            let read_options = ZipReadOptions::default()
623                .override_compressed_size(compressed_size)
624                .override_crc(3632233996);
625            let result = read_zipfile_from_stream_with_options(&mut reader, read_options);
626            let optional_file = result.unwrap();
627            let mut file = optional_file.unwrap();
628            let file_name = file.name().unwrap();
629            assert_eq!(file_name, "name");
630            // test reading
631            let mut content = Vec::new();
632            file.read_to_end(&mut content).unwrap();
633            assert_eq!(content, b"test");
634        }
635        {
636            // reading the file will work because we skip checksum
637            let mut reader = Cursor::new(buffer.clone());
638            let read_options = ZipReadOptions::default()
639                .override_compressed_size(compressed_size)
640                .ignore_crc32(true);
641            let result = read_zipfile_from_stream_with_options(&mut reader, read_options);
642            let optional_file = result.unwrap();
643            let mut file = optional_file.unwrap();
644            let file_name = file.name().unwrap();
645            assert_eq!(file_name, "name");
646            // test reading
647            let mut content = Vec::new();
648            let read_result = file.read_to_end(&mut content);
649            assert!(read_result.is_ok());
650            assert_eq!(content, b"test");
651        }
652    }
653}