1use std::collections::HashMap;
5use std::sync::{Mutex, OnceLock};
6use std::time::{Duration, Instant};
7use crate::calc::safe_calc;
8use crate::parser;
9use crate::rng;
10use crate::value::*;
11
12static SPAM_BUCKETS: OnceLock<Mutex<HashMap<String, Vec<Instant>>>> = OnceLock::new();
13
14const MAX_CALC_EXPR_LEN: usize = 4096;
16const MAX_CALC_RESOLVED_LEN: usize = 64 * 1024;
19const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
21const DEFAULT_MAX_INCLUDE_DEPTH: usize = 16;
23const MAX_RESOLVE_DEPTH: usize = 512;
25
26const MAX_ENGINE_SCRATCH_STRING: usize = 4 * 1024 * 1024;
28
29fn jail_path(base: &str, file_path: &str) -> Result<std::path::PathBuf, String> {
32 if let Some(first) = file_path.chars().next() {
36 if first == '/' || first == '\\' {
37 return Err(format!("SECURITY: rooted paths are not allowed: '{}'", file_path));
38 }
39 }
40 let fp = std::path::Path::new(file_path);
42 if fp.is_absolute() {
43 return Err(format!("SECURITY: absolute paths are not allowed: '{}'", file_path));
44 }
45
46 let base_canonical = match std::fs::canonicalize(base) {
47 Ok(p) => p,
48 Err(_) => std::path::PathBuf::from(base),
49 };
50 let full = base_canonical.join(file_path);
51 let full_canonical = match std::fs::canonicalize(&full) {
52 Ok(p) => p,
53 Err(_) => {
54 let normalized = full.to_string_lossy();
56 if normalized.contains("..") {
57 return Err(format!("SECURITY: path traversal detected: '{}'", file_path));
58 }
59 if !full.starts_with(&base_canonical) {
62 return Err(format!("SECURITY: path escapes base directory: '{}'", file_path));
63 }
64 return Ok(full);
65 }
66 };
67 if !full_canonical.starts_with(&base_canonical) {
68 return Err(format!("SECURITY: path escapes base directory: '{}'", file_path));
69 }
70 Ok(full_canonical)
71}
72
73fn check_file_size(path: &std::path::Path) -> Result<(), String> {
75 match std::fs::metadata(path) {
76 Ok(meta) if meta.len() > MAX_FILE_SIZE => {
77 Err(format!("SECURITY: file too large ({} bytes, max {})", meta.len(), MAX_FILE_SIZE))
78 }
79 _ => Ok(()),
80 }
81}
82
83fn fmt_io_err(e: &std::io::Error, ctx: &str) -> String {
88 use std::io::ErrorKind;
89 match e.kind() {
90 ErrorKind::NotFound => format!("file not found: {}", ctx),
91 ErrorKind::PermissionDenied => format!("permission denied: {}", ctx),
92 _ => e.to_string(),
93 }
94}
95
96pub fn resolve(result: &mut ParseResult, options: &Options) {
99 if result.mode != Mode::Active {
100 return;
101 }
102 let metadata = std::mem::take(&mut result.metadata);
103 let includes_directives = std::mem::take(&mut result.includes);
104 let use_directives = std::mem::take(&mut result.uses);
105
106 #[cfg(feature = "wasm")]
108 let mut wasm_runtime = crate::wasm::WasmMarkerRuntime::new();
109 let packages_map = load_packages(
110 &use_directives,
111 options,
112 #[cfg(feature = "wasm")]
113 &mut wasm_runtime,
114 );
115
116 #[cfg(feature = "wasm")]
118 let wasm_options;
119 #[cfg(feature = "wasm")]
120 let options = if !wasm_runtime.marker_names().is_empty() {
121 wasm_options = Options {
122 wasm_runtime: Some(std::sync::Arc::new(wasm_runtime)),
123 ..options.clone()
124 };
125 &wasm_options
126 } else {
127 options
128 };
129
130 let mut includes_map = load_includes(&includes_directives, options);
132
133 if let Value::Object(ref root_map) = result.root {
141 for (key, _) in root_map.iter() {
142 let meta = match metadata.get("").and_then(|mm| mm.get(key)) {
143 Some(m) => m,
144 None => continue,
145 };
146 let is_inc = meta.markers.iter().any(|m| m == "include" || m == "import");
147 if !is_inc { continue; }
148 let path = match root_map.get(key) {
150 Some(Value::String(s)) => s.clone(),
151 _ => continue,
152 };
153 let base = options.base_path.as_deref().unwrap_or(".");
154 let full = match jail_path(base, &path) {
155 Ok(p) => p,
156 Err(_) => continue,
157 };
158 if check_file_size(&full).is_err() { continue; }
159 let text = match std::fs::read_to_string(&full) {
160 Ok(t) => t,
161 Err(_) => continue,
162 };
163 let mut included = parser::parse(&text);
164 if included.mode == Mode::Active {
165 let mut child_opts = options.clone();
166 child_opts._include_depth += 1;
167 if let Some(parent) = full.parent() {
168 child_opts.base_path = Some(parent.to_string_lossy().into_owned());
169 }
170 resolve(&mut included, &child_opts);
171 }
172 includes_map.entry(key.clone()).or_insert(included.root);
173 }
174 }
175
176 if let Value::Object(ref mut root_map) = result.root {
178 for (alias, pkg_value) in &packages_map {
179 root_map.entry(alias.clone()).or_insert_with(|| pkg_value.clone());
180 }
181 }
182
183 apply_inheritance(&mut result.root, &metadata);
185 if let Value::Object(ref mut root_map) = result.root {
187 root_map.retain(|k, _| !k.starts_with('_'));
188 }
189
190 mask_secret_literals(&mut result.root, &metadata, "");
198
199 let type_registry = build_type_registry(&metadata);
201 let constraint_registry = build_constraint_registry(&metadata);
203
204 let root_ptr = &mut result.root as *mut Value;
214 resolve_value(&mut result.root, root_ptr, options, &metadata, "", &includes_map, 0);
215
216 validate_field_constraints(&mut result.root, &constraint_registry);
218
219 validate_field_types(&mut result.root, &type_registry, "");
221
222 result.metadata = metadata;
223 result.includes = includes_directives;
224}
225
226fn resolve_value(
227 value: &mut Value,
228 root_ptr: *mut Value,
229 options: &Options,
230 metadata: &HashMap<String, MetaMap>,
231 path: &str,
232 includes: &HashMap<String, Value>,
233 depth: usize,
234) {
235 if depth >= MAX_RESOLVE_DEPTH {
237 if let Value::Object(ref mut map) = value {
240 for val in map.values_mut() {
241 *val = Value::String(
242 "NESTING_ERR: maximum object nesting depth exceeded".to_string()
243 );
244 }
245 }
246 return;
247 }
248
249 let meta_map = metadata.get(path).cloned();
250
251 if let Value::Object(ref mut map) = value {
252 let keys: Vec<String> = map.keys().cloned().collect();
253
254 for key in &keys {
256 let child_path = if path.is_empty() {
257 key.clone()
258 } else {
259 format!("{}.{}", path, key)
260 };
261
262 if let Some(child) = map.get_mut(key) {
263 match child {
264 Value::Object(_) => {
265 resolve_value(child, root_ptr, options, metadata, &child_path, includes, depth + 1);
266 }
267 Value::Array(arr) => {
268 for item in arr.iter_mut() {
269 if let Value::Object(_) = item {
270 resolve_value(item, root_ptr, options, metadata, &child_path, includes, depth + 1);
271 }
272 }
273 }
274 _ => {}
275 }
276 }
277 }
278
279 if let Some(ref mm) = meta_map {
281 for key in &keys {
282 let meta = match mm.get(key) {
283 Some(m) => m.clone(),
284 None => continue,
285 };
286
287 apply_markers(map, key, &meta, root_ptr, options, path, metadata, includes);
288 }
289 }
290
291 let keys2: Vec<String> = map.keys().cloned().collect();
293 for key in &keys2 {
294 if let Some(Value::String(s)) = map.get(key) {
295 if s.contains('{') {
296 let root_ref = unsafe { &*root_ptr };
297 let result = resolve_interpolation(s, root_ref, map, includes);
298 if result != *s {
299 map.insert(key.to_string(), Value::String(result));
300 }
301 }
302 }
303 }
304 }
305}
306
307fn apply_markers(
308 map: &mut HashMap<String, Value>,
309 key: &str,
310 meta: &Meta,
311 root_ptr: *mut Value,
312 options: &Options,
313 path: &str,
314 metadata: &HashMap<String, MetaMap>,
315 _includes: &HashMap<String, Value>,
316) {
317 let markers = &meta.markers;
318
319 if markers.contains(&"spam".to_string()) {
324 let spam_idx = markers.iter().position(|m| m == "spam").unwrap();
325 let max_calls = markers
326 .get(spam_idx + 1)
327 .and_then(|s| s.parse::<usize>().ok())
328 .unwrap_or(0);
329 let window_sec = markers
330 .get(spam_idx + 2)
331 .and_then(|s| s.parse::<u64>().ok())
332 .unwrap_or(1);
333
334 if max_calls == 0 {
335 map.insert(
336 key.to_string(),
337 Value::String("SPAM_ERR: invalid limit, use :spam:MAX[:WINDOW_SEC]".to_string()),
338 );
339 return;
340 }
341
342 let target = map
343 .get(key)
344 .map(value_to_string)
345 .unwrap_or_else(|| key.to_string());
346 let bucket_key = format!("{}::{}", key, target);
347
348 if !allow_spam_access(&bucket_key, max_calls, window_sec) {
349 map.insert(
350 key.to_string(),
351 Value::String(format!(
352 "SPAM_ERR: '{}' exceeded {} calls per {}s",
353 target, max_calls, window_sec
354 )),
355 );
356 return;
357 }
358
359 if let Some(resolved) = map
360 .get(key)
361 .and_then(|v| {
362 let t = value_to_string(v);
363 let root_ref = unsafe { &*root_ptr };
364 deep_get(root_ref, &t).or_else(|| map.get(t.as_str()).cloned())
365 })
366 {
367 map.insert(key.to_string(), resolved);
368 }
369 }
370
371 if markers.contains(&"include".to_string()) || markers.contains(&"import".to_string()) {
373 if let Some(Value::String(file_path)) = map.get(key) {
374 let max_depth = options.max_include_depth.unwrap_or(DEFAULT_MAX_INCLUDE_DEPTH);
375 if options._include_depth >= max_depth {
376 map.insert(
377 key.to_string(),
378 Value::String(format!("INCLUDE_ERR: max include depth ({}) exceeded", max_depth)),
379 );
380 return;
381 }
382 let base = options
383 .base_path
384 .as_deref()
385 .unwrap_or(".");
386 let full = match jail_path(base, file_path) {
387 Ok(p) => p,
388 Err(e) => {
389 map.insert(key.to_string(), Value::String(format!("INCLUDE_ERR: {}", e)));
390 return;
391 }
392 };
393 if let Err(e) = check_file_size(&full) {
394 map.insert(key.to_string(), Value::String(format!("INCLUDE_ERR: {}", e)));
395 return;
396 }
397 match std::fs::read_to_string(&full) {
398 Ok(text) => {
399 let mut included = parser::parse(&text);
400 if included.mode == Mode::Active {
401 let mut child_opts = options.clone();
402 child_opts._include_depth += 1;
403 if let Some(parent) = full.parent() {
404 child_opts.base_path = Some(parent.to_string_lossy().into_owned());
405 }
406 resolve(&mut included, &child_opts);
407 }
408 map.insert(key.to_string(), included.root);
409 }
410 Err(e) => {
411 map.insert(
412 key.to_string(),
413 Value::String(format!("INCLUDE_ERR: {}", fmt_io_err(&e, file_path))),
414 );
415 }
416 }
417 }
418 return;
419 }
420
421 if markers.contains(&"env".to_string()) {
423 if let Some(Value::String(var_name)) = map.get(key) {
424 let env_val = if let Some(ref env_map) = options.env {
425 env_map.get(var_name.as_str()).cloned()
426 } else {
427 std::env::var(var_name).ok()
428 };
429
430 let force_string = meta.type_hint.as_deref() == Some("string");
431 let default_idx = markers.iter().position(|m| m == "default");
432 if let Some(val) = env_val.filter(|v| !v.is_empty()) {
433 let resolved = if force_string {
434 Value::String(val)
435 } else {
436 cast_primitive(&val)
437 };
438 map.insert(key.to_string(), resolved);
439 } else if let Some(di) = default_idx {
440 if markers.len() > di + 1 {
441 let fallback = markers[di + 1..].join(":");
444 let resolved = if force_string {
445 Value::String(fallback)
446 } else {
447 cast_primitive(&fallback)
448 };
449 map.insert(key.to_string(), resolved);
450 } else {
451 map.insert(key.to_string(), Value::Null);
452 }
453 } else {
454 map.insert(key.to_string(), Value::Null);
455 }
456 }
457 }
458
459 if markers.contains(&"random".to_string()) {
461 if let Some(Value::Array(arr)) = map.get(key) {
462 if arr.is_empty() {
463 map.insert(key.to_string(), Value::Null);
464 return;
465 }
466 let picked = if !meta.args.is_empty() {
467 let weights: Vec<f64> = meta.args.iter().filter_map(|s| s.parse().ok()).collect();
468 weighted_random(arr, &weights)
469 } else {
470 arr[rng::random_usize(arr.len())].clone()
471 };
472 map.insert(key.to_string(), picked);
473 }
474 }
475
476 if markers.contains(&"ref".to_string()) {
480 if let Some(Value::String(target)) = map.get(key) {
481 let root_ref = unsafe { &*root_ptr };
482 let resolved = deep_get(root_ref, target)
483 .or_else(|| map.get(target.as_str()).cloned())
484 .unwrap_or(Value::Null);
485
486 if markers.contains(&"calc".to_string()) {
488 if let Some(n) = value_as_number(&resolved) {
489 let calc_idx = markers.iter().position(|m| m == "calc").unwrap();
490 if let Some(calc_expr) = markers.get(calc_idx + 1) {
491 let first = calc_expr.chars().next().unwrap_or(' ');
492 if "+-*/%".contains(first) {
493 let expr = format!("{} {}", format_number(n), calc_expr);
494 match safe_calc(&expr) {
495 Ok(result) => {
496 let v = if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
497 Value::Int(result as i64)
498 } else {
499 Value::Float(result)
500 };
501 map.insert(key.to_string(), v);
502 }
503 Err(e) => {
504 map.insert(key.to_string(), Value::String(format!("CALC_ERR: {}", e)));
505 }
506 }
507 } else {
508 map.insert(key.to_string(), resolved);
509 }
510 } else {
511 map.insert(key.to_string(), resolved);
512 }
513 } else {
514 map.insert(key.to_string(), resolved);
515 }
516 } else {
517 map.insert(key.to_string(), resolved);
518 }
519 }
520 }
521
522 if markers.contains(&"i18n".to_string()) {
536 if let Some(Value::Object(translations)) = map.get(key) {
537 let lang = options.lang.as_deref().unwrap_or("en");
538 let val = translations.get(lang)
539 .or_else(|| translations.get("en"))
540 .or_else(|| translations.values().next())
541 .cloned()
542 .unwrap_or(Value::Null);
543
544 let i18n_idx = markers.iter().position(|m| m == "i18n").unwrap();
546 let count_field = markers.get(i18n_idx + 1).cloned();
547
548 if let (Some(ref cf), Value::Object(ref plural_forms)) = (&count_field, &val) {
549 let count_val = map.get(cf)
551 .and_then(value_as_number)
552 .or_else(|| {
553 let root_ref = unsafe { &*root_ptr };
554 deep_get(root_ref, cf).and_then(|v| value_as_number(&v))
555 })
556 .unwrap_or(0.0) as i64;
557
558 let category = plural_category(lang, count_val);
559 let chosen = plural_forms.get(category)
560 .or_else(|| plural_forms.get("other"))
561 .or_else(|| plural_forms.values().next())
562 .cloned()
563 .unwrap_or(Value::Null);
564
565 if let Value::String(ref s) = chosen {
567 let replaced = s.replace("{count}", &count_val.to_string());
568 map.insert(key.to_string(), Value::String(replaced));
569 } else {
570 map.insert(key.to_string(), chosen);
571 }
572 } else {
573 map.insert(key.to_string(), val);
574 }
575 }
576 }
577
578 if markers.contains(&"calc".to_string()) {
580 if let Some(Value::String(expr)) = map.get(key) {
581 if expr.len() > MAX_CALC_EXPR_LEN {
582 map.insert(
583 key.to_string(),
584 Value::String(format!("CALC_ERR: expression too long ({} chars, max {})", expr.len(), MAX_CALC_EXPR_LEN)),
585 );
586 return;
587 }
588 let mut resolved = expr.clone();
589
590 let root_ref = unsafe { &*root_ptr };
592 if let Value::Object(ref root_map) = root_ref {
593 for (rk, rv) in root_map {
594 if let Some(n) = value_as_number(rv) {
595 resolved = replace_word(&resolved, rk, &format_number(n));
596 if resolved.len() > MAX_CALC_RESOLVED_LEN {
597 map.insert(
598 key.to_string(),
599 Value::String(format!(
600 "CALC_ERR: resolved expression too long (max {} bytes)",
601 MAX_CALC_RESOLVED_LEN
602 )),
603 );
604 return;
605 }
606 }
607 }
608 }
609
610 for (rk, rv) in map.iter() {
612 if rk != key {
613 if let Some(n) = value_as_number(rv) {
614 resolved = replace_word(&resolved, rk, &format_number(n));
615 if resolved.len() > MAX_CALC_RESOLVED_LEN {
616 map.insert(
617 key.to_string(),
618 Value::String(format!(
619 "CALC_ERR: resolved expression too long (max {} bytes)",
620 MAX_CALC_RESOLVED_LEN
621 )),
622 );
623 return;
624 }
625 }
626 }
627 }
628
629 let root_ref2 = unsafe { &*root_ptr };
631 let mut dot_resolved = String::new();
632 let bytes = resolved.as_bytes();
633 let len = bytes.len();
634 let mut i = 0;
635 while i < len {
636 if is_word_char(bytes[i]) {
637 let start = i;
638 let mut has_dot = false;
639 while i < len && (is_word_char(bytes[i]) || bytes[i] == b'.') {
640 if bytes[i] == b'.' { has_dot = true; }
641 i += 1;
642 }
643 let token = &resolved[start..i];
644 if has_dot && token.contains('.') {
645 if let Some(val) = deep_get(root_ref2, token) {
646 if let Some(n) = value_as_number(&val) {
647 dot_resolved.push_str(&format_number(n));
648 if dot_resolved.len() > MAX_CALC_RESOLVED_LEN {
649 map.insert(
650 key.to_string(),
651 Value::String(format!(
652 "CALC_ERR: resolved expression too long (max {} bytes)",
653 MAX_CALC_RESOLVED_LEN
654 )),
655 );
656 return;
657 }
658 continue;
659 }
660 }
661 }
662 dot_resolved.push_str(token);
663 if dot_resolved.len() > MAX_CALC_RESOLVED_LEN {
664 map.insert(
665 key.to_string(),
666 Value::String(format!(
667 "CALC_ERR: resolved expression too long (max {} bytes)",
668 MAX_CALC_RESOLVED_LEN
669 )),
670 );
671 return;
672 }
673 } else {
674 dot_resolved.push(bytes[i] as char);
675 i += 1;
676 if dot_resolved.len() > MAX_CALC_RESOLVED_LEN {
677 map.insert(
678 key.to_string(),
679 Value::String(format!(
680 "CALC_ERR: resolved expression too long (max {} bytes)",
681 MAX_CALC_RESOLVED_LEN
682 )),
683 );
684 return;
685 }
686 }
687 }
688 resolved = dot_resolved;
689
690 match safe_calc(&resolved) {
691 Ok(result) => {
692 let v = if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
693 Value::Int(result as i64)
694 } else {
695 Value::Float(result)
696 };
697 map.insert(key.to_string(), v);
698 }
699 Err(e) => {
700 map.insert(
701 key.to_string(),
702 Value::String(format!("CALC_ERR: {}", e)),
703 );
704 }
705 }
706 }
707 }
708
709 if markers.contains(&"alias".to_string()) {
711 if let Some(Value::String(target)) = map.get(key) {
712 let target = target.clone();
713 let current_path = if path.is_empty() {
715 key.to_string()
716 } else {
717 format!("{}.{}", path, key)
718 };
719 if target == key || target == current_path {
721 map.insert(
722 key.to_string(),
723 Value::String(format!("ALIAS_ERR: self-referential alias: {} → {}", current_path, target)),
724 );
725 } else {
726 let root_ref = unsafe { &*root_ptr };
731 let target_val = deep_get(root_ref, &target);
732 let (target_parent, target_key_name) = if let Some(dot) = target.rfind('.') {
734 (target[..dot].to_string(), target[dot + 1..].to_string())
735 } else {
736 (String::new(), target.clone())
737 };
738 let target_has_alias = metadata
739 .get(&target_parent)
740 .and_then(|mm| mm.get(&target_key_name))
741 .map(|m| m.markers.contains(&"alias".to_string()))
742 .unwrap_or(false);
743 let is_cycle = target_has_alias && match &target_val {
744 Some(Value::String(s)) => s == key || s == ¤t_path,
745 _ => false,
746 };
747 if is_cycle {
748 let (a, b) = if current_path <= target {
752 (current_path.as_str(), target.as_str())
753 } else {
754 (target.as_str(), current_path.as_str())
755 };
756 map.insert(
757 key.to_string(),
758 Value::String(format!("ALIAS_ERR: circular alias detected: {} → {}", a, b)),
759 );
760 } else {
761 let val = target_val.unwrap_or(Value::Null);
762 map.insert(key.to_string(), val);
763 }
764 }
765 }
766 }
767
768 if markers.contains(&"secret".to_string()) {
770 match map.get(key) {
774 Some(Value::Secret(_)) => {}
775 Some(val) => {
776 let s = value_to_string(val);
777 map.insert(key.to_string(), Value::Secret(s));
778 }
779 None => {}
780 }
781 }
782
783 if markers.contains(&"unique".to_string()) {
785 if let Some(Value::Array(arr)) = map.get(key) {
786 let mut seen = std::collections::HashSet::new();
789 let mut unique = Vec::new();
790 for item in arr {
791 let s = value_to_string(item);
792 if seen.insert(s) {
793 unique.push(item.clone());
794 }
795 }
796 map.insert(key.to_string(), Value::Array(unique));
797 }
798 }
799
800 if markers.contains(&"geo".to_string()) {
802 if let Some(Value::Array(arr)) = map.get(key) {
803 let region = options.region.as_deref().unwrap_or("US");
804 let prefix = format!("{} ", region);
805 let found = arr.iter().find(|item| {
806 if let Value::String(s) = item {
807 s.starts_with(&prefix)
808 } else {
809 false
810 }
811 });
812
813 let result = if let Some(Value::String(s)) = found {
814 Value::String(s[prefix.len()..].trim().to_string())
815 } else if let Some(first) = arr.first() {
816 if let Value::String(s) = first {
817 if let Some(space) = s.find(' ') {
818 Value::String(s[space + 1..].trim().to_string())
819 } else {
820 first.clone()
821 }
822 } else {
823 first.clone()
824 }
825 } else {
826 Value::Null
827 };
828 map.insert(key.to_string(), result);
829 }
830 }
831
832 if markers.contains(&"split".to_string()) {
836 if let Some(Value::String(s)) = map.get(key) {
837 let split_idx = markers.iter().position(|m| m == "split").unwrap();
838 let sep = if split_idx + 1 < markers.len() {
839 delimiter_from_keyword(&markers[split_idx + 1])
840 } else {
841 ",".to_string()
842 };
843 let items: Vec<Value> = s
844 .split(&sep)
845 .map(|p| p.trim())
846 .filter(|p| !p.is_empty())
847 .map(|p| cast_primitive(p))
848 .collect();
849 map.insert(key.to_string(), Value::Array(items));
850 }
851 }
852
853 if markers.contains(&"join".to_string()) {
855 if let Some(Value::Array(arr)) = map.get(key) {
856 let join_idx = markers.iter().position(|m| m == "join").unwrap();
857 let sep = if join_idx + 1 < markers.len() {
858 delimiter_from_keyword(&markers[join_idx + 1])
859 } else {
860 ",".to_string()
861 };
862 let joined: String = arr
863 .iter()
864 .map(|v| value_to_string(v))
865 .collect::<Vec<_>>()
866 .join(&sep);
867 map.insert(key.to_string(), Value::String(joined));
868 }
869 }
870
871 if markers.contains(&"default".to_string()) && !markers.contains(&"env".to_string()) {
873 let is_empty = match map.get(key) {
874 Some(Value::Null) | None => true,
875 Some(Value::String(s)) if s.is_empty() => true,
876 _ => false,
877 };
878 if is_empty {
879 let di = markers.iter().position(|m| m == "default").unwrap();
880 if markers.len() > di + 1 {
881 let fallback = markers[di + 1..].join(":");
882 let resolved = if meta.type_hint.as_deref() == Some("string") {
883 Value::String(fallback)
884 } else {
885 cast_primitive(&fallback)
886 };
887 map.insert(key.to_string(), resolved);
888 }
889 }
890 }
891
892 if markers.contains(&"clamp".to_string()) {
896 let clamp_idx = markers.iter().position(|m| m == "clamp").unwrap();
897 let min_s = markers.get(clamp_idx + 1).cloned().unwrap_or_default();
898 let max_s = markers.get(clamp_idx + 2).cloned().unwrap_or_default();
899 if let (Ok(lo), Ok(hi)) = (min_s.parse::<f64>(), max_s.parse::<f64>()) {
900 if lo > hi {
901 map.insert(key.to_string(), Value::String(
902 format!("CONSTRAINT_ERR: clamp min ({}) > max ({})", lo, hi),
903 ));
904 } else if let Some(n) = map.get(key).and_then(value_as_number) {
905 let clamped = n.clamp(lo, hi);
906 let v = if clamped.fract() == 0.0 && clamped.abs() < i64::MAX as f64 {
907 Value::Int(clamped as i64)
908 } else {
909 Value::Float(clamped)
910 };
911 map.insert(key.to_string(), v);
912 }
913 }
914 }
915
916 if markers.contains(&"round".to_string()) {
920 let round_idx = markers.iter().position(|m| m == "round").unwrap();
921 let decimals: u32 = markers.get(round_idx + 1)
922 .and_then(|s| s.parse().ok())
923 .unwrap_or(0);
924 if let Some(n) = map.get(key).and_then(value_as_number) {
925 let factor = 10f64.powi(decimals as i32);
926 let rounded = (n * factor).round() / factor;
927 let v = if decimals == 0 {
928 Value::Int(rounded as i64)
929 } else {
930 Value::Float(rounded)
931 };
932 map.insert(key.to_string(), v);
933 }
934 }
935
936 if markers.contains(&"map".to_string()) {
940 if let Some(Value::Array(arr)) = map.get(key) {
941 let map_idx = markers.iter().position(|m| m == "map").unwrap();
942 let source_key = markers.get(map_idx + 1).cloned().unwrap_or_default();
943 let lookup_val = if !source_key.is_empty() {
944 let root_ref = unsafe { &*root_ptr };
945 deep_get(root_ref, &source_key)
946 .or_else(|| map.get(&source_key).cloned())
947 .map(|v| value_to_string(&v))
948 .unwrap_or_default()
949 } else {
950 match map.get(key) {
952 Some(Value::String(s)) => s.clone(),
953 _ => String::new(),
954 }
955 };
956
957 let arr_clone = arr.clone();
959 let result = arr_clone.iter().find_map(|item| {
960 if let Value::String(s) = item {
961 if let Some(space) = s.find(' ') {
962 if s[..space].trim() == lookup_val {
963 return Some(cast_primitive(s[space + 1..].trim()));
964 }
965 }
966 }
967 None
968 });
969 map.insert(key.to_string(), result.unwrap_or(Value::Null));
970 }
971 }
972
973 if markers.contains(&"format".to_string()) {
977 let fmt_idx = markers.iter().position(|m| m == "format").unwrap();
978 let pattern = markers.get(fmt_idx + 1).cloned().unwrap_or_else(|| "%s".to_string());
979 if let Some(current) = map.get(key) {
980 let formatted = apply_format_pattern(&pattern, current);
981 map.insert(key.to_string(), Value::String(formatted));
982 }
983 }
984
985 if markers.contains(&"replace".to_string()) {
990 if let Some(Value::String(s)) = map.get(key) {
991 let idx = markers.iter().position(|m| m == "replace").unwrap();
992 let from = markers.get(idx + 1).cloned().unwrap_or_default();
993 let to = markers.get(idx + 2).cloned().unwrap_or_default();
994 if !from.is_empty() {
995 let replaced = s.replace(&from, &to);
996 map.insert(key.to_string(), Value::String(replaced));
997 }
998 }
999 }
1000
1001 if markers.contains(&"sort".to_string()) {
1004 if let Some(Value::Array(arr)) = map.get(key) {
1005 let idx = markers.iter().position(|m| m == "sort").unwrap();
1006 let desc = matches!(markers.get(idx + 1).map(|s| s.as_str()), Some("desc"));
1007 let mut sorted = arr.clone();
1008 sorted.sort_by(|a, b| {
1009 match (value_as_number(a), value_as_number(b)) {
1010 (Some(an), Some(bn)) => an
1011 .partial_cmp(&bn)
1012 .unwrap_or(std::cmp::Ordering::Equal),
1013 _ => value_to_string(a).cmp(&value_to_string(b)),
1014 }
1015 });
1016 if desc { sorted.reverse(); }
1017 map.insert(key.to_string(), Value::Array(sorted));
1018 }
1019 }
1020
1021 if markers.contains(&"sum".to_string()) {
1025 if let Some(Value::Array(arr)) = map.get(key) {
1026 let mut total: f64 = 0.0;
1027 let mut all_int = true;
1028 for v in arr {
1029 match v {
1030 Value::Int(n) => total += *n as f64,
1031 Value::Float(f) => {
1032 total += *f;
1033 if f.fract() != 0.0 { all_int = false; }
1034 }
1035 Value::String(s) => {
1036 if let Ok(f) = s.parse::<f64>() {
1037 total += f;
1038 if f.fract() != 0.0 { all_int = false; }
1039 }
1040 }
1041 _ => {}
1042 }
1043 }
1044 let result = if all_int && total.fract() == 0.0 && total.abs() < i64::MAX as f64 {
1045 Value::Int(total as i64)
1046 } else {
1047 Value::Float(total)
1048 };
1049 map.insert(key.to_string(), result);
1050 }
1051 }
1052
1053 if markers.contains(&"fallback".to_string()) {
1058 let fb_idx = markers.iter().position(|m| m == "fallback").unwrap();
1059 let default_val = markers.get(fb_idx + 1).cloned().unwrap_or_default();
1060 let use_fallback = match map.get(key) {
1061 None | Some(Value::Null) => true,
1062 Some(Value::String(s)) if s.is_empty() => true,
1063 Some(Value::String(s)) => {
1064 let base = options.base_path.as_deref().unwrap_or(".");
1065 match jail_path(base, s) {
1066 Ok(safe) => !safe.exists(),
1067 Err(_) => true, }
1069 }
1070 _ => false,
1071 };
1072 if use_fallback && !default_val.is_empty() {
1073 map.insert(key.to_string(), Value::String(default_val));
1074 }
1075 }
1076
1077 if markers.contains(&"once".to_string()) {
1081 let once_idx = markers.iter().position(|m| m == "once").unwrap();
1082 let gen_type = markers.get(once_idx + 1).map(|s| s.as_str()).unwrap_or("uuid");
1083 let lock_path = options.base_path.as_deref()
1084 .map(|b| std::path::Path::new(b).join(".synx.lock"))
1085 .unwrap_or_else(|| std::path::Path::new(".synx.lock").to_path_buf());
1086
1087 let existing = read_lock_value(&lock_path, key);
1089 if let Some(locked) = existing {
1090 map.insert(key.to_string(), Value::String(locked));
1091 } else {
1092 let generated = match gen_type {
1093 "uuid" => rng::generate_uuid(),
1094 "timestamp" => std::time::SystemTime::now()
1095 .duration_since(std::time::UNIX_EPOCH)
1096 .unwrap_or_default()
1097 .as_secs()
1098 .to_string(),
1099 "random" => rng::random_usize(u32::MAX as usize).to_string(),
1100 _ => rng::generate_uuid(),
1101 };
1102 write_lock_value(&lock_path, key, &generated);
1103 map.insert(key.to_string(), Value::String(generated));
1104 }
1105 }
1106
1107 if markers.contains(&"version".to_string()) {
1112 if let Some(Value::String(current_ver)) = map.get(key) {
1113 let ver_idx = markers.iter().position(|m| m == "version").unwrap();
1114 let op = markers.get(ver_idx + 1).map(|s| s.as_str()).unwrap_or(">=");
1115 let required = markers.get(ver_idx + 2).cloned().unwrap_or_default();
1116 let result = compare_versions(current_ver, op, &required);
1117 map.insert(key.to_string(), Value::Bool(result));
1118 }
1119 }
1120
1121 if markers.contains(&"watch".to_string()) {
1125 if let Some(Value::String(file_path)) = map.get(key) {
1126 let max_depth = options.max_include_depth.unwrap_or(DEFAULT_MAX_INCLUDE_DEPTH);
1127 if options._include_depth >= max_depth {
1128 map.insert(
1129 key.to_string(),
1130 Value::String(format!("WATCH_ERR: max include depth ({}) exceeded", max_depth)),
1131 );
1132 return;
1133 }
1134 let base = options.base_path.as_deref().unwrap_or(".");
1135 let full = match jail_path(base, file_path) {
1136 Ok(p) => p,
1137 Err(e) => {
1138 map.insert(key.to_string(), Value::String(format!("WATCH_ERR: {}", e)));
1139 return;
1140 }
1141 };
1142 if let Err(e) = check_file_size(&full) {
1143 map.insert(key.to_string(), Value::String(format!("WATCH_ERR: {}", e)));
1144 return;
1145 }
1146 let watch_idx = markers.iter().position(|m| m == "watch").unwrap();
1147 let key_path = markers.get(watch_idx + 1).cloned();
1148
1149 match std::fs::read_to_string(&full) {
1150 Ok(content) => {
1151 let value = if let Some(ref kp) = key_path {
1152 extract_from_file_content(&content, kp, full.extension().and_then(|e| e.to_str()).unwrap_or("")).unwrap_or(Value::Null)
1153 } else {
1154 Value::String(content.trim().to_string())
1155 };
1156 map.insert(key.to_string(), value);
1157 }
1158 Err(e) => {
1159 map.insert(key.to_string(), Value::String(format!("WATCH_ERR: {}", fmt_io_err(&e, file_path))));
1160 }
1161 }
1162 }
1163 }
1164
1165 if markers.contains(&"prompt".to_string()) {
1170 let prompt_idx = markers.iter().position(|m| m == "prompt").unwrap();
1171 let label = markers.get(prompt_idx + 1).cloned().unwrap_or_else(|| key.to_string());
1172 if let Some(val) = map.get(key) {
1173 let synx_text = stringify_value(val, 0);
1174 let block = format!("{} (SYNX):\n```synx\n{}```", label, synx_text);
1175 map.insert(key.to_string(), Value::String(block));
1176 }
1177 }
1178
1179 #[cfg(feature = "wasm")]
1190 if let Some(ref wasm_rt) = options.wasm_runtime {
1191 for marker in markers {
1192 if crate::wasm::BUILTIN_MARKERS.contains(&marker.as_str()) {
1193 continue;
1194 }
1195 if wasm_rt.has_marker(marker) {
1196 let marker_idx = markers.iter().position(|m| m == marker).unwrap();
1198 let args: Vec<String> = markers[marker_idx + 1..].to_vec();
1199 let current_value = map.get(key).cloned().unwrap_or(Value::Null);
1200 match wasm_rt.apply_marker(marker, ¤t_value, &args) {
1201 Ok(result) => {
1202 map.insert(key.to_string(), result);
1203 }
1204 Err(e) => {
1205 map.insert(key.to_string(), Value::String(format!("WASM_ERR: {}", e)));
1206 }
1207 }
1208 break; }
1210 }
1211 }
1212
1213 if let Some(ref c) = meta.constraints {
1215 validate_constraints(map, key, c);
1216 }
1217}
1218
1219fn validate_constraints(map: &mut HashMap<String, Value>, key: &str, c: &Constraints) {
1222 let val = match map.get(key) {
1223 Some(v) => v.clone(),
1224 None => {
1225 if c.required {
1226 map.insert(key.to_string(), Value::String(
1227 format!("CONSTRAINT_ERR: '{}' is required", key),
1228 ));
1229 }
1230 return;
1231 }
1232 };
1233
1234 if c.required {
1236 let empty = matches!(val, Value::Null)
1237 || matches!(&val, Value::String(s) if s.is_empty());
1238 if empty {
1239 map.insert(key.to_string(), Value::String(
1240 format!("CONSTRAINT_ERR: '{}' is required", key),
1241 ));
1242 return;
1243 }
1244 }
1245
1246 if let Some(ref type_name) = c.type_name {
1248 let ok = match type_name.as_str() {
1249 "int" => matches!(val, Value::Int(_)),
1250 "float" => matches!(val, Value::Float(_) | Value::Int(_)),
1251 "bool" => matches!(val, Value::Bool(_)),
1252 "string" => matches!(val, Value::String(_)),
1253 _ => true,
1254 };
1255 if !ok {
1256 map.insert(key.to_string(), Value::String(
1257 format!("CONSTRAINT_ERR: '{}' expected type '{}'", key, type_name),
1258 ));
1259 return;
1260 }
1261 }
1262
1263 if let Some(ref enum_vals) = c.enum_values {
1265 let val_str = match &val {
1266 Value::String(s) => s.clone(),
1267 Value::Int(n) => n.to_string(),
1268 Value::Float(f) => f.to_string(),
1269 Value::Bool(b) => b.to_string(),
1270 _ => String::new(),
1271 };
1272 if !enum_vals.contains(&val_str) {
1273 map.insert(key.to_string(), Value::String(
1274 format!("CONSTRAINT_ERR: '{}' must be one of [{}]", key, enum_vals.join("|")),
1275 ));
1276 return;
1277 }
1278 }
1279
1280 let num = match &val {
1282 Value::Int(n) => Some(*n as f64),
1283 Value::Float(f) => Some(*f),
1284 Value::String(s) if c.min.is_some() || c.max.is_some() => Some(s.len() as f64),
1285 _ => None,
1286 };
1287 if let Some(n) = num {
1288 if let Some(min) = c.min {
1289 if n < min {
1290 map.insert(key.to_string(), Value::String(
1291 format!("CONSTRAINT_ERR: '{}' value {} is below min {}", key, n, min),
1292 ));
1293 return;
1294 }
1295 }
1296 if let Some(max) = c.max {
1297 if n > max {
1298 map.insert(key.to_string(), Value::String(
1299 format!("CONSTRAINT_ERR: '{}' value {} exceeds max {}", key, n, max),
1300 ));
1301 return;
1302 }
1303 }
1304 }
1305
1306 if let Some(ref pat) = c.pattern {
1308 if pat.len() <= 256 {
1309 if let Value::String(ref s) = val {
1310 match regex::Regex::new(pat) {
1311 Ok(re) if !re.is_match(s) => {
1312 map.insert(key.to_string(), Value::String(
1313 format!("CONSTRAINT_ERR: '{}' does not match pattern /{}/", key, pat),
1314 ));
1315 return;
1316 }
1317 Ok(_) => {}
1318 Err(_) => {}
1320 }
1321 }
1322 }
1323 }
1324}
1325
1326fn apply_format_pattern(pattern: &str, value: &Value) -> String {
1330 match value {
1331 Value::Int(n) => {
1332 if pattern.contains('d') || pattern.contains('i') {
1333 format_int_pattern(pattern, *n)
1334 } else if pattern.contains('f') || pattern.contains('e') {
1335 format_float_pattern(pattern, *n as f64)
1336 } else {
1337 n.to_string()
1338 }
1339 }
1340 Value::Float(f) => {
1341 if pattern.contains('f') || pattern.contains('e') {
1342 format_float_pattern(pattern, *f)
1343 } else {
1344 format_number(*f)
1345 }
1346 }
1347 Value::String(s) => s.clone(),
1348 other => value_to_string(other),
1349 }
1350}
1351
1352fn format_int_pattern(pattern: &str, n: i64) -> String {
1353 const MAX_FMT_WIDTH: usize = 4096;
1356 if let Some(s) = pattern.strip_prefix('%') {
1357 if let Some(inner) = s.strip_suffix('d').or_else(|| s.strip_suffix('i')) {
1358 if let Some(w) = inner.strip_prefix('0') {
1359 if let Ok(width) = w.parse::<usize>() {
1360 let width = width.min(MAX_FMT_WIDTH);
1361 return format!("{:0>width$}", n, width = width);
1362 }
1363 }
1364 if let Ok(width) = inner.parse::<usize>() {
1365 let width = width.min(MAX_FMT_WIDTH);
1366 return format!("{:>width$}", n, width = width);
1367 }
1368 }
1369 }
1370 n.to_string()
1371}
1372
1373fn format_float_pattern(pattern: &str, f: f64) -> String {
1374 const MAX_FMT_PREC: usize = 1024;
1376 if let Some(s) = pattern.strip_prefix('%') {
1377 if s == "e" {
1380 if f == 0.0 {
1381 return "0e+0".to_string();
1382 }
1383 let raw = format!("{:e}", f); if let Some(epos) = raw.rfind('e') {
1388 let mantissa = &raw[..epos];
1389 let exp_part = &raw[epos + 1..];
1390 let (sign, digits) = match exp_part.chars().next() {
1391 Some('+') => ('+', &exp_part[1..]),
1392 Some('-') => ('-', &exp_part[1..]),
1393 _ => ('+', exp_part),
1394 };
1395 let mantissa_clean = if mantissa.ends_with(".0") {
1397 &mantissa[..mantissa.len() - 2]
1398 } else {
1399 mantissa
1400 };
1401 return format!("{}e{}{}", mantissa_clean, sign, digits);
1402 }
1403 return raw;
1404 }
1405 if let Some(inner) = s.strip_suffix('f').or_else(|| s.strip_suffix('e')) {
1406 if let Some(prec_s) = inner.strip_prefix('.') {
1407 if let Ok(prec) = prec_s.parse::<usize>() {
1408 let prec = prec.min(MAX_FMT_PREC);
1409 if s.ends_with('e') {
1410 return format!("{:.prec$e}", f, prec = prec);
1411 }
1412 return format!("{:.prec$}", f, prec = prec);
1413 }
1414 }
1415 }
1416 }
1417 f.to_string()
1418}
1419
1420fn read_lock_value(lock_path: &std::path::Path, key: &str) -> Option<String> {
1422 let content = std::fs::read_to_string(lock_path).ok()?;
1423 for line in content.lines() {
1424 if let Some(rest) = line.strip_prefix(key) {
1425 if rest.starts_with(' ') {
1426 return Some(rest.trim_start().to_string());
1427 }
1428 }
1429 }
1430 None
1431}
1432
1433fn write_lock_value(lock_path: &std::path::Path, key: &str, value: &str) {
1435 let mut lines: Vec<String> = std::fs::read_to_string(lock_path)
1436 .unwrap_or_default()
1437 .lines()
1438 .map(|l| l.to_string())
1439 .collect();
1440
1441 let new_line = format!("{} {}", key, value);
1442 let mut found = false;
1443 for line in lines.iter_mut() {
1444 if line.starts_with(key) && line[key.len()..].starts_with(' ') {
1445 *line = new_line.clone();
1446 found = true;
1447 break;
1448 }
1449 }
1450 if !found {
1451 lines.push(new_line);
1452 }
1453 let _ = std::fs::write(lock_path, lines.join("\n") + "\n");
1454}
1455
1456fn compare_versions(current: &str, op: &str, required: &str) -> bool {
1458 let parse_ver = |s: &str| -> Vec<u64> {
1459 s.split('.').filter_map(|p| p.parse().ok()).collect()
1460 };
1461 let cv = parse_ver(current);
1462 let rv = parse_ver(required);
1463 let len = cv.len().max(rv.len());
1464 let mut ord = std::cmp::Ordering::Equal;
1465 for i in 0..len {
1466 let a = cv.get(i).copied().unwrap_or(0);
1467 let b = rv.get(i).copied().unwrap_or(0);
1468 if a != b {
1469 ord = a.cmp(&b);
1470 break;
1471 }
1472 }
1473 match op {
1474 ">=" => ord != std::cmp::Ordering::Less,
1475 "<=" => ord != std::cmp::Ordering::Greater,
1476 ">" => ord == std::cmp::Ordering::Greater,
1477 "<" => ord == std::cmp::Ordering::Less,
1478 "==" | "=" => ord == std::cmp::Ordering::Equal,
1479 "!=" => ord != std::cmp::Ordering::Equal,
1480 _ => false,
1481 }
1482}
1483
1484fn allow_spam_access(bucket_key: &str, max_calls: usize, window_sec: u64) -> bool {
1485 let now = Instant::now();
1486 let window = Duration::from_secs(window_sec.max(1));
1487
1488 let buckets = SPAM_BUCKETS.get_or_init(|| Mutex::new(HashMap::new()));
1489 let mut guard = match buckets.lock() {
1490 Ok(g) => g,
1491 Err(poisoned) => poisoned.into_inner(),
1492 };
1493
1494 let calls = guard.entry(bucket_key.to_string()).or_default();
1495 calls.retain(|ts| now.duration_since(*ts) <= window);
1496
1497 if calls.len() >= max_calls {
1498 return false;
1499 }
1500
1501 calls.push(now);
1502 true
1503}
1504
1505#[cfg(test)]
1506fn clear_spam_buckets() {
1507 let buckets = SPAM_BUCKETS.get_or_init(|| Mutex::new(HashMap::new()));
1508 if let Ok(mut guard) = buckets.lock() {
1509 guard.clear();
1510 }
1511}
1512
1513fn extract_from_file_content(content: &str, key_path: &str, ext: &str) -> Option<Value> {
1515 if ext == "json" {
1516 let parsed: serde_json::Value = serde_json::from_str(content).ok()?;
1518 let mut current = &parsed;
1519 for part in key_path.split('.') {
1520 current = current.get(part)?;
1521 }
1522 return Some(json_value_to_synx(current));
1523 }
1524
1525 let parsed = crate::parser::parse(content);
1527 let mut current = &parsed.root;
1528 for part in key_path.split('.') {
1529 match current {
1530 Value::Object(map) => {
1531 current = map.get(part)?;
1532 }
1533 _ => return None,
1534 }
1535 }
1536 Some(current.clone())
1537}
1538
1539fn json_value_to_synx(v: &serde_json::Value) -> Value {
1541 match v {
1542 serde_json::Value::Null => Value::Null,
1543 serde_json::Value::Bool(b) => Value::Bool(*b),
1544 serde_json::Value::Number(n) => {
1545 if let Some(i) = n.as_i64() {
1546 Value::Int(i)
1547 } else if let Some(f) = n.as_f64() {
1548 Value::Float(f)
1549 } else {
1550 Value::Null
1551 }
1552 }
1553 serde_json::Value::String(s) => Value::String(s.clone()),
1554 serde_json::Value::Array(arr) => {
1555 Value::Array(arr.iter().map(json_value_to_synx).collect())
1556 }
1557 serde_json::Value::Object(map) => {
1558 let mut out = HashMap::new();
1559 for (k, v) in map {
1560 out.insert(k.clone(), json_value_to_synx(v));
1561 }
1562 Value::Object(out)
1563 }
1564 }
1565}
1566
1567fn stringify_value(value: &Value, indent: usize) -> String {
1571 let spaces = " ".repeat(indent);
1572 match value {
1573 Value::Object(map) => {
1574 let mut out = String::new();
1575 let mut keys: Vec<&str> = map.keys().map(|k| k.as_str()).collect();
1576 keys.sort_unstable();
1577 for key in keys {
1578 let val = &map[key];
1579 match val {
1580 Value::Object(_) => {
1581 out.push_str(&format!("{}{}\n", spaces, key));
1582 out.push_str(&stringify_value(val, indent + 2));
1583 }
1584 Value::Array(arr) => {
1585 out.push_str(&format!("{}{}\n", spaces, key));
1586 for item in arr {
1587 out.push_str(&format!("{} - {}\n", spaces, value_to_string(item)));
1588 }
1589 }
1590 _ => {
1591 out.push_str(&format!("{}{} {}\n", spaces, key, value_to_string(val)));
1592 }
1593 }
1594 }
1595 out
1596 }
1597 _ => format!("{}{}\n", spaces, value_to_string(value)),
1598 }
1599}
1600
1601pub(crate) fn cast_primitive(val: &str) -> Value {
1602 if val.len() >= 2 {
1604 let bytes = val.as_bytes();
1605 if (bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"')
1606 || (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\'')
1607 {
1608 return Value::String(val[1..val.len() - 1].to_string());
1609 }
1610 }
1611 match val {
1612 "true" => Value::Bool(true),
1613 "false" => Value::Bool(false),
1614 "null" => Value::Null,
1615 _ => {
1616 if let Ok(i) = val.parse::<i64>() {
1617 Value::Int(i)
1618 } else if let Ok(f) = val.parse::<f64>() {
1619 Value::Float(f)
1620 } else {
1621 Value::String(val.to_string())
1622 }
1623 }
1624 }
1625}
1626
1627fn delimiter_from_keyword(keyword: &str) -> String {
1628 match keyword {
1629 "space" => " ".to_string(),
1630 "pipe" => "|".to_string(),
1631 "dash" => "-".to_string(),
1632 "dot" => ".".to_string(),
1633 "semi" => ";".to_string(),
1634 "tab" => "\t".to_string(),
1635 "slash" => "/".to_string(),
1636 other => other.to_string(),
1637 }
1638}
1639
1640fn value_as_number(v: &Value) -> Option<f64> {
1641 match v {
1642 Value::Int(n) => Some(*n as f64),
1643 Value::Float(f) => Some(*f),
1644 _ => None,
1645 }
1646}
1647
1648fn value_to_string(v: &Value) -> String {
1649 match v {
1650 Value::String(s) => s.clone(),
1651 Value::Int(n) => n.to_string(),
1652 Value::Float(f) => format_number(*f as f64),
1653 Value::Bool(b) => b.to_string(),
1654 Value::Null => "null".to_string(),
1655 Value::Secret(_) => "[SECRET]".to_string(),
1661 Value::Array(_) | Value::Object(_) => String::new(),
1662 }
1663}
1664
1665fn format_number(n: f64) -> String {
1666 if n.fract() == 0.0 && n.abs() < i64::MAX as f64 {
1667 (n as i64).to_string()
1668 } else {
1669 n.to_string()
1670 }
1671}
1672
1673fn replace_word(haystack: &str, word: &str, replacement: &str) -> String {
1675 let word_bytes = word.as_bytes();
1676 let word_len = word_bytes.len();
1677 let hay_bytes = haystack.as_bytes();
1678 let hay_len = hay_bytes.len();
1679
1680 if word_len > hay_len {
1681 return haystack.to_string();
1682 }
1683
1684 let mut result = String::with_capacity(hay_len.min(MAX_ENGINE_SCRATCH_STRING));
1685 let mut i = 0;
1686
1687 while i <= hay_len - word_len {
1688 if result.len() >= MAX_ENGINE_SCRATCH_STRING {
1689 break;
1690 }
1691 if &hay_bytes[i..i + word_len] == word_bytes {
1692 let before_ok = i == 0 || !is_word_char(hay_bytes[i - 1]);
1693 let after_ok = i + word_len >= hay_len || !is_word_char(hay_bytes[i + word_len]);
1694 if before_ok && after_ok {
1695 let room = MAX_ENGINE_SCRATCH_STRING.saturating_sub(result.len());
1696 if room > 0 {
1697 let take = replacement.len().min(room);
1698 let end = replacement.floor_char_boundary(take);
1699 result.push_str(&replacement[..end]);
1700 }
1701 i += word_len;
1702 continue;
1703 }
1704 }
1705 if result.len() < MAX_ENGINE_SCRATCH_STRING {
1706 result.push(hay_bytes[i] as char);
1707 }
1708 i += 1;
1709 }
1710 while i < hay_len && result.len() < MAX_ENGINE_SCRATCH_STRING {
1711 result.push(hay_bytes[i] as char);
1712 i += 1;
1713 }
1714 result
1715}
1716
1717fn is_word_char(b: u8) -> bool {
1718 b.is_ascii_alphanumeric() || b == b'_'
1719}
1720
1721fn weighted_random(items: &[Value], weights: &[f64]) -> Value {
1722 let mut w: Vec<f64> = weights.to_vec();
1723 if w.len() < items.len() {
1724 let assigned: f64 = w.iter().sum();
1725 let per_item = if assigned < 100.0 {
1729 (100.0 - assigned) / (items.len() - w.len()) as f64
1730 } else {
1731 assigned / w.len() as f64
1732 };
1733 while w.len() < items.len() {
1734 w.push(per_item);
1735 }
1736 }
1737 let total: f64 = w.iter().sum();
1738 if total <= 0.0 {
1739 return items[rng::random_usize(items.len())].clone();
1740 }
1741
1742 let rand_val = rng::random_f64_01();
1743 let mut cumulative = 0.0;
1744 for (i, item) in items.iter().enumerate() {
1745 cumulative += w[i] / total;
1746 if rand_val <= cumulative {
1747 return item.clone();
1748 }
1749 }
1750 items.last().cloned().unwrap_or(Value::Null)
1751}
1752
1753fn needs_resolution_before_secret(markers: &[String]) -> bool {
1759 const RESOLVING: &[&str] = &[
1760 "env", "include", "import", "watch", "ref", "alias", "calc", "map", "i18n", "default",
1761 "fallback", "once", "split", "join", "format", "replace",
1762 ];
1763 markers.iter().any(|m| RESOLVING.contains(&m.as_str()))
1764}
1765
1766fn mask_secret_literals(value: &mut Value, metadata: &HashMap<String, MetaMap>, path: &str) {
1769 let map = match value {
1770 Value::Object(m) => m,
1771 _ => return,
1772 };
1773 if let Some(mm) = metadata.get(path) {
1774 let secret_keys: Vec<String> = mm
1775 .iter()
1776 .filter(|(_, meta)| {
1777 meta.markers.iter().any(|m| m == "secret")
1778 && !needs_resolution_before_secret(&meta.markers)
1779 })
1780 .map(|(key, _)| key.clone())
1781 .collect();
1782 for key in secret_keys {
1783 if let Some(v) = map.get(key.as_str()) {
1784 if !matches!(v, Value::Secret(_) | Value::Object(_) | Value::Array(_)) {
1785 let raw = value_to_string(v);
1786 map.insert(key, Value::Secret(raw));
1787 }
1788 }
1789 }
1790 }
1791 let keys: Vec<String> = map.keys().cloned().collect();
1792 for key in keys {
1793 let child_path = if path.is_empty() { key.clone() } else { format!("{}.{}", path, key) };
1794 if let Some(child) = map.get_mut(&key) {
1795 match child {
1796 Value::Object(_) => mask_secret_literals(child, metadata, &child_path),
1797 Value::Array(arr) => {
1798 for item in arr.iter_mut() {
1799 if let Value::Object(_) = item {
1800 mask_secret_literals(item, metadata, &child_path);
1801 }
1802 }
1803 }
1804 _ => {}
1805 }
1806 }
1807 }
1808}
1809
1810fn apply_inheritance(root: &mut Value, metadata: &HashMap<String, MetaMap>) {
1811 let root_meta = match metadata.get("") {
1812 Some(m) => m.clone(),
1813 None => return,
1814 };
1815
1816 let root_map = match root.as_object_mut() {
1817 Some(m) => m as *mut HashMap<String, Value>,
1818 None => return,
1819 };
1820
1821 let mut inherits: Vec<(String, Vec<String>)> = Vec::new();
1827 for (key, meta) in &root_meta {
1828 if meta.markers.contains(&"inherit".to_string()) {
1829 let idx = meta.markers.iter().position(|m| m == "inherit").unwrap();
1830 let mut parents: Vec<String> = meta.markers[idx + 1..].to_vec();
1831 if parents.is_empty() && !meta.args.is_empty() {
1833 parents = meta.args.clone();
1834 }
1835 if !parents.is_empty() {
1836 inherits.push((key.clone(), parents));
1837 }
1838 }
1839 }
1840
1841 let map = unsafe { &mut *root_map };
1842 for (child_key, parents) in &inherits {
1843 let mut merged: HashMap<String, Value> = HashMap::new();
1845 for parent_name in parents {
1846 if let Some(Value::Object(p)) = map.get(parent_name) {
1847 for (k, v) in p {
1848 merged.insert(k.clone(), v.clone());
1849 }
1850 }
1851 }
1852 if let Some(Value::Object(c)) = map.get(child_key) {
1854 for (k, v) in c {
1855 merged.insert(k.clone(), v.clone());
1856 }
1857 }
1858 map.insert(child_key.clone(), Value::Object(merged));
1859 }
1860}
1861
1862fn deep_get(root: &Value, path: &str) -> Option<Value> {
1863 if let Value::Object(map) = root {
1865 if let Some(val) = map.get(path) {
1866 return Some(val.clone());
1867 }
1868 }
1869 let parts: Vec<&str> = path.split('.').collect();
1871 let mut current = root;
1872 for part in parts {
1873 match current {
1874 Value::Object(map) => match map.get(part) {
1875 Some(v) => current = v,
1876 None => return None,
1877 },
1878 _ => return None,
1879 }
1880 }
1881 Some(current.clone())
1882}
1883
1884fn resolve_interpolation(
1887 tpl: &str,
1888 root: &Value,
1889 local_map: &HashMap<String, Value>,
1890 includes: &HashMap<String, Value>,
1891) -> String {
1892 let bytes = tpl.as_bytes();
1893 let len = bytes.len();
1894 let mut result = String::with_capacity(len.min(MAX_ENGINE_SCRATCH_STRING));
1895 let mut i = 0;
1896
1897 while i < len {
1898 if result.len() >= MAX_ENGINE_SCRATCH_STRING {
1899 break;
1900 }
1901 if bytes[i] == b'{' {
1902 if let Some(close) = tpl[i + 1..].find('}') {
1903 let inner = &tpl[i + 1..i + 1 + close];
1904 if let Some(colon) = inner.find(':') {
1906 let ref_name = &inner[..colon];
1907 let scope = &inner[colon + 1..];
1908 if ref_name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '.') {
1910 let resolved = if scope == "include" {
1911 if includes.len() == 1 {
1913 let first = includes.values().next().unwrap();
1914 deep_get(first, ref_name)
1915 } else {
1916 None
1917 }
1918 } else {
1919 includes.get(scope).and_then(|inc| deep_get(inc, ref_name))
1921 };
1922 if let Some(val) = resolved {
1923 let s = value_to_string(&val);
1924 let room = MAX_ENGINE_SCRATCH_STRING.saturating_sub(result.len());
1925 if room > 0 {
1926 let take = s.len().min(room);
1927 let end = s.floor_char_boundary(take);
1928 result.push_str(&s[..end]);
1929 }
1930 } else {
1931 result.push('{');
1932 let rem = MAX_ENGINE_SCRATCH_STRING.saturating_sub(result.len() + 1);
1933 if rem > 0 {
1934 let end = inner.floor_char_boundary(inner.len().min(rem));
1935 result.push_str(&inner[..end]);
1936 }
1937 if result.len() < MAX_ENGINE_SCRATCH_STRING {
1938 result.push('}');
1939 }
1940 }
1941 i += 2 + close;
1942 continue;
1943 }
1944 } else {
1945 let ref_name = inner;
1947 if ref_name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '.') {
1948 let resolved = deep_get(root, ref_name).or_else(|| {
1949 local_map.get(ref_name).cloned()
1950 });
1951 if let Some(val) = resolved {
1952 let s = value_to_string(&val);
1953 let room = MAX_ENGINE_SCRATCH_STRING.saturating_sub(result.len());
1954 if room > 0 {
1955 let take = s.len().min(room);
1956 let end = s.floor_char_boundary(take);
1957 result.push_str(&s[..end]);
1958 }
1959 } else {
1960 result.push('{');
1961 let rem = MAX_ENGINE_SCRATCH_STRING.saturating_sub(result.len() + 1);
1962 if rem > 0 {
1963 let end = ref_name.floor_char_boundary(ref_name.len().min(rem));
1964 result.push_str(&ref_name[..end]);
1965 }
1966 if result.len() < MAX_ENGINE_SCRATCH_STRING {
1967 result.push('}');
1968 }
1969 }
1970 i += 2 + close;
1971 continue;
1972 }
1973 }
1974 }
1975 }
1976 let ch = tpl[i..].chars().next().unwrap_or('\u{fffd}');
1980 if result.len() < MAX_ENGINE_SCRATCH_STRING {
1981 result.push(ch);
1982 }
1983 i += ch.len_utf8();
1984 }
1985 result
1986}
1987
1988fn load_includes(
1990 directives: &[IncludeDirective],
1991 options: &Options,
1992) -> HashMap<String, Value> {
1993 let mut map = HashMap::new();
1994 let base = options.base_path.as_deref().unwrap_or(".");
1995 let max_depth = options.max_include_depth.unwrap_or(DEFAULT_MAX_INCLUDE_DEPTH);
1996 if options._include_depth >= max_depth {
1997 return map;
1998 }
1999 for inc in directives {
2000 let full = match jail_path(base, &inc.path) {
2001 Ok(p) => p,
2002 Err(_) => continue,
2003 };
2004 if check_file_size(&full).is_err() {
2005 continue;
2006 }
2007 if let Ok(text) = std::fs::read_to_string(&full) {
2008 let mut included = parser::parse(&text);
2009 if included.mode == Mode::Active {
2010 let mut child_opts = options.clone();
2011 child_opts._include_depth += 1;
2012 if let Some(parent) = full.parent() {
2013 child_opts.base_path = Some(parent.to_string_lossy().into_owned());
2014 }
2015 resolve(&mut included, &child_opts);
2016 }
2017 map.insert(inc.alias.clone(), included.root);
2018 }
2019 }
2020 map
2021}
2022
2023fn load_packages(
2028 directives: &[UseDirective],
2029 options: &Options,
2030 #[cfg(feature = "wasm")] wasm_runtime: &mut crate::wasm::WasmMarkerRuntime,
2031) -> HashMap<String, Value> {
2032 let mut map = HashMap::new();
2033 let pkg_base = options
2034 .packages_path
2035 .as_deref()
2036 .unwrap_or("./synx_packages");
2037 let base = options.base_path.as_deref().unwrap_or(".");
2038 let pkg_root = std::path::Path::new(base).join(pkg_base);
2039
2040 for ud in directives {
2041 let pkg_dir = pkg_root.join(&ud.package);
2043
2044 #[cfg(feature = "wasm")]
2046 {
2047 if is_marker_package(&pkg_dir) {
2048 let wasm_entry = read_manifest_wasm(&pkg_dir)
2050 .unwrap_or_else(|| pkg_dir.join("src").join("main.wasm"));
2051 let caps = read_manifest_capabilities(&pkg_dir);
2052 if wasm_entry.is_file() {
2053 if let Ok(wasm_bytes) = std::fs::read(&wasm_entry) {
2054 match wasm_runtime.load_module(&wasm_bytes, caps) {
2055 Ok(_markers) => {}
2056 Err(e) => {
2057 map.insert(
2058 ud.alias.clone(),
2059 Value::String(format!("WASM_ERR: {}", e)),
2060 );
2061 }
2062 }
2063 }
2064 }
2065 continue;
2066 }
2067 }
2068
2069 let entry = read_manifest_main(&pkg_dir)
2071 .unwrap_or_else(|| pkg_dir.join("src").join("main.synx"));
2072
2073 if !entry.is_file() {
2074 continue;
2075 }
2076 if check_file_size(&entry).is_err() {
2077 continue;
2078 }
2079 if let Ok(text) = std::fs::read_to_string(&entry) {
2080 let mut parsed = parser::parse(&text);
2081 if parsed.mode == Mode::Active {
2082 let mut child_opts = options.clone();
2083 child_opts._include_depth += 1;
2084 child_opts.base_path = Some(
2085 entry.parent().unwrap_or(&pkg_dir).to_string_lossy().into_owned()
2086 );
2087 resolve(&mut parsed, &child_opts);
2088 }
2089 map.insert(ud.alias.clone(), parsed.root);
2090 }
2091 }
2092 map
2093}
2094
2095fn read_manifest_main(pkg_dir: &std::path::Path) -> Option<std::path::PathBuf> {
2098 let manifest = pkg_dir.join("synx-pkg.synx");
2099 let text = std::fs::read_to_string(&manifest).ok()?;
2100 for line in text.lines() {
2101 let trimmed = line.trim();
2102 if let Some(rest) = trimmed.strip_prefix("main ") {
2103 let entry_path = rest.trim();
2104 if !entry_path.is_empty() {
2105 return Some(pkg_dir.join(entry_path));
2106 }
2107 }
2108 if let Some(rest) = trimmed.strip_prefix("entry ") {
2110 let entry_path = rest.trim();
2111 if !entry_path.is_empty() {
2112 return Some(pkg_dir.join(entry_path));
2113 }
2114 }
2115 }
2116 None
2117}
2118
2119#[cfg(feature = "wasm")]
2122fn is_marker_package(pkg_dir: &std::path::Path) -> bool {
2123 let manifest = pkg_dir.join("synx-pkg.synx");
2124 if let Ok(text) = std::fs::read_to_string(&manifest) {
2125 for line in text.lines() {
2126 let trimmed = line.trim();
2127 if trimmed == "type markers" {
2128 return true;
2129 }
2130 if let Some(rest) = trimmed.strip_prefix("main ") {
2131 if rest.trim().ends_with(".wasm") {
2132 return true;
2133 }
2134 }
2135 }
2136 }
2137 false
2138}
2139
2140#[cfg(feature = "wasm")]
2142fn read_manifest_wasm(pkg_dir: &std::path::Path) -> Option<std::path::PathBuf> {
2143 let manifest = pkg_dir.join("synx-pkg.synx");
2144 let text = std::fs::read_to_string(&manifest).ok()?;
2145 for line in text.lines() {
2146 let trimmed = line.trim();
2147 if let Some(rest) = trimmed.strip_prefix("wasm ") {
2148 let wasm_path = rest.trim();
2149 if !wasm_path.is_empty() {
2150 return Some(pkg_dir.join(wasm_path));
2151 }
2152 }
2153 if let Some(rest) = trimmed.strip_prefix("main ") {
2155 let path = rest.trim();
2156 if path.ends_with(".wasm") {
2157 return Some(pkg_dir.join(path));
2158 }
2159 }
2160 }
2161 None
2162}
2163
2164#[cfg(feature = "wasm")]
2166fn read_manifest_capabilities(pkg_dir: &std::path::Path) -> crate::wasm::WasmCapabilities {
2167 let manifest = pkg_dir.join("synx-pkg.synx");
2168 let text = match std::fs::read_to_string(&manifest) {
2169 Ok(t) => t,
2170 Err(_) => return crate::wasm::WasmCapabilities::default(),
2171 };
2172 for line in text.lines() {
2173 let trimmed = line.trim();
2174 if let Some(rest) = trimmed.strip_prefix("permissions ") {
2175 return crate::wasm::WasmCapabilities::from_manifest_line(rest);
2176 }
2177 }
2178 crate::wasm::WasmCapabilities::default()
2179}
2180
2181fn build_type_registry(metadata: &HashMap<String, MetaMap>) -> HashMap<String, String> {
2186 let mut registry: HashMap<String, String> = HashMap::new();
2187
2188 for meta_map in metadata.values() {
2189 for (key, meta) in meta_map {
2190 if let Some(ref type_hint) = meta.type_hint {
2191 if let Some(existing) = registry.get(key) {
2193 if existing != type_hint {
2194 }
2197 } else {
2198 registry.insert(key.clone(), type_hint.clone());
2199 }
2200 }
2201 }
2202 }
2203
2204 registry
2205}
2206
2207fn build_constraint_registry(metadata: &HashMap<String, MetaMap>) -> HashMap<String, Constraints> {
2210 let mut registry: HashMap<String, Constraints> = HashMap::new();
2211
2212 for meta_map in metadata.values() {
2213 for (key, meta) in meta_map {
2214 if let Some(ref constraints) = meta.constraints {
2215 registry
2216 .entry(key.clone())
2217 .and_modify(|existing| merge_constraints(existing, constraints))
2218 .or_insert_with(|| constraints.clone());
2219 }
2220 }
2221 }
2222
2223 registry
2224}
2225
2226fn merge_constraints(base: &mut Constraints, incoming: &Constraints) {
2229 if incoming.required {
2230 base.required = true;
2231 }
2232 if incoming.readonly {
2233 base.readonly = true;
2234 }
2235
2236 base.min = match (base.min, incoming.min) {
2238 (Some(a), Some(b)) => Some(a.max(b)),
2239 (None, Some(b)) => Some(b),
2240 (a, None) => a,
2241 };
2242 base.max = match (base.max, incoming.max) {
2243 (Some(a), Some(b)) => Some(a.min(b)),
2244 (None, Some(b)) => Some(b),
2245 (a, None) => a,
2246 };
2247
2248 if base.type_name.is_none() {
2250 base.type_name = incoming.type_name.clone();
2251 }
2252 if base.pattern.is_none() {
2253 base.pattern = incoming.pattern.clone();
2254 }
2255 if base.enum_values.is_none() {
2256 base.enum_values = incoming.enum_values.clone();
2257 }
2258}
2259
2260fn validate_field_constraints(value: &mut Value, registry: &HashMap<String, Constraints>) {
2268 if let Value::Object(ref mut map) = value {
2269 let keys: Vec<String> = map.keys().cloned().collect();
2270 for key in &keys {
2271 if let Some(constraints) = registry.get(key) {
2272 let already_errored = matches!(
2273 map.get(key),
2274 Some(Value::String(s)) if s.starts_with("CONSTRAINT_ERR:")
2275 || s.starts_with("TYPE_ERR:")
2276 );
2277 if !already_errored {
2278 validate_constraints(map, key, constraints);
2279 }
2280 }
2281
2282 if let Some(child) = map.get_mut(key) {
2283 match child {
2284 Value::Object(_) => validate_field_constraints(child, registry),
2285 Value::Array(arr) => {
2286 for item in arr.iter_mut() {
2287 if let Value::Object(_) = item {
2288 validate_field_constraints(item, registry);
2289 }
2290 }
2291 }
2292 _ => {}
2293 }
2294 }
2295 }
2296 }
2297}
2298
2299fn validate_field_types(value: &mut Value, registry: &HashMap<String, String>, path: &str) {
2301 match value {
2302 Value::Object(ref mut map) => {
2303 let keys: Vec<String> = map.keys().cloned().collect();
2304 for key in &keys {
2305 if let Some(expected_type) = registry.get(key) {
2306 if let Some(val) = map.get(key) {
2307 if !value_matches_type(val, expected_type) {
2308 let current_type = value_type_name(val);
2310 map.insert(key.clone(), Value::String(
2311 format!("TYPE_ERR: '{}' expected {} but got {}", key, expected_type, current_type)
2312 ));
2313 }
2314 }
2315 }
2316
2317 if let Some(child) = map.get_mut(key) {
2319 match child {
2320 Value::Object(_) => {
2321 let child_path = if path.is_empty() {
2322 key.clone()
2323 } else {
2324 format!("{}.{}", path, key)
2325 };
2326 validate_field_types(child, registry, &child_path);
2327 }
2328 Value::Array(ref mut arr) => {
2329 for item in arr.iter_mut() {
2330 if let Value::Object(_) = item {
2331 validate_field_types(item, registry, path);
2332 }
2333 }
2334 }
2335 _ => {}
2336 }
2337 }
2338 }
2339 }
2340 _ => {}
2341 }
2342}
2343
2344fn value_matches_type(value: &Value, expected_type: &str) -> bool {
2346 match expected_type {
2347 "int" => matches!(value, Value::Int(_)),
2348 "float" => matches!(value, Value::Float(_) | Value::Int(_)),
2349 "bool" => matches!(value, Value::Bool(_)),
2350 "string" => matches!(value, Value::String(_) | Value::Secret(_)),
2351 "array" => matches!(value, Value::Array(_)),
2352 "object" => matches!(value, Value::Object(_)),
2353 _ => true, }
2355}
2356
2357fn value_type_name(value: &Value) -> String {
2359 match value {
2360 Value::Int(_) => "int".to_string(),
2361 Value::Float(_) => "float".to_string(),
2362 Value::Bool(_) => "bool".to_string(),
2363 Value::String(_) => "string".to_string(),
2364 Value::Secret(_) => "secret".to_string(),
2365 Value::Array(_) => "array".to_string(),
2366 Value::Object(_) => "object".to_string(),
2367 Value::Null => "null".to_string(),
2368 }
2369}
2370
2371fn plural_category(lang: &str, n: i64) -> &'static str {
2376 let abs_n = n.unsigned_abs();
2377 let n10 = abs_n % 10;
2378 let n100 = abs_n % 100;
2379
2380 match lang {
2381 "ru" | "uk" | "be" => {
2383 if n10 == 1 && n100 != 11 {
2384 "one"
2385 } else if (2..=4).contains(&n10) && !(12..=14).contains(&n100) {
2386 "few"
2387 } else {
2388 "many"
2389 }
2390 }
2391 "pl" => {
2393 if n10 == 1 && n100 != 11 {
2394 "one"
2395 } else if (2..=4).contains(&n10) && !(12..=14).contains(&n100) {
2396 "few"
2397 } else {
2398 "many"
2399 }
2400 }
2401 "cs" | "sk" => {
2403 if abs_n == 1 { "one" }
2404 else if (2..=4).contains(&abs_n) { "few" }
2405 else { "other" }
2406 }
2407 "ar" => {
2409 if abs_n == 0 { "zero" }
2410 else if abs_n == 1 { "one" }
2411 else if abs_n == 2 { "two" }
2412 else if (3..=10).contains(&n100) { "few" }
2413 else if (11..=99).contains(&n100) { "many" }
2414 else { "other" }
2415 }
2416 "fr" | "pt" => {
2418 if abs_n <= 1 { "one" } else { "other" }
2419 }
2420 "ja" | "zh" | "ko" | "vi" | "th" => "other",
2422 _ => {
2424 if abs_n == 1 { "one" } else { "other" }
2425 }
2426 }
2427}
2428
2429#[cfg(test)]
2430mod tests {
2431 use crate::{parse, Options, Value};
2432 use super::{resolve, read_manifest_main};
2433 use std::sync::Mutex;
2434
2435 static SPAM_TESTS: Mutex<()> = Mutex::new(());
2439
2440 #[test]
2441 fn test_ref_simple() {
2442 let mut r = parse("!active\nbase_rate 50\nquick_rate:ref base_rate");
2443 resolve(&mut r, &Options::default());
2444 let map = r.root.as_object().unwrap();
2445 assert_eq!(map["quick_rate"], Value::Int(50));
2446 }
2447
2448 #[test]
2449 fn test_ref_calc_shorthand() {
2450 let mut r = parse("!active\nbase_rate 50\ndouble_rate:ref:calc:*2 base_rate");
2451 resolve(&mut r, &Options::default());
2452 let map = r.root.as_object().unwrap();
2453 assert_eq!(map["double_rate"], Value::Int(100));
2454 }
2455
2456 #[test]
2457 fn test_inherit() {
2458 let mut r = parse("!active\n_base\n weight 10\n stackable true\nsteel:inherit:_base\n weight 25\n material metal");
2459 resolve(&mut r, &Options::default());
2460 let map = r.root.as_object().unwrap();
2461 assert!(!map.contains_key("_base"));
2462 let steel = map["steel"].as_object().unwrap();
2463 assert_eq!(steel["weight"], Value::Int(25));
2464 assert_eq!(steel["stackable"], Value::Bool(true));
2465 assert_eq!(steel["material"], Value::String("metal".into()));
2466 }
2467
2468 #[test]
2469 fn test_i18n_select_lang() {
2470 let mut r = parse("!active\ntitle:i18n\n en Hello\n ru Привет\n de Hallo");
2471 let opts = Options { lang: Some("ru".into()), ..Default::default() };
2472 resolve(&mut r, &opts);
2473 let map = r.root.as_object().unwrap();
2474 assert_eq!(map["title"], Value::String("Привет".into()));
2475 }
2476
2477 #[test]
2478 fn test_i18n_fallback_en() {
2479 let mut r = parse("!active\ntitle:i18n\n en Hello\n ru Привет");
2480 let opts = Options { lang: Some("fr".into()), ..Default::default() };
2481 resolve(&mut r, &opts);
2482 let map = r.root.as_object().unwrap();
2483 assert_eq!(map["title"], Value::String("Hello".into()));
2484 }
2485
2486 #[test]
2487 fn test_auto_interpolation_simple() {
2488 let mut r = parse("!active\nname Wario\ngreeting Hello, {name}!");
2489 resolve(&mut r, &Options::default());
2490 let map = r.root.as_object().unwrap();
2491 assert_eq!(map["greeting"], Value::String("Hello, Wario!".into()));
2492 }
2493
2494 #[test]
2495 fn test_auto_interpolation_nested() {
2496 let mut r = parse("!active\nserver\n host localhost\n port 8080\nurl http://{server.host}:{server.port}/api");
2497 resolve(&mut r, &Options::default());
2498 let map = r.root.as_object().unwrap();
2499 assert_eq!(map["url"], Value::String("http://localhost:8080/api".into()));
2500 }
2501
2502 #[test]
2503 fn test_template_legacy_still_works() {
2504 let mut r = parse("!active\nname Wario\ngreeting:template Hello, {name}!");
2505 resolve(&mut r, &Options::default());
2506 let map = r.root.as_object().unwrap();
2507 assert_eq!(map["greeting"], Value::String("Hello, Wario!".into()));
2508 }
2509
2510 #[test]
2513 fn test_interpolation_preserves_non_ascii() {
2514 let mut r = parse("!active\ngreeting Привет\nmsg {greeting} мир\n");
2515 resolve(&mut r, &Options::default());
2516 let map = r.root.as_object().unwrap();
2517 assert_eq!(map["msg"], Value::String("Привет мир".into()));
2518 }
2519
2520 #[test]
2522 fn test_secret_never_leaks_through_string_sinks() {
2523 let mut r = parse("!active\ntoken:secret abc123\nmsg hello {token} world\njoined:join\n - {token}\n - x\n");
2524 resolve(&mut r, &Options::default());
2525 let json = crate::to_json(&r.root);
2526 assert!(!json.contains("abc123"), "secret leaked into output: {json}");
2527 let map = r.root.as_object().unwrap();
2528 assert_eq!(map["msg"], Value::String("hello [SECRET] world".into()));
2529 }
2530
2531 #[test]
2534 fn test_secret_not_leaked_via_nested_interpolation() {
2535 let mut r = parse("!active\ntoken:secret abc123\nsection\n msg use {token} now\n");
2536 resolve(&mut r, &Options::default());
2537 let json = crate::to_json(&r.root);
2538 assert!(!json.contains("abc123"), "secret leaked: {json}");
2539 let section = r.root.as_object().unwrap()["section"].as_object().unwrap();
2540 assert_eq!(section["msg"], Value::String("use [SECRET] now".into()));
2541 }
2542
2543 #[test]
2544 fn test_unique_dedupes_large_list() {
2545 let mut src = String::from("!active\nl:unique\n");
2546 for i in 0..5000 {
2547 src.push_str(&format!(" - v{}\n", i % 100));
2548 }
2549 let mut r = parse(&src);
2550 resolve(&mut r, &Options::default());
2551 let arr = r.root.as_object().unwrap()["l"].as_array().unwrap();
2552 assert_eq!(arr.len(), 100);
2553 }
2554
2555 #[test]
2556 fn test_type_validation() {
2557 let mut r = parse(
2560 "!active\n\
2561 _base_unit\n \
2562 hp(int) 100\n \
2563 speed(float) 1.5\n\
2564 infantry:inherit:_base_unit\n \
2565 name Infantry\n \
2566 hp 80"
2567 );
2568 resolve(&mut r, &Options::default());
2569 let map = r.root.as_object().unwrap();
2570
2571 assert!(!map.contains_key("_base_unit"));
2573
2574 let infantry = map["infantry"].as_object().unwrap();
2576 assert_eq!(infantry["hp"], Value::Int(80)); assert_eq!(infantry["speed"], Value::Float(1.5)); }
2579
2580 #[test]
2581 fn test_type_validation_error() {
2582 let mut r = parse(
2584 "!active\n\
2585 _base_unit\n \
2586 hp(int) 100\n\
2587 infantry:inherit:_base_unit\n \
2588 hp hello" );
2590 resolve(&mut r, &Options::default());
2591 let map = r.root.as_object().unwrap();
2592
2593 let infantry = map["infantry"].as_object().unwrap();
2594 if let Value::String(s) = &infantry["hp"] {
2596 assert!(s.contains("TYPE_ERR"));
2597 } else {
2598 panic!("Expected error string for type mismatch");
2599 }
2600 }
2601
2602 #[test]
2603 fn test_constraint_validation_inherited_range() {
2604 let mut r = parse(
2605 "!active\n\
2606 _base_unit\n \
2607 hp[min:1, max:50000] 1000\n\
2608 infantry:inherit:_base_unit\n \
2609 hp 60000"
2610 );
2611 resolve(&mut r, &Options::default());
2612 let map = r.root.as_object().unwrap();
2613 let infantry = map["infantry"].as_object().unwrap();
2614
2615 if let Value::String(s) = &infantry["hp"] {
2616 assert!(s.contains("CONSTRAINT_ERR"));
2617 assert!(s.contains("exceeds max"));
2618 } else {
2619 panic!("Expected constraint error string");
2620 }
2621 }
2622
2623 #[test]
2624 fn test_constraint_validation_required() {
2625 let mut r = parse(
2626 "!active\n\
2627 _base_unit\n \
2628 description[type:string, required] hello\n\
2629 scout:inherit:_base_unit\n \
2630 description null"
2631 );
2632 resolve(&mut r, &Options::default());
2633 let map = r.root.as_object().unwrap();
2634 let scout = map["scout"].as_object().unwrap();
2635
2636 if let Value::String(s) = &scout["description"] {
2637 assert!(s.contains("CONSTRAINT_ERR"));
2638 assert!(s.contains("required"));
2639 } else {
2640 panic!("Expected required-constraint error string");
2641 }
2642 }
2643
2644 #[test]
2645 fn test_multi_parent_inherit() {
2646 let mut r = parse(
2647 "!active\n\
2648 _movable\n \
2649 speed 10\n \
2650 can_move true\n\
2651 _damageable\n \
2652 hp 100\n \
2653 armor 5\n\
2654 tank:inherit:_movable:_damageable\n \
2655 name Tank\n \
2656 armor 20"
2657 );
2658 resolve(&mut r, &Options::default());
2659 let map = r.root.as_object().unwrap();
2660
2661 assert!(!map.contains_key("_movable"));
2662 assert!(!map.contains_key("_damageable"));
2663
2664 let tank = map["tank"].as_object().unwrap();
2665 assert_eq!(tank["speed"], Value::Int(10)); assert_eq!(tank["can_move"], Value::Bool(true)); assert_eq!(tank["hp"], Value::Int(100)); assert_eq!(tank["armor"], Value::Int(20)); assert_eq!(tank["name"], Value::String("Tank".into()));
2670 }
2671
2672 #[test]
2673 fn test_calc_dot_path() {
2674 let mut r = parse(
2675 "!active\n\
2676 stats\n \
2677 base_hp 100\n \
2678 multiplier 3\n\
2679 total_hp:calc stats.base_hp * stats.multiplier"
2680 );
2681 resolve(&mut r, &Options::default());
2682 let map = r.root.as_object().unwrap();
2683 assert_eq!(map["total_hp"], Value::Int(300));
2684 }
2685
2686 #[test]
2687 fn test_i18n_plural_en() {
2688 let mut r = parse(
2689 "!active\n\
2690 count 5\n\
2691 items:i18n:count\n \
2692 en\n \
2693 one item\n \
2694 other items"
2695 );
2696 let opts = Options { lang: Some("en".into()), ..Default::default() };
2697 resolve(&mut r, &opts);
2698 let map = r.root.as_object().unwrap();
2699 assert_eq!(map["items"], Value::String("items".into()));
2700 }
2701
2702 #[test]
2703 fn test_i18n_plural_en_one() {
2704 let mut r = parse(
2705 "!active\n\
2706 count 1\n\
2707 items:i18n:count\n \
2708 en\n \
2709 one {count} item\n \
2710 other {count} items"
2711 );
2712 let opts = Options { lang: Some("en".into()), ..Default::default() };
2713 resolve(&mut r, &opts);
2714 let map = r.root.as_object().unwrap();
2715 assert_eq!(map["items"], Value::String("1 item".into()));
2716 }
2717
2718 #[test]
2719 fn test_i18n_plural_ru() {
2720 let mut r = parse(
2721 "!active\n\
2722 count 3\n\
2723 items:i18n:count\n \
2724 ru\n \
2725 one предмет\n \
2726 few предмета\n \
2727 many предметов\n \
2728 other предметов"
2729 );
2730 let opts = Options { lang: Some("ru".into()), ..Default::default() };
2731 resolve(&mut r, &opts);
2732 let map = r.root.as_object().unwrap();
2733 assert_eq!(map["items"], Value::String("предмета".into()));
2734 }
2735
2736 #[test]
2737 fn test_quoted_null_preserved() {
2738 let r = parse("status \"null\"\nenabled \"true\"\ncount \"42\"");
2739 let map = r.root.as_object().unwrap();
2740 assert_eq!(map["status"], Value::String("null".into()));
2741 assert_eq!(map["enabled"], Value::String("true".into()));
2742 assert_eq!(map["count"], Value::String("42".into()));
2743 }
2744
2745 #[test]
2746 fn test_unquoted_null_is_null() {
2747 let r = parse("status null\nenabled true\ncount 42");
2748 let map = r.root.as_object().unwrap();
2749 assert_eq!(map["status"], Value::Null);
2750 assert_eq!(map["enabled"], Value::Bool(true));
2751 assert_eq!(map["count"], Value::Int(42));
2752 }
2753
2754 #[test]
2755 fn test_spam_rate_limit_exceeded() {
2756 let _serialized = SPAM_TESTS.lock().unwrap_or_else(|e| e.into_inner());
2757 super::clear_spam_buckets();
2758
2759 let mut r1 = parse("!active\nsecret_token abc\naccess:spam:1:5 secret_token");
2760 resolve(&mut r1, &Options::default());
2761 let map1 = r1.root.as_object().unwrap();
2762 assert_eq!(map1["access"], Value::String("abc".into()));
2763
2764 let mut r2 = parse("!active\nsecret_token abc\naccess:spam:1:5 secret_token");
2765 resolve(&mut r2, &Options::default());
2766 let map2 = r2.root.as_object().unwrap();
2767 match &map2["access"] {
2768 Value::String(s) => assert!(s.starts_with("SPAM_ERR:")),
2769 _ => panic!("Expected SPAM_ERR string"),
2770 }
2771 }
2772
2773 #[test]
2774 fn test_spam_default_window_sec_is_one() {
2775 let _serialized = SPAM_TESTS.lock().unwrap_or_else(|e| e.into_inner());
2776 super::clear_spam_buckets();
2777
2778 let mut r = parse("!active\na 1\nx:spam:2 a");
2779 resolve(&mut r, &Options::default());
2780 let map = r.root.as_object().unwrap();
2781 assert_eq!(map["x"], Value::Int(1));
2782 }
2783
2784 #[test]
2785 fn test_deep_nesting_does_not_overflow() {
2786 let mut synx = String::from("!active\n");
2789 let mut indent = String::new();
2790 for i in 0..200 {
2791 synx.push_str(&format!("{}level_{}\n", indent, i));
2792 indent.push_str(" ");
2793 }
2794 synx.push_str(&format!("{}value deep\n", indent));
2795
2796 let mut result = parse(&synx);
2797 resolve(&mut result, &Default::default());
2798 assert!(matches!(result.root, Value::Object(_)));
2799
2800 let mut cur = &result.root;
2801 let mut depth = 0usize;
2802 loop {
2803 let Value::Object(map) = cur else { break };
2804 let key = format!("level_{}", depth);
2805 match map.get(&key) {
2806 Some(next) => {
2807 cur = next;
2808 depth += 1;
2809 }
2810 None => break,
2811 }
2812 }
2813 assert!(
2814 depth >= 100,
2815 "expected at least 100 chained levels from parse, got {}",
2816 depth
2817 );
2818 assert!(
2819 depth <= 130,
2820 "parser nesting cap should keep chain shallow, got {}",
2821 depth
2822 );
2823 }
2824
2825 #[test]
2826 fn test_circular_alias_returns_error() {
2827 let mut r = parse("!active\na:alias b\nb:alias a");
2828 resolve(&mut r, &Default::default());
2829 let root = r.root.as_object().unwrap();
2830 let a_val = root.get("a").unwrap();
2831 let b_val = root.get("b").unwrap();
2832 assert!(
2833 matches!(a_val, Value::String(s) if s.starts_with("ALIAS_ERR:")),
2834 "expected ALIAS_ERR for 'a', got: {:?}", a_val
2835 );
2836 assert!(
2837 matches!(b_val, Value::String(s) if s.starts_with("ALIAS_ERR:")),
2838 "expected ALIAS_ERR for 'b', got: {:?}", b_val
2839 );
2840 }
2841
2842 #[test]
2843 fn test_self_alias_returns_error() {
2844 let mut r = parse("!active\na:alias a");
2845 resolve(&mut r, &Default::default());
2846 let root = r.root.as_object().unwrap();
2847 let a_val = root.get("a").unwrap();
2848 assert!(
2849 matches!(a_val, Value::String(s) if s.starts_with("ALIAS_ERR:")),
2850 "expected ALIAS_ERR for self-alias, got: {:?}", a_val
2851 );
2852 }
2853
2854 #[test]
2855 fn test_valid_alias_still_works() {
2856 let mut r = parse("!active\nbase 42\ncopy:alias base");
2857 resolve(&mut r, &Default::default());
2858 let root = r.root.as_object().unwrap();
2859 assert_eq!(root.get("copy"), Some(&Value::Int(42)));
2860 }
2861
2862 #[test]
2863 fn test_alias_to_string_valued_key_no_false_positive() {
2864 let mut r = parse("!active\na b\nb:alias a");
2867 resolve(&mut r, &Default::default());
2868 let root = r.root.as_object().unwrap();
2869 assert_eq!(
2870 root.get("b"),
2871 Some(&Value::String("b".to_string())),
2872 "alias to a string-valued key should not produce ALIAS_ERR"
2873 );
2874 }
2875
2876 #[test]
2877 fn test_prompt_marker() {
2878 let mut r = parse("!active\nmemory:prompt:Core\n identity ASAI\n creator APERTURESyndicate");
2879 resolve(&mut r, &Options::default());
2880 let map = r.root.as_object().unwrap();
2881 if let Value::String(s) = &map["memory"] {
2882 assert!(s.starts_with("Core (SYNX):"));
2883 assert!(s.contains("```synx"));
2884 assert!(s.contains("identity ASAI"));
2885 } else {
2886 panic!("Expected :prompt to produce a string");
2887 }
2888 }
2889
2890 #[test]
2891 fn test_vision_marker_passthrough() {
2892 let mut r = parse("!active\nimage:vision Generate a sunset");
2893 resolve(&mut r, &Options::default());
2894 let map = r.root.as_object().unwrap();
2895 assert_eq!(map["image"], Value::String("Generate a sunset".into()));
2896 }
2897
2898 #[test]
2899 fn test_audio_marker_passthrough() {
2900 let mut r = parse("!active\nnarration:audio Read this summary aloud");
2901 resolve(&mut r, &Options::default());
2902 let map = r.root.as_object().unwrap();
2903 assert_eq!(map["narration"], Value::String("Read this summary aloud".into()));
2904 }
2905
2906 #[test]
2907 fn test_use_directive_loads_package() {
2908 let tmp = std::env::temp_dir().join("synx-use-test-load");
2910 let _ = std::fs::remove_dir_all(&tmp);
2911 let pkg_dir = tmp.join("synx_packages/@test/config");
2912 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
2913 std::fs::write(pkg_dir.join("synx-pkg.synx"), "name @test/config\nversion 1.0.0\nmain src/main.synx\n").unwrap();
2914 std::fs::write(pkg_dir.join("src/main.synx"), "identity APERTURESyndicate\ndefault_port 8080\n").unwrap();
2915
2916 let mut r = parse("!active\n!use @test/config\napp MyApp");
2917 let opts = Options {
2918 base_path: Some(tmp.to_string_lossy().into_owned()),
2919 ..Default::default()
2920 };
2921 resolve(&mut r, &opts);
2922 let map = r.root.as_object().unwrap();
2923 assert!(map.contains_key("config"), "package not loaded");
2924 let pkg = map["config"].as_object().unwrap();
2925 assert_eq!(pkg["identity"], Value::String("APERTURESyndicate".into()));
2926 assert_eq!(pkg["default_port"], Value::Int(8080));
2927 assert_eq!(map["app"], Value::String("MyApp".into()));
2928 let _ = std::fs::remove_dir_all(&tmp);
2929 }
2930
2931 #[test]
2932 fn test_use_directive_with_alias() {
2933 let tmp = std::env::temp_dir().join("synx-use-test-alias");
2934 let _ = std::fs::remove_dir_all(&tmp);
2935 let pkg_dir = tmp.join("synx_packages/@test/config");
2936 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
2937 std::fs::write(pkg_dir.join("synx-pkg.synx"), "name @test/config\nversion 1.0.0\nmain src/main.synx\n").unwrap();
2938 std::fs::write(pkg_dir.join("src/main.synx"), "identity APERTURESyndicate\ndefault_port 8080\n").unwrap();
2939
2940 let mut r = parse("!active\n!use @test/config as defaults\napp MyApp");
2941 let opts = Options {
2942 base_path: Some(tmp.to_string_lossy().into_owned()),
2943 ..Default::default()
2944 };
2945 resolve(&mut r, &opts);
2946 let map = r.root.as_object().unwrap();
2947 assert!(map.contains_key("defaults"), "aliased package not loaded");
2948 assert!(!map.contains_key("config"), "should use alias, not auto name");
2949 let pkg = map["defaults"].as_object().unwrap();
2950 assert_eq!(pkg["identity"], Value::String("APERTURESyndicate".into()));
2951 let _ = std::fs::remove_dir_all(&tmp);
2952 }
2953
2954 #[test]
2955 fn test_use_directive_missing_package_ignored() {
2956 let mut r = parse("!active\n!use @nonexistent/pkg\napp MyApp");
2957 resolve(&mut r, &Options::default());
2958 let map = r.root.as_object().unwrap();
2959 assert!(!map.contains_key("pkg"));
2961 assert_eq!(map["app"], Value::String("MyApp".into()));
2962 }
2963
2964 #[test]
2965 fn test_use_reads_manifest_main_field() {
2966 let tmp = std::env::temp_dir().join("synx-use-test-main");
2967 let _ = std::fs::remove_dir_all(&tmp);
2968 let pkg_dir = tmp.join("synx_packages/@test/myapp");
2969 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
2970 std::fs::write(pkg_dir.join("synx-pkg.synx"), "name @test/myapp\nversion 1.0.0\nmain src/main.synx\n").unwrap();
2971 std::fs::write(pkg_dir.join("src/main.synx"), "app_name MyApp\nversion 2.0.0\n").unwrap();
2972
2973 let mut r = parse("!active\n!use @test/myapp as myapp\napp Test");
2974 let opts = Options {
2975 base_path: Some(tmp.to_string_lossy().into_owned()),
2976 ..Default::default()
2977 };
2978 resolve(&mut r, &opts);
2979 let map = r.root.as_object().unwrap();
2980 assert!(map.contains_key("myapp"), "package not loaded via manifest");
2981 let pkg = map["myapp"].as_object().unwrap();
2982 assert_eq!(pkg["app_name"], Value::String("MyApp".into()));
2983 let _ = std::fs::remove_dir_all(&tmp);
2984 }
2985
2986 #[test]
2987 fn test_read_manifest_main_function() {
2988 let tmp = std::env::temp_dir().join("synx-manifest-main-test");
2989 let _ = std::fs::remove_dir_all(&tmp);
2990 std::fs::create_dir_all(tmp.join("src")).unwrap();
2991 std::fs::write(tmp.join("synx-pkg.synx"), "name @test/pkg\nversion 1.0.0\nmain src/main.synx\n").unwrap();
2992 std::fs::write(tmp.join("src/main.synx"), "key value\n").unwrap();
2993
2994 let result = read_manifest_main(&tmp);
2995 assert!(result.is_some(), "should read main from synx-pkg.synx");
2996 let path = result.unwrap();
2997 assert!(path.ends_with("src/main.synx") || path.ends_with("src\\main.synx"));
2998 let _ = std::fs::remove_dir_all(&tmp);
2999 }
3000}