use super::*;
use walkdir::WalkDir;
use zip::write::SimpleFileOptions;
use zip::{CompressionMethod, ZipArchive, ZipWriter};
fn join_error(error: tokio::task::JoinError) -> RuntimeError {
runtime_error(format!("archive worker failed: {error}"))
}
fn zip_error(error: zip::result::ZipError) -> RuntimeError {
runtime_error(format!("zip operation failed: {error}"))
}
pub(crate) async fn archive_call(
state: NativeState,
method: u32,
payload: Value,
) -> Result<Value, RuntimeError> {
match method {
1 => {
let source_path = path_arg(&payload, 0)?;
let archive_path = path_arg(&payload, 1)?;
let overwrite = bool_arg(&payload, 2).unwrap_or(false);
let include_root = bool_arg(&payload, 3).unwrap_or(true);
state
.policy
.allow_fs_path("archive_zip_read", &source_path)?;
state
.policy
.allow_fs_path("archive_zip_write", &archive_path)?;
tokio::task::spawn_blocking(move || {
zip_path_sync(&source_path, &archive_path, overwrite, include_root)
})
.await
.map_err(join_error)??;
Ok(empty())
}
2 => {
let archive_path = path_arg(&payload, 0)?;
let destination_path = path_arg(&payload, 1)?;
let overwrite = bool_arg(&payload, 2).unwrap_or(false);
state
.policy
.allow_fs_path("archive_unzip_read", &archive_path)?;
state
.policy
.allow_fs_path("archive_unzip_write", &destination_path)?;
tokio::task::spawn_blocking(move || {
unzip_path_sync(&archive_path, &destination_path, overwrite)
})
.await
.map_err(join_error)??;
Ok(empty())
}
_ => Err(method_not_found(method)),
}
}
fn zip_path_sync(
source_path: &Path,
archive_path: &Path,
overwrite: bool,
include_root: bool,
) -> Result<(), RuntimeError> {
let source_meta = std::fs::symlink_metadata(source_path).map_err(io_error)?;
if source_meta.file_type().is_symlink() {
return Err(runtime_error("cannot zip symbolic link source paths"));
}
if source_meta.is_dir() && path_is_under(archive_path, source_path) {
return Err(runtime_error(
"archive path cannot be inside the source directory",
));
}
if archive_path.exists() && !overwrite {
return Err(runtime_error(format!(
"archive `{}` already exists",
archive_path.display()
)));
}
if let Some(parent) = archive_path.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent).map_err(io_error)?;
}
}
let archive_file = std::fs::File::create(archive_path).map_err(io_error)?;
let mut zip = ZipWriter::new(archive_file);
let options = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
let base = if source_meta.is_dir() && !include_root {
source_path.to_path_buf()
} else {
source_path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(PathBuf::new)
};
if source_meta.is_file() {
add_zip_file(&mut zip, source_path, &base, options)?;
} else if source_meta.is_dir() {
for entry in WalkDir::new(source_path).follow_links(false) {
let entry = entry.map_err(|error| runtime_error(error.to_string()))?;
let path = entry.path();
let file_type = entry.file_type();
if file_type.is_symlink() {
return Err(runtime_error(format!(
"cannot zip symbolic link `{}`",
path.display()
)));
}
if path == source_path && !include_root {
continue;
}
if file_type.is_dir() {
add_zip_directory(&mut zip, path, &base, options)?;
} else if file_type.is_file() {
add_zip_file(&mut zip, path, &base, options)?;
}
}
} else {
return Err(runtime_error("zip source must be a file or directory"));
}
zip.finish().map_err(zip_error)?;
Ok(())
}
fn add_zip_directory(
zip: &mut ZipWriter<std::fs::File>,
path: &Path,
base: &Path,
options: SimpleFileOptions,
) -> Result<(), RuntimeError> {
let relative = relative_zip_path(path, base)?;
if relative.as_os_str().is_empty() {
return Ok(());
}
zip.add_directory_from_path(relative, options)
.map_err(zip_error)?;
Ok(())
}
fn add_zip_file(
zip: &mut ZipWriter<std::fs::File>,
path: &Path,
base: &Path,
options: SimpleFileOptions,
) -> Result<(), RuntimeError> {
let relative = relative_zip_path(path, base)?;
if relative.as_os_str().is_empty() {
return Err(runtime_error("zip file entry path cannot be empty"));
}
zip.start_file_from_path(relative, options)
.map_err(zip_error)?;
let mut file = std::fs::File::open(path).map_err(io_error)?;
std::io::copy(&mut file, zip).map_err(io_error)?;
Ok(())
}
fn relative_zip_path<'a>(path: &'a Path, base: &Path) -> Result<&'a Path, RuntimeError> {
path.strip_prefix(base)
.map_err(|error| runtime_error(format!("failed to build relative zip path: {error}")))
}
fn unzip_path_sync(
archive_path: &Path,
destination_path: &Path,
overwrite: bool,
) -> Result<(), RuntimeError> {
std::fs::create_dir_all(destination_path).map_err(io_error)?;
let archive_file = std::fs::File::open(archive_path).map_err(io_error)?;
let mut archive = ZipArchive::new(archive_file).map_err(zip_error)?;
for index in 0..archive.len() {
let mut file = archive.by_index(index).map_err(zip_error)?;
if file.is_symlink() {
return Err(runtime_error(format!(
"cannot unzip symbolic link `{}`",
file.name()
)));
}
let enclosed = file
.enclosed_name()
.ok_or_else(|| runtime_error(format!("zip entry `{}` is not enclosed", file.name())))?;
let output_path = destination_path.join(enclosed);
if file.is_dir() {
std::fs::create_dir_all(&output_path).map_err(io_error)?;
continue;
}
if output_path.exists() && !overwrite {
return Err(runtime_error(format!(
"destination `{}` already exists",
output_path.display()
)));
}
if let Some(parent) = output_path.parent() {
std::fs::create_dir_all(parent).map_err(io_error)?;
}
let mut output = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&output_path)
.map_err(io_error)?;
std::io::copy(&mut file, &mut output).map_err(io_error)?;
}
Ok(())
}