start_command/
store_decode_cache.rs1use crate::atomic_write::atomic_write;
18use crate::execution_store::ExecutionRecord;
19use serde_json::{json, Value};
20use sha2::{Digest, Sha256};
21use std::collections::HashMap;
22use std::fs;
23use std::path::{Path, PathBuf};
24use std::sync::{Mutex, OnceLock};
25
26pub const DECODED_COPY_FORMAT: &str = "start-command-decoded-store-rust-1";
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum DecodeSource {
32 Memory,
34 Copy,
36 Decode,
38}
39
40struct DecodedStore {
41 content: String,
42 records: Vec<ExecutionRecord>,
43 decodes: usize,
44}
45
46fn decoded_stores() -> &'static Mutex<HashMap<PathBuf, DecodedStore>> {
47 static STORES: OnceLock<Mutex<HashMap<PathBuf, DecodedStore>>> = OnceLock::new();
48 STORES.get_or_init(|| Mutex::new(HashMap::new()))
49}
50
51fn digest_of(content: &str) -> String {
52 Sha256::digest(content.as_bytes())
53 .iter()
54 .map(|byte| format!("{:02x}", byte))
55 .collect()
56}
57
58fn read_decoded_copy(copy_path: &Path, digest: &str) -> Option<Vec<Value>> {
61 let copy: Value = serde_json::from_str(&fs::read_to_string(copy_path).ok()?).ok()?;
62 if copy.get("format")?.as_str()? != DECODED_COPY_FORMAT
63 || copy.get("sha256")?.as_str()? != digest
64 {
65 return None;
66 }
67 copy.get("records")?.as_array().cloned()
68}
69
70fn write_decoded_copy(copy_path: &Path, digest: &str, values: &[Value]) {
73 let copy = json!({
74 "format": DECODED_COPY_FORMAT,
75 "sha256": digest,
76 "records": values,
77 });
78 if let Ok(text) = serde_json::to_string(©) {
79 let _ = atomic_write(copy_path, text.as_bytes());
80 }
81}
82
83fn to_records(values: &[Value]) -> Vec<ExecutionRecord> {
84 values
85 .iter()
86 .filter_map(ExecutionRecord::from_json)
87 .collect()
88}
89
90pub fn decode_store_content<E>(
95 path: &Path,
96 content: &str,
97 decode: impl FnOnce(&str) -> Result<Vec<Value>, E>,
98 copy_path: Option<&Path>,
99) -> Result<(Vec<ExecutionRecord>, DecodeSource), E> {
100 let mut stores = decoded_stores()
101 .lock()
102 .unwrap_or_else(|poisoned| poisoned.into_inner());
103 if let Some(cached) = stores.get(path).filter(|cached| cached.content == content) {
104 return Ok((cached.records.clone(), DecodeSource::Memory));
105 }
106 let decodes = stores.get(path).map_or(0, |cached| cached.decodes);
107 let digest = copy_path.map(|_| digest_of(content));
108 let copied = copy_path
109 .zip(digest.as_deref())
110 .and_then(|(copy_path, digest)| read_decoded_copy(copy_path, digest));
111 let (records, source, decodes) = match copied {
112 Some(values) => (to_records(&values), DecodeSource::Copy, decodes),
113 None => {
114 let values = decode(content)?;
115 if let Some((copy_path, digest)) = copy_path.zip(digest.as_deref()) {
116 write_decoded_copy(copy_path, digest, &values);
117 }
118 (to_records(&values), DecodeSource::Decode, decodes + 1)
119 }
120 };
121 stores.insert(
122 path.to_path_buf(),
123 DecodedStore {
124 content: content.to_string(),
125 records: records.clone(),
126 decodes,
127 },
128 );
129 Ok((records, source))
130}
131
132pub fn decode_count(path: &Path) -> usize {
135 decoded_stores()
136 .lock()
137 .unwrap_or_else(|poisoned| poisoned.into_inner())
138 .get(path)
139 .map_or(0, |cached| cached.decodes)
140}
141
142pub fn forget_decoded_store(path: Option<&Path>) {
145 let mut stores = decoded_stores()
146 .lock()
147 .unwrap_or_else(|poisoned| poisoned.into_inner());
148 match path {
149 Some(path) => {
150 stores.remove(path);
151 }
152 None => stores.clear(),
153 }
154}