hdiff-update-core 0.1.3

Core library for signed HDiffPatch-based differential application updates.
Documentation
use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};

use crate::{Error, Result};

pub const DEFAULT_ALGORITHM_NAME: &str = "hdiffpatch-v5-window-zstd";
pub const MANIFEST_SIGNATURE_ALGORITHM: &str = "ed25519";

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Artifact {
    pub url: String,
    pub sha256: String,
    pub size: u64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub signature: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeltaArtifact {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub from_version: Option<String>,
    pub from_sha256: String,
    pub url: String,
    pub sha256: String,
    pub size: u64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target_sha256: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target_size: Option<u64>,
    #[serde(default = "default_algorithm")]
    pub algorithm: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ManifestSignature {
    #[serde(default = "default_signature_algorithm")]
    pub algorithm: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub key_id: Option<String>,
    pub value: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlatformRelease {
    pub full: Artifact,
    #[serde(default)]
    pub deltas: Vec<DeltaArtifact>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateManifest {
    pub version: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub notes: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub published_at: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub signature: Option<ManifestSignature>,
    pub platforms: BTreeMap<String, PlatformRelease>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum SelectedArtifact {
    Delta { artifact: DeltaArtifact },
    Full { artifact: Artifact },
}

impl UpdateManifest {
    pub fn signing_payload(&self) -> Result<Vec<u8>> {
        let mut unsigned = self.clone();
        unsigned.signature = None;
        Ok(serde_json::to_vec(&unsigned)?)
    }
}

impl UpdateManifest {
    pub fn platform(&self, platform: &str) -> Result<&PlatformRelease> {
        self.platforms
            .get(platform)
            .ok_or_else(|| Error::MissingPlatform {
                platform: platform.to_string(),
            })
    }

    pub fn select_artifact(
        &self,
        platform: &str,
        current_version: Option<&str>,
        current_sha256: Option<&str>,
    ) -> Result<SelectedArtifact> {
        let platform_release = self.platform(platform)?;

        if let Some(current_sha256) = current_sha256 {
            if let Some(delta) = platform_release.deltas.iter().find(|delta| {
                let hash_matches = delta.from_sha256.eq_ignore_ascii_case(current_sha256);
                let version_matches = match (&delta.from_version, current_version) {
                    (Some(from), Some(current)) => from == current,
                    (Some(_), None) => true,
                    (None, _) => true,
                };
                hash_matches && version_matches
            }) {
                return Ok(SelectedArtifact::Delta {
                    artifact: delta.clone(),
                });
            }
        }

        Ok(SelectedArtifact::Full {
            artifact: platform_release.full.clone(),
        })
    }
}

pub fn default_platform() -> String {
    let os = if cfg!(target_os = "windows") {
        "windows"
    } else if cfg!(target_os = "macos") {
        "darwin"
    } else if cfg!(target_os = "linux") {
        "linux"
    } else {
        std::env::consts::OS
    };

    let arch = if cfg!(target_arch = "x86_64") {
        "x86_64"
    } else if cfg!(target_arch = "aarch64") {
        "aarch64"
    } else {
        std::env::consts::ARCH
    };

    format!("{os}-{arch}")
}

fn default_algorithm() -> String {
    DEFAULT_ALGORITHM_NAME.to_string()
}

fn default_signature_algorithm() -> String {
    MANIFEST_SIGNATURE_ALGORITHM.to_string()
}