use anyhow::{anyhow, Context, Error, Result};
use fehler::throws;
use regex::Regex;
use serde::{Deserialize, Deserializer};
use std::path::{Path, PathBuf};
use url::Url;
use versions::Versioning;
fn deserialize_versioning<'de, D>(d: D) -> std::result::Result<Versioning, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(d).and_then(|s| {
Versioning::new(&s)
.ok_or_else(|| serde::de::Error::custom(format!("Invalid version: {:?}", s)))
})
}
fn deserialize_spdx<'de, D>(d: D) -> std::result::Result<spdx::Expression, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(d)
.and_then(|s| spdx::Expression::parse(&s).map_err(serde::de::Error::custom))
}
#[derive(Debug, PartialEq, Deserialize)]
pub struct Info {
pub name: String,
#[serde(deserialize_with = "deserialize_versioning")]
pub version: Versioning,
pub url: String,
#[serde(deserialize_with = "deserialize_spdx", alias = "licence")]
pub license: spdx::Expression,
}
#[derive(Debug, PartialEq, Deserialize)]
pub struct VersionCheck {
pub args: Vec<String>,
pub pattern: String,
}
impl VersionCheck {
pub fn regex(&self) -> std::result::Result<Regex, regex::Error> {
Regex::new(&self.pattern)
}
}
#[derive(Debug, PartialEq, Deserialize)]
pub struct Discover {
pub binary: String,
pub version_check: VersionCheck,
}
fn deserialize_hex<'de, D>(d: D) -> std::result::Result<Option<Vec<u8>>, D::Error>
where
D: Deserializer<'de>,
{
Option::<String>::deserialize(d).and_then(|v| {
v.map(|s| hex::decode(s).map_err(serde::de::Error::custom))
.transpose()
})
}
#[derive(Debug, Default, PartialEq, Deserialize)]
pub struct Checksums {
#[serde(deserialize_with = "deserialize_hex", default)]
pub b2: Option<Vec<u8>>,
#[serde(deserialize_with = "deserialize_hex", default)]
pub sha512: Option<Vec<u8>>,
#[serde(deserialize_with = "deserialize_hex", default)]
pub sha256: Option<Vec<u8>>,
#[serde(deserialize_with = "deserialize_hex", default)]
pub sha1: Option<Vec<u8>>,
}
impl Checksums {
pub fn is_empty(&self) -> bool {
if let Checksums {
b2: None,
sha512: None,
sha256: None,
sha1: None,
} = self
{
true
} else {
false
}
}
}
#[derive(Debug, PartialEq, Deserialize, Copy, Clone)]
pub enum Shell {
#[serde(rename = "fish")]
Fish,
}
#[derive(Debug, PartialEq, Deserialize, Copy, Clone)]
#[serde(tag = "type")]
pub enum Target {
#[serde(rename = "binary", alias = "bin")]
Binary,
#[serde(rename = "manpage", alias = "man")]
Manpage {
section: u8,
},
#[serde(rename = "completion")]
Completion {
shell: Shell,
},
}
impl Target {
pub fn is_executable(self) -> bool {
match self {
Target::Binary => true,
_ => false,
}
}
}
#[derive(Debug, PartialEq, Deserialize)]
pub struct InstallFile {
pub source: PathBuf,
pub name: Option<String>,
#[serde(flatten)]
pub target: Target,
}
fn deserialize_url<'de, D>(d: D) -> std::result::Result<Url, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(d).and_then(|s| Url::parse(&s).map_err(serde::de::Error::custom))
}
#[derive(Debug, PartialEq, Deserialize)]
#[serde(untagged)]
pub enum Install {
SingleFile {
name: Option<String>,
#[serde(flatten)]
target: Target,
},
FilesFromArchive {
files: Vec<InstallFile>,
},
}
fn deserialize_and_validate_checksums<'de, D>(d: D) -> std::result::Result<Checksums, D::Error>
where
D: Deserializer<'de>,
{
Checksums::deserialize(d).and_then(|checksums| {
if checksums.is_empty() {
Err(serde::de::Error::custom("No checksums given"))
} else {
Ok(checksums)
}
})
}
#[derive(Debug, PartialEq, Deserialize)]
pub struct InstallDownload {
#[serde(deserialize_with = "deserialize_url")]
pub download: Url,
#[serde(deserialize_with = "deserialize_and_validate_checksums")]
pub checksums: Checksums,
#[serde(flatten)]
pub install: Install,
}
impl InstallDownload {
#[throws]
pub fn filename(&self) -> &str {
self.download
.path_segments()
.ok_or_else(|| anyhow!("Expected path segments in URL {}", self.download))?
.last()
.unwrap()
}
}
#[derive(Debug, PartialEq, Deserialize)]
pub struct Manifest {
pub info: Info,
pub discover: Discover,
pub install: Vec<InstallDownload>,
}
impl Manifest {
pub fn read_from_path<P: AsRef<Path>>(path: P) -> Result<Manifest> {
toml::from_str(&std::fs::read_to_string(path.as_ref())?)
.with_context(|| format!("File {} is no valid manifest", path.as_ref().display()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn deserialize_manifest_with_files() {
let manifest = Manifest::read_from_path("tests/manifests/ripgrep.toml").unwrap();
assert_eq!(manifest, Manifest {
info: Info {
name: "ripgrep".to_string(),
version: Versioning::new("12.1.1").unwrap(),
url: "https://github.com/BurntSushi/ripgrep".to_string(),
license: spdx::Expression::parse("Unlicense OR MIT").unwrap(),
},
discover: Discover {
binary: "rg".to_string(),
version_check: VersionCheck {
args: vec!["--version".to_string()],
pattern: "ripgrep ([^ ]+)".to_string(),
},
},
install: vec![
InstallDownload {
download: Url::parse("https://github.com/BurntSushi/ripgrep/releases/download/12.1.1/ripgrep-12.1.1-x86_64-unknown-linux-musl.tar.gz").unwrap(),
checksums: Checksums {
b2: Some(hex::decode("1c97a37e109f818bce8e974eb3a29eb8d1ca488e048caff658696211e8cad23728a767a2d6b97fed365d24f9545f1bc49a3e2687ab437eb4189993ad5fe30663").unwrap()),
..Checksums::default()
},
install: Install::FilesFromArchive {
files: vec![
InstallFile {
source: Path::new("ripgrep-12.1.1-x86_64-unknown-linux-musl/rg").to_path_buf(),
name: None,
target: Target::Binary,
},
InstallFile {
source: Path::new("ripgrep-12.1.1-x86_64-unknown-linux-musl/doc/rg.1").to_path_buf(),
name: None,
target: Target::Manpage { section: 1 },
},
InstallFile {
source: Path::new("ripgrep-12.1.1-x86_64-unknown-linux-musl/complete/rg.fish").to_path_buf(),
name: None,
target: Target::Completion { shell: Shell::Fish },
}
],
}
}
],
})
}
#[test]
fn deserialize_manifest_with_single_file() {
let manifest = Manifest::read_from_path("tests/manifests/shfmt.toml").unwrap();
assert_eq!(
manifest,
Manifest {
info: Info {
name: "shfmt".to_string(),
version: Versioning::new("3.1.1").unwrap(),
url: "https://github.com/mvdan/sh".to_string(),
license: spdx::Expression::parse("BSD-3-Clause").unwrap()
},
discover: Discover {
binary: "shfmt".to_string(),
version_check: VersionCheck {
args: vec!["-version".to_string()],
pattern: "v(\\d\\S+)".to_string()
}
},
install: vec![InstallDownload {
download: Url::parse("https://github.com/mvdan/sh/releases/download/v3.1.1/shfmt_v3.1.1_linux_amd64").unwrap(),
checksums: Checksums {
b2: Some(hex::decode("15b203be254ca46b25d35654ceaae91b7e9200f49cd81e103eae7dd80d9e73ab4455c33e6f20073ba2b45f93b06e94e46556c1ab619812718185e071576cf48c").unwrap()),
..Checksums::default()
},
install: Install::SingleFile {
name: Some("shfmt".to_string()),
target: Target::Binary
}
}]
}
)
}
}