use std::path::Path;
use serde::Deserialize;
use super::ContentError;
use crate::integrity::Sha256Digest;
pub const TAG_PREFIX: &str = "content-v";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContentLock {
tag: String,
sha256: Sha256Digest,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawLock {
tag: String,
sha256: String,
}
impl ContentLock {
pub fn new(tag: impl Into<String>, sha256: Sha256Digest) -> Result<Self, ContentError> {
let tag = tag.into();
validate_tag(&tag)?;
Ok(Self { tag, sha256 })
}
pub fn load(path: &Path) -> Result<Self, ContentError> {
let text = match std::fs::read_to_string(path) {
Ok(text) => text,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(ContentError::NotInstalled {
lock_path: path.to_path_buf(),
});
}
Err(source) => {
return Err(ContentError::LockUnreadable {
path: path.to_path_buf(),
source,
});
}
};
let invalid = |reason: String| ContentError::LockInvalid {
path: path.to_path_buf(),
reason,
};
let raw: RawLock = toml::from_str(&text).map_err(|e| invalid(e.to_string()))?;
let sha256 = Sha256Digest::parse_hex(&raw.sha256).map_err(|e| invalid(e.to_string()))?;
Self::new(raw.tag, sha256).map_err(|e| invalid(e.to_string()))
}
pub fn store(&self, path: &Path) -> Result<(), ContentError> {
let write_err = |source| ContentError::LockWrite {
path: path.to_path_buf(),
source,
};
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(write_err)?;
}
let body = format!(
"# Written by the trusty content resolver (ADR-0064). Do not edit.\n\
tag = \"{}\"\nsha256 = \"{}\"\n",
self.tag, self.sha256
);
crate::atomic_file::write_atomic(path, body.as_bytes()).map_err(write_err)
}
pub fn tag(&self) -> &str {
&self.tag
}
pub fn sha256(&self) -> &Sha256Digest {
&self.sha256
}
pub fn bundle_file_name(&self) -> String {
format!("{}.tar.gz", self.tag)
}
}
pub fn validate_tag(tag: &str) -> Result<(), ContentError> {
let invalid = || ContentError::InvalidTag {
tag: tag.to_owned(),
};
let version = tag.strip_prefix(TAG_PREFIX).ok_or_else(invalid)?;
let (core, pre) = match version.split_once('-') {
Some((core, pre)) => (core, Some(pre)),
None => (version, None),
};
let numeric = |part: &str| {
!part.is_empty()
&& part.bytes().all(|b| b.is_ascii_digit())
&& (part == "0" || !part.starts_with('0'))
};
let parts: Vec<&str> = core.split('.').collect();
if parts.len() != 3 || !parts.iter().all(|p| numeric(p)) {
return Err(invalid());
}
if let Some(pre) = pre {
let ok = !pre.is_empty()
&& pre
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'-');
if !ok {
return Err(invalid());
}
}
Ok(())
}