Skip to main content

eazip/read/
mod.rs

1//! Utilities to read an archive.
2
3use std::{
4    borrow::Cow,
5    io::{self, BufRead, Read, Seek},
6};
7
8use crate::{CompressionMethod, Decompressor, FileType, Timestamp, types, utils};
9
10mod extra_field;
11mod raw;
12
13use extra_field::{ExtraField, ExtraFields};
14
15#[cold]
16fn invalid(msg: &str) -> io::Error {
17    io::Error::new(io::ErrorKind::InvalidData, msg)
18}
19
20#[cold]
21fn encrypted_file() -> io::Error {
22    io::Error::new(io::ErrorKind::Unsupported, "encrypted file")
23}
24
25#[cold]
26fn compressed() -> io::Error {
27    io::Error::new(io::ErrorKind::Unsupported, "compressed file")
28}
29
30trait ReadSeek: Read + Seek {}
31impl<R: Read + Seek> ReadSeek for R {}
32
33trait BufReadSeek: BufRead + Seek {}
34impl<R: BufRead + Seek> BufReadSeek for R {}
35
36/// The method used to encrypt a file.
37///
38/// `eazip` does not provide the tools to decrypt these files, but provides the
39/// required metadata if you really need to.
40///
41/// This is only provided for completeness, please don't use this in scenarios
42/// where security actually matters and use proper tools (eg `age`).
43#[derive(Debug, Clone, Copy)]
44#[non_exhaustive]
45pub enum EncryptionMethod {
46    /// Legacy ZipCrypto encryption.
47    ZipCrypto,
48    /// PKWARE proprietary "Strong Encryption".
49    StrongEncrytion,
50    /// The file is encrypted using AES in CTR mode.
51    ///
52    /// See [the specification](https://www.winzip.com/en/support/aes-encryption/#file-format1)
53    /// for the format of the encrypted files.
54    Aes {
55        /// The size of the AES key. This may be 128, 192 or 256 bytes.
56        key_size: u16,
57        /// Whether to check the CRC32 of the decypted content.
58        ///
59        /// If `true`, this will lead to data leak.
60        check_crc32: bool,
61    },
62}
63
64/// An open ZIP archive without a reader
65pub struct RawArchive {
66    entries: Vec<Metadata>,
67    names: utils::NameTable<usize>,
68    comment: Box<[u8]>,
69}
70
71impl std::fmt::Debug for RawArchive {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        f.debug_struct("RawArchive")
74            .field("entries", &self.entries)
75            .field("comment", &String::from_utf8_lossy(&self.comment))
76            .finish()
77    }
78}
79
80impl RawArchive {
81    /// Creates a `RawArchive` from a reader.
82    ///
83    /// The same reader should be used for other methods.
84    #[inline]
85    pub fn new<R: Read + Seek>(reader: &mut R) -> io::Result<Self> {
86        raw::read_archive(reader)
87    }
88
89    /// Gets the list of entries in this archive.
90    #[inline]
91    pub fn entries(&self) -> &[Metadata] {
92        &self.entries
93    }
94
95    /// Gets the comment of the archive.
96    #[inline]
97    pub fn comment(&self) -> &[u8] {
98        &self.comment
99    }
100
101    /// Gets a file by its name.
102    #[inline]
103    pub fn get_by_name(&mut self, name: &str) -> Option<&Metadata> {
104        let index = self.index_of(name)?;
105        self.entries.get(index)
106    }
107
108    /// Gets the index of a file in [`Self::entries`] by its name.
109    pub fn index_of(&self, name: &str) -> Option<usize> {
110        let stripped_name = name.strip_suffix('/');
111        let name = stripped_name.unwrap_or(name);
112
113        let index = *self
114            .names
115            .get(name.as_bytes(), |i| self.entries[*i].stripped_name())?;
116
117        // To avoid duplicated names, directory names are stored without a
118        // trailing '/' in the map, but this implementation detail should not
119        // leak.
120        if self.entries[index].file_type.is_directory() != stripped_name.is_some() {
121            return None;
122        }
123
124        Some(index)
125    }
126
127    /// Extracts the archive to the given directory.
128    ///
129    /// The directory will be created if needed, but *not* its parent.
130    pub fn extract<R: BufRead + Seek>(
131        &self,
132        reader: &mut R,
133        at: &std::path::Path,
134    ) -> io::Result<()> {
135        match std::fs::create_dir(at) {
136            Ok(()) => (),
137            Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
138            Err(err) => return Err(err),
139        };
140
141        for entry in &self.entries {
142            entry.extract(reader, at)?;
143        }
144
145        Ok(())
146    }
147
148    /// Extracts the archive to the given directory in parallel.
149    ///
150    /// The directory will be created if needed, but *not* its parent.
151    ///
152    /// The reader should implement [`sync_file::ReadAt`], like [`io::Cursor`]
153    /// or [`sync_file::RandomAccessFile`].
154    #[cfg(feature = "parallel")]
155    pub fn parallel_extract<R: sync_file::ReadAt + sync_file::Size + Sync>(
156        &self,
157        reader: &R,
158        at: &std::path::Path,
159    ) -> io::Result<()> {
160        use rayon::prelude::*;
161
162        match std::fs::create_dir(at) {
163            Ok(()) => (),
164            Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
165            Err(err) => return Err(err),
166        };
167
168        self.entries.par_iter().try_for_each_init(
169            || io::BufReader::new(sync_file::Adapter::new(reader)),
170            |reader, entry| entry.extract(reader, at),
171        )?;
172
173        Ok(())
174    }
175}
176
177impl FileType {
178    fn test(attr: u32, name: &str) -> Option<Self> {
179        let dos_attr = attr as u16;
180        let unix_mode = (attr >> 16) as u16;
181        let unix_kind = unix_mode >> 12;
182
183        let is_file = (dos_attr & (1 << 5)) != 0 || unix_kind == 8;
184        let is_dir = (dos_attr & (1 << 4)) != 0 || unix_kind == 4;
185        let is_symlink = unix_kind == 10;
186        let trailing_slash = name.ends_with('/');
187
188        match (is_file, is_dir, trailing_slash, is_symlink) {
189            (_, false, false, false) => Some(FileType::File),
190            (false, _, true, false) => Some(FileType::Directory),
191            (false, false, false, true) => Some(FileType::Symlink),
192            _ => None,
193        }
194    }
195}
196
197fn convert_string(raw: &[u8], force_unicode: bool) -> Option<(Cow<'_, str>, Option<u32>)> {
198    // MacOS stores the file name as UTF8, but does not use the unicode flag,
199    // and everyone seems fine with that, so if we meet UTF8 we'll just pretend
200    // that everything is fine.
201    if let Ok(name) = str::from_utf8(raw) {
202        return Some((Cow::Borrowed(name), None));
203    }
204
205    // If we didn't find UTF8 and it wasn't expected, handle it as CP437.
206    if force_unicode {
207        None
208    } else {
209        let name = utils::cp437::convert(raw);
210        Some((Cow::Owned(name), Some(crc32fast::hash(raw))))
211    }
212}
213
214/// The metadata of a ZIP entry.
215#[derive(Debug)]
216pub struct Metadata {
217    header_offset: u64,
218    pub data_offset: u64,
219
220    pub compressed_size: u64,
221    pub uncompressed_size: u64,
222    pub compression_method: CompressionMethod,
223    pub crc32: u32,
224    pub file_type: FileType,
225
226    pub modification_time: Option<Timestamp>,
227    pub access_time: Option<Timestamp>,
228    pub creation_time: Option<Timestamp>,
229
230    pub encryption: Option<EncryptionMethod>,
231
232    name: Box<str>,
233    comment: Box<str>,
234
235    is_streaming: bool,
236    is_zip64: bool,
237    flags: u16,
238}
239
240impl Metadata {
241    fn from_local_header(
242        header: types::LocalFileHeader,
243        file_name: &[u8],
244        extra_fields: &[u8],
245    ) -> Option<Self> {
246        let flags = header.flags.get();
247        let is_encrypted = flags & (1 << 0) != 0;
248        let is_streaming = flags & (1 << 3) != 0;
249        let strong_encryption = flags & (1 << 6) != 0;
250        let is_unicode = flags & (1 << 11) != 0;
251
252        if { header.signature } != types::LocalFileHeader::SIGNATURE {
253            return None;
254        }
255
256        let (name, name_crc) = convert_string(file_name, is_unicode)?;
257        let name = utils::validate_name(&name)?;
258
259        let encryption = match (is_encrypted, strong_encryption) {
260            (false, false) => None,
261            (false, true) => return None,
262            (true, false) => Some(EncryptionMethod::ZipCrypto),
263            (true, true) => Some(EncryptionMethod::StrongEncrytion),
264        };
265
266        let mut meta = Self {
267            crc32: header.crc32.get(),
268            encryption,
269            header_offset: 0,
270            data_offset: 0,
271
272            compressed_size: header.compressed_size.get() as u64,
273            uncompressed_size: header.uncompressed_size.get() as u64,
274            compression_method: CompressionMethod(header.compression_method.get()),
275            file_type: FileType::File,
276
277            modification_time: None,
278            access_time: None,
279            creation_time: None,
280
281            name,
282            comment: Box::default(),
283
284            is_streaming,
285            is_zip64: false,
286            flags,
287        };
288
289        meta.parse_extra_fields(ExtraFields(extra_fields), name_crc, None)?;
290
291        Some(meta)
292    }
293
294    fn from_central_header(
295        header: types::CentralFileHeader,
296        file_name: &[u8],
297        extra_fields: &[u8],
298        comment: &[u8],
299    ) -> Option<Self> {
300        let flags = header.flags.get();
301        let is_encrypted = flags & (1 << 0) != 0;
302        let is_streaming = flags & (1 << 3) != 0;
303        let strong_encryption = flags & (1 << 6) != 0;
304        let is_unicode = flags & (1 << 11) != 0;
305
306        if { header.signature } != types::CentralFileHeader::SIGNATURE
307            || header.disk_number.get() != 0
308        {
309            return None;
310        }
311
312        let (comment, comment_crc) = convert_string(comment, is_unicode)?;
313        let comment = comment.into_owned().into_boxed_str();
314        let (name, name_crc) = convert_string(file_name, is_unicode)?;
315        let name = utils::validate_name(&name)?;
316        let file_type = FileType::test(header.external_attributes.get(), &name)?;
317
318        let encryption = match (is_encrypted, strong_encryption) {
319            (false, false) => None,
320            (false, true) => return None,
321            (true, false) => Some(EncryptionMethod::ZipCrypto),
322            (true, true) => Some(EncryptionMethod::StrongEncrytion),
323        };
324
325        let mut meta = Self {
326            crc32: header.crc32.get(),
327            encryption,
328            header_offset: header.local_header_offset.get() as u64,
329            data_offset: 0,
330
331            compressed_size: header.compressed_size.get() as u64,
332            uncompressed_size: header.uncompressed_size.get() as u64,
333            compression_method: CompressionMethod(header.compression_method.get()),
334            file_type,
335
336            modification_time: None,
337            access_time: None,
338            creation_time: None,
339
340            name,
341            comment,
342
343            is_streaming,
344            is_zip64: false,
345            flags,
346        };
347
348        meta.parse_extra_fields(ExtraFields(extra_fields), name_crc, comment_crc)?;
349
350        Some(meta)
351    }
352
353    fn parse_extra_fields(
354        &mut self,
355        extra_fields: ExtraFields,
356        name_crc: Option<u32>,
357        comment_crc: Option<u32>,
358    ) -> Option<()> {
359        for field in extra_fields.iter() {
360            match field {
361                ExtraField::Zip64ExtendedInformation(mut info) => {
362                    if self.uncompressed_size == 0xffff_ffff {
363                        self.uncompressed_size = info.next()?;
364                    }
365                    if self.compressed_size == 0xffff_ffff {
366                        self.compressed_size = info.next()?;
367                    }
368                    if self.header_offset == 0xffff_ffff {
369                        self.header_offset = info.next()?;
370                    }
371                    // Disk number must be 0
372                    info.end()?;
373                    self.is_zip64 = true;
374                }
375                ExtraField::UnicodeComment(unicode) => {
376                    if Some(unicode.header_comment_crc32) != comment_crc {
377                        return None;
378                    }
379                    self.comment = unicode.comment.into();
380                }
381
382                ExtraField::UnicodeName(unicode) => {
383                    if Some(unicode.header_name_crc32) != name_crc {
384                        return None;
385                    }
386                    self.name = utils::validate_name(unicode.name)?;
387                }
388
389                ExtraField::Ntfs(ntfs) => {
390                    self.modification_time = ntfs.times.mtime;
391                    self.access_time = ntfs.times.atime;
392                    self.creation_time = ntfs.times.ctime;
393                }
394
395                ExtraField::ExtendedTimestamp(ts) => {
396                    self.modification_time = ts.modification_time;
397                    self.access_time = ts.access_time;
398                    self.creation_time = ts.creation_time;
399                }
400
401                ExtraField::Aes(aes) => {
402                    if self.compression_method != CompressionMethod::AES
403                        || (!aes.check_crc32 && self.crc32 != 0)
404                    {
405                        return None;
406                    }
407                    let Some(enc @ EncryptionMethod::ZipCrypto) = &mut self.encryption else {
408                        return None;
409                    };
410
411                    *enc = EncryptionMethod::Aes {
412                        key_size: aes.key_size,
413                        check_crc32: aes.check_crc32,
414                    };
415                    self.compression_method = aes.compression;
416                }
417
418                ExtraField::Invalid => return None,
419
420                _ => (),
421            }
422        }
423
424        if self.compression_method == CompressionMethod::AES {
425            return None;
426        }
427
428        Some(())
429    }
430
431    /// Returns `true` if this file is encrypted.
432    #[inline]
433    pub fn is_encrypted(&self) -> bool {
434        self.encryption.is_some()
435    }
436
437    /// Gets the name of this entry.
438    #[inline]
439    pub fn name(&self) -> &str {
440        &self.name
441    }
442
443    fn stripped_name(&self) -> &[u8] {
444        self.name.strip_suffix('/').unwrap_or(&self.name).as_bytes()
445    }
446
447    /// Gets the comment of this entry.
448    #[inline]
449    pub fn comment(&self) -> &str {
450        &self.comment
451    }
452
453    /// Returns a reader with the content of the file.
454    ///
455    /// Unsupported compression methods and encrypted files will return an error.
456    pub fn read<R: BufRead + Seek>(&self, reader: R) -> io::Result<impl Read + use<R>> {
457        if self.encryption.is_some() {
458            return Err(encrypted_file());
459        }
460
461        let reader = Decompressor::new(self.read_raw(reader)?, self.compression_method)?;
462        Ok(self.content_checker(reader))
463    }
464
465    /// Returns a reader with the content of the file.
466    ///
467    /// Errors if the file is compressed, encrypted or corrupted. Is is not
468    /// necessary to use `Metadata::content_checker` on the result.
469    ///
470    /// It is useful if you know that the file is stored as-is and you want to
471    /// take advantage of the `BufReader` or the `Seek` implementation.
472    pub fn read_stored<R: Read + Seek>(&self, mut reader: R) -> io::Result<io::Take<R>> {
473        if self.encryption.is_some() {
474            return Err(encrypted_file());
475        }
476        if self.compression_method != CompressionMethod::STORE {
477            return Err(compressed());
478        }
479
480        // Check CRC beforehand. Length has already been checked.
481        let mut checker = utils::Crc32Checker::new(self.read_raw(&mut reader)?, self.crc32);
482        std::io::copy(&mut checker, &mut io::sink())?;
483
484        self.read_raw(reader)
485    }
486
487    /// Returns a reader with the raw, uncompressed, content of the file.
488    ///
489    /// The uncompressed content should be checked with `content_checker`.
490    pub fn read_raw<R: Read + Seek>(&self, mut reader: R) -> io::Result<io::Take<R>> {
491        reader.seek(io::SeekFrom::Start(self.data_offset))?;
492        Ok(reader.take(self.compressed_size))
493    }
494
495    /// Wraps a reader to check that its content matches this metadata.
496    ///
497    /// It is particularly  useful in combinaison of `read_raw`.
498    #[inline]
499    pub fn content_checker<R: Read>(&self, reader: R) -> impl Read + use<R> {
500        utils::Crc32Checker::new(
501            utils::LengthChecker::new(reader, self.uncompressed_size),
502            self.crc32,
503        )
504    }
505
506    /// Extracts this entry as if the root of the archive was at `root`.
507    #[inline]
508    pub fn extract<R: BufRead + Seek>(
509        &self,
510        reader: &mut R,
511        root: impl AsRef<std::path::Path>,
512    ) -> io::Result<()> {
513        self._extract(reader, root.as_ref())
514    }
515
516    fn _extract(&self, reader: &mut dyn BufReadSeek, at: &std::path::Path) -> io::Result<()> {
517        if !std::fs::metadata(at)?.is_dir() {
518            return Err(io::Error::from(io::ErrorKind::NotFound));
519        }
520
521        let path = at.join(&*self.name);
522        std::fs::create_dir_all(path.parent().unwrap())?;
523
524        match self.file_type {
525            FileType::File => {
526                let mut f = std::fs::File::create_new(&path)?;
527                io::copy(&mut self.read(reader)?, &mut f)?;
528
529                if let Some(mod_time) = self.modification_time {
530                    f.set_times(std::fs::FileTimes::new().set_modified(mod_time.to_std()))?;
531                }
532            }
533            FileType::Directory => {
534                std::fs::create_dir(path)?;
535            }
536            FileType::Symlink => {
537                let target = io::read_to_string(self.read(reader)?)?;
538                if !utils::validate_symlink(&self.name, &target) {
539                    return Err(invalid("invalid symlink target"));
540                }
541
542                #[cfg(unix)]
543                std::os::unix::fs::symlink(target, path)?;
544
545                #[cfg(windows)]
546                if target.ends_with('/') {
547                    std::os::windows::fs::symlink_dir(target, path)?;
548                } else {
549                    std::os::windows::fs::symlink_file(target, path)?;
550                }
551
552                #[cfg(not(any(unix, windows)))]
553                std::fs::write(path, target.as_bytes())?;
554            }
555        }
556
557        Ok(())
558    }
559}
560
561/// An open ZIP archive.
562///
563/// This type owns the reader. If you need something more flexible, use
564/// [`RawArchive`] instead.
565///
566/// # Example
567///
568/// Print the name and content of each file in the archive:
569///
570/// ```no_run
571/// let mut archive = eazip::ArchiveReader::open("example.zip")?;
572///
573/// for i in 0..archive.entries().len() {
574///     let mut entry = archive.get_by_index(i).unwrap();
575///     let name = entry.metadata().name();
576///     let content = std::io::read_to_string(entry.read()?)?;
577///
578///     println!("{name}: {content}");
579/// }
580///
581/// # Ok::<(), std::io::Error>(())
582/// ```
583#[derive(Debug)]
584pub struct ArchiveReader<R> {
585    inner: RawArchive,
586    reader: R,
587}
588
589impl ArchiveReader<io::BufReader<std::fs::File>> {
590    /// Opens the given file as a ZIP archive.
591    #[inline]
592    pub fn open(path: impl AsRef<std::path::Path>) -> io::Result<Self> {
593        Self::_open(path.as_ref())
594    }
595
596    fn _open(path: &std::path::Path) -> io::Result<Self> {
597        Self::new(io::BufReader::new(std::fs::File::open(path)?))
598    }
599}
600
601#[cfg(feature = "parallel")]
602impl ArchiveReader<io::BufReader<sync_file::SyncFile>> {
603    /// Opens the given file as a ZIP archive ready for parallel extract.
604    #[inline]
605    pub fn open_parallel(path: impl AsRef<std::path::Path>) -> io::Result<Self> {
606        Self::_open(path.as_ref())
607    }
608
609    fn _open(path: &std::path::Path) -> io::Result<Self> {
610        Self::new(io::BufReader::new(sync_file::SyncFile::open(path)?))
611    }
612}
613
614impl<R: Read + Seek> ArchiveReader<R> {
615    /// Opens a ZIP archive from a reader.
616    ///
617    /// This also perform many validation checks on the archive to make sure
618    /// that is it well-formed and does not have dangerous or duplicated paths.
619    /// The validity of file contents is checked lazily when reading them.
620    ///
621    /// The exact rules around validation are not part of semver guaranties and
622    /// may change at every release.
623    ///
624    /// **The targets of symlinks are not checked yet here**, though they are
625    /// through `extract` and `extract_parallel`.
626    pub fn new(mut reader: R) -> io::Result<Self> {
627        let inner = RawArchive::new(&mut reader)?;
628        Ok(Self { inner, reader })
629    }
630
631    /// Gets the list of entries in the archive.
632    #[inline]
633    pub fn entries(&self) -> &[Metadata] {
634        &self.inner.entries
635    }
636
637    /// Gets a file by its index.
638    #[inline]
639    pub fn get_by_index(&mut self, index: usize) -> Option<File<'_, R>> {
640        let metadata = self.inner.entries().get(index)?;
641        Some(File {
642            metadata,
643            reader: &mut self.reader,
644        })
645    }
646
647    /// Gets a file by its name.
648    pub fn get_by_name(&mut self, name: &str) -> Option<File<'_, R>> {
649        let index = self.index_of(name)?;
650        self.get_by_index(index)
651    }
652
653    /// Gets the index of a file in [`Self::entries`] by its name.
654    pub fn index_of(&self, name: &str) -> Option<usize> {
655        self.inner.index_of(name)
656    }
657
658    /// Gets the comment of the archive.
659    #[inline]
660    pub fn commment(&self) -> &[u8] {
661        &self.inner.comment
662    }
663
664    /// Extracts the archive to the given directory.
665    ///
666    /// The directory will be created if needed, but *not* its parent.
667    #[inline]
668    pub fn extract(&mut self, at: impl AsRef<std::path::Path>) -> io::Result<()>
669    where
670        R: BufRead,
671    {
672        self.inner.extract(&mut self.reader, at.as_ref())
673    }
674
675    /// Extracts the archive to the given directory in parallel.
676    ///
677    /// The directory will be created if needed, but *not* its parent.
678    ///
679    /// The reader should implement [`sync_file::ReadAt`], like [`io::Cursor`]
680    /// or [`sync_file::SyncFile`].
681    #[cfg(feature = "parallel")]
682    #[inline]
683    pub fn parallel_extract(&self, at: impl AsRef<std::path::Path>) -> io::Result<()>
684    where
685        R: sync_file::ReadAt + sync_file::Size + Sync,
686    {
687        self.inner.parallel_extract(&self.reader, at.as_ref())
688    }
689
690    /// Gets a shared reference to the underlying reader.
691    #[inline]
692    pub fn get_ref(&self) -> &R {
693        &self.reader
694    }
695
696    /// Gets a mutable reference to the underlying reader.
697    #[inline]
698    pub fn get_mut(&mut self) -> &mut R {
699        &mut self.reader
700    }
701}
702
703/// Type alias to [`ArchiveReader`] for compatibility.
704pub type Archive<R> = ArchiveReader<R>;
705
706/// A file in a ZIP archive.
707#[derive(Debug)]
708pub struct File<'a, R> {
709    metadata: &'a Metadata,
710    reader: &'a mut R,
711}
712
713impl<'a, R: Read + Seek> File<'a, R> {
714    /// Gets the metadata of the file.
715    ///
716    /// The lifetime of the returned reference is bound to the `ArchiveReader`,
717    /// so it can outlive `self`.
718    #[inline]
719    pub fn metadata(&self) -> &'a Metadata {
720        self.metadata
721    }
722
723    /// Returns a reader with the content of the file.
724    ///
725    /// Unsupported compression methods will return an error.
726    #[inline]
727    pub fn read(&mut self) -> io::Result<impl Read + '_>
728    where
729        R: BufRead,
730    {
731        self.metadata.read(&mut *self.reader)
732    }
733
734    /// Returns a reader with the content of the file.
735    ///
736    /// Errors if the file is compressed, encrypted or corrupted. Is is not
737    /// necessary to use `Metadata::content_checker` on the result.
738    ///
739    /// It is useful if you know that the file is stored as-is and you want to
740    /// take advantage of the `BufReader` or the `Seek` implementation.
741    pub fn read_stored(self) -> io::Result<io::Take<&'a mut R>> {
742        self.metadata.read_stored(self.reader)
743    }
744
745    /// Returns a reader with the raw, compressed, content of the file.
746    ///
747    /// The uncompressed content should be checked with [`Metadata::content_checker`].
748    #[inline]
749    pub fn read_raw(&mut self) -> io::Result<io::Take<&mut R>> {
750        self.metadata.read_raw(self.reader)
751    }
752
753    /// Consumes self, returning the underlying reader.
754    ///
755    /// This reader can be used with [`Metadata::read_raw`] to read the raw,
756    /// compressed content of the file. The uncompressed content should then be
757    /// checked with [`Metadata::content_checker`].
758    pub fn into_reader(self) -> &'a mut R {
759        self.reader
760    }
761}