remozipsy 0.0.1

zip implementation independent structs and helpers
Documentation
use zip_core::{
    Signature,
    raw::{
        CentralDirectoryHeader, EndOfCentralDirectory, EndOfCentralDirectoryFixed, LocalFileHeaderFixed,
        parse::{Parse, find_next_signature},
    },
};

use crate::model::{Config, Error, RemoteFileInfo};

use super::{FileSystem, RemoteZip};

pub(super) async fn rfile_infos<R, F>(
    remote: &R,
    config: &Config,
) -> Result<Vec<RemoteFileInfo>, Error<R::Error, F::Error>>
where
    R: RemoteZip,
    F: FileSystem,
{
    let eocd = download_eocd::<R, F>(remote, config).await?;
    let mut cds = download_cds::<R, F>(remote, &eocd).await?;

    tracing::trace!(?eocd, "eocd information");

    cds.sort_by_key(|e| e.fixed.relative_offset_of_local_header);

    let build_rfi =
        |cd: &CentralDirectoryHeader, end_offset: u64| -> Result<RemoteFileInfo, Error<R::Error, F::Error>> {
            let file_name =
                String::from_utf8(cd.file_name.clone()).map_err(|_| Error::<R::Error, F::Error>::InvalidFileName)?;
            if file_name.contains('\0') {
                return Err(Error::InvalidFileName);
            }
            //tracing::trace!(?file_name, ?cd, "cd information");

            Ok(RemoteFileInfo {
                crc32: cd.fixed.crc_32,
                compressed_size: cd.fixed.compressed_size,
                uncompressed_size: cd.fixed.uncompressed_size,
                compression_method: cd.fixed.compression_method,
                file_name,
                start_offset: cd.fixed.relative_offset_of_local_header,
                end_offset_inclusive: end_offset,
            })
        };
    let assume_size = |cd: &CentralDirectoryHeader| -> u64 {
        cd.fixed.relative_offset_of_local_header as u64
            + LocalFileHeaderFixed::SIZE_IN_BYTES as u64
            + cd.fixed.file_name_length as u64
            + cd.fixed.extra_field_length as u64
            + cd.fixed.compressed_size as u64
    };
    let windows = cds.windows(2);

    let mut rfiles: Vec<_> = windows
        .filter(|slice| slice[0].fixed.compressed_size != 0) // ignore directories
        .map(|slice| {
            let cd = &slice[0];
            let next_cd = &slice[1];
            let max_possible_end =  (next_cd.fixed.relative_offset_of_local_header as u64).saturating_sub(1);
            let end = if config.assume_cd_contains_lh_content {
                assume_size(cd).min(max_possible_end)
            } else {
                max_possible_end
            };
            build_rfi(cd, end)
        })
        .collect::<Result<Vec<_>, Error<R::Error, F::Error>>>()?;

    // Due to the nature of windows() we ALWAYS need to add an entry for the last
    // element
    if let Some(last) = cds.last() {
        let rfi = build_rfi(
            last,
            eocd.fixed
                .offset_of_start_of_central_directory_with_respect_to_the_starting_disk_number as u64,
        )?;
        rfiles.push(rfi);
    }

    Ok(rfiles)
}

async fn download_eocd<R, F>(remote: &R, config: &Config) -> Result<EndOfCentralDirectory, Error<R::Error, F::Error>>
where
    R: RemoteZip,
    F: FileSystem,
{
    let content_length = remote.get_zip_size().await.map_err(Error::Remote)?;
    let approx_eocd_start = content_length.saturating_sub(config.max_eocd_size);

    let eocd_bytes = remote
        .fetch_bytes(approx_eocd_start..=content_length)
        .await
        .map_err(Error::Remote)?;

    let pos = find_next_signature(
        &eocd_bytes,
        EndOfCentralDirectoryFixed::END_OF_CENTRAL_DIR_SIGNATURE.to_le_bytes(),
    )
    .ok_or(Error::NoEocdFound)?;
    let mut buf = &eocd_bytes[pos..];
    EndOfCentralDirectory::from_buf(&mut buf).map_err(|_| Error::NoEocdFound)
}

async fn download_cds<R, F>(
    remote: &R,
    eocd: &EndOfCentralDirectory,
) -> Result<Vec<CentralDirectoryHeader>, Error<R::Error, F::Error>>
where
    R: RemoteZip,
    F: FileSystem,
{
    let cd_start = eocd
        .fixed
        .offset_of_start_of_central_directory_with_respect_to_the_starting_disk_number as usize;
    let cd_end = cd_start
        .saturating_add(eocd.fixed.size_of_the_central_directory as usize)
        .saturating_sub(1);

    let cds_bytes = remote.fetch_bytes(cd_start..=cd_end).await.map_err(Error::Remote)?;

    let mut buf = &cds_bytes[..];
    let mut cds = Vec::new();
    while let Ok(cd) = CentralDirectoryHeader::from_buf(&mut buf) {
        if !cd.is_valid_signature() {
            return Err(Error::InvalidCentralDirectoryHeaderSignature);
        }
        //tracing::trace!(?cd, "cd information");
        cds.push(cd);
    }

    if cds.is_empty() {
        return Err(Error::NoCentralDirectoryHeaderFound);
    }

    Ok(cds)
}