use std::fs;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::{Component, Path};
use crate::archive::{AlzArchive, AlzFileEntry, CompressionMethod};
use crate::crypto::ZipCrypto;
use crate::decompress::{bzip2, deflate, raw};
use crate::dostime::dos_datetime_to_systime;
use crate::encoding::password_to_cp949;
use crate::error::{AlzError, AlzResult};
const SYMLINK_TARGET_MAX: u64 = 65536;
fn has_unsafe_components(path: &Path) -> bool {
path.components().any(|c| {
matches!(
c,
Component::ParentDir | Component::RootDir | Component::Prefix(_)
)
})
}
pub fn extract_entry(
archive: &mut AlzArchive,
entry: &AlzFileEntry,
dest_dir: &Path,
password: Option<&str>,
pipe_mode: bool,
) -> AlzResult<()> {
let mut crypto = if entry.is_encrypted() {
let pwd = password.ok_or(AlzError::PasswordNotSet)?;
let enc_chk = entry.enc_check.as_ref().ok_or(AlzError::PasswordNotSet)?;
let pwd_bytes = password_to_cp949(pwd);
let mut c = ZipCrypto::new(&pwd_bytes);
if !c.check_header(
enc_chk,
entry.file_crc,
entry.file_time_date,
entry.has_data_descriptor(),
) {
return Err(AlzError::InvalidPassword);
}
let mut c = ZipCrypto::new(&pwd_bytes);
let mut hdr_copy = *enc_chk;
c.decrypt(&mut hdr_copy);
Some(c)
} else {
None
};
let file_name = entry.file_name.replace('\\', "/");
if has_unsafe_components(Path::new(&file_name)) {
return Err(AlzError::PathTraversal(file_name));
}
let dest_path = dest_dir.join(&file_name);
if !pipe_mode {
let canonical_dest = fs::canonicalize(dest_dir)?;
let resolved = if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent)?;
fs::canonicalize(parent)?.join(dest_path.file_name().unwrap_or_default())
} else {
dest_path.clone()
};
if !resolved.starts_with(&canonical_dest) {
return Err(AlzError::PathTraversal(file_name));
}
}
if entry.is_directory() {
if !pipe_mode {
fs::create_dir_all(&dest_path)?;
}
return Ok(());
}
if entry.is_symlink() {
archive.reader.seek(SeekFrom::Start(entry.data_pos))?;
let mut limited = (&mut archive.reader).take(entry.compressed_size);
let mut buf = Vec::new();
let crc = decompress_to(
&mut limited,
&mut buf,
entry,
SYMLINK_TARGET_MAX,
crypto.as_mut(),
)?;
if crc != entry.file_crc {
return Err(AlzError::InvalidFileCrc {
expected: entry.file_crc,
got: crc,
});
}
let target = String::from_utf8_lossy(&buf);
if pipe_mode {
let stdout = io::stdout();
let mut out = stdout.lock();
out.write_all(target.as_bytes())
.map_err(AlzError::CantOpenDestFile)?;
} else {
let normalized = target.replace('\\', "/");
if has_unsafe_components(Path::new(&normalized)) {
return Err(AlzError::PathTraversal(target.into_owned()));
}
#[cfg(unix)]
std::os::unix::fs::symlink(target.as_ref(), &dest_path)?;
#[cfg(not(unix))]
fs::write(&dest_path, target.as_bytes())?;
}
return Ok(());
}
archive.reader.seek(SeekFrom::Start(entry.data_pos))?;
let mut limited = (&mut archive.reader).take(entry.compressed_size);
let crc = if pipe_mode {
let stdout = io::stdout();
let mut out = stdout.lock();
decompress_to(
&mut limited,
&mut out,
entry,
entry.uncompressed_size,
crypto.as_mut(),
)?
} else {
let mut file = fs::File::create(&dest_path).map_err(AlzError::CantOpenDestFile)?;
let crc = match decompress_to(
&mut limited,
&mut file,
entry,
entry.uncompressed_size,
crypto.as_mut(),
) {
Ok(crc) => crc,
Err(e) => {
drop(file);
let _ = fs::remove_file(&dest_path);
return Err(e);
}
};
file.flush().map_err(AlzError::CantOpenDestFile)?;
if let Some(systime) = dos_datetime_to_systime(entry.file_time_date) {
let _ = file.set_modified(systime);
}
drop(file);
crc
};
if crc != entry.file_crc {
if !pipe_mode {
let _ = fs::remove_file(&dest_path);
}
return Err(AlzError::InvalidFileCrc {
expected: entry.file_crc,
got: crc,
});
}
Ok(())
}
fn decompress_to<R: io::Read, W: Write>(
reader: &mut R,
writer: &mut W,
entry: &AlzFileEntry,
max_output: u64,
crypto: Option<&mut ZipCrypto>,
) -> AlzResult<u32> {
match entry.compression_method {
CompressionMethod::Store => {
raw::extract_raw(reader, writer, entry.compressed_size, max_output, crypto)
}
CompressionMethod::Deflate => {
deflate::extract_deflate(reader, writer, entry.compressed_size, max_output, crypto)
}
CompressionMethod::Bzip2 => {
bzip2::extract_bzip2(reader, writer, entry.compressed_size, max_output, crypto)
}
CompressionMethod::Unknown(n) => Err(AlzError::UnknownCompressionMethod(n)),
}
}
pub fn extract_all(
archive: &mut AlzArchive,
dest_dir: &Path,
password: Option<&str>,
pipe_mode: bool,
quiet: bool,
) -> AlzResult<()> {
let entries: Vec<AlzFileEntry> = archive.entries.clone();
for entry in &entries {
if !quiet && !pipe_mode {
eprint!(
"\nextracting : {} ({}bytes) ",
entry.file_name, entry.uncompressed_size
);
}
extract_entry(archive, entry, dest_dir, password, pipe_mode)?;
if !quiet && !pipe_mode {
eprint!(".. ok");
}
}
Ok(())
}
pub fn extract_files(
archive: &mut AlzArchive,
dest_dir: &Path,
file_names: &[String],
password: Option<&str>,
pipe_mode: bool,
quiet: bool,
) -> AlzResult<()> {
let entries: Vec<AlzFileEntry> = archive.entries.clone();
for name in file_names {
if let Some(entry) = entries.iter().find(|e| e.file_name == *name) {
if !quiet && !pipe_mode {
eprint!(
"\nextracting : {} ({}bytes) ",
entry.file_name, entry.uncompressed_size
);
}
extract_entry(archive, entry, dest_dir, password, pipe_mode)?;
if !quiet && !pipe_mode {
eprint!(".. ok");
}
} else if !quiet && !pipe_mode {
eprintln!("\nfilename not matched : {name}");
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unsafe_components_are_rejected() {
for p in ["../etc", "a/../../b", "..", "a/..", "/etc/passwd"] {
assert!(has_unsafe_components(Path::new(p)), "should reject {p}");
}
for p in ["a/b/c.txt", "한글.txt", "deep/dir/file", "a.b/c"] {
assert!(!has_unsafe_components(Path::new(p)), "should allow {p}");
}
}
}