Skip to main content

synx_core/
binary.rs

1//! SYNX Binary Format (.synxb) — compact binary serialization with string interning.
2//!
3//! Layout:
4//! ```text
5//! HEADER       5 bytes magic "SYNXB"
6//!              1 byte  version (currently 1)
7//!              1 byte  flags   (bit 0: active, bit 1: locked,
8//!                               bit 2: has_metadata, bit 3: resolved)
9//! STRING_TABLE varint count
10//!              for each: varint len + UTF-8 bytes
11//! VALUE_TREE   Root Value (recursive, strings as table indices)
12//! [METADATA]   if flag bit 2 set
13//! [INCLUDES]   if flag bit 2 set
14//! ```
15//!
16//! Type tags (1 byte):
17//!   0x00 Null
18//!   0x01 Bool(false)
19//!   0x02 Bool(true)
20//!   0x03 Int       + zigzag varint
21//!   0x04 Float     + 8 bytes LE
22//!   0x05 String    + varint string_table_index
23//!   0x06 Array     + varint count + values
24//!   0x07 Object    + varint count + (varint key_index + value) pairs
25//!   0x08 Secret    + varint string_table_index
26
27use 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
52// ─── Varint Encoding (LEB128 unsigned) ───────────────────────────────────────
53
54fn 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
86// Zigzag encode i64 → u64
87fn zigzag_encode(n: i64) -> u64 {
88    ((n << 1) ^ (n >> 63)) as u64
89}
90
91// Zigzag decode u64 → i64
92fn zigzag_decode(n: u64) -> i64 {
93    ((n >> 1) as i64) ^ (-((n & 1) as i64))
94}
95
96// ─── String Table ────────────────────────────────────────────────────────────
97
98struct 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
198// ─── Value encoding (with string table) ─────────────────────────────────────
199
200fn 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
297// ─── Metadata encoding ──────────────────────────────────────────────────────
298
299fn 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
470// ─── Public API ──────────────────────────────────────────────────────────────
471
472/// Compile a `ParseResult` into compact binary `.synxb` format.
473///
474/// Uses a string interning table so every unique string is stored once,
475/// then deflate-compresses the payload for maximum compactness.
476/// If `resolved` is true, metadata and includes are stripped.
477pub 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    // Build uncompressed payload
487    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    // Compress payload.
496    //
497    // This is a storage format: favor size over speed. Using level 9 also makes
498    // size-reduction expectations stable across platforms/runner images.
499    let compressed = miniz_oxide::deflate::compress_to_vec(&payload, 9);
500
501    // Build final output: header + compressed data
502    let mut out = Vec::with_capacity(7 + 4 + compressed.len());
503
504    // Header (always uncompressed for magic detection)
505    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    // Uncompressed size (for pre-allocation on decode)
518    out.extend_from_slice(&(payload.len() as u32).to_le_bytes());
519
520    // Compressed payload
521    out.extend_from_slice(&compressed);
522
523    out
524}
525
526/// Decompile a `.synxb` binary back into a `ParseResult`.
527pub 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    // Read uncompressed size
541    let uncomp_size = u32::from_le_bytes(
542        data[7..11].try_into().map_err(|_| "failed to read size")?
543    ) as usize;
544
545    // Decompress payload
546    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    // String table
555    let st = StringTableReader::decode(&payload, &mut pos)?;
556
557    // Root value
558    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
576/// Check if data starts with the `.synxb` magic bytes.
577pub fn is_synxb(data: &[u8]) -> bool {
578    data.len() >= 5 && &data[0..5] == MAGIC
579}
580
581// ─── Tests ───────────────────────────────────────────────────────────────────
582
583#[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}