clnrm_core/reporting/
digest.rs1use crate::error::{CleanroomError, Result};
6use sha2::{Digest, Sha256};
7use std::path::Path;
8
9pub struct DigestReporter;
11
12impl DigestReporter {
13 pub fn write(path: &Path, spans_json: &str) -> Result<()> {
25 let digest = Self::compute_digest(spans_json);
26 Self::write_file(path, &digest)
27 }
28
29 pub fn compute_digest(spans_json: &str) -> String {
37 let mut hasher = Sha256::new();
38 hasher.update(spans_json.as_bytes());
39 format!("{:x}", hasher.finalize())
40 }
41
42 fn write_file(path: &Path, digest: &str) -> Result<()> {
44 std::fs::write(path, format!("{}\n", digest))
45 .map_err(|e| CleanroomError::report_error(format!("Failed to write digest: {}", e)))
46 }
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52 use tempfile::TempDir;
53
54 #[test]
55 fn test_digest_reporter_basic() -> Result<()> {
56 let temp_dir = TempDir::new()
58 .map_err(|e| CleanroomError::io_error(format!("Failed to create temp dir: {}", e)))?;
59 let digest_path = temp_dir.path().join("digest.txt");
60 let spans_json = r#"{"spans": []}"#;
61
62 DigestReporter::write(&digest_path, spans_json)?;
64
65 let content = std::fs::read_to_string(&digest_path)
67 .map_err(|e| CleanroomError::io_error(format!("Failed to read file: {}", e)))?;
68
69 assert_eq!(content.trim().len(), 64);
71 assert!(content.chars().all(|c| c.is_ascii_hexdigit() || c == '\n'));
72
73 Ok(())
74 }
75
76 #[test]
77 fn test_digest_reporter_deterministic() -> Result<()> {
78 let temp_dir = TempDir::new()
80 .map_err(|e| CleanroomError::io_error(format!("Failed to create temp dir: {}", e)))?;
81 let digest_path1 = temp_dir.path().join("digest1.txt");
82 let digest_path2 = temp_dir.path().join("digest2.txt");
83 let spans_json = r#"{"spans": [{"name": "test"}]}"#;
84
85 DigestReporter::write(&digest_path1, spans_json)?;
87 DigestReporter::write(&digest_path2, spans_json)?;
88
89 let content1 = std::fs::read_to_string(&digest_path1)
91 .map_err(|e| CleanroomError::io_error(format!("Failed to read file: {}", e)))?;
92 let content2 = std::fs::read_to_string(&digest_path2)
93 .map_err(|e| CleanroomError::io_error(format!("Failed to read file: {}", e)))?;
94
95 assert_eq!(content1, content2);
96
97 Ok(())
98 }
99
100 #[test]
101 fn test_digest_reporter_different_inputs() -> Result<()> {
102 let temp_dir = TempDir::new()
104 .map_err(|e| CleanroomError::io_error(format!("Failed to create temp dir: {}", e)))?;
105 let digest_path1 = temp_dir.path().join("digest1.txt");
106 let digest_path2 = temp_dir.path().join("digest2.txt");
107 let spans_json1 = r#"{"spans": []}"#;
108 let spans_json2 = r#"{"spans": [{"name": "test"}]}"#;
109
110 DigestReporter::write(&digest_path1, spans_json1)?;
112 DigestReporter::write(&digest_path2, spans_json2)?;
113
114 let content1 = std::fs::read_to_string(&digest_path1)
116 .map_err(|e| CleanroomError::io_error(format!("Failed to read file: {}", e)))?;
117 let content2 = std::fs::read_to_string(&digest_path2)
118 .map_err(|e| CleanroomError::io_error(format!("Failed to read file: {}", e)))?;
119
120 assert_ne!(content1, content2);
121
122 Ok(())
123 }
124
125 #[test]
126 fn test_compute_digest_known_value() {
127 let input = "test";
129 let expected = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08";
131
132 let digest = DigestReporter::compute_digest(input);
134
135 assert_eq!(digest, expected);
137 }
138
139 #[test]
140 fn test_compute_digest_empty_string() {
141 let input = "";
143 let expected = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
145
146 let digest = DigestReporter::compute_digest(input);
148
149 assert_eq!(digest, expected);
151 }
152
153 #[test]
154 fn test_compute_digest_complex_json() {
155 let input = r#"{"spans":[{"name":"root","span_id":"1"},{"name":"child","span_id":"2"}]}"#;
157
158 let digest = DigestReporter::compute_digest(input);
160
161 assert_eq!(digest.len(), 64);
163 assert!(digest.chars().all(|c| c.is_ascii_hexdigit()));
164 }
165
166 #[test]
167 fn test_digest_sensitivity_to_whitespace() {
168 let input1 = r#"{"spans": []}"#;
170 let input2 = r#"{"spans":[]}"#; let digest1 = DigestReporter::compute_digest(input1);
174 let digest2 = DigestReporter::compute_digest(input2);
175
176 assert_ne!(digest1, digest2, "Digest should be sensitive to whitespace");
178 }
179
180 #[test]
181 fn test_digest_file_format() -> Result<()> {
182 let temp_dir = TempDir::new()
184 .map_err(|e| CleanroomError::io_error(format!("Failed to create temp dir: {}", e)))?;
185 let digest_path = temp_dir.path().join("digest.txt");
186 let spans_json = "test";
187
188 DigestReporter::write(&digest_path, spans_json)?;
190
191 let content = std::fs::read_to_string(&digest_path)
193 .map_err(|e| CleanroomError::io_error(format!("Failed to read file: {}", e)))?;
194
195 assert!(content.ends_with('\n'));
197 assert_eq!(content.lines().count(), 1);
198
199 Ok(())
200 }
201}