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 let type_registry = build_type_registry(&metadata);
192 let constraint_registry = build_constraint_registry(&metadata);
194
195 let root_ptr = &mut result.root as *mut Value;
205 resolve_value(&mut result.root, root_ptr, options, &metadata, "", &includes_map, 0);
206
207 validate_field_constraints(&mut result.root, &constraint_registry);
209
210 validate_field_types(&mut result.root, &type_registry, "");
212
213 result.metadata = metadata;
214 result.includes = includes_directives;
215}
216
217fn resolve_value(
218 value: &mut Value,
219 root_ptr: *mut Value,
220 options: &Options,
221 metadata: &HashMap<String, MetaMap>,
222 path: &str,
223 includes: &HashMap<String, Value>,
224 depth: usize,
225) {
226 if depth >= MAX_RESOLVE_DEPTH {
228 if let Value::Object(ref mut map) = value {
231 for val in map.values_mut() {
232 *val = Value::String(
233 "NESTING_ERR: maximum object nesting depth exceeded".to_string()
234 );
235 }
236 }
237 return;
238 }
239
240 let meta_map = metadata.get(path).cloned();
241
242 if let Value::Object(ref mut map) = value {
243 let keys: Vec<String> = map.keys().cloned().collect();
244
245 for key in &keys {
247 let child_path = if path.is_empty() {
248 key.clone()
249 } else {
250 format!("{}.{}", path, key)
251 };
252
253 if let Some(child) = map.get_mut(key) {
254 match child {
255 Value::Object(_) => {
256 resolve_value(child, root_ptr, options, metadata, &child_path, includes, depth + 1);
257 }
258 Value::Array(arr) => {
259 for item in arr.iter_mut() {
260 if let Value::Object(_) = item {
261 resolve_value(item, root_ptr, options, metadata, &child_path, includes, depth + 1);
262 }
263 }
264 }
265 _ => {}
266 }
267 }
268 }
269
270 if let Some(ref mm) = meta_map {
272 for key in &keys {
273 let meta = match mm.get(key) {
274 Some(m) => m.clone(),
275 None => continue,
276 };
277
278 apply_markers(map, key, &meta, root_ptr, options, path, metadata, includes);
279 }
280 }
281
282 let keys2: Vec<String> = map.keys().cloned().collect();
284 for key in &keys2 {
285 if let Some(Value::String(s)) = map.get(key) {
286 if s.contains('{') {
287 let root_ref = unsafe { &*root_ptr };
288 let result = resolve_interpolation(s, root_ref, map, includes);
289 if result != *s {
290 map.insert(key.to_string(), Value::String(result));
291 }
292 }
293 }
294 }
295 }
296}
297
298fn apply_markers(
299 map: &mut HashMap<String, Value>,
300 key: &str,
301 meta: &Meta,
302 root_ptr: *mut Value,
303 options: &Options,
304 path: &str,
305 metadata: &HashMap<String, MetaMap>,
306 _includes: &HashMap<String, Value>,
307) {
308 let markers = &meta.markers;
309
310 if markers.contains(&"spam".to_string()) {
315 let spam_idx = markers.iter().position(|m| m == "spam").unwrap();
316 let max_calls = markers
317 .get(spam_idx + 1)
318 .and_then(|s| s.parse::<usize>().ok())
319 .unwrap_or(0);
320 let window_sec = markers
321 .get(spam_idx + 2)
322 .and_then(|s| s.parse::<u64>().ok())
323 .unwrap_or(1);
324
325 if max_calls == 0 {
326 map.insert(
327 key.to_string(),
328 Value::String("SPAM_ERR: invalid limit, use :spam:MAX[:WINDOW_SEC]".to_string()),
329 );
330 return;
331 }
332
333 let target = map
334 .get(key)
335 .map(value_to_string)
336 .unwrap_or_else(|| key.to_string());
337 let bucket_key = format!("{}::{}", key, target);
338
339 if !allow_spam_access(&bucket_key, max_calls, window_sec) {
340 map.insert(
341 key.to_string(),
342 Value::String(format!(
343 "SPAM_ERR: '{}' exceeded {} calls per {}s",
344 target, max_calls, window_sec
345 )),
346 );
347 return;
348 }
349
350 if let Some(resolved) = map
351 .get(key)
352 .and_then(|v| {
353 let t = value_to_string(v);
354 let root_ref = unsafe { &*root_ptr };
355 deep_get(root_ref, &t).or_else(|| map.get(t.as_str()).cloned())
356 })
357 {
358 map.insert(key.to_string(), resolved);
359 }
360 }
361
362 if markers.contains(&"include".to_string()) || markers.contains(&"import".to_string()) {
364 if let Some(Value::String(file_path)) = map.get(key) {
365 let max_depth = options.max_include_depth.unwrap_or(DEFAULT_MAX_INCLUDE_DEPTH);
366 if options._include_depth >= max_depth {
367 map.insert(
368 key.to_string(),
369 Value::String(format!("INCLUDE_ERR: max include depth ({}) exceeded", max_depth)),
370 );
371 return;
372 }
373 let base = options
374 .base_path
375 .as_deref()
376 .unwrap_or(".");
377 let full = match jail_path(base, file_path) {
378 Ok(p) => p,
379 Err(e) => {
380 map.insert(key.to_string(), Value::String(format!("INCLUDE_ERR: {}", e)));
381 return;
382 }
383 };
384 if let Err(e) = check_file_size(&full) {
385 map.insert(key.to_string(), Value::String(format!("INCLUDE_ERR: {}", e)));
386 return;
387 }
388 match std::fs::read_to_string(&full) {
389 Ok(text) => {
390 let mut included = parser::parse(&text);
391 if included.mode == Mode::Active {
392 let mut child_opts = options.clone();
393 child_opts._include_depth += 1;
394 if let Some(parent) = full.parent() {
395 child_opts.base_path = Some(parent.to_string_lossy().into_owned());
396 }
397 resolve(&mut included, &child_opts);
398 }
399 map.insert(key.to_string(), included.root);
400 }
401 Err(e) => {
402 map.insert(
403 key.to_string(),
404 Value::String(format!("INCLUDE_ERR: {}", fmt_io_err(&e, file_path))),
405 );
406 }
407 }
408 }
409 return;
410 }
411
412 if markers.contains(&"env".to_string()) {
414 if let Some(Value::String(var_name)) = map.get(key) {
415 let env_val = if let Some(ref env_map) = options.env {
416 env_map.get(var_name.as_str()).cloned()
417 } else {
418 std::env::var(var_name).ok()
419 };
420
421 let force_string = meta.type_hint.as_deref() == Some("string");
422 let default_idx = markers.iter().position(|m| m == "default");
423 if let Some(val) = env_val.filter(|v| !v.is_empty()) {
424 let resolved = if force_string {
425 Value::String(val)
426 } else {
427 cast_primitive(&val)
428 };
429 map.insert(key.to_string(), resolved);
430 } else if let Some(di) = default_idx {
431 if markers.len() > di + 1 {
432 let fallback = markers[di + 1..].join(":");
435 let resolved = if force_string {
436 Value::String(fallback)
437 } else {
438 cast_primitive(&fallback)
439 };
440 map.insert(key.to_string(), resolved);
441 } else {
442 map.insert(key.to_string(), Value::Null);
443 }
444 } else {
445 map.insert(key.to_string(), Value::Null);
446 }
447 }
448 }
449
450 if markers.contains(&"random".to_string()) {
452 if let Some(Value::Array(arr)) = map.get(key) {
453 if arr.is_empty() {
454 map.insert(key.to_string(), Value::Null);
455 return;
456 }
457 let picked = if !meta.args.is_empty() {
458 let weights: Vec<f64> = meta.args.iter().filter_map(|s| s.parse().ok()).collect();
459 weighted_random(arr, &weights)
460 } else {
461 arr[rng::random_usize(arr.len())].clone()
462 };
463 map.insert(key.to_string(), picked);
464 }
465 }
466
467 if markers.contains(&"ref".to_string()) {
471 if let Some(Value::String(target)) = map.get(key) {
472 let root_ref = unsafe { &*root_ptr };
473 let resolved = deep_get(root_ref, target)
474 .or_else(|| map.get(target.as_str()).cloned())
475 .unwrap_or(Value::Null);
476
477 if markers.contains(&"calc".to_string()) {
479 if let Some(n) = value_as_number(&resolved) {
480 let calc_idx = markers.iter().position(|m| m == "calc").unwrap();
481 if let Some(calc_expr) = markers.get(calc_idx + 1) {
482 let first = calc_expr.chars().next().unwrap_or(' ');
483 if "+-*/%".contains(first) {
484 let expr = format!("{} {}", format_number(n), calc_expr);
485 match safe_calc(&expr) {
486 Ok(result) => {
487 let v = if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
488 Value::Int(result as i64)
489 } else {
490 Value::Float(result)
491 };
492 map.insert(key.to_string(), v);
493 }
494 Err(e) => {
495 map.insert(key.to_string(), Value::String(format!("CALC_ERR: {}", e)));
496 }
497 }
498 } else {
499 map.insert(key.to_string(), resolved);
500 }
501 } else {
502 map.insert(key.to_string(), resolved);
503 }
504 } else {
505 map.insert(key.to_string(), resolved);
506 }
507 } else {
508 map.insert(key.to_string(), resolved);
509 }
510 }
511 }
512
513 if markers.contains(&"i18n".to_string()) {
527 if let Some(Value::Object(translations)) = map.get(key) {
528 let lang = options.lang.as_deref().unwrap_or("en");
529 let val = translations.get(lang)
530 .or_else(|| translations.get("en"))
531 .or_else(|| translations.values().next())
532 .cloned()
533 .unwrap_or(Value::Null);
534
535 let i18n_idx = markers.iter().position(|m| m == "i18n").unwrap();
537 let count_field = markers.get(i18n_idx + 1).cloned();
538
539 if let (Some(ref cf), Value::Object(ref plural_forms)) = (&count_field, &val) {
540 let count_val = map.get(cf)
542 .and_then(value_as_number)
543 .or_else(|| {
544 let root_ref = unsafe { &*root_ptr };
545 deep_get(root_ref, cf).and_then(|v| value_as_number(&v))
546 })
547 .unwrap_or(0.0) as i64;
548
549 let category = plural_category(lang, count_val);
550 let chosen = plural_forms.get(category)
551 .or_else(|| plural_forms.get("other"))
552 .or_else(|| plural_forms.values().next())
553 .cloned()
554 .unwrap_or(Value::Null);
555
556 if let Value::String(ref s) = chosen {
558 let replaced = s.replace("{count}", &count_val.to_string());
559 map.insert(key.to_string(), Value::String(replaced));
560 } else {
561 map.insert(key.to_string(), chosen);
562 }
563 } else {
564 map.insert(key.to_string(), val);
565 }
566 }
567 }
568
569 if markers.contains(&"calc".to_string()) {
571 if let Some(Value::String(expr)) = map.get(key) {
572 if expr.len() > MAX_CALC_EXPR_LEN {
573 map.insert(
574 key.to_string(),
575 Value::String(format!("CALC_ERR: expression too long ({} chars, max {})", expr.len(), MAX_CALC_EXPR_LEN)),
576 );
577 return;
578 }
579 let mut resolved = expr.clone();
580
581 let root_ref = unsafe { &*root_ptr };
583 if let Value::Object(ref root_map) = root_ref {
584 for (rk, rv) in root_map {
585 if let Some(n) = value_as_number(rv) {
586 resolved = replace_word(&resolved, rk, &format_number(n));
587 if resolved.len() > MAX_CALC_RESOLVED_LEN {
588 map.insert(
589 key.to_string(),
590 Value::String(format!(
591 "CALC_ERR: resolved expression too long (max {} bytes)",
592 MAX_CALC_RESOLVED_LEN
593 )),
594 );
595 return;
596 }
597 }
598 }
599 }
600
601 for (rk, rv) in map.iter() {
603 if rk != key {
604 if let Some(n) = value_as_number(rv) {
605 resolved = replace_word(&resolved, rk, &format_number(n));
606 if resolved.len() > MAX_CALC_RESOLVED_LEN {
607 map.insert(
608 key.to_string(),
609 Value::String(format!(
610 "CALC_ERR: resolved expression too long (max {} bytes)",
611 MAX_CALC_RESOLVED_LEN
612 )),
613 );
614 return;
615 }
616 }
617 }
618 }
619
620 let root_ref2 = unsafe { &*root_ptr };
622 let mut dot_resolved = String::new();
623 let bytes = resolved.as_bytes();
624 let len = bytes.len();
625 let mut i = 0;
626 while i < len {
627 if is_word_char(bytes[i]) {
628 let start = i;
629 let mut has_dot = false;
630 while i < len && (is_word_char(bytes[i]) || bytes[i] == b'.') {
631 if bytes[i] == b'.' { has_dot = true; }
632 i += 1;
633 }
634 let token = &resolved[start..i];
635 if has_dot && token.contains('.') {
636 if let Some(val) = deep_get(root_ref2, token) {
637 if let Some(n) = value_as_number(&val) {
638 dot_resolved.push_str(&format_number(n));
639 if dot_resolved.len() > MAX_CALC_RESOLVED_LEN {
640 map.insert(
641 key.to_string(),
642 Value::String(format!(
643 "CALC_ERR: resolved expression too long (max {} bytes)",
644 MAX_CALC_RESOLVED_LEN
645 )),
646 );
647 return;
648 }
649 continue;
650 }
651 }
652 }
653 dot_resolved.push_str(token);
654 if dot_resolved.len() > MAX_CALC_RESOLVED_LEN {
655 map.insert(
656 key.to_string(),
657 Value::String(format!(
658 "CALC_ERR: resolved expression too long (max {} bytes)",
659 MAX_CALC_RESOLVED_LEN
660 )),
661 );
662 return;
663 }
664 } else {
665 dot_resolved.push(bytes[i] as char);
666 i += 1;
667 if dot_resolved.len() > MAX_CALC_RESOLVED_LEN {
668 map.insert(
669 key.to_string(),
670 Value::String(format!(
671 "CALC_ERR: resolved expression too long (max {} bytes)",
672 MAX_CALC_RESOLVED_LEN
673 )),
674 );
675 return;
676 }
677 }
678 }
679 resolved = dot_resolved;
680
681 match safe_calc(&resolved) {
682 Ok(result) => {
683 let v = if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
684 Value::Int(result as i64)
685 } else {
686 Value::Float(result)
687 };
688 map.insert(key.to_string(), v);
689 }
690 Err(e) => {
691 map.insert(
692 key.to_string(),
693 Value::String(format!("CALC_ERR: {}", e)),
694 );
695 }
696 }
697 }
698 }
699
700 if markers.contains(&"alias".to_string()) {
702 if let Some(Value::String(target)) = map.get(key) {
703 let target = target.clone();
704 let current_path = if path.is_empty() {
706 key.to_string()
707 } else {
708 format!("{}.{}", path, key)
709 };
710 if target == key || target == current_path {
712 map.insert(
713 key.to_string(),
714 Value::String(format!("ALIAS_ERR: self-referential alias: {} → {}", current_path, target)),
715 );
716 } else {
717 let root_ref = unsafe { &*root_ptr };
722 let target_val = deep_get(root_ref, &target);
723 let (target_parent, target_key_name) = if let Some(dot) = target.rfind('.') {
725 (target[..dot].to_string(), target[dot + 1..].to_string())
726 } else {
727 (String::new(), target.clone())
728 };
729 let target_has_alias = metadata
730 .get(&target_parent)
731 .and_then(|mm| mm.get(&target_key_name))
732 .map(|m| m.markers.contains(&"alias".to_string()))
733 .unwrap_or(false);
734 let is_cycle = target_has_alias && match &target_val {
735 Some(Value::String(s)) => s == key || s == ¤t_path,
736 _ => false,
737 };
738 if is_cycle {
739 let (a, b) = if current_path <= target {
743 (current_path.as_str(), target.as_str())
744 } else {
745 (target.as_str(), current_path.as_str())
746 };
747 map.insert(
748 key.to_string(),
749 Value::String(format!("ALIAS_ERR: circular alias detected: {} → {}", a, b)),
750 );
751 } else {
752 let val = target_val.unwrap_or(Value::Null);
753 map.insert(key.to_string(), val);
754 }
755 }
756 }
757 }
758
759 if markers.contains(&"secret".to_string()) {
761 if let Some(val) = map.get(key) {
762 let s = value_to_string(val);
763 map.insert(key.to_string(), Value::Secret(s));
764 }
765 }
766
767 if markers.contains(&"unique".to_string()) {
769 if let Some(Value::Array(arr)) = map.get(key) {
770 let mut seen = Vec::new();
771 let mut unique = Vec::new();
772 for item in arr {
773 let s = value_to_string(item);
774 if !seen.contains(&s) {
775 seen.push(s);
776 unique.push(item.clone());
777 }
778 }
779 map.insert(key.to_string(), Value::Array(unique));
780 }
781 }
782
783 if markers.contains(&"geo".to_string()) {
785 if let Some(Value::Array(arr)) = map.get(key) {
786 let region = options.region.as_deref().unwrap_or("US");
787 let prefix = format!("{} ", region);
788 let found = arr.iter().find(|item| {
789 if let Value::String(s) = item {
790 s.starts_with(&prefix)
791 } else {
792 false
793 }
794 });
795
796 let result = if let Some(Value::String(s)) = found {
797 Value::String(s[prefix.len()..].trim().to_string())
798 } else if let Some(first) = arr.first() {
799 if let Value::String(s) = first {
800 if let Some(space) = s.find(' ') {
801 Value::String(s[space + 1..].trim().to_string())
802 } else {
803 first.clone()
804 }
805 } else {
806 first.clone()
807 }
808 } else {
809 Value::Null
810 };
811 map.insert(key.to_string(), result);
812 }
813 }
814
815 if markers.contains(&"split".to_string()) {
819 if let Some(Value::String(s)) = map.get(key) {
820 let split_idx = markers.iter().position(|m| m == "split").unwrap();
821 let sep = if split_idx + 1 < markers.len() {
822 delimiter_from_keyword(&markers[split_idx + 1])
823 } else {
824 ",".to_string()
825 };
826 let items: Vec<Value> = s
827 .split(&sep)
828 .map(|p| p.trim())
829 .filter(|p| !p.is_empty())
830 .map(|p| cast_primitive(p))
831 .collect();
832 map.insert(key.to_string(), Value::Array(items));
833 }
834 }
835
836 if markers.contains(&"join".to_string()) {
838 if let Some(Value::Array(arr)) = map.get(key) {
839 let join_idx = markers.iter().position(|m| m == "join").unwrap();
840 let sep = if join_idx + 1 < markers.len() {
841 delimiter_from_keyword(&markers[join_idx + 1])
842 } else {
843 ",".to_string()
844 };
845 let joined: String = arr
846 .iter()
847 .map(|v| value_to_string(v))
848 .collect::<Vec<_>>()
849 .join(&sep);
850 map.insert(key.to_string(), Value::String(joined));
851 }
852 }
853
854 if markers.contains(&"default".to_string()) && !markers.contains(&"env".to_string()) {
856 let is_empty = match map.get(key) {
857 Some(Value::Null) | None => true,
858 Some(Value::String(s)) if s.is_empty() => true,
859 _ => false,
860 };
861 if is_empty {
862 let di = markers.iter().position(|m| m == "default").unwrap();
863 if markers.len() > di + 1 {
864 let fallback = markers[di + 1..].join(":");
865 let resolved = if meta.type_hint.as_deref() == Some("string") {
866 Value::String(fallback)
867 } else {
868 cast_primitive(&fallback)
869 };
870 map.insert(key.to_string(), resolved);
871 }
872 }
873 }
874
875 if markers.contains(&"clamp".to_string()) {
879 let clamp_idx = markers.iter().position(|m| m == "clamp").unwrap();
880 let min_s = markers.get(clamp_idx + 1).cloned().unwrap_or_default();
881 let max_s = markers.get(clamp_idx + 2).cloned().unwrap_or_default();
882 if let (Ok(lo), Ok(hi)) = (min_s.parse::<f64>(), max_s.parse::<f64>()) {
883 if lo > hi {
884 map.insert(key.to_string(), Value::String(
885 format!("CONSTRAINT_ERR: clamp min ({}) > max ({})", lo, hi),
886 ));
887 } else if let Some(n) = map.get(key).and_then(value_as_number) {
888 let clamped = n.clamp(lo, hi);
889 let v = if clamped.fract() == 0.0 && clamped.abs() < i64::MAX as f64 {
890 Value::Int(clamped as i64)
891 } else {
892 Value::Float(clamped)
893 };
894 map.insert(key.to_string(), v);
895 }
896 }
897 }
898
899 if markers.contains(&"round".to_string()) {
903 let round_idx = markers.iter().position(|m| m == "round").unwrap();
904 let decimals: u32 = markers.get(round_idx + 1)
905 .and_then(|s| s.parse().ok())
906 .unwrap_or(0);
907 if let Some(n) = map.get(key).and_then(value_as_number) {
908 let factor = 10f64.powi(decimals as i32);
909 let rounded = (n * factor).round() / factor;
910 let v = if decimals == 0 {
911 Value::Int(rounded as i64)
912 } else {
913 Value::Float(rounded)
914 };
915 map.insert(key.to_string(), v);
916 }
917 }
918
919 if markers.contains(&"map".to_string()) {
923 if let Some(Value::Array(arr)) = map.get(key) {
924 let map_idx = markers.iter().position(|m| m == "map").unwrap();
925 let source_key = markers.get(map_idx + 1).cloned().unwrap_or_default();
926 let lookup_val = if !source_key.is_empty() {
927 let root_ref = unsafe { &*root_ptr };
928 deep_get(root_ref, &source_key)
929 .or_else(|| map.get(&source_key).cloned())
930 .map(|v| value_to_string(&v))
931 .unwrap_or_default()
932 } else {
933 match map.get(key) {
935 Some(Value::String(s)) => s.clone(),
936 _ => String::new(),
937 }
938 };
939
940 let arr_clone = arr.clone();
942 let result = arr_clone.iter().find_map(|item| {
943 if let Value::String(s) = item {
944 if let Some(space) = s.find(' ') {
945 if s[..space].trim() == lookup_val {
946 return Some(cast_primitive(s[space + 1..].trim()));
947 }
948 }
949 }
950 None
951 });
952 map.insert(key.to_string(), result.unwrap_or(Value::Null));
953 }
954 }
955
956 if markers.contains(&"format".to_string()) {
960 let fmt_idx = markers.iter().position(|m| m == "format").unwrap();
961 let pattern = markers.get(fmt_idx + 1).cloned().unwrap_or_else(|| "%s".to_string());
962 if let Some(current) = map.get(key) {
963 let formatted = apply_format_pattern(&pattern, current);
964 map.insert(key.to_string(), Value::String(formatted));
965 }
966 }
967
968 if markers.contains(&"replace".to_string()) {
973 if let Some(Value::String(s)) = map.get(key) {
974 let idx = markers.iter().position(|m| m == "replace").unwrap();
975 let from = markers.get(idx + 1).cloned().unwrap_or_default();
976 let to = markers.get(idx + 2).cloned().unwrap_or_default();
977 if !from.is_empty() {
978 let replaced = s.replace(&from, &to);
979 map.insert(key.to_string(), Value::String(replaced));
980 }
981 }
982 }
983
984 if markers.contains(&"sort".to_string()) {
987 if let Some(Value::Array(arr)) = map.get(key) {
988 let idx = markers.iter().position(|m| m == "sort").unwrap();
989 let desc = matches!(markers.get(idx + 1).map(|s| s.as_str()), Some("desc"));
990 let mut sorted = arr.clone();
991 sorted.sort_by(|a, b| {
992 match (value_as_number(a), value_as_number(b)) {
993 (Some(an), Some(bn)) => an
994 .partial_cmp(&bn)
995 .unwrap_or(std::cmp::Ordering::Equal),
996 _ => value_to_string(a).cmp(&value_to_string(b)),
997 }
998 });
999 if desc { sorted.reverse(); }
1000 map.insert(key.to_string(), Value::Array(sorted));
1001 }
1002 }
1003
1004 if markers.contains(&"sum".to_string()) {
1008 if let Some(Value::Array(arr)) = map.get(key) {
1009 let mut total: f64 = 0.0;
1010 let mut all_int = true;
1011 for v in arr {
1012 match v {
1013 Value::Int(n) => total += *n as f64,
1014 Value::Float(f) => {
1015 total += *f;
1016 if f.fract() != 0.0 { all_int = false; }
1017 }
1018 Value::String(s) => {
1019 if let Ok(f) = s.parse::<f64>() {
1020 total += f;
1021 if f.fract() != 0.0 { all_int = false; }
1022 }
1023 }
1024 _ => {}
1025 }
1026 }
1027 let result = if all_int && total.fract() == 0.0 && total.abs() < i64::MAX as f64 {
1028 Value::Int(total as i64)
1029 } else {
1030 Value::Float(total)
1031 };
1032 map.insert(key.to_string(), result);
1033 }
1034 }
1035
1036 if markers.contains(&"fallback".to_string()) {
1041 let fb_idx = markers.iter().position(|m| m == "fallback").unwrap();
1042 let default_val = markers.get(fb_idx + 1).cloned().unwrap_or_default();
1043 let use_fallback = match map.get(key) {
1044 None | Some(Value::Null) => true,
1045 Some(Value::String(s)) if s.is_empty() => true,
1046 Some(Value::String(s)) => {
1047 let base = options.base_path.as_deref().unwrap_or(".");
1048 match jail_path(base, s) {
1049 Ok(safe) => !safe.exists(),
1050 Err(_) => true, }
1052 }
1053 _ => false,
1054 };
1055 if use_fallback && !default_val.is_empty() {
1056 map.insert(key.to_string(), Value::String(default_val));
1057 }
1058 }
1059
1060 if markers.contains(&"once".to_string()) {
1064 let once_idx = markers.iter().position(|m| m == "once").unwrap();
1065 let gen_type = markers.get(once_idx + 1).map(|s| s.as_str()).unwrap_or("uuid");
1066 let lock_path = options.base_path.as_deref()
1067 .map(|b| std::path::Path::new(b).join(".synx.lock"))
1068 .unwrap_or_else(|| std::path::Path::new(".synx.lock").to_path_buf());
1069
1070 let existing = read_lock_value(&lock_path, key);
1072 if let Some(locked) = existing {
1073 map.insert(key.to_string(), Value::String(locked));
1074 } else {
1075 let generated = match gen_type {
1076 "uuid" => rng::generate_uuid(),
1077 "timestamp" => std::time::SystemTime::now()
1078 .duration_since(std::time::UNIX_EPOCH)
1079 .unwrap_or_default()
1080 .as_secs()
1081 .to_string(),
1082 "random" => rng::random_usize(u32::MAX as usize).to_string(),
1083 _ => rng::generate_uuid(),
1084 };
1085 write_lock_value(&lock_path, key, &generated);
1086 map.insert(key.to_string(), Value::String(generated));
1087 }
1088 }
1089
1090 if markers.contains(&"version".to_string()) {
1095 if let Some(Value::String(current_ver)) = map.get(key) {
1096 let ver_idx = markers.iter().position(|m| m == "version").unwrap();
1097 let op = markers.get(ver_idx + 1).map(|s| s.as_str()).unwrap_or(">=");
1098 let required = markers.get(ver_idx + 2).cloned().unwrap_or_default();
1099 let result = compare_versions(current_ver, op, &required);
1100 map.insert(key.to_string(), Value::Bool(result));
1101 }
1102 }
1103
1104 if markers.contains(&"watch".to_string()) {
1108 if let Some(Value::String(file_path)) = map.get(key) {
1109 let max_depth = options.max_include_depth.unwrap_or(DEFAULT_MAX_INCLUDE_DEPTH);
1110 if options._include_depth >= max_depth {
1111 map.insert(
1112 key.to_string(),
1113 Value::String(format!("WATCH_ERR: max include depth ({}) exceeded", max_depth)),
1114 );
1115 return;
1116 }
1117 let base = options.base_path.as_deref().unwrap_or(".");
1118 let full = match jail_path(base, file_path) {
1119 Ok(p) => p,
1120 Err(e) => {
1121 map.insert(key.to_string(), Value::String(format!("WATCH_ERR: {}", e)));
1122 return;
1123 }
1124 };
1125 if let Err(e) = check_file_size(&full) {
1126 map.insert(key.to_string(), Value::String(format!("WATCH_ERR: {}", e)));
1127 return;
1128 }
1129 let watch_idx = markers.iter().position(|m| m == "watch").unwrap();
1130 let key_path = markers.get(watch_idx + 1).cloned();
1131
1132 match std::fs::read_to_string(&full) {
1133 Ok(content) => {
1134 let value = if let Some(ref kp) = key_path {
1135 extract_from_file_content(&content, kp, full.extension().and_then(|e| e.to_str()).unwrap_or("")).unwrap_or(Value::Null)
1136 } else {
1137 Value::String(content.trim().to_string())
1138 };
1139 map.insert(key.to_string(), value);
1140 }
1141 Err(e) => {
1142 map.insert(key.to_string(), Value::String(format!("WATCH_ERR: {}", fmt_io_err(&e, file_path))));
1143 }
1144 }
1145 }
1146 }
1147
1148 if markers.contains(&"prompt".to_string()) {
1153 let prompt_idx = markers.iter().position(|m| m == "prompt").unwrap();
1154 let label = markers.get(prompt_idx + 1).cloned().unwrap_or_else(|| key.to_string());
1155 if let Some(val) = map.get(key) {
1156 let synx_text = stringify_value(val, 0);
1157 let block = format!("{} (SYNX):\n```synx\n{}```", label, synx_text);
1158 map.insert(key.to_string(), Value::String(block));
1159 }
1160 }
1161
1162 #[cfg(feature = "wasm")]
1173 if let Some(ref wasm_rt) = options.wasm_runtime {
1174 for marker in markers {
1175 if crate::wasm::BUILTIN_MARKERS.contains(&marker.as_str()) {
1176 continue;
1177 }
1178 if wasm_rt.has_marker(marker) {
1179 let marker_idx = markers.iter().position(|m| m == marker).unwrap();
1181 let args: Vec<String> = markers[marker_idx + 1..].to_vec();
1182 let current_value = map.get(key).cloned().unwrap_or(Value::Null);
1183 match wasm_rt.apply_marker(marker, ¤t_value, &args) {
1184 Ok(result) => {
1185 map.insert(key.to_string(), result);
1186 }
1187 Err(e) => {
1188 map.insert(key.to_string(), Value::String(format!("WASM_ERR: {}", e)));
1189 }
1190 }
1191 break; }
1193 }
1194 }
1195
1196 if let Some(ref c) = meta.constraints {
1198 validate_constraints(map, key, c);
1199 }
1200}
1201
1202fn validate_constraints(map: &mut HashMap<String, Value>, key: &str, c: &Constraints) {
1205 let val = match map.get(key) {
1206 Some(v) => v.clone(),
1207 None => {
1208 if c.required {
1209 map.insert(key.to_string(), Value::String(
1210 format!("CONSTRAINT_ERR: '{}' is required", key),
1211 ));
1212 }
1213 return;
1214 }
1215 };
1216
1217 if c.required {
1219 let empty = matches!(val, Value::Null)
1220 || matches!(&val, Value::String(s) if s.is_empty());
1221 if empty {
1222 map.insert(key.to_string(), Value::String(
1223 format!("CONSTRAINT_ERR: '{}' is required", key),
1224 ));
1225 return;
1226 }
1227 }
1228
1229 if let Some(ref type_name) = c.type_name {
1231 let ok = match type_name.as_str() {
1232 "int" => matches!(val, Value::Int(_)),
1233 "float" => matches!(val, Value::Float(_) | Value::Int(_)),
1234 "bool" => matches!(val, Value::Bool(_)),
1235 "string" => matches!(val, Value::String(_)),
1236 _ => true,
1237 };
1238 if !ok {
1239 map.insert(key.to_string(), Value::String(
1240 format!("CONSTRAINT_ERR: '{}' expected type '{}'", key, type_name),
1241 ));
1242 return;
1243 }
1244 }
1245
1246 if let Some(ref enum_vals) = c.enum_values {
1248 let val_str = match &val {
1249 Value::String(s) => s.clone(),
1250 Value::Int(n) => n.to_string(),
1251 Value::Float(f) => f.to_string(),
1252 Value::Bool(b) => b.to_string(),
1253 _ => String::new(),
1254 };
1255 if !enum_vals.contains(&val_str) {
1256 map.insert(key.to_string(), Value::String(
1257 format!("CONSTRAINT_ERR: '{}' must be one of [{}]", key, enum_vals.join("|")),
1258 ));
1259 return;
1260 }
1261 }
1262
1263 let num = match &val {
1265 Value::Int(n) => Some(*n as f64),
1266 Value::Float(f) => Some(*f),
1267 Value::String(s) if c.min.is_some() || c.max.is_some() => Some(s.len() as f64),
1268 _ => None,
1269 };
1270 if let Some(n) = num {
1271 if let Some(min) = c.min {
1272 if n < min {
1273 map.insert(key.to_string(), Value::String(
1274 format!("CONSTRAINT_ERR: '{}' value {} is below min {}", key, n, min),
1275 ));
1276 return;
1277 }
1278 }
1279 if let Some(max) = c.max {
1280 if n > max {
1281 map.insert(key.to_string(), Value::String(
1282 format!("CONSTRAINT_ERR: '{}' value {} exceeds max {}", key, n, max),
1283 ));
1284 return;
1285 }
1286 }
1287 }
1288
1289 if let Some(ref pat) = c.pattern {
1291 if pat.len() <= 256 {
1292 if let Value::String(ref s) = val {
1293 match regex::Regex::new(pat) {
1294 Ok(re) if !re.is_match(s) => {
1295 map.insert(key.to_string(), Value::String(
1296 format!("CONSTRAINT_ERR: '{}' does not match pattern /{}/", key, pat),
1297 ));
1298 return;
1299 }
1300 Ok(_) => {}
1301 Err(_) => {}
1303 }
1304 }
1305 }
1306 }
1307}
1308
1309fn apply_format_pattern(pattern: &str, value: &Value) -> String {
1313 match value {
1314 Value::Int(n) => {
1315 if pattern.contains('d') || pattern.contains('i') {
1316 format_int_pattern(pattern, *n)
1317 } else if pattern.contains('f') || pattern.contains('e') {
1318 format_float_pattern(pattern, *n as f64)
1319 } else {
1320 n.to_string()
1321 }
1322 }
1323 Value::Float(f) => {
1324 if pattern.contains('f') || pattern.contains('e') {
1325 format_float_pattern(pattern, *f)
1326 } else {
1327 format_number(*f)
1328 }
1329 }
1330 Value::String(s) => s.clone(),
1331 other => value_to_string(other),
1332 }
1333}
1334
1335fn format_int_pattern(pattern: &str, n: i64) -> String {
1336 const MAX_FMT_WIDTH: usize = 4096;
1339 if let Some(s) = pattern.strip_prefix('%') {
1340 if let Some(inner) = s.strip_suffix('d').or_else(|| s.strip_suffix('i')) {
1341 if let Some(w) = inner.strip_prefix('0') {
1342 if let Ok(width) = w.parse::<usize>() {
1343 let width = width.min(MAX_FMT_WIDTH);
1344 return format!("{:0>width$}", n, width = width);
1345 }
1346 }
1347 if let Ok(width) = inner.parse::<usize>() {
1348 let width = width.min(MAX_FMT_WIDTH);
1349 return format!("{:>width$}", n, width = width);
1350 }
1351 }
1352 }
1353 n.to_string()
1354}
1355
1356fn format_float_pattern(pattern: &str, f: f64) -> String {
1357 const MAX_FMT_PREC: usize = 1024;
1359 if let Some(s) = pattern.strip_prefix('%') {
1360 if s == "e" {
1363 if f == 0.0 {
1364 return "0e+0".to_string();
1365 }
1366 let raw = format!("{:e}", f); if let Some(epos) = raw.rfind('e') {
1371 let mantissa = &raw[..epos];
1372 let exp_part = &raw[epos + 1..];
1373 let (sign, digits) = match exp_part.chars().next() {
1374 Some('+') => ('+', &exp_part[1..]),
1375 Some('-') => ('-', &exp_part[1..]),
1376 _ => ('+', exp_part),
1377 };
1378 let mantissa_clean = if mantissa.ends_with(".0") {
1380 &mantissa[..mantissa.len() - 2]
1381 } else {
1382 mantissa
1383 };
1384 return format!("{}e{}{}", mantissa_clean, sign, digits);
1385 }
1386 return raw;
1387 }
1388 if let Some(inner) = s.strip_suffix('f').or_else(|| s.strip_suffix('e')) {
1389 if let Some(prec_s) = inner.strip_prefix('.') {
1390 if let Ok(prec) = prec_s.parse::<usize>() {
1391 let prec = prec.min(MAX_FMT_PREC);
1392 if s.ends_with('e') {
1393 return format!("{:.prec$e}", f, prec = prec);
1394 }
1395 return format!("{:.prec$}", f, prec = prec);
1396 }
1397 }
1398 }
1399 }
1400 f.to_string()
1401}
1402
1403fn read_lock_value(lock_path: &std::path::Path, key: &str) -> Option<String> {
1405 let content = std::fs::read_to_string(lock_path).ok()?;
1406 for line in content.lines() {
1407 if let Some(rest) = line.strip_prefix(key) {
1408 if rest.starts_with(' ') {
1409 return Some(rest.trim_start().to_string());
1410 }
1411 }
1412 }
1413 None
1414}
1415
1416fn write_lock_value(lock_path: &std::path::Path, key: &str, value: &str) {
1418 let mut lines: Vec<String> = std::fs::read_to_string(lock_path)
1419 .unwrap_or_default()
1420 .lines()
1421 .map(|l| l.to_string())
1422 .collect();
1423
1424 let new_line = format!("{} {}", key, value);
1425 let mut found = false;
1426 for line in lines.iter_mut() {
1427 if line.starts_with(key) && line[key.len()..].starts_with(' ') {
1428 *line = new_line.clone();
1429 found = true;
1430 break;
1431 }
1432 }
1433 if !found {
1434 lines.push(new_line);
1435 }
1436 let _ = std::fs::write(lock_path, lines.join("\n") + "\n");
1437}
1438
1439fn compare_versions(current: &str, op: &str, required: &str) -> bool {
1441 let parse_ver = |s: &str| -> Vec<u64> {
1442 s.split('.').filter_map(|p| p.parse().ok()).collect()
1443 };
1444 let cv = parse_ver(current);
1445 let rv = parse_ver(required);
1446 let len = cv.len().max(rv.len());
1447 let mut ord = std::cmp::Ordering::Equal;
1448 for i in 0..len {
1449 let a = cv.get(i).copied().unwrap_or(0);
1450 let b = rv.get(i).copied().unwrap_or(0);
1451 if a != b {
1452 ord = a.cmp(&b);
1453 break;
1454 }
1455 }
1456 match op {
1457 ">=" => ord != std::cmp::Ordering::Less,
1458 "<=" => ord != std::cmp::Ordering::Greater,
1459 ">" => ord == std::cmp::Ordering::Greater,
1460 "<" => ord == std::cmp::Ordering::Less,
1461 "==" | "=" => ord == std::cmp::Ordering::Equal,
1462 "!=" => ord != std::cmp::Ordering::Equal,
1463 _ => false,
1464 }
1465}
1466
1467fn allow_spam_access(bucket_key: &str, max_calls: usize, window_sec: u64) -> bool {
1468 let now = Instant::now();
1469 let window = Duration::from_secs(window_sec.max(1));
1470
1471 let buckets = SPAM_BUCKETS.get_or_init(|| Mutex::new(HashMap::new()));
1472 let mut guard = match buckets.lock() {
1473 Ok(g) => g,
1474 Err(poisoned) => poisoned.into_inner(),
1475 };
1476
1477 let calls = guard.entry(bucket_key.to_string()).or_default();
1478 calls.retain(|ts| now.duration_since(*ts) <= window);
1479
1480 if calls.len() >= max_calls {
1481 return false;
1482 }
1483
1484 calls.push(now);
1485 true
1486}
1487
1488#[cfg(test)]
1489fn clear_spam_buckets() {
1490 let buckets = SPAM_BUCKETS.get_or_init(|| Mutex::new(HashMap::new()));
1491 if let Ok(mut guard) = buckets.lock() {
1492 guard.clear();
1493 }
1494}
1495
1496fn extract_from_file_content(content: &str, key_path: &str, ext: &str) -> Option<Value> {
1498 if ext == "json" {
1499 let parsed: serde_json::Value = serde_json::from_str(content).ok()?;
1501 let mut current = &parsed;
1502 for part in key_path.split('.') {
1503 current = current.get(part)?;
1504 }
1505 return Some(json_value_to_synx(current));
1506 }
1507
1508 let parsed = crate::parser::parse(content);
1510 let mut current = &parsed.root;
1511 for part in key_path.split('.') {
1512 match current {
1513 Value::Object(map) => {
1514 current = map.get(part)?;
1515 }
1516 _ => return None,
1517 }
1518 }
1519 Some(current.clone())
1520}
1521
1522fn json_value_to_synx(v: &serde_json::Value) -> Value {
1524 match v {
1525 serde_json::Value::Null => Value::Null,
1526 serde_json::Value::Bool(b) => Value::Bool(*b),
1527 serde_json::Value::Number(n) => {
1528 if let Some(i) = n.as_i64() {
1529 Value::Int(i)
1530 } else if let Some(f) = n.as_f64() {
1531 Value::Float(f)
1532 } else {
1533 Value::Null
1534 }
1535 }
1536 serde_json::Value::String(s) => Value::String(s.clone()),
1537 serde_json::Value::Array(arr) => {
1538 Value::Array(arr.iter().map(json_value_to_synx).collect())
1539 }
1540 serde_json::Value::Object(map) => {
1541 let mut out = HashMap::new();
1542 for (k, v) in map {
1543 out.insert(k.clone(), json_value_to_synx(v));
1544 }
1545 Value::Object(out)
1546 }
1547 }
1548}
1549
1550fn stringify_value(value: &Value, indent: usize) -> String {
1554 let spaces = " ".repeat(indent);
1555 match value {
1556 Value::Object(map) => {
1557 let mut out = String::new();
1558 let mut keys: Vec<&str> = map.keys().map(|k| k.as_str()).collect();
1559 keys.sort_unstable();
1560 for key in keys {
1561 let val = &map[key];
1562 match val {
1563 Value::Object(_) => {
1564 out.push_str(&format!("{}{}\n", spaces, key));
1565 out.push_str(&stringify_value(val, indent + 2));
1566 }
1567 Value::Array(arr) => {
1568 out.push_str(&format!("{}{}\n", spaces, key));
1569 for item in arr {
1570 out.push_str(&format!("{} - {}\n", spaces, value_to_string(item)));
1571 }
1572 }
1573 _ => {
1574 out.push_str(&format!("{}{} {}\n", spaces, key, value_to_string(val)));
1575 }
1576 }
1577 }
1578 out
1579 }
1580 _ => format!("{}{}\n", spaces, value_to_string(value)),
1581 }
1582}
1583
1584pub(crate) fn cast_primitive(val: &str) -> Value {
1585 if val.len() >= 2 {
1587 let bytes = val.as_bytes();
1588 if (bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"')
1589 || (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\'')
1590 {
1591 return Value::String(val[1..val.len() - 1].to_string());
1592 }
1593 }
1594 match val {
1595 "true" => Value::Bool(true),
1596 "false" => Value::Bool(false),
1597 "null" => Value::Null,
1598 _ => {
1599 if let Ok(i) = val.parse::<i64>() {
1600 Value::Int(i)
1601 } else if let Ok(f) = val.parse::<f64>() {
1602 Value::Float(f)
1603 } else {
1604 Value::String(val.to_string())
1605 }
1606 }
1607 }
1608}
1609
1610fn delimiter_from_keyword(keyword: &str) -> String {
1611 match keyword {
1612 "space" => " ".to_string(),
1613 "pipe" => "|".to_string(),
1614 "dash" => "-".to_string(),
1615 "dot" => ".".to_string(),
1616 "semi" => ";".to_string(),
1617 "tab" => "\t".to_string(),
1618 "slash" => "/".to_string(),
1619 other => other.to_string(),
1620 }
1621}
1622
1623fn value_as_number(v: &Value) -> Option<f64> {
1624 match v {
1625 Value::Int(n) => Some(*n as f64),
1626 Value::Float(f) => Some(*f),
1627 _ => None,
1628 }
1629}
1630
1631fn value_to_string(v: &Value) -> String {
1632 match v {
1633 Value::String(s) => s.clone(),
1634 Value::Int(n) => n.to_string(),
1635 Value::Float(f) => format_number(*f as f64),
1636 Value::Bool(b) => b.to_string(),
1637 Value::Null => "null".to_string(),
1638 Value::Secret(s) => s.clone(),
1639 Value::Array(_) | Value::Object(_) => String::new(),
1640 }
1641}
1642
1643fn format_number(n: f64) -> String {
1644 if n.fract() == 0.0 && n.abs() < i64::MAX as f64 {
1645 (n as i64).to_string()
1646 } else {
1647 n.to_string()
1648 }
1649}
1650
1651fn replace_word(haystack: &str, word: &str, replacement: &str) -> String {
1653 let word_bytes = word.as_bytes();
1654 let word_len = word_bytes.len();
1655 let hay_bytes = haystack.as_bytes();
1656 let hay_len = hay_bytes.len();
1657
1658 if word_len > hay_len {
1659 return haystack.to_string();
1660 }
1661
1662 let mut result = String::with_capacity(hay_len.min(MAX_ENGINE_SCRATCH_STRING));
1663 let mut i = 0;
1664
1665 while i <= hay_len - word_len {
1666 if result.len() >= MAX_ENGINE_SCRATCH_STRING {
1667 break;
1668 }
1669 if &hay_bytes[i..i + word_len] == word_bytes {
1670 let before_ok = i == 0 || !is_word_char(hay_bytes[i - 1]);
1671 let after_ok = i + word_len >= hay_len || !is_word_char(hay_bytes[i + word_len]);
1672 if before_ok && after_ok {
1673 let room = MAX_ENGINE_SCRATCH_STRING.saturating_sub(result.len());
1674 if room > 0 {
1675 let take = replacement.len().min(room);
1676 let end = replacement.floor_char_boundary(take);
1677 result.push_str(&replacement[..end]);
1678 }
1679 i += word_len;
1680 continue;
1681 }
1682 }
1683 if result.len() < MAX_ENGINE_SCRATCH_STRING {
1684 result.push(hay_bytes[i] as char);
1685 }
1686 i += 1;
1687 }
1688 while i < hay_len && result.len() < MAX_ENGINE_SCRATCH_STRING {
1689 result.push(hay_bytes[i] as char);
1690 i += 1;
1691 }
1692 result
1693}
1694
1695fn is_word_char(b: u8) -> bool {
1696 b.is_ascii_alphanumeric() || b == b'_'
1697}
1698
1699fn weighted_random(items: &[Value], weights: &[f64]) -> Value {
1700 let mut w: Vec<f64> = weights.to_vec();
1701 if w.len() < items.len() {
1702 let assigned: f64 = w.iter().sum();
1703 let per_item = if assigned < 100.0 {
1707 (100.0 - assigned) / (items.len() - w.len()) as f64
1708 } else {
1709 assigned / w.len() as f64
1710 };
1711 while w.len() < items.len() {
1712 w.push(per_item);
1713 }
1714 }
1715 let total: f64 = w.iter().sum();
1716 if total <= 0.0 {
1717 return items[rng::random_usize(items.len())].clone();
1718 }
1719
1720 let rand_val = rng::random_f64_01();
1721 let mut cumulative = 0.0;
1722 for (i, item) in items.iter().enumerate() {
1723 cumulative += w[i] / total;
1724 if rand_val <= cumulative {
1725 return item.clone();
1726 }
1727 }
1728 items.last().cloned().unwrap_or(Value::Null)
1729}
1730
1731fn apply_inheritance(root: &mut Value, metadata: &HashMap<String, MetaMap>) {
1734 let root_meta = match metadata.get("") {
1735 Some(m) => m.clone(),
1736 None => return,
1737 };
1738
1739 let root_map = match root.as_object_mut() {
1740 Some(m) => m as *mut HashMap<String, Value>,
1741 None => return,
1742 };
1743
1744 let mut inherits: Vec<(String, Vec<String>)> = Vec::new();
1750 for (key, meta) in &root_meta {
1751 if meta.markers.contains(&"inherit".to_string()) {
1752 let idx = meta.markers.iter().position(|m| m == "inherit").unwrap();
1753 let mut parents: Vec<String> = meta.markers[idx + 1..].to_vec();
1754 if parents.is_empty() && !meta.args.is_empty() {
1756 parents = meta.args.clone();
1757 }
1758 if !parents.is_empty() {
1759 inherits.push((key.clone(), parents));
1760 }
1761 }
1762 }
1763
1764 let map = unsafe { &mut *root_map };
1765 for (child_key, parents) in &inherits {
1766 let mut merged: HashMap<String, Value> = HashMap::new();
1768 for parent_name in parents {
1769 if let Some(Value::Object(p)) = map.get(parent_name) {
1770 for (k, v) in p {
1771 merged.insert(k.clone(), v.clone());
1772 }
1773 }
1774 }
1775 if let Some(Value::Object(c)) = map.get(child_key) {
1777 for (k, v) in c {
1778 merged.insert(k.clone(), v.clone());
1779 }
1780 }
1781 map.insert(child_key.clone(), Value::Object(merged));
1782 }
1783}
1784
1785fn deep_get(root: &Value, path: &str) -> Option<Value> {
1786 if let Value::Object(map) = root {
1788 if let Some(val) = map.get(path) {
1789 return Some(val.clone());
1790 }
1791 }
1792 let parts: Vec<&str> = path.split('.').collect();
1794 let mut current = root;
1795 for part in parts {
1796 match current {
1797 Value::Object(map) => match map.get(part) {
1798 Some(v) => current = v,
1799 None => return None,
1800 },
1801 _ => return None,
1802 }
1803 }
1804 Some(current.clone())
1805}
1806
1807fn resolve_interpolation(
1810 tpl: &str,
1811 root: &Value,
1812 local_map: &HashMap<String, Value>,
1813 includes: &HashMap<String, Value>,
1814) -> String {
1815 let bytes = tpl.as_bytes();
1816 let len = bytes.len();
1817 let mut result = String::with_capacity(len.min(MAX_ENGINE_SCRATCH_STRING));
1818 let mut i = 0;
1819
1820 while i < len {
1821 if result.len() >= MAX_ENGINE_SCRATCH_STRING {
1822 break;
1823 }
1824 if bytes[i] == b'{' {
1825 if let Some(close) = tpl[i + 1..].find('}') {
1826 let inner = &tpl[i + 1..i + 1 + close];
1827 if let Some(colon) = inner.find(':') {
1829 let ref_name = &inner[..colon];
1830 let scope = &inner[colon + 1..];
1831 if ref_name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '.') {
1833 let resolved = if scope == "include" {
1834 if includes.len() == 1 {
1836 let first = includes.values().next().unwrap();
1837 deep_get(first, ref_name)
1838 } else {
1839 None
1840 }
1841 } else {
1842 includes.get(scope).and_then(|inc| deep_get(inc, ref_name))
1844 };
1845 if let Some(val) = resolved {
1846 let s = value_to_string(&val);
1847 let room = MAX_ENGINE_SCRATCH_STRING.saturating_sub(result.len());
1848 if room > 0 {
1849 let take = s.len().min(room);
1850 let end = s.floor_char_boundary(take);
1851 result.push_str(&s[..end]);
1852 }
1853 } else {
1854 result.push('{');
1855 let rem = MAX_ENGINE_SCRATCH_STRING.saturating_sub(result.len() + 1);
1856 if rem > 0 {
1857 let end = inner.floor_char_boundary(inner.len().min(rem));
1858 result.push_str(&inner[..end]);
1859 }
1860 if result.len() < MAX_ENGINE_SCRATCH_STRING {
1861 result.push('}');
1862 }
1863 }
1864 i += 2 + close;
1865 continue;
1866 }
1867 } else {
1868 let ref_name = inner;
1870 if ref_name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '.') {
1871 let resolved = deep_get(root, ref_name).or_else(|| {
1872 local_map.get(ref_name).cloned()
1873 });
1874 if let Some(val) = resolved {
1875 let s = value_to_string(&val);
1876 let room = MAX_ENGINE_SCRATCH_STRING.saturating_sub(result.len());
1877 if room > 0 {
1878 let take = s.len().min(room);
1879 let end = s.floor_char_boundary(take);
1880 result.push_str(&s[..end]);
1881 }
1882 } else {
1883 result.push('{');
1884 let rem = MAX_ENGINE_SCRATCH_STRING.saturating_sub(result.len() + 1);
1885 if rem > 0 {
1886 let end = ref_name.floor_char_boundary(ref_name.len().min(rem));
1887 result.push_str(&ref_name[..end]);
1888 }
1889 if result.len() < MAX_ENGINE_SCRATCH_STRING {
1890 result.push('}');
1891 }
1892 }
1893 i += 2 + close;
1894 continue;
1895 }
1896 }
1897 }
1898 }
1899 if result.len() < MAX_ENGINE_SCRATCH_STRING {
1900 result.push(bytes[i] as char);
1901 }
1902 i += 1;
1903 }
1904 result
1905}
1906
1907fn load_includes(
1909 directives: &[IncludeDirective],
1910 options: &Options,
1911) -> HashMap<String, Value> {
1912 let mut map = HashMap::new();
1913 let base = options.base_path.as_deref().unwrap_or(".");
1914 let max_depth = options.max_include_depth.unwrap_or(DEFAULT_MAX_INCLUDE_DEPTH);
1915 if options._include_depth >= max_depth {
1916 return map;
1917 }
1918 for inc in directives {
1919 let full = match jail_path(base, &inc.path) {
1920 Ok(p) => p,
1921 Err(_) => continue,
1922 };
1923 if check_file_size(&full).is_err() {
1924 continue;
1925 }
1926 if let Ok(text) = std::fs::read_to_string(&full) {
1927 let mut included = parser::parse(&text);
1928 if included.mode == Mode::Active {
1929 let mut child_opts = options.clone();
1930 child_opts._include_depth += 1;
1931 if let Some(parent) = full.parent() {
1932 child_opts.base_path = Some(parent.to_string_lossy().into_owned());
1933 }
1934 resolve(&mut included, &child_opts);
1935 }
1936 map.insert(inc.alias.clone(), included.root);
1937 }
1938 }
1939 map
1940}
1941
1942fn load_packages(
1947 directives: &[UseDirective],
1948 options: &Options,
1949 #[cfg(feature = "wasm")] wasm_runtime: &mut crate::wasm::WasmMarkerRuntime,
1950) -> HashMap<String, Value> {
1951 let mut map = HashMap::new();
1952 let pkg_base = options
1953 .packages_path
1954 .as_deref()
1955 .unwrap_or("./synx_packages");
1956 let base = options.base_path.as_deref().unwrap_or(".");
1957 let pkg_root = std::path::Path::new(base).join(pkg_base);
1958
1959 for ud in directives {
1960 let pkg_dir = pkg_root.join(&ud.package);
1962
1963 #[cfg(feature = "wasm")]
1965 {
1966 if is_marker_package(&pkg_dir) {
1967 let wasm_entry = read_manifest_wasm(&pkg_dir)
1969 .unwrap_or_else(|| pkg_dir.join("src").join("main.wasm"));
1970 let caps = read_manifest_capabilities(&pkg_dir);
1971 if wasm_entry.is_file() {
1972 if let Ok(wasm_bytes) = std::fs::read(&wasm_entry) {
1973 match wasm_runtime.load_module(&wasm_bytes, caps) {
1974 Ok(_markers) => {}
1975 Err(e) => {
1976 map.insert(
1977 ud.alias.clone(),
1978 Value::String(format!("WASM_ERR: {}", e)),
1979 );
1980 }
1981 }
1982 }
1983 }
1984 continue;
1985 }
1986 }
1987
1988 let entry = read_manifest_main(&pkg_dir)
1990 .unwrap_or_else(|| pkg_dir.join("src").join("main.synx"));
1991
1992 if !entry.is_file() {
1993 continue;
1994 }
1995 if check_file_size(&entry).is_err() {
1996 continue;
1997 }
1998 if let Ok(text) = std::fs::read_to_string(&entry) {
1999 let mut parsed = parser::parse(&text);
2000 if parsed.mode == Mode::Active {
2001 let mut child_opts = options.clone();
2002 child_opts._include_depth += 1;
2003 child_opts.base_path = Some(
2004 entry.parent().unwrap_or(&pkg_dir).to_string_lossy().into_owned()
2005 );
2006 resolve(&mut parsed, &child_opts);
2007 }
2008 map.insert(ud.alias.clone(), parsed.root);
2009 }
2010 }
2011 map
2012}
2013
2014fn read_manifest_main(pkg_dir: &std::path::Path) -> Option<std::path::PathBuf> {
2017 let manifest = pkg_dir.join("synx-pkg.synx");
2018 let text = std::fs::read_to_string(&manifest).ok()?;
2019 for line in text.lines() {
2020 let trimmed = line.trim();
2021 if let Some(rest) = trimmed.strip_prefix("main ") {
2022 let entry_path = rest.trim();
2023 if !entry_path.is_empty() {
2024 return Some(pkg_dir.join(entry_path));
2025 }
2026 }
2027 if let Some(rest) = trimmed.strip_prefix("entry ") {
2029 let entry_path = rest.trim();
2030 if !entry_path.is_empty() {
2031 return Some(pkg_dir.join(entry_path));
2032 }
2033 }
2034 }
2035 None
2036}
2037
2038#[cfg(feature = "wasm")]
2041fn is_marker_package(pkg_dir: &std::path::Path) -> bool {
2042 let manifest = pkg_dir.join("synx-pkg.synx");
2043 if let Ok(text) = std::fs::read_to_string(&manifest) {
2044 for line in text.lines() {
2045 let trimmed = line.trim();
2046 if trimmed == "type markers" {
2047 return true;
2048 }
2049 if let Some(rest) = trimmed.strip_prefix("main ") {
2050 if rest.trim().ends_with(".wasm") {
2051 return true;
2052 }
2053 }
2054 }
2055 }
2056 false
2057}
2058
2059#[cfg(feature = "wasm")]
2061fn read_manifest_wasm(pkg_dir: &std::path::Path) -> Option<std::path::PathBuf> {
2062 let manifest = pkg_dir.join("synx-pkg.synx");
2063 let text = std::fs::read_to_string(&manifest).ok()?;
2064 for line in text.lines() {
2065 let trimmed = line.trim();
2066 if let Some(rest) = trimmed.strip_prefix("wasm ") {
2067 let wasm_path = rest.trim();
2068 if !wasm_path.is_empty() {
2069 return Some(pkg_dir.join(wasm_path));
2070 }
2071 }
2072 if let Some(rest) = trimmed.strip_prefix("main ") {
2074 let path = rest.trim();
2075 if path.ends_with(".wasm") {
2076 return Some(pkg_dir.join(path));
2077 }
2078 }
2079 }
2080 None
2081}
2082
2083#[cfg(feature = "wasm")]
2085fn read_manifest_capabilities(pkg_dir: &std::path::Path) -> crate::wasm::WasmCapabilities {
2086 let manifest = pkg_dir.join("synx-pkg.synx");
2087 let text = match std::fs::read_to_string(&manifest) {
2088 Ok(t) => t,
2089 Err(_) => return crate::wasm::WasmCapabilities::default(),
2090 };
2091 for line in text.lines() {
2092 let trimmed = line.trim();
2093 if let Some(rest) = trimmed.strip_prefix("permissions ") {
2094 return crate::wasm::WasmCapabilities::from_manifest_line(rest);
2095 }
2096 }
2097 crate::wasm::WasmCapabilities::default()
2098}
2099
2100fn build_type_registry(metadata: &HashMap<String, MetaMap>) -> HashMap<String, String> {
2105 let mut registry: HashMap<String, String> = HashMap::new();
2106
2107 for meta_map in metadata.values() {
2108 for (key, meta) in meta_map {
2109 if let Some(ref type_hint) = meta.type_hint {
2110 if let Some(existing) = registry.get(key) {
2112 if existing != type_hint {
2113 }
2116 } else {
2117 registry.insert(key.clone(), type_hint.clone());
2118 }
2119 }
2120 }
2121 }
2122
2123 registry
2124}
2125
2126fn build_constraint_registry(metadata: &HashMap<String, MetaMap>) -> HashMap<String, Constraints> {
2129 let mut registry: HashMap<String, Constraints> = HashMap::new();
2130
2131 for meta_map in metadata.values() {
2132 for (key, meta) in meta_map {
2133 if let Some(ref constraints) = meta.constraints {
2134 registry
2135 .entry(key.clone())
2136 .and_modify(|existing| merge_constraints(existing, constraints))
2137 .or_insert_with(|| constraints.clone());
2138 }
2139 }
2140 }
2141
2142 registry
2143}
2144
2145fn merge_constraints(base: &mut Constraints, incoming: &Constraints) {
2148 if incoming.required {
2149 base.required = true;
2150 }
2151 if incoming.readonly {
2152 base.readonly = true;
2153 }
2154
2155 base.min = match (base.min, incoming.min) {
2157 (Some(a), Some(b)) => Some(a.max(b)),
2158 (None, Some(b)) => Some(b),
2159 (a, None) => a,
2160 };
2161 base.max = match (base.max, incoming.max) {
2162 (Some(a), Some(b)) => Some(a.min(b)),
2163 (None, Some(b)) => Some(b),
2164 (a, None) => a,
2165 };
2166
2167 if base.type_name.is_none() {
2169 base.type_name = incoming.type_name.clone();
2170 }
2171 if base.pattern.is_none() {
2172 base.pattern = incoming.pattern.clone();
2173 }
2174 if base.enum_values.is_none() {
2175 base.enum_values = incoming.enum_values.clone();
2176 }
2177}
2178
2179fn validate_field_constraints(value: &mut Value, registry: &HashMap<String, Constraints>) {
2187 if let Value::Object(ref mut map) = value {
2188 let keys: Vec<String> = map.keys().cloned().collect();
2189 for key in &keys {
2190 if let Some(constraints) = registry.get(key) {
2191 let already_errored = matches!(
2192 map.get(key),
2193 Some(Value::String(s)) if s.starts_with("CONSTRAINT_ERR:")
2194 || s.starts_with("TYPE_ERR:")
2195 );
2196 if !already_errored {
2197 validate_constraints(map, key, constraints);
2198 }
2199 }
2200
2201 if let Some(child) = map.get_mut(key) {
2202 match child {
2203 Value::Object(_) => validate_field_constraints(child, registry),
2204 Value::Array(arr) => {
2205 for item in arr.iter_mut() {
2206 if let Value::Object(_) = item {
2207 validate_field_constraints(item, registry);
2208 }
2209 }
2210 }
2211 _ => {}
2212 }
2213 }
2214 }
2215 }
2216}
2217
2218fn validate_field_types(value: &mut Value, registry: &HashMap<String, String>, path: &str) {
2220 match value {
2221 Value::Object(ref mut map) => {
2222 let keys: Vec<String> = map.keys().cloned().collect();
2223 for key in &keys {
2224 if let Some(expected_type) = registry.get(key) {
2225 if let Some(val) = map.get(key) {
2226 if !value_matches_type(val, expected_type) {
2227 let current_type = value_type_name(val);
2229 map.insert(key.clone(), Value::String(
2230 format!("TYPE_ERR: '{}' expected {} but got {}", key, expected_type, current_type)
2231 ));
2232 }
2233 }
2234 }
2235
2236 if let Some(child) = map.get_mut(key) {
2238 match child {
2239 Value::Object(_) => {
2240 let child_path = if path.is_empty() {
2241 key.clone()
2242 } else {
2243 format!("{}.{}", path, key)
2244 };
2245 validate_field_types(child, registry, &child_path);
2246 }
2247 Value::Array(ref mut arr) => {
2248 for item in arr.iter_mut() {
2249 if let Value::Object(_) = item {
2250 validate_field_types(item, registry, path);
2251 }
2252 }
2253 }
2254 _ => {}
2255 }
2256 }
2257 }
2258 }
2259 _ => {}
2260 }
2261}
2262
2263fn value_matches_type(value: &Value, expected_type: &str) -> bool {
2265 match expected_type {
2266 "int" => matches!(value, Value::Int(_)),
2267 "float" => matches!(value, Value::Float(_) | Value::Int(_)),
2268 "bool" => matches!(value, Value::Bool(_)),
2269 "string" => matches!(value, Value::String(_) | Value::Secret(_)),
2270 "array" => matches!(value, Value::Array(_)),
2271 "object" => matches!(value, Value::Object(_)),
2272 _ => true, }
2274}
2275
2276fn value_type_name(value: &Value) -> String {
2278 match value {
2279 Value::Int(_) => "int".to_string(),
2280 Value::Float(_) => "float".to_string(),
2281 Value::Bool(_) => "bool".to_string(),
2282 Value::String(_) => "string".to_string(),
2283 Value::Secret(_) => "secret".to_string(),
2284 Value::Array(_) => "array".to_string(),
2285 Value::Object(_) => "object".to_string(),
2286 Value::Null => "null".to_string(),
2287 }
2288}
2289
2290fn plural_category(lang: &str, n: i64) -> &'static str {
2295 let abs_n = n.unsigned_abs();
2296 let n10 = abs_n % 10;
2297 let n100 = abs_n % 100;
2298
2299 match lang {
2300 "ru" | "uk" | "be" => {
2302 if n10 == 1 && n100 != 11 {
2303 "one"
2304 } else if (2..=4).contains(&n10) && !(12..=14).contains(&n100) {
2305 "few"
2306 } else {
2307 "many"
2308 }
2309 }
2310 "pl" => {
2312 if n10 == 1 && n100 != 11 {
2313 "one"
2314 } else if (2..=4).contains(&n10) && !(12..=14).contains(&n100) {
2315 "few"
2316 } else {
2317 "many"
2318 }
2319 }
2320 "cs" | "sk" => {
2322 if abs_n == 1 { "one" }
2323 else if (2..=4).contains(&abs_n) { "few" }
2324 else { "other" }
2325 }
2326 "ar" => {
2328 if abs_n == 0 { "zero" }
2329 else if abs_n == 1 { "one" }
2330 else if abs_n == 2 { "two" }
2331 else if (3..=10).contains(&n100) { "few" }
2332 else if (11..=99).contains(&n100) { "many" }
2333 else { "other" }
2334 }
2335 "fr" | "pt" => {
2337 if abs_n <= 1 { "one" } else { "other" }
2338 }
2339 "ja" | "zh" | "ko" | "vi" | "th" => "other",
2341 _ => {
2343 if abs_n == 1 { "one" } else { "other" }
2344 }
2345 }
2346}
2347
2348#[cfg(test)]
2349mod tests {
2350 use crate::{parse, Options, Value};
2351 use super::{resolve, read_manifest_main};
2352
2353 #[test]
2354 fn test_ref_simple() {
2355 let mut r = parse("!active\nbase_rate 50\nquick_rate:ref base_rate");
2356 resolve(&mut r, &Options::default());
2357 let map = r.root.as_object().unwrap();
2358 assert_eq!(map["quick_rate"], Value::Int(50));
2359 }
2360
2361 #[test]
2362 fn test_ref_calc_shorthand() {
2363 let mut r = parse("!active\nbase_rate 50\ndouble_rate:ref:calc:*2 base_rate");
2364 resolve(&mut r, &Options::default());
2365 let map = r.root.as_object().unwrap();
2366 assert_eq!(map["double_rate"], Value::Int(100));
2367 }
2368
2369 #[test]
2370 fn test_inherit() {
2371 let mut r = parse("!active\n_base\n weight 10\n stackable true\nsteel:inherit:_base\n weight 25\n material metal");
2372 resolve(&mut r, &Options::default());
2373 let map = r.root.as_object().unwrap();
2374 assert!(!map.contains_key("_base"));
2375 let steel = map["steel"].as_object().unwrap();
2376 assert_eq!(steel["weight"], Value::Int(25));
2377 assert_eq!(steel["stackable"], Value::Bool(true));
2378 assert_eq!(steel["material"], Value::String("metal".into()));
2379 }
2380
2381 #[test]
2382 fn test_i18n_select_lang() {
2383 let mut r = parse("!active\ntitle:i18n\n en Hello\n ru Привет\n de Hallo");
2384 let opts = Options { lang: Some("ru".into()), ..Default::default() };
2385 resolve(&mut r, &opts);
2386 let map = r.root.as_object().unwrap();
2387 assert_eq!(map["title"], Value::String("Привет".into()));
2388 }
2389
2390 #[test]
2391 fn test_i18n_fallback_en() {
2392 let mut r = parse("!active\ntitle:i18n\n en Hello\n ru Привет");
2393 let opts = Options { lang: Some("fr".into()), ..Default::default() };
2394 resolve(&mut r, &opts);
2395 let map = r.root.as_object().unwrap();
2396 assert_eq!(map["title"], Value::String("Hello".into()));
2397 }
2398
2399 #[test]
2400 fn test_auto_interpolation_simple() {
2401 let mut r = parse("!active\nname Wario\ngreeting Hello, {name}!");
2402 resolve(&mut r, &Options::default());
2403 let map = r.root.as_object().unwrap();
2404 assert_eq!(map["greeting"], Value::String("Hello, Wario!".into()));
2405 }
2406
2407 #[test]
2408 fn test_auto_interpolation_nested() {
2409 let mut r = parse("!active\nserver\n host localhost\n port 8080\nurl http://{server.host}:{server.port}/api");
2410 resolve(&mut r, &Options::default());
2411 let map = r.root.as_object().unwrap();
2412 assert_eq!(map["url"], Value::String("http://localhost:8080/api".into()));
2413 }
2414
2415 #[test]
2416 fn test_template_legacy_still_works() {
2417 let mut r = parse("!active\nname Wario\ngreeting:template Hello, {name}!");
2418 resolve(&mut r, &Options::default());
2419 let map = r.root.as_object().unwrap();
2420 assert_eq!(map["greeting"], Value::String("Hello, Wario!".into()));
2421 }
2422
2423 #[test]
2424 fn test_type_validation() {
2425 let mut r = parse(
2428 "!active\n\
2429 _base_unit\n \
2430 hp(int) 100\n \
2431 speed(float) 1.5\n\
2432 infantry:inherit:_base_unit\n \
2433 name Infantry\n \
2434 hp 80"
2435 );
2436 resolve(&mut r, &Options::default());
2437 let map = r.root.as_object().unwrap();
2438
2439 assert!(!map.contains_key("_base_unit"));
2441
2442 let infantry = map["infantry"].as_object().unwrap();
2444 assert_eq!(infantry["hp"], Value::Int(80)); assert_eq!(infantry["speed"], Value::Float(1.5)); }
2447
2448 #[test]
2449 fn test_type_validation_error() {
2450 let mut r = parse(
2452 "!active\n\
2453 _base_unit\n \
2454 hp(int) 100\n\
2455 infantry:inherit:_base_unit\n \
2456 hp hello" );
2458 resolve(&mut r, &Options::default());
2459 let map = r.root.as_object().unwrap();
2460
2461 let infantry = map["infantry"].as_object().unwrap();
2462 if let Value::String(s) = &infantry["hp"] {
2464 assert!(s.contains("TYPE_ERR"));
2465 } else {
2466 panic!("Expected error string for type mismatch");
2467 }
2468 }
2469
2470 #[test]
2471 fn test_constraint_validation_inherited_range() {
2472 let mut r = parse(
2473 "!active\n\
2474 _base_unit\n \
2475 hp[min:1, max:50000] 1000\n\
2476 infantry:inherit:_base_unit\n \
2477 hp 60000"
2478 );
2479 resolve(&mut r, &Options::default());
2480 let map = r.root.as_object().unwrap();
2481 let infantry = map["infantry"].as_object().unwrap();
2482
2483 if let Value::String(s) = &infantry["hp"] {
2484 assert!(s.contains("CONSTRAINT_ERR"));
2485 assert!(s.contains("exceeds max"));
2486 } else {
2487 panic!("Expected constraint error string");
2488 }
2489 }
2490
2491 #[test]
2492 fn test_constraint_validation_required() {
2493 let mut r = parse(
2494 "!active\n\
2495 _base_unit\n \
2496 description[type:string, required] hello\n\
2497 scout:inherit:_base_unit\n \
2498 description null"
2499 );
2500 resolve(&mut r, &Options::default());
2501 let map = r.root.as_object().unwrap();
2502 let scout = map["scout"].as_object().unwrap();
2503
2504 if let Value::String(s) = &scout["description"] {
2505 assert!(s.contains("CONSTRAINT_ERR"));
2506 assert!(s.contains("required"));
2507 } else {
2508 panic!("Expected required-constraint error string");
2509 }
2510 }
2511
2512 #[test]
2513 fn test_multi_parent_inherit() {
2514 let mut r = parse(
2515 "!active\n\
2516 _movable\n \
2517 speed 10\n \
2518 can_move true\n\
2519 _damageable\n \
2520 hp 100\n \
2521 armor 5\n\
2522 tank:inherit:_movable:_damageable\n \
2523 name Tank\n \
2524 armor 20"
2525 );
2526 resolve(&mut r, &Options::default());
2527 let map = r.root.as_object().unwrap();
2528
2529 assert!(!map.contains_key("_movable"));
2530 assert!(!map.contains_key("_damageable"));
2531
2532 let tank = map["tank"].as_object().unwrap();
2533 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()));
2538 }
2539
2540 #[test]
2541 fn test_calc_dot_path() {
2542 let mut r = parse(
2543 "!active\n\
2544 stats\n \
2545 base_hp 100\n \
2546 multiplier 3\n\
2547 total_hp:calc stats.base_hp * stats.multiplier"
2548 );
2549 resolve(&mut r, &Options::default());
2550 let map = r.root.as_object().unwrap();
2551 assert_eq!(map["total_hp"], Value::Int(300));
2552 }
2553
2554 #[test]
2555 fn test_i18n_plural_en() {
2556 let mut r = parse(
2557 "!active\n\
2558 count 5\n\
2559 items:i18n:count\n \
2560 en\n \
2561 one item\n \
2562 other items"
2563 );
2564 let opts = Options { lang: Some("en".into()), ..Default::default() };
2565 resolve(&mut r, &opts);
2566 let map = r.root.as_object().unwrap();
2567 assert_eq!(map["items"], Value::String("items".into()));
2568 }
2569
2570 #[test]
2571 fn test_i18n_plural_en_one() {
2572 let mut r = parse(
2573 "!active\n\
2574 count 1\n\
2575 items:i18n:count\n \
2576 en\n \
2577 one {count} item\n \
2578 other {count} items"
2579 );
2580 let opts = Options { lang: Some("en".into()), ..Default::default() };
2581 resolve(&mut r, &opts);
2582 let map = r.root.as_object().unwrap();
2583 assert_eq!(map["items"], Value::String("1 item".into()));
2584 }
2585
2586 #[test]
2587 fn test_i18n_plural_ru() {
2588 let mut r = parse(
2589 "!active\n\
2590 count 3\n\
2591 items:i18n:count\n \
2592 ru\n \
2593 one предмет\n \
2594 few предмета\n \
2595 many предметов\n \
2596 other предметов"
2597 );
2598 let opts = Options { lang: Some("ru".into()), ..Default::default() };
2599 resolve(&mut r, &opts);
2600 let map = r.root.as_object().unwrap();
2601 assert_eq!(map["items"], Value::String("предмета".into()));
2602 }
2603
2604 #[test]
2605 fn test_quoted_null_preserved() {
2606 let r = parse("status \"null\"\nenabled \"true\"\ncount \"42\"");
2607 let map = r.root.as_object().unwrap();
2608 assert_eq!(map["status"], Value::String("null".into()));
2609 assert_eq!(map["enabled"], Value::String("true".into()));
2610 assert_eq!(map["count"], Value::String("42".into()));
2611 }
2612
2613 #[test]
2614 fn test_unquoted_null_is_null() {
2615 let r = parse("status null\nenabled true\ncount 42");
2616 let map = r.root.as_object().unwrap();
2617 assert_eq!(map["status"], Value::Null);
2618 assert_eq!(map["enabled"], Value::Bool(true));
2619 assert_eq!(map["count"], Value::Int(42));
2620 }
2621
2622 #[test]
2623 fn test_spam_rate_limit_exceeded() {
2624 super::clear_spam_buckets();
2625
2626 let mut r1 = parse("!active\nsecret_token abc\naccess:spam:1:5 secret_token");
2627 resolve(&mut r1, &Options::default());
2628 let map1 = r1.root.as_object().unwrap();
2629 assert_eq!(map1["access"], Value::String("abc".into()));
2630
2631 let mut r2 = parse("!active\nsecret_token abc\naccess:spam:1:5 secret_token");
2632 resolve(&mut r2, &Options::default());
2633 let map2 = r2.root.as_object().unwrap();
2634 match &map2["access"] {
2635 Value::String(s) => assert!(s.starts_with("SPAM_ERR:")),
2636 _ => panic!("Expected SPAM_ERR string"),
2637 }
2638 }
2639
2640 #[test]
2641 fn test_spam_default_window_sec_is_one() {
2642 super::clear_spam_buckets();
2643
2644 let mut r = parse("!active\na 1\nx:spam:2 a");
2645 resolve(&mut r, &Options::default());
2646 let map = r.root.as_object().unwrap();
2647 assert_eq!(map["x"], Value::Int(1));
2648 }
2649
2650 #[test]
2651 fn test_deep_nesting_does_not_overflow() {
2652 let mut synx = String::from("!active\n");
2655 let mut indent = String::new();
2656 for i in 0..200 {
2657 synx.push_str(&format!("{}level_{}\n", indent, i));
2658 indent.push_str(" ");
2659 }
2660 synx.push_str(&format!("{}value deep\n", indent));
2661
2662 let mut result = parse(&synx);
2663 resolve(&mut result, &Default::default());
2664 assert!(matches!(result.root, Value::Object(_)));
2665
2666 let mut cur = &result.root;
2667 let mut depth = 0usize;
2668 loop {
2669 let Value::Object(map) = cur else { break };
2670 let key = format!("level_{}", depth);
2671 match map.get(&key) {
2672 Some(next) => {
2673 cur = next;
2674 depth += 1;
2675 }
2676 None => break,
2677 }
2678 }
2679 assert!(
2680 depth >= 100,
2681 "expected at least 100 chained levels from parse, got {}",
2682 depth
2683 );
2684 assert!(
2685 depth <= 130,
2686 "parser nesting cap should keep chain shallow, got {}",
2687 depth
2688 );
2689 }
2690
2691 #[test]
2692 fn test_circular_alias_returns_error() {
2693 let mut r = parse("!active\na:alias b\nb:alias a");
2694 resolve(&mut r, &Default::default());
2695 let root = r.root.as_object().unwrap();
2696 let a_val = root.get("a").unwrap();
2697 let b_val = root.get("b").unwrap();
2698 assert!(
2699 matches!(a_val, Value::String(s) if s.starts_with("ALIAS_ERR:")),
2700 "expected ALIAS_ERR for 'a', got: {:?}", a_val
2701 );
2702 assert!(
2703 matches!(b_val, Value::String(s) if s.starts_with("ALIAS_ERR:")),
2704 "expected ALIAS_ERR for 'b', got: {:?}", b_val
2705 );
2706 }
2707
2708 #[test]
2709 fn test_self_alias_returns_error() {
2710 let mut r = parse("!active\na:alias a");
2711 resolve(&mut r, &Default::default());
2712 let root = r.root.as_object().unwrap();
2713 let a_val = root.get("a").unwrap();
2714 assert!(
2715 matches!(a_val, Value::String(s) if s.starts_with("ALIAS_ERR:")),
2716 "expected ALIAS_ERR for self-alias, got: {:?}", a_val
2717 );
2718 }
2719
2720 #[test]
2721 fn test_valid_alias_still_works() {
2722 let mut r = parse("!active\nbase 42\ncopy:alias base");
2723 resolve(&mut r, &Default::default());
2724 let root = r.root.as_object().unwrap();
2725 assert_eq!(root.get("copy"), Some(&Value::Int(42)));
2726 }
2727
2728 #[test]
2729 fn test_alias_to_string_valued_key_no_false_positive() {
2730 let mut r = parse("!active\na b\nb:alias a");
2733 resolve(&mut r, &Default::default());
2734 let root = r.root.as_object().unwrap();
2735 assert_eq!(
2736 root.get("b"),
2737 Some(&Value::String("b".to_string())),
2738 "alias to a string-valued key should not produce ALIAS_ERR"
2739 );
2740 }
2741
2742 #[test]
2743 fn test_prompt_marker() {
2744 let mut r = parse("!active\nmemory:prompt:Core\n identity ASAI\n creator APERTURESyndicate");
2745 resolve(&mut r, &Options::default());
2746 let map = r.root.as_object().unwrap();
2747 if let Value::String(s) = &map["memory"] {
2748 assert!(s.starts_with("Core (SYNX):"));
2749 assert!(s.contains("```synx"));
2750 assert!(s.contains("identity ASAI"));
2751 } else {
2752 panic!("Expected :prompt to produce a string");
2753 }
2754 }
2755
2756 #[test]
2757 fn test_vision_marker_passthrough() {
2758 let mut r = parse("!active\nimage:vision Generate a sunset");
2759 resolve(&mut r, &Options::default());
2760 let map = r.root.as_object().unwrap();
2761 assert_eq!(map["image"], Value::String("Generate a sunset".into()));
2762 }
2763
2764 #[test]
2765 fn test_audio_marker_passthrough() {
2766 let mut r = parse("!active\nnarration:audio Read this summary aloud");
2767 resolve(&mut r, &Options::default());
2768 let map = r.root.as_object().unwrap();
2769 assert_eq!(map["narration"], Value::String("Read this summary aloud".into()));
2770 }
2771
2772 #[test]
2773 fn test_use_directive_loads_package() {
2774 let tmp = std::env::temp_dir().join("synx-use-test-load");
2776 let _ = std::fs::remove_dir_all(&tmp);
2777 let pkg_dir = tmp.join("synx_packages/@test/config");
2778 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
2779 std::fs::write(pkg_dir.join("synx-pkg.synx"), "name @test/config\nversion 1.0.0\nmain src/main.synx\n").unwrap();
2780 std::fs::write(pkg_dir.join("src/main.synx"), "identity APERTURESyndicate\ndefault_port 8080\n").unwrap();
2781
2782 let mut r = parse("!active\n!use @test/config\napp MyApp");
2783 let opts = Options {
2784 base_path: Some(tmp.to_string_lossy().into_owned()),
2785 ..Default::default()
2786 };
2787 resolve(&mut r, &opts);
2788 let map = r.root.as_object().unwrap();
2789 assert!(map.contains_key("config"), "package not loaded");
2790 let pkg = map["config"].as_object().unwrap();
2791 assert_eq!(pkg["identity"], Value::String("APERTURESyndicate".into()));
2792 assert_eq!(pkg["default_port"], Value::Int(8080));
2793 assert_eq!(map["app"], Value::String("MyApp".into()));
2794 let _ = std::fs::remove_dir_all(&tmp);
2795 }
2796
2797 #[test]
2798 fn test_use_directive_with_alias() {
2799 let tmp = std::env::temp_dir().join("synx-use-test-alias");
2800 let _ = std::fs::remove_dir_all(&tmp);
2801 let pkg_dir = tmp.join("synx_packages/@test/config");
2802 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
2803 std::fs::write(pkg_dir.join("synx-pkg.synx"), "name @test/config\nversion 1.0.0\nmain src/main.synx\n").unwrap();
2804 std::fs::write(pkg_dir.join("src/main.synx"), "identity APERTURESyndicate\ndefault_port 8080\n").unwrap();
2805
2806 let mut r = parse("!active\n!use @test/config as defaults\napp MyApp");
2807 let opts = Options {
2808 base_path: Some(tmp.to_string_lossy().into_owned()),
2809 ..Default::default()
2810 };
2811 resolve(&mut r, &opts);
2812 let map = r.root.as_object().unwrap();
2813 assert!(map.contains_key("defaults"), "aliased package not loaded");
2814 assert!(!map.contains_key("config"), "should use alias, not auto name");
2815 let pkg = map["defaults"].as_object().unwrap();
2816 assert_eq!(pkg["identity"], Value::String("APERTURESyndicate".into()));
2817 let _ = std::fs::remove_dir_all(&tmp);
2818 }
2819
2820 #[test]
2821 fn test_use_directive_missing_package_ignored() {
2822 let mut r = parse("!active\n!use @nonexistent/pkg\napp MyApp");
2823 resolve(&mut r, &Options::default());
2824 let map = r.root.as_object().unwrap();
2825 assert!(!map.contains_key("pkg"));
2827 assert_eq!(map["app"], Value::String("MyApp".into()));
2828 }
2829
2830 #[test]
2831 fn test_use_reads_manifest_main_field() {
2832 let tmp = std::env::temp_dir().join("synx-use-test-main");
2833 let _ = std::fs::remove_dir_all(&tmp);
2834 let pkg_dir = tmp.join("synx_packages/@test/myapp");
2835 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
2836 std::fs::write(pkg_dir.join("synx-pkg.synx"), "name @test/myapp\nversion 1.0.0\nmain src/main.synx\n").unwrap();
2837 std::fs::write(pkg_dir.join("src/main.synx"), "app_name MyApp\nversion 2.0.0\n").unwrap();
2838
2839 let mut r = parse("!active\n!use @test/myapp as myapp\napp Test");
2840 let opts = Options {
2841 base_path: Some(tmp.to_string_lossy().into_owned()),
2842 ..Default::default()
2843 };
2844 resolve(&mut r, &opts);
2845 let map = r.root.as_object().unwrap();
2846 assert!(map.contains_key("myapp"), "package not loaded via manifest");
2847 let pkg = map["myapp"].as_object().unwrap();
2848 assert_eq!(pkg["app_name"], Value::String("MyApp".into()));
2849 let _ = std::fs::remove_dir_all(&tmp);
2850 }
2851
2852 #[test]
2853 fn test_read_manifest_main_function() {
2854 let tmp = std::env::temp_dir().join("synx-manifest-main-test");
2855 let _ = std::fs::remove_dir_all(&tmp);
2856 std::fs::create_dir_all(tmp.join("src")).unwrap();
2857 std::fs::write(tmp.join("synx-pkg.synx"), "name @test/pkg\nversion 1.0.0\nmain src/main.synx\n").unwrap();
2858 std::fs::write(tmp.join("src/main.synx"), "key value\n").unwrap();
2859
2860 let result = read_manifest_main(&tmp);
2861 assert!(result.is_some(), "should read main from synx-pkg.synx");
2862 let path = result.unwrap();
2863 assert!(path.ends_with("src/main.synx") || path.ends_with("src\\main.synx"));
2864 let _ = std::fs::remove_dir_all(&tmp);
2865 }
2866}