tripley-native-core 0.1.2

Core Tripley Native xRPC services for desktop and WebView containers
Documentation
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(())
}