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 let fp = std::path::Path::new(file_path);
34 if fp.is_absolute() {
35 return Err(format!("SECURITY: absolute paths are not allowed: '{}'", file_path));
36 }
37 let base_canonical = match std::fs::canonicalize(base) {
38 Ok(p) => p,
39 Err(_) => std::path::PathBuf::from(base),
40 };
41 let full = base_canonical.join(file_path);
42 let full_canonical = match std::fs::canonicalize(&full) {
43 Ok(p) => p,
44 Err(_) => {
45 let normalized = full.to_string_lossy();
47 if normalized.contains("..") {
48 return Err(format!("SECURITY: path traversal detected: '{}'", file_path));
49 }
50 return Ok(full);
51 }
52 };
53 if !full_canonical.starts_with(&base_canonical) {
54 return Err(format!("SECURITY: path escapes base directory: '{}'", file_path));
55 }
56 Ok(full_canonical)
57}
58
59fn check_file_size(path: &std::path::Path) -> Result<(), String> {
61 match std::fs::metadata(path) {
62 Ok(meta) if meta.len() > MAX_FILE_SIZE => {
63 Err(format!("SECURITY: file too large ({} bytes, max {})", meta.len(), MAX_FILE_SIZE))
64 }
65 _ => Ok(()),
66 }
67}
68
69pub fn resolve(result: &mut ParseResult, options: &Options) {
72 if result.mode != Mode::Active {
73 return;
74 }
75 let metadata = std::mem::take(&mut result.metadata);
76 let includes_directives = std::mem::take(&mut result.includes);
77 let use_directives = std::mem::take(&mut result.uses);
78
79 #[cfg(feature = "wasm")]
81 let mut wasm_runtime = crate::wasm::WasmMarkerRuntime::new();
82 let packages_map = load_packages(
83 &use_directives,
84 options,
85 #[cfg(feature = "wasm")]
86 &mut wasm_runtime,
87 );
88
89 #[cfg(feature = "wasm")]
91 let wasm_options;
92 #[cfg(feature = "wasm")]
93 let options = if !wasm_runtime.marker_names().is_empty() {
94 wasm_options = Options {
95 wasm_runtime: Some(std::sync::Arc::new(wasm_runtime)),
96 ..options.clone()
97 };
98 &wasm_options
99 } else {
100 options
101 };
102
103 let includes_map = load_includes(&includes_directives, options);
105
106 if let Value::Object(ref mut root_map) = result.root {
108 for (alias, pkg_value) in &packages_map {
109 root_map.entry(alias.clone()).or_insert_with(|| pkg_value.clone());
110 }
111 }
112
113 apply_inheritance(&mut result.root, &metadata);
115 if let Value::Object(ref mut root_map) = result.root {
117 root_map.retain(|k, _| !k.starts_with('_'));
118 }
119
120 let type_registry = build_type_registry(&metadata);
122 let constraint_registry = build_constraint_registry(&metadata);
124
125 let root_ptr = &mut result.root as *mut Value;
135 resolve_value(&mut result.root, root_ptr, options, &metadata, "", &includes_map, 0);
136
137 validate_field_constraints(&mut result.root, &constraint_registry);
139
140 validate_field_types(&mut result.root, &type_registry, "");
142
143 result.metadata = metadata;
144 result.includes = includes_directives;
145}
146
147fn resolve_value(
148 value: &mut Value,
149 root_ptr: *mut Value,
150 options: &Options,
151 metadata: &HashMap<String, MetaMap>,
152 path: &str,
153 includes: &HashMap<String, Value>,
154 depth: usize,
155) {
156 if depth >= MAX_RESOLVE_DEPTH {
158 if let Value::Object(ref mut map) = value {
161 for val in map.values_mut() {
162 *val = Value::String(
163 "NESTING_ERR: maximum object nesting depth exceeded".to_string()
164 );
165 }
166 }
167 return;
168 }
169
170 let meta_map = metadata.get(path).cloned();
171
172 if let Value::Object(ref mut map) = value {
173 let keys: Vec<String> = map.keys().cloned().collect();
174
175 for key in &keys {
177 let child_path = if path.is_empty() {
178 key.clone()
179 } else {
180 format!("{}.{}", path, key)
181 };
182
183 if let Some(child) = map.get_mut(key) {
184 match child {
185 Value::Object(_) => {
186 resolve_value(child, root_ptr, options, metadata, &child_path, includes, depth + 1);
187 }
188 Value::Array(arr) => {
189 for item in arr.iter_mut() {
190 if let Value::Object(_) = item {
191 resolve_value(item, root_ptr, options, metadata, &child_path, includes, depth + 1);
192 }
193 }
194 }
195 _ => {}
196 }
197 }
198 }
199
200 if let Some(ref mm) = meta_map {
202 for key in &keys {
203 let meta = match mm.get(key) {
204 Some(m) => m.clone(),
205 None => continue,
206 };
207
208 apply_markers(map, key, &meta, root_ptr, options, path, metadata, includes);
209 }
210 }
211
212 let keys2: Vec<String> = map.keys().cloned().collect();
214 for key in &keys2 {
215 if let Some(Value::String(s)) = map.get(key) {
216 if s.contains('{') {
217 let root_ref = unsafe { &*root_ptr };
218 let result = resolve_interpolation(s, root_ref, map, includes);
219 if result != *s {
220 map.insert(key.to_string(), Value::String(result));
221 }
222 }
223 }
224 }
225 }
226}
227
228fn apply_markers(
229 map: &mut HashMap<String, Value>,
230 key: &str,
231 meta: &Meta,
232 root_ptr: *mut Value,
233 options: &Options,
234 path: &str,
235 metadata: &HashMap<String, MetaMap>,
236 _includes: &HashMap<String, Value>,
237) {
238 let markers = &meta.markers;
239
240 if markers.contains(&"spam".to_string()) {
245 let spam_idx = markers.iter().position(|m| m == "spam").unwrap();
246 let max_calls = markers
247 .get(spam_idx + 1)
248 .and_then(|s| s.parse::<usize>().ok())
249 .unwrap_or(0);
250 let window_sec = markers
251 .get(spam_idx + 2)
252 .and_then(|s| s.parse::<u64>().ok())
253 .unwrap_or(1);
254
255 if max_calls == 0 {
256 map.insert(
257 key.to_string(),
258 Value::String("SPAM_ERR: invalid limit, use :spam:MAX[:WINDOW_SEC]".to_string()),
259 );
260 return;
261 }
262
263 let target = map
264 .get(key)
265 .map(value_to_string)
266 .unwrap_or_else(|| key.to_string());
267 let bucket_key = format!("{}::{}", key, target);
268
269 if !allow_spam_access(&bucket_key, max_calls, window_sec) {
270 map.insert(
271 key.to_string(),
272 Value::String(format!(
273 "SPAM_ERR: '{}' exceeded {} calls per {}s",
274 target, max_calls, window_sec
275 )),
276 );
277 return;
278 }
279
280 if let Some(resolved) = map
281 .get(key)
282 .and_then(|v| {
283 let t = value_to_string(v);
284 let root_ref = unsafe { &*root_ptr };
285 deep_get(root_ref, &t).or_else(|| map.get(t.as_str()).cloned())
286 })
287 {
288 map.insert(key.to_string(), resolved);
289 }
290 }
291
292 if markers.contains(&"include".to_string()) || markers.contains(&"import".to_string()) {
294 if let Some(Value::String(file_path)) = map.get(key) {
295 let max_depth = options.max_include_depth.unwrap_or(DEFAULT_MAX_INCLUDE_DEPTH);
296 if options._include_depth >= max_depth {
297 map.insert(
298 key.to_string(),
299 Value::String(format!("INCLUDE_ERR: max include depth ({}) exceeded", max_depth)),
300 );
301 return;
302 }
303 let base = options
304 .base_path
305 .as_deref()
306 .unwrap_or(".");
307 let full = match jail_path(base, file_path) {
308 Ok(p) => p,
309 Err(e) => {
310 map.insert(key.to_string(), Value::String(format!("INCLUDE_ERR: {}", e)));
311 return;
312 }
313 };
314 if let Err(e) = check_file_size(&full) {
315 map.insert(key.to_string(), Value::String(format!("INCLUDE_ERR: {}", e)));
316 return;
317 }
318 match std::fs::read_to_string(&full) {
319 Ok(text) => {
320 let mut included = parser::parse(&text);
321 if included.mode == Mode::Active {
322 let mut child_opts = options.clone();
323 child_opts._include_depth += 1;
324 if let Some(parent) = full.parent() {
325 child_opts.base_path = Some(parent.to_string_lossy().into_owned());
326 }
327 resolve(&mut included, &child_opts);
328 }
329 map.insert(key.to_string(), included.root);
330 }
331 Err(e) => {
332 map.insert(
333 key.to_string(),
334 Value::String(format!("INCLUDE_ERR: {}", e)),
335 );
336 }
337 }
338 }
339 return;
340 }
341
342 if markers.contains(&"env".to_string()) {
344 if let Some(Value::String(var_name)) = map.get(key) {
345 let env_val = if let Some(ref env_map) = options.env {
346 env_map.get(var_name.as_str()).cloned()
347 } else {
348 std::env::var(var_name).ok()
349 };
350
351 let force_string = meta.type_hint.as_deref() == Some("string");
352 let default_idx = markers.iter().position(|m| m == "default");
353 if let Some(val) = env_val.filter(|v| !v.is_empty()) {
354 let resolved = if force_string {
355 Value::String(val)
356 } else {
357 cast_primitive(&val)
358 };
359 map.insert(key.to_string(), resolved);
360 } else if let Some(di) = default_idx {
361 if markers.len() > di + 1 {
362 let fallback = markers[di + 1..].join(":");
365 let resolved = if force_string {
366 Value::String(fallback)
367 } else {
368 cast_primitive(&fallback)
369 };
370 map.insert(key.to_string(), resolved);
371 } else {
372 map.insert(key.to_string(), Value::Null);
373 }
374 } else {
375 map.insert(key.to_string(), Value::Null);
376 }
377 }
378 }
379
380 if markers.contains(&"random".to_string()) {
382 if let Some(Value::Array(arr)) = map.get(key) {
383 if arr.is_empty() {
384 map.insert(key.to_string(), Value::Null);
385 return;
386 }
387 let picked = if !meta.args.is_empty() {
388 let weights: Vec<f64> = meta.args.iter().filter_map(|s| s.parse().ok()).collect();
389 weighted_random(arr, &weights)
390 } else {
391 arr[rng::random_usize(arr.len())].clone()
392 };
393 map.insert(key.to_string(), picked);
394 }
395 }
396
397 if markers.contains(&"ref".to_string()) {
401 if let Some(Value::String(target)) = map.get(key) {
402 let root_ref = unsafe { &*root_ptr };
403 let resolved = deep_get(root_ref, target)
404 .or_else(|| map.get(target.as_str()).cloned())
405 .unwrap_or(Value::Null);
406
407 if markers.contains(&"calc".to_string()) {
409 if let Some(n) = value_as_number(&resolved) {
410 let calc_idx = markers.iter().position(|m| m == "calc").unwrap();
411 if let Some(calc_expr) = markers.get(calc_idx + 1) {
412 let first = calc_expr.chars().next().unwrap_or(' ');
413 if "+-*/%".contains(first) {
414 let expr = format!("{} {}", format_number(n), calc_expr);
415 match safe_calc(&expr) {
416 Ok(result) => {
417 let v = if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
418 Value::Int(result as i64)
419 } else {
420 Value::Float(result)
421 };
422 map.insert(key.to_string(), v);
423 }
424 Err(e) => {
425 map.insert(key.to_string(), Value::String(format!("CALC_ERR: {}", e)));
426 }
427 }
428 } else {
429 map.insert(key.to_string(), resolved);
430 }
431 } else {
432 map.insert(key.to_string(), resolved);
433 }
434 } else {
435 map.insert(key.to_string(), resolved);
436 }
437 } else {
438 map.insert(key.to_string(), resolved);
439 }
440 }
441 }
442
443 if markers.contains(&"i18n".to_string()) {
457 if let Some(Value::Object(translations)) = map.get(key) {
458 let lang = options.lang.as_deref().unwrap_or("en");
459 let val = translations.get(lang)
460 .or_else(|| translations.get("en"))
461 .or_else(|| translations.values().next())
462 .cloned()
463 .unwrap_or(Value::Null);
464
465 let i18n_idx = markers.iter().position(|m| m == "i18n").unwrap();
467 let count_field = markers.get(i18n_idx + 1).cloned();
468
469 if let (Some(ref cf), Value::Object(ref plural_forms)) = (&count_field, &val) {
470 let count_val = map.get(cf)
472 .and_then(value_as_number)
473 .or_else(|| {
474 let root_ref = unsafe { &*root_ptr };
475 deep_get(root_ref, cf).and_then(|v| value_as_number(&v))
476 })
477 .unwrap_or(0.0) as i64;
478
479 let category = plural_category(lang, count_val);
480 let chosen = plural_forms.get(category)
481 .or_else(|| plural_forms.get("other"))
482 .or_else(|| plural_forms.values().next())
483 .cloned()
484 .unwrap_or(Value::Null);
485
486 if let Value::String(ref s) = chosen {
488 let replaced = s.replace("{count}", &count_val.to_string());
489 map.insert(key.to_string(), Value::String(replaced));
490 } else {
491 map.insert(key.to_string(), chosen);
492 }
493 } else {
494 map.insert(key.to_string(), val);
495 }
496 }
497 }
498
499 if markers.contains(&"calc".to_string()) {
501 if let Some(Value::String(expr)) = map.get(key) {
502 if expr.len() > MAX_CALC_EXPR_LEN {
503 map.insert(
504 key.to_string(),
505 Value::String(format!("CALC_ERR: expression too long ({} chars, max {})", expr.len(), MAX_CALC_EXPR_LEN)),
506 );
507 return;
508 }
509 let mut resolved = expr.clone();
510
511 let root_ref = unsafe { &*root_ptr };
513 if let Value::Object(ref root_map) = root_ref {
514 for (rk, rv) in root_map {
515 if let Some(n) = value_as_number(rv) {
516 resolved = replace_word(&resolved, rk, &format_number(n));
517 if resolved.len() > MAX_CALC_RESOLVED_LEN {
518 map.insert(
519 key.to_string(),
520 Value::String(format!(
521 "CALC_ERR: resolved expression too long (max {} bytes)",
522 MAX_CALC_RESOLVED_LEN
523 )),
524 );
525 return;
526 }
527 }
528 }
529 }
530
531 for (rk, rv) in map.iter() {
533 if rk != key {
534 if let Some(n) = value_as_number(rv) {
535 resolved = replace_word(&resolved, rk, &format_number(n));
536 if resolved.len() > MAX_CALC_RESOLVED_LEN {
537 map.insert(
538 key.to_string(),
539 Value::String(format!(
540 "CALC_ERR: resolved expression too long (max {} bytes)",
541 MAX_CALC_RESOLVED_LEN
542 )),
543 );
544 return;
545 }
546 }
547 }
548 }
549
550 let root_ref2 = unsafe { &*root_ptr };
552 let mut dot_resolved = String::new();
553 let bytes = resolved.as_bytes();
554 let len = bytes.len();
555 let mut i = 0;
556 while i < len {
557 if is_word_char(bytes[i]) {
558 let start = i;
559 let mut has_dot = false;
560 while i < len && (is_word_char(bytes[i]) || bytes[i] == b'.') {
561 if bytes[i] == b'.' { has_dot = true; }
562 i += 1;
563 }
564 let token = &resolved[start..i];
565 if has_dot && token.contains('.') {
566 if let Some(val) = deep_get(root_ref2, token) {
567 if let Some(n) = value_as_number(&val) {
568 dot_resolved.push_str(&format_number(n));
569 if dot_resolved.len() > MAX_CALC_RESOLVED_LEN {
570 map.insert(
571 key.to_string(),
572 Value::String(format!(
573 "CALC_ERR: resolved expression too long (max {} bytes)",
574 MAX_CALC_RESOLVED_LEN
575 )),
576 );
577 return;
578 }
579 continue;
580 }
581 }
582 }
583 dot_resolved.push_str(token);
584 if dot_resolved.len() > MAX_CALC_RESOLVED_LEN {
585 map.insert(
586 key.to_string(),
587 Value::String(format!(
588 "CALC_ERR: resolved expression too long (max {} bytes)",
589 MAX_CALC_RESOLVED_LEN
590 )),
591 );
592 return;
593 }
594 } else {
595 dot_resolved.push(bytes[i] as char);
596 i += 1;
597 if dot_resolved.len() > MAX_CALC_RESOLVED_LEN {
598 map.insert(
599 key.to_string(),
600 Value::String(format!(
601 "CALC_ERR: resolved expression too long (max {} bytes)",
602 MAX_CALC_RESOLVED_LEN
603 )),
604 );
605 return;
606 }
607 }
608 }
609 resolved = dot_resolved;
610
611 match safe_calc(&resolved) {
612 Ok(result) => {
613 let v = if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
614 Value::Int(result as i64)
615 } else {
616 Value::Float(result)
617 };
618 map.insert(key.to_string(), v);
619 }
620 Err(e) => {
621 map.insert(
622 key.to_string(),
623 Value::String(format!("CALC_ERR: {}", e)),
624 );
625 }
626 }
627 }
628 }
629
630 if markers.contains(&"alias".to_string()) {
632 if let Some(Value::String(target)) = map.get(key) {
633 let target = target.clone();
634 let current_path = if path.is_empty() {
636 key.to_string()
637 } else {
638 format!("{}.{}", path, key)
639 };
640 if target == key || target == current_path {
642 map.insert(
643 key.to_string(),
644 Value::String(format!("ALIAS_ERR: self-referential alias: {} → {}", current_path, target)),
645 );
646 } else {
647 let root_ref = unsafe { &*root_ptr };
652 let target_val = deep_get(root_ref, &target);
653 let (target_parent, target_key_name) = if let Some(dot) = target.rfind('.') {
655 (target[..dot].to_string(), target[dot + 1..].to_string())
656 } else {
657 (String::new(), target.clone())
658 };
659 let target_has_alias = metadata
660 .get(&target_parent)
661 .and_then(|mm| mm.get(&target_key_name))
662 .map(|m| m.markers.contains(&"alias".to_string()))
663 .unwrap_or(false);
664 let is_cycle = target_has_alias && match &target_val {
665 Some(Value::String(s)) => s == key || s == ¤t_path,
666 _ => false,
667 };
668 if is_cycle {
669 map.insert(
670 key.to_string(),
671 Value::String(format!("ALIAS_ERR: circular alias detected: {} → {}", current_path, target)),
672 );
673 } else {
674 let val = target_val.unwrap_or(Value::Null);
675 map.insert(key.to_string(), val);
676 }
677 }
678 }
679 }
680
681 if markers.contains(&"secret".to_string()) {
683 if let Some(val) = map.get(key) {
684 let s = value_to_string(val);
685 map.insert(key.to_string(), Value::Secret(s));
686 }
687 }
688
689 if markers.contains(&"unique".to_string()) {
691 if let Some(Value::Array(arr)) = map.get(key) {
692 let mut seen = Vec::new();
693 let mut unique = Vec::new();
694 for item in arr {
695 let s = value_to_string(item);
696 if !seen.contains(&s) {
697 seen.push(s);
698 unique.push(item.clone());
699 }
700 }
701 map.insert(key.to_string(), Value::Array(unique));
702 }
703 }
704
705 if markers.contains(&"geo".to_string()) {
707 if let Some(Value::Array(arr)) = map.get(key) {
708 let region = options.region.as_deref().unwrap_or("US");
709 let prefix = format!("{} ", region);
710 let found = arr.iter().find(|item| {
711 if let Value::String(s) = item {
712 s.starts_with(&prefix)
713 } else {
714 false
715 }
716 });
717
718 let result = if let Some(Value::String(s)) = found {
719 Value::String(s[prefix.len()..].trim().to_string())
720 } else if let Some(first) = arr.first() {
721 if let Value::String(s) = first {
722 if let Some(space) = s.find(' ') {
723 Value::String(s[space + 1..].trim().to_string())
724 } else {
725 first.clone()
726 }
727 } else {
728 first.clone()
729 }
730 } else {
731 Value::Null
732 };
733 map.insert(key.to_string(), result);
734 }
735 }
736
737 if markers.contains(&"split".to_string()) {
741 if let Some(Value::String(s)) = map.get(key) {
742 let split_idx = markers.iter().position(|m| m == "split").unwrap();
743 let sep = if split_idx + 1 < markers.len() {
744 delimiter_from_keyword(&markers[split_idx + 1])
745 } else {
746 ",".to_string()
747 };
748 let items: Vec<Value> = s
749 .split(&sep)
750 .map(|p| p.trim())
751 .filter(|p| !p.is_empty())
752 .map(|p| cast_primitive(p))
753 .collect();
754 map.insert(key.to_string(), Value::Array(items));
755 }
756 }
757
758 if markers.contains(&"join".to_string()) {
760 if let Some(Value::Array(arr)) = map.get(key) {
761 let join_idx = markers.iter().position(|m| m == "join").unwrap();
762 let sep = if join_idx + 1 < markers.len() {
763 delimiter_from_keyword(&markers[join_idx + 1])
764 } else {
765 ",".to_string()
766 };
767 let joined: String = arr
768 .iter()
769 .map(|v| value_to_string(v))
770 .collect::<Vec<_>>()
771 .join(&sep);
772 map.insert(key.to_string(), Value::String(joined));
773 }
774 }
775
776 if markers.contains(&"default".to_string()) && !markers.contains(&"env".to_string()) {
778 let is_empty = match map.get(key) {
779 Some(Value::Null) | None => true,
780 Some(Value::String(s)) if s.is_empty() => true,
781 _ => false,
782 };
783 if is_empty {
784 let di = markers.iter().position(|m| m == "default").unwrap();
785 if markers.len() > di + 1 {
786 let fallback = markers[di + 1..].join(":");
787 let resolved = if meta.type_hint.as_deref() == Some("string") {
788 Value::String(fallback)
789 } else {
790 cast_primitive(&fallback)
791 };
792 map.insert(key.to_string(), resolved);
793 }
794 }
795 }
796
797 if markers.contains(&"clamp".to_string()) {
801 let clamp_idx = markers.iter().position(|m| m == "clamp").unwrap();
802 let min_s = markers.get(clamp_idx + 1).cloned().unwrap_or_default();
803 let max_s = markers.get(clamp_idx + 2).cloned().unwrap_or_default();
804 if let (Ok(lo), Ok(hi)) = (min_s.parse::<f64>(), max_s.parse::<f64>()) {
805 if lo > hi {
806 map.insert(key.to_string(), Value::String(
807 format!("CONSTRAINT_ERR: clamp min ({}) > max ({})", lo, hi),
808 ));
809 } else if let Some(n) = map.get(key).and_then(value_as_number) {
810 let clamped = n.clamp(lo, hi);
811 let v = if clamped.fract() == 0.0 && clamped.abs() < i64::MAX as f64 {
812 Value::Int(clamped as i64)
813 } else {
814 Value::Float(clamped)
815 };
816 map.insert(key.to_string(), v);
817 }
818 }
819 }
820
821 if markers.contains(&"round".to_string()) {
825 let round_idx = markers.iter().position(|m| m == "round").unwrap();
826 let decimals: u32 = markers.get(round_idx + 1)
827 .and_then(|s| s.parse().ok())
828 .unwrap_or(0);
829 if let Some(n) = map.get(key).and_then(value_as_number) {
830 let factor = 10f64.powi(decimals as i32);
831 let rounded = (n * factor).round() / factor;
832 let v = if decimals == 0 {
833 Value::Int(rounded as i64)
834 } else {
835 Value::Float(rounded)
836 };
837 map.insert(key.to_string(), v);
838 }
839 }
840
841 if markers.contains(&"map".to_string()) {
845 if let Some(Value::Array(arr)) = map.get(key) {
846 let map_idx = markers.iter().position(|m| m == "map").unwrap();
847 let source_key = markers.get(map_idx + 1).cloned().unwrap_or_default();
848 let lookup_val = if !source_key.is_empty() {
849 let root_ref = unsafe { &*root_ptr };
850 deep_get(root_ref, &source_key)
851 .or_else(|| map.get(&source_key).cloned())
852 .map(|v| value_to_string(&v))
853 .unwrap_or_default()
854 } else {
855 match map.get(key) {
857 Some(Value::String(s)) => s.clone(),
858 _ => String::new(),
859 }
860 };
861
862 let arr_clone = arr.clone();
864 let result = arr_clone.iter().find_map(|item| {
865 if let Value::String(s) = item {
866 if let Some(space) = s.find(' ') {
867 if s[..space].trim() == lookup_val {
868 return Some(cast_primitive(s[space + 1..].trim()));
869 }
870 }
871 }
872 None
873 });
874 map.insert(key.to_string(), result.unwrap_or(Value::Null));
875 }
876 }
877
878 if markers.contains(&"format".to_string()) {
882 let fmt_idx = markers.iter().position(|m| m == "format").unwrap();
883 let pattern = markers.get(fmt_idx + 1).cloned().unwrap_or_else(|| "%s".to_string());
884 if let Some(current) = map.get(key) {
885 let formatted = apply_format_pattern(&pattern, current);
886 map.insert(key.to_string(), Value::String(formatted));
887 }
888 }
889
890 if markers.contains(&"fallback".to_string()) {
895 let fb_idx = markers.iter().position(|m| m == "fallback").unwrap();
896 let default_val = markers.get(fb_idx + 1).cloned().unwrap_or_default();
897 let use_fallback = match map.get(key) {
898 None | Some(Value::Null) => true,
899 Some(Value::String(s)) if s.is_empty() => true,
900 Some(Value::String(s)) => {
901 let base = options.base_path.as_deref().unwrap_or(".");
902 match jail_path(base, s) {
903 Ok(safe) => !safe.exists(),
904 Err(_) => true, }
906 }
907 _ => false,
908 };
909 if use_fallback && !default_val.is_empty() {
910 map.insert(key.to_string(), Value::String(default_val));
911 }
912 }
913
914 if markers.contains(&"once".to_string()) {
918 let once_idx = markers.iter().position(|m| m == "once").unwrap();
919 let gen_type = markers.get(once_idx + 1).map(|s| s.as_str()).unwrap_or("uuid");
920 let lock_path = options.base_path.as_deref()
921 .map(|b| std::path::Path::new(b).join(".synx.lock"))
922 .unwrap_or_else(|| std::path::Path::new(".synx.lock").to_path_buf());
923
924 let existing = read_lock_value(&lock_path, key);
926 if let Some(locked) = existing {
927 map.insert(key.to_string(), Value::String(locked));
928 } else {
929 let generated = match gen_type {
930 "uuid" => rng::generate_uuid(),
931 "timestamp" => std::time::SystemTime::now()
932 .duration_since(std::time::UNIX_EPOCH)
933 .unwrap_or_default()
934 .as_secs()
935 .to_string(),
936 "random" => rng::random_usize(u32::MAX as usize).to_string(),
937 _ => rng::generate_uuid(),
938 };
939 write_lock_value(&lock_path, key, &generated);
940 map.insert(key.to_string(), Value::String(generated));
941 }
942 }
943
944 if markers.contains(&"version".to_string()) {
949 if let Some(Value::String(current_ver)) = map.get(key) {
950 let ver_idx = markers.iter().position(|m| m == "version").unwrap();
951 let op = markers.get(ver_idx + 1).map(|s| s.as_str()).unwrap_or(">=");
952 let required = markers.get(ver_idx + 2).cloned().unwrap_or_default();
953 let result = compare_versions(current_ver, op, &required);
954 map.insert(key.to_string(), Value::Bool(result));
955 }
956 }
957
958 if markers.contains(&"watch".to_string()) {
962 if let Some(Value::String(file_path)) = map.get(key) {
963 let max_depth = options.max_include_depth.unwrap_or(DEFAULT_MAX_INCLUDE_DEPTH);
964 if options._include_depth >= max_depth {
965 map.insert(
966 key.to_string(),
967 Value::String(format!("WATCH_ERR: max include depth ({}) exceeded", max_depth)),
968 );
969 return;
970 }
971 let base = options.base_path.as_deref().unwrap_or(".");
972 let full = match jail_path(base, file_path) {
973 Ok(p) => p,
974 Err(e) => {
975 map.insert(key.to_string(), Value::String(format!("WATCH_ERR: {}", e)));
976 return;
977 }
978 };
979 if let Err(e) = check_file_size(&full) {
980 map.insert(key.to_string(), Value::String(format!("WATCH_ERR: {}", e)));
981 return;
982 }
983 let watch_idx = markers.iter().position(|m| m == "watch").unwrap();
984 let key_path = markers.get(watch_idx + 1).cloned();
985
986 match std::fs::read_to_string(&full) {
987 Ok(content) => {
988 let value = if let Some(ref kp) = key_path {
989 extract_from_file_content(&content, kp, full.extension().and_then(|e| e.to_str()).unwrap_or("")).unwrap_or(Value::Null)
990 } else {
991 Value::String(content.trim().to_string())
992 };
993 map.insert(key.to_string(), value);
994 }
995 Err(e) => {
996 map.insert(key.to_string(), Value::String(format!("WATCH_ERR: {}", e)));
997 }
998 }
999 }
1000 }
1001
1002 if markers.contains(&"prompt".to_string()) {
1007 let prompt_idx = markers.iter().position(|m| m == "prompt").unwrap();
1008 let label = markers.get(prompt_idx + 1).cloned().unwrap_or_else(|| key.to_string());
1009 if let Some(val) = map.get(key) {
1010 let synx_text = stringify_value(val, 0);
1011 let block = format!("{} (SYNX):\n```synx\n{}```", label, synx_text);
1012 map.insert(key.to_string(), Value::String(block));
1013 }
1014 }
1015
1016 #[cfg(feature = "wasm")]
1027 if let Some(ref wasm_rt) = options.wasm_runtime {
1028 for marker in markers {
1029 if crate::wasm::BUILTIN_MARKERS.contains(&marker.as_str()) {
1030 continue;
1031 }
1032 if wasm_rt.has_marker(marker) {
1033 let marker_idx = markers.iter().position(|m| m == marker).unwrap();
1035 let args: Vec<String> = markers[marker_idx + 1..].to_vec();
1036 let current_value = map.get(key).cloned().unwrap_or(Value::Null);
1037 match wasm_rt.apply_marker(marker, ¤t_value, &args) {
1038 Ok(result) => {
1039 map.insert(key.to_string(), result);
1040 }
1041 Err(e) => {
1042 map.insert(key.to_string(), Value::String(format!("WASM_ERR: {}", e)));
1043 }
1044 }
1045 break; }
1047 }
1048 }
1049
1050 if let Some(ref c) = meta.constraints {
1052 validate_constraints(map, key, c);
1053 }
1054}
1055
1056fn validate_constraints(map: &mut HashMap<String, Value>, key: &str, c: &Constraints) {
1059 let val = match map.get(key) {
1060 Some(v) => v.clone(),
1061 None => {
1062 if c.required {
1063 map.insert(key.to_string(), Value::String(
1064 format!("CONSTRAINT_ERR: '{}' is required", key),
1065 ));
1066 }
1067 return;
1068 }
1069 };
1070
1071 if c.required {
1073 let empty = matches!(val, Value::Null)
1074 || matches!(&val, Value::String(s) if s.is_empty());
1075 if empty {
1076 map.insert(key.to_string(), Value::String(
1077 format!("CONSTRAINT_ERR: '{}' is required", key),
1078 ));
1079 return;
1080 }
1081 }
1082
1083 if let Some(ref type_name) = c.type_name {
1085 let ok = match type_name.as_str() {
1086 "int" => matches!(val, Value::Int(_)),
1087 "float" => matches!(val, Value::Float(_) | Value::Int(_)),
1088 "bool" => matches!(val, Value::Bool(_)),
1089 "string" => matches!(val, Value::String(_)),
1090 _ => true,
1091 };
1092 if !ok {
1093 map.insert(key.to_string(), Value::String(
1094 format!("CONSTRAINT_ERR: '{}' expected type '{}'", key, type_name),
1095 ));
1096 return;
1097 }
1098 }
1099
1100 if let Some(ref enum_vals) = c.enum_values {
1102 let val_str = match &val {
1103 Value::String(s) => s.clone(),
1104 Value::Int(n) => n.to_string(),
1105 Value::Float(f) => f.to_string(),
1106 Value::Bool(b) => b.to_string(),
1107 _ => String::new(),
1108 };
1109 if !enum_vals.contains(&val_str) {
1110 map.insert(key.to_string(), Value::String(
1111 format!("CONSTRAINT_ERR: '{}' must be one of [{}]", key, enum_vals.join("|")),
1112 ));
1113 return;
1114 }
1115 }
1116
1117 let num = match &val {
1119 Value::Int(n) => Some(*n as f64),
1120 Value::Float(f) => Some(*f),
1121 Value::String(s) if c.min.is_some() || c.max.is_some() => Some(s.len() as f64),
1122 _ => None,
1123 };
1124 if let Some(n) = num {
1125 if let Some(min) = c.min {
1126 if n < min {
1127 map.insert(key.to_string(), Value::String(
1128 format!("CONSTRAINT_ERR: '{}' value {} is below min {}", key, n, min),
1129 ));
1130 return;
1131 }
1132 }
1133 if let Some(max) = c.max {
1134 if n > max {
1135 map.insert(key.to_string(), Value::String(
1136 format!("CONSTRAINT_ERR: '{}' value {} exceeds max {}", key, n, max),
1137 ));
1138 return;
1139 }
1140 }
1141 }
1142 }
1146
1147fn apply_format_pattern(pattern: &str, value: &Value) -> String {
1151 match value {
1152 Value::Int(n) => {
1153 if pattern.contains('d') || pattern.contains('i') {
1154 format_int_pattern(pattern, *n)
1155 } else if pattern.contains('f') || pattern.contains('e') {
1156 format_float_pattern(pattern, *n as f64)
1157 } else {
1158 n.to_string()
1159 }
1160 }
1161 Value::Float(f) => {
1162 if pattern.contains('f') || pattern.contains('e') {
1163 format_float_pattern(pattern, *f)
1164 } else {
1165 format_number(*f)
1166 }
1167 }
1168 Value::String(s) => s.clone(),
1169 other => value_to_string(other),
1170 }
1171}
1172
1173fn format_int_pattern(pattern: &str, n: i64) -> String {
1174 const MAX_FMT_WIDTH: usize = 4096;
1177 if let Some(s) = pattern.strip_prefix('%') {
1178 if let Some(inner) = s.strip_suffix('d').or_else(|| s.strip_suffix('i')) {
1179 if let Some(w) = inner.strip_prefix('0') {
1180 if let Ok(width) = w.parse::<usize>() {
1181 let width = width.min(MAX_FMT_WIDTH);
1182 return format!("{:0>width$}", n, width = width);
1183 }
1184 }
1185 if let Ok(width) = inner.parse::<usize>() {
1186 let width = width.min(MAX_FMT_WIDTH);
1187 return format!("{:>width$}", n, width = width);
1188 }
1189 }
1190 }
1191 n.to_string()
1192}
1193
1194fn format_float_pattern(pattern: &str, f: f64) -> String {
1195 const MAX_FMT_PREC: usize = 1024;
1197 if let Some(s) = pattern.strip_prefix('%') {
1198 if let Some(inner) = s.strip_suffix('f').or_else(|| s.strip_suffix('e')) {
1199 if let Some(prec_s) = inner.strip_prefix('.') {
1200 if let Ok(prec) = prec_s.parse::<usize>() {
1201 let prec = prec.min(MAX_FMT_PREC);
1202 return format!("{:.prec$}", f, prec = prec);
1203 }
1204 }
1205 }
1206 }
1207 f.to_string()
1208}
1209
1210fn read_lock_value(lock_path: &std::path::Path, key: &str) -> Option<String> {
1212 let content = std::fs::read_to_string(lock_path).ok()?;
1213 for line in content.lines() {
1214 if let Some(rest) = line.strip_prefix(key) {
1215 if rest.starts_with(' ') {
1216 return Some(rest.trim_start().to_string());
1217 }
1218 }
1219 }
1220 None
1221}
1222
1223fn write_lock_value(lock_path: &std::path::Path, key: &str, value: &str) {
1225 let mut lines: Vec<String> = std::fs::read_to_string(lock_path)
1226 .unwrap_or_default()
1227 .lines()
1228 .map(|l| l.to_string())
1229 .collect();
1230
1231 let new_line = format!("{} {}", key, value);
1232 let mut found = false;
1233 for line in lines.iter_mut() {
1234 if line.starts_with(key) && line[key.len()..].starts_with(' ') {
1235 *line = new_line.clone();
1236 found = true;
1237 break;
1238 }
1239 }
1240 if !found {
1241 lines.push(new_line);
1242 }
1243 let _ = std::fs::write(lock_path, lines.join("\n") + "\n");
1244}
1245
1246fn compare_versions(current: &str, op: &str, required: &str) -> bool {
1248 let parse_ver = |s: &str| -> Vec<u64> {
1249 s.split('.').filter_map(|p| p.parse().ok()).collect()
1250 };
1251 let cv = parse_ver(current);
1252 let rv = parse_ver(required);
1253 let len = cv.len().max(rv.len());
1254 let mut ord = std::cmp::Ordering::Equal;
1255 for i in 0..len {
1256 let a = cv.get(i).copied().unwrap_or(0);
1257 let b = rv.get(i).copied().unwrap_or(0);
1258 if a != b {
1259 ord = a.cmp(&b);
1260 break;
1261 }
1262 }
1263 match op {
1264 ">=" => ord != std::cmp::Ordering::Less,
1265 "<=" => ord != std::cmp::Ordering::Greater,
1266 ">" => ord == std::cmp::Ordering::Greater,
1267 "<" => ord == std::cmp::Ordering::Less,
1268 "==" | "=" => ord == std::cmp::Ordering::Equal,
1269 "!=" => ord != std::cmp::Ordering::Equal,
1270 _ => false,
1271 }
1272}
1273
1274fn allow_spam_access(bucket_key: &str, max_calls: usize, window_sec: u64) -> bool {
1275 let now = Instant::now();
1276 let window = Duration::from_secs(window_sec.max(1));
1277
1278 let buckets = SPAM_BUCKETS.get_or_init(|| Mutex::new(HashMap::new()));
1279 let mut guard = match buckets.lock() {
1280 Ok(g) => g,
1281 Err(poisoned) => poisoned.into_inner(),
1282 };
1283
1284 let calls = guard.entry(bucket_key.to_string()).or_default();
1285 calls.retain(|ts| now.duration_since(*ts) <= window);
1286
1287 if calls.len() >= max_calls {
1288 return false;
1289 }
1290
1291 calls.push(now);
1292 true
1293}
1294
1295#[cfg(test)]
1296fn clear_spam_buckets() {
1297 let buckets = SPAM_BUCKETS.get_or_init(|| Mutex::new(HashMap::new()));
1298 if let Ok(mut guard) = buckets.lock() {
1299 guard.clear();
1300 }
1301}
1302
1303fn extract_from_file_content(content: &str, key_path: &str, ext: &str) -> Option<Value> {
1305 if ext == "json" {
1306 let search = format!("\"{}\"", key_path);
1307 if let Some(pos) = content.find(&search) {
1308 let after = content[pos + search.len()..].trim_start();
1309 if let Some(rest) = after.strip_prefix(':') {
1310 let val_s = rest.trim_start()
1311 .trim_end_matches(',')
1312 .trim_end_matches('}')
1313 .trim()
1314 .trim_matches('"');
1315 return Some(cast_primitive(val_s));
1316 }
1317 }
1318 None
1319 } else {
1320 for line in content.lines() {
1321 let trimmed = line.trim_start();
1322 if trimmed.starts_with(key_path) {
1323 let rest = &trimmed[key_path.len()..];
1324 if rest.starts_with(' ') {
1325 return Some(cast_primitive(rest.trim_start()));
1326 }
1327 }
1328 }
1329 None
1330 }
1331}
1332
1333fn stringify_value(value: &Value, indent: usize) -> String {
1337 let spaces = " ".repeat(indent);
1338 match value {
1339 Value::Object(map) => {
1340 let mut out = String::new();
1341 let mut keys: Vec<&str> = map.keys().map(|k| k.as_str()).collect();
1342 keys.sort_unstable();
1343 for key in keys {
1344 let val = &map[key];
1345 match val {
1346 Value::Object(_) => {
1347 out.push_str(&format!("{}{}\n", spaces, key));
1348 out.push_str(&stringify_value(val, indent + 2));
1349 }
1350 Value::Array(arr) => {
1351 out.push_str(&format!("{}{}\n", spaces, key));
1352 for item in arr {
1353 out.push_str(&format!("{} - {}\n", spaces, value_to_string(item)));
1354 }
1355 }
1356 _ => {
1357 out.push_str(&format!("{}{} {}\n", spaces, key, value_to_string(val)));
1358 }
1359 }
1360 }
1361 out
1362 }
1363 _ => format!("{}{}\n", spaces, value_to_string(value)),
1364 }
1365}
1366
1367pub(crate) fn cast_primitive(val: &str) -> Value {
1368 if val.len() >= 2 {
1370 let bytes = val.as_bytes();
1371 if (bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"')
1372 || (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\'')
1373 {
1374 return Value::String(val[1..val.len() - 1].to_string());
1375 }
1376 }
1377 match val {
1378 "true" => Value::Bool(true),
1379 "false" => Value::Bool(false),
1380 "null" => Value::Null,
1381 _ => {
1382 if let Ok(i) = val.parse::<i64>() {
1383 Value::Int(i)
1384 } else if let Ok(f) = val.parse::<f64>() {
1385 Value::Float(f)
1386 } else {
1387 Value::String(val.to_string())
1388 }
1389 }
1390 }
1391}
1392
1393fn delimiter_from_keyword(keyword: &str) -> String {
1394 match keyword {
1395 "space" => " ".to_string(),
1396 "pipe" => "|".to_string(),
1397 "dash" => "-".to_string(),
1398 "dot" => ".".to_string(),
1399 "semi" => ";".to_string(),
1400 "tab" => "\t".to_string(),
1401 "slash" => "/".to_string(),
1402 other => other.to_string(),
1403 }
1404}
1405
1406fn value_as_number(v: &Value) -> Option<f64> {
1407 match v {
1408 Value::Int(n) => Some(*n as f64),
1409 Value::Float(f) => Some(*f),
1410 _ => None,
1411 }
1412}
1413
1414fn value_to_string(v: &Value) -> String {
1415 match v {
1416 Value::String(s) => s.clone(),
1417 Value::Int(n) => n.to_string(),
1418 Value::Float(f) => format_number(*f as f64),
1419 Value::Bool(b) => b.to_string(),
1420 Value::Null => "null".to_string(),
1421 Value::Secret(s) => s.clone(),
1422 Value::Array(_) | Value::Object(_) => String::new(),
1423 }
1424}
1425
1426fn format_number(n: f64) -> String {
1427 if n.fract() == 0.0 && n.abs() < i64::MAX as f64 {
1428 (n as i64).to_string()
1429 } else {
1430 n.to_string()
1431 }
1432}
1433
1434fn replace_word(haystack: &str, word: &str, replacement: &str) -> String {
1436 let word_bytes = word.as_bytes();
1437 let word_len = word_bytes.len();
1438 let hay_bytes = haystack.as_bytes();
1439 let hay_len = hay_bytes.len();
1440
1441 if word_len > hay_len {
1442 return haystack.to_string();
1443 }
1444
1445 let mut result = String::with_capacity(hay_len.min(MAX_ENGINE_SCRATCH_STRING));
1446 let mut i = 0;
1447
1448 while i <= hay_len - word_len {
1449 if result.len() >= MAX_ENGINE_SCRATCH_STRING {
1450 break;
1451 }
1452 if &hay_bytes[i..i + word_len] == word_bytes {
1453 let before_ok = i == 0 || !is_word_char(hay_bytes[i - 1]);
1454 let after_ok = i + word_len >= hay_len || !is_word_char(hay_bytes[i + word_len]);
1455 if before_ok && after_ok {
1456 let room = MAX_ENGINE_SCRATCH_STRING.saturating_sub(result.len());
1457 if room > 0 {
1458 let take = replacement.len().min(room);
1459 let end = replacement.floor_char_boundary(take);
1460 result.push_str(&replacement[..end]);
1461 }
1462 i += word_len;
1463 continue;
1464 }
1465 }
1466 if result.len() < MAX_ENGINE_SCRATCH_STRING {
1467 result.push(hay_bytes[i] as char);
1468 }
1469 i += 1;
1470 }
1471 while i < hay_len && result.len() < MAX_ENGINE_SCRATCH_STRING {
1472 result.push(hay_bytes[i] as char);
1473 i += 1;
1474 }
1475 result
1476}
1477
1478fn is_word_char(b: u8) -> bool {
1479 b.is_ascii_alphanumeric() || b == b'_'
1480}
1481
1482fn weighted_random(items: &[Value], weights: &[f64]) -> Value {
1483 let mut w: Vec<f64> = weights.to_vec();
1484 if w.len() < items.len() {
1485 let assigned: f64 = w.iter().sum();
1486 let per_item = if assigned < 100.0 {
1490 (100.0 - assigned) / (items.len() - w.len()) as f64
1491 } else {
1492 assigned / w.len() as f64
1493 };
1494 while w.len() < items.len() {
1495 w.push(per_item);
1496 }
1497 }
1498 let total: f64 = w.iter().sum();
1499 if total <= 0.0 {
1500 return items[rng::random_usize(items.len())].clone();
1501 }
1502
1503 let rand_val = rng::random_f64_01();
1504 let mut cumulative = 0.0;
1505 for (i, item) in items.iter().enumerate() {
1506 cumulative += w[i] / total;
1507 if rand_val <= cumulative {
1508 return item.clone();
1509 }
1510 }
1511 items.last().cloned().unwrap_or(Value::Null)
1512}
1513
1514fn apply_inheritance(root: &mut Value, metadata: &HashMap<String, MetaMap>) {
1517 let root_meta = match metadata.get("") {
1518 Some(m) => m.clone(),
1519 None => return,
1520 };
1521
1522 let root_map = match root.as_object_mut() {
1523 Some(m) => m as *mut HashMap<String, Value>,
1524 None => return,
1525 };
1526
1527 let mut inherits: Vec<(String, Vec<String>)> = Vec::new();
1529 for (key, meta) in &root_meta {
1530 if meta.markers.contains(&"inherit".to_string()) {
1531 let idx = meta.markers.iter().position(|m| m == "inherit").unwrap();
1532 let parents: Vec<String> = meta.markers[idx + 1..].to_vec();
1534 if !parents.is_empty() {
1535 inherits.push((key.clone(), parents));
1536 }
1537 }
1538 }
1539
1540 let map = unsafe { &mut *root_map };
1541 for (child_key, parents) in &inherits {
1542 let mut merged: HashMap<String, Value> = HashMap::new();
1544 for parent_name in parents {
1545 if let Some(Value::Object(p)) = map.get(parent_name) {
1546 for (k, v) in p {
1547 merged.insert(k.clone(), v.clone());
1548 }
1549 }
1550 }
1551 if let Some(Value::Object(c)) = map.get(child_key) {
1553 for (k, v) in c {
1554 merged.insert(k.clone(), v.clone());
1555 }
1556 }
1557 map.insert(child_key.clone(), Value::Object(merged));
1558 }
1559}
1560
1561fn deep_get(root: &Value, path: &str) -> Option<Value> {
1562 if let Value::Object(map) = root {
1564 if let Some(val) = map.get(path) {
1565 return Some(val.clone());
1566 }
1567 }
1568 let parts: Vec<&str> = path.split('.').collect();
1570 let mut current = root;
1571 for part in parts {
1572 match current {
1573 Value::Object(map) => match map.get(part) {
1574 Some(v) => current = v,
1575 None => return None,
1576 },
1577 _ => return None,
1578 }
1579 }
1580 Some(current.clone())
1581}
1582
1583fn resolve_interpolation(
1586 tpl: &str,
1587 root: &Value,
1588 local_map: &HashMap<String, Value>,
1589 includes: &HashMap<String, Value>,
1590) -> String {
1591 let bytes = tpl.as_bytes();
1592 let len = bytes.len();
1593 let mut result = String::with_capacity(len.min(MAX_ENGINE_SCRATCH_STRING));
1594 let mut i = 0;
1595
1596 while i < len {
1597 if result.len() >= MAX_ENGINE_SCRATCH_STRING {
1598 break;
1599 }
1600 if bytes[i] == b'{' {
1601 if let Some(close) = tpl[i + 1..].find('}') {
1602 let inner = &tpl[i + 1..i + 1 + close];
1603 if let Some(colon) = inner.find(':') {
1605 let ref_name = &inner[..colon];
1606 let scope = &inner[colon + 1..];
1607 if ref_name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '.') {
1609 let resolved = if scope == "include" {
1610 if includes.len() == 1 {
1612 let first = includes.values().next().unwrap();
1613 deep_get(first, ref_name)
1614 } else {
1615 None
1616 }
1617 } else {
1618 includes.get(scope).and_then(|inc| deep_get(inc, ref_name))
1620 };
1621 if let Some(val) = resolved {
1622 let s = value_to_string(&val);
1623 let room = MAX_ENGINE_SCRATCH_STRING.saturating_sub(result.len());
1624 if room > 0 {
1625 let take = s.len().min(room);
1626 let end = s.floor_char_boundary(take);
1627 result.push_str(&s[..end]);
1628 }
1629 } else {
1630 result.push('{');
1631 let rem = MAX_ENGINE_SCRATCH_STRING.saturating_sub(result.len() + 1);
1632 if rem > 0 {
1633 let end = inner.floor_char_boundary(inner.len().min(rem));
1634 result.push_str(&inner[..end]);
1635 }
1636 if result.len() < MAX_ENGINE_SCRATCH_STRING {
1637 result.push('}');
1638 }
1639 }
1640 i += 2 + close;
1641 continue;
1642 }
1643 } else {
1644 let ref_name = inner;
1646 if ref_name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '.') {
1647 let resolved = deep_get(root, ref_name).or_else(|| {
1648 local_map.get(ref_name).cloned()
1649 });
1650 if let Some(val) = resolved {
1651 let s = value_to_string(&val);
1652 let room = MAX_ENGINE_SCRATCH_STRING.saturating_sub(result.len());
1653 if room > 0 {
1654 let take = s.len().min(room);
1655 let end = s.floor_char_boundary(take);
1656 result.push_str(&s[..end]);
1657 }
1658 } else {
1659 result.push('{');
1660 let rem = MAX_ENGINE_SCRATCH_STRING.saturating_sub(result.len() + 1);
1661 if rem > 0 {
1662 let end = ref_name.floor_char_boundary(ref_name.len().min(rem));
1663 result.push_str(&ref_name[..end]);
1664 }
1665 if result.len() < MAX_ENGINE_SCRATCH_STRING {
1666 result.push('}');
1667 }
1668 }
1669 i += 2 + close;
1670 continue;
1671 }
1672 }
1673 }
1674 }
1675 if result.len() < MAX_ENGINE_SCRATCH_STRING {
1676 result.push(bytes[i] as char);
1677 }
1678 i += 1;
1679 }
1680 result
1681}
1682
1683fn load_includes(
1685 directives: &[IncludeDirective],
1686 options: &Options,
1687) -> HashMap<String, Value> {
1688 let mut map = HashMap::new();
1689 let base = options.base_path.as_deref().unwrap_or(".");
1690 let max_depth = options.max_include_depth.unwrap_or(DEFAULT_MAX_INCLUDE_DEPTH);
1691 if options._include_depth >= max_depth {
1692 return map;
1693 }
1694 for inc in directives {
1695 let full = match jail_path(base, &inc.path) {
1696 Ok(p) => p,
1697 Err(_) => continue,
1698 };
1699 if check_file_size(&full).is_err() {
1700 continue;
1701 }
1702 if let Ok(text) = std::fs::read_to_string(&full) {
1703 let mut included = parser::parse(&text);
1704 if included.mode == Mode::Active {
1705 let mut child_opts = options.clone();
1706 child_opts._include_depth += 1;
1707 if let Some(parent) = full.parent() {
1708 child_opts.base_path = Some(parent.to_string_lossy().into_owned());
1709 }
1710 resolve(&mut included, &child_opts);
1711 }
1712 map.insert(inc.alias.clone(), included.root);
1713 }
1714 }
1715 map
1716}
1717
1718fn load_packages(
1723 directives: &[UseDirective],
1724 options: &Options,
1725 #[cfg(feature = "wasm")] wasm_runtime: &mut crate::wasm::WasmMarkerRuntime,
1726) -> HashMap<String, Value> {
1727 let mut map = HashMap::new();
1728 let pkg_base = options
1729 .packages_path
1730 .as_deref()
1731 .unwrap_or("./synx_packages");
1732 let base = options.base_path.as_deref().unwrap_or(".");
1733 let pkg_root = std::path::Path::new(base).join(pkg_base);
1734
1735 for ud in directives {
1736 let pkg_dir = pkg_root.join(&ud.package);
1738
1739 #[cfg(feature = "wasm")]
1741 {
1742 if is_marker_package(&pkg_dir) {
1743 let wasm_entry = read_manifest_wasm(&pkg_dir)
1745 .unwrap_or_else(|| pkg_dir.join("src").join("main.wasm"));
1746 let caps = read_manifest_capabilities(&pkg_dir);
1747 if wasm_entry.is_file() {
1748 if let Ok(wasm_bytes) = std::fs::read(&wasm_entry) {
1749 match wasm_runtime.load_module(&wasm_bytes, caps) {
1750 Ok(_markers) => {}
1751 Err(e) => {
1752 map.insert(
1753 ud.alias.clone(),
1754 Value::String(format!("WASM_ERR: {}", e)),
1755 );
1756 }
1757 }
1758 }
1759 }
1760 continue;
1761 }
1762 }
1763
1764 let entry = read_manifest_main(&pkg_dir)
1766 .unwrap_or_else(|| pkg_dir.join("src").join("main.synx"));
1767
1768 if !entry.is_file() {
1769 continue;
1770 }
1771 if check_file_size(&entry).is_err() {
1772 continue;
1773 }
1774 if let Ok(text) = std::fs::read_to_string(&entry) {
1775 let mut parsed = parser::parse(&text);
1776 if parsed.mode == Mode::Active {
1777 let mut child_opts = options.clone();
1778 child_opts._include_depth += 1;
1779 child_opts.base_path = Some(
1780 entry.parent().unwrap_or(&pkg_dir).to_string_lossy().into_owned()
1781 );
1782 resolve(&mut parsed, &child_opts);
1783 }
1784 map.insert(ud.alias.clone(), parsed.root);
1785 }
1786 }
1787 map
1788}
1789
1790fn read_manifest_main(pkg_dir: &std::path::Path) -> Option<std::path::PathBuf> {
1793 let manifest = pkg_dir.join("synx-pkg.synx");
1794 let text = std::fs::read_to_string(&manifest).ok()?;
1795 for line in text.lines() {
1796 let trimmed = line.trim();
1797 if let Some(rest) = trimmed.strip_prefix("main ") {
1798 let entry_path = rest.trim();
1799 if !entry_path.is_empty() {
1800 return Some(pkg_dir.join(entry_path));
1801 }
1802 }
1803 if let Some(rest) = trimmed.strip_prefix("entry ") {
1805 let entry_path = rest.trim();
1806 if !entry_path.is_empty() {
1807 return Some(pkg_dir.join(entry_path));
1808 }
1809 }
1810 }
1811 None
1812}
1813
1814#[cfg(feature = "wasm")]
1817fn is_marker_package(pkg_dir: &std::path::Path) -> bool {
1818 let manifest = pkg_dir.join("synx-pkg.synx");
1819 if let Ok(text) = std::fs::read_to_string(&manifest) {
1820 for line in text.lines() {
1821 let trimmed = line.trim();
1822 if trimmed == "type markers" {
1823 return true;
1824 }
1825 if let Some(rest) = trimmed.strip_prefix("main ") {
1826 if rest.trim().ends_with(".wasm") {
1827 return true;
1828 }
1829 }
1830 }
1831 }
1832 false
1833}
1834
1835#[cfg(feature = "wasm")]
1837fn read_manifest_wasm(pkg_dir: &std::path::Path) -> Option<std::path::PathBuf> {
1838 let manifest = pkg_dir.join("synx-pkg.synx");
1839 let text = std::fs::read_to_string(&manifest).ok()?;
1840 for line in text.lines() {
1841 let trimmed = line.trim();
1842 if let Some(rest) = trimmed.strip_prefix("wasm ") {
1843 let wasm_path = rest.trim();
1844 if !wasm_path.is_empty() {
1845 return Some(pkg_dir.join(wasm_path));
1846 }
1847 }
1848 if let Some(rest) = trimmed.strip_prefix("main ") {
1850 let path = rest.trim();
1851 if path.ends_with(".wasm") {
1852 return Some(pkg_dir.join(path));
1853 }
1854 }
1855 }
1856 None
1857}
1858
1859#[cfg(feature = "wasm")]
1861fn read_manifest_capabilities(pkg_dir: &std::path::Path) -> crate::wasm::WasmCapabilities {
1862 let manifest = pkg_dir.join("synx-pkg.synx");
1863 let text = match std::fs::read_to_string(&manifest) {
1864 Ok(t) => t,
1865 Err(_) => return crate::wasm::WasmCapabilities::default(),
1866 };
1867 for line in text.lines() {
1868 let trimmed = line.trim();
1869 if let Some(rest) = trimmed.strip_prefix("permissions ") {
1870 return crate::wasm::WasmCapabilities::from_manifest_line(rest);
1871 }
1872 }
1873 crate::wasm::WasmCapabilities::default()
1874}
1875
1876fn build_type_registry(metadata: &HashMap<String, MetaMap>) -> HashMap<String, String> {
1881 let mut registry: HashMap<String, String> = HashMap::new();
1882
1883 for meta_map in metadata.values() {
1884 for (key, meta) in meta_map {
1885 if let Some(ref type_hint) = meta.type_hint {
1886 if let Some(existing) = registry.get(key) {
1888 if existing != type_hint {
1889 }
1892 } else {
1893 registry.insert(key.clone(), type_hint.clone());
1894 }
1895 }
1896 }
1897 }
1898
1899 registry
1900}
1901
1902fn build_constraint_registry(metadata: &HashMap<String, MetaMap>) -> HashMap<String, Constraints> {
1905 let mut registry: HashMap<String, Constraints> = HashMap::new();
1906
1907 for meta_map in metadata.values() {
1908 for (key, meta) in meta_map {
1909 if let Some(ref constraints) = meta.constraints {
1910 registry
1911 .entry(key.clone())
1912 .and_modify(|existing| merge_constraints(existing, constraints))
1913 .or_insert_with(|| constraints.clone());
1914 }
1915 }
1916 }
1917
1918 registry
1919}
1920
1921fn merge_constraints(base: &mut Constraints, incoming: &Constraints) {
1924 if incoming.required {
1925 base.required = true;
1926 }
1927 if incoming.readonly {
1928 base.readonly = true;
1929 }
1930
1931 base.min = match (base.min, incoming.min) {
1933 (Some(a), Some(b)) => Some(a.max(b)),
1934 (None, Some(b)) => Some(b),
1935 (a, None) => a,
1936 };
1937 base.max = match (base.max, incoming.max) {
1938 (Some(a), Some(b)) => Some(a.min(b)),
1939 (None, Some(b)) => Some(b),
1940 (a, None) => a,
1941 };
1942
1943 if base.type_name.is_none() {
1945 base.type_name = incoming.type_name.clone();
1946 }
1947 if base.pattern.is_none() {
1948 base.pattern = incoming.pattern.clone();
1949 }
1950 if base.enum_values.is_none() {
1951 base.enum_values = incoming.enum_values.clone();
1952 }
1953}
1954
1955fn validate_field_constraints(value: &mut Value, registry: &HashMap<String, Constraints>) {
1958 if let Value::Object(ref mut map) = value {
1959 let keys: Vec<String> = map.keys().cloned().collect();
1960 for key in &keys {
1961 if let Some(constraints) = registry.get(key) {
1962 validate_constraints(map, key, constraints);
1963 }
1964
1965 if let Some(child) = map.get_mut(key) {
1966 match child {
1967 Value::Object(_) => validate_field_constraints(child, registry),
1968 Value::Array(arr) => {
1969 for item in arr.iter_mut() {
1970 if let Value::Object(_) = item {
1971 validate_field_constraints(item, registry);
1972 }
1973 }
1974 }
1975 _ => {}
1976 }
1977 }
1978 }
1979 }
1980}
1981
1982fn validate_field_types(value: &mut Value, registry: &HashMap<String, String>, path: &str) {
1984 match value {
1985 Value::Object(ref mut map) => {
1986 let keys: Vec<String> = map.keys().cloned().collect();
1987 for key in &keys {
1988 if let Some(expected_type) = registry.get(key) {
1989 if let Some(val) = map.get(key) {
1990 if !value_matches_type(val, expected_type) {
1991 let current_type = value_type_name(val);
1993 map.insert(key.clone(), Value::String(
1994 format!("TYPE_ERR: '{}' expected {} but got {}", key, expected_type, current_type)
1995 ));
1996 }
1997 }
1998 }
1999
2000 if let Some(child) = map.get_mut(key) {
2002 match child {
2003 Value::Object(_) => {
2004 let child_path = if path.is_empty() {
2005 key.clone()
2006 } else {
2007 format!("{}.{}", path, key)
2008 };
2009 validate_field_types(child, registry, &child_path);
2010 }
2011 Value::Array(ref mut arr) => {
2012 for item in arr.iter_mut() {
2013 if let Value::Object(_) = item {
2014 validate_field_types(item, registry, path);
2015 }
2016 }
2017 }
2018 _ => {}
2019 }
2020 }
2021 }
2022 }
2023 _ => {}
2024 }
2025}
2026
2027fn value_matches_type(value: &Value, expected_type: &str) -> bool {
2029 match expected_type {
2030 "int" => matches!(value, Value::Int(_)),
2031 "float" => matches!(value, Value::Float(_) | Value::Int(_)),
2032 "bool" => matches!(value, Value::Bool(_)),
2033 "string" => matches!(value, Value::String(_) | Value::Secret(_)),
2034 "array" => matches!(value, Value::Array(_)),
2035 "object" => matches!(value, Value::Object(_)),
2036 _ => true, }
2038}
2039
2040fn value_type_name(value: &Value) -> String {
2042 match value {
2043 Value::Int(_) => "int".to_string(),
2044 Value::Float(_) => "float".to_string(),
2045 Value::Bool(_) => "bool".to_string(),
2046 Value::String(_) => "string".to_string(),
2047 Value::Secret(_) => "secret".to_string(),
2048 Value::Array(_) => "array".to_string(),
2049 Value::Object(_) => "object".to_string(),
2050 Value::Null => "null".to_string(),
2051 }
2052}
2053
2054fn plural_category(lang: &str, n: i64) -> &'static str {
2059 let abs_n = n.unsigned_abs();
2060 let n10 = abs_n % 10;
2061 let n100 = abs_n % 100;
2062
2063 match lang {
2064 "ru" | "uk" | "be" => {
2066 if n10 == 1 && n100 != 11 {
2067 "one"
2068 } else if (2..=4).contains(&n10) && !(12..=14).contains(&n100) {
2069 "few"
2070 } else {
2071 "many"
2072 }
2073 }
2074 "pl" => {
2076 if n10 == 1 && n100 != 11 {
2077 "one"
2078 } else if (2..=4).contains(&n10) && !(12..=14).contains(&n100) {
2079 "few"
2080 } else {
2081 "many"
2082 }
2083 }
2084 "cs" | "sk" => {
2086 if abs_n == 1 { "one" }
2087 else if (2..=4).contains(&abs_n) { "few" }
2088 else { "other" }
2089 }
2090 "ar" => {
2092 if abs_n == 0 { "zero" }
2093 else if abs_n == 1 { "one" }
2094 else if abs_n == 2 { "two" }
2095 else if (3..=10).contains(&n100) { "few" }
2096 else if (11..=99).contains(&n100) { "many" }
2097 else { "other" }
2098 }
2099 "fr" | "pt" => {
2101 if abs_n <= 1 { "one" } else { "other" }
2102 }
2103 "ja" | "zh" | "ko" | "vi" | "th" => "other",
2105 _ => {
2107 if abs_n == 1 { "one" } else { "other" }
2108 }
2109 }
2110}
2111
2112#[cfg(test)]
2113mod tests {
2114 use crate::{parse, Options, Value};
2115 use super::{resolve, read_manifest_main};
2116
2117 #[test]
2118 fn test_ref_simple() {
2119 let mut r = parse("!active\nbase_rate 50\nquick_rate:ref base_rate");
2120 resolve(&mut r, &Options::default());
2121 let map = r.root.as_object().unwrap();
2122 assert_eq!(map["quick_rate"], Value::Int(50));
2123 }
2124
2125 #[test]
2126 fn test_ref_calc_shorthand() {
2127 let mut r = parse("!active\nbase_rate 50\ndouble_rate:ref:calc:*2 base_rate");
2128 resolve(&mut r, &Options::default());
2129 let map = r.root.as_object().unwrap();
2130 assert_eq!(map["double_rate"], Value::Int(100));
2131 }
2132
2133 #[test]
2134 fn test_inherit() {
2135 let mut r = parse("!active\n_base\n weight 10\n stackable true\nsteel:inherit:_base\n weight 25\n material metal");
2136 resolve(&mut r, &Options::default());
2137 let map = r.root.as_object().unwrap();
2138 assert!(!map.contains_key("_base"));
2139 let steel = map["steel"].as_object().unwrap();
2140 assert_eq!(steel["weight"], Value::Int(25));
2141 assert_eq!(steel["stackable"], Value::Bool(true));
2142 assert_eq!(steel["material"], Value::String("metal".into()));
2143 }
2144
2145 #[test]
2146 fn test_i18n_select_lang() {
2147 let mut r = parse("!active\ntitle:i18n\n en Hello\n ru Привет\n de Hallo");
2148 let opts = Options { lang: Some("ru".into()), ..Default::default() };
2149 resolve(&mut r, &opts);
2150 let map = r.root.as_object().unwrap();
2151 assert_eq!(map["title"], Value::String("Привет".into()));
2152 }
2153
2154 #[test]
2155 fn test_i18n_fallback_en() {
2156 let mut r = parse("!active\ntitle:i18n\n en Hello\n ru Привет");
2157 let opts = Options { lang: Some("fr".into()), ..Default::default() };
2158 resolve(&mut r, &opts);
2159 let map = r.root.as_object().unwrap();
2160 assert_eq!(map["title"], Value::String("Hello".into()));
2161 }
2162
2163 #[test]
2164 fn test_auto_interpolation_simple() {
2165 let mut r = parse("!active\nname Wario\ngreeting Hello, {name}!");
2166 resolve(&mut r, &Options::default());
2167 let map = r.root.as_object().unwrap();
2168 assert_eq!(map["greeting"], Value::String("Hello, Wario!".into()));
2169 }
2170
2171 #[test]
2172 fn test_auto_interpolation_nested() {
2173 let mut r = parse("!active\nserver\n host localhost\n port 8080\nurl http://{server.host}:{server.port}/api");
2174 resolve(&mut r, &Options::default());
2175 let map = r.root.as_object().unwrap();
2176 assert_eq!(map["url"], Value::String("http://localhost:8080/api".into()));
2177 }
2178
2179 #[test]
2180 fn test_template_legacy_still_works() {
2181 let mut r = parse("!active\nname Wario\ngreeting:template Hello, {name}!");
2182 resolve(&mut r, &Options::default());
2183 let map = r.root.as_object().unwrap();
2184 assert_eq!(map["greeting"], Value::String("Hello, Wario!".into()));
2185 }
2186
2187 #[test]
2188 fn test_type_validation() {
2189 let mut r = parse(
2192 "!active\n\
2193 _base_unit\n \
2194 hp(int) 100\n \
2195 speed(float) 1.5\n\
2196 infantry:inherit:_base_unit\n \
2197 name Infantry\n \
2198 hp 80"
2199 );
2200 resolve(&mut r, &Options::default());
2201 let map = r.root.as_object().unwrap();
2202
2203 assert!(!map.contains_key("_base_unit"));
2205
2206 let infantry = map["infantry"].as_object().unwrap();
2208 assert_eq!(infantry["hp"], Value::Int(80)); assert_eq!(infantry["speed"], Value::Float(1.5)); }
2211
2212 #[test]
2213 fn test_type_validation_error() {
2214 let mut r = parse(
2216 "!active\n\
2217 _base_unit\n \
2218 hp(int) 100\n\
2219 infantry:inherit:_base_unit\n \
2220 hp hello" );
2222 resolve(&mut r, &Options::default());
2223 let map = r.root.as_object().unwrap();
2224
2225 let infantry = map["infantry"].as_object().unwrap();
2226 if let Value::String(s) = &infantry["hp"] {
2228 assert!(s.contains("TYPE_ERR"));
2229 } else {
2230 panic!("Expected error string for type mismatch");
2231 }
2232 }
2233
2234 #[test]
2235 fn test_constraint_validation_inherited_range() {
2236 let mut r = parse(
2237 "!active\n\
2238 _base_unit\n \
2239 hp[min:1, max:50000] 1000\n\
2240 infantry:inherit:_base_unit\n \
2241 hp 60000"
2242 );
2243 resolve(&mut r, &Options::default());
2244 let map = r.root.as_object().unwrap();
2245 let infantry = map["infantry"].as_object().unwrap();
2246
2247 if let Value::String(s) = &infantry["hp"] {
2248 assert!(s.contains("CONSTRAINT_ERR"));
2249 assert!(s.contains("exceeds max"));
2250 } else {
2251 panic!("Expected constraint error string");
2252 }
2253 }
2254
2255 #[test]
2256 fn test_constraint_validation_required() {
2257 let mut r = parse(
2258 "!active\n\
2259 _base_unit\n \
2260 description[type:string, required] hello\n\
2261 scout:inherit:_base_unit\n \
2262 description null"
2263 );
2264 resolve(&mut r, &Options::default());
2265 let map = r.root.as_object().unwrap();
2266 let scout = map["scout"].as_object().unwrap();
2267
2268 if let Value::String(s) = &scout["description"] {
2269 assert!(s.contains("CONSTRAINT_ERR"));
2270 assert!(s.contains("required"));
2271 } else {
2272 panic!("Expected required-constraint error string");
2273 }
2274 }
2275
2276 #[test]
2277 fn test_multi_parent_inherit() {
2278 let mut r = parse(
2279 "!active\n\
2280 _movable\n \
2281 speed 10\n \
2282 can_move true\n\
2283 _damageable\n \
2284 hp 100\n \
2285 armor 5\n\
2286 tank:inherit:_movable:_damageable\n \
2287 name Tank\n \
2288 armor 20"
2289 );
2290 resolve(&mut r, &Options::default());
2291 let map = r.root.as_object().unwrap();
2292
2293 assert!(!map.contains_key("_movable"));
2294 assert!(!map.contains_key("_damageable"));
2295
2296 let tank = map["tank"].as_object().unwrap();
2297 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()));
2302 }
2303
2304 #[test]
2305 fn test_calc_dot_path() {
2306 let mut r = parse(
2307 "!active\n\
2308 stats\n \
2309 base_hp 100\n \
2310 multiplier 3\n\
2311 total_hp:calc stats.base_hp * stats.multiplier"
2312 );
2313 resolve(&mut r, &Options::default());
2314 let map = r.root.as_object().unwrap();
2315 assert_eq!(map["total_hp"], Value::Int(300));
2316 }
2317
2318 #[test]
2319 fn test_i18n_plural_en() {
2320 let mut r = parse(
2321 "!active\n\
2322 count 5\n\
2323 items:i18n:count\n \
2324 en\n \
2325 one item\n \
2326 other items"
2327 );
2328 let opts = Options { lang: Some("en".into()), ..Default::default() };
2329 resolve(&mut r, &opts);
2330 let map = r.root.as_object().unwrap();
2331 assert_eq!(map["items"], Value::String("items".into()));
2332 }
2333
2334 #[test]
2335 fn test_i18n_plural_en_one() {
2336 let mut r = parse(
2337 "!active\n\
2338 count 1\n\
2339 items:i18n:count\n \
2340 en\n \
2341 one {count} item\n \
2342 other {count} items"
2343 );
2344 let opts = Options { lang: Some("en".into()), ..Default::default() };
2345 resolve(&mut r, &opts);
2346 let map = r.root.as_object().unwrap();
2347 assert_eq!(map["items"], Value::String("1 item".into()));
2348 }
2349
2350 #[test]
2351 fn test_i18n_plural_ru() {
2352 let mut r = parse(
2353 "!active\n\
2354 count 3\n\
2355 items:i18n:count\n \
2356 ru\n \
2357 one предмет\n \
2358 few предмета\n \
2359 many предметов\n \
2360 other предметов"
2361 );
2362 let opts = Options { lang: Some("ru".into()), ..Default::default() };
2363 resolve(&mut r, &opts);
2364 let map = r.root.as_object().unwrap();
2365 assert_eq!(map["items"], Value::String("предмета".into()));
2366 }
2367
2368 #[test]
2369 fn test_quoted_null_preserved() {
2370 let r = parse("status \"null\"\nenabled \"true\"\ncount \"42\"");
2371 let map = r.root.as_object().unwrap();
2372 assert_eq!(map["status"], Value::String("null".into()));
2373 assert_eq!(map["enabled"], Value::String("true".into()));
2374 assert_eq!(map["count"], Value::String("42".into()));
2375 }
2376
2377 #[test]
2378 fn test_unquoted_null_is_null() {
2379 let r = parse("status null\nenabled true\ncount 42");
2380 let map = r.root.as_object().unwrap();
2381 assert_eq!(map["status"], Value::Null);
2382 assert_eq!(map["enabled"], Value::Bool(true));
2383 assert_eq!(map["count"], Value::Int(42));
2384 }
2385
2386 #[test]
2387 fn test_spam_rate_limit_exceeded() {
2388 super::clear_spam_buckets();
2389
2390 let mut r1 = parse("!active\nsecret_token abc\naccess:spam:1:5 secret_token");
2391 resolve(&mut r1, &Options::default());
2392 let map1 = r1.root.as_object().unwrap();
2393 assert_eq!(map1["access"], Value::String("abc".into()));
2394
2395 let mut r2 = parse("!active\nsecret_token abc\naccess:spam:1:5 secret_token");
2396 resolve(&mut r2, &Options::default());
2397 let map2 = r2.root.as_object().unwrap();
2398 match &map2["access"] {
2399 Value::String(s) => assert!(s.starts_with("SPAM_ERR:")),
2400 _ => panic!("Expected SPAM_ERR string"),
2401 }
2402 }
2403
2404 #[test]
2405 fn test_spam_default_window_sec_is_one() {
2406 super::clear_spam_buckets();
2407
2408 let mut r = parse("!active\na 1\nx:spam:2 a");
2409 resolve(&mut r, &Options::default());
2410 let map = r.root.as_object().unwrap();
2411 assert_eq!(map["x"], Value::Int(1));
2412 }
2413
2414 #[test]
2415 fn test_deep_nesting_does_not_overflow() {
2416 let mut synx = String::from("!active\n");
2419 let mut indent = String::new();
2420 for i in 0..200 {
2421 synx.push_str(&format!("{}level_{}\n", indent, i));
2422 indent.push_str(" ");
2423 }
2424 synx.push_str(&format!("{}value deep\n", indent));
2425
2426 let mut result = parse(&synx);
2427 resolve(&mut result, &Default::default());
2428 assert!(matches!(result.root, Value::Object(_)));
2429
2430 let mut cur = &result.root;
2431 let mut depth = 0usize;
2432 loop {
2433 let Value::Object(map) = cur else { break };
2434 let key = format!("level_{}", depth);
2435 match map.get(&key) {
2436 Some(next) => {
2437 cur = next;
2438 depth += 1;
2439 }
2440 None => break,
2441 }
2442 }
2443 assert!(
2444 depth >= 100,
2445 "expected at least 100 chained levels from parse, got {}",
2446 depth
2447 );
2448 assert!(
2449 depth <= 130,
2450 "parser nesting cap should keep chain shallow, got {}",
2451 depth
2452 );
2453 }
2454
2455 #[test]
2456 fn test_circular_alias_returns_error() {
2457 let mut r = parse("!active\na:alias b\nb:alias a");
2458 resolve(&mut r, &Default::default());
2459 let root = r.root.as_object().unwrap();
2460 let a_val = root.get("a").unwrap();
2461 let b_val = root.get("b").unwrap();
2462 assert!(
2463 matches!(a_val, Value::String(s) if s.starts_with("ALIAS_ERR:")),
2464 "expected ALIAS_ERR for 'a', got: {:?}", a_val
2465 );
2466 assert!(
2467 matches!(b_val, Value::String(s) if s.starts_with("ALIAS_ERR:")),
2468 "expected ALIAS_ERR for 'b', got: {:?}", b_val
2469 );
2470 }
2471
2472 #[test]
2473 fn test_self_alias_returns_error() {
2474 let mut r = parse("!active\na:alias a");
2475 resolve(&mut r, &Default::default());
2476 let root = r.root.as_object().unwrap();
2477 let a_val = root.get("a").unwrap();
2478 assert!(
2479 matches!(a_val, Value::String(s) if s.starts_with("ALIAS_ERR:")),
2480 "expected ALIAS_ERR for self-alias, got: {:?}", a_val
2481 );
2482 }
2483
2484 #[test]
2485 fn test_valid_alias_still_works() {
2486 let mut r = parse("!active\nbase 42\ncopy:alias base");
2487 resolve(&mut r, &Default::default());
2488 let root = r.root.as_object().unwrap();
2489 assert_eq!(root.get("copy"), Some(&Value::Int(42)));
2490 }
2491
2492 #[test]
2493 fn test_alias_to_string_valued_key_no_false_positive() {
2494 let mut r = parse("!active\na b\nb:alias a");
2497 resolve(&mut r, &Default::default());
2498 let root = r.root.as_object().unwrap();
2499 assert_eq!(
2500 root.get("b"),
2501 Some(&Value::String("b".to_string())),
2502 "alias to a string-valued key should not produce ALIAS_ERR"
2503 );
2504 }
2505
2506 #[test]
2507 fn test_prompt_marker() {
2508 let mut r = parse("!active\nmemory:prompt:Core\n identity ASAI\n creator APERTURESyndicate");
2509 resolve(&mut r, &Options::default());
2510 let map = r.root.as_object().unwrap();
2511 if let Value::String(s) = &map["memory"] {
2512 assert!(s.starts_with("Core (SYNX):"));
2513 assert!(s.contains("```synx"));
2514 assert!(s.contains("identity ASAI"));
2515 } else {
2516 panic!("Expected :prompt to produce a string");
2517 }
2518 }
2519
2520 #[test]
2521 fn test_vision_marker_passthrough() {
2522 let mut r = parse("!active\nimage:vision Generate a sunset");
2523 resolve(&mut r, &Options::default());
2524 let map = r.root.as_object().unwrap();
2525 assert_eq!(map["image"], Value::String("Generate a sunset".into()));
2526 }
2527
2528 #[test]
2529 fn test_audio_marker_passthrough() {
2530 let mut r = parse("!active\nnarration:audio Read this summary aloud");
2531 resolve(&mut r, &Options::default());
2532 let map = r.root.as_object().unwrap();
2533 assert_eq!(map["narration"], Value::String("Read this summary aloud".into()));
2534 }
2535
2536 #[test]
2537 fn test_use_directive_loads_package() {
2538 let tmp = std::env::temp_dir().join("synx-use-test-load");
2540 let _ = std::fs::remove_dir_all(&tmp);
2541 let pkg_dir = tmp.join("synx_packages/@test/config");
2542 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
2543 std::fs::write(pkg_dir.join("synx-pkg.synx"), "name @test/config\nversion 1.0.0\nmain src/main.synx\n").unwrap();
2544 std::fs::write(pkg_dir.join("src/main.synx"), "identity APERTURESyndicate\ndefault_port 8080\n").unwrap();
2545
2546 let mut r = parse("!active\n!use @test/config\napp MyApp");
2547 let opts = Options {
2548 base_path: Some(tmp.to_string_lossy().into_owned()),
2549 ..Default::default()
2550 };
2551 resolve(&mut r, &opts);
2552 let map = r.root.as_object().unwrap();
2553 assert!(map.contains_key("config"), "package not loaded");
2554 let pkg = map["config"].as_object().unwrap();
2555 assert_eq!(pkg["identity"], Value::String("APERTURESyndicate".into()));
2556 assert_eq!(pkg["default_port"], Value::Int(8080));
2557 assert_eq!(map["app"], Value::String("MyApp".into()));
2558 let _ = std::fs::remove_dir_all(&tmp);
2559 }
2560
2561 #[test]
2562 fn test_use_directive_with_alias() {
2563 let tmp = std::env::temp_dir().join("synx-use-test-alias");
2564 let _ = std::fs::remove_dir_all(&tmp);
2565 let pkg_dir = tmp.join("synx_packages/@test/config");
2566 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
2567 std::fs::write(pkg_dir.join("synx-pkg.synx"), "name @test/config\nversion 1.0.0\nmain src/main.synx\n").unwrap();
2568 std::fs::write(pkg_dir.join("src/main.synx"), "identity APERTURESyndicate\ndefault_port 8080\n").unwrap();
2569
2570 let mut r = parse("!active\n!use @test/config as defaults\napp MyApp");
2571 let opts = Options {
2572 base_path: Some(tmp.to_string_lossy().into_owned()),
2573 ..Default::default()
2574 };
2575 resolve(&mut r, &opts);
2576 let map = r.root.as_object().unwrap();
2577 assert!(map.contains_key("defaults"), "aliased package not loaded");
2578 assert!(!map.contains_key("config"), "should use alias, not auto name");
2579 let pkg = map["defaults"].as_object().unwrap();
2580 assert_eq!(pkg["identity"], Value::String("APERTURESyndicate".into()));
2581 let _ = std::fs::remove_dir_all(&tmp);
2582 }
2583
2584 #[test]
2585 fn test_use_directive_missing_package_ignored() {
2586 let mut r = parse("!active\n!use @nonexistent/pkg\napp MyApp");
2587 resolve(&mut r, &Options::default());
2588 let map = r.root.as_object().unwrap();
2589 assert!(!map.contains_key("pkg"));
2591 assert_eq!(map["app"], Value::String("MyApp".into()));
2592 }
2593
2594 #[test]
2595 fn test_use_reads_manifest_main_field() {
2596 let tmp = std::env::temp_dir().join("synx-use-test-main");
2597 let _ = std::fs::remove_dir_all(&tmp);
2598 let pkg_dir = tmp.join("synx_packages/@test/myapp");
2599 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
2600 std::fs::write(pkg_dir.join("synx-pkg.synx"), "name @test/myapp\nversion 1.0.0\nmain src/main.synx\n").unwrap();
2601 std::fs::write(pkg_dir.join("src/main.synx"), "app_name MyApp\nversion 2.0.0\n").unwrap();
2602
2603 let mut r = parse("!active\n!use @test/myapp as myapp\napp Test");
2604 let opts = Options {
2605 base_path: Some(tmp.to_string_lossy().into_owned()),
2606 ..Default::default()
2607 };
2608 resolve(&mut r, &opts);
2609 let map = r.root.as_object().unwrap();
2610 assert!(map.contains_key("myapp"), "package not loaded via manifest");
2611 let pkg = map["myapp"].as_object().unwrap();
2612 assert_eq!(pkg["app_name"], Value::String("MyApp".into()));
2613 let _ = std::fs::remove_dir_all(&tmp);
2614 }
2615
2616 #[test]
2617 fn test_read_manifest_main_function() {
2618 let tmp = std::env::temp_dir().join("synx-manifest-main-test");
2619 let _ = std::fs::remove_dir_all(&tmp);
2620 std::fs::create_dir_all(tmp.join("src")).unwrap();
2621 std::fs::write(tmp.join("synx-pkg.synx"), "name @test/pkg\nversion 1.0.0\nmain src/main.synx\n").unwrap();
2622 std::fs::write(tmp.join("src/main.synx"), "key value\n").unwrap();
2623
2624 let result = read_manifest_main(&tmp);
2625 assert!(result.is_some(), "should read main from synx-pkg.synx");
2626 let path = result.unwrap();
2627 assert!(path.ends_with("src/main.synx") || path.ends_with("src\\main.synx"));
2628 let _ = std::fs::remove_dir_all(&tmp);
2629 }
2630}