1use super::types::{DepGraph, SymbolIndex};
7use std::fs::{self, File};
8use std::io::{BufReader, BufWriter, Write};
9use std::path::{Path, PathBuf};
10use thiserror::Error;
11
12pub const INDEX_DIR: &str = ".infiniloom";
14
15pub const INDEX_FILE: &str = "index.bin";
17pub const GRAPH_FILE: &str = "graph.bin";
18pub const META_FILE: &str = "meta.json";
19pub const CONFIG_FILE: &str = "config.toml";
20
21#[derive(Error, Debug)]
23pub enum StorageError {
24 #[error("IO error: {0}")]
25 Io(#[from] std::io::Error),
26
27 #[error("Serialization error: {0}")]
28 Serialize(#[from] bincode::Error),
29
30 #[error("JSON error: {0}")]
31 Json(#[from] serde_json::Error),
32
33 #[error("Index not found at {0}")]
34 NotFound(PathBuf),
35
36 #[error("Index version mismatch: found {found}, expected {expected}")]
37 VersionMismatch { found: u32, expected: u32 },
38
39 #[error("Invalid index directory: {0}")]
40 InvalidDirectory(String),
41}
42
43#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
45pub struct IndexMeta {
46 pub version: u32,
48 pub repo_name: String,
50 pub commit_hash: Option<String>,
52 pub created_at: u64,
54 pub file_count: usize,
56 pub symbol_count: usize,
58 pub index_size_bytes: u64,
60}
61
62pub struct IndexStorage {
64 index_dir: PathBuf,
66}
67
68impl IndexStorage {
69 pub fn new(repo_root: impl AsRef<Path>) -> Self {
71 Self { index_dir: repo_root.as_ref().join(INDEX_DIR) }
72 }
73
74 pub fn index_dir(&self) -> &Path {
76 &self.index_dir
77 }
78
79 pub fn exists(&self) -> bool {
81 self.index_dir.join(INDEX_FILE).exists() && self.index_dir.join(GRAPH_FILE).exists()
82 }
83
84 pub fn init(&self) -> Result<(), StorageError> {
86 fs::create_dir_all(&self.index_dir)?;
88
89 let gitignore_path = self.index_dir.join(".gitignore");
91 if !gitignore_path.exists() {
92 fs::write(&gitignore_path, "*.tmp\n*.lock\n")?;
93 }
94
95 Ok(())
96 }
97
98 pub fn save_index(&self, index: &SymbolIndex) -> Result<(), StorageError> {
100 self.init()?;
101
102 let path = self.index_dir.join(INDEX_FILE);
103 let tmp_path = self.index_dir.join(format!("{}.tmp", INDEX_FILE));
104
105 let file = File::create(&tmp_path)?;
107 let mut writer = BufWriter::new(file);
108 bincode::serialize_into(&mut writer, index)?;
109 writer.flush()?;
110
111 fs::rename(&tmp_path, &path)?;
113
114 Ok(())
115 }
116
117 pub fn load_index(&self) -> Result<SymbolIndex, StorageError> {
119 let path = self.index_dir.join(INDEX_FILE);
120
121 if !path.exists() {
122 return Err(StorageError::NotFound(path));
123 }
124
125 let file = File::open(&path)?;
126 let reader = BufReader::new(file);
127 let mut index: SymbolIndex = bincode::deserialize_from(reader)?;
128
129 if index.version != SymbolIndex::CURRENT_VERSION {
131 return Err(StorageError::VersionMismatch {
132 found: index.version,
133 expected: SymbolIndex::CURRENT_VERSION,
134 });
135 }
136
137 index.rebuild_lookups();
139
140 Ok(index)
141 }
142
143 pub fn save_graph(&self, graph: &DepGraph) -> Result<(), StorageError> {
145 self.init()?;
146
147 let path = self.index_dir.join(GRAPH_FILE);
148 let tmp_path = self.index_dir.join(format!("{}.tmp", GRAPH_FILE));
149
150 let file = File::create(&tmp_path)?;
151 let mut writer = BufWriter::new(file);
152 bincode::serialize_into(&mut writer, graph)?;
153 writer.flush()?;
154
155 fs::rename(&tmp_path, &path)?;
156
157 Ok(())
158 }
159
160 pub fn load_graph(&self) -> Result<DepGraph, StorageError> {
162 let path = self.index_dir.join(GRAPH_FILE);
163
164 if !path.exists() {
165 return Err(StorageError::NotFound(path));
166 }
167
168 let file = File::open(&path)?;
169 let reader = BufReader::new(file);
170 let graph: DepGraph = bincode::deserialize_from(reader)?;
171
172 Ok(graph)
173 }
174
175 pub fn save_meta(&self, meta: &IndexMeta) -> Result<(), StorageError> {
177 self.init()?;
178
179 let path = self.index_dir.join(META_FILE);
180 let json = serde_json::to_string_pretty(meta)?;
181 fs::write(&path, json)?;
182
183 Ok(())
184 }
185
186 pub fn load_meta(&self) -> Result<IndexMeta, StorageError> {
188 let path = self.index_dir.join(META_FILE);
189
190 if !path.exists() {
191 return Err(StorageError::NotFound(path));
192 }
193
194 let content = fs::read_to_string(&path)?;
195 let meta: IndexMeta = serde_json::from_str(&content)?;
196
197 Ok(meta)
198 }
199
200 pub fn save_all(
202 &self,
203 index: &SymbolIndex,
204 graph: &DepGraph,
205 ) -> Result<IndexMeta, StorageError> {
206 self.save_index(index)?;
208 self.save_graph(graph)?;
209
210 let index_size = fs::metadata(self.index_dir.join(INDEX_FILE))?.len();
212 let graph_size = fs::metadata(self.index_dir.join(GRAPH_FILE))?.len();
213
214 let meta = IndexMeta {
216 version: index.version,
217 repo_name: index.repo_name.clone(),
218 commit_hash: index.commit_hash.clone(),
219 created_at: index.created_at,
220 file_count: index.files.len(),
221 symbol_count: index.symbols.len(),
222 index_size_bytes: index_size + graph_size,
223 };
224
225 self.save_meta(&meta)?;
226
227 Ok(meta)
228 }
229
230 pub fn load_all(&self) -> Result<(SymbolIndex, DepGraph), StorageError> {
232 let index = self.load_index()?;
233 let graph = self.load_graph()?;
234 Ok((index, graph))
235 }
236
237 pub fn storage_size(&self) -> u64 {
239 let mut total = 0u64;
240
241 for name in [INDEX_FILE, GRAPH_FILE, META_FILE] {
242 if let Ok(metadata) = fs::metadata(self.index_dir.join(name)) {
243 total += metadata.len();
244 }
245 }
246
247 total
248 }
249
250 pub fn delete(&self) -> Result<(), StorageError> {
252 if self.index_dir.exists() {
253 fs::remove_dir_all(&self.index_dir)?;
254 }
255 Ok(())
256 }
257}
258
259#[cfg(test)]
264mod tests {
265 use super::*;
266 use crate::index::types::{
267 FileEntry, FileId, IndexSymbol, IndexSymbolKind, Language, Span, SymbolId, Visibility,
268 };
269 use tempfile::TempDir;
270
271 #[test]
272 fn test_storage_roundtrip() {
273 let tmp = TempDir::new().unwrap();
274 let storage = IndexStorage::new(tmp.path());
275
276 let mut index = SymbolIndex::new();
278 index.repo_name = "test-repo".to_owned();
279 index.created_at = 12345;
280 index.files.push(FileEntry {
281 id: FileId::new(0),
282 path: "src/main.rs".to_owned(),
283 language: Language::Rust,
284 content_hash: [1; 32],
285 symbols: 0..1,
286 imports: vec![],
287 lines: 100,
288 tokens: 500,
289 });
290 index.symbols.push(IndexSymbol {
291 id: SymbolId::new(0),
292 name: "main".to_owned(),
293 kind: IndexSymbolKind::Function,
294 file_id: FileId::new(0),
295 span: Span::new(1, 0, 10, 0),
296 signature: Some("fn main()".to_owned()),
297 parent: None,
298 visibility: Visibility::Public,
299 docstring: None,
300 });
301
302 let mut graph = DepGraph::new();
304 graph.add_file_import(0, 1);
305
306 storage.save_all(&index, &graph).unwrap();
308
309 assert!(storage.exists());
311 assert!(storage.storage_size() > 0);
312
313 let (loaded_index, loaded_graph) = storage.load_all().unwrap();
315 assert_eq!(loaded_index.repo_name, "test-repo");
316 assert_eq!(loaded_index.files.len(), 1);
317 assert_eq!(loaded_index.symbols.len(), 1);
318 assert_eq!(loaded_graph.file_imports.len(), 1);
319
320 assert!(loaded_index.get_file("src/main.rs").is_some());
322 }
323
324 #[test]
325 fn test_meta_roundtrip() {
326 let tmp = TempDir::new().unwrap();
327 let storage = IndexStorage::new(tmp.path());
328 storage.init().unwrap();
329
330 let meta = IndexMeta {
331 version: 1,
332 repo_name: "test".to_owned(),
333 commit_hash: Some("abc123".to_owned()),
334 created_at: 12345,
335 file_count: 10,
336 symbol_count: 100,
337 index_size_bytes: 1024,
338 };
339
340 storage.save_meta(&meta).unwrap();
341 let loaded = storage.load_meta().unwrap();
342
343 assert_eq!(loaded.repo_name, "test");
344 assert_eq!(loaded.file_count, 10);
345 }
346
347 #[test]
348 fn test_not_found() {
349 let tmp = TempDir::new().unwrap();
350 let storage = IndexStorage::new(tmp.path());
351
352 assert!(!storage.exists());
353 assert!(matches!(storage.load_index(), Err(StorageError::NotFound(_))));
354 }
355}