1use std::fs;
8use std::io::{BufReader, BufWriter, Read, Write};
9use std::path::Path;
10
11const MAGIC: &[u8; 4] = b"MSEX";
12const VERSION: u32 = 1;
13
14const INDEX_FILES: &[&str] = &[
16 "meta.bin",
17 "microscope.bin",
18 "data.bin",
19 "merkle.bin",
20 "append.bin",
21 "embeddings.bin",
22 "fingerprints.idx",
23 "links.bin",
24 "emotions.bin",
25];
26
27pub fn export(output_dir: &Path, archive_path: &Path) -> Result<(), String> {
29 let mut files: Vec<(String, Vec<u8>)> = Vec::new();
30
31 for &name in INDEX_FILES {
32 let path = output_dir.join(name);
33 if path.exists() {
34 let data = fs::read(&path).map_err(|e| format!("read {}: {}", name, e))?;
35 files.push((name.to_string(), data));
36 }
37 }
38
39 if files.is_empty() {
40 return Err("no index files found to export".to_string());
41 }
42
43 let tmp_path = archive_path.with_extension("mscope.tmp");
45 let f = fs::File::create(&tmp_path).map_err(|e| format!("create archive: {}", e))?;
46 let mut w = BufWriter::new(f);
47
48 w.write_all(MAGIC).map_err(|e| e.to_string())?;
50 w.write_all(&VERSION.to_le_bytes())
51 .map_err(|e| e.to_string())?;
52 w.write_all(&(files.len() as u32).to_le_bytes())
53 .map_err(|e| e.to_string())?;
54
55 let mut total_size = 12u64; for (name, data) in &files {
58 let name_bytes = name.as_bytes();
59 w.write_all(&(name_bytes.len() as u16).to_le_bytes())
60 .map_err(|e| e.to_string())?;
61 w.write_all(name_bytes).map_err(|e| e.to_string())?;
62 w.write_all(&(data.len() as u64).to_le_bytes())
63 .map_err(|e| e.to_string())?;
64 w.write_all(data).map_err(|e| e.to_string())?;
65 total_size += 2 + name_bytes.len() as u64 + 8 + data.len() as u64;
66 }
67
68 w.flush().map_err(|e| e.to_string())?;
69 drop(w);
70 fs::rename(&tmp_path, archive_path).map_err(|e| format!("rename archive: {}", e))?;
71
72 println!(
73 " Exported {} files ({:.1} KB) → {}",
74 files.len(),
75 total_size as f64 / 1024.0,
76 archive_path.display()
77 );
78 for (name, data) in &files {
79 println!(" {}: {:.1} KB", name, data.len() as f64 / 1024.0);
80 }
81
82 Ok(())
83}
84
85pub fn import(archive_path: &Path, output_dir: &Path) -> Result<(), String> {
87 let f = fs::File::open(archive_path).map_err(|e| format!("open archive: {}", e))?;
88 let mut r = BufReader::new(f);
89
90 let mut magic = [0u8; 4];
92 r.read_exact(&mut magic).map_err(|e| e.to_string())?;
93 if &magic != MAGIC {
94 return Err(format!("invalid magic: expected MSEX, got {:?}", magic));
95 }
96
97 let mut ver_buf = [0u8; 4];
98 r.read_exact(&mut ver_buf).map_err(|e| e.to_string())?;
99 let version = u32::from_le_bytes(ver_buf);
100 if version > VERSION {
101 return Err(format!(
102 "unsupported version: {} (max: {})",
103 version, VERSION
104 ));
105 }
106
107 let mut count_buf = [0u8; 4];
108 r.read_exact(&mut count_buf).map_err(|e| e.to_string())?;
109 let file_count = u32::from_le_bytes(count_buf);
110
111 fs::create_dir_all(output_dir).map_err(|e| format!("create output dir: {}", e))?;
112
113 for _ in 0..file_count {
114 let mut name_len_buf = [0u8; 2];
116 r.read_exact(&mut name_len_buf).map_err(|e| e.to_string())?;
117 let name_len = u16::from_le_bytes(name_len_buf) as usize;
118 let mut name_buf = vec![0u8; name_len];
119 r.read_exact(&mut name_buf).map_err(|e| e.to_string())?;
120 let name = String::from_utf8(name_buf).map_err(|e| e.to_string())?;
121
122 let mut data_len_buf = [0u8; 8];
124 r.read_exact(&mut data_len_buf).map_err(|e| e.to_string())?;
125 let data_len = u64::from_le_bytes(data_len_buf) as usize;
126 let mut data = vec![0u8; data_len];
127 r.read_exact(&mut data).map_err(|e| e.to_string())?;
128
129 if !INDEX_FILES.contains(&name.as_str()) {
131 println!(" Skipping unknown file: {}", name);
132 continue;
133 }
134
135 let out_path = output_dir.join(&name);
136 let out_tmp = output_dir.join(format!("{}.tmp", name));
137 fs::write(&out_tmp, &data).map_err(|e| format!("write {}: {}", name, e))?;
138 fs::rename(&out_tmp, &out_path).map_err(|e| format!("rename {}: {}", name, e))?;
139 println!(" {}: {:.1} KB", name, data.len() as f64 / 1024.0);
140 }
141
142 println!(" Imported {} files → {}", file_count, output_dir.display());
143 Ok(())
144}
145
146pub fn diff(a_path: &Path, b_path: &Path) -> Result<(), String> {
148 let a_files = read_archive(a_path)?;
149 let b_files = read_archive(b_path)?;
150
151 println!(" {} vs {}", a_path.display(), b_path.display());
152
153 let a_root = extract_merkle_root(&a_files);
155 let b_root = extract_merkle_root(&b_files);
156 match (a_root, b_root) {
157 (Some(ar), Some(br)) => {
158 if ar == br {
159 println!(" Merkle root: {} (identical)", hex_str(&ar));
160 } else {
161 println!(" Merkle root A: {}", hex_str(&ar));
162 println!(" Merkle root B: {}", hex_str(&br));
163 println!(" DIFF Merkle roots differ — data changed");
164 }
165 }
166 _ => println!(" (cannot compare Merkle roots — meta.bin missing)"),
167 }
168
169 let all_names: std::collections::BTreeSet<&str> = a_files
171 .keys()
172 .chain(b_files.keys())
173 .map(|s| s.as_str())
174 .collect();
175
176 for name in all_names {
177 let a_size = a_files.get(name).map(|d| d.len());
178 let b_size = b_files.get(name).map(|d| d.len());
179 match (a_size, b_size) {
180 (Some(a), Some(b)) => {
181 let delta = b as i64 - a as i64;
182 let sign = if delta >= 0 { "+" } else { "" };
183 let status = if a == b { "=" } else { "~" };
184 println!(
185 " {} {}: {} → {} ({}{} bytes)",
186 status, name, a, b, sign, delta
187 );
188 }
189 (Some(a), None) => println!(" - {}: {} (removed)", name, a),
190 (None, Some(b)) => println!(" + {}: {} (added)", name, b),
191 (None, None) => {}
192 }
193 }
194
195 let a_blocks = extract_block_count(&a_files);
197 let b_blocks = extract_block_count(&b_files);
198 if let (Some(a), Some(b)) = (a_blocks, b_blocks) {
199 println!(
200 " Blocks: {} → {} ({}{})",
201 a,
202 b,
203 if b >= a { "+" } else { "" },
204 b as i64 - a as i64
205 );
206 }
207
208 Ok(())
209}
210
211fn read_archive(path: &Path) -> Result<std::collections::HashMap<String, Vec<u8>>, String> {
212 let f = fs::File::open(path).map_err(|e| format!("open {}: {}", path.display(), e))?;
213 let mut r = BufReader::new(f);
214 let mut magic = [0u8; 4];
215 r.read_exact(&mut magic).map_err(|e| e.to_string())?;
216 if &magic != MAGIC {
217 return Err(format!("invalid magic in {}", path.display()));
218 }
219
220 let mut ver_buf = [0u8; 4];
221 r.read_exact(&mut ver_buf).map_err(|e| e.to_string())?;
222 let mut count_buf = [0u8; 4];
223 r.read_exact(&mut count_buf).map_err(|e| e.to_string())?;
224 let file_count = u32::from_le_bytes(count_buf);
225
226 let mut files = std::collections::HashMap::new();
227 for _ in 0..file_count {
228 let mut name_len_buf = [0u8; 2];
229 r.read_exact(&mut name_len_buf).map_err(|e| e.to_string())?;
230 let name_len = u16::from_le_bytes(name_len_buf) as usize;
231 let mut name_buf = vec![0u8; name_len];
232 r.read_exact(&mut name_buf).map_err(|e| e.to_string())?;
233 let name = String::from_utf8(name_buf).map_err(|e| e.to_string())?;
234
235 let mut data_len_buf = [0u8; 8];
236 r.read_exact(&mut data_len_buf).map_err(|e| e.to_string())?;
237 let data_len = u64::from_le_bytes(data_len_buf) as usize;
238 let mut data = vec![0u8; data_len];
239 r.read_exact(&mut data).map_err(|e| e.to_string())?;
240
241 files.insert(name, data);
242 }
243 Ok(files)
244}
245
246fn extract_merkle_root(files: &std::collections::HashMap<String, Vec<u8>>) -> Option<[u8; 32]> {
247 let meta = files.get("meta.bin")?;
248 if meta.len() < 4 || &meta[0..4] != b"MSC2" {
249 return None;
250 }
251 let offset = crate::META_HEADER_SIZE + 9 * crate::DEPTH_ENTRY_SIZE;
252 if meta.len() < offset + 32 {
253 return None;
254 }
255 let mut root = [0u8; 32];
256 root.copy_from_slice(&meta[offset..offset + 32]);
257 Some(root)
258}
259
260fn extract_block_count(files: &std::collections::HashMap<String, Vec<u8>>) -> Option<u32> {
261 let meta = files.get("meta.bin")?;
262 if meta.len() < 12 {
263 return None;
264 }
265 Some(u32::from_le_bytes(meta[8..12].try_into().ok()?))
266}
267
268fn hex_str(bytes: &[u8]) -> String {
269 bytes
270 .iter()
271 .map(|b| format!("{:02x}", b))
272 .collect::<Vec<_>>()
273 .join("")
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 #[test]
280 fn test_export_import_roundtrip() {
281 let dir = std::env::temp_dir().join("mscope_snap_test");
282 let _ = fs::create_dir_all(&dir);
283
284 let src_dir = dir.join("src");
286 let _ = fs::create_dir_all(&src_dir);
287 fs::write(src_dir.join("meta.bin"), b"MSC2testdata1234").unwrap();
288 fs::write(src_dir.join("microscope.bin"), b"headers_here").unwrap();
289 fs::write(src_dir.join("data.bin"), b"block_data_here").unwrap();
290
291 let archive = dir.join("test.mscope");
293 export(&src_dir, &archive).unwrap();
294 assert!(archive.exists());
295
296 let dst_dir = dir.join("dst");
298 import(&archive, &dst_dir).unwrap();
299
300 assert_eq!(
302 fs::read(dst_dir.join("meta.bin")).unwrap(),
303 b"MSC2testdata1234"
304 );
305 assert_eq!(
306 fs::read(dst_dir.join("microscope.bin")).unwrap(),
307 b"headers_here"
308 );
309 assert_eq!(
310 fs::read(dst_dir.join("data.bin")).unwrap(),
311 b"block_data_here"
312 );
313
314 let _ = fs::remove_dir_all(&dir);
315 }
316}