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