polykit_core/remote_cache/
integrity.rs1use std::io::Read;
4use std::path::Path;
5
6use sha2::{Digest, Sha256};
7
8use crate::error::{Error, Result};
9
10use super::artifact::Artifact;
11
12pub struct ArtifactVerifier;
19
20impl ArtifactVerifier {
21 pub fn verify(artifact: &Artifact, expected_hash: Option<&str>) -> Result<()> {
32 if let Some(expected) = expected_hash {
34 let actual_hash = artifact.hash();
35 if actual_hash != expected {
36 return Err(Error::Adapter {
37 package: "artifact-verification".to_string(),
38 message: format!(
39 "Artifact hash mismatch: expected {}, got {}",
40 expected, actual_hash
41 ),
42 });
43 }
44 }
45
46 Self::verify_manifest(artifact)?;
48
49 Ok(())
50 }
51
52 fn verify_manifest(artifact: &Artifact) -> Result<()> {
58 use tar::Archive;
59
60 let tar_data = zstd::decode_all(artifact.compressed_data()).map_err(|e| Error::Adapter {
62 package: "artifact-verification".to_string(),
63 message: format!("Failed to decompress artifact: {}", e),
64 })?;
65
66 let mut archive = Archive::new(&tar_data[..]);
68 let outputs_dir = Path::new("outputs");
69 let manifest = artifact.manifest();
70
71 for entry_result in archive.entries().map_err(|e| Error::Adapter {
72 package: "artifact-verification".to_string(),
73 message: format!("Failed to read tar archive: {}", e),
74 })? {
75 let mut entry = entry_result.map_err(|e| Error::Adapter {
76 package: "artifact-verification".to_string(),
77 message: format!("Failed to read tar entry: {}", e),
78 })?;
79
80 let path = entry.path().map_err(|e| Error::Adapter {
81 package: "artifact-verification".to_string(),
82 message: format!("Failed to get entry path: {}", e),
83 })?;
84
85 if path == Path::new("metadata.json") || path == Path::new("manifest.json") {
87 continue;
88 }
89
90 if let Ok(relative_path) = path.strip_prefix(outputs_dir) {
92 let expected_hash = manifest.files.get(relative_path).ok_or_else(|| {
93 Error::Adapter {
94 package: "artifact-verification".to_string(),
95 message: format!(
96 "File {} in artifact but not in manifest",
97 relative_path.display()
98 ),
99 }
100 })?;
101
102 let relative_path = relative_path.to_path_buf();
104 let mut content = Vec::new();
105 entry.read_to_end(&mut content).map_err(|e| Error::Adapter {
106 package: "artifact-verification".to_string(),
107 message: format!("Failed to read file content: {}", e),
108 })?;
109
110 let mut hasher = Sha256::new();
112 hasher.update(&content);
113 let actual_hash = format!("{:x}", hasher.finalize());
114
115 if actual_hash != *expected_hash {
116 return Err(Error::Adapter {
117 package: "artifact-verification".to_string(),
118 message: format!(
119 "File {} hash mismatch: expected {}, got {}",
120 relative_path.display(),
121 expected_hash,
122 actual_hash
123 ),
124 });
125 }
126 }
127 }
128
129 Ok(())
132 }
133
134 pub fn verify_size(artifact: &Artifact, max_size: u64) -> Result<()> {
138 let manifest = artifact.manifest();
139 if manifest.total_size > max_size {
140 return Err(Error::Adapter {
141 package: "artifact-verification".to_string(),
142 message: format!(
143 "Artifact size {} exceeds maximum {}",
144 manifest.total_size, max_size
145 ),
146 });
147 }
148 Ok(())
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::remote_cache::artifact::Artifact;
156 use std::collections::BTreeMap;
157 use std::path::PathBuf;
158
159 #[test]
160 fn test_verify_valid_artifact() {
161 let mut output_files = BTreeMap::new();
162 output_files.insert(PathBuf::from("file.txt"), b"content".to_vec());
163
164 let artifact = Artifact::new(
165 "test".to_string(),
166 "build".to_string(),
167 "echo".to_string(),
168 "hash123".to_string(),
169 output_files,
170 )
171 .unwrap();
172
173 assert!(ArtifactVerifier::verify(&artifact, None).is_ok());
175 }
176
177 #[test]
178 fn test_verify_hash_mismatch() {
179 let mut output_files = BTreeMap::new();
180 output_files.insert(PathBuf::from("file.txt"), b"content".to_vec());
181
182 let artifact = Artifact::new(
183 "test".to_string(),
184 "build".to_string(),
185 "echo".to_string(),
186 "hash123".to_string(),
187 output_files,
188 )
189 .unwrap();
190
191 assert!(ArtifactVerifier::verify(&artifact, Some("wrong_hash")).is_err());
193 }
194}