Skip to main content

faf_rust_sdk/binary/
compile.rs

1//! Compile/decompile API for .faf ↔ .fafb conversion
2//!
3//! Provides high-level functions to convert between YAML (.faf) and binary (.fafb) formats.
4
5use std::io::Write;
6
7use super::error::FafbResult;
8use super::header::{FafbHeader, HEADER_SIZE, MAX_FILE_SIZE, MAX_SECTIONS};
9use super::priority::Priority;
10use super::section::{SectionEntry, SectionTable, SECTION_ENTRY_SIZE};
11use super::section_type::SectionType;
12
13/// A decompiled .fafb file with header, section table, and raw data
14#[derive(Debug, Clone)]
15pub struct DecompiledFafb {
16    /// The 32-byte header
17    pub header: FafbHeader,
18    /// Section table with all entries
19    pub section_table: SectionTable,
20    /// Raw file data (for extracting section content)
21    pub data: Vec<u8>,
22}
23
24impl DecompiledFafb {
25    /// Extract the raw bytes for a section entry
26    pub fn section_data(&self, entry: &SectionEntry) -> Option<&[u8]> {
27        let start = entry.offset as usize;
28        let end = start + entry.length as usize;
29        if end <= self.data.len() {
30            Some(&self.data[start..end])
31        } else {
32            None
33        }
34    }
35
36    /// Extract section data as a UTF-8 string
37    pub fn section_string(&self, entry: &SectionEntry) -> Option<String> {
38        self.section_data(entry)
39            .and_then(|bytes| std::str::from_utf8(bytes).ok())
40            .map(|s| s.to_string())
41    }
42
43    /// Get section data by type
44    pub fn get_section(&self, section_type: SectionType) -> Option<&[u8]> {
45        self.section_table
46            .get_by_type(section_type)
47            .and_then(|entry| self.section_data(entry))
48    }
49
50    /// Get section data by type as string
51    pub fn get_section_string(&self, section_type: SectionType) -> Option<String> {
52        self.section_table
53            .get_by_type(section_type)
54            .and_then(|entry| self.section_string(entry))
55    }
56}
57
58/// Compile a .faf YAML source string into .fafb binary bytes.
59///
60/// Extracts sections from the YAML and assembles them into the binary format
61/// with header, section data, and section table.
62///
63/// # Example
64///
65/// ```rust
66/// use faf_rust_sdk::binary::compile::compile;
67///
68/// let yaml = r#"
69/// faf_version: 2.5.0
70/// project:
71///   name: my-project
72///   goal: Build something great
73/// instant_context:
74///   tech_stack: Rust, TypeScript
75///   key_files:
76///     - src/main.rs
77/// "#;
78///
79/// let fafb_bytes = compile(yaml).unwrap();
80/// assert_eq!(&fafb_bytes[0..4], b"FAFB");
81/// ```
82pub fn compile(yaml_source: &str) -> Result<Vec<u8>, String> {
83    let source_bytes = yaml_source.as_bytes();
84    if source_bytes.is_empty() {
85        return Err("Source content is empty".to_string());
86    }
87
88    // Parse as raw YAML value for section extraction
89    let yaml: serde_yaml_ng::Value =
90        serde_yaml_ng::from_str(yaml_source).map_err(|e| format!("Invalid YAML: {}", e))?;
91
92    // Build sections from YAML
93    let mut sections: Vec<(SectionType, Priority, Vec<u8>)> = Vec::new();
94
95    // META section (critical) — project identity
96    let version = yaml
97        .get("faf_version")
98        .and_then(|v| v.as_str())
99        .unwrap_or("2.5.0");
100    let name = yaml
101        .get("project")
102        .and_then(|p| p.get("name"))
103        .and_then(|n| n.as_str())
104        .unwrap_or("unknown");
105    let meta_content = format!("faf_version: {}\nname: {}\n", version, name);
106    sections.push((
107        SectionType::Meta,
108        Priority::critical(),
109        meta_content.into_bytes(),
110    ));
111
112    // TECH_STACK section (high)
113    if let Some(content) = extract_section(&yaml, "tech_stack") {
114        sections.push((
115            SectionType::TechStack,
116            Priority::high(),
117            format!("tech_stack:\n{}", content).into_bytes(),
118        ));
119    }
120    // Also check instant_context.tech_stack
121    if sections
122        .iter()
123        .all(|(t, _, _)| *t != SectionType::TechStack)
124    {
125        if let Some(tech) = yaml
126            .get("instant_context")
127            .and_then(|ic| ic.get("tech_stack"))
128        {
129            if let Ok(content) = serde_yaml_ng::to_string(tech) {
130                if !content.trim().is_empty() {
131                    sections.push((
132                        SectionType::TechStack,
133                        Priority::high(),
134                        format!("tech_stack: {}", content).into_bytes(),
135                    ));
136                }
137            }
138        }
139    }
140
141    // KEY_FILES section (high)
142    if let Some(content) = extract_section(&yaml, "key_files") {
143        sections.push((
144            SectionType::KeyFiles,
145            Priority::high(),
146            format!("key_files:\n{}", content).into_bytes(),
147        ));
148    } else if let Some(kf) = yaml
149        .get("instant_context")
150        .and_then(|ic| ic.get("key_files"))
151    {
152        if let Ok(content) = serde_yaml_ng::to_string(kf) {
153            if !content.trim().is_empty() {
154                sections.push((
155                    SectionType::KeyFiles,
156                    Priority::high(),
157                    format!("key_files:\n{}", content).into_bytes(),
158                ));
159            }
160        }
161    }
162
163    // COMMANDS section (high)
164    if let Some(content) = extract_section(&yaml, "commands") {
165        sections.push((
166            SectionType::Commands,
167            Priority::new(180),
168            format!("commands:\n{}", content).into_bytes(),
169        ));
170    } else if let Some(cmds) = yaml
171        .get("instant_context")
172        .and_then(|ic| ic.get("commands"))
173    {
174        if let Ok(content) = serde_yaml_ng::to_string(cmds) {
175            if !content.trim().is_empty() {
176                sections.push((
177                    SectionType::Commands,
178                    Priority::new(180),
179                    format!("commands:\n{}", content).into_bytes(),
180                ));
181            }
182        }
183    }
184
185    // ARCHITECTURE section (medium)
186    if let Some(content) = extract_section(&yaml, "architecture") {
187        sections.push((
188            SectionType::Architecture,
189            Priority::medium(),
190            format!("architecture:\n{}", content).into_bytes(),
191        ));
192    }
193
194    // CONTEXT section (low)
195    if let Some(content) = extract_section(&yaml, "context") {
196        sections.push((
197            SectionType::Context,
198            Priority::low(),
199            format!("context:\n{}", content).into_bytes(),
200        ));
201    }
202
203    if sections.len() > MAX_SECTIONS as usize {
204        return Err(format!(
205            "Too many sections: {} exceeds maximum {}",
206            sections.len(),
207            MAX_SECTIONS
208        ));
209    }
210
211    // Calculate layout
212    let section_count = sections.len();
213    let section_table_size = section_count * SECTION_ENTRY_SIZE;
214
215    let mut data_offset: u32 = HEADER_SIZE as u32;
216    let mut section_data: Vec<u8> = Vec::new();
217    let mut section_table = SectionTable::new();
218
219    for (section_type, priority, data) in &sections {
220        let entry = SectionEntry::new(*section_type, data_offset, data.len() as u32)
221            .with_priority(*priority);
222        section_table.push(entry);
223        section_data.extend_from_slice(data);
224        data_offset = data_offset
225            .checked_add(data.len() as u32)
226            .ok_or_else(|| "Section data exceeds u32::MAX bytes".to_string())?;
227    }
228
229    let section_table_offset = data_offset;
230    let total_size = section_table_offset
231        .checked_add(section_table_size as u32)
232        .ok_or_else(|| "Total file size exceeds u32::MAX bytes".to_string())?;
233
234    if total_size > MAX_FILE_SIZE {
235        return Err(format!(
236            "Output size {} bytes exceeds maximum {} bytes (10MB)",
237            total_size, MAX_FILE_SIZE
238        ));
239    }
240
241    // Build header
242    let mut header = FafbHeader::with_timestamp();
243    header.set_source_checksum(source_bytes);
244    header.section_count = section_count as u16;
245    header.section_table_offset = section_table_offset;
246    header.total_size = total_size;
247
248    // Assemble binary
249    let mut output: Vec<u8> = Vec::with_capacity(total_size as usize);
250    header.write(&mut output).map_err(|e| e.to_string())?;
251    output.write_all(&section_data).map_err(|e| e.to_string())?;
252    section_table
253        .write(&mut output)
254        .map_err(|e| e.to_string())?;
255
256    if output.len() != total_size as usize {
257        return Err(format!(
258            "Internal error: size mismatch (expected {} bytes, got {} bytes)",
259            total_size,
260            output.len()
261        ));
262    }
263
264    Ok(output)
265}
266
267/// Decompile .fafb binary bytes into a structured representation.
268///
269/// Parses the header and section table, returning a `DecompiledFafb`
270/// that allows access to individual sections.
271///
272/// # Example
273///
274/// ```rust
275/// use faf_rust_sdk::binary::compile::{compile, decompile};
276/// use faf_rust_sdk::binary::SectionType;
277///
278/// let yaml = "faf_version: 2.5.0\nproject:\n  name: test\n";
279/// let fafb_bytes = compile(yaml).unwrap();
280///
281/// let result = decompile(&fafb_bytes).unwrap();
282/// assert_eq!(result.header.version_major, 1);
283///
284/// let meta = result.get_section_string(SectionType::Meta).unwrap();
285/// assert!(meta.contains("test"));
286/// ```
287pub fn decompile(fafb_bytes: &[u8]) -> FafbResult<DecompiledFafb> {
288    let header = FafbHeader::from_bytes(fafb_bytes)?;
289    header.validate(fafb_bytes)?;
290
291    // Read section table from the offset
292    let table_start = header.section_table_offset as usize;
293    let table_data = &fafb_bytes[table_start..];
294    let section_table = SectionTable::from_bytes(table_data, header.section_count as usize)?;
295    section_table.validate_bounds(header.total_size)?;
296
297    Ok(DecompiledFafb {
298        header,
299        section_table,
300        data: fafb_bytes.to_vec(),
301    })
302}
303
304/// Extract a YAML section as string
305fn extract_section(yaml: &serde_yaml_ng::Value, key: &str) -> Option<String> {
306    yaml.get(key)
307        .and_then(|v| serde_yaml_ng::to_string(v).ok())
308        .filter(|s| !s.trim().is_empty())
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    fn sample_yaml() -> &'static str {
316        r#"faf_version: 2.5.0
317project:
318  name: test-project
319  goal: Test the compiler
320instant_context:
321  tech_stack: Rust
322  key_files:
323    - src/main.rs
324    - src/lib.rs
325  commands:
326    build: cargo build
327    test: cargo test
328"#
329    }
330
331    #[test]
332    fn test_compile_produces_valid_fafb() {
333        let bytes = compile(sample_yaml()).unwrap();
334        assert_eq!(&bytes[0..4], b"FAFB");
335        assert!(bytes.len() >= HEADER_SIZE);
336    }
337
338    #[test]
339    fn test_compile_empty_fails() {
340        assert!(compile("").is_err());
341    }
342
343    #[test]
344    fn test_roundtrip_compile_decompile() {
345        let yaml = sample_yaml();
346        let bytes = compile(yaml).unwrap();
347        let result = decompile(&bytes).unwrap();
348
349        assert_eq!(result.header.version_major, 1);
350        assert_eq!(result.header.version_minor, 0);
351        assert!(result.section_table.len() >= 1);
352
353        // META section must exist
354        let meta = result.get_section_string(SectionType::Meta).unwrap();
355        assert!(meta.contains("test-project"));
356        assert!(meta.contains("2.5.0"));
357    }
358
359    #[test]
360    fn test_roundtrip_preserves_sections() {
361        let yaml = sample_yaml();
362        let bytes = compile(yaml).unwrap();
363        let result = decompile(&bytes).unwrap();
364
365        // Check tech stack section
366        let tech = result.get_section_string(SectionType::TechStack);
367        assert!(tech.is_some());
368
369        // Check key files section
370        let kf = result.get_section_string(SectionType::KeyFiles);
371        assert!(kf.is_some());
372
373        // Check commands section
374        let cmds = result.get_section_string(SectionType::Commands);
375        assert!(cmds.is_some());
376    }
377
378    #[test]
379    fn test_decompile_invalid_magic() {
380        let bytes = vec![0u8; 32];
381        assert!(decompile(&bytes).is_err());
382    }
383
384    #[test]
385    fn test_decompile_too_small() {
386        let bytes = vec![0u8; 16];
387        assert!(decompile(&bytes).is_err());
388    }
389
390    #[test]
391    fn test_compile_minimal_yaml() {
392        let yaml = "faf_version: 2.5.0\nproject:\n  name: minimal\n";
393        let bytes = compile(yaml).unwrap();
394        let result = decompile(&bytes).unwrap();
395
396        assert_eq!(result.section_table.len(), 1); // Just META
397        let meta = result.get_section_string(SectionType::Meta).unwrap();
398        assert!(meta.contains("minimal"));
399    }
400
401    #[test]
402    fn test_source_checksum_matches() {
403        let yaml = sample_yaml();
404        let bytes = compile(yaml).unwrap();
405        let result = decompile(&bytes).unwrap();
406
407        let expected = FafbHeader::compute_checksum(yaml.as_bytes());
408        assert_eq!(result.header.source_checksum, expected);
409    }
410}