1use crate::value::{
29 Constraints, IncludeDirective, Meta, MetaMap, Mode, ParseResult, Value,
30};
31use std::collections::HashMap;
32
33const MAGIC: &[u8; 5] = b"SYNXB";
34const FORMAT_VERSION: u8 = 1;
35
36const FLAG_ACTIVE: u8 = 0b0000_0001;
37const FLAG_LOCKED: u8 = 0b0000_0010;
38const FLAG_HAS_META: u8 = 0b0000_0100;
39const FLAG_RESOLVED: u8 = 0b0000_1000;
40const FLAG_TOOL: u8 = 0b0001_0000;
41const FLAG_SCHEMA: u8 = 0b0010_0000;
42const FLAG_LLM: u8 = 0b0100_0000;
43
44const TAG_NULL: u8 = 0x00;
45const TAG_FALSE: u8 = 0x01;
46const TAG_TRUE: u8 = 0x02;
47const TAG_INT: u8 = 0x03;
48const TAG_FLOAT: u8 = 0x04;
49const TAG_STRING: u8 = 0x05;
50const TAG_ARRAY: u8 = 0x06;
51const TAG_OBJECT: u8 = 0x07;
52const TAG_SECRET: u8 = 0x08;
53
54fn encode_varint(out: &mut Vec<u8>, mut val: u64) {
57 loop {
58 let byte = (val & 0x7F) as u8;
59 val >>= 7;
60 if val == 0 {
61 out.push(byte);
62 return;
63 }
64 out.push(byte | 0x80);
65 }
66}
67
68fn decode_varint(data: &[u8], pos: &mut usize) -> Result<u64, String> {
69 let mut result: u64 = 0;
70 let mut shift = 0u32;
71 loop {
72 if *pos >= data.len() {
73 return Err("unexpected end of data in varint".into());
74 }
75 let byte = data[*pos];
76 *pos += 1;
77 result |= ((byte & 0x7F) as u64) << shift;
78 if byte & 0x80 == 0 {
79 return Ok(result);
80 }
81 shift += 7;
82 if shift >= 64 {
83 return Err("varint overflow".into());
84 }
85 }
86}
87
88fn zigzag_encode(n: i64) -> u64 {
90 ((n << 1) ^ (n >> 63)) as u64
91}
92
93fn zigzag_decode(n: u64) -> i64 {
95 ((n >> 1) as i64) ^ (-((n & 1) as i64))
96}
97
98struct StringTable {
101 strings: Vec<String>,
102 index: HashMap<String, u32>,
103}
104
105impl StringTable {
106 fn new() -> Self {
107 Self { strings: Vec::new(), index: HashMap::new() }
108 }
109
110 fn intern(&mut self, s: &str) -> u32 {
111 if let Some(&idx) = self.index.get(s) {
112 return idx;
113 }
114 let idx = self.strings.len() as u32;
115 self.strings.push(s.to_string());
116 self.index.insert(s.to_string(), idx);
117 idx
118 }
119
120 fn collect_value(&mut self, val: &Value) {
121 match val {
122 Value::String(s) | Value::Secret(s) => { self.intern(s); }
123 Value::Array(arr) => {
124 for item in arr { self.collect_value(item); }
125 }
126 Value::Object(map) => {
127 for (key, val) in map {
128 self.intern(key);
129 self.collect_value(val);
130 }
131 }
132 _ => {}
133 }
134 }
135
136 fn collect_metadata(&mut self, metadata: &HashMap<String, MetaMap>) {
137 for (path, meta_map) in metadata {
138 self.intern(path);
139 for (key, meta) in meta_map {
140 self.intern(key);
141 for m in &meta.markers { self.intern(m); }
142 for a in &meta.args { self.intern(a); }
143 if let Some(ref th) = meta.type_hint { self.intern(th); }
144 if let Some(ref c) = meta.constraints {
145 if let Some(ref tn) = c.type_name { self.intern(tn); }
146 if let Some(ref pat) = c.pattern { self.intern(pat); }
147 if let Some(ref ev) = c.enum_values {
148 for v in ev { self.intern(v); }
149 }
150 }
151 }
152 }
153 }
154
155 fn collect_includes(&mut self, includes: &[IncludeDirective]) {
156 for inc in includes {
157 self.intern(&inc.path);
158 self.intern(&inc.alias);
159 }
160 }
161
162 fn encode(&self, out: &mut Vec<u8>) {
163 encode_varint(out, self.strings.len() as u64);
164 for s in &self.strings {
165 encode_varint(out, s.len() as u64);
166 out.extend_from_slice(s.as_bytes());
167 }
168 }
169}
170
171struct StringTableReader {
172 strings: Vec<String>,
173}
174
175impl StringTableReader {
176 fn decode(data: &[u8], pos: &mut usize) -> Result<Self, String> {
177 let count = decode_varint(data, pos)? as usize;
178 let mut strings = Vec::with_capacity(count);
179 for _ in 0..count {
180 let len = decode_varint(data, pos)? as usize;
181 if *pos + len > data.len() {
182 return Err("unexpected end of data in string table".into());
183 }
184 let s = std::str::from_utf8(&data[*pos..*pos + len])
185 .map_err(|e| format!("invalid UTF-8 in string table: {}", e))?
186 .to_string();
187 *pos += len;
188 strings.push(s);
189 }
190 Ok(Self { strings })
191 }
192
193 fn get(&self, idx: u32) -> Result<&str, String> {
194 self.strings.get(idx as usize)
195 .map(|s| s.as_str())
196 .ok_or_else(|| format!("string index {} out of bounds (size {})", idx, self.strings.len()))
197 }
198}
199
200fn encode_value(out: &mut Vec<u8>, val: &Value, st: &StringTable) {
203 match val {
204 Value::Null => out.push(TAG_NULL),
205 Value::Bool(false) => out.push(TAG_FALSE),
206 Value::Bool(true) => out.push(TAG_TRUE),
207 Value::Int(n) => {
208 out.push(TAG_INT);
209 encode_varint(out, zigzag_encode(*n));
210 }
211 Value::Float(f) => {
212 out.push(TAG_FLOAT);
213 out.extend_from_slice(&f.to_le_bytes());
214 }
215 Value::String(s) => {
216 out.push(TAG_STRING);
217 encode_varint(out, st.index[s] as u64);
218 }
219 Value::Array(arr) => {
220 out.push(TAG_ARRAY);
221 encode_varint(out, arr.len() as u64);
222 for item in arr {
223 encode_value(out, item, st);
224 }
225 }
226 Value::Object(map) => {
227 out.push(TAG_OBJECT);
228 let mut entries: Vec<(&str, &Value)> =
229 map.iter().map(|(k, v)| (k.as_str(), v)).collect();
230 entries.sort_unstable_by_key(|(k, _)| *k);
231 encode_varint(out, entries.len() as u64);
232 for (key, val) in entries {
233 encode_varint(out, st.index[key] as u64);
234 encode_value(out, val, st);
235 }
236 }
237 Value::Secret(s) => {
238 out.push(TAG_SECRET);
239 encode_varint(out, st.index[s] as u64);
240 }
241 }
242}
243
244fn decode_value(data: &[u8], pos: &mut usize, st: &StringTableReader) -> Result<Value, String> {
245 if *pos >= data.len() {
246 return Err("unexpected end of data".into());
247 }
248 let tag = data[*pos];
249 *pos += 1;
250 match tag {
251 TAG_NULL => Ok(Value::Null),
252 TAG_FALSE => Ok(Value::Bool(false)),
253 TAG_TRUE => Ok(Value::Bool(true)),
254 TAG_INT => {
255 let raw = decode_varint(data, pos)?;
256 Ok(Value::Int(zigzag_decode(raw)))
257 }
258 TAG_FLOAT => {
259 if *pos + 8 > data.len() {
260 return Err("unexpected end of data in float".into());
261 }
262 let bytes: [u8; 8] = data[*pos..*pos + 8]
263 .try_into()
264 .map_err(|_| "float decode error")?;
265 *pos += 8;
266 Ok(Value::Float(f64::from_le_bytes(bytes)))
267 }
268 TAG_STRING => {
269 let idx = decode_varint(data, pos)? as u32;
270 Ok(Value::String(st.get(idx)?.to_string()))
271 }
272 TAG_ARRAY => {
273 let count = decode_varint(data, pos)? as usize;
274 let mut arr = Vec::with_capacity(count);
275 for _ in 0..count {
276 arr.push(decode_value(data, pos, st)?);
277 }
278 Ok(Value::Array(arr))
279 }
280 TAG_OBJECT => {
281 let count = decode_varint(data, pos)? as usize;
282 let mut map = HashMap::with_capacity(count);
283 for _ in 0..count {
284 let key_idx = decode_varint(data, pos)? as u32;
285 let key = st.get(key_idx)?.to_string();
286 let val = decode_value(data, pos, st)?;
287 map.insert(key, val);
288 }
289 Ok(Value::Object(map))
290 }
291 TAG_SECRET => {
292 let idx = decode_varint(data, pos)? as u32;
293 Ok(Value::Secret(st.get(idx)?.to_string()))
294 }
295 _ => Err(format!("unknown type tag: 0x{:02x}", tag)),
296 }
297}
298
299fn encode_constraints(out: &mut Vec<u8>, c: &Constraints, st: &StringTable) {
302 let mut bits: u8 = 0;
303 if c.min.is_some() { bits |= 0x01; }
304 if c.max.is_some() { bits |= 0x02; }
305 if c.type_name.is_some() { bits |= 0x04; }
306 if c.required { bits |= 0x08; }
307 if c.readonly { bits |= 0x10; }
308 if c.pattern.is_some() { bits |= 0x20; }
309 if c.enum_values.is_some() { bits |= 0x40; }
310 out.push(bits);
311
312 if let Some(min) = c.min { out.extend_from_slice(&min.to_le_bytes()); }
313 if let Some(max) = c.max { out.extend_from_slice(&max.to_le_bytes()); }
314 if let Some(ref tn) = c.type_name { encode_varint(out, st.index[tn] as u64); }
315 if let Some(ref pat) = c.pattern { encode_varint(out, st.index[pat] as u64); }
316 if let Some(ref ev) = c.enum_values {
317 encode_varint(out, ev.len() as u64);
318 for v in ev { encode_varint(out, st.index[v] as u64); }
319 }
320}
321
322fn decode_constraints(data: &[u8], pos: &mut usize, st: &StringTableReader) -> Result<Constraints, String> {
323 if *pos >= data.len() {
324 return Err("unexpected end of data in constraints".into());
325 }
326 let bits = data[*pos];
327 *pos += 1;
328 let mut c = Constraints::default();
329
330 if bits & 0x01 != 0 {
331 if *pos + 8 > data.len() { return Err("truncated min".into()); }
332 let bytes: [u8; 8] = data[*pos..*pos + 8].try_into().map_err(|_| "min decode")?;
333 c.min = Some(f64::from_le_bytes(bytes));
334 *pos += 8;
335 }
336 if bits & 0x02 != 0 {
337 if *pos + 8 > data.len() { return Err("truncated max".into()); }
338 let bytes: [u8; 8] = data[*pos..*pos + 8].try_into().map_err(|_| "max decode")?;
339 c.max = Some(f64::from_le_bytes(bytes));
340 *pos += 8;
341 }
342 if bits & 0x04 != 0 {
343 let idx = decode_varint(data, pos)? as u32;
344 c.type_name = Some(st.get(idx)?.to_string());
345 }
346 if bits & 0x08 != 0 { c.required = true; }
347 if bits & 0x10 != 0 { c.readonly = true; }
348 if bits & 0x20 != 0 {
349 let idx = decode_varint(data, pos)? as u32;
350 c.pattern = Some(st.get(idx)?.to_string());
351 }
352 if bits & 0x40 != 0 {
353 let count = decode_varint(data, pos)? as usize;
354 let mut vals = Vec::with_capacity(count);
355 for _ in 0..count {
356 let idx = decode_varint(data, pos)? as u32;
357 vals.push(st.get(idx)?.to_string());
358 }
359 c.enum_values = Some(vals);
360 }
361 Ok(c)
362}
363
364fn encode_meta(out: &mut Vec<u8>, meta: &Meta, st: &StringTable) {
365 encode_varint(out, meta.markers.len() as u64);
366 for m in &meta.markers { encode_varint(out, st.index[m] as u64); }
367 encode_varint(out, meta.args.len() as u64);
368 for a in &meta.args { encode_varint(out, st.index[a] as u64); }
369 if let Some(ref th) = meta.type_hint {
370 out.push(1);
371 encode_varint(out, st.index[th] as u64);
372 } else {
373 out.push(0);
374 }
375 if let Some(ref c) = meta.constraints {
376 out.push(1);
377 encode_constraints(out, c, st);
378 } else {
379 out.push(0);
380 }
381}
382
383fn decode_meta(data: &[u8], pos: &mut usize, st: &StringTableReader) -> Result<Meta, String> {
384 let marker_count = decode_varint(data, pos)? as usize;
385 let mut markers = Vec::with_capacity(marker_count);
386 for _ in 0..marker_count {
387 let idx = decode_varint(data, pos)? as u32;
388 markers.push(st.get(idx)?.to_string());
389 }
390
391 let arg_count = decode_varint(data, pos)? as usize;
392 let mut args = Vec::with_capacity(arg_count);
393 for _ in 0..arg_count {
394 let idx = decode_varint(data, pos)? as u32;
395 args.push(st.get(idx)?.to_string());
396 }
397
398 if *pos >= data.len() { return Err("unexpected end in meta".into()); }
399 let has_th = data[*pos];
400 *pos += 1;
401 let type_hint = if has_th != 0 {
402 let idx = decode_varint(data, pos)? as u32;
403 Some(st.get(idx)?.to_string())
404 } else { None };
405
406 if *pos >= data.len() { return Err("unexpected end in meta".into()); }
407 let has_c = data[*pos];
408 *pos += 1;
409 let constraints = if has_c != 0 { Some(decode_constraints(data, pos, st)?) } else { None };
410
411 Ok(Meta { markers, args, type_hint, constraints })
412}
413
414fn encode_metadata(out: &mut Vec<u8>, metadata: &HashMap<String, MetaMap>, st: &StringTable) {
415 let mut outer_keys: Vec<&str> = metadata.keys().map(|k| k.as_str()).collect();
416 outer_keys.sort_unstable();
417 encode_varint(out, outer_keys.len() as u64);
418 for key_path in outer_keys {
419 encode_varint(out, st.index[key_path] as u64);
420 let meta_map = &metadata[key_path];
421 let mut inner_keys: Vec<&str> = meta_map.keys().map(|k| k.as_str()).collect();
422 inner_keys.sort_unstable();
423 encode_varint(out, inner_keys.len() as u64);
424 for field_key in inner_keys {
425 encode_varint(out, st.index[field_key] as u64);
426 encode_meta(out, &meta_map[field_key], st);
427 }
428 }
429}
430
431fn decode_metadata(data: &[u8], pos: &mut usize, st: &StringTableReader) -> Result<HashMap<String, MetaMap>, String> {
432 let outer_count = decode_varint(data, pos)? as usize;
433 let mut metadata = HashMap::with_capacity(outer_count);
434 for _ in 0..outer_count {
435 let path_idx = decode_varint(data, pos)? as u32;
436 let key_path = st.get(path_idx)?.to_string();
437 let inner_count = decode_varint(data, pos)? as usize;
438 let mut meta_map = HashMap::with_capacity(inner_count);
439 for _ in 0..inner_count {
440 let key_idx = decode_varint(data, pos)? as u32;
441 let field_key = st.get(key_idx)?.to_string();
442 let meta = decode_meta(data, pos, st)?;
443 meta_map.insert(field_key, meta);
444 }
445 metadata.insert(key_path, meta_map);
446 }
447 Ok(metadata)
448}
449
450fn encode_includes(out: &mut Vec<u8>, includes: &[IncludeDirective], st: &StringTable) {
451 encode_varint(out, includes.len() as u64);
452 for inc in includes {
453 encode_varint(out, st.index[&inc.path] as u64);
454 encode_varint(out, st.index[&inc.alias] as u64);
455 }
456}
457
458fn decode_includes(data: &[u8], pos: &mut usize, st: &StringTableReader) -> Result<Vec<IncludeDirective>, String> {
459 let count = decode_varint(data, pos)? as usize;
460 let mut includes = Vec::with_capacity(count);
461 for _ in 0..count {
462 let path_idx = decode_varint(data, pos)? as u32;
463 let alias_idx = decode_varint(data, pos)? as u32;
464 includes.push(IncludeDirective {
465 path: st.get(path_idx)?.to_string(),
466 alias: st.get(alias_idx)?.to_string(),
467 });
468 }
469 Ok(includes)
470}
471
472pub fn compile(result: &ParseResult, resolved: bool) -> Vec<u8> {
480 let mut st = StringTable::new();
481 st.collect_value(&result.root);
482 let has_meta = !resolved && !result.metadata.is_empty();
483 if has_meta {
484 st.collect_metadata(&result.metadata);
485 st.collect_includes(&result.includes);
486 }
487
488 let mut payload = Vec::with_capacity(1024);
490 st.encode(&mut payload);
491 encode_value(&mut payload, &result.root, &st);
492 if has_meta {
493 encode_metadata(&mut payload, &result.metadata, &st);
494 encode_includes(&mut payload, &result.includes, &st);
495 }
496
497 let compressed = miniz_oxide::deflate::compress_to_vec(&payload, 9);
502
503 let mut out = Vec::with_capacity(7 + 4 + compressed.len());
505
506 out.extend_from_slice(MAGIC);
508 out.push(FORMAT_VERSION);
509
510 let mut flags: u8 = 0;
511 if result.mode == Mode::Active { flags |= FLAG_ACTIVE; }
512 if result.locked { flags |= FLAG_LOCKED; }
513 if has_meta { flags |= FLAG_HAS_META; }
514 if resolved { flags |= FLAG_RESOLVED; }
515 if result.tool { flags |= FLAG_TOOL; }
516 if result.schema { flags |= FLAG_SCHEMA; }
517 if result.llm { flags |= FLAG_LLM; }
518 out.push(flags);
519
520 out.extend_from_slice(&(payload.len() as u32).to_le_bytes());
522
523 out.extend_from_slice(&compressed);
525
526 out
527}
528
529pub fn decompile(data: &[u8]) -> Result<ParseResult, String> {
531 if data.len() < 11 {
532 return Err("file too small for .synxb header".into());
533 }
534 if &data[0..5] != MAGIC {
535 return Err("invalid .synxb magic (expected SYNXB)".into());
536 }
537 let version = data[5];
538 if version != FORMAT_VERSION {
539 return Err(format!("unsupported .synxb version: {} (expected {})", version, FORMAT_VERSION));
540 }
541 let flags = data[6];
542
543 let uncomp_size = u32::from_le_bytes(
545 data[7..11].try_into().map_err(|_| "failed to read size")?
546 ) as usize;
547
548 let payload = miniz_oxide::inflate::decompress_to_vec(&data[11..])
550 .map_err(|e| format!("decompression failed: {:?}", e))?;
551 if payload.len() != uncomp_size {
552 return Err(format!("size mismatch: expected {}, got {}", uncomp_size, payload.len()));
553 }
554
555 let mut pos = 0;
556
557 let st = StringTableReader::decode(&payload, &mut pos)?;
559
560 let root = decode_value(&payload, &mut pos, &st)?;
562
563 let mode = if flags & FLAG_ACTIVE != 0 { Mode::Active } else { Mode::Static };
564 let locked = flags & FLAG_LOCKED != 0;
565 let tool = flags & FLAG_TOOL != 0;
566 let schema = flags & FLAG_SCHEMA != 0;
567 let llm = flags & FLAG_LLM != 0;
568
569 let (metadata, includes) = if flags & FLAG_HAS_META != 0 {
570 let meta = decode_metadata(&payload, &mut pos, &st)?;
571 let inc = decode_includes(&payload, &mut pos, &st)?;
572 (meta, inc)
573 } else {
574 (HashMap::new(), Vec::new())
575 };
576
577 Ok(ParseResult {
578 root,
579 mode,
580 locked,
581 tool,
582 schema,
583 llm,
584 metadata,
585 includes,
586 uses: Vec::new(),
587 truncated: false,
590 })
591}
592
593pub fn is_synxb(data: &[u8]) -> bool {
595 data.len() >= 5 && &data[0..5] == MAGIC
596}
597
598#[cfg(test)]
601mod tests {
602 use super::*;
603
604 #[test]
605 fn test_varint_roundtrip() {
606 for &val in &[0u64, 1, 127, 128, 300, 16383, 16384, u64::MAX >> 1] {
607 let mut buf = Vec::new();
608 encode_varint(&mut buf, val);
609 let mut pos = 0;
610 let decoded = decode_varint(&buf, &mut pos).unwrap();
611 assert_eq!(val, decoded, "varint roundtrip failed for {}", val);
612 }
613 }
614
615 #[test]
616 fn test_zigzag_roundtrip() {
617 for &val in &[0i64, 1, -1, 42, -42, i64::MAX, i64::MIN] {
618 let encoded = zigzag_encode(val);
619 let decoded = zigzag_decode(encoded);
620 assert_eq!(val, decoded, "zigzag roundtrip failed for {}", val);
621 }
622 }
623
624 #[test]
625 fn test_compile_decompile_static() {
626 let mut root = HashMap::new();
627 root.insert("name".to_string(), Value::String("Test".into()));
628 root.insert("port".to_string(), Value::Int(8080));
629
630 let result = ParseResult {
631 root: Value::Object(root),
632 mode: Mode::Static,
633 locked: false,
634 tool: false,
635 schema: false,
636 llm: false,
637 metadata: HashMap::new(),
638 includes: Vec::new(),
639 uses: Vec::new(),
640 truncated: false,
641 };
642
643 let binary = compile(&result, false);
644 assert!(is_synxb(&binary));
645
646 let restored = decompile(&binary).unwrap();
647 assert_eq!(restored.root, result.root);
648 assert_eq!(restored.mode, Mode::Static);
649 assert!(!restored.locked);
650 }
651
652 #[test]
653 fn test_compile_decompile_active_with_metadata() {
654 let mut root = HashMap::new();
655 root.insert("host".to_string(), Value::String("0.0.0.0".into()));
656 root.insert("port".to_string(), Value::Int(3000));
657
658 let mut meta_map = HashMap::new();
659 meta_map.insert("port".to_string(), Meta {
660 markers: vec!["env".to_string()],
661 args: vec!["default".to_string(), "3000".to_string()],
662 type_hint: Some("int".to_string()),
663 constraints: Some(Constraints {
664 min: Some(1.0),
665 max: Some(65535.0),
666 required: true,
667 ..Default::default()
668 }),
669 });
670
671 let mut metadata = HashMap::new();
672 metadata.insert(String::new(), meta_map);
673
674 let includes = vec![IncludeDirective {
675 path: "./base.synx".to_string(),
676 alias: "base".to_string(),
677 }];
678
679 let result = ParseResult {
680 root: Value::Object(root),
681 mode: Mode::Active,
682 locked: true,
683 tool: false,
684 schema: false,
685 llm: false,
686 metadata,
687 includes,
688 uses: Vec::new(),
689 truncated: false,
690 };
691
692 let binary = compile(&result, false);
693 let restored = decompile(&binary).unwrap();
694
695 assert_eq!(restored.root, result.root);
696 assert_eq!(restored.mode, Mode::Active);
697 assert!(restored.locked);
698 assert_eq!(restored.metadata.len(), 1);
699 let rm = &restored.metadata[""];
700 assert_eq!(rm["port"].markers, vec!["env"]);
701 assert_eq!(rm["port"].args, vec!["default", "3000"]);
702 assert_eq!(rm["port"].type_hint, Some("int".to_string()));
703 let c = rm["port"].constraints.as_ref().unwrap();
704 assert_eq!(c.min, Some(1.0));
705 assert_eq!(c.max, Some(65535.0));
706 assert!(c.required);
707 assert_eq!(restored.includes.len(), 1);
708 assert_eq!(restored.includes[0].path, "./base.synx");
709 }
710
711 #[test]
712 fn test_compile_resolved_strips_metadata() {
713 let mut root = HashMap::new();
714 root.insert("val".to_string(), Value::Int(42));
715
716 let mut meta_map = HashMap::new();
717 meta_map.insert("val".to_string(), Meta {
718 markers: vec!["calc".to_string()],
719 args: Vec::new(),
720 type_hint: None,
721 constraints: None,
722 });
723 let mut metadata = HashMap::new();
724 metadata.insert(String::new(), meta_map);
725
726 let result = ParseResult {
727 root: Value::Object(root),
728 mode: Mode::Active,
729 locked: false,
730 tool: false,
731 schema: false,
732 llm: false,
733 metadata,
734 includes: Vec::new(),
735 uses: Vec::new(),
736 truncated: false,
737 };
738
739 let binary = compile(&result, true);
740 let restored = decompile(&binary).unwrap();
741
742 assert_eq!(restored.root, result.root);
743 assert!(restored.metadata.is_empty());
744 assert!(restored.includes.is_empty());
745 }
746
747 #[test]
748 fn test_is_synxb() {
749 assert!(is_synxb(b"SYNXB\x01\x00"));
750 assert!(!is_synxb(b"JSON{"));
751 assert!(!is_synxb(b"SYN"));
752 }
753
754 #[test]
755 fn test_invalid_magic() {
756 let err = decompile(b"WRONG\x01\x00\x00\x00\x00\x00").unwrap_err();
757 assert!(err.contains("invalid .synxb magic"));
758 }
759
760 #[test]
761 fn test_invalid_version() {
762 let err = decompile(b"SYNXB\xFF\x00\x00\x00\x00\x00").unwrap_err();
763 assert!(err.contains("unsupported .synxb version"));
764 }
765
766 #[test]
767 fn test_nested_object_roundtrip() {
768 let mut inner = HashMap::new();
769 inner.insert("host".to_string(), Value::String("localhost".into()));
770 inner.insert("port".to_string(), Value::Int(5432));
771
772 let mut root = HashMap::new();
773 root.insert("name".to_string(), Value::String("app".into()));
774 root.insert("database".to_string(), Value::Object(inner));
775 root.insert("tags".to_string(), Value::Array(vec![
776 Value::String("prod".into()),
777 Value::String("v2".into()),
778 ]));
779
780 let result = ParseResult {
781 root: Value::Object(root),
782 mode: Mode::Static,
783 locked: false,
784 tool: false,
785 schema: false,
786 llm: false,
787 metadata: HashMap::new(),
788 includes: Vec::new(),
789 uses: Vec::new(),
790 truncated: false,
791 };
792
793 let binary = compile(&result, false);
794 let restored = decompile(&binary).unwrap();
795 assert_eq!(restored.root, result.root);
796 }
797
798 #[test]
799 fn test_full_roundtrip_parse_compile_decompile() {
800 let synx_text = "name TotalWario\nversion 3.0.0\nport 8080\ndebug false\n";
801 let parsed = crate::parse(synx_text);
802 let binary = compile(&parsed, false);
803
804 let restored = decompile(&binary).unwrap();
805 assert_eq!(restored.root, parsed.root);
806 assert_eq!(restored.mode, parsed.mode);
807 }
808
809 #[test]
810 fn test_large_config_size_reduction() {
811 let synx_text = include_str!("../../../benchmarks/config.synx");
812 let parsed = crate::parse(synx_text);
813 let binary = compile(&parsed, false);
814 let ratio = binary.len() as f64 / synx_text.len() as f64;
815 assert!(
819 ratio < 0.65,
820 "binary should be at least 35% smaller: {} bytes vs {} bytes (ratio {:.2})",
821 binary.len(), synx_text.len(), ratio
822 );
823 }
824
825 #[test]
826 fn test_large_config_full_roundtrip() {
827 let synx_text = include_str!("../../../benchmarks/config.synx");
828 let parsed = crate::parse(synx_text);
829 let binary = compile(&parsed, false);
830 let restored = decompile(&binary).unwrap();
831 assert_eq!(restored.root, parsed.root);
832 assert_eq!(restored.mode, parsed.mode);
833 }
834
835 #[test]
836 fn test_constraints_full_roundtrip() {
837 let c_orig = Constraints {
838 min: Some(0.0),
839 max: Some(100.0),
840 type_name: Some("int".to_string()),
841 required: true,
842 readonly: true,
843 pattern: Some(r"^\d+$".to_string()),
844 enum_values: Some(vec!["a".into(), "b".into(), "c".into()]),
845 };
846
847 let mut meta_map = HashMap::new();
848 meta_map.insert("field".to_string(), Meta {
849 markers: Vec::new(),
850 args: Vec::new(),
851 type_hint: None,
852 constraints: Some(c_orig.clone()),
853 });
854 let mut metadata = HashMap::new();
855 metadata.insert(String::new(), meta_map);
856
857 let mut root = HashMap::new();
858 root.insert("field".to_string(), Value::Int(42));
859
860 let result = ParseResult {
861 root: Value::Object(root),
862 mode: Mode::Active,
863 locked: false,
864 tool: false,
865 schema: false,
866 llm: false,
867 metadata,
868 includes: Vec::new(),
869 uses: Vec::new(),
870 truncated: false,
871 };
872
873 let binary = compile(&result, false);
874 let restored = decompile(&binary).unwrap();
875 let rm = &restored.metadata[""];
876 let c = rm["field"].constraints.as_ref().unwrap();
877 assert_eq!(c.min, c_orig.min);
878 assert_eq!(c.max, c_orig.max);
879 assert_eq!(c.type_name, c_orig.type_name);
880 assert_eq!(c.required, c_orig.required);
881 assert_eq!(c.readonly, c_orig.readonly);
882 assert_eq!(c.pattern, c_orig.pattern);
883 assert_eq!(c.enum_values, c_orig.enum_values);
884 }
885
886 #[test]
887 fn test_all_value_types() {
888 let mut map = HashMap::new();
889 map.insert("null_val".to_string(), Value::Null);
890 map.insert("bool_t".to_string(), Value::Bool(true));
891 map.insert("bool_f".to_string(), Value::Bool(false));
892 map.insert("int_pos".to_string(), Value::Int(42));
893 map.insert("int_neg".to_string(), Value::Int(-100));
894 map.insert("int_zero".to_string(), Value::Int(0));
895 map.insert("float_val".to_string(), Value::Float(3.14));
896 map.insert("string_val".to_string(), Value::String("hello world".into()));
897 map.insert("secret_val".to_string(), Value::Secret("s3cr3t".into()));
898 map.insert("array_val".to_string(), Value::Array(vec![
899 Value::Int(1), Value::String("two".into()), Value::Null,
900 ]));
901
902 let result = ParseResult {
903 root: Value::Object(map),
904 mode: Mode::Static,
905 locked: false,
906 tool: false,
907 schema: false,
908 llm: false,
909 metadata: HashMap::new(),
910 includes: Vec::new(),
911 uses: Vec::new(),
912 truncated: false,
913 };
914
915 let binary = compile(&result, false);
916 let restored = decompile(&binary).unwrap();
917 assert_eq!(restored.root, result.root);
918 }
919
920 #[test]
921 fn test_empty_object() {
922 let result = ParseResult {
923 root: Value::Object(HashMap::new()),
924 mode: Mode::Static,
925 locked: false,
926 tool: false,
927 schema: false,
928 llm: false,
929 metadata: HashMap::new(),
930 includes: Vec::new(),
931 uses: Vec::new(),
932 truncated: false,
933 };
934
935 let binary = compile(&result, false);
936 let restored = decompile(&binary).unwrap();
937 assert_eq!(restored.root, result.root);
938 }
939
940 #[test]
941 fn test_llm_flag_roundtrip() {
942 let mut root = HashMap::new();
943 root.insert("task".to_string(), Value::String("ping".into()));
944 let result = ParseResult {
945 root: Value::Object(root),
946 mode: Mode::Static,
947 locked: false,
948 tool: false,
949 schema: false,
950 llm: true,
951 metadata: HashMap::new(),
952 includes: Vec::new(),
953 uses: Vec::new(),
954 truncated: false,
955 };
956 let binary = compile(&result, false);
957 let restored = decompile(&binary).unwrap();
958 assert!(restored.llm);
959 assert_eq!(restored.root, result.root);
960 }
961
962 #[test]
963 fn test_synx_api_compile_decompile() {
964 use crate::Synx;
965
966 let text = "name Wario\nport 8080\ndebug false\n";
967 let binary = Synx::compile(text, false);
968 assert!(Synx::is_synxb(&binary));
969
970 let decompiled = Synx::decompile(&binary).unwrap();
971 let original = crate::parse(text);
972 let reparsed = crate::parse(&decompiled);
973 assert_eq!(original.root, reparsed.root);
974 }
975}