use std::io::Read;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::error::{self, Result};
use crate::host::{ChangeId, Sha};
use crate::{home, ids, merge_path, provenance};
const WRITER: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct Boundary {
pub(crate) identity: String,
pub(crate) branch: String,
pub(crate) tip: Sha,
pub(crate) base: String,
pub(crate) base_commit: Sha,
pub(crate) inputs: String,
pub(crate) change: ChangeId,
pub(crate) change_url: url::Url,
}
pub(crate) enum Stored {
Nothing,
Unreadable(String),
Found(Box<Boundary>),
}
fn path(identity: &str, branch: &str) -> Result<PathBuf> {
Ok(home::verified_dir()?.join(format!(
"{}.json",
ids::digest(&format!("{identity}\n{branch}"))
)))
}
impl Boundary {
fn malformed(&self) -> Option<&'static str> {
let object_id = |sha: &Sha| crate::git::ObjectId::parse(&sha.0).is_some();
let digest = |value: &str| {
value.len() == 64
&& value
.chars()
.all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c))
};
if !object_id(&self.tip) {
Some("tip")
} else if !crate::git::is_valid_branch_name(&self.base) {
Some("base")
} else if !object_id(&self.base_commit) {
Some("base_commit")
} else if !digest(&self.inputs) {
Some("inputs")
} else {
None
}
}
pub(crate) fn record(&self) {
let written = path(&self.identity, &self.branch).and_then(|at| {
let text = serde_json::to_string_pretty(self)
.map_err(|failure| error::invalid(failure.to_string()))?;
home::ensure_dir(at.parent().expect("a record has a directory"))?;
home::atomic_write(&at, &format!("{text}\n"))
});
if let Err(failure) = written {
eprintln!(
"onevcs: warning: the verified boundary of {branch:?} could not be recorded, so \
publishing it again verifies it again: {failure}",
branch = self.branch,
);
}
}
}
pub(crate) fn read(identity: &str, branch: &str) -> Stored {
let at = match path(identity, branch) {
Ok(at) => at,
Err(failure) => return Stored::Unreadable(failure.to_string()),
};
let text = match std::fs::read_to_string(&at) {
Ok(text) => text,
Err(failure) if failure.kind() == std::io::ErrorKind::NotFound => return Stored::Nothing,
Err(failure) => return Stored::Unreadable(format!("{}: {failure}", at.display())),
};
match serde_json::from_str::<Boundary>(&text) {
Ok(boundary) if boundary.identity == identity && boundary.branch == branch => {
match boundary.malformed() {
Some(field) => Stored::Unreadable(format!(
"{} records a {field} that is not one",
at.display()
)),
None => Stored::Found(Box::new(boundary)),
}
}
Ok(_) => Stored::Unreadable(format!(
"{} records another branch's publication",
at.display()
)),
Err(failure) => Stored::Unreadable(format!("{}: {failure}", at.display())),
}
}
pub(crate) fn forget(identity: &str, branch: &str) {
if let Ok(at) = path(identity, branch) {
let _ = std::fs::remove_file(at);
}
}
pub(crate) fn inputs(
hooks: Option<&Path>,
base: &str,
trailers: &provenance::Trailers,
) -> Result<String> {
let mut read = format!("onevcs {WRITER}\n");
for (name, value) in merge_path::comparison_env("origin", base) {
read.push_str(&format!("env {name}={value}\n"));
}
read.push_str(&format!(
"trailers {}\n{}\n",
trailers.landed(),
trailers.incomplete()
));
match hooks {
None => read.push_str("hooks none\n"),
Some(relative) if relative.is_relative() => {
read.push_str(&format!("hooks in-tree {}\n", relative.display()));
}
Some(installed) => {
read.push_str(&format!("hooks {}\n", installed.display()));
for (name, digest) in installed_hooks(installed)? {
read.push_str(&format!("hook {name} {digest}\n"));
}
}
}
Ok(ids::digest(&read))
}
fn installed_hooks(directory: &Path) -> Result<Vec<(String, String)>> {
let entries = match std::fs::read_dir(directory) {
Ok(entries) => entries,
Err(failure) if failure.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(failure) => return Err(error::at("list the hooks in", directory)(failure)),
};
let mut hooks = Vec::new();
for entry in entries {
let entry = entry.map_err(error::at("list the hooks in", directory))?;
let hook = entry.path();
if !hook.is_file() {
continue;
}
let mut content = Vec::new();
std::fs::File::open(&hook)
.and_then(|mut file| file.read_to_end(&mut content))
.map_err(error::at("read the hook", &hook))?;
let digest: String = Sha256::digest(&content)
.iter()
.map(|byte| format!("{byte:02x}"))
.collect();
hooks.push((entry.file_name().to_string_lossy().into_owned(), digest));
}
hooks.sort();
Ok(hooks)
}