async_zip 0.0.19

An asynchronous ZIP archive reading/writing crate.
Documentation
// Copyright (c) 2026 Harry [Majored] [hello@majored.pw]
// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)

use std::io::SeekFrom;

use futures_lite::AsyncBufRead;
use futures_lite::AsyncSeekExt;
use futures_lite::AsyncRead;
use futures_lite::AsyncSeek;

#[cfg(feature = "tracing")]
use tracing::{instrument, trace};

use crate::base::read1::loc::Method0;
use crate::base::read1::opts::ZipLocateMethod;
use crate::base::read1::opts::ZipOptions;
use crate::base::read1::seek::ZipArchiveInner;
use crate::error::ZipError;
use crate::spec::constructs::CEOCDR;
use crate::spec::headers1::CDRH;
use crate::spec::constructs::{CDR, LF, EOCDR};
use crate::spec::headers1::EOCDR64H;
use crate::spec::headers1::EOCDRH;
use crate::spec::KnownSize;
use crate::spec::headers1::EOCDL64H;
use crate::{error::Result, spec::headers1::{LFH, Signature}};

pub(crate) struct Ops<'o, R> {
    options: &'o ZipOptions,
    reader: R,
}

impl<'o, R: AsyncRead + Unpin> Ops<'o, R> {
    pub fn new(reader: R, options: &'o ZipOptions) -> Self {
        Self { reader, options }
    }

    #[cfg_attr(feature = "tracing", instrument(skip(self), level = "trace"))]
    pub async fn assert_signature(&mut self, expected: Signature) -> Result<()> {
        let signature = crate::spec::headers1::read::<Signature, R>(&mut self.reader).await?;
        #[cfg(feature = "tracing")]
        trace!("read signature: {:02X?}", signature);

        if signature != expected {
            return Err(crate::error::ZipError::UnexpectedHeaderError(signature.into(), expected.into()));
        }

        Ok(())
    }

    #[cfg_attr(feature = "tracing", instrument(skip(self), level = "trace"))]
    pub async fn lf(&mut self) -> Result<LF> {
        self.assert_signature(Signature::LFH).await?;

        let options = self.options;
        let lf = crate::spec::headers1::read_record::<LFH, LF, R>(&mut self.reader, |lfh| {
            crate::base::read1::valid::validate_extra_field_size(lfh.extra_field_length, options)?;
            Ok(usize::from(lfh.file_name_length) + usize::from(lfh.extra_field_length))
        })
        .await?;

        crate::base::read1::valid::validate_extra_field_num(&lf.efs, options)?;

        Ok(lf)
    }

    #[cfg_attr(feature = "tracing", instrument(skip(self), level = "trace"))]
    pub async fn cdr(&mut self, assert_signature: bool) -> Result<CDR> {
        if assert_signature {
            self.assert_signature(Signature::CDRH).await?;
        }

        let options = self.options;
        let cdr = crate::spec::headers1::read_record::<CDRH, CDR, R>(&mut self.reader, |cdrh| {
            crate::base::read1::valid::validate_extra_field_size(cdrh.extra_field_length, options)?;
            Ok(usize::from(cdrh.file_name_length)
                + usize::from(cdrh.extra_field_length)
                + usize::from(cdrh.file_comment_length))
        }).await?;

        crate::base::read1::valid::validate_extra_field_num(&cdr.efs, options)?;

        Ok(cdr)
    }

    #[cfg_attr(feature = "tracing", instrument(skip(self), level = "trace"))]
    pub async fn eocdr(&mut self) -> Result<EOCDR> {
        // We don't assert the signature, as this is verified by the locator.

        crate::spec::headers1::read_record::<EOCDRH, EOCDR, R>(&mut self.reader, |eocdrh| {
            Ok(usize::from(eocdrh.comment_length))
        }).await
    }
}

pub(crate) struct SeekOps<R> {
    reader: R,
}

impl<R: AsyncBufRead + AsyncSeek + Unpin> SeekOps<R> {
    pub fn new(reader: R) -> Self {
        Self { reader }
    }

    #[cfg_attr(feature = "tracing", instrument(skip(self), level = "trace"))]
    pub async fn open(&mut self, opts: ZipOptions) -> Result<ZipArchiveInner> {
        let eor = self.reader.seek(SeekFrom::End(0)).await?;

        let eocdr_offset = match opts.eocdr_locate_method {
            ZipLocateMethod::SeekBackReadLinearly => Method0::new(&mut self.reader).locate(eor).await?,
        };

        // The locator returns the offset of the signature, which it has already matched for us.
        let offset = crate::base::read1::valid_offset(eocdr_offset + Signature::SIZE as u64, eor)?;
        self.reader.seek(SeekFrom::Start(offset)).await?;

        let eocdr = Ops::new(&mut self.reader, &opts).eocdr().await?;
        let mut ceocdr = CEOCDR { eocdr, eocdr64: None, eocdl64: None };

        if opts.validate_eor_is_eoa {
            // TODO: We should be able to do this without any seeks. Though, attempting a one-byte
            //       read might be equivalent performance wise, not sure. This is clean anyway.

            let current = self.reader.seek(SeekFrom::Current(0)).await?;
            let end = self.reader.seek(SeekFrom::End(0)).await?;

            if current != end {
                return Err(ZipError::EORIsNotEOA);
            }
        }

        if let Some((locator, record)) = SeekOps::new(&mut self.reader).zip64(eocdr_offset, eor, &opts).await? {
            ceocdr.eocdr64 = Some(record);
            ceocdr.eocdl64 = Some(locator);
        }

        let offset = crate::base::read1::valid_offset(ceocdr.cd_offset()?, eor)?;
        self.reader.seek(SeekFrom::Start(offset)).await?;

        crate::base::read1::valid::validate_archive(&ceocdr, &opts)?;
        let (loaded_cdrs, offsets) = SeekOps::new(&mut self.reader).cd(&opts, &ceocdr).await?;

        let inner = ZipArchiveInner {
            loaded_cdrs,
            cdr_offsets: offsets,
            options: opts,
            ceocdr,
            eor,
        };

        Ok(inner)
    }

