faf_rust_sdk/binary/
compile.rs1use 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#[derive(Debug, Clone)]
15pub struct DecompiledFafb {
16 pub header: FafbHeader,
18 pub section_table: SectionTable,
20 pub data: Vec<u8>,
22}
23
24impl DecompiledFafb {
25 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 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 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 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
58pub 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 let yaml: serde_yaml_ng::Value =
90 serde_yaml_ng::from_str(yaml_source).map_err(|e| format!("Invalid YAML: {}", e))?;
91
92 let mut sections: Vec<(SectionType, Priority, Vec<u8>)> = Vec::new();
94
95 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 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 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 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 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 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 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 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 §ions {
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 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 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(§ion_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
267pub fn decompile(fafb_bytes: &[u8]) -> FafbResult<DecompiledFafb> {
288 let header = FafbHeader::from_bytes(fafb_bytes)?;
289 header.validate(fafb_bytes)?;
290
291 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
304fn 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 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 let tech = result.get_section_string(SectionType::TechStack);
367 assert!(tech.is_some());
368
369 let kf = result.get_section_string(SectionType::KeyFiles);
371 assert!(kf.is_some());
372
373 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); 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}