tauri-plugin-hdiff-update 0.1.1

Tauri v2 plugin for signed HDiffPatch differential installer updates.
Documentation
use std::{path::PathBuf, process::Command};

use hdiff_update_core::{
    apply_patch as core_apply_patch, create_patch as core_create_patch,
    prepare_update as core_prepare_update, sha256_file, verify_sha256, ApplyPatchOptions,
    CreatePatchOptions, DownloadEvent, FileDigest, PatchApplyResult, PatchCreateResult,
    PrepareUpdateOptions, PreparedUpdate,
};
use serde::{Deserialize, Serialize};
use tauri::ipc::Channel;

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CommandError {
    pub message: String,
}

impl<E> From<E> for CommandError
where
    E: std::fmt::Display,
{
    fn from(error: E) -> Self {
        Self {
            message: error.to_string(),
        }
    }
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RunInstallerOptions {
    pub installer_path: PathBuf,
    #[serde(default)]
    pub args: Vec<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub current_dir: Option<PathBuf>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub expected_sha256: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DownloadAndInstallOptions {
    pub update: PrepareUpdateOptions,
    #[serde(default)]
    pub installer_args: Vec<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub current_dir: Option<PathBuf>,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RunInstallerResult {
    pub pid: u32,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DownloadAndInstallResult {
    pub update: PreparedUpdate,
    pub pid: u32,
}

#[tauri::command]
pub async fn artifact_sha256(path: PathBuf) -> Result<FileDigest, CommandError> {
    tokio::task::spawn_blocking(move || sha256_file(path))
        .await
        .map_err(CommandError::from)?
        .map_err(CommandError::from)
}

#[tauri::command]
pub async fn create_patch(options: CreatePatchOptions) -> Result<PatchCreateResult, CommandError> {
    tokio::task::spawn_blocking(move || core_create_patch(&options))
        .await
        .map_err(CommandError::from)?
        .map_err(CommandError::from)
}

#[tauri::command]
pub async fn apply_patch(options: ApplyPatchOptions) -> Result<PatchApplyResult, CommandError> {
    tokio::task::spawn_blocking(move || core_apply_patch(&options))
        .await
        .map_err(CommandError::from)?
        .map_err(CommandError::from)
}

#[tauri::command]
pub async fn download_and_apply(
    options: PrepareUpdateOptions,
    on_event: Channel<DownloadEvent>,
) -> Result<PreparedUpdate, CommandError> {
    core_prepare_update(options, |event| {
        let _ = on_event.send(event);
    })
    .await
    .map_err(CommandError::from)
}

#[tauri::command]
pub async fn download_and_install(
    options: DownloadAndInstallOptions,
    on_event: Channel<DownloadEvent>,
) -> Result<DownloadAndInstallResult, CommandError> {
    let installer_args = options.installer_args;
    let current_dir = options.current_dir;
    let update = core_prepare_update(options.update, |event| {
        let _ = on_event.send(event);
    })
    .await
    .map_err(CommandError::from)?;
    let install = tokio::task::spawn_blocking({
        let update = update.clone();
        move || {
            spawn_installer(
                RunInstallerOptions {
                    installer_path: update.artifact_path.clone(),
                    args: installer_args,
                    current_dir,
                    expected_sha256: Some(update.artifact.sha256.clone()),
                },
                true,
            )
        }
    })
    .await
    .map_err(CommandError::from)??;

    Ok(DownloadAndInstallResult {
        update,
        pid: install.pid,
    })
}

#[tauri::command]
pub async fn run_installer(
    options: RunInstallerOptions,
) -> Result<RunInstallerResult, CommandError> {
    tokio::task::spawn_blocking(move || spawn_installer(options, false))
        .await
        .map_err(CommandError::from)?
}

fn spawn_installer(
    options: RunInstallerOptions,
    require_expected_hash: bool,
) -> Result<RunInstallerResult, CommandError> {
    match &options.expected_sha256 {
        Some(expected_sha256) => {
            verify_sha256(&options.installer_path, expected_sha256).map_err(CommandError::from)?;
        }
        None if require_expected_hash => {
            return Err(CommandError::from(
                "refusing to run installer without an expected SHA-256",
            ));
        }
        None => {}
    }
    let mut command = Command::new(&options.installer_path);
    command.args(&options.args);
    if let Some(current_dir) = &options.current_dir {
        command.current_dir(current_dir);
    }
    let child = command.spawn().map_err(|error| {
        CommandError::from(format!(
            "failed to start installer {}: {error}",
            options.installer_path.display()
        ))
    })?;
    Ok(RunInstallerResult { pid: child.id() })
}