aede_core/audio_features/
cache.rs1use super::*;
2use md5::{Digest, Md5};
3use std::fs;
4use std::io::Read;
5use std::path::PathBuf;
6#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct InputIdentity {
9 pub path: String,
11 pub size: u64,
13 pub modified: u64,
15 pub nanos: u32,
17}
18#[derive(Clone, Debug, PartialEq)]
20pub struct Record {
21 pub input: InputIdentity,
23 pub features: Features,
25}
26fn location(root: &Path, input: &InputIdentity) -> PathBuf {
27 let token = format!(
28 "{}\0{}\0{}\0{}",
29 input.path, input.size, input.modified, input.nanos
30 );
31 let hash = Md5::digest(token.as_bytes());
32 root.join(format!("acoustic-v{}", aede_moods::PROFILE))
33 .join(format!("{hash:x}.json"))
34}
35fn safe_root(root: &Path) -> Result<(), String> {
36 let metadata = fs::symlink_metadata(root).map_err(|_| "acoustic cache root unavailable")?;
37 if !metadata.is_dir() || metadata.file_type().is_symlink() {
38 return Err("unsafe acoustic cache root".into());
39 }
40 Ok(())
41}
42fn valid_input(input: &InputIdentity) -> bool {
43 Path::new(&input.path).is_absolute()
44 && input.path.len() <= 4096
45 && !input.path.contains('\0')
46 && input.nanos < 1_000_000_000
47}
48pub fn load(root: &Path, input: &InputIdentity) -> Result<Option<Record>, String> {
50 if !valid_input(input) {
51 return Err("invalid acoustic input identity".into());
52 }
53 let path = location(root, input);
54 let parent = path.parent().ok_or("invalid acoustic cache path")?;
55 if !parent.exists() {
56 return Ok(None);
57 }
58 safe_root(parent)?;
59 let metadata = match fs::symlink_metadata(&path) {
60 Ok(v) => v,
61 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
62 Err(_) => return Err("acoustic cache unavailable".into()),
63 };
64 if !metadata.is_file() || metadata.file_type().is_symlink() || metadata.len() > 8192 {
65 return Err("invalid acoustic cache file".into());
66 }
67 let mut bytes = Vec::new();
68 fs::File::open(&path)
69 .map_err(|_| "acoustic cache unavailable")?
70 .take(8193)
71 .read_to_end(&mut bytes)
72 .map_err(|_| "acoustic cache read failed")?;
73 if bytes.len() > 8192 {
74 return Err("acoustic cache too large".into());
75 }
76 let value: serde_json::Value =
77 serde_json::from_slice(&bytes).map_err(|_| "acoustic cache unreadable")?;
78 if value["path"].as_str() != Some(&input.path)
79 || value["size"].as_u64() != Some(input.size)
80 || value["modified"].as_u64() != Some(input.modified)
81 || value["nanos"].as_u64() != Some(u64::from(input.nanos))
82 || value.as_object().is_none_or(|o| o.len() != 5)
83 {
84 return Err("acoustic cache identity mismatch".into());
85 }
86 let features: Features = serde_json::from_value(value["features"].clone())
87 .map_err(|_| "acoustic cache features invalid")?;
88 if !features.valid() {
89 return Err("acoustic cache profile invalid".into());
90 }
91 Ok(Some(Record {
92 input: input.clone(),
93 features,
94 }))
95}
96pub fn save(root: &Path, record: &Record) -> Result<(), String> {
98 if !valid_input(&record.input) || !record.features.valid() {
99 return Err("invalid acoustic record".into());
100 }
101 safe_root(root)?;
102 let path = location(root, &record.input);
103 let parent = path.parent().ok_or("invalid acoustic cache path")?;
104 match fs::create_dir(parent) {
105 Ok(()) => {}
106 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
107 Err(_) => return Err("acoustic cache directory unavailable".into()),
108 };
109 safe_root(parent)?;
110 let value = serde_json::json!({"path":record.input.path,"size":record.input.size,"modified":record.input.modified,"nanos":record.input.nanos,"features":record.features});
111 let bytes = serde_json::to_vec(&value).map_err(|_| "acoustic cache serialization failed")?;
112 if bytes.len() > 8192 {
113 return Err("acoustic cache too large".into());
114 }
115 crate::atomic_file::write(&path, &bytes).map_err(|_| "acoustic cache publication failed".into())
116}
117#[cfg(test)]
118#[path = "cache_tests.rs"]
119mod tests;