use super::error::DataError;
use crate::document::read_document;
use serde::Deserialize;
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Entry {
pub name: String,
pub version: String,
pub url: String,
pub licence: String,
pub sha256: String,
pub extension: String,
#[serde(default)]
pub source: Option<String>,
#[serde(default)]
pub converter: Option<String>,
#[serde(default)]
pub append: Vec<PathBuf>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct File {
entry: Vec<Entry>,
}
#[derive(Clone, Debug)]
pub struct Manifest {
path: PathBuf,
entries: Vec<Entry>,
}
impl Manifest {
pub fn load(path: &Path) -> Result<Self, DataError> {
let fail = |message: String| DataError::Manifest {
path: path.to_owned(),
message,
};
let file: File = read_document(path).map_err(|error| fail(error.to_string()))?;
let mut names = std::collections::BTreeSet::new();
for entry in &file.entry {
check(entry).map_err(|message| fail(format!("entry `{}`: {message}", entry.name)))?;
if !names.insert(entry.name.as_str()) {
return Err(fail(format!("entry `{}` is declared twice", entry.name)));
}
}
Ok(Self {
path: path.to_owned(),
entries: file.entry,
})
}
#[cfg(test)]
pub(super) fn from_entries(path: PathBuf, entries: Vec<Entry>) -> Self {
Self { path, entries }
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn entries(&self) -> &[Entry] {
&self.entries
}
pub fn entry(&self, name: &str) -> Result<&Entry, DataError> {
self.entries
.iter()
.find(|entry| entry.name == name)
.ok_or_else(|| DataError::NoEntry {
path: self.path.clone(),
name: name.to_owned(),
})
}
#[must_use]
pub fn base(&self) -> &Path {
match self.path.parent() {
Some(parent) => parent,
None => Path::new(""),
}
}
}
impl Entry {
#[must_use]
pub fn file_name(&self) -> String {
format!("{}.{}", self.name, self.extension)
}
}
fn check(entry: &Entry) -> Result<(), String> {
let word = |key: &str, value: &str| {
let fits = !value.is_empty()
&& value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'));
if fits {
Ok(())
} else {
Err(format!("`{key}` must be letters, digits, `-`, `_` or `.`"))
}
};
word("name", &entry.name)?;
word("extension", &entry.extension)?;
for (key, value) in [("version", &entry.version), ("licence", &entry.licence)] {
if value.trim().is_empty() || value.contains('\n') {
return Err(format!("`{key}` must be one non-empty line"));
}
}
if let Some(source) = &entry.source
&& (source.trim().is_empty() || source.contains('\n'))
{
return Err("`source` must be one non-empty line".to_owned());
}
if !entry.url.starts_with("https://") {
return Err("`url` must start with https://".to_owned());
}
if entry.sha256.len() != 64 || !entry.sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return Err("`sha256` must be 64 hexadecimal digits".to_owned());
}
Ok(())
}
#[cfg(test)]
#[path = "manifest_tests.rs"]
mod tests;