lit/storage/
binary_index.rs1use 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
8const BINARY_INDEX_MAGIC: &[u8; 4] = b"LITX";
10const BINARY_INDEX_VERSION: u32 = 1;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct BinaryIndexEntry {
16 pub path: String,
17 pub hash: String,
18 pub mode: String,
19 pub size: u64,
21 pub mtime: i64,
23}
24
25#[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 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 pub fn remove(&mut self, path: &str) -> Option<BinaryIndexEntry> {
60 self.entries.remove(path)
61 }
62
63 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 pub fn save(&self, path: &Path) -> Result<(), String> {
78 let mut buf: Vec<u8> = Vec::new();
79
80 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 let sorted = self.sorted_entries();
89 for entry in &sorted {
90 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 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 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 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 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 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 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 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 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 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 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, 0, );
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}