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);
}
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) .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>>>()?;
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);
}
cds.push(cd);
}
if cds.is_empty() {
return Err(Error::NoCentralDirectoryHeaderFound);
}
Ok(cds)
}