Skip to main content

miwa/
hash.rs

1//! Text normalization and content hashing.
2
3/// Normalize line endings (CRLF / CR) to LF. Nothing else is changed.
4pub fn normalize(text: &str) -> String {
5    text.replace("\r\n", "\n").replace('\r', "\n")
6}
7
8/// Normalize `text` and return its SHA-256 hash as `"sha256:<hex>"` (lowercase hex).
9pub fn content_hash(text: &str) -> String {
10    use sha2::Digest;
11    let normalized = normalize(text);
12    let mut hasher = sha2::Sha256::new();
13    hasher.update(normalized.as_bytes());
14    let digest = hasher.finalize();
15    format!("sha256:{}", hex::encode(digest))
16}
17
18#[cfg(test)]
19mod tests {
20    use super::*;
21
22    // [FR-001]
23    #[test]
24    fn crlf_と_lf_は同じハッシュになる() {
25        let lf = "line one\nline two\n";
26        let crlf = "line one\r\nline two\r\n";
27        assert_eq!(content_hash(lf), content_hash(crlf));
28    }
29
30    // [FR-001]
31    #[test]
32    fn content_hash_はsha256プレフィックス付きの小文字hexを返す() {
33        let hash = content_hash("hello");
34        assert!(hash.starts_with("sha256:"), "unexpected hash: {hash}");
35        let hex = hash.strip_prefix("sha256:").unwrap();
36        assert_eq!(hex.len(), 64);
37        assert!(hex
38            .chars()
39            .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
40        // 空白除去や大小文字変換のような正規化は行われないため、内容が違えば別ハッシュになる
41        assert_ne!(content_hash("hello"), content_hash("hello "));
42    }
43
44    // [FR-001]
45    #[test]
46    fn 単独crもlfと同じハッシュになる() {
47        let lf = "a\nb";
48        let cr = "a\rb";
49        assert_eq!(content_hash(lf), content_hash(cr));
50    }
51}