use sha2::{Digest, Sha256};
pub const HEADER_PREFIX: &str = "<!-- nbspec:";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Provenance {
pub change_id: String,
pub notebook: String,
pub note: String,
pub hash: String,
}
pub fn content_hash(body: &str) -> String {
let digest = Sha256::digest(body.as_bytes());
format!("{digest:x}")
}
pub fn render_header(provenance: &Provenance) -> String {
format!(
"{HEADER_PREFIX} change={change} notebook={notebook} note={note} \
hash=sha256:{hash} -->",
change = provenance.change_id,
notebook = provenance.notebook,
note = provenance.note,
hash = provenance.hash,
)
}
pub fn stamp(body: &str, change_id: &str, notebook: &str, note: &str) -> String {
let provenance = Provenance {
change_id: change_id.to_string(),
notebook: notebook.to_string(),
note: note.to_string(),
hash: content_hash(body),
};
format!("{}\n{body}", render_header(&provenance))
}
pub fn split_document(content: &str) -> (Option<Provenance>, &str) {
let Some((first_line, rest)) = content.split_once('\n') else {
return (parse_header(content), "");
};
match parse_header(first_line) {
Some(provenance) => (Some(provenance), rest),
None => (None, content),
}
}
pub fn body_matches(provenance: &Provenance, body: &str) -> bool {
content_hash(body) == provenance.hash
}
fn parse_header(line: &str) -> Option<Provenance> {
let inner = line
.trim_end()
.strip_prefix(HEADER_PREFIX)?
.strip_suffix("-->")?;
let mut change_id = None;
let mut notebook = None;
let mut note = None;
let mut hash = None;
for token in inner.split_whitespace() {
let (key, value) = token.split_once('=')?;
match key {
"change" => change_id = Some(value.to_string()),
"notebook" => notebook = Some(value.to_string()),
"note" => note = Some(value.to_string()),
"hash" => hash = Some(value.strip_prefix("sha256:")?.to_string()),
_ => {}
}
}
Some(Provenance {
change_id: change_id?,
notebook: notebook?,
note: note?,
hash: hash?,
})
}