use sha2::{Digest, Sha256};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Fixity {
Off,
#[default]
On,
}
impl Fixity {
pub fn covers(self, doc: &crate::document::Document) -> bool {
self == Self::On && doc.content_attr().is_some()
}
pub fn is_on(self) -> bool {
self == Self::On
}
pub fn from_config_str(value: &str) -> Option<Self> {
match value {
"off" => Some(Self::Off),
"on" | "attachments" => Some(Self::On),
_ => None,
}
}
pub fn as_config_str(self) -> &'static str {
match self {
Self::Off => "off",
Self::On => "on",
}
}
}
pub fn digest(bytes: &[u8]) -> String {
let mut s = String::with_capacity(7 + 64);
s.push_str("sha256:");
for byte in Sha256::digest(bytes) {
s.push(char::from_digit((byte >> 4) as u32, 16).unwrap());
s.push(char::from_digit((byte & 0xf) as u32, 16).unwrap());
}
s
}
pub fn verify(bytes: &[u8], recorded: &str) -> bool {
match recorded.strip_prefix("sha256:") {
Some(_) => digest(bytes) == recorded,
None if recorded.is_empty() => true,
None => true,
}
}
pub fn is_recognized(recorded: &str) -> bool {
recorded.starts_with("sha256:")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matches_the_published_sha256_vectors() {
assert_eq!(
digest(b""),
"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
assert_eq!(
digest(b"abc"),
"sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
assert_eq!(
digest(b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"),
"sha256:248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"
);
}
#[test]
fn crosses_a_block_boundary_correctly() {
let million_a = vec![b'a'; 1_000_000];
assert_eq!(
digest(&million_a),
"sha256:cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0"
);
}
#[test]
fn verify_accepts_the_matching_digest_and_rejects_a_changed_byte() {
let recorded = digest(b"the original bytes");
assert!(verify(b"the original bytes", &recorded));
assert!(!verify(b"the corrupted bytes", &recorded));
}
#[cfg(feature = "yaml")]
#[test]
fn covers_exactly_the_documents_whose_hash_would_name_another_file() {
use crate::document::Document;
let parse = |p: &str, t: &str| Document::parse(p, t).unwrap();
let separated = parse("notes/a.yaml", "title: A\ncontent: a.md\n");
let sidecar = parse("photo.jpg.yaml", "title: Photo\ncontent: photo.jpg\n");
assert!(Fixity::On.covers(&separated));
assert!(Fixity::On.covers(&sidecar));
let combined = parse("note.md", "---\ntitle: Note\n---\nhello\n");
assert!(!Fixity::On.covers(&combined));
let node = parse(
"photos.yaml",
"title: Photos\nmanifest: photos.manifest.yaml\n",
);
assert!(!Fixity::On.covers(&node));
assert!(!Fixity::Off.covers(&separated));
assert!(!Fixity::Off.covers(&sidecar));
}
#[test]
fn reads_the_retired_default_spelling_but_not_the_retired_tier() {
assert_eq!(Fixity::from_config_str("attachments"), Some(Fixity::On));
assert_eq!(Fixity::from_config_str("on"), Some(Fixity::On));
assert_eq!(Fixity::from_config_str("off"), Some(Fixity::Off));
assert_eq!(Fixity::from_config_str("all"), None);
assert_eq!(Fixity::On.as_config_str(), "on");
assert_eq!(Fixity::Off.as_config_str(), "off");
}
#[test]
fn verify_never_cries_wolf_over_an_unrecorded_or_unknown_digest() {
assert!(verify(b"anything", ""));
assert!(verify(b"anything", "blake3:deadbeef"));
assert!(!is_recognized("blake3:deadbeef"));
assert!(is_recognized("sha256:e3b0c442"));
}
}