use crate::atomic_write::atomic_write;
use crate::execution_store::ExecutionRecord;
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
pub const DECODED_COPY_FORMAT: &str = "start-command-decoded-store-rust-1";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecodeSource {
Memory,
Copy,
Decode,
}
struct DecodedStore {
content: String,
records: Vec<ExecutionRecord>,
decodes: usize,
}
fn decoded_stores() -> &'static Mutex<HashMap<PathBuf, DecodedStore>> {
static STORES: OnceLock<Mutex<HashMap<PathBuf, DecodedStore>>> = OnceLock::new();
STORES.get_or_init(|| Mutex::new(HashMap::new()))
}
fn digest_of(content: &str) -> String {
Sha256::digest(content.as_bytes())
.iter()
.map(|byte| format!("{:02x}", byte))
.collect()
}
fn read_decoded_copy(copy_path: &Path, digest: &str) -> Option<Vec<Value>> {
let copy: Value = serde_json::from_str(&fs::read_to_string(copy_path).ok()?).ok()?;
if copy.get("format")?.as_str()? != DECODED_COPY_FORMAT
|| copy.get("sha256")?.as_str()? != digest
{
return None;
}
copy.get("records")?.as_array().cloned()
}
fn write_decoded_copy(copy_path: &Path, digest: &str, values: &[Value]) {
let copy = json!({
"format": DECODED_COPY_FORMAT,
"sha256": digest,
"records": values,
});
if let Ok(text) = serde_json::to_string(©) {
let _ = atomic_write(copy_path, text.as_bytes());
}
}
fn to_records(values: &[Value]) -> Vec<ExecutionRecord> {
values
.iter()
.filter_map(ExecutionRecord::from_json)
.collect()
}
pub fn decode_store_content<E>(
path: &Path,
content: &str,
decode: impl FnOnce(&str) -> Result<Vec<Value>, E>,
copy_path: Option<&Path>,
) -> Result<(Vec<ExecutionRecord>, DecodeSource), E> {
let mut stores = decoded_stores()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if let Some(cached) = stores.get(path).filter(|cached| cached.content == content) {
return Ok((cached.records.clone(), DecodeSource::Memory));
}
let decodes = stores.get(path).map_or(0, |cached| cached.decodes);
let digest = copy_path.map(|_| digest_of(content));
let copied = copy_path
.zip(digest.as_deref())
.and_then(|(copy_path, digest)| read_decoded_copy(copy_path, digest));
let (records, source, decodes) = match copied {
Some(values) => (to_records(&values), DecodeSource::Copy, decodes),
None => {
let values = decode(content)?;
if let Some((copy_path, digest)) = copy_path.zip(digest.as_deref()) {
write_decoded_copy(copy_path, digest, &values);
}
(to_records(&values), DecodeSource::Decode, decodes + 1)
}
};
stores.insert(
path.to_path_buf(),
DecodedStore {
content: content.to_string(),
records: records.clone(),
decodes,
},
);
Ok((records, source))
}
pub fn decode_count(path: &Path) -> usize {
decoded_stores()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.get(path)
.map_or(0, |cached| cached.decodes)
}
pub fn forget_decoded_store(path: Option<&Path>) {
let mut stores = decoded_stores()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
match path {
Some(path) => {
stores.remove(path);
}
None => stores.clear(),
}
}