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 = 8;
pub const AUDIO_REVISION: u32 = 2;
#[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>;
fn ffmpeg_build(&self) -> String {
String::from("unknown")
}
}
#[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()?;
let hash = Sha256::digest(&bytes)
.iter()
.fold(String::new(), |mut s, b| {
let _ = write!(s, "{b:02x}");
s
});
Some(Key::of(&Value::String(hash)).0)
}
fn ffmpeg_build(&self) -> String {
static BUILD: std::sync::OnceLock<String> = std::sync::OnceLock::new();
BUILD
.get_or_init(|| {
let ffmpeg = std::env::var_os("FFMPEG").unwrap_or_else(|| "ffmpeg".into());
std::process::Command::new(ffmpeg)
.args(["-hide_banner", "-version"])
.output()
.ok()
.filter(|o| o.status.success())
.map_or_else(
|| String::from("unavailable"),
|o| {
let out = String::from_utf8_lossy(&o.stdout);
let mut lines = out.lines();
let first = lines.next().unwrap_or("").trim().to_owned();
let lavfi = out
.lines()
.find(|l| l.trim_start().starts_with("libavfilter"))
.unwrap_or("")
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
format!("{first}; {lavfi}")
},
)
})
.clone()
}
}
#[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);
}
}