socket_patch_core/manifest/
operations.rs1use std::collections::HashSet;
2use std::path::{Path, PathBuf};
3
4use crate::manifest::schema::PatchManifest;
5
6pub fn resolve_manifest_path(cwd: &Path, manifest_path: &str) -> PathBuf {
10 if Path::new(manifest_path).is_absolute() {
11 PathBuf::from(manifest_path)
12 } else {
13 cwd.join(manifest_path)
14 }
15}
16
17pub fn get_after_hash_blobs(manifest: &PatchManifest) -> HashSet<String> {
21 let mut blobs = HashSet::new();
22
23 for record in manifest.patches.values() {
24 for file_info in record.files.values() {
25 blobs.insert(file_info.after_hash.clone());
26 }
27 }
28
29 blobs
30}
31
32pub fn get_before_hash_blobs(manifest: &PatchManifest) -> HashSet<String> {
35 let mut blobs = HashSet::new();
36
37 for record in manifest.patches.values() {
38 for file_info in record.files.values() {
39 blobs.insert(file_info.before_hash.clone());
40 }
41 }
42
43 blobs
44}
45
46pub fn validate_manifest(value: &serde_json::Value) -> Result<PatchManifest, String> {
49 serde_json::from_value::<PatchManifest>(value.clone())
50 .map_err(|e| format!("Invalid manifest: {}", e))
51}
52
53pub async fn read_manifest(path: impl AsRef<Path>) -> Result<Option<PatchManifest>, std::io::Error> {
57 let path = path.as_ref();
58
59 let content = match tokio::fs::read_to_string(path).await {
60 Ok(c) => c,
61 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
62 Err(e) => return Err(e), };
64
65 let parsed: serde_json::Value = match serde_json::from_str(&content) {
66 Ok(v) => v,
67 Err(e) => return Err(std::io::Error::new(
68 std::io::ErrorKind::InvalidData,
69 format!("Failed to parse manifest JSON: {}", e),
70 )),
71 };
72
73 match validate_manifest(&parsed) {
74 Ok(manifest) => Ok(Some(manifest)),
75 Err(e) => Err(std::io::Error::new(
76 std::io::ErrorKind::InvalidData,
77 e,
78 )),
79 }
80}
81
82pub async fn write_manifest(
84 path: impl AsRef<Path>,
85 manifest: &PatchManifest,
86) -> Result<(), std::io::Error> {
87 let content = serde_json::to_string_pretty(manifest)
88 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
89 tokio::fs::write(path, content).await
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::manifest::schema::{PatchFileInfo, PatchRecord};
96 use std::collections::HashMap;
97
98 const TEST_UUID_1: &str = "11111111-1111-4111-8111-111111111111";
99 const TEST_UUID_2: &str = "22222222-2222-4222-8222-222222222222";
100
101 const BEFORE_HASH_1: &str =
102 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1111";
103 const AFTER_HASH_1: &str =
104 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb1111";
105 const BEFORE_HASH_2: &str =
106 "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc2222";
107 const AFTER_HASH_2: &str =
108 "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd2222";
109 const BEFORE_HASH_3: &str =
110 "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3333";
111 const AFTER_HASH_3: &str =
112 "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3333";
113
114 fn create_test_manifest() -> PatchManifest {
115 let mut patches = HashMap::new();
116
117 let mut files_a = HashMap::new();
118 files_a.insert(
119 "package/index.js".to_string(),
120 PatchFileInfo {
121 before_hash: BEFORE_HASH_1.to_string(),
122 after_hash: AFTER_HASH_1.to_string(),
123 },
124 );
125 files_a.insert(
126 "package/lib/utils.js".to_string(),
127 PatchFileInfo {
128 before_hash: BEFORE_HASH_2.to_string(),
129 after_hash: AFTER_HASH_2.to_string(),
130 },
131 );
132
133 patches.insert(
134 "pkg:npm/pkg-a@1.0.0".to_string(),
135 PatchRecord {
136 uuid: TEST_UUID_1.to_string(),
137 exported_at: "2024-01-01T00:00:00Z".to_string(),
138 files: files_a,
139 vulnerabilities: HashMap::new(),
140 description: "Test patch 1".to_string(),
141 license: "MIT".to_string(),
142 tier: "free".to_string(),
143 },
144 );
145
146 let mut files_b = HashMap::new();
147 files_b.insert(
148 "package/main.js".to_string(),
149 PatchFileInfo {
150 before_hash: BEFORE_HASH_3.to_string(),
151 after_hash: AFTER_HASH_3.to_string(),
152 },
153 );
154
155 patches.insert(
156 "pkg:npm/pkg-b@2.0.0".to_string(),
157 PatchRecord {
158 uuid: TEST_UUID_2.to_string(),
159 exported_at: "2024-01-01T00:00:00Z".to_string(),
160 files: files_b,
161 vulnerabilities: HashMap::new(),
162 description: "Test patch 2".to_string(),
163 license: "MIT".to_string(),
164 tier: "free".to_string(),
165 },
166 );
167
168 PatchManifest { patches }
169 }
170
171 #[test]
172 fn test_get_after_hash_blobs() {
173 let manifest = create_test_manifest();
174 let blobs = get_after_hash_blobs(&manifest);
175
176 assert_eq!(blobs.len(), 3);
177 assert!(blobs.contains(AFTER_HASH_1));
178 assert!(blobs.contains(AFTER_HASH_2));
179 assert!(blobs.contains(AFTER_HASH_3));
180 assert!(!blobs.contains(BEFORE_HASH_1));
181 assert!(!blobs.contains(BEFORE_HASH_2));
182 assert!(!blobs.contains(BEFORE_HASH_3));
183 }
184
185 #[test]
186 fn test_get_after_hash_blobs_empty() {
187 let manifest = PatchManifest::new();
188 let blobs = get_after_hash_blobs(&manifest);
189 assert_eq!(blobs.len(), 0);
190 }
191
192 #[test]
193 fn test_get_before_hash_blobs() {
194 let manifest = create_test_manifest();
195 let blobs = get_before_hash_blobs(&manifest);
196
197 assert_eq!(blobs.len(), 3);
198 assert!(blobs.contains(BEFORE_HASH_1));
199 assert!(blobs.contains(BEFORE_HASH_2));
200 assert!(blobs.contains(BEFORE_HASH_3));
201 assert!(!blobs.contains(AFTER_HASH_1));
202 assert!(!blobs.contains(AFTER_HASH_2));
203 assert!(!blobs.contains(AFTER_HASH_3));
204 }
205
206 #[test]
207 fn test_get_before_hash_blobs_empty() {
208 let manifest = PatchManifest::new();
209 let blobs = get_before_hash_blobs(&manifest);
210 assert_eq!(blobs.len(), 0);
211 }
212
213
214 #[test]
215 fn test_validate_manifest_valid() {
216 let json = serde_json::json!({
217 "patches": {
218 "pkg:npm/test@1.0.0": {
219 "uuid": "11111111-1111-4111-8111-111111111111",
220 "exportedAt": "2024-01-01T00:00:00Z",
221 "files": {},
222 "vulnerabilities": {},
223 "description": "test",
224 "license": "MIT",
225 "tier": "free"
226 }
227 }
228 });
229
230 let result = validate_manifest(&json);
231 assert!(result.is_ok());
232 let manifest = result.unwrap();
233 assert_eq!(manifest.patches.len(), 1);
234 }
235
236 #[test]
237 fn test_validate_manifest_invalid() {
238 let json = serde_json::json!({
239 "patches": "not-an-object"
240 });
241
242 let result = validate_manifest(&json);
243 assert!(result.is_err());
244 }
245
246 #[test]
247 fn test_validate_manifest_missing_fields() {
248 let json = serde_json::json!({
249 "patches": {
250 "pkg:npm/test@1.0.0": {
251 "uuid": "test"
252 }
253 }
254 });
255
256 let result = validate_manifest(&json);
257 assert!(result.is_err());
258 }
259
260 #[tokio::test]
261 async fn test_read_manifest_not_found() {
262 let result = read_manifest("/nonexistent/path/manifest.json").await;
263 assert!(result.is_ok());
264 assert!(result.unwrap().is_none());
265 }
266
267 #[tokio::test]
268 async fn test_write_and_read_manifest() {
269 let dir = tempfile::tempdir().unwrap();
270 let path = dir.path().join("manifest.json");
271
272 let manifest = create_test_manifest();
273 write_manifest(&path, &manifest).await.unwrap();
274
275 let read_back = read_manifest(&path).await.unwrap();
276 assert!(read_back.is_some());
277 let read_back = read_back.unwrap();
278 assert_eq!(read_back.patches.len(), 2);
279 }
280
281 #[test]
282 fn test_resolve_manifest_path_relative_joins_cwd() {
283 let cwd = Path::new("/tmp/proj");
284 let resolved = resolve_manifest_path(cwd, ".socket/manifest.json");
285 assert_eq!(resolved, PathBuf::from("/tmp/proj/.socket/manifest.json"));
286 }
287
288 #[test]
289 fn test_resolve_manifest_path_absolute_unchanged() {
290 let cwd = Path::new("/tmp/proj");
291 let absolute = if cfg!(windows) {
292 r"C:\custom\manifest.json"
293 } else {
294 "/etc/custom/manifest.json"
295 };
296 let resolved = resolve_manifest_path(cwd, absolute);
297 assert_eq!(resolved, PathBuf::from(absolute));
298 }
299
300 #[test]
301 fn test_resolve_manifest_path_relative_dotted() {
302 let cwd = Path::new("/tmp/proj");
303 let resolved = resolve_manifest_path(cwd, "../manifest.json");
304 assert_eq!(resolved, PathBuf::from("/tmp/proj/../manifest.json"));
305 }
306}