use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom};
use std::path::{Component, Path, PathBuf};
use std::sync::Arc;
use memmap2::Mmap;
use zip::{CompressionMethod, DateTime, ZipArchive};
use crate::codec::EncodingKind;
use crate::error::XzipError;
use crate::filter::PathFilter;
use crate::fs_util;
#[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 struct MappedArchive {
_file: File,
mmap: Arc<Mmap>,
}
#[derive(Clone)]
pub struct MmapReader {
mmap: Arc<Mmap>,
pos: u64,
}
impl MmapReader {
fn new(mmap: Arc<Mmap>) -> Self {
Self { mmap, pos: 0 }
}
}
impl Read for MmapReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let start = usize::try_from(self.pos).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "archive offset out of range")
})?;
if start >= self.mmap.len() {
return Ok(0);
}
let end = start.saturating_add(buf.len()).min(self.mmap.len());
let count = end - start;
buf[..count].copy_from_slice(&self.mmap[start..end]);
self.pos += count as u64;
Ok(count)
}
}
impl Seek for MmapReader {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let len = self.mmap.len() as u64;
let new_pos = match pos {
SeekFrom::Start(offset) => offset,
SeekFrom::End(offset) => {
if offset >= 0 {
len.saturating_add(offset as u64)
} else {
let magnitude = (-offset) as u64;
len.saturating_sub(magnitude)
}
}
SeekFrom::Current(offset) => {
if offset >= 0 {
self.pos.saturating_add(offset as u64)
} else {
self.pos.saturating_sub((-offset) as u64)
}
}
};
self.pos = new_pos;
Ok(new_pos)
}
}
impl MappedArchive {
pub fn open(path: &Path) -> Result<Self, XzipError> {
let file = fs_util::open_zip_file(path)?;
let mmap = unsafe { Mmap::map(&file)? };
Ok(Self {
_file: file,
mmap: Arc::new(mmap),
})
}
pub fn open_archive(&self) -> Result<ZipArchive<MmapReader>, XzipError> {
Ok(ZipArchive::new(MmapReader::new(self.mmap.clone()))?)
}
}
pub fn open_archive(path: &Path) -> Result<ZipArchive<File>, XzipError> {
let file = fs_util::open_zip_file(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());
}
}