1use std::fs::{self, File};
2use std::io::{BufReader, BufWriter, Read, Write};
3use std::path::{Path, PathBuf};
4use std::time::{Duration, SystemTime, UNIX_EPOCH};
5
6use crate::parser::SymbolCache;
7use crate::symbols::Symbol;
8use crate::{slog_info, slog_warn};
9
10const MAGIC: &[u8; 8] = b"AFTSYM1\0";
11const VERSION: u32 = 2;
12const MAX_ENTRIES: usize = 2_000_000;
13const MAX_PATH_BYTES: usize = 16 * 1024;
14const MAX_SYMBOL_BYTES: usize = 16 * 1024 * 1024;
15
16#[derive(Debug, Clone)]
17pub struct DiskSymbolCache {
18 pub(crate) entries: Vec<DiskSymbolEntry>,
19}
20
21#[derive(Debug, Clone)]
22pub(crate) struct DiskSymbolEntry {
23 pub(crate) relative_path: PathBuf,
24 pub(crate) mtime: SystemTime,
25 pub(crate) size: u64,
26 pub(crate) content_hash: blake3::Hash,
27 pub(crate) symbols: Vec<Symbol>,
28}
29
30impl DiskSymbolCache {
31 pub fn len(&self) -> usize {
32 self.entries.len()
33 }
34
35 pub fn is_empty(&self) -> bool {
36 self.entries.is_empty()
37 }
38}
39
40pub(crate) fn cache_path(storage_dir: &Path, project_key: &str) -> PathBuf {
41 storage_dir
42 .join("symbols")
43 .join(project_key)
44 .join("symbols.bin")
45}
46
47pub fn read_from_disk(storage_dir: &Path, project_key: &str) -> Option<DiskSymbolCache> {
48 let data_path = cache_path(storage_dir, project_key);
49 if !data_path.exists() {
50 return None;
51 }
52
53 match read_cache_file(&data_path) {
54 Ok(cache) => Some(cache),
55 Err(error) => {
56 slog_warn!(
57 "corrupt symbol cache at {}: {}, rebuilding",
58 data_path.display(),
59 error
60 );
61 None
62 }
63 }
64}
65
66pub fn write_to_disk(
67 cache: &SymbolCache,
68 storage_dir: &Path,
69 project_key: &str,
70) -> std::io::Result<()> {
71 if cache.len() == 0 {
72 slog_info!("skipping symbol cache persistence (0 entries)");
73 return Ok(());
74 }
75
76 let project_root = cache.project_root().ok_or_else(|| {
77 std::io::Error::other("symbol cache project root is not set; cannot persist relative paths")
78 })?;
79
80 let dir = storage_dir.join("symbols").join(project_key);
81 fs::create_dir_all(&dir)?;
82
83 let data_path = dir.join("symbols.bin");
84 let tmp_path = dir.join("symbols.bin.tmp");
85 let write_result = write_cache_file(cache, &project_root, &tmp_path).and_then(|()| {
86 fs::rename(&tmp_path, &data_path)?;
87 if let Ok(dir_file) = File::open(&dir) {
88 let _ = dir_file.sync_all();
89 }
90 Ok(())
91 });
92
93 if write_result.is_err() {
94 let _ = fs::remove_file(&tmp_path);
95 }
96
97 write_result
98}
99
100fn read_cache_file(path: &Path) -> Result<DiskSymbolCache, String> {
101 let mut reader = BufReader::new(File::open(path).map_err(|error| error.to_string())?);
102
103 let mut magic = [0u8; 8];
104 reader
105 .read_exact(&mut magic)
106 .map_err(|error| format!("failed to read symbol cache magic: {error}"))?;
107 if &magic != MAGIC {
108 return Err("invalid symbol cache magic".to_string());
109 }
110
111 let version = read_u32(&mut reader)?;
112 if version != VERSION {
113 return Err(format!(
114 "unsupported symbol cache version: {version} (expected {VERSION})"
115 ));
116 }
117
118 let root_len = read_u32(&mut reader)? as usize;
119 let entry_count = read_u32(&mut reader)? as usize;
120 if root_len > MAX_PATH_BYTES {
121 return Err(format!("project root path too large: {root_len} bytes"));
122 }
123 if entry_count > MAX_ENTRIES {
124 return Err(format!("too many symbol cache entries: {entry_count}"));
125 }
126
127 let _project_root = PathBuf::from(read_string_with_len(&mut reader, root_len)?);
128 let mut entries = Vec::with_capacity(entry_count);
129
130 for _ in 0..entry_count {
131 let path_len = read_u32(&mut reader)? as usize;
132 if path_len > MAX_PATH_BYTES {
133 return Err(format!("cached path too large: {path_len} bytes"));
134 }
135 let relative_path = PathBuf::from(read_string_with_len(&mut reader, path_len)?);
136 let mtime_secs = read_i64(&mut reader)?;
137 let mtime_nanos = read_u32(&mut reader)?;
138 let size = read_u64(&mut reader)?;
139 let mut hash_bytes = [0u8; 32];
140 reader
141 .read_exact(&mut hash_bytes)
142 .map_err(|error| format!("failed to read symbol content hash: {error}"))?;
143 let content_hash = blake3::Hash::from_bytes(hash_bytes);
144 let symbol_bytes_len = read_u32(&mut reader)? as usize;
145 if symbol_bytes_len > MAX_SYMBOL_BYTES {
146 return Err(format!(
147 "cached symbol payload too large: {symbol_bytes_len} bytes"
148 ));
149 }
150
151 let mut symbol_bytes = vec![0u8; symbol_bytes_len];
152 reader
153 .read_exact(&mut symbol_bytes)
154 .map_err(|error| format!("failed to read symbol payload: {error}"))?;
155 let symbols: Vec<Symbol> = serde_json::from_slice(&symbol_bytes)
156 .map_err(|error| format!("failed to decode cached symbols: {error}"))?;
157
158 entries.push(DiskSymbolEntry {
159 relative_path,
160 mtime: system_time_from_parts(mtime_secs, mtime_nanos)?,
161 size,
162 content_hash,
163 symbols,
164 });
165 }
166
167 Ok(DiskSymbolCache { entries })
168}
169
170fn write_cache_file(
171 cache: &SymbolCache,
172 project_root: &Path,
173 tmp_path: &Path,
174) -> std::io::Result<()> {
175 let mut writer = BufWriter::new(File::create(tmp_path)?);
176 let entries = cache.disk_entries();
177 let root = project_root.to_string_lossy();
178 let root_len = u32::try_from(root.len())
179 .map_err(|_| std::io::Error::other("project root too large to cache"))?;
180 let entry_count = u32::try_from(entries.len())
181 .map_err(|_| std::io::Error::other("too many symbol cache entries"))?;
182
183 writer.write_all(MAGIC)?;
184 write_u32(&mut writer, VERSION)?;
185 write_u32(&mut writer, root_len)?;
186 write_u32(&mut writer, entry_count)?;
187 writer.write_all(root.as_bytes())?;
188
189 for (path, mtime, size, content_hash, symbols) in entries {
190 if symbols.is_empty() {
191 continue;
192 }
193 let relative_path = path.strip_prefix(project_root).unwrap_or(path.as_path());
194 let path_bytes = relative_path.to_string_lossy();
195 let path_len = u32::try_from(path_bytes.len())
196 .map_err(|_| std::io::Error::other("cached path too large"))?;
197 let (secs, nanos) = system_time_parts(mtime);
198 let symbol_bytes = serde_json::to_vec(symbols).map_err(|error| {
199 std::io::Error::other(format!("symbol serialization failed: {error}"))
200 })?;
201 let symbol_len = u32::try_from(symbol_bytes.len())
202 .map_err(|_| std::io::Error::other("cached symbol payload too large"))?;
203
204 write_u32(&mut writer, path_len)?;
205 writer.write_all(path_bytes.as_bytes())?;
206 write_i64(&mut writer, secs)?;
207 write_u32(&mut writer, nanos)?;
208 write_u64(&mut writer, size)?;
209 writer.write_all(content_hash.as_bytes())?;
210 write_u32(&mut writer, symbol_len)?;
211 writer.write_all(&symbol_bytes)?;
212 }
213
214 writer.flush()?;
215 writer.get_ref().sync_all()?;
216 Ok(())
217}
218
219fn system_time_parts(time: SystemTime) -> (i64, u32) {
220 match time.duration_since(UNIX_EPOCH) {
221 Ok(duration) => (
222 i64::try_from(duration.as_secs()).unwrap_or(i64::MAX),
223 duration.subsec_nanos(),
224 ),
225 Err(error) => {
226 let duration = error.duration();
227 let nanos = duration.subsec_nanos();
228 if nanos == 0 {
229 (-(duration.as_secs() as i64), 0)
230 } else {
231 (-(duration.as_secs() as i64) - 1, 1_000_000_000 - nanos)
232 }
233 }
234 }
235}
236
237fn system_time_from_parts(secs: i64, nanos: u32) -> Result<SystemTime, String> {
238 if nanos >= 1_000_000_000 {
239 return Err(format!(
240 "invalid symbol cache mtime nanos: {nanos} >= 1_000_000_000"
241 ));
242 }
243
244 if secs >= 0 {
245 let duration = Duration::new(secs as u64, nanos);
246 UNIX_EPOCH
247 .checked_add(duration)
248 .ok_or_else(|| format!("symbol cache mtime overflows SystemTime: {secs}.{nanos}"))
249 } else {
250 let whole = Duration::new(secs.unsigned_abs(), 0);
251 let base = UNIX_EPOCH.checked_sub(whole).ok_or_else(|| {
252 format!("symbol cache negative mtime overflows SystemTime: {secs}.{nanos}")
253 })?;
254 base.checked_add(Duration::new(0, nanos)).ok_or_else(|| {
255 format!("symbol cache negative mtime overflows SystemTime: {secs}.{nanos}")
256 })
257 }
258}
259
260fn read_string_with_len<R: Read>(reader: &mut R, len: usize) -> Result<String, String> {
261 let mut bytes = vec![0u8; len];
262 reader
263 .read_exact(&mut bytes)
264 .map_err(|error| format!("failed to read string: {error}"))?;
265 String::from_utf8(bytes).map_err(|error| format!("invalid utf-8 string: {error}"))
266}
267
268fn read_u32<R: Read>(reader: &mut R) -> Result<u32, String> {
269 let mut bytes = [0u8; 4];
270 reader
271 .read_exact(&mut bytes)
272 .map_err(|error| format!("failed to read u32: {error}"))?;
273 Ok(u32::from_le_bytes(bytes))
274}
275
276fn read_i64<R: Read>(reader: &mut R) -> Result<i64, String> {
277 let mut bytes = [0u8; 8];
278 reader
279 .read_exact(&mut bytes)
280 .map_err(|error| format!("failed to read i64: {error}"))?;
281 Ok(i64::from_le_bytes(bytes))
282}
283
284fn read_u64<R: Read>(reader: &mut R) -> Result<u64, String> {
285 let mut bytes = [0u8; 8];
286 reader
287 .read_exact(&mut bytes)
288 .map_err(|error| format!("failed to read u64: {error}"))?;
289 Ok(u64::from_le_bytes(bytes))
290}
291
292fn write_u32<W: Write>(writer: &mut W, value: u32) -> std::io::Result<()> {
293 writer.write_all(&value.to_le_bytes())
294}
295
296fn write_i64<W: Write>(writer: &mut W, value: i64) -> std::io::Result<()> {
297 writer.write_all(&value.to_le_bytes())
298}
299
300fn write_u64<W: Write>(writer: &mut W, value: u64) -> std::io::Result<()> {
301 writer.write_all(&value.to_le_bytes())
302}