use blake3::Hasher;
use unicode_normalization::UnicodeNormalization;
use crate::memo::MemoHash;
pub fn normalize(input: &[u8]) -> String {
let text = String::from_utf8_lossy(input);
let text = text.replace("\r\n", "\n").replace('\r', "\n");
let mut out = String::with_capacity(text.len());
for line in text.split('\n') {
let trimmed = line.trim_end();
let nfc: String = trimmed.nfc().collect();
out.push_str(&nfc);
out.push('\n');
}
while out.ends_with("\n\n") {
out.pop();
}
if !out.ends_with('\n') {
out.push('\n');
}
if out == "\n" && text.is_empty() {
}
out
}
pub fn hash_content(input: &[u8]) -> MemoHash {
let normalized = normalize(input);
let mut hasher = Hasher::new();
hasher.update(normalized.as_bytes());
MemoHash::new(hasher.finalize().to_hex().to_string())
}
pub fn hash_normalized(normalized: &str) -> MemoHash {
let mut hasher = Hasher::new();
hasher.update(normalized.as_bytes());
MemoHash::new(hasher.finalize().to_hex().to_string())
}
pub fn hash_memo(body: &[u8], favorite: bool, category: &str) -> MemoHash {
let normalized_body = normalize(body);
let mut hasher = Hasher::new();
hasher.update(normalized_body.as_bytes());
hasher.update(b"\x1f"); hasher.update(if favorite { b"1" } else { b"0" });
hasher.update(b"\x1f");
hasher.update(category.as_bytes());
MemoHash::new(hasher.finalize().to_hex().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crlf_and_lf_hash_equal() {
let a = hash_content(b"hello\nworld");
let b = hash_content(b"hello\r\nworld");
assert_eq!(a, b);
}
#[test]
fn trailing_whitespace_ignored() {
let a = hash_content(b"line one \nline two");
let b = hash_content(b"line one\nline two");
assert_eq!(a, b);
}
#[test]
fn trailing_newline_normalized() {
let a = hash_content(b"text\n\n\n");
let b = hash_content(b"text");
assert_eq!(a, b);
}
#[test]
fn nfc_normalization() {
let a = hash_content("A\u{301}".as_bytes());
let b = hash_content("\u{C1}".as_bytes());
assert_eq!(a, b);
}
#[test]
fn hash_is_prefixed() {
let h = hash_content(b"x");
assert!(h.as_str().starts_with("b3:"));
}
#[test]
fn metadata_only_edit_changes_hash() {
let base = hash_memo(b"body", false, "");
let favorite = hash_memo(b"body", true, "");
let colored = hash_memo(b"body", false, "todo");
assert_ne!(base, favorite);
assert_ne!(base, colored);
}
#[test]
fn tag_in_body_changes_hash() {
let a = hash_memo(b"note", false, "");
let b = hash_memo(b"note #x", false, "");
assert_ne!(a, b);
}
#[test]
fn identical_state_hashes_equal() {
let a = hash_memo(b"body", true, "todo");
let b = hash_memo(b"body", true, "todo");
assert_eq!(a, b);
}
}