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 })
588}
589
590pub fn is_synxb(data: &[u8]) -> bool {
592 data.len() >= 5 && &data[0..5] == MAGIC
593}
594
595#[cfg(test)]
598mod tests {
599 use super::*;
600
601 #[test]
602 fn test_varint_roundtrip() {
603 for &val in &[0u64, 1, 127, 128, 300, 16383, 16384, u64::MAX >> 1] {
604 let mut buf = Vec::new();
605 encode_varint(&mut buf, val);
606 let mut pos = 0;
607 let decoded = decode_varint(&buf, &mut pos).unwrap();
608 assert_eq!(val, decoded, "varint roundtrip failed for {}", val);
609 }
610 }
611
612 #[test]
613 fn test_zigzag_roundtrip() {
614 for &val in &[0i64, 1, -1, 42, -42, i64::MAX, i64::MIN] {
615 let encoded = zigzag_encode(val);
616 let decoded = zigzag_decode(encoded);
617 assert_eq!(val, decoded, "zigzag roundtrip failed for {}", val);
618 }
619 }
620
621 #[test]
622 fn test_compile_decompile_static() {
623 let mut root = HashMap::new();
624 root.insert("name".to_string(), Value::String("Test".into()));
625 root.insert("port".to_string(), Value::Int(8080));
626
627 let result = ParseResult {
628 root: Value::Object(root),
629 mode: Mode::Static,
630 locked: false,
631 tool: false,
632 schema: false,
633 llm: false,
634 metadata: HashMap::new(),
635 includes: Vec::new(),
636 uses: Vec::new(),
637 };
638
639 let binary = compile(&result, false);
640 assert!(is_synxb(&binary));
641
642 let restored = decompile(&binary).unwrap();
643 assert_eq!(restored.root, result.root);
644 assert_eq!(restored.mode, Mode::Static);
645 assert!(!restored.locked);
646 }
647
648 #[test]
649 fn test_compile_decompile_active_with_metadata() {
650 let mut root = HashMap::new();
651 root.insert("host".to_string(), Value::String("0.0.0.0".into()));
652 root.insert("port".to_string(), Value::Int(3000));
653
654 let mut meta_map = HashMap::new();
655 meta_map.insert("port".to_string(), Meta {
656 markers: vec!["env".to_string()],
657 args: vec!["default".to_string(), "3000".to_string()],
658 type_hint: Some("int".to_string()),
659 constraints: Some(Constraints {
660 min: Some(1.0),
661 max: Some(65535.0),
662 required: true,
663 ..Default::default()
664 }),
665 });
666
667 let mut metadata = HashMap::new();
668 metadata.insert(String::new(), meta_map);
669
670 let includes = vec![IncludeDirective {
671 path: "./base.synx".to_string(),
672 alias: "base".to_string(),
673 }];
674
675 let result = ParseResult {
676 root: Value::Object(root),
677 mode: Mode::Active,
678 locked: true,
679 tool: false,
680 schema: false,
681 llm: false,
682 metadata,
683 includes,
684 uses: Vec::new(),
685 };
686
687 let binary = compile(&result, false);
688 let restored = decompile(&binary).unwrap();
689
690 assert_eq!(restored.root, result.root);
691 assert_eq!(restored.mode, Mode::Active);
692 assert!(restored.locked);
693 assert_eq!(restored.metadata.len(), 1);
694 let rm = &restored.metadata[""];
695 assert_eq!(rm["port"].markers, vec!["env"]);
696 assert_eq!(rm["port"].args, vec!["default", "3000"]);
697 assert_eq!(rm["port"].type_hint, Some("int".to_string()));
698 let c = rm["port"].constraints.as_ref().unwrap();
699 assert_eq!(c.min, Some(1.0));
700 assert_eq!(c.max, Some(65535.0));
701 assert!(c.required);
702 assert_eq!(restored.includes.len(), 1);
703 assert_eq!(restored.includes[0].path, "./base.synx");
704 }
705
706 #[test]
707 fn test_compile_resolved_strips_metadata() {
708 let mut root = HashMap::new();
709 root.insert("val".to_string(), Value::Int(42));
710
711 let mut meta_map = HashMap::new();
712 meta_map.insert("val".to_string(), Meta {
713 markers: vec!["calc".to_string()],
714 args: Vec::new(),
715 type_hint: None,
716 constraints: None,
717 });
718 let mut metadata = HashMap::new();
719 metadata.insert(String::new(), meta_map);
720
721 let result = ParseResult {
722 root: Value::Object(root),
723 mode: Mode::Active,
724 locked: false,
725 tool: false,
726 schema: false,
727 llm: false,
728 metadata,
729 includes: Vec::new(),
730 uses: Vec::new(),
731 };
732
733 let binary = compile(&result, true);
734 let restored = decompile(&binary).unwrap();
735
736 assert_eq!(restored.root, result.root);
737 assert!(restored.metadata.is_empty());
738 assert!(restored.includes.is_empty());
739 }
740
741 #[test]
742 fn test_is_synxb() {
743 assert!(is_synxb(b"SYNXB\x01\x00"));
744 assert!(!is_synxb(b"JSON{"));
745 assert!(!is_synxb(b"SYN"));
746 }
747
748 #[test]
749 fn test_invalid_magic() {
750 let err = decompile(b"WRONG\x01\x00\x00\x00\x00\x00").unwrap_err();
751 assert!(err.contains("invalid .synxb magic"));
752 }
753
754 #[test]
755 fn test_invalid_version() {
756 let err = decompile(b"SYNXB\xFF\x00\x00\x00\x00\x00").unwrap_err();
757 assert!(err.contains("unsupported .synxb version"));
758 }
759
760 #[test]
761 fn test_nested_object_roundtrip() {
762 let mut inner = HashMap::new();
763 inner.insert("host".to_string(), Value::String("localhost".into()));
764 inner.insert("port".to_string(), Value::Int(5432));
765
766 let mut root = HashMap::new();
767 root.insert("name".to_string(), Value::String("app".into()));
768 root.insert("database".to_string(), Value::Object(inner));
769 root.insert("tags".to_string(), Value::Array(vec![
770 Value::String("prod".into()),
771 Value::String("v2".into()),
772 ]));
773
774 let result = ParseResult {
775 root: Value::Object(root),
776 mode: Mode::Static,
777 locked: false,
778 tool: false,
779 schema: false,
780 llm: false,
781 metadata: HashMap::new(),
782 includes: Vec::new(),
783 uses: Vec::new(),
784 };
785
786 let binary = compile(&result, false);
787 let restored = decompile(&binary).unwrap();
788 assert_eq!(restored.root, result.root);
789 }
790
791 #[test]
792 fn test_full_roundtrip_parse_compile_decompile() {
793 let synx_text = "name TotalWario\nversion 3.0.0\nport 8080\ndebug false\n";
794 let parsed = crate::parse(synx_text);
795 let binary = compile(&parsed, false);
796
797 let restored = decompile(&binary).unwrap();
798 assert_eq!(restored.root, parsed.root);
799 assert_eq!(restored.mode, parsed.mode);
800 }
801
802 #[test]
803 fn test_large_config_size_reduction() {
804 let synx_text = include_str!("../../../benchmarks/config.synx");
805 let parsed = crate::parse(synx_text);
806 let binary = compile(&parsed, false);
807 let ratio = binary.len() as f64 / synx_text.len() as f64;
808 assert!(
812 ratio < 0.65,
813 "binary should be at least 35% smaller: {} bytes vs {} bytes (ratio {:.2})",
814 binary.len(), synx_text.len(), ratio
815 );
816 }
817
818 #[test]
819 fn test_large_config_full_roundtrip() {
820 let synx_text = include_str!("../../../benchmarks/config.synx");
821 let parsed = crate::parse(synx_text);
822 let binary = compile(&parsed, false);
823 let restored = decompile(&binary).unwrap();
824 assert_eq!(restored.root, parsed.root);
825 assert_eq!(restored.mode, parsed.mode);
826 }
827
828 #[test]
829 fn test_constraints_full_roundtrip() {
830 let c_orig = Constraints {
831 min: Some(0.0),
832 max: Some(100.0),
833 type_name: Some("int".to_string()),
834 required: true,
835 readonly: true,
836 pattern: Some(r"^\d+$".to_string()),
837 enum_values: Some(vec!["a".into(), "b".into(), "c".into()]),
838 };
839
840 let mut meta_map = HashMap::new();
841 meta_map.insert("field".to_string(), Meta {
842 markers: Vec::new(),
843 args: Vec::new(),
844 type_hint: None,
845 constraints: Some(c_orig.clone()),
846 });
847 let mut metadata = HashMap::new();
848 metadata.insert(String::new(), meta_map);
849
850 let mut root = HashMap::new();
851 root.insert("field".to_string(), Value::Int(42));
852
853 let result = ParseResult {
854 root: Value::Object(root),
855 mode: Mode::Active,
856 locked: false,
857 tool: false,
858 schema: false,
859 llm: false,
860 metadata,
861 includes: Vec::new(),
862 uses: Vec::new(),
863 };
864
865 let binary = compile(&result, false);
866 let restored = decompile(&binary).unwrap();
867 let rm = &restored.metadata[""];
868 let c = rm["field"].constraints.as_ref().unwrap();
869 assert_eq!(c.min, c_orig.min);
870 assert_eq!(c.max, c_orig.max);
871 assert_eq!(c.type_name, c_orig.type_name);
872 assert_eq!(c.required, c_orig.required);
873 assert_eq!(c.readonly, c_orig.readonly);
874 assert_eq!(c.pattern, c_orig.pattern);
875 assert_eq!(c.enum_values, c_orig.enum_values);
876 }
877
878 #[test]
879 fn test_all_value_types() {
880 let mut map = HashMap::new();
881 map.insert("null_val".to_string(), Value::Null);
882 map.insert("bool_t".to_string(), Value::Bool(true));
883 map.insert("bool_f".to_string(), Value::Bool(false));
884 map.insert("int_pos".to_string(), Value::Int(42));
885 map.insert("int_neg".to_string(), Value::Int(-100));
886 map.insert("int_zero".to_string(), Value::Int(0));
887 map.insert("float_val".to_string(), Value::Float(3.14));
888 map.insert("string_val".to_string(), Value::String("hello world".into()));
889 map.insert("secret_val".to_string(), Value::Secret("s3cr3t".into()));
890 map.insert("array_val".to_string(), Value::Array(vec![
891 Value::Int(1), Value::String("two".into()), Value::Null,
892 ]));
893
894 let result = ParseResult {
895 root: Value::Object(map),
896 mode: Mode::Static,
897 locked: false,
898 tool: false,
899 schema: false,
900 llm: false,
901 metadata: HashMap::new(),
902 includes: Vec::new(),
903 uses: Vec::new(),
904 };
905
906 let binary = compile(&result, false);
907 let restored = decompile(&binary).unwrap();
908 assert_eq!(restored.root, result.root);
909 }
910
911 #[test]
912 fn test_empty_object() {
913 let result = ParseResult {
914 root: Value::Object(HashMap::new()),
915 mode: Mode::Static,
916 locked: false,
917 tool: false,
918 schema: false,
919 llm: false,
920 metadata: HashMap::new(),
921 includes: Vec::new(),
922 uses: Vec::new(),
923 };
924
925 let binary = compile(&result, false);
926 let restored = decompile(&binary).unwrap();
927 assert_eq!(restored.root, result.root);
928 }
929
930 #[test]
931 fn test_llm_flag_roundtrip() {
932 let mut root = HashMap::new();
933 root.insert("task".to_string(), Value::String("ping".into()));
934 let result = ParseResult {
935 root: Value::Object(root),
936 mode: Mode::Static,
937 locked: false,
938 tool: false,
939 schema: false,
940 llm: true,
941 metadata: HashMap::new(),
942 includes: Vec::new(),
943 uses: Vec::new(),
944 };
945 let binary = compile(&result, false);
946 let restored = decompile(&binary).unwrap();
947 assert!(restored.llm);
948 assert_eq!(restored.root, result.root);
949 }
950
951 #[test]
952 fn test_synx_api_compile_decompile() {
953 use crate::Synx;
954
955 let text = "name Wario\nport 8080\ndebug false\n";
956 let binary = Synx::compile(text, false);
957 assert!(Synx::is_synxb(&binary));
958
959 let decompiled = Synx::decompile(&binary).unwrap();
960 let original = crate::parse(text);
961 let reparsed = crate::parse(&decompiled);
962 assert_eq!(original.root, reparsed.root);
963 }
964}