    pub async fn zip64(&mut self, eocdr_offset: u64, eor: u64, opts: &ZipOptions) -> Result<Option<(EOCDL64H, EOCDR64H)>> {
        let zip64_locator_pos = eocdr_offset.saturating_sub(Signature::SIZE as u64);
        let zip64_locator_pos = zip64_locator_pos.saturating_sub(EOCDL64H::SIZE as u64);
        
        let offset = crate::base::read1::valid_offset(zip64_locator_pos, eor)?;
        self.reader.seek(SeekFrom::Start(offset)).await?;
        let signature = crate::spec::headers1::read::<Signature, R>(&mut self.reader).await;

        match signature {
            // Invalid signature, which means there is no ZIP64 locator.
            // TODO: Signature being a discriminant enum makes this a bit awkward.
            Err(ZipError::BinaryParseError(_)) => return Ok(None),
            Err(err) => return Err(err),
            Ok(Signature::EOCDL64H) => {},
            Ok(_) => return Ok(None),
        }

        let eocdl64h = crate::spec::headers1::read::<EOCDL64H, R>(&mut self.reader).await?;
        let offset = crate::base::read1::valid_offset(eocdl64h.relative_offset_of_eocdr64, eor)?;
        self.reader.seek(SeekFrom::Start(offset)).await?;
        Ops::new(&mut self.reader, opts).assert_signature(Signature::EOCDR64H).await?;

        let eocdr64h = crate::spec::headers1::read::<EOCDR64H, R>(&mut self.reader).await?;

        Ok(Some((eocdl64h, eocdr64h)))
    }

    #[cfg_attr(feature = "tracing", instrument(skip(self), level = "trace"))]
    pub async fn cd(&mut self, options: &ZipOptions, ceocdr: &CEOCDR) -> Result<(Vec<CDR>, Vec<u64>)> {
        let cdrs_capacity = ceocdr.num_entries()?.min(options.max_cd_num_files_load);
        let offsets_capacity = ceocdr.num_entries()?.min(options.max_cd_num_files);
        let load_cdrs = ceocdr.num_entries()? <= options.max_cd_num_files_load;

        let mut offsets = Vec::with_capacity(offsets_capacity as usize);
        let mut cdrs = Vec::with_capacity(cdrs_capacity as usize);
        let mut offset = ceocdr.cd_offset()?;

        loop {
            let signature = crate::spec::headers1::read::<Signature, R>(&mut self.reader).await?;

            if signature != Signature::CDRH {
                break;
            }

            offsets.push(offset);

            let cdr = Ops::new(&mut self.reader, options).cdr(false).await?;

            if load_cdrs {
                cdrs.push(cdr);
            }

            // TODO: use math instead of seeks for position calc.
            offset = self.reader.seek(SeekFrom::Current(0)).await?;
        }

        // Validate that the number of CDRs read matches the number of entries in the EOCDR
        let num_matched = offsets.len() == ceocdr.num_entries()? as usize;
        if options.validate_num_cd_files && !num_matched {
            return Err(crate::error::ZipError::NumFilesMismatch(offsets.len() as u64, ceocdr.num_entries()?));
        }

        Ok((cdrs, offsets))
    }

    #[cfg_attr(feature = "tracing", instrument(skip(self), level = "trace"))]
    pub async fn file(&mut self, cdr: CDR, eor: u64, opts: &ZipOptions) -> Result<LF> {
        // We read the LF instead of just using the CDR because the extra fields may differ (and so the data offset).
        
        let offset = crate::base::read1::valid_offset(cdr.lfh_offset()?, eor)?;
        self.reader.seek(SeekFrom::Start(offset)).await?;

        let mut lf = Ops::new(&mut self.reader, opts).lf().await?;
        crate::base::read1::valid::validate_file(&lf, &cdr, opts)?;

        if cdr.cdrh.gpf.data_descriptor() {
            // We know the values from the CDR are 'good', because they were written after the file finished writing.
            
            lf.lfh.compressed_size = cdr.cdrh.compressed_size;
            lf.lfh.uncompressed_size = cdr.cdrh.uncompressed_size;
            lf.lfh.crc = cdr.cdrh.crc;

            // TODO: should we move this into the if branch?
            lf.efs.retain(|ef| ef.efh.efid != crate::spec::extra::EFHID::EI64);
            if let Some(zip64ei) = cdr.find_ef(crate::spec::extra::EFHID::EI64) {
                lf.efs.push(zip64ei.clone());
            }
        }

        Ok(lf)
    }
}