Skip to main content

pray_core/
hashing.rs

1use sha2::{Digest, Sha256};
2
3pub fn sha256_hex(bytes: &[u8]) -> String {
4    let digest = Sha256::digest(bytes);
5    let mut output = String::with_capacity(digest.len() * 2);
6    for byte in digest {
7        use std::fmt::Write as _;
8        let _ = write!(output, "{byte:02x}");
9    }
10    output
11}
12
13pub fn sha256_prefixed(bytes: &[u8]) -> String {
14    prefixed_hex_digest(Sha256::digest(bytes))
15}
16
17pub(crate) fn prefixed_hex_digest(digest: impl AsRef<[u8]>) -> String {
18    let digest = digest.as_ref();
19    let mut output = String::with_capacity(7 + digest.len() * 2);
20    output.push_str("sha256:");
21    for byte in digest {
22        use std::fmt::Write as _;
23        let _ = write!(output, "{byte:02x}");
24    }
25    output
26}
27
28pub fn marker_id(seed: &str) -> String {
29    sha256_hex(seed.as_bytes())[0..8].to_string()
30}
31
32pub fn normalize_line_endings(text: &str) -> String {
33    text.replace("\r\n", "\n").replace('\r', "\n")
34}
35
36pub fn checksum_managed_span_content(body: &str) -> String {
37    sha256_prefixed(
38        normalize_line_endings(body)
39            .trim_end_matches('\n')
40            .as_bytes(),
41    )
42}
43
44pub fn checksum_managed_body_line_refs(body_lines: &[&str]) -> String {
45    let body_lines = trim_trailing_empty_lines(body_lines);
46    let mut hasher = Sha256::new();
47    for (index, line) in body_lines.iter().enumerate() {
48        if index > 0 {
49            hasher.update(b"\n");
50        }
51        update_line_endings_normalized(&mut hasher, line);
52    }
53    prefixed_hex_digest(hasher.finalize())
54}
55
56fn trim_trailing_empty_lines<'a>(mut lines: &'a [&'a str]) -> &'a [&'a str] {
57    while lines.last().is_some_and(|line| line.is_empty()) {
58        lines = &lines[..lines.len() - 1];
59    }
60    lines
61}
62
63fn update_line_endings_normalized(hasher: &mut Sha256, line: &str) {
64    if !line.contains('\r') {
65        hasher.update(line.as_bytes());
66        return;
67    }
68    hasher.update(normalize_line_endings(line).as_bytes());
69}
70
71#[cfg(test)]
72mod tests {
73    use super::{checksum_managed_body_line_refs, checksum_managed_span_content};
74
75    #[test]
76    fn checksum_trims_trailing_newlines() {
77        let checksum = checksum_managed_span_content("alpha\nbeta\n\n");
78        assert_eq!(checksum, checksum_managed_span_content("alpha\nbeta"));
79    }
80
81    #[test]
82    fn checksum_body_line_refs_matches_joined_content() {
83        let joined = checksum_managed_span_content("alpha\nbeta\n\n");
84        let refs = checksum_managed_body_line_refs(&["alpha", "beta", "", ""]);
85        assert_eq!(joined, refs);
86
87        let blank_line = checksum_managed_span_content("alpha\n\nbeta");
88        assert_eq!(
89            blank_line,
90            checksum_managed_body_line_refs(&["alpha", "", "beta"])
91        );
92    }
93}