use std::fs::File;
use std::path::{Component, Path, PathBuf};
use zip::{CompressionMethod, DateTime, ZipArchive};
use crate::codec::EncodingKind;
use crate::error::XzipError;
use crate::filter::PathFilter;
#[derive(Debug, Clone)]
pub struct ArchiveEntry {
pub name: String,
pub is_dir: bool,
pub size: u64,
pub compressed_size: u64,
pub compression: CompressionMethod,
pub modified: Option<DateTime>,
}
pub fn open_archive(path: &Path) -> Result<ZipArchive<File>, XzipError> {
let file = File::open(path)?;
Ok(ZipArchive::new(file)?)
}
pub fn collect_filtered_entries(
archive: &mut ZipArchive<File>,
encoding: EncodingKind,
filter: &PathFilter,
) -> Result<Vec<ArchiveEntry>, XzipError> {
let mut entries = Vec::new();
for i in 0..archive.len() {
let entry = archive.by_index(i)?;
let raw_name = entry.name_raw();
let decoded_name = encoding.decode(raw_name)?;
if !filter.allows(&decoded_name) {
continue;
}
let display_name = if entry.is_dir() && !decoded_name.ends_with('/') {
format!("{decoded_name}/")
} else {
decoded_name
};
entries.push(ArchiveEntry {
name: display_name,
is_dir: entry.is_dir(),
size: entry.size(),
compressed_size: entry.compressed_size(),
compression: entry.compression(),
modified: entry.last_modified(),
});
}
Ok(entries)
}
pub fn sanitize_relative_path(path: &str) -> Result<PathBuf, XzipError> {
let candidate = Path::new(path);
let mut safe = PathBuf::new();
for comp in candidate.components() {
match comp {
Component::Normal(part) => safe.push(part),
Component::CurDir => {}
Component::ParentDir | Component::Prefix(_) | Component::RootDir => {
return Err(XzipError::UnsafePath(path.to_string()));
}
}
}
if safe.as_os_str().is_empty() {
return Err(XzipError::UnsafePath(path.to_string()));
}
Ok(safe)
}
pub fn compression_label(method: CompressionMethod) -> &'static str {
match method {
CompressionMethod::Stored => "stored",
CompressionMethod::Deflated => "deflated",
_ => "unknown",
}
}
pub fn format_datetime(modified: Option<DateTime>) -> (String, String) {
match modified {
Some(dt) => (
format!("{:04}-{:02}-{:02}", dt.year(), dt.month(), dt.day()),
format!("{:02}:{:02}", dt.hour(), dt.minute()),
),
None => ("----------".to_string(), "-----".to_string()),
}
}
#[cfg(test)]
mod tests {
use super::sanitize_relative_path;
#[test]
fn rejects_parent_dir() {
assert!(sanitize_relative_path("../evil.txt").is_err());
}
}