Skip to main content

st/
m8_format_converter.rs

1// M8 Format Converter - "No more format confusion!" 🔄
2// Convert between .m8 (binary wave), .m8j (JSON), and .m8z (compressed)
3// "One format to rule them all? Nah, let's have options!" - Hue
4
5use anyhow::{Context, Result};
6use std::fs;
7use std::io::{Read, Write};
8use std::path::Path;
9
10/// Supported M8 formats
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum M8Format {
13    /// Binary wave-based format (the REAL .m8)
14    Binary,
15
16    /// JSON context format (.m8j)
17    Json,
18
19    /// Compressed format (.m8z)
20    Compressed,
21
22    /// Marqant quantum-compressed (.mq)
23    Marqant,
24}
25
26impl M8Format {
27    /// Detect format from file extension
28    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    /// Detect format from file content
44    pub fn detect_from_content(data: &[u8]) -> Self {
45        // Check magic bytes
46        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            // zlib compressed
52            M8Format::Compressed
53        } else if data.starts_with(b"MARQANT") {
54            M8Format::Marqant
55        } else {
56            // Default to JSON
57            M8Format::Json
58        }
59    }
60
61    /// Get recommended extension
62    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
72/// Convert between M8 formats
73pub struct M8Converter;
74
75impl M8Converter {
76    /// Convert any M8 format to another
77    pub fn convert(
78        input_path: &Path,
79        output_path: &Path,
80        target_format: Option<M8Format>,
81    ) -> Result<()> {
82        // Read input file
83        let data = fs::read(input_path)?;
84
85        // Detect source format
86        let source_format = M8Format::detect_from_content(&data);
87
88        // Determine target format
89        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        // Perform conversion
95        match (source_format, target) {
96            // Same format - just copy
97            (a, b) if a == b => {
98                fs::copy(input_path, output_path)?;
99            }
100
101            // JSON to Binary
102            (M8Format::Json, M8Format::Binary) => {
103                Self::json_to_binary(&data, output_path)?;
104            }
105
106            // Binary to JSON
107            (M8Format::Binary, M8Format::Json) => {
108                Self::binary_to_json(&data, output_path)?;
109            }
110
111            // Compressed to anything - decompress first
112            (M8Format::Compressed, target) => {
113                let decompressed = Self::decompress(&data)?;
114                let temp_format = M8Format::detect_from_content(&decompressed);
115
116                // Recursive call with decompressed data
117                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            // Anything to Compressed
124            (_, M8Format::Compressed) => {
125                Self::compress(&data, output_path)?;
126            }
127
128            // Marqant conversions
129            (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    /// Convert JSON to binary wave format
150    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        // Handle different JSON structures
159        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            // Single object
173            let content = serde_json::to_vec(&value)?;
174            m8_file.append_block(&content, 1.0)?;
175        }
176
177        Ok(())
178    }
179
180    /// Convert binary to JSON
181    fn binary_to_json(binary_data: &[u8], output_path: &Path) -> Result<()> {
182        use crate::mem8_binary::M8BinaryFile;
183
184        // Create temporary file from data
185        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        // Read all blocks
192        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        // Create JSON structure
199        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    /// Compress data with zlib
212    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    /// Decompress zlib data
225    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    /// Convert Marqant to JSON
236    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    /// Convert JSON to Marqant
252    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            // Convert JSON to markdown representation
262            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    /// Batch convert all files in a directory
272    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
308/// Fix all misnamed .m8 files in the system
309pub 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                // Read first few bytes to detect format
333                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                    // Rename file with correct extension
341                    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}