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