use std::fmt::{self, Write as _};
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use sha2::{Digest, Sha256};
pub const RENDER_REVISION: u32 = 6;
pub const AUDIO_REVISION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Key(pub String);
impl Key {
pub fn of(v: &Value) -> Self {
let digest = Sha256::digest(canonical(v).as_bytes());
Self(digest[..16].iter().fold(String::new(), |mut s, b| {
let _ = write!(s, "{b:02x}");
s
}))
}
pub fn short(&self) -> &str {
&self.0[..self.0.len().min(10)]
}
}
impl fmt::Display for Key {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
pub fn canonical(v: &Value) -> String {
fn sorted(v: &Value) -> Value {
match v {
Value::Object(m) => {
let mut keys: Vec<&String> = m.keys().collect();
keys.sort();
Value::Object(
keys.into_iter()
.map(|k| (k.clone(), sorted(&m[k])))
.collect::<Map<_, _>>(),
)
}
Value::Array(a) => Value::Array(a.iter().map(sorted).collect()),
v => v.clone(),
}
}
sorted(v).to_string()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Fingerprint {
pub path: PathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mtime_ns: Option<u128>,
}
pub trait Fingerprinter {
fn fingerprint(&self, path: &Path) -> Fingerprint;
fn content_hash(&self, path: &Path) -> Option<String>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FsFingerprinter;
impl Fingerprinter for FsFingerprinter {
fn fingerprint(&self, path: &Path) -> Fingerprint {
let canon = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_owned());
let meta = std::fs::metadata(&canon).ok();
Fingerprint {
path: canon,
size: meta.as_ref().map(std::fs::Metadata::len),
mtime_ns: meta
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_nanos()),
}
}
fn content_hash(&self, path: &Path) -> Option<String> {
let bytes = std::fs::read(path).ok()?;
Some(Key::of(&Value::String(format!("{:x}", Sha256::digest(&bytes)))).0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn key_ignores_object_key_order() {
let a = json!({"a": 1, "b": {"x": [1, 2], "y": null}});
let b = json!({"b": {"y": null, "x": [1, 2]}, "a": 1});
assert_eq!(Key::of(&a), Key::of(&b));
assert_ne!(
Key::of(&a),
Key::of(&json!({"a": 2, "b": {"x": [1, 2], "y": null}}))
);
assert_eq!(Key::of(&a).0.len(), 32);
}
}