socket_patch_core/package_json/
detect.rs1const SOCKET_PATCH_COMMAND: &str = "socket patch apply --silent --ecosystems npm";
3
4const LEGACY_PATCH_PATTERNS: &[&str] = &[
6 "socket-patch apply",
7 "npx @socketsecurity/socket-patch apply",
8 "socket patch apply",
9];
10
11#[derive(Debug, Clone)]
13pub struct PostinstallStatus {
14 pub configured: bool,
15 pub current_script: String,
16 pub needs_update: bool,
17}
18
19pub fn is_postinstall_configured(package_json: &serde_json::Value) -> PostinstallStatus {
21 let current_script = package_json
22 .get("scripts")
23 .and_then(|s| s.get("postinstall"))
24 .and_then(|v| v.as_str())
25 .unwrap_or("")
26 .to_string();
27
28 let configured = LEGACY_PATCH_PATTERNS
29 .iter()
30 .any(|pattern| current_script.contains(pattern));
31
32 PostinstallStatus {
33 configured,
34 current_script,
35 needs_update: !configured,
36 }
37}
38
39pub fn is_postinstall_configured_str(content: &str) -> PostinstallStatus {
41 match serde_json::from_str::<serde_json::Value>(content) {
42 Ok(val) => is_postinstall_configured(&val),
43 Err(_) => PostinstallStatus {
44 configured: false,
45 current_script: String::new(),
46 needs_update: true,
47 },
48 }
49}
50
51pub fn generate_updated_postinstall(current_postinstall: &str) -> String {
53 let trimmed = current_postinstall.trim();
54
55 if trimmed.is_empty() {
57 return SOCKET_PATCH_COMMAND.to_string();
58 }
59
60 let already_configured = LEGACY_PATCH_PATTERNS
62 .iter()
63 .any(|pattern| trimmed.contains(pattern));
64 if already_configured {
65 return trimmed.to_string();
66 }
67
68 format!("{SOCKET_PATCH_COMMAND} && {trimmed}")
70}
71
72pub fn update_package_json_object(
75 package_json: &mut serde_json::Value,
76) -> (bool, String) {
77 let status = is_postinstall_configured(package_json);
78
79 if !status.needs_update {
80 return (false, status.current_script);
81 }
82
83 let new_postinstall = generate_updated_postinstall(&status.current_script);
84
85 if package_json.get("scripts").is_none() {
87 package_json["scripts"] = serde_json::json!({});
88 }
89
90 package_json["scripts"]["postinstall"] =
91 serde_json::Value::String(new_postinstall.clone());
92
93 (true, new_postinstall)
94}
95
96pub fn update_package_json_content(
99 content: &str,
100) -> Result<(bool, String, String, String), String> {
101 let mut package_json: serde_json::Value =
102 serde_json::from_str(content).map_err(|e| format!("Invalid package.json: {e}"))?;
103
104 let status = is_postinstall_configured(&package_json);
105
106 if !status.needs_update {
107 return Ok((
108 false,
109 content.to_string(),
110 status.current_script.clone(),
111 status.current_script,
112 ));
113 }
114
115 let (_, new_script) = update_package_json_object(&mut package_json);
116 let new_content = serde_json::to_string_pretty(&package_json).unwrap() + "\n";
117
118 Ok((true, new_content, status.current_script, new_script))
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn test_not_configured() {
127 let pkg: serde_json::Value = serde_json::json!({
128 "name": "test",
129 "scripts": {
130 "build": "tsc"
131 }
132 });
133 let status = is_postinstall_configured(&pkg);
134 assert!(!status.configured);
135 assert!(status.needs_update);
136 }
137
138 #[test]
139 fn test_already_configured() {
140 let pkg: serde_json::Value = serde_json::json!({
141 "name": "test",
142 "scripts": {
143 "postinstall": "socket patch apply --silent --ecosystems npm"
144 }
145 });
146 let status = is_postinstall_configured(&pkg);
147 assert!(status.configured);
148 assert!(!status.needs_update);
149 }
150
151 #[test]
152 fn test_generate_empty() {
153 assert_eq!(
154 generate_updated_postinstall(""),
155 "socket patch apply --silent --ecosystems npm"
156 );
157 }
158
159 #[test]
160 fn test_generate_prepend() {
161 assert_eq!(
162 generate_updated_postinstall("echo done"),
163 "socket patch apply --silent --ecosystems npm && echo done"
164 );
165 }
166
167 #[test]
168 fn test_generate_already_configured() {
169 let current = "socket-patch apply && echo done";
170 assert_eq!(generate_updated_postinstall(current), current);
171 }
172
173 #[test]
176 fn test_is_postinstall_configured_str_invalid_json() {
177 let status = is_postinstall_configured_str("not json");
178 assert!(!status.configured);
179 assert!(status.needs_update);
180 }
181
182 #[test]
183 fn test_is_postinstall_configured_str_legacy_npx_pattern() {
184 let content = r#"{"scripts":{"postinstall":"npx @socketsecurity/socket-patch apply --silent"}}"#;
185 let status = is_postinstall_configured_str(content);
186 assert!(status.configured);
188 assert!(!status.needs_update);
189 }
190
191 #[test]
192 fn test_is_postinstall_configured_str_socket_dash_patch() {
193 let content =
194 r#"{"scripts":{"postinstall":"socket-patch apply --silent --ecosystems npm"}}"#;
195 let status = is_postinstall_configured_str(content);
196 assert!(status.configured);
197 assert!(!status.needs_update);
198 }
199
200 #[test]
201 fn test_is_postinstall_configured_no_scripts() {
202 let pkg: serde_json::Value = serde_json::json!({"name": "test"});
203 let status = is_postinstall_configured(&pkg);
204 assert!(!status.configured);
205 assert!(status.current_script.is_empty());
206 }
207
208 #[test]
209 fn test_is_postinstall_configured_no_postinstall() {
210 let pkg: serde_json::Value = serde_json::json!({
211 "scripts": {"build": "tsc"}
212 });
213 let status = is_postinstall_configured(&pkg);
214 assert!(!status.configured);
215 assert!(status.current_script.is_empty());
216 }
217
218 #[test]
219 fn test_update_object_creates_scripts() {
220 let mut pkg: serde_json::Value = serde_json::json!({"name": "test"});
221 let (modified, new_script) = update_package_json_object(&mut pkg);
222 assert!(modified);
223 assert!(new_script.contains("socket patch apply"));
224 assert!(pkg.get("scripts").is_some());
225 assert!(pkg["scripts"]["postinstall"].is_string());
226 }
227
228 #[test]
229 fn test_update_object_noop_when_configured() {
230 let mut pkg: serde_json::Value = serde_json::json!({
231 "scripts": {
232 "postinstall": "socket patch apply --silent --ecosystems npm"
233 }
234 });
235 let (modified, existing) = update_package_json_object(&mut pkg);
236 assert!(!modified);
237 assert!(existing.contains("socket patch apply"));
238 }
239
240 #[test]
241 fn test_update_content_roundtrip_no_scripts() {
242 let content = r#"{"name": "test"}"#;
243 let (modified, new_content, old_script, new_script) =
244 update_package_json_content(content).unwrap();
245 assert!(modified);
246 assert!(old_script.is_empty());
247 assert!(new_script.contains("socket patch apply"));
248 let parsed: serde_json::Value = serde_json::from_str(&new_content).unwrap();
250 assert!(parsed["scripts"]["postinstall"].is_string());
251 }
252
253 #[test]
254 fn test_update_content_already_configured() {
255 let content = r#"{"scripts":{"postinstall":"socket patch apply --silent --ecosystems npm"}}"#;
256 let (modified, _new_content, _old, _new) =
257 update_package_json_content(content).unwrap();
258 assert!(!modified);
259 }
260
261 #[test]
262 fn test_update_content_invalid_json() {
263 let result = update_package_json_content("not json");
264 assert!(result.is_err());
265 assert!(result.unwrap_err().contains("Invalid package.json"));
266 }
267
268 #[test]
269 fn test_generate_whitespace_only() {
270 let result = generate_updated_postinstall(" \t ");
272 assert_eq!(result, "socket patch apply --silent --ecosystems npm");
273 }
274}