Skip to main content

socket_patch_core/manifest/
schema.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// Information about a vulnerability fixed by a patch.
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
6pub struct VulnerabilityInfo {
7    pub cves: Vec<String>,
8    pub summary: String,
9    pub severity: String,
10    pub description: String,
11}
12
13/// Hash information for a single patched file.
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15#[serde(rename_all = "camelCase")]
16pub struct PatchFileInfo {
17    pub before_hash: String,
18    pub after_hash: String,
19}
20
21/// A single patch record in the manifest.
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
23#[serde(rename_all = "camelCase")]
24pub struct PatchRecord {
25    pub uuid: String,
26    pub exported_at: String,
27    /// Maps relative file path -> hash info.
28    pub files: HashMap<String, PatchFileInfo>,
29    /// Maps vulnerability ID (e.g., "GHSA-...") -> vulnerability info.
30    pub vulnerabilities: HashMap<String, VulnerabilityInfo>,
31    pub description: String,
32    pub license: String,
33    pub tier: String,
34}
35
36/// The top-level patch manifest structure.
37/// Stored as `.socket/manifest.json`.
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39pub struct PatchManifest {
40    /// Maps package PURL (e.g., "pkg:npm/lodash@4.17.21") -> patch record.
41    pub patches: HashMap<String, PatchRecord>,
42}
43
44impl PatchManifest {
45    /// Create an empty manifest.
46    pub fn new() -> Self {
47        Self {
48            patches: HashMap::new(),
49        }
50    }
51}
52
53impl Default for PatchManifest {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn test_empty_manifest_roundtrip() {
65        let manifest = PatchManifest::new();
66        let json = serde_json::to_string_pretty(&manifest).unwrap();
67        let parsed: PatchManifest = serde_json::from_str(&json).unwrap();
68        assert_eq!(parsed.patches.len(), 0);
69    }
70
71    #[test]
72    fn test_manifest_with_patch_roundtrip() {
73        let json = r#"{
74  "patches": {
75    "pkg:npm/simplehttpserver@0.0.6": {
76      "uuid": "12345678-1234-1234-1234-123456789abc",
77      "exportedAt": "2024-01-15T10:00:00Z",
78      "files": {
79        "package/lib/server.js": {
80          "beforeHash": "aaaa000000000000000000000000000000000000000000000000000000000000",
81          "afterHash": "bbbb000000000000000000000000000000000000000000000000000000000000"
82        }
83      },
84      "vulnerabilities": {
85        "GHSA-jrhj-2j3q-xf3v": {
86          "cves": ["CVE-2024-1234"],
87          "summary": "Path traversal vulnerability",
88          "severity": "high",
89          "description": "A path traversal vulnerability exists in simplehttpserver"
90        }
91      },
92      "description": "Fix path traversal vulnerability",
93      "license": "MIT",
94      "tier": "free"
95    }
96  }
97}"#;
98
99        let manifest: PatchManifest = serde_json::from_str(json).unwrap();
100        assert_eq!(manifest.patches.len(), 1);
101
102        let patch = manifest.patches.get("pkg:npm/simplehttpserver@0.0.6").unwrap();
103        assert_eq!(patch.uuid, "12345678-1234-1234-1234-123456789abc");
104        assert_eq!(patch.files.len(), 1);
105        assert_eq!(patch.vulnerabilities.len(), 1);
106        assert_eq!(patch.tier, "free");
107
108        let file_info = patch.files.get("package/lib/server.js").unwrap();
109        assert_eq!(
110            file_info.before_hash,
111            "aaaa000000000000000000000000000000000000000000000000000000000000"
112        );
113
114        let vuln = patch.vulnerabilities.get("GHSA-jrhj-2j3q-xf3v").unwrap();
115        assert_eq!(vuln.cves, vec!["CVE-2024-1234"]);
116        assert_eq!(vuln.severity, "high");
117
118        // Verify round-trip
119        let serialized = serde_json::to_string_pretty(&manifest).unwrap();
120        let reparsed: PatchManifest = serde_json::from_str(&serialized).unwrap();
121        assert_eq!(manifest, reparsed);
122    }
123
124    #[test]
125    fn test_camel_case_serialization() {
126        let file_info = PatchFileInfo {
127            before_hash: "aaa".to_string(),
128            after_hash: "bbb".to_string(),
129        };
130        let json = serde_json::to_string(&file_info).unwrap();
131        assert!(json.contains("beforeHash"));
132        assert!(json.contains("afterHash"));
133        assert!(!json.contains("before_hash"));
134        assert!(!json.contains("after_hash"));
135    }
136
137    #[test]
138    fn test_patch_record_camel_case() {
139        let record = PatchRecord {
140            uuid: "test-uuid".to_string(),
141            exported_at: "2024-01-01T00:00:00Z".to_string(),
142            files: HashMap::new(),
143            vulnerabilities: HashMap::new(),
144            description: "test".to_string(),
145            license: "MIT".to_string(),
146            tier: "free".to_string(),
147        };
148        let json = serde_json::to_string(&record).unwrap();
149        assert!(json.contains("exportedAt"));
150        assert!(!json.contains("exported_at"));
151    }
152}