mod labels;
mod lock;
mod repo;
pub mod rewrite;
pub mod settle;
mod time;
use std::fmt;
use std::num::NonZeroU64;
pub use labels::{Allocated, allocate};
pub use lock::{LOCK, Lock, LockError};
pub use repo::{Operation, RepoState, Stopped};
pub use time::{PublishedAt, TimeError};
use settle::Settlement;
use crate::canonical::Digest;
use crate::check::{Placed, hashed, placed};
use crate::codec::{self, Files, LABELS, ReadError, Sites, SpecPath, read_sited};
use crate::document::Document;
use crate::hash::hash_document;
use crate::identity::{Key, Label};
use crate::validate::validate;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Relabel {
pub element: Key,
pub withdrawn: Label,
pub label: Label,
}
pub struct ReadTree<'a> {
files: &'a Files,
document: Document,
sites: Sites,
}
impl<'a> ReadTree<'a> {
pub fn read(files: &'a Files) -> Result<Self, Vec<ReadError>> {
let (document, sites) = read_sited(files)?;
Ok(Self {
files,
document,
sites,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Publication {
Unchanged(Lock),
Published {
lock: Lock,
files: Files,
allocated: Vec<Allocated>,
rewritten: Vec<Relabel>,
withdrawn: Vec<Relabel>,
finish: Option<Operation>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Refusal {
Modified(Vec<String>),
Branch {
current: Option<String>,
publish: String,
},
Invalid(Vec<Placed>),
Unmerged(Vec<String>),
UnmergedBindings,
Lock {
revision: &'static str,
message: String,
},
}
pub fn publish(
tree: ReadTree<'_>,
repo: &RepoState,
previous: Option<&Lock>,
at: PublishedAt,
) -> Result<Publication, Refusal> {
let ReadTree {
files,
mut document,
sites,
} = tree;
let branch = document.publish_branch.as_deref().unwrap_or("main");
let (sequence, settlement) = if let Some(stopped) = &repo.stopped {
let sequence = settle::preconditions(stopped, branch)?;
(sequence, settle::settle(&mut document, stopped.operation))
} else {
preconditions(repo, branch)?;
let sequence = previous.map_or(NonZeroU64::MIN, |lock| {
lock.publish_sequence.saturating_add(1)
});
(sequence, Settlement::default())
};
let unrecorded = repo
.stopped
.as_ref()
.is_some_and(|stopped| settlement.unrecorded(stopped));
let invalid = placed(validate(&document), &sites);
if !invalid.is_empty() {
return Err(Refusal::Invalid(invalid));
}
let allocated = allocate(&mut document);
let withdrawn = settlement.reattach(&mut document.bindings);
let rewrites = rewrite::rewrite(files, &document.bindings);
let mut written = rewrites.files;
if !written.is_empty() {
write_labels(&mut written, files, &document);
let mut next = files.clone();
next.extend(written.clone());
document = codec::read(&next).expect("rewritten citations read back");
}
let roots = hash_document(&document)
.expect("a document the validator accepts indexes")
.roots;
if let Some(previous) = previous.filter(|lock| lock.state_root == roots.state) {
return Ok(Publication::Unchanged(previous.clone()));
}
if unrecorded {
return Err(Refusal::UnmergedBindings);
}
write_labels(&mut written, files, &document);
Ok(Publication::Published {
lock: Lock {
document_key: document.id,
state_root: roots.state,
content_root: roots.content,
binding_root: roots.bindings,
publish_sequence: sequence,
published_at: at,
derived_from: document.derived_from,
},
files: written,
allocated,
rewritten: rewrites.rewritten,
withdrawn,
finish: repo.stopped.as_ref().map(|stopped| stopped.operation),
})
}
fn preconditions(repo: &RepoState, branch: &str) -> Result<(), Refusal> {
if !repo.modified.is_empty() {
return Err(Refusal::Modified(repo.modified.clone()));
}
if repo.branch.as_deref() != Some(branch) {
return Err(Refusal::Branch {
current: repo.branch.clone(),
publish: branch.to_owned(),
});
}
Ok(())
}
fn write_labels(written: &mut Files, files: &Files, document: &Document) {
let path = SpecPath::new(LABELS);
if written.contains_key(&path) {
return;
}
let labels = codec::write_labels(document);
if files.get(&path) != Some(&labels) {
written.insert(path, labels);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Root {
Content,
Binding,
State,
}
impl Root {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Content => "content",
Self::Binding => "binding",
Self::State => "state",
}
}
}
impl fmt::Display for Root {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{} root", self.name())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Unverified {
Invalid(Vec<Placed>),
Mismatch {
root: Root,
lock: Digest,
tree: Digest,
},
}
pub fn verify(tree: &ReadTree<'_>, lock: &Lock) -> Result<Digest, Unverified> {
let roots = hashed(&tree.document, &tree.sites)
.map_err(Unverified::Invalid)?
.roots;
for (root, lock, tree) in [
(Root::Content, lock.content_root, roots.content),
(Root::Binding, lock.binding_root, roots.bindings),
(Root::State, lock.state_root, roots.state),
] {
if lock != tree {
return Err(Unverified::Mismatch { root, lock, tree });
}
}
Ok(roots.state)
}