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}