st/
m8_format_converter.rs1use anyhow::{Context, Result};
6use std::fs;
7use std::io::{Read, Write};
8use std::path::Path;
9
10#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum M8Format {
13 Binary,
15
16 Json,
18
19 Compressed,
21
22 Marqant,
24}
25
26impl M8Format {
27 pub fn from_extension(path: &Path) -> Result<Self> {
29 let ext = path
30 .extension()
31 .and_then(|e| e.to_str())
32 .context("No file extension")?;
33
34 match ext {
35 "m8" => Ok(M8Format::Binary),
36 "m8j" => Ok(M8Format::Json),
37 "m8z" => Ok(M8Format::Compressed),
38 "mq" => Ok(M8Format::Marqant),
39 _ => anyhow::bail!("Unknown M8 format: .{}", ext),
40 }
41 }
42
43 pub fn detect_from_content(data: &[u8]) -> Self {
45 if data.starts_with(b"MEM8") {
47 M8Format::Binary
48 } else if data.starts_with(b"{") || data.starts_with(b"[") {
49 M8Format::Json
50 } else if data.starts_with(b"\x78\x9c") || data.starts_with(b"\x78\xda") {
51 M8Format::Compressed
53 } else if data.starts_with(b"MARQANT") {
54 M8Format::Marqant
55 } else {
56 M8Format::Json
58 }
59 }
60
61 pub fn extension(&self) -> &str {
63 match self {
64 M8Format::Binary => "m8",
65 M8Format::Json => "m8j",
66 M8Format::Compressed => "m8z",
67 M8Format::Marqant => "mq",
68 }
69 }
70}
71
72pub struct M8Converter;
74
75impl M8Converter {
76 pub fn convert(
78 input_path: &Path,
79 output_path: &Path,
80 target_format: Option<M8Format>,
81 ) -> Result<()> {
82 let data = fs::read(input_path)?;
84
85 let source_format = M8Format::detect_from_content(&data);
87
88 let target = target_format
90 .unwrap_or_else(|| M8Format::from_extension(output_path).unwrap_or(M8Format::Binary));
91
92 println!("🔄 Converting {:?} -> {:?}", source_format, target);
93
94 match (source_format, target) {
96 (a, b) if a == b => {
98 fs::copy(input_path, output_path)?;
99 }
100
101 (M8Format::Json, M8Format::Binary) => {
103 Self::json_to_binary(&data, output_path)?;
104 }
105
106 (M8Format::Binary, M8Format::Json) => {
108 Self::binary_to_json(&data, output_path)?;
109 }
110
111 (M8Format::Compressed, target) => {
113 let decompressed = Self::decompress(&data)?;
114 let temp_format = M8Format::detect_from_content(&decompressed);
115
116 let temp_path = format!("/tmp/temp.{}", temp_format.extension());
118 fs::write(&temp_path, decompressed)?;
119 Self::convert(Path::new(&temp_path), output_path, Some(target))?;
120 fs::remove_file(&temp_path)?;
121 }
122
123 (_, M8Format::Compressed) => {
125 Self::compress(&data, output_path)?;
126 }
127
128 (M8Format::Marqant, M8Format::Json) => {
130 Self::marqant_to_json(&data, output_path)?;
131 }
132 (M8Format::Json, M8Format::Marqant) => {
133 Self::json_to_marqant(&data, output_path)?;
134 }
135
136 _ => {
137 anyhow::bail!(
138 "Conversion from {:?} to {:?} not yet implemented",
139 source_format,
140 target
141 );
142 }
143 }
144
145 println!("✅ Conversion complete: {}", output_path.display());
146 Ok(())
147 }
148
149 fn json_to_binary(json_data: &[u8], output_path: &Path) -> Result<()> {
151 use crate::mem8_binary::M8BinaryFile;
152
153 let json_str = String::from_utf8_lossy(json_data);
154 let value: serde_json::Value = serde_json::from_str(&json_str)?;
155
156 let mut m8_file = M8BinaryFile::create(output_path)?;
157
158 if let Some(contexts) = value.get("contexts").and_then(|c| c.as_array()) {
160 for context in contexts {
161 let content = serde_json::to_vec(context)?;
162 let importance =
163 context.get("score").and_then(|s| s.as_f64()).unwrap_or(0.5) as f32;
164 m8_file.append_block(&content, importance)?;
165 }
166 } else if let Some(array) = value.as_array() {
167 for item in array {
168 let content = serde_json::to_vec(item)?;
169 m8_file.append_block(&content, 0.5)?;
170 }
171 } else {
172 let content = serde_json::to_vec(&value)?;
174 m8_file.append_block(&content, 1.0)?;
175 }
176
177 Ok(())
178 }
179
180 fn binary_to_json(binary_data: &[u8], output_path: &Path) -> Result<()> {
182 use crate::mem8_binary::M8BinaryFile;
183
184 let temp_path = "/tmp/temp_convert.m8";
186 fs::write(temp_path, binary_data)?;
187
188 let mut m8_file = M8BinaryFile::open(temp_path)?;
189 let mut contexts = Vec::new();
190
191 while let Some(block) = m8_file.read_backwards()? {
193 if let Ok(json) = serde_json::from_slice::<serde_json::Value>(&block.content) {
194 contexts.push(json);
195 }
196 }
197
198 let output = serde_json::json!({
200 "format": "m8j",
201 "version": 1,
202 "contexts": contexts
203 });
204
205 fs::write(output_path, serde_json::to_string_pretty(&output)?)?;
206 fs::remove_file(temp_path)?;
207
208 Ok(())
209 }
210
211 fn compress(data: &[u8], output_path: &Path) -> Result<()> {
213 use flate2::write::ZlibEncoder;
214 use flate2::Compression;
215
216 let file = fs::File::create(output_path)?;
217 let mut encoder = ZlibEncoder::new(file, Compression::default());
218 encoder.write_all(data)?;
219 encoder.finish()?;
220
221 Ok(())
222 }
223
224 fn decompress(data: &[u8]) -> Result<Vec<u8>> {
226 use flate2::read::ZlibDecoder;
227
228 let mut decoder = ZlibDecoder::new(data);
229 let mut decompressed = Vec::new();
230 decoder.read_to_end(&mut decompressed)?;
231
232 Ok(decompressed)
233 }
234
235 fn marqant_to_json(mq_data: &[u8], output_path: &Path) -> Result<()> {
237 use crate::formatters::marqant::MarqantFormatter;
238
239 let mq_str = String::from_utf8_lossy(mq_data);
240 let markdown = MarqantFormatter::decompress_marqant(&mq_str)?;
241
242 let json = serde_json::json!({
243 "format": "markdown",
244 "content": markdown
245 });
246
247 fs::write(output_path, serde_json::to_string_pretty(&json)?)?;
248 Ok(())
249 }
250
251 fn json_to_marqant(json_data: &[u8], output_path: &Path) -> Result<()> {
253 use crate::formatters::marqant::MarqantFormatter;
254
255 let json_str = String::from_utf8_lossy(json_data);
256 let value: serde_json::Value = serde_json::from_str(&json_str)?;
257
258 let markdown = if let Some(content) = value.get("content").and_then(|c| c.as_str()) {
259 content.to_string()
260 } else {
261 format!("```json\n{}\n```", serde_json::to_string_pretty(&value)?)
263 };
264
265 let compressed = MarqantFormatter::compress_markdown(&markdown)?;
266 fs::write(output_path, compressed)?;
267
268 Ok(())
269 }
270
271 pub fn convert_directory(
273 input_dir: &Path,
274 output_dir: &Path,
275 target_format: M8Format,
276 ) -> Result<()> {
277 fs::create_dir_all(output_dir)?;
278
279 for entry in fs::read_dir(input_dir)? {
280 let entry = entry?;
281 let path = entry.path();
282
283 if path.is_file() {
284 if let Ok(_format) = M8Format::from_extension(&path) {
285 let file_name = path
286 .file_stem()
287 .and_then(|s| s.to_str())
288 .unwrap_or("unknown");
289
290 let output_path =
291 output_dir.join(format!("{}.{}", file_name, target_format.extension()));
292
293 println!(
294 "Converting: {} -> {}",
295 path.display(),
296 output_path.display()
297 );
298
299 Self::convert(&path, &output_path, Some(target_format))?;
300 }
301 }
302 }
303
304 Ok(())
305 }
306}
307
308pub fn fix_m8_extensions() -> Result<()> {
310 println!("🔧 Fixing .m8 file extensions...");
311
312 let dirs = [
313 "~/.mem8",
314 "~/.mem8/projects",
315 "~/.mem8/users",
316 "~/.mem8/llms",
317 ];
318
319 for dir in &dirs {
320 let expanded = shellexpand::tilde(dir);
321 let path = Path::new(expanded.as_ref());
322
323 if !path.exists() {
324 continue;
325 }
326
327 for entry in fs::read_dir(path)? {
328 let entry = entry?;
329 let file_path = entry.path();
330
331 if file_path.extension().and_then(|e| e.to_str()) == Some("m8") {
332 let mut file = fs::File::open(&file_path)?;
334 let mut buffer = [0u8; 16];
335 file.read_exact(&mut buffer)?;
336
337 let detected = M8Format::detect_from_content(&buffer);
338
339 if detected != M8Format::Binary {
340 let new_path = file_path.with_extension(detected.extension());
342 println!(
343 " Renaming: {} -> {}",
344 file_path.display(),
345 new_path.display()
346 );
347 fs::rename(&file_path, &new_path)?;
348 }
349 }
350 }
351 }
352
353 println!("✅ Extension fix complete!");
354 Ok(())
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
362 fn test_format_detection() {
363 assert_eq!(M8Format::detect_from_content(b"MEM8"), M8Format::Binary);
364 assert_eq!(
365 M8Format::detect_from_content(b"{\"test\":1}"),
366 M8Format::Json
367 );
368 assert_eq!(
369 M8Format::detect_from_content(b"\x78\x9c"),
370 M8Format::Compressed
371 );
372 assert_eq!(M8Format::detect_from_content(b"MARQANT"), M8Format::Marqant);
373 }
374
375 #[test]
376 fn test_extension_mapping() {
377 let path = Path::new("test.m8");
378 assert_eq!(M8Format::from_extension(path).unwrap(), M8Format::Binary);
379
380 let path = Path::new("test.m8j");
381 assert_eq!(M8Format::from_extension(path).unwrap(), M8Format::Json);
382 }
383}