Skip to main content

lit/storage/
binary_index.rs

1use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs;
5use std::io::{Cursor, Read};
6use std::path::Path;
7
8/// Magic header for binary index files
9const BINARY_INDEX_MAGIC: &[u8; 4] = b"LITX";
10/// Binary index version
11const BINARY_INDEX_VERSION: u32 = 1;
12
13/// Binary index entry — fixed-format, fast load/save replacement for JSON index
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct BinaryIndexEntry {
16    pub path: String,
17    pub hash: String,
18    pub mode: String,
19    /// File size in bytes
20    pub size: u64,
21    /// Modification timestamp (unix epoch)
22    pub mtime: i64,
23}
24
25/// Binary index — high-performance staging area
26#[derive(Debug, Clone)]
27pub struct BinaryIndex {
28    pub entries: HashMap<String, BinaryIndexEntry>,
29}
30
31impl Default for BinaryIndex {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37impl BinaryIndex {
38    pub fn new() -> Self {
39        BinaryIndex {
40            entries: HashMap::new(),
41        }
42    }
43
44    /// Add or update an entry
45    pub fn add(&mut self, path: String, hash: String, mode: String, size: u64, mtime: i64) {
46        self.entries.insert(
47            path.clone(),
48            BinaryIndexEntry {
49                path,
50                hash,
51                mode,
52                size,
53                mtime,
54            },
55        );
56    }
57
58    /// Remove an entry
59    pub fn remove(&mut self, path: &str) -> Option<BinaryIndexEntry> {
60        self.entries.remove(path)
61    }
62
63    /// Get sorted entries
64    pub fn sorted_entries(&self) -> Vec<&BinaryIndexEntry> {
65        let mut entries: Vec<&BinaryIndexEntry> = self.entries.values().collect();
66        entries.sort_by(|a, b| a.path.cmp(&b.path));
67        entries
68    }
69
70    /// Save to binary format
71    ///
72    /// Format:
73    ///   Header: LITX(4) + version(4) + entry_count(4)
74    ///   Each entry:
75    ///     path_len(4) + path_bytes + hash_len(4) + hash_bytes +
76    ///     mode_len(2) + mode_bytes + size(8) + mtime(8)
77    pub fn save(&self, path: &Path) -> Result<(), String> {
78        let mut buf: Vec<u8> = Vec::new();
79
80        // Header
81        buf.extend_from_slice(BINARY_INDEX_MAGIC);
82        buf.write_u32::<BigEndian>(BINARY_INDEX_VERSION)
83            .map_err(|e| format!("Write error: {}", e))?;
84        buf.write_u32::<BigEndian>(self.entries.len() as u32)
85            .map_err(|e| format!("Write error: {}", e))?;
86
87        // Write entries sorted by path for deterministic output
88        let sorted = self.sorted_entries();
89        for entry in &sorted {
90            // path
91            let path_bytes = entry.path.as_bytes();
92            buf.write_u32::<BigEndian>(path_bytes.len() as u32)
93                .map_err(|e| format!("Write error: {}", e))?;
94            buf.extend_from_slice(path_bytes);
95
96            // hash
97            let hash_bytes = entry.hash.as_bytes();
98            buf.write_u32::<BigEndian>(hash_bytes.len() as u32)
99                .map_err(|e| format!("Write error: {}", e))?;
100            buf.extend_from_slice(hash_bytes);
101
102            // mode
103            let mode_bytes = entry.mode.as_bytes();
104            buf.write_u16::<BigEndian>(mode_bytes.len() as u16)
105                .map_err(|e| format!("Write error: {}", e))?;
106            buf.extend_from_slice(mode_bytes);
107
108            // size + mtime
109            buf.write_u64::<BigEndian>(entry.size)
110                .map_err(|e| format!("Write error: {}", e))?;
111            buf.write_i64::<BigEndian>(entry.mtime)
112                .map_err(|e| format!("Write error: {}", e))?;
113        }
114
115        fs::write(path, &buf).map_err(|e| format!("Failed to write binary index: {}", e))
116    }
117
118    /// Load from binary format
119    pub fn load(path: &Path) -> Result<Self, String> {
120        if !path.exists() {
121            return Ok(BinaryIndex::new());
122        }
123
124        let data = fs::read(path).map_err(|e| format!("Failed to read binary index: {}", e))?;
125        let mut cursor = Cursor::new(&data);
126
127        // Verify header
128        let mut magic = [0u8; 4];
129        cursor
130            .read_exact(&mut magic)
131            .map_err(|e| format!("Read error: {}", e))?;
132        if &magic != BINARY_INDEX_MAGIC {
133            return Err("Invalid binary index magic".to_string());
134        }
135
136        let version = cursor
137            .read_u32::<BigEndian>()
138            .map_err(|e| format!("Read error: {}", e))?;
139        if version != BINARY_INDEX_VERSION {
140            return Err(format!("Unsupported binary index version: {}", version));
141        }
142
143        let count = cursor
144            .read_u32::<BigEndian>()
145            .map_err(|e| format!("Read error: {}", e))? as usize;
146
147        let mut entries = HashMap::with_capacity(count);
148
149        for _ in 0..count {
150            // path
151            let path_len = cursor
152                .read_u32::<BigEndian>()
153                .map_err(|e| format!("Read error: {}", e))? as usize;
154            let mut path_buf = vec![0u8; path_len];
155            cursor
156                .read_exact(&mut path_buf)
157                .map_err(|e| format!("Read error: {}", e))?;
158            let path_str =
159                String::from_utf8(path_buf).map_err(|e| format!("Invalid path UTF-8: {}", e))?;
160
161            // hash
162            let hash_len = cursor
163                .read_u32::<BigEndian>()
164                .map_err(|e| format!("Read error: {}", e))? as usize;
165            let mut hash_buf = vec![0u8; hash_len];
166            cursor
167                .read_exact(&mut hash_buf)
168                .map_err(|e| format!("Read error: {}", e))?;
169            let hash_str =
170                String::from_utf8(hash_buf).map_err(|e| format!("Invalid hash UTF-8: {}", e))?;
171
172            // mode
173            let mode_len = cursor
174                .read_u16::<BigEndian>()
175                .map_err(|e| format!("Read error: {}", e))? as usize;
176            let mut mode_buf = vec![0u8; mode_len];
177            cursor
178                .read_exact(&mut mode_buf)
179                .map_err(|e| format!("Read error: {}", e))?;
180            let mode_str =
181                String::from_utf8(mode_buf).map_err(|e| format!("Invalid mode UTF-8: {}", e))?;
182
183            // size + mtime
184            let size = cursor
185                .read_u64::<BigEndian>()
186                .map_err(|e| format!("Read error: {}", e))?;
187            let mtime = cursor
188                .read_i64::<BigEndian>()
189                .map_err(|e| format!("Read error: {}", e))?;
190
191            entries.insert(
192                path_str.clone(),
193                BinaryIndexEntry {
194                    path: path_str,
195                    hash: hash_str,
196                    mode: mode_str,
197                    size,
198                    mtime,
199                },
200            );
201        }
202
203        Ok(BinaryIndex { entries })
204    }
205
206    /// Convert from the JSON-based Index
207    pub fn from_json_index(index: &crate::storage::Index) -> Self {
208        let mut binary = BinaryIndex::new();
209        for entry in index.entries.values() {
210            binary.add(
211                entry.path.clone(),
212                entry.hash.clone(),
213                entry.mode.clone(),
214                0, // size not tracked in old format
215                0, // mtime not tracked in old format
216            );
217        }
218        binary
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use tempfile::TempDir;
226
227    #[test]
228    fn test_binary_index_roundtrip() {
229        let temp_dir = TempDir::new().unwrap();
230        let index_path = temp_dir.path().join("index.bin");
231
232        let mut index = BinaryIndex::new();
233        index.add(
234            "src/main.rs".to_string(),
235            "abc123def456".to_string(),
236            "100644".to_string(),
237            1024,
238            1700000000,
239        );
240        index.add(
241            "README.md".to_string(),
242            "789xyz".to_string(),
243            "100644".to_string(),
244            256,
245            1700000100,
246        );
247
248        index.save(&index_path).unwrap();
249        let loaded = BinaryIndex::load(&index_path).unwrap();
250
251        assert_eq!(loaded.entries.len(), 2);
252        assert!(loaded.entries.contains_key("src/main.rs"));
253        assert!(loaded.entries.contains_key("README.md"));
254        assert_eq!(loaded.entries["src/main.rs"].size, 1024);
255        assert_eq!(loaded.entries["README.md"].mtime, 1700000100);
256    }
257}