use crate::error::{Error, Result};
use crate::phpjson::php_json_encode;
use md5::{Digest, Md5};
use serde_json::{Map, Value};
const RELEVANT_KEYS: [&str; 11] = [
"name",
"version",
"require",
"require-dev",
"conflict",
"replace",
"provide",
"minimum-stability",
"prefer-stable",
"repositories",
"extra",
];
pub fn content_hash(composer_json_text: &str) -> Result<String> {
let content: Value =
serde_json::from_str(composer_json_text).map_err(|source| Error::Json {
context: "composer.json".to_owned(),
source,
})?;
let mut relevant = Map::new();
if let Value::Object(obj) = &content {
for key in RELEVANT_KEYS {
if let Some(v) = obj.get(key).filter(|v| !v.is_null()) {
relevant.insert(key.to_owned(), v.clone());
}
}
if let Some(platform) = obj
.get("config")
.and_then(|c| c.get("platform"))
.filter(|v| !v.is_null())
{
let mut config = Map::new();
config.insert("platform".to_owned(), platform.clone());
relevant.insert("config".to_owned(), Value::Object(config));
}
}
let mut entries: Vec<(String, Value)> = relevant.into_iter().collect();
entries.sort_by(|(a, _), (b, _)| a.cmp(b));
let sorted: Map<String, Value> = entries.into_iter().collect();
let encoded = php_json_encode(&Value::Object(sorted))?;
let mut hasher = Md5::new();
hasher.update(encoded.as_bytes());
Ok(format!("{:x}", hasher.finalize()))
}
pub fn md5_hex(data: &[u8]) -> String {
let mut h = Md5::new();
h.update(data);
h.finalize().iter().map(|b| format!("{b:02x}")).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn minimal_manifest_is_stable() {
let h = content_hash(r#"{"require":{"php":">=8.1"}}"#).expect("hash");
assert_eq!(h.len(), 32);
let h2 = content_hash(r#"{"require":{"php":">=8.1"},"description":"x"}"#).expect("hash");
assert_eq!(h, h2);
let h3 = content_hash(r#"{"require":{"php":">=8.2"}}"#).expect("hash");
assert_ne!(h, h3);
}
#[test]
fn config_platform_is_renested() {
let a =
content_hash(r#"{"require":{},"config":{"platform":{"php":"8.2.0"}}}"#).expect("hash");
let b = content_hash(r#"{"require":{},"config":{"sort-packages":true}}"#).expect("hash");
let c = content_hash(r#"{"require":{}}"#).expect("hash");
assert_ne!(a, c); assert_eq!(b, c); }
}