const SOCKET_PATCH_COMMAND: &str = "socket patch apply --silent --ecosystems npm";
const LEGACY_PATCH_PATTERNS: &[&str] = &[
"socket-patch apply",
"npx @socketsecurity/socket-patch apply",
"socket patch apply",
];
#[derive(Debug, Clone)]
pub struct PostinstallStatus {
pub configured: bool,
pub current_script: String,
pub needs_update: bool,
}
pub fn is_postinstall_configured(package_json: &serde_json::Value) -> PostinstallStatus {
let current_script = package_json
.get("scripts")
.and_then(|s| s.get("postinstall"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let configured = LEGACY_PATCH_PATTERNS
.iter()
.any(|pattern| current_script.contains(pattern));
PostinstallStatus {
configured,
current_script,
needs_update: !configured,
}
}
pub fn is_postinstall_configured_str(content: &str) -> PostinstallStatus {
match serde_json::from_str::<serde_json::Value>(content) {
Ok(val) => is_postinstall_configured(&val),
Err(_) => PostinstallStatus {
configured: false,
current_script: String::new(),
needs_update: true,
},
}
}
pub fn generate_updated_postinstall(current_postinstall: &str) -> String {
let trimmed = current_postinstall.trim();
if trimmed.is_empty() {
return SOCKET_PATCH_COMMAND.to_string();
}
let already_configured = LEGACY_PATCH_PATTERNS
.iter()
.any(|pattern| trimmed.contains(pattern));
if already_configured {
return trimmed.to_string();
}
format!("{SOCKET_PATCH_COMMAND} && {trimmed}")
}
pub fn update_package_json_object(
package_json: &mut serde_json::Value,
) -> (bool, String) {
let status = is_postinstall_configured(package_json);
if !status.needs_update {
return (false, status.current_script);
}
let new_postinstall = generate_updated_postinstall(&status.current_script);
if package_json.get("scripts").is_none() {
package_json["scripts"] = serde_json::json!({});
}
package_json["scripts"]["postinstall"] =
serde_json::Value::String(new_postinstall.clone());
(true, new_postinstall)
}
pub fn update_package_json_content(
content: &str,
) -> Result<(bool, String, String, String), String> {
let mut package_json: serde_json::Value =
serde_json::from_str(content).map_err(|e| format!("Invalid package.json: {e}"))?;
let status = is_postinstall_configured(&package_json);
if !status.needs_update {
return Ok((
false,
content.to_string(),
status.current_script.clone(),
status.current_script,
));
}
let (_, new_script) = update_package_json_object(&mut package_json);
let new_content = serde_json::to_string_pretty(&package_json).unwrap() + "\n";
Ok((true, new_content, status.current_script, new_script))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_not_configured() {
let pkg: serde_json::Value = serde_json::json!({
"name": "test",
"scripts": {
"build": "tsc"
}
});
let status = is_postinstall_configured(&pkg);
assert!(!status.configured);
assert!(status.needs_update);
}
#[test]
fn test_already_configured() {
let pkg: serde_json::Value = serde_json::json!({
"name": "test",
"scripts": {
"postinstall": "socket patch apply --silent --ecosystems npm"
}
});
let status = is_postinstall_configured(&pkg);
assert!(status.configured);
assert!(!status.needs_update);
}
#[test]
fn test_generate_empty() {
assert_eq!(
generate_updated_postinstall(""),
"socket patch apply --silent --ecosystems npm"
);
}
#[test]
fn test_generate_prepend() {
assert_eq!(
generate_updated_postinstall("echo done"),
"socket patch apply --silent --ecosystems npm && echo done"
);
}
#[test]
fn test_generate_already_configured() {
let current = "socket-patch apply && echo done";
assert_eq!(generate_updated_postinstall(current), current);
}
#[test]
fn test_is_postinstall_configured_str_invalid_json() {
let status = is_postinstall_configured_str("not json");
assert!(!status.configured);
assert!(status.needs_update);
}
#[test]
fn test_is_postinstall_configured_str_legacy_npx_pattern() {
let content = r#"{"scripts":{"postinstall":"npx @socketsecurity/socket-patch apply --silent"}}"#;
let status = is_postinstall_configured_str(content);
assert!(status.configured);
assert!(!status.needs_update);
}
#[test]
fn test_is_postinstall_configured_str_socket_dash_patch() {
let content =
r#"{"scripts":{"postinstall":"socket-patch apply --silent --ecosystems npm"}}"#;
let status = is_postinstall_configured_str(content);
assert!(status.configured);
assert!(!status.needs_update);
}
#[test]
fn test_is_postinstall_configured_no_scripts() {
let pkg: serde_json::Value = serde_json::json!({"name": "test"});
let status = is_postinstall_configured(&pkg);
assert!(!status.configured);
assert!(status.current_script.is_empty());
}
#[test]
fn test_is_postinstall_configured_no_postinstall() {
let pkg: serde_json::Value = serde_json::json!({
"scripts": {"build": "tsc"}
});
let status = is_postinstall_configured(&pkg);
assert!(!status.configured);
assert!(status.current_script.is_empty());
}
#[test]
fn test_update_object_creates_scripts() {
let mut pkg: serde_json::Value = serde_json::json!({"name": "test"});
let (modified, new_script) = update_package_json_object(&mut pkg);
assert!(modified);
assert!(new_script.contains("socket patch apply"));
assert!(pkg.get("scripts").is_some());
assert!(pkg["scripts"]["postinstall"].is_string());
}
#[test]
fn test_update_object_noop_when_configured() {
let mut pkg: serde_json::Value = serde_json::json!({
"scripts": {
"postinstall": "socket patch apply --silent --ecosystems npm"
}
});
let (modified, existing) = update_package_json_object(&mut pkg);
assert!(!modified);
assert!(existing.contains("socket patch apply"));
}
#[test]
fn test_update_content_roundtrip_no_scripts() {
let content = r#"{"name": "test"}"#;
let (modified, new_content, old_script, new_script) =
update_package_json_content(content).unwrap();
assert!(modified);
assert!(old_script.is_empty());
assert!(new_script.contains("socket patch apply"));
let parsed: serde_json::Value = serde_json::from_str(&new_content).unwrap();
assert!(parsed["scripts"]["postinstall"].is_string());
}
#[test]
fn test_update_content_already_configured() {
let content = r#"{"scripts":{"postinstall":"socket patch apply --silent --ecosystems npm"}}"#;
let (modified, _new_content, _old, _new) =
update_package_json_content(content).unwrap();
assert!(!modified);
}
#[test]
fn test_update_content_invalid_json() {
let result = update_package_json_content("not json");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid package.json"));
}
#[test]
fn test_generate_whitespace_only() {
let result = generate_updated_postinstall(" \t ");
assert_eq!(result, "socket patch apply --silent --ecosystems npm");
}
}