From 23a73f7ea2faf84e1c2769eb3c3a6129d5694ba3 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 5 Sep 2021 22:59:47 +0200 Subject: [PATCH] feat(package): implement first version of espanso-hub provider --- Cargo.lock | 74 ++++++++++++++- espanso-package/Cargo.toml | 4 +- espanso-package/src/lib.rs | 17 ++-- espanso-package/src/logging.rs | 23 +++++ espanso-package/src/provider/hub.rs | 137 +++++++++++++++++++++++++++ espanso-package/src/provider/mod.rs | 1 + espanso-package/src/util/download.rs | 30 +++++- 7 files changed, 275 insertions(+), 11 deletions(-) create mode 100644 espanso-package/src/logging.rs create mode 100644 espanso-package/src/provider/hub.rs diff --git a/Cargo.lock b/Cargo.lock index fc7edb9..7b0f7e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,6 +105,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "bstr" version = "0.2.15" @@ -302,6 +311,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +[[package]] +name = "cpufeatures" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.2.1" @@ -425,6 +443,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "dirs" version = "1.0.5" @@ -786,6 +813,7 @@ dependencies = [ "anyhow", "fs_extra", "glob", + "hex", "lazy_static", "log", "natord", @@ -795,6 +823,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "sha2", "tempdir", "thiserror", "zip", @@ -1034,6 +1063,16 @@ version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -1105,6 +1144,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "html2text" version = "0.2.1" @@ -1338,9 +1383,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.98" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" +checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21" [[package]] name = "libdbus-sys" @@ -1785,6 +1830,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "opener" version = "0.5.0" @@ -2350,6 +2401,19 @@ dependencies = [ "yaml-rust 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "sha2" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9204c41a1597a8c5af23c82d1c921cb01ec0a4c59e07a9c7306062829a3903f3" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpufeatures", + "digest", + "opaque-debug", +] + [[package]] name = "simplelog" version = "0.9.0" @@ -2721,6 +2785,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "typenum" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" + [[package]] name = "unicase" version = "2.6.0" diff --git a/espanso-package/Cargo.toml b/espanso-package/Cargo.toml index 72b1d42..0ceeeb6 100644 --- a/espanso-package/Cargo.toml +++ b/espanso-package/Cargo.toml @@ -19,4 +19,6 @@ lazy_static = "1.4.0" regex = "1.4.3" zip = "0.5.13" scopeguard = "1.1.0" -fs_extra = "1.2.0" \ No newline at end of file +fs_extra = "1.2.0" +sha2 = "0.9.6" +hex = "0.4.3" \ No newline at end of file diff --git a/espanso-package/src/lib.rs b/espanso-package/src/lib.rs index 4183d9e..f2bef0b 100644 --- a/espanso-package/src/lib.rs +++ b/espanso-package/src/lib.rs @@ -22,6 +22,8 @@ use std::path::Path; use anyhow::{bail, Result}; mod archive; +#[macro_use] +mod logging; mod manifest; mod package; mod provider; @@ -29,12 +31,12 @@ mod resolver; mod util; pub use archive::{ArchivedPackage, Archiver, SaveOptions, StoredPackage}; -pub use provider::{PackageSpecifier, PackageProvider}; pub use package::Package; +pub use provider::{PackageProvider, PackageSpecifier}; // TODO: once the download is completed, avoid copying files beginning with "." -pub fn get_provider(package: &PackageSpecifier) -> Result> { +pub fn get_provider(package: &PackageSpecifier,) -> Result> { if let Some(git_repo_url) = package.git_repo_url.as_deref() { if !package.use_native_git { let matches_known_hosts = @@ -73,14 +75,15 @@ pub fn get_provider(package: &PackageSpecifier) -> Result Result> { - Ok(Box::new(archive::default::DefaultArchiver::new(package_dir))) + Ok(Box::new(archive::default::DefaultArchiver::new( + package_dir, + ))) } #[cfg(test)] @@ -94,4 +97,4 @@ pub(crate) mod tests { action(&tmp_path); } -} \ No newline at end of file +} diff --git a/espanso-package/src/logging.rs b/espanso-package/src/logging.rs new file mode 100644 index 0000000..cb27597 --- /dev/null +++ b/espanso-package/src/logging.rs @@ -0,0 +1,23 @@ +#[macro_export] +macro_rules! info_println { + ($($tts:tt)*) => { + println!($($tts)*); + log::info!($($tts)*); + } +} + +#[macro_export] +macro_rules! warn_eprintln { + ($($tts:tt)*) => { + eprintln!($($tts)*); + log::warn!($($tts)*); + } +} + +#[macro_export] +macro_rules! error_eprintln { + ($($tts:tt)*) => { + eprintln!($($tts)*); + log::error!($($tts)*); + } +} \ No newline at end of file diff --git a/espanso-package/src/provider/hub.rs b/espanso-package/src/provider/hub.rs new file mode 100644 index 0000000..9380c8c --- /dev/null +++ b/espanso-package/src/provider/hub.rs @@ -0,0 +1,137 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use super::PackageProvider; +use crate::{ + package::DefaultPackage, resolver::resolve_package, util::download::read_string_from_url, + Package, PackageSpecifier, +}; +use anyhow::{anyhow, Context, Result}; +use serde::{Deserialize, Serialize}; + +pub const ESPANSO_HUB_PACKAGE_INDEX_URL: &str = + "https://github.com/espanso/hub/releases/latest/download/package_index.json"; + +pub struct EspansoHubPackageProvider {} + +impl EspansoHubPackageProvider { + pub fn new() -> Self { + Self {} + } +} + +impl PackageProvider for EspansoHubPackageProvider { + fn name(&self) -> String { + "espanso-hub".to_string() + } + + fn download(&self, package: &PackageSpecifier) -> Result> { + // TODO: pass index update flag + let index = self + .get_index(true) + .context("unable to get package index from espanso hub")?; + + let package_info = index + .get_package(&package.name, package.version.as_deref()) + .ok_or_else(|| { + anyhow!( + "unable to find package '{}@{}' in the espanso hub", + package.name, + package.version.as_deref().unwrap_or("latest") + ) + })?; + + let archive_sha256 = read_string_from_url(&package_info.archive_sha256_url) + .context("unable to read archive sha256 signature")?; + + let temp_dir = tempdir::TempDir::new("espanso-package-download")?; + + crate::util::download::download_and_extract_zip_verify_sha256( + &package_info.archive_url, + temp_dir.path(), + Some(&archive_sha256), + )?; + + let resolved_package = + resolve_package(temp_dir.path(), &package.name, package.version.as_deref())?; + + let package = DefaultPackage::new( + resolved_package.manifest, + temp_dir, + resolved_package.base_dir, + ); + + Ok(Box::new(package)) + } +} + +impl EspansoHubPackageProvider { + fn get_index(&self, _force_update: bool) -> Result { + // TODO: if force_update is false, we should try to use a locally-cached version of the package index + self.download_index() + } + + fn download_index(&self) -> Result { + info_println!("fetching package index..."); + let json_body = read_string_from_url(ESPANSO_HUB_PACKAGE_INDEX_URL)?; + + let index: PackageIndex = serde_json::from_str(&json_body)?; + + Ok(index) + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct PackageIndex { + last_update: u64, + packages: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct PackageInfo { + name: String, + title: String, + author: String, + description: String, + version: String, + + archive_url: String, + archive_sha256_url: String, +} + +impl PackageIndex { + fn get_package(&self, name: &str, version: Option<&str>) -> Option { + let mut matching_packages: Vec = self + .packages + .iter() + .filter(|package| package.name == name) + .cloned() + .collect(); + + matching_packages.sort_by(|a, b| natord::compare(&a.version, &b.version)); + + if let Some(explicit_version) = version { + matching_packages + .into_iter() + .find(|package| package.version == explicit_version) + } else { + matching_packages.into_iter().last() + } + } +} \ No newline at end of file diff --git a/espanso-package/src/provider/mod.rs b/espanso-package/src/provider/mod.rs index cf4388b..af51224 100644 --- a/espanso-package/src/provider/mod.rs +++ b/espanso-package/src/provider/mod.rs @@ -21,6 +21,7 @@ use anyhow::Result; use crate::Package; +pub(crate) mod hub; pub(crate) mod git; pub(crate) mod github; diff --git a/espanso-package/src/util/download.rs b/espanso-package/src/util/download.rs index 645e0f5..49724d9 100644 --- a/espanso-package/src/util/download.rs +++ b/espanso-package/src/util/download.rs @@ -17,15 +17,35 @@ * along with espanso. If not, see . */ -use anyhow::{Context, Result}; +use anyhow::{Context, Result, bail}; use std::io::{copy, Cursor}; use std::path::Path; +use sha2::{Sha256, Digest}; pub fn download_and_extract_zip(url: &str, dest_dir: &Path) -> Result<()> { + download_and_extract_zip_verify_sha256(url, dest_dir, None) +} + +pub fn download_and_extract_zip_verify_sha256(url: &str, dest_dir: &Path, sha256: Option<&str>) -> Result<()> { let data = download(url).context("error downloading archive")?; + if let Some(sha256) = sha256 { + info_println!("validating sha256 signature..."); + if !verify_sha256(&data, sha256) { + bail!("signature mismatch"); + } + } extract_zip(data, dest_dir).context("error extracting archive") } +pub fn read_string_from_url(url: &str) -> Result { + let client = reqwest::blocking::Client::builder(); + let client = client.build()?; + + let response = client.get(url).send()?; + + Ok(response.text()?) +} + fn download(url: &str) -> Result> { let client = reqwest::blocking::Client::builder(); let client = client.build()?; @@ -37,6 +57,14 @@ fn download(url: &str) -> Result> { Ok(buffer) } +fn verify_sha256(data: &[u8], sha256: &str) -> bool { + let mut hasher = Sha256::new(); + hasher.update(data); + let result = hasher.finalize(); + let hash = hex::encode(result); + hash == sha256 +} + // Adapted from zip-rs extract.rs example fn extract_zip(data: Vec, dest_dir: &Path) -> Result<()> { let reader = Cursor::new(data);