dlin_core/parser/
manifest_cache.rs1use std::path::{Path, PathBuf};
2use std::time::SystemTime;
3
4use serde::{Deserialize, Serialize};
5
6use crate::graph::types::LineageGraph;
7
8const CACHE_DIR: &str = ".dlin_cache";
9const CACHE_FILENAME: &str = "manifest_graph_cache.json";
10
11#[derive(Debug, Serialize, Deserialize)]
12struct ManifestCacheFile {
13 #[serde(default)]
14 version: String,
15 entry: Option<ManifestCacheEntry>,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19struct ManifestCacheEntry {
20 #[serde(default)]
21 manifest_identity: String,
22 mtime_secs: u64,
23 #[serde(default)]
24 mtime_nanos: u32,
25 file_size: u64,
26 #[serde(default)]
27 content_hash: u64,
28 graph: LineageGraph,
29}
30
31pub struct ManifestGraphCache {
32 version: String,
33 entry: Option<ManifestCacheEntry>,
34 cache_path: Option<PathBuf>,
35 dirty: bool,
36}
37
38impl ManifestGraphCache {
39 pub fn disabled() -> Self {
40 Self {
41 version: String::new(),
42 entry: None,
43 cache_path: None,
44 dirty: false,
45 }
46 }
47
48 pub fn load(project_dir: &Path, cache_dir: Option<&Path>) -> Self {
49 let cache_path = match cache_dir {
50 Some(dir) => dir.join(CACHE_FILENAME),
51 None => project_dir.join(CACHE_DIR).join(CACHE_FILENAME),
52 };
53 let version = env!("CARGO_PKG_VERSION").to_string();
54 let entry = std::fs::read_to_string(&cache_path)
55 .ok()
56 .and_then(|content| serde_json::from_str::<ManifestCacheFile>(&content).ok())
57 .filter(|cf| cf.version == version)
58 .and_then(|cf| cf.entry);
59
60 Self {
61 version,
62 entry,
63 cache_path: Some(cache_path),
64 dirty: false,
65 }
66 }
67
68 pub fn fresh(project_dir: &Path, cache_dir: Option<&Path>) -> Self {
69 let cache_path = match cache_dir {
70 Some(dir) => dir.join(CACHE_FILENAME),
71 None => project_dir.join(CACHE_DIR).join(CACHE_FILENAME),
72 };
73 Self {
74 version: env!("CARGO_PKG_VERSION").to_string(),
75 entry: None,
76 cache_path: Some(cache_path),
77 dirty: false,
78 }
79 }
80
81 pub fn get(&self, manifest_path: &Path) -> Option<&LineageGraph> {
82 let entry = self.entry.as_ref()?;
83 let stat = file_stat(manifest_path)?;
84 let identity = manifest_identity(manifest_path);
85 if entry.manifest_identity == identity
86 && entry.mtime_secs == stat.mtime_secs
87 && entry.mtime_nanos == stat.mtime_nanos
88 && entry.file_size == stat.file_size
89 && entry.content_hash == stat.content_hash
90 {
91 Some(&entry.graph)
92 } else {
93 None
94 }
95 }
96
97 pub fn insert_if_fingerprint_matches(
98 &mut self,
99 manifest_path: &Path,
100 graph: &LineageGraph,
101 expected: (u64, u32, u64, u64),
102 ) -> bool {
103 let Some(stat) = file_stat(manifest_path) else {
104 return false;
105 };
106 if (
107 stat.mtime_secs,
108 stat.mtime_nanos,
109 stat.file_size,
110 stat.content_hash,
111 ) != expected
112 {
113 return false;
114 }
115 self.entry = Some(ManifestCacheEntry {
116 manifest_identity: manifest_identity(manifest_path),
117 mtime_secs: stat.mtime_secs,
118 mtime_nanos: stat.mtime_nanos,
119 file_size: stat.file_size,
120 content_hash: stat.content_hash,
121 graph: graph.clone(),
122 });
123 self.dirty = true;
124 true
125 }
126
127 pub fn save(&self) {
128 let cache_path = match (&self.cache_path, self.dirty) {
129 (Some(p), true) => p,
130 _ => return,
131 };
132 let cf = ManifestCacheFile {
133 version: self.version.clone(),
134 entry: self.entry.clone(),
135 };
136 if let Some(parent) = cache_path.parent() {
137 if std::fs::create_dir_all(parent).is_err() {
138 crate::warn!("could not create cache directory: {}", parent.display());
139 return;
140 }
141 let gitignore = parent.join(".gitignore");
142 if !gitignore.exists()
143 && let Err(e) = std::fs::write(&gitignore, "# Automatically created by dlin\n*\n")
144 {
145 crate::warn!("could not create {}: {}", gitignore.display(), e);
146 }
147 }
148 match serde_json::to_string(&cf) {
149 Ok(json) => {
150 if let Err(e) = std::fs::write(cache_path, json) {
151 crate::warn!("could not write cache file {}: {}", cache_path.display(), e);
152 }
153 }
154 Err(e) => {
155 crate::warn!("could not serialize manifest graph cache: {}", e);
156 }
157 }
158 }
159}
160
161struct FileStat {
162 mtime_secs: u64,
163 mtime_nanos: u32,
164 file_size: u64,
165 content_hash: u64,
166}
167
168fn file_stat(path: &Path) -> Option<FileStat> {
169 let meta = std::fs::metadata(path).ok()?;
170 let content = std::fs::read(path).ok()?;
171 let mtime_secs = meta
172 .modified()
173 .ok()?
174 .duration_since(SystemTime::UNIX_EPOCH)
175 .ok()?
176 .as_secs();
177 let mtime_nanos = meta
178 .modified()
179 .ok()?
180 .duration_since(SystemTime::UNIX_EPOCH)
181 .ok()?
182 .subsec_nanos();
183 Some(FileStat {
184 mtime_secs,
185 mtime_nanos,
186 file_size: meta.len(),
187 content_hash: hash_bytes(&content),
188 })
189}
190
191fn manifest_identity(path: &Path) -> String {
192 path.canonicalize()
193 .unwrap_or_else(|_| path.to_path_buf())
194 .to_string_lossy()
195 .to_string()
196}
197
198fn hash_bytes(bytes: &[u8]) -> u64 {
199 const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
200 const FNV_PRIME: u64 = 0x100000001b3;
201 let mut hash = FNV_OFFSET_BASIS;
202 for &b in bytes {
203 hash ^= b as u64;
204 hash = hash.wrapping_mul(FNV_PRIME);
205 }
206 hash
207}