Skip to main content

aft/
symbol_cache_disk.rs

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 = 1;
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) project_root: PathBuf,
19    pub(crate) entries: Vec<DiskSymbolEntry>,
20}
21
22#[derive(Debug, Clone)]
23pub(crate) struct DiskSymbolEntry {
24    pub(crate) relative_path: PathBuf,
25    pub(crate) mtime: SystemTime,
26    pub(crate) size: u64,
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 symbol_bytes_len = read_u32(&mut reader)? as usize;
140        if symbol_bytes_len > MAX_SYMBOL_BYTES {
141            return Err(format!(
142                "cached symbol payload too large: {symbol_bytes_len} bytes"
143            ));
144        }
145
146        let mut symbol_bytes = vec![0u8; symbol_bytes_len];
147        reader
148            .read_exact(&mut symbol_bytes)
149            .map_err(|error| format!("failed to read symbol payload: {error}"))?;
150        let symbols: Vec<Symbol> = serde_json::from_slice(&symbol_bytes)
151            .map_err(|error| format!("failed to decode cached symbols: {error}"))?;
152
153        entries.push(DiskSymbolEntry {
154            relative_path,
155            mtime: system_time_from_parts(mtime_secs, mtime_nanos)?,
156            size,
157            symbols,
158        });
159    }
160
161    Ok(DiskSymbolCache {
162        project_root,
163        entries,
164    })
165}
166
167fn write_cache_file(
168    cache: &SymbolCache,
169    project_root: &Path,
170    tmp_path: &Path,
171) -> std::io::Result<()> {
172    let mut writer = BufWriter::new(File::create(tmp_path)?);
173    let entries = cache.disk_entries();
174    let root = project_root.to_string_lossy();
175    let root_len = u32::try_from(root.len())
176        .map_err(|_| std::io::Error::other("project root too large to cache"))?;
177    let entry_count = u32::try_from(entries.len())
178        .map_err(|_| std::io::Error::other("too many symbol cache entries"))?;
179
180    writer.write_all(MAGIC)?;
181    write_u32(&mut writer, VERSION)?;
182    write_u32(&mut writer, root_len)?;
183    write_u32(&mut writer, entry_count)?;
184    writer.write_all(root.as_bytes())?;
185
186    for (path, mtime, size, symbols) in entries {
187        if symbols.is_empty() {
188            continue;
189        }
190        let relative_path = path.strip_prefix(project_root).unwrap_or(path.as_path());
191        let path_bytes = relative_path.to_string_lossy();
192        let path_len = u32::try_from(path_bytes.len())
193            .map_err(|_| std::io::Error::other("cached path too large"))?;
194        let (secs, nanos) = system_time_parts(mtime);
195        let symbol_bytes = serde_json::to_vec(symbols).map_err(|error| {
196            std::io::Error::other(format!("symbol serialization failed: {error}"))
197        })?;
198        let symbol_len = u32::try_from(symbol_bytes.len())
199            .map_err(|_| std::io::Error::other("cached symbol payload too large"))?;
200
201        write_u32(&mut writer, path_len)?;
202        writer.write_all(path_bytes.as_bytes())?;
203        write_i64(&mut writer, secs)?;
204        write_u32(&mut writer, nanos)?;
205        write_u64(&mut writer, size)?;
206        write_u32(&mut writer, symbol_len)?;
207        writer.write_all(&symbol_bytes)?;
208    }
209
210    writer.flush()?;
211    writer.get_ref().sync_all()?;
212    Ok(())
213}
214
215fn system_time_parts(time: SystemTime) -> (i64, u32) {
216    match time.duration_since(UNIX_EPOCH) {
217        Ok(duration) => (
218            i64::try_from(duration.as_secs()).unwrap_or(i64::MAX),
219            duration.subsec_nanos(),
220        ),
221        Err(error) => {
222            let duration = error.duration();
223            let nanos = duration.subsec_nanos();
224            if nanos == 0 {
225                (-(duration.as_secs() as i64), 0)
226            } else {
227                (-(duration.as_secs() as i64) - 1, 1_000_000_000 - nanos)
228            }
229        }
230    }
231}
232
233fn system_time_from_parts(secs: i64, nanos: u32) -> Result<SystemTime, String> {
234    if nanos >= 1_000_000_000 {
235        return Err(format!(
236            "invalid symbol cache mtime nanos: {nanos} >= 1_000_000_000"
237        ));
238    }
239
240    if secs >= 0 {
241        let duration = Duration::new(secs as u64, nanos);
242        UNIX_EPOCH
243            .checked_add(duration)
244            .ok_or_else(|| format!("symbol cache mtime overflows SystemTime: {secs}.{nanos}"))
245    } else {
246        let whole = Duration::new(secs.unsigned_abs(), 0);
247        let base = UNIX_EPOCH.checked_sub(whole).ok_or_else(|| {
248            format!("symbol cache negative mtime overflows SystemTime: {secs}.{nanos}")
249        })?;
250        base.checked_add(Duration::new(0, nanos)).ok_or_else(|| {
251            format!("symbol cache negative mtime overflows SystemTime: {secs}.{nanos}")
252        })
253    }
254}
255
256fn read_string_with_len<R: Read>(reader: &mut R, len: usize) -> Result<String, String> {
257    let mut bytes = vec![0u8; len];
258    reader
259        .read_exact(&mut bytes)
260        .map_err(|error| format!("failed to read string: {error}"))?;
261    String::from_utf8(bytes).map_err(|error| format!("invalid utf-8 string: {error}"))
262}
263
264fn read_u32<R: Read>(reader: &mut R) -> Result<u32, String> {
265    let mut bytes = [0u8; 4];
266    reader
267        .read_exact(&mut bytes)
268        .map_err(|error| format!("failed to read u32: {error}"))?;
269    Ok(u32::from_le_bytes(bytes))
270}
271
272fn read_i64<R: Read>(reader: &mut R) -> Result<i64, String> {
273    let mut bytes = [0u8; 8];
274    reader
275        .read_exact(&mut bytes)
276        .map_err(|error| format!("failed to read i64: {error}"))?;
277    Ok(i64::from_le_bytes(bytes))
278}
279
280fn read_u64<R: Read>(reader: &mut R) -> Result<u64, String> {
281    let mut bytes = [0u8; 8];
282    reader
283        .read_exact(&mut bytes)
284        .map_err(|error| format!("failed to read u64: {error}"))?;
285    Ok(u64::from_le_bytes(bytes))
286}
287
288fn write_u32<W: Write>(writer: &mut W, value: u32) -> std::io::Result<()> {
289    writer.write_all(&value.to_le_bytes())
290}
291
292fn write_i64<W: Write>(writer: &mut W, value: i64) -> std::io::Result<()> {
293    writer.write_all(&value.to_le_bytes())
294}
295
296fn write_u64<W: Write>(writer: &mut W, value: u64) -> std::io::Result<()> {
297    writer.write_all(&value.to_le_bytes())
298}