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_FILE_SIZE: u64 = 10 * 1024 * 1024;
18const DEFAULT_MAX_INCLUDE_DEPTH: usize = 16;
20const MAX_RESOLVE_DEPTH: usize = 512;
22
23fn jail_path(base: &str, file_path: &str) -> Result<std::path::PathBuf, String> {
26 let fp = std::path::Path::new(file_path);
28 if fp.is_absolute() {
29 return Err(format!("SECURITY: absolute paths are not allowed: '{}'", file_path));
30 }
31 let base_canonical = match std::fs::canonicalize(base) {
32 Ok(p) => p,
33 Err(_) => std::path::PathBuf::from(base),
34 };
35 let full = base_canonical.join(file_path);
36 let full_canonical = match std::fs::canonicalize(&full) {
37 Ok(p) => p,
38 Err(_) => {
39 let normalized = full.to_string_lossy();
41 if normalized.contains("..") {
42 return Err(format!("SECURITY: path traversal detected: '{}'", file_path));
43 }
44 return Ok(full);
45 }
46 };
47 if !full_canonical.starts_with(&base_canonical) {
48 return Err(format!("SECURITY: path escapes base directory: '{}'", file_path));
49 }
50 Ok(full_canonical)
51}
52
53fn check_file_size(path: &std::path::Path) -> Result<(), String> {
55 match std::fs::metadata(path) {
56 Ok(meta) if meta.len() > MAX_FILE_SIZE => {
57 Err(format!("SECURITY: file too large ({} bytes, max {})", meta.len(), MAX_FILE_SIZE))
58 }
59 _ => Ok(()),
60 }
61}
62
63pub fn resolve(result: &mut ParseResult, options: &Options) {
66 if result.mode != Mode::Active {
67 return;
68 }
69 let metadata = std::mem::take(&mut result.metadata);
70 let includes_directives = std::mem::take(&mut result.includes);
71
72 let includes_map = load_includes(&includes_directives, options);
74
75 apply_inheritance(&mut result.root, &metadata);
77 if let Value::Object(ref mut root_map) = result.root {
79 root_map.retain(|k, _| !k.starts_with('_'));
80 }
81
82 let type_registry = build_type_registry(&metadata);
84 let constraint_registry = build_constraint_registry(&metadata);
86
87 let root_ptr = &mut result.root as *mut Value;
97 resolve_value(&mut result.root, root_ptr, options, &metadata, "", &includes_map, 0);
98
99 validate_field_constraints(&mut result.root, &constraint_registry);
101
102 validate_field_types(&mut result.root, &type_registry, "");
104
105 result.metadata = metadata;
106 result.includes = includes_directives;
107}
108
109fn resolve_value(
110 value: &mut Value,
111 root_ptr: *mut Value,
112 options: &Options,
113 metadata: &HashMap<String, MetaMap>,
114 path: &str,
115 includes: &HashMap<String, Value>,
116 depth: usize,
117) {
118 if depth >= MAX_RESOLVE_DEPTH {
120 if let Value::Object(ref mut map) = value {
123 for val in map.values_mut() {
124 *val = Value::String(
125 "NESTING_ERR: maximum object nesting depth exceeded".to_string()
126 );
127 }
128 }
129 return;
130 }
131
132 let meta_map = metadata.get(path).cloned();
133
134 if let Value::Object(ref mut map) = value {
135 let keys: Vec<String> = map.keys().cloned().collect();
136
137 for key in &keys {
139 let child_path = if path.is_empty() {
140 key.clone()
141 } else {
142 format!("{}.{}", path, key)
143 };
144
145 if let Some(child) = map.get_mut(key) {
146 match child {
147 Value::Object(_) => {
148 resolve_value(child, root_ptr, options, metadata, &child_path, includes, depth + 1);
149 }
150 Value::Array(arr) => {
151 for item in arr.iter_mut() {
152 if let Value::Object(_) = item {
153 resolve_value(item, root_ptr, options, metadata, &child_path, includes, depth + 1);
154 }
155 }
156 }
157 _ => {}
158 }
159 }
160 }
161
162 if let Some(ref mm) = meta_map {
164 for key in &keys {
165 let meta = match mm.get(key) {
166 Some(m) => m.clone(),
167 None => continue,
168 };
169
170 apply_markers(map, key, &meta, root_ptr, options, path, metadata, includes);
171 }
172 }
173
174 let keys2: Vec<String> = map.keys().cloned().collect();
176 for key in &keys2 {
177 if let Some(Value::String(s)) = map.get(key) {
178 if s.contains('{') {
179 let root_ref = unsafe { &*root_ptr };
180 let result = resolve_interpolation(s, root_ref, map, includes);
181 if result != *s {
182 map.insert(key.to_string(), Value::String(result));
183 }
184 }
185 }
186 }
187 }
188}
189
190fn apply_markers(
191 map: &mut HashMap<String, Value>,
192 key: &str,
193 meta: &Meta,
194 root_ptr: *mut Value,
195 options: &Options,
196 path: &str,
197 metadata: &HashMap<String, MetaMap>,
198 _includes: &HashMap<String, Value>,
199) {
200 let markers = &meta.markers;
201
202 if markers.contains(&"spam".to_string()) {
207 let spam_idx = markers.iter().position(|m| m == "spam").unwrap();
208 let max_calls = markers
209 .get(spam_idx + 1)
210 .and_then(|s| s.parse::<usize>().ok())
211 .unwrap_or(0);
212 let window_sec = markers
213 .get(spam_idx + 2)
214 .and_then(|s| s.parse::<u64>().ok())
215 .unwrap_or(1);
216
217 if max_calls == 0 {
218 map.insert(
219 key.to_string(),
220 Value::String("SPAM_ERR: invalid limit, use :spam:MAX[:WINDOW_SEC]".to_string()),
221 );
222 return;
223 }
224
225 let target = map
226 .get(key)
227 .map(value_to_string)
228 .unwrap_or_else(|| key.to_string());
229 let bucket_key = format!("{}::{}", key, target);
230
231 if !allow_spam_access(&bucket_key, max_calls, window_sec) {
232 map.insert(
233 key.to_string(),
234 Value::String(format!(
235 "SPAM_ERR: '{}' exceeded {} calls per {}s",
236 target, max_calls, window_sec
237 )),
238 );
239 return;
240 }
241
242 if let Some(resolved) = map
243 .get(key)
244 .and_then(|v| {
245 let t = value_to_string(v);
246 let root_ref = unsafe { &*root_ptr };
247 deep_get(root_ref, &t).or_else(|| map.get(t.as_str()).cloned())
248 })
249 {
250 map.insert(key.to_string(), resolved);
251 }
252 }
253
254 if markers.contains(&"include".to_string()) || markers.contains(&"import".to_string()) {
256 if let Some(Value::String(file_path)) = map.get(key) {
257 let max_depth = options.max_include_depth.unwrap_or(DEFAULT_MAX_INCLUDE_DEPTH);
258 if options._include_depth >= max_depth {
259 map.insert(
260 key.to_string(),
261 Value::String(format!("INCLUDE_ERR: max include depth ({}) exceeded", max_depth)),
262 );
263 return;
264 }
265 let base = options
266 .base_path
267 .as_deref()
268 .unwrap_or(".");
269 let full = match jail_path(base, file_path) {
270 Ok(p) => p,
271 Err(e) => {
272 map.insert(key.to_string(), Value::String(format!("INCLUDE_ERR: {}", e)));
273 return;
274 }
275 };
276 if let Err(e) = check_file_size(&full) {
277 map.insert(key.to_string(), Value::String(format!("INCLUDE_ERR: {}", e)));
278 return;
279 }
280 match std::fs::read_to_string(&full) {
281 Ok(text) => {
282 let mut included = parser::parse(&text);
283 if included.mode == Mode::Active {
284 let mut child_opts = options.clone();
285 child_opts._include_depth += 1;
286 if let Some(parent) = full.parent() {
287 child_opts.base_path = Some(parent.to_string_lossy().into_owned());
288 }
289 resolve(&mut included, &child_opts);
290 }
291 map.insert(key.to_string(), included.root);
292 }
293 Err(e) => {
294 map.insert(
295 key.to_string(),
296 Value::String(format!("INCLUDE_ERR: {}", e)),
297 );
298 }
299 }
300 }
301 return;
302 }
303
304 if markers.contains(&"env".to_string()) {
306 if let Some(Value::String(var_name)) = map.get(key) {
307 let env_val = if let Some(ref env_map) = options.env {
308 env_map.get(var_name.as_str()).cloned()
309 } else {
310 std::env::var(var_name).ok()
311 };
312
313 let force_string = meta.type_hint.as_deref() == Some("string");
314 let default_idx = markers.iter().position(|m| m == "default");
315 if let Some(val) = env_val.filter(|v| !v.is_empty()) {
316 let resolved = if force_string {
317 Value::String(val)
318 } else {
319 cast_primitive(&val)
320 };
321 map.insert(key.to_string(), resolved);
322 } else if let Some(di) = default_idx {
323 if markers.len() > di + 1 {
324 let fallback = markers[di + 1..].join(":");
327 let resolved = if force_string {
328 Value::String(fallback)
329 } else {
330 cast_primitive(&fallback)
331 };
332 map.insert(key.to_string(), resolved);
333 } else {
334 map.insert(key.to_string(), Value::Null);
335 }
336 } else {
337 map.insert(key.to_string(), Value::Null);
338 }
339 }
340 }
341
342 if markers.contains(&"random".to_string()) {
344 if let Some(Value::Array(arr)) = map.get(key) {
345 if arr.is_empty() {
346 map.insert(key.to_string(), Value::Null);
347 return;
348 }
349 let picked = if !meta.args.is_empty() {
350 let weights: Vec<f64> = meta.args.iter().filter_map(|s| s.parse().ok()).collect();
351 weighted_random(arr, &weights)
352 } else {
353 arr[rng::random_usize(arr.len())].clone()
354 };
355 map.insert(key.to_string(), picked);
356 }
357 }
358
359 if markers.contains(&"ref".to_string()) {
363 if let Some(Value::String(target)) = map.get(key) {
364 let root_ref = unsafe { &*root_ptr };
365 let resolved = deep_get(root_ref, target)
366 .or_else(|| map.get(target.as_str()).cloned())
367 .unwrap_or(Value::Null);
368
369 if markers.contains(&"calc".to_string()) {
371 if let Some(n) = value_as_number(&resolved) {
372 let calc_idx = markers.iter().position(|m| m == "calc").unwrap();
373 if let Some(calc_expr) = markers.get(calc_idx + 1) {
374 let first = calc_expr.chars().next().unwrap_or(' ');
375 if "+-*/%".contains(first) {
376 let expr = format!("{} {}", format_number(n), calc_expr);
377 match safe_calc(&expr) {
378 Ok(result) => {
379 let v = if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
380 Value::Int(result as i64)
381 } else {
382 Value::Float(result)
383 };
384 map.insert(key.to_string(), v);
385 }
386 Err(e) => {
387 map.insert(key.to_string(), Value::String(format!("CALC_ERR: {}", e)));
388 }
389 }
390 } else {
391 map.insert(key.to_string(), resolved);
392 }
393 } else {
394 map.insert(key.to_string(), resolved);
395 }
396 } else {
397 map.insert(key.to_string(), resolved);
398 }
399 } else {
400 map.insert(key.to_string(), resolved);
401 }
402 }
403 }
404
405 if markers.contains(&"i18n".to_string()) {
419 if let Some(Value::Object(translations)) = map.get(key) {
420 let lang = options.lang.as_deref().unwrap_or("en");
421 let val = translations.get(lang)
422 .or_else(|| translations.get("en"))
423 .or_else(|| translations.values().next())
424 .cloned()
425 .unwrap_or(Value::Null);
426
427 let i18n_idx = markers.iter().position(|m| m == "i18n").unwrap();
429 let count_field = markers.get(i18n_idx + 1).cloned();
430
431 if let (Some(ref cf), Value::Object(ref plural_forms)) = (&count_field, &val) {
432 let count_val = map.get(cf)
434 .and_then(value_as_number)
435 .or_else(|| {
436 let root_ref = unsafe { &*root_ptr };
437 deep_get(root_ref, cf).and_then(|v| value_as_number(&v))
438 })
439 .unwrap_or(0.0) as i64;
440
441 let category = plural_category(lang, count_val);
442 let chosen = plural_forms.get(category)
443 .or_else(|| plural_forms.get("other"))
444 .or_else(|| plural_forms.values().next())
445 .cloned()
446 .unwrap_or(Value::Null);
447
448 if let Value::String(ref s) = chosen {
450 let replaced = s.replace("{count}", &count_val.to_string());
451 map.insert(key.to_string(), Value::String(replaced));
452 } else {
453 map.insert(key.to_string(), chosen);
454 }
455 } else {
456 map.insert(key.to_string(), val);
457 }
458 }
459 }
460
461 if markers.contains(&"calc".to_string()) {
463 if let Some(Value::String(expr)) = map.get(key) {
464 if expr.len() > MAX_CALC_EXPR_LEN {
465 map.insert(
466 key.to_string(),
467 Value::String(format!("CALC_ERR: expression too long ({} chars, max {})", expr.len(), MAX_CALC_EXPR_LEN)),
468 );
469 return;
470 }
471 let mut resolved = expr.clone();
472
473 let root_ref = unsafe { &*root_ptr };
475 if let Value::Object(ref root_map) = root_ref {
476 for (rk, rv) in root_map {
477 if let Some(n) = value_as_number(rv) {
478 resolved = replace_word(&resolved, rk, &format_number(n));
479 }
480 }
481 }
482
483 for (rk, rv) in map.iter() {
485 if rk != key {
486 if let Some(n) = value_as_number(rv) {
487 resolved = replace_word(&resolved, rk, &format_number(n));
488 }
489 }
490 }
491
492 let root_ref2 = unsafe { &*root_ptr };
494 let mut dot_resolved = String::new();
495 let bytes = resolved.as_bytes();
496 let len = bytes.len();
497 let mut i = 0;
498 while i < len {
499 if is_word_char(bytes[i]) {
500 let start = i;
501 let mut has_dot = false;
502 while i < len && (is_word_char(bytes[i]) || bytes[i] == b'.') {
503 if bytes[i] == b'.' { has_dot = true; }
504 i += 1;
505 }
506 let token = &resolved[start..i];
507 if has_dot && token.contains('.') {
508 if let Some(val) = deep_get(root_ref2, token) {
509 if let Some(n) = value_as_number(&val) {
510 dot_resolved.push_str(&format_number(n));
511 continue;
512 }
513 }
514 }
515 dot_resolved.push_str(token);
516 } else {
517 dot_resolved.push(bytes[i] as char);
518 i += 1;
519 }
520 }
521 resolved = dot_resolved;
522
523 match safe_calc(&resolved) {
524 Ok(result) => {
525 let v = if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
526 Value::Int(result as i64)
527 } else {
528 Value::Float(result)
529 };
530 map.insert(key.to_string(), v);
531 }
532 Err(e) => {
533 map.insert(
534 key.to_string(),
535 Value::String(format!("CALC_ERR: {}", e)),
536 );
537 }
538 }
539 }
540 }
541
542 if markers.contains(&"alias".to_string()) {
544 if let Some(Value::String(target)) = map.get(key) {
545 let target = target.clone();
546 let current_path = if path.is_empty() {
548 key.to_string()
549 } else {
550 format!("{}.{}", path, key)
551 };
552 if target == key || target == current_path {
554 map.insert(
555 key.to_string(),
556 Value::String(format!("ALIAS_ERR: self-referential alias: {} → {}", current_path, target)),
557 );
558 } else {
559 let root_ref = unsafe { &*root_ptr };
564 let target_val = deep_get(root_ref, &target);
565 let (target_parent, target_key_name) = if let Some(dot) = target.rfind('.') {
567 (target[..dot].to_string(), target[dot + 1..].to_string())
568 } else {
569 (String::new(), target.clone())
570 };
571 let target_has_alias = metadata
572 .get(&target_parent)
573 .and_then(|mm| mm.get(&target_key_name))
574 .map(|m| m.markers.contains(&"alias".to_string()))
575 .unwrap_or(false);
576 let is_cycle = target_has_alias && match &target_val {
577 Some(Value::String(s)) => s == key || s == ¤t_path,
578 _ => false,
579 };
580 if is_cycle {
581 map.insert(
582 key.to_string(),
583 Value::String(format!("ALIAS_ERR: circular alias detected: {} → {}", current_path, target)),
584 );
585 } else {
586 let val = target_val.unwrap_or(Value::Null);
587 map.insert(key.to_string(), val);
588 }
589 }
590 }
591 }
592
593 if markers.contains(&"secret".to_string()) {
595 if let Some(val) = map.get(key) {
596 let s = value_to_string(val);
597 map.insert(key.to_string(), Value::Secret(s));
598 }
599 }
600
601 if markers.contains(&"unique".to_string()) {
603 if let Some(Value::Array(arr)) = map.get(key) {
604 let mut seen = Vec::new();
605 let mut unique = Vec::new();
606 for item in arr {
607 let s = value_to_string(item);
608 if !seen.contains(&s) {
609 seen.push(s);
610 unique.push(item.clone());
611 }
612 }
613 map.insert(key.to_string(), Value::Array(unique));
614 }
615 }
616
617 if markers.contains(&"geo".to_string()) {
619 if let Some(Value::Array(arr)) = map.get(key) {
620 let region = options.region.as_deref().unwrap_or("US");
621 let prefix = format!("{} ", region);
622 let found = arr.iter().find(|item| {
623 if let Value::String(s) = item {
624 s.starts_with(&prefix)
625 } else {
626 false
627 }
628 });
629
630 let result = if let Some(Value::String(s)) = found {
631 Value::String(s[prefix.len()..].trim().to_string())
632 } else if let Some(first) = arr.first() {
633 if let Value::String(s) = first {
634 if let Some(space) = s.find(' ') {
635 Value::String(s[space + 1..].trim().to_string())
636 } else {
637 first.clone()
638 }
639 } else {
640 first.clone()
641 }
642 } else {
643 Value::Null
644 };
645 map.insert(key.to_string(), result);
646 }
647 }
648
649 if markers.contains(&"split".to_string()) {
653 if let Some(Value::String(s)) = map.get(key) {
654 let split_idx = markers.iter().position(|m| m == "split").unwrap();
655 let sep = if split_idx + 1 < markers.len() {
656 delimiter_from_keyword(&markers[split_idx + 1])
657 } else {
658 ",".to_string()
659 };
660 let items: Vec<Value> = s
661 .split(&sep)
662 .map(|p| p.trim())
663 .filter(|p| !p.is_empty())
664 .map(|p| cast_primitive(p))
665 .collect();
666 map.insert(key.to_string(), Value::Array(items));
667 }
668 }
669
670 if markers.contains(&"join".to_string()) {
672 if let Some(Value::Array(arr)) = map.get(key) {
673 let join_idx = markers.iter().position(|m| m == "join").unwrap();
674 let sep = if join_idx + 1 < markers.len() {
675 delimiter_from_keyword(&markers[join_idx + 1])
676 } else {
677 ",".to_string()
678 };
679 let joined: String = arr
680 .iter()
681 .map(|v| value_to_string(v))
682 .collect::<Vec<_>>()
683 .join(&sep);
684 map.insert(key.to_string(), Value::String(joined));
685 }
686 }
687
688 if markers.contains(&"default".to_string()) && !markers.contains(&"env".to_string()) {
690 let is_empty = match map.get(key) {
691 Some(Value::Null) | None => true,
692 Some(Value::String(s)) if s.is_empty() => true,
693 _ => false,
694 };
695 if is_empty {
696 let di = markers.iter().position(|m| m == "default").unwrap();
697 if markers.len() > di + 1 {
698 let fallback = markers[di + 1..].join(":");
699 let resolved = if meta.type_hint.as_deref() == Some("string") {
700 Value::String(fallback)
701 } else {
702 cast_primitive(&fallback)
703 };
704 map.insert(key.to_string(), resolved);
705 }
706 }
707 }
708
709 if markers.contains(&"clamp".to_string()) {
713 let clamp_idx = markers.iter().position(|m| m == "clamp").unwrap();
714 let min_s = markers.get(clamp_idx + 1).cloned().unwrap_or_default();
715 let max_s = markers.get(clamp_idx + 2).cloned().unwrap_or_default();
716 if let (Ok(lo), Ok(hi)) = (min_s.parse::<f64>(), max_s.parse::<f64>()) {
717 if lo > hi {
718 map.insert(key.to_string(), Value::String(
719 format!("CONSTRAINT_ERR: clamp min ({}) > max ({})", lo, hi),
720 ));
721 } else if let Some(n) = map.get(key).and_then(value_as_number) {
722 let clamped = n.clamp(lo, hi);
723 let v = if clamped.fract() == 0.0 && clamped.abs() < i64::MAX as f64 {
724 Value::Int(clamped as i64)
725 } else {
726 Value::Float(clamped)
727 };
728 map.insert(key.to_string(), v);
729 }
730 }
731 }
732
733 if markers.contains(&"round".to_string()) {
737 let round_idx = markers.iter().position(|m| m == "round").unwrap();
738 let decimals: u32 = markers.get(round_idx + 1)
739 .and_then(|s| s.parse().ok())
740 .unwrap_or(0);
741 if let Some(n) = map.get(key).and_then(value_as_number) {
742 let factor = 10f64.powi(decimals as i32);
743 let rounded = (n * factor).round() / factor;
744 let v = if decimals == 0 {
745 Value::Int(rounded as i64)
746 } else {
747 Value::Float(rounded)
748 };
749 map.insert(key.to_string(), v);
750 }
751 }
752
753 if markers.contains(&"map".to_string()) {
757 if let Some(Value::Array(arr)) = map.get(key) {
758 let map_idx = markers.iter().position(|m| m == "map").unwrap();
759 let source_key = markers.get(map_idx + 1).cloned().unwrap_or_default();
760 let lookup_val = if !source_key.is_empty() {
761 let root_ref = unsafe { &*root_ptr };
762 deep_get(root_ref, &source_key)
763 .or_else(|| map.get(&source_key).cloned())
764 .map(|v| value_to_string(&v))
765 .unwrap_or_default()
766 } else {
767 match map.get(key) {
769 Some(Value::String(s)) => s.clone(),
770 _ => String::new(),
771 }
772 };
773
774 let arr_clone = arr.clone();
776 let result = arr_clone.iter().find_map(|item| {
777 if let Value::String(s) = item {
778 if let Some(space) = s.find(' ') {
779 if s[..space].trim() == lookup_val {
780 return Some(cast_primitive(s[space + 1..].trim()));
781 }
782 }
783 }
784 None
785 });
786 map.insert(key.to_string(), result.unwrap_or(Value::Null));
787 }
788 }
789
790 if markers.contains(&"format".to_string()) {
794 let fmt_idx = markers.iter().position(|m| m == "format").unwrap();
795 let pattern = markers.get(fmt_idx + 1).cloned().unwrap_or_else(|| "%s".to_string());
796 if let Some(current) = map.get(key) {
797 let formatted = apply_format_pattern(&pattern, current);
798 map.insert(key.to_string(), Value::String(formatted));
799 }
800 }
801
802 if markers.contains(&"fallback".to_string()) {
807 let fb_idx = markers.iter().position(|m| m == "fallback").unwrap();
808 let default_val = markers.get(fb_idx + 1).cloned().unwrap_or_default();
809 let use_fallback = match map.get(key) {
810 None | Some(Value::Null) => true,
811 Some(Value::String(s)) if s.is_empty() => true,
812 Some(Value::String(s)) => {
813 let base = options.base_path.as_deref().unwrap_or(".");
814 match jail_path(base, s) {
815 Ok(safe) => !safe.exists(),
816 Err(_) => true, }
818 }
819 _ => false,
820 };
821 if use_fallback && !default_val.is_empty() {
822 map.insert(key.to_string(), Value::String(default_val));
823 }
824 }
825
826 if markers.contains(&"once".to_string()) {
830 let once_idx = markers.iter().position(|m| m == "once").unwrap();
831 let gen_type = markers.get(once_idx + 1).map(|s| s.as_str()).unwrap_or("uuid");
832 let lock_path = options.base_path.as_deref()
833 .map(|b| std::path::Path::new(b).join(".synx.lock"))
834 .unwrap_or_else(|| std::path::Path::new(".synx.lock").to_path_buf());
835
836 let existing = read_lock_value(&lock_path, key);
838 if let Some(locked) = existing {
839 map.insert(key.to_string(), Value::String(locked));
840 } else {
841 let generated = match gen_type {
842 "uuid" => rng::generate_uuid(),
843 "timestamp" => std::time::SystemTime::now()
844 .duration_since(std::time::UNIX_EPOCH)
845 .unwrap_or_default()
846 .as_secs()
847 .to_string(),
848 "random" => rng::random_usize(u32::MAX as usize).to_string(),
849 _ => rng::generate_uuid(),
850 };
851 write_lock_value(&lock_path, key, &generated);
852 map.insert(key.to_string(), Value::String(generated));
853 }
854 }
855
856 if markers.contains(&"version".to_string()) {
861 if let Some(Value::String(current_ver)) = map.get(key) {
862 let ver_idx = markers.iter().position(|m| m == "version").unwrap();
863 let op = markers.get(ver_idx + 1).map(|s| s.as_str()).unwrap_or(">=");
864 let required = markers.get(ver_idx + 2).cloned().unwrap_or_default();
865 let result = compare_versions(current_ver, op, &required);
866 map.insert(key.to_string(), Value::Bool(result));
867 }
868 }
869
870 if markers.contains(&"watch".to_string()) {
874 if let Some(Value::String(file_path)) = map.get(key) {
875 let max_depth = options.max_include_depth.unwrap_or(DEFAULT_MAX_INCLUDE_DEPTH);
876 if options._include_depth >= max_depth {
877 map.insert(
878 key.to_string(),
879 Value::String(format!("WATCH_ERR: max include depth ({}) exceeded", max_depth)),
880 );
881 return;
882 }
883 let base = options.base_path.as_deref().unwrap_or(".");
884 let full = match jail_path(base, file_path) {
885 Ok(p) => p,
886 Err(e) => {
887 map.insert(key.to_string(), Value::String(format!("WATCH_ERR: {}", e)));
888 return;
889 }
890 };
891 if let Err(e) = check_file_size(&full) {
892 map.insert(key.to_string(), Value::String(format!("WATCH_ERR: {}", e)));
893 return;
894 }
895 let watch_idx = markers.iter().position(|m| m == "watch").unwrap();
896 let key_path = markers.get(watch_idx + 1).cloned();
897
898 match std::fs::read_to_string(&full) {
899 Ok(content) => {
900 let value = if let Some(ref kp) = key_path {
901 extract_from_file_content(&content, kp, full.extension().and_then(|e| e.to_str()).unwrap_or("")).unwrap_or(Value::Null)
902 } else {
903 Value::String(content.trim().to_string())
904 };
905 map.insert(key.to_string(), value);
906 }
907 Err(e) => {
908 map.insert(key.to_string(), Value::String(format!("WATCH_ERR: {}", e)));
909 }
910 }
911 }
912 }
913
914 if let Some(ref c) = meta.constraints {
916 validate_constraints(map, key, c);
917 }
918}
919
920fn validate_constraints(map: &mut HashMap<String, Value>, key: &str, c: &Constraints) {
923 let val = match map.get(key) {
924 Some(v) => v.clone(),
925 None => {
926 if c.required {
927 map.insert(key.to_string(), Value::String(
928 format!("CONSTRAINT_ERR: '{}' is required", key),
929 ));
930 }
931 return;
932 }
933 };
934
935 if c.required {
937 let empty = matches!(val, Value::Null)
938 || matches!(&val, Value::String(s) if s.is_empty());
939 if empty {
940 map.insert(key.to_string(), Value::String(
941 format!("CONSTRAINT_ERR: '{}' is required", key),
942 ));
943 return;
944 }
945 }
946
947 if let Some(ref type_name) = c.type_name {
949 let ok = match type_name.as_str() {
950 "int" => matches!(val, Value::Int(_)),
951 "float" => matches!(val, Value::Float(_) | Value::Int(_)),
952 "bool" => matches!(val, Value::Bool(_)),
953 "string" => matches!(val, Value::String(_)),
954 _ => true,
955 };
956 if !ok {
957 map.insert(key.to_string(), Value::String(
958 format!("CONSTRAINT_ERR: '{}' expected type '{}'", key, type_name),
959 ));
960 return;
961 }
962 }
963
964 if let Some(ref enum_vals) = c.enum_values {
966 let val_str = match &val {
967 Value::String(s) => s.clone(),
968 Value::Int(n) => n.to_string(),
969 Value::Float(f) => f.to_string(),
970 Value::Bool(b) => b.to_string(),
971 _ => String::new(),
972 };
973 if !enum_vals.contains(&val_str) {
974 map.insert(key.to_string(), Value::String(
975 format!("CONSTRAINT_ERR: '{}' must be one of [{}]", key, enum_vals.join("|")),
976 ));
977 return;
978 }
979 }
980
981 let num = match &val {
983 Value::Int(n) => Some(*n as f64),
984 Value::Float(f) => Some(*f),
985 Value::String(s) if c.min.is_some() || c.max.is_some() => Some(s.len() as f64),
986 _ => None,
987 };
988 if let Some(n) = num {
989 if let Some(min) = c.min {
990 if n < min {
991 map.insert(key.to_string(), Value::String(
992 format!("CONSTRAINT_ERR: '{}' value {} is below min {}", key, n, min),
993 ));
994 return;
995 }
996 }
997 if let Some(max) = c.max {
998 if n > max {
999 map.insert(key.to_string(), Value::String(
1000 format!("CONSTRAINT_ERR: '{}' value {} exceeds max {}", key, n, max),
1001 ));
1002 return;
1003 }
1004 }
1005 }
1006 }
1010
1011fn apply_format_pattern(pattern: &str, value: &Value) -> String {
1015 match value {
1016 Value::Int(n) => {
1017 if pattern.contains('d') || pattern.contains('i') {
1018 format_int_pattern(pattern, *n)
1019 } else if pattern.contains('f') || pattern.contains('e') {
1020 format_float_pattern(pattern, *n as f64)
1021 } else {
1022 n.to_string()
1023 }
1024 }
1025 Value::Float(f) => {
1026 if pattern.contains('f') || pattern.contains('e') {
1027 format_float_pattern(pattern, *f)
1028 } else {
1029 format_number(*f)
1030 }
1031 }
1032 Value::String(s) => s.clone(),
1033 other => value_to_string(other),
1034 }
1035}
1036
1037fn format_int_pattern(pattern: &str, n: i64) -> String {
1038 if let Some(s) = pattern.strip_prefix('%') {
1039 if let Some(inner) = s.strip_suffix('d').or_else(|| s.strip_suffix('i')) {
1040 if let Some(w) = inner.strip_prefix('0') {
1041 if let Ok(width) = w.parse::<usize>() {
1042 return format!("{:0>width$}", n, width = width);
1043 }
1044 }
1045 if let Ok(width) = inner.parse::<usize>() {
1046 return format!("{:>width$}", n, width = width);
1047 }
1048 }
1049 }
1050 n.to_string()
1051}
1052
1053fn format_float_pattern(pattern: &str, f: f64) -> String {
1054 if let Some(s) = pattern.strip_prefix('%') {
1055 if let Some(inner) = s.strip_suffix('f').or_else(|| s.strip_suffix('e')) {
1056 if let Some(prec_s) = inner.strip_prefix('.') {
1057 if let Ok(prec) = prec_s.parse::<usize>() {
1058 return format!("{:.prec$}", f, prec = prec);
1059 }
1060 }
1061 }
1062 }
1063 f.to_string()
1064}
1065
1066fn read_lock_value(lock_path: &std::path::Path, key: &str) -> Option<String> {
1068 let content = std::fs::read_to_string(lock_path).ok()?;
1069 for line in content.lines() {
1070 if let Some(rest) = line.strip_prefix(key) {
1071 if rest.starts_with(' ') {
1072 return Some(rest.trim_start().to_string());
1073 }
1074 }
1075 }
1076 None
1077}
1078
1079fn write_lock_value(lock_path: &std::path::Path, key: &str, value: &str) {
1081 let mut lines: Vec<String> = std::fs::read_to_string(lock_path)
1082 .unwrap_or_default()
1083 .lines()
1084 .map(|l| l.to_string())
1085 .collect();
1086
1087 let new_line = format!("{} {}", key, value);
1088 let mut found = false;
1089 for line in lines.iter_mut() {
1090 if line.starts_with(key) && line[key.len()..].starts_with(' ') {
1091 *line = new_line.clone();
1092 found = true;
1093 break;
1094 }
1095 }
1096 if !found {
1097 lines.push(new_line);
1098 }
1099 let _ = std::fs::write(lock_path, lines.join("\n") + "\n");
1100}
1101
1102fn compare_versions(current: &str, op: &str, required: &str) -> bool {
1104 let parse_ver = |s: &str| -> Vec<u64> {
1105 s.split('.').filter_map(|p| p.parse().ok()).collect()
1106 };
1107 let cv = parse_ver(current);
1108 let rv = parse_ver(required);
1109 let len = cv.len().max(rv.len());
1110 let mut ord = std::cmp::Ordering::Equal;
1111 for i in 0..len {
1112 let a = cv.get(i).copied().unwrap_or(0);
1113 let b = rv.get(i).copied().unwrap_or(0);
1114 if a != b {
1115 ord = a.cmp(&b);
1116 break;
1117 }
1118 }
1119 match op {
1120 ">=" => ord != std::cmp::Ordering::Less,
1121 "<=" => ord != std::cmp::Ordering::Greater,
1122 ">" => ord == std::cmp::Ordering::Greater,
1123 "<" => ord == std::cmp::Ordering::Less,
1124 "==" | "=" => ord == std::cmp::Ordering::Equal,
1125 "!=" => ord != std::cmp::Ordering::Equal,
1126 _ => false,
1127 }
1128}
1129
1130fn allow_spam_access(bucket_key: &str, max_calls: usize, window_sec: u64) -> bool {
1131 let now = Instant::now();
1132 let window = Duration::from_secs(window_sec.max(1));
1133
1134 let buckets = SPAM_BUCKETS.get_or_init(|| Mutex::new(HashMap::new()));
1135 let mut guard = match buckets.lock() {
1136 Ok(g) => g,
1137 Err(poisoned) => poisoned.into_inner(),
1138 };
1139
1140 let calls = guard.entry(bucket_key.to_string()).or_default();
1141 calls.retain(|ts| now.duration_since(*ts) <= window);
1142
1143 if calls.len() >= max_calls {
1144 return false;
1145 }
1146
1147 calls.push(now);
1148 true
1149}
1150
1151#[cfg(test)]
1152fn clear_spam_buckets() {
1153 let buckets = SPAM_BUCKETS.get_or_init(|| Mutex::new(HashMap::new()));
1154 if let Ok(mut guard) = buckets.lock() {
1155 guard.clear();
1156 }
1157}
1158
1159fn extract_from_file_content(content: &str, key_path: &str, ext: &str) -> Option<Value> {
1161 if ext == "json" {
1162 let search = format!("\"{}\"", key_path);
1163 if let Some(pos) = content.find(&search) {
1164 let after = content[pos + search.len()..].trim_start();
1165 if let Some(rest) = after.strip_prefix(':') {
1166 let val_s = rest.trim_start()
1167 .trim_end_matches(',')
1168 .trim_end_matches('}')
1169 .trim()
1170 .trim_matches('"');
1171 return Some(cast_primitive(val_s));
1172 }
1173 }
1174 None
1175 } else {
1176 for line in content.lines() {
1177 let trimmed = line.trim_start();
1178 if trimmed.starts_with(key_path) {
1179 let rest = &trimmed[key_path.len()..];
1180 if rest.starts_with(' ') {
1181 return Some(cast_primitive(rest.trim_start()));
1182 }
1183 }
1184 }
1185 None
1186 }
1187}
1188
1189fn cast_primitive(val: &str) -> Value {
1192 if val.len() >= 2 {
1194 let bytes = val.as_bytes();
1195 if (bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"')
1196 || (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\'')
1197 {
1198 return Value::String(val[1..val.len() - 1].to_string());
1199 }
1200 }
1201 match val {
1202 "true" => Value::Bool(true),
1203 "false" => Value::Bool(false),
1204 "null" => Value::Null,
1205 _ => {
1206 if let Ok(i) = val.parse::<i64>() {
1207 Value::Int(i)
1208 } else if let Ok(f) = val.parse::<f64>() {
1209 Value::Float(f)
1210 } else {
1211 Value::String(val.to_string())
1212 }
1213 }
1214 }
1215}
1216
1217fn delimiter_from_keyword(keyword: &str) -> String {
1218 match keyword {
1219 "space" => " ".to_string(),
1220 "pipe" => "|".to_string(),
1221 "dash" => "-".to_string(),
1222 "dot" => ".".to_string(),
1223 "semi" => ";".to_string(),
1224 "tab" => "\t".to_string(),
1225 "slash" => "/".to_string(),
1226 other => other.to_string(),
1227 }
1228}
1229
1230fn value_as_number(v: &Value) -> Option<f64> {
1231 match v {
1232 Value::Int(n) => Some(*n as f64),
1233 Value::Float(f) => Some(*f),
1234 _ => None,
1235 }
1236}
1237
1238fn value_to_string(v: &Value) -> String {
1239 match v {
1240 Value::String(s) => s.clone(),
1241 Value::Int(n) => n.to_string(),
1242 Value::Float(f) => format_number(*f as f64),
1243 Value::Bool(b) => b.to_string(),
1244 Value::Null => "null".to_string(),
1245 Value::Secret(s) => s.clone(),
1246 Value::Array(_) | Value::Object(_) => String::new(),
1247 }
1248}
1249
1250fn format_number(n: f64) -> String {
1251 if n.fract() == 0.0 && n.abs() < i64::MAX as f64 {
1252 (n as i64).to_string()
1253 } else {
1254 n.to_string()
1255 }
1256}
1257
1258fn replace_word(haystack: &str, word: &str, replacement: &str) -> String {
1260 let word_bytes = word.as_bytes();
1261 let word_len = word_bytes.len();
1262 let hay_bytes = haystack.as_bytes();
1263 let hay_len = hay_bytes.len();
1264
1265 if word_len > hay_len {
1266 return haystack.to_string();
1267 }
1268
1269 let mut result = String::with_capacity(hay_len);
1270 let mut i = 0;
1271
1272 while i <= hay_len - word_len {
1273 if &hay_bytes[i..i + word_len] == word_bytes {
1274 let before_ok = i == 0 || !is_word_char(hay_bytes[i - 1]);
1275 let after_ok = i + word_len >= hay_len || !is_word_char(hay_bytes[i + word_len]);
1276 if before_ok && after_ok {
1277 result.push_str(replacement);
1278 i += word_len;
1279 continue;
1280 }
1281 }
1282 result.push(hay_bytes[i] as char);
1283 i += 1;
1284 }
1285 while i < hay_len {
1286 result.push(hay_bytes[i] as char);
1287 i += 1;
1288 }
1289 result
1290}
1291
1292fn is_word_char(b: u8) -> bool {
1293 b.is_ascii_alphanumeric() || b == b'_'
1294}
1295
1296fn weighted_random(items: &[Value], weights: &[f64]) -> Value {
1297 let mut w: Vec<f64> = weights.to_vec();
1298 if w.len() < items.len() {
1299 let assigned: f64 = w.iter().sum();
1300 let per_item = if assigned < 100.0 {
1304 (100.0 - assigned) / (items.len() - w.len()) as f64
1305 } else {
1306 assigned / w.len() as f64
1307 };
1308 while w.len() < items.len() {
1309 w.push(per_item);
1310 }
1311 }
1312 let total: f64 = w.iter().sum();
1313 if total <= 0.0 {
1314 return items[rng::random_usize(items.len())].clone();
1315 }
1316
1317 let rand_val = rng::random_f64_01();
1318 let mut cumulative = 0.0;
1319 for (i, item) in items.iter().enumerate() {
1320 cumulative += w[i] / total;
1321 if rand_val <= cumulative {
1322 return item.clone();
1323 }
1324 }
1325 items.last().cloned().unwrap_or(Value::Null)
1326}
1327
1328fn apply_inheritance(root: &mut Value, metadata: &HashMap<String, MetaMap>) {
1331 let root_meta = match metadata.get("") {
1332 Some(m) => m.clone(),
1333 None => return,
1334 };
1335
1336 let root_map = match root.as_object_mut() {
1337 Some(m) => m as *mut HashMap<String, Value>,
1338 None => return,
1339 };
1340
1341 let mut inherits: Vec<(String, Vec<String>)> = Vec::new();
1343 for (key, meta) in &root_meta {
1344 if meta.markers.contains(&"inherit".to_string()) {
1345 let idx = meta.markers.iter().position(|m| m == "inherit").unwrap();
1346 let parents: Vec<String> = meta.markers[idx + 1..].to_vec();
1348 if !parents.is_empty() {
1349 inherits.push((key.clone(), parents));
1350 }
1351 }
1352 }
1353
1354 let map = unsafe { &mut *root_map };
1355 for (child_key, parents) in &inherits {
1356 let mut merged: HashMap<String, Value> = HashMap::new();
1358 for parent_name in parents {
1359 if let Some(Value::Object(p)) = map.get(parent_name) {
1360 for (k, v) in p {
1361 merged.insert(k.clone(), v.clone());
1362 }
1363 }
1364 }
1365 if let Some(Value::Object(c)) = map.get(child_key) {
1367 for (k, v) in c {
1368 merged.insert(k.clone(), v.clone());
1369 }
1370 }
1371 map.insert(child_key.clone(), Value::Object(merged));
1372 }
1373}
1374
1375fn deep_get(root: &Value, path: &str) -> Option<Value> {
1376 if let Value::Object(map) = root {
1378 if let Some(val) = map.get(path) {
1379 return Some(val.clone());
1380 }
1381 }
1382 let parts: Vec<&str> = path.split('.').collect();
1384 let mut current = root;
1385 for part in parts {
1386 match current {
1387 Value::Object(map) => match map.get(part) {
1388 Some(v) => current = v,
1389 None => return None,
1390 },
1391 _ => return None,
1392 }
1393 }
1394 Some(current.clone())
1395}
1396
1397fn resolve_interpolation(
1400 tpl: &str,
1401 root: &Value,
1402 local_map: &HashMap<String, Value>,
1403 includes: &HashMap<String, Value>,
1404) -> String {
1405 let bytes = tpl.as_bytes();
1406 let len = bytes.len();
1407 let mut result = String::with_capacity(len);
1408 let mut i = 0;
1409
1410 while i < len {
1411 if bytes[i] == b'{' {
1412 if let Some(close) = tpl[i + 1..].find('}') {
1413 let inner = &tpl[i + 1..i + 1 + close];
1414 if let Some(colon) = inner.find(':') {
1416 let ref_name = &inner[..colon];
1417 let scope = &inner[colon + 1..];
1418 if ref_name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '.') {
1420 let resolved = if scope == "include" {
1421 if includes.len() == 1 {
1423 let first = includes.values().next().unwrap();
1424 deep_get(first, ref_name)
1425 } else {
1426 None
1427 }
1428 } else {
1429 includes.get(scope).and_then(|inc| deep_get(inc, ref_name))
1431 };
1432 if let Some(val) = resolved {
1433 result.push_str(&value_to_string(&val));
1434 } else {
1435 result.push('{');
1436 result.push_str(inner);
1437 result.push('}');
1438 }
1439 i += 2 + close;
1440 continue;
1441 }
1442 } else {
1443 let ref_name = inner;
1445 if ref_name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '.') {
1446 let resolved = deep_get(root, ref_name).or_else(|| {
1447 local_map.get(ref_name).cloned()
1448 });
1449 if let Some(val) = resolved {
1450 result.push_str(&value_to_string(&val));
1451 } else {
1452 result.push('{');
1453 result.push_str(ref_name);
1454 result.push('}');
1455 }
1456 i += 2 + close;
1457 continue;
1458 }
1459 }
1460 }
1461 }
1462 result.push(bytes[i] as char);
1463 i += 1;
1464 }
1465 result
1466}
1467
1468fn load_includes(
1470 directives: &[IncludeDirective],
1471 options: &Options,
1472) -> HashMap<String, Value> {
1473 let mut map = HashMap::new();
1474 let base = options.base_path.as_deref().unwrap_or(".");
1475 let max_depth = options.max_include_depth.unwrap_or(DEFAULT_MAX_INCLUDE_DEPTH);
1476 if options._include_depth >= max_depth {
1477 return map;
1478 }
1479 for inc in directives {
1480 let full = match jail_path(base, &inc.path) {
1481 Ok(p) => p,
1482 Err(_) => continue,
1483 };
1484 if check_file_size(&full).is_err() {
1485 continue;
1486 }
1487 if let Ok(text) = std::fs::read_to_string(&full) {
1488 let mut included = parser::parse(&text);
1489 if included.mode == Mode::Active {
1490 let mut child_opts = options.clone();
1491 child_opts._include_depth += 1;
1492 if let Some(parent) = full.parent() {
1493 child_opts.base_path = Some(parent.to_string_lossy().into_owned());
1494 }
1495 resolve(&mut included, &child_opts);
1496 }
1497 map.insert(inc.alias.clone(), included.root);
1498 }
1499 }
1500 map
1501}
1502
1503fn build_type_registry(metadata: &HashMap<String, MetaMap>) -> HashMap<String, String> {
1508 let mut registry: HashMap<String, String> = HashMap::new();
1509
1510 for meta_map in metadata.values() {
1511 for (key, meta) in meta_map {
1512 if let Some(ref type_hint) = meta.type_hint {
1513 if let Some(existing) = registry.get(key) {
1515 if existing != type_hint {
1516 }
1519 } else {
1520 registry.insert(key.clone(), type_hint.clone());
1521 }
1522 }
1523 }
1524 }
1525
1526 registry
1527}
1528
1529fn build_constraint_registry(metadata: &HashMap<String, MetaMap>) -> HashMap<String, Constraints> {
1532 let mut registry: HashMap<String, Constraints> = HashMap::new();
1533
1534 for meta_map in metadata.values() {
1535 for (key, meta) in meta_map {
1536 if let Some(ref constraints) = meta.constraints {
1537 registry
1538 .entry(key.clone())
1539 .and_modify(|existing| merge_constraints(existing, constraints))
1540 .or_insert_with(|| constraints.clone());
1541 }
1542 }
1543 }
1544
1545 registry
1546}
1547
1548fn merge_constraints(base: &mut Constraints, incoming: &Constraints) {
1551 if incoming.required {
1552 base.required = true;
1553 }
1554 if incoming.readonly {
1555 base.readonly = true;
1556 }
1557
1558 base.min = match (base.min, incoming.min) {
1560 (Some(a), Some(b)) => Some(a.max(b)),
1561 (None, Some(b)) => Some(b),
1562 (a, None) => a,
1563 };
1564 base.max = match (base.max, incoming.max) {
1565 (Some(a), Some(b)) => Some(a.min(b)),
1566 (None, Some(b)) => Some(b),
1567 (a, None) => a,
1568 };
1569
1570 if base.type_name.is_none() {
1572 base.type_name = incoming.type_name.clone();
1573 }
1574 if base.pattern.is_none() {
1575 base.pattern = incoming.pattern.clone();
1576 }
1577 if base.enum_values.is_none() {
1578 base.enum_values = incoming.enum_values.clone();
1579 }
1580}
1581
1582fn validate_field_constraints(value: &mut Value, registry: &HashMap<String, Constraints>) {
1585 if let Value::Object(ref mut map) = value {
1586 let keys: Vec<String> = map.keys().cloned().collect();
1587 for key in &keys {
1588 if let Some(constraints) = registry.get(key) {
1589 validate_constraints(map, key, constraints);
1590 }
1591
1592 if let Some(child) = map.get_mut(key) {
1593 match child {
1594 Value::Object(_) => validate_field_constraints(child, registry),
1595 Value::Array(arr) => {
1596 for item in arr.iter_mut() {
1597 if let Value::Object(_) = item {
1598 validate_field_constraints(item, registry);
1599 }
1600 }
1601 }
1602 _ => {}
1603 }
1604 }
1605 }
1606 }
1607}
1608
1609fn validate_field_types(value: &mut Value, registry: &HashMap<String, String>, path: &str) {
1611 match value {
1612 Value::Object(ref mut map) => {
1613 let keys: Vec<String> = map.keys().cloned().collect();
1614 for key in &keys {
1615 if let Some(expected_type) = registry.get(key) {
1616 if let Some(val) = map.get(key) {
1617 if !value_matches_type(val, expected_type) {
1618 let current_type = value_type_name(val);
1620 map.insert(key.clone(), Value::String(
1621 format!("TYPE_ERR: '{}' expected {} but got {}", key, expected_type, current_type)
1622 ));
1623 }
1624 }
1625 }
1626
1627 if let Some(child) = map.get_mut(key) {
1629 match child {
1630 Value::Object(_) => {
1631 let child_path = if path.is_empty() {
1632 key.clone()
1633 } else {
1634 format!("{}.{}", path, key)
1635 };
1636 validate_field_types(child, registry, &child_path);
1637 }
1638 Value::Array(ref mut arr) => {
1639 for item in arr.iter_mut() {
1640 if let Value::Object(_) = item {
1641 validate_field_types(item, registry, path);
1642 }
1643 }
1644 }
1645 _ => {}
1646 }
1647 }
1648 }
1649 }
1650 _ => {}
1651 }
1652}
1653
1654fn value_matches_type(value: &Value, expected_type: &str) -> bool {
1656 match expected_type {
1657 "int" => matches!(value, Value::Int(_)),
1658 "float" => matches!(value, Value::Float(_) | Value::Int(_)),
1659 "bool" => matches!(value, Value::Bool(_)),
1660 "string" => matches!(value, Value::String(_) | Value::Secret(_)),
1661 "array" => matches!(value, Value::Array(_)),
1662 "object" => matches!(value, Value::Object(_)),
1663 _ => true, }
1665}
1666
1667fn value_type_name(value: &Value) -> String {
1669 match value {
1670 Value::Int(_) => "int".to_string(),
1671 Value::Float(_) => "float".to_string(),
1672 Value::Bool(_) => "bool".to_string(),
1673 Value::String(_) => "string".to_string(),
1674 Value::Secret(_) => "secret".to_string(),
1675 Value::Array(_) => "array".to_string(),
1676 Value::Object(_) => "object".to_string(),
1677 Value::Null => "null".to_string(),
1678 }
1679}
1680
1681fn plural_category(lang: &str, n: i64) -> &'static str {
1686 let abs_n = n.unsigned_abs();
1687 let n10 = abs_n % 10;
1688 let n100 = abs_n % 100;
1689
1690 match lang {
1691 "ru" | "uk" | "be" => {
1693 if n10 == 1 && n100 != 11 {
1694 "one"
1695 } else if (2..=4).contains(&n10) && !(12..=14).contains(&n100) {
1696 "few"
1697 } else {
1698 "many"
1699 }
1700 }
1701 "pl" => {
1703 if n10 == 1 && n100 != 11 {
1704 "one"
1705 } else if (2..=4).contains(&n10) && !(12..=14).contains(&n100) {
1706 "few"
1707 } else {
1708 "many"
1709 }
1710 }
1711 "cs" | "sk" => {
1713 if abs_n == 1 { "one" }
1714 else if (2..=4).contains(&abs_n) { "few" }
1715 else { "other" }
1716 }
1717 "ar" => {
1719 if abs_n == 0 { "zero" }
1720 else if abs_n == 1 { "one" }
1721 else if abs_n == 2 { "two" }
1722 else if (3..=10).contains(&n100) { "few" }
1723 else if (11..=99).contains(&n100) { "many" }
1724 else { "other" }
1725 }
1726 "fr" | "pt" => {
1728 if abs_n <= 1 { "one" } else { "other" }
1729 }
1730 "ja" | "zh" | "ko" | "vi" | "th" => "other",
1732 _ => {
1734 if abs_n == 1 { "one" } else { "other" }
1735 }
1736 }
1737}
1738
1739#[cfg(test)]
1740mod tests {
1741 use crate::{parse, Options, Value};
1742 use super::resolve;
1743
1744 #[test]
1745 fn test_ref_simple() {
1746 let mut r = parse("!active\nbase_rate 50\nquick_rate:ref base_rate");
1747 resolve(&mut r, &Options::default());
1748 let map = r.root.as_object().unwrap();
1749 assert_eq!(map["quick_rate"], Value::Int(50));
1750 }
1751
1752 #[test]
1753 fn test_ref_calc_shorthand() {
1754 let mut r = parse("!active\nbase_rate 50\ndouble_rate:ref:calc:*2 base_rate");
1755 resolve(&mut r, &Options::default());
1756 let map = r.root.as_object().unwrap();
1757 assert_eq!(map["double_rate"], Value::Int(100));
1758 }
1759
1760 #[test]
1761 fn test_inherit() {
1762 let mut r = parse("!active\n_base\n weight 10\n stackable true\nsteel:inherit:_base\n weight 25\n material metal");
1763 resolve(&mut r, &Options::default());
1764 let map = r.root.as_object().unwrap();
1765 assert!(!map.contains_key("_base"));
1766 let steel = map["steel"].as_object().unwrap();
1767 assert_eq!(steel["weight"], Value::Int(25));
1768 assert_eq!(steel["stackable"], Value::Bool(true));
1769 assert_eq!(steel["material"], Value::String("metal".into()));
1770 }
1771
1772 #[test]
1773 fn test_i18n_select_lang() {
1774 let mut r = parse("!active\ntitle:i18n\n en Hello\n ru Привет\n de Hallo");
1775 let opts = Options { lang: Some("ru".into()), ..Default::default() };
1776 resolve(&mut r, &opts);
1777 let map = r.root.as_object().unwrap();
1778 assert_eq!(map["title"], Value::String("Привет".into()));
1779 }
1780
1781 #[test]
1782 fn test_i18n_fallback_en() {
1783 let mut r = parse("!active\ntitle:i18n\n en Hello\n ru Привет");
1784 let opts = Options { lang: Some("fr".into()), ..Default::default() };
1785 resolve(&mut r, &opts);
1786 let map = r.root.as_object().unwrap();
1787 assert_eq!(map["title"], Value::String("Hello".into()));
1788 }
1789
1790 #[test]
1791 fn test_auto_interpolation_simple() {
1792 let mut r = parse("!active\nname Wario\ngreeting Hello, {name}!");
1793 resolve(&mut r, &Options::default());
1794 let map = r.root.as_object().unwrap();
1795 assert_eq!(map["greeting"], Value::String("Hello, Wario!".into()));
1796 }
1797
1798 #[test]
1799 fn test_auto_interpolation_nested() {
1800 let mut r = parse("!active\nserver\n host localhost\n port 8080\nurl http://{server.host}:{server.port}/api");
1801 resolve(&mut r, &Options::default());
1802 let map = r.root.as_object().unwrap();
1803 assert_eq!(map["url"], Value::String("http://localhost:8080/api".into()));
1804 }
1805
1806 #[test]
1807 fn test_template_legacy_still_works() {
1808 let mut r = parse("!active\nname Wario\ngreeting:template Hello, {name}!");
1809 resolve(&mut r, &Options::default());
1810 let map = r.root.as_object().unwrap();
1811 assert_eq!(map["greeting"], Value::String("Hello, Wario!".into()));
1812 }
1813
1814 #[test]
1815 fn test_type_validation() {
1816 let mut r = parse(
1819 "!active\n\
1820 _base_unit\n \
1821 hp(int) 100\n \
1822 speed(float) 1.5\n\
1823 infantry:inherit:_base_unit\n \
1824 name Infantry\n \
1825 hp 80"
1826 );
1827 resolve(&mut r, &Options::default());
1828 let map = r.root.as_object().unwrap();
1829
1830 assert!(!map.contains_key("_base_unit"));
1832
1833 let infantry = map["infantry"].as_object().unwrap();
1835 assert_eq!(infantry["hp"], Value::Int(80)); assert_eq!(infantry["speed"], Value::Float(1.5)); }
1838
1839 #[test]
1840 fn test_type_validation_error() {
1841 let mut r = parse(
1843 "!active\n\
1844 _base_unit\n \
1845 hp(int) 100\n\
1846 infantry:inherit:_base_unit\n \
1847 hp hello" );
1849 resolve(&mut r, &Options::default());
1850 let map = r.root.as_object().unwrap();
1851
1852 let infantry = map["infantry"].as_object().unwrap();
1853 if let Value::String(s) = &infantry["hp"] {
1855 assert!(s.contains("TYPE_ERR"));
1856 } else {
1857 panic!("Expected error string for type mismatch");
1858 }
1859 }
1860
1861 #[test]
1862 fn test_constraint_validation_inherited_range() {
1863 let mut r = parse(
1864 "!active\n\
1865 _base_unit\n \
1866 hp[min:1, max:50000] 1000\n\
1867 infantry:inherit:_base_unit\n \
1868 hp 60000"
1869 );
1870 resolve(&mut r, &Options::default());
1871 let map = r.root.as_object().unwrap();
1872 let infantry = map["infantry"].as_object().unwrap();
1873
1874 if let Value::String(s) = &infantry["hp"] {
1875 assert!(s.contains("CONSTRAINT_ERR"));
1876 assert!(s.contains("exceeds max"));
1877 } else {
1878 panic!("Expected constraint error string");
1879 }
1880 }
1881
1882 #[test]
1883 fn test_constraint_validation_required() {
1884 let mut r = parse(
1885 "!active\n\
1886 _base_unit\n \
1887 description[type:string, required] hello\n\
1888 scout:inherit:_base_unit\n \
1889 description null"
1890 );
1891 resolve(&mut r, &Options::default());
1892 let map = r.root.as_object().unwrap();
1893 let scout = map["scout"].as_object().unwrap();
1894
1895 if let Value::String(s) = &scout["description"] {
1896 assert!(s.contains("CONSTRAINT_ERR"));
1897 assert!(s.contains("required"));
1898 } else {
1899 panic!("Expected required-constraint error string");
1900 }
1901 }
1902
1903 #[test]
1904 fn test_multi_parent_inherit() {
1905 let mut r = parse(
1906 "!active\n\
1907 _movable\n \
1908 speed 10\n \
1909 can_move true\n\
1910 _damageable\n \
1911 hp 100\n \
1912 armor 5\n\
1913 tank:inherit:_movable:_damageable\n \
1914 name Tank\n \
1915 armor 20"
1916 );
1917 resolve(&mut r, &Options::default());
1918 let map = r.root.as_object().unwrap();
1919
1920 assert!(!map.contains_key("_movable"));
1921 assert!(!map.contains_key("_damageable"));
1922
1923 let tank = map["tank"].as_object().unwrap();
1924 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()));
1929 }
1930
1931 #[test]
1932 fn test_calc_dot_path() {
1933 let mut r = parse(
1934 "!active\n\
1935 stats\n \
1936 base_hp 100\n \
1937 multiplier 3\n\
1938 total_hp:calc stats.base_hp * stats.multiplier"
1939 );
1940 resolve(&mut r, &Options::default());
1941 let map = r.root.as_object().unwrap();
1942 assert_eq!(map["total_hp"], Value::Int(300));
1943 }
1944
1945 #[test]
1946 fn test_i18n_plural_en() {
1947 let mut r = parse(
1948 "!active\n\
1949 count 5\n\
1950 items:i18n:count\n \
1951 en\n \
1952 one item\n \
1953 other items"
1954 );
1955 let opts = Options { lang: Some("en".into()), ..Default::default() };
1956 resolve(&mut r, &opts);
1957 let map = r.root.as_object().unwrap();
1958 assert_eq!(map["items"], Value::String("items".into()));
1959 }
1960
1961 #[test]
1962 fn test_i18n_plural_en_one() {
1963 let mut r = parse(
1964 "!active\n\
1965 count 1\n\
1966 items:i18n:count\n \
1967 en\n \
1968 one {count} item\n \
1969 other {count} items"
1970 );
1971 let opts = Options { lang: Some("en".into()), ..Default::default() };
1972 resolve(&mut r, &opts);
1973 let map = r.root.as_object().unwrap();
1974 assert_eq!(map["items"], Value::String("1 item".into()));
1975 }
1976
1977 #[test]
1978 fn test_i18n_plural_ru() {
1979 let mut r = parse(
1980 "!active\n\
1981 count 3\n\
1982 items:i18n:count\n \
1983 ru\n \
1984 one предмет\n \
1985 few предмета\n \
1986 many предметов\n \
1987 other предметов"
1988 );
1989 let opts = Options { lang: Some("ru".into()), ..Default::default() };
1990 resolve(&mut r, &opts);
1991 let map = r.root.as_object().unwrap();
1992 assert_eq!(map["items"], Value::String("предмета".into()));
1993 }
1994
1995 #[test]
1996 fn test_quoted_null_preserved() {
1997 let r = parse("status \"null\"\nenabled \"true\"\ncount \"42\"");
1998 let map = r.root.as_object().unwrap();
1999 assert_eq!(map["status"], Value::String("null".into()));
2000 assert_eq!(map["enabled"], Value::String("true".into()));
2001 assert_eq!(map["count"], Value::String("42".into()));
2002 }
2003
2004 #[test]
2005 fn test_unquoted_null_is_null() {
2006 let r = parse("status null\nenabled true\ncount 42");
2007 let map = r.root.as_object().unwrap();
2008 assert_eq!(map["status"], Value::Null);
2009 assert_eq!(map["enabled"], Value::Bool(true));
2010 assert_eq!(map["count"], Value::Int(42));
2011 }
2012
2013 #[test]
2014 fn test_spam_rate_limit_exceeded() {
2015 super::clear_spam_buckets();
2016
2017 let mut r1 = parse("!active\nsecret_token abc\naccess:spam:1:5 secret_token");
2018 resolve(&mut r1, &Options::default());
2019 let map1 = r1.root.as_object().unwrap();
2020 assert_eq!(map1["access"], Value::String("abc".into()));
2021
2022 let mut r2 = parse("!active\nsecret_token abc\naccess:spam:1:5 secret_token");
2023 resolve(&mut r2, &Options::default());
2024 let map2 = r2.root.as_object().unwrap();
2025 match &map2["access"] {
2026 Value::String(s) => assert!(s.starts_with("SPAM_ERR:")),
2027 _ => panic!("Expected SPAM_ERR string"),
2028 }
2029 }
2030
2031 #[test]
2032 fn test_spam_default_window_sec_is_one() {
2033 super::clear_spam_buckets();
2034
2035 let mut r = parse("!active\na 1\nx:spam:2 a");
2036 resolve(&mut r, &Options::default());
2037 let map = r.root.as_object().unwrap();
2038 assert_eq!(map["x"], Value::Int(1));
2039 }
2040
2041 #[test]
2042 fn test_deep_nesting_does_not_overflow() {
2043 let mut synx = String::from("!active\n");
2045 let mut indent = String::new();
2046 for i in 0..600 {
2047 synx.push_str(&format!("{}level_{}\n", indent, i));
2048 indent.push_str(" ");
2049 }
2050 synx.push_str(&format!("{}value deep\n", indent));
2051
2052 let mut result = parse(&synx);
2054 resolve(&mut result, &Default::default());
2055 assert!(matches!(result.root, Value::Object(_)));
2056
2057 let mut cur = &result.root;
2059 for i in 0..510 {
2060 match cur {
2061 Value::Object(map) => {
2062 let key = format!("level_{}", i);
2063 cur = map.get(&key).expect(&format!("key '{}' should exist", key));
2064 }
2065 _ => panic!("expected object at level {}", i),
2066 }
2067 }
2068 assert!(matches!(cur, Value::Object(_)), "level_510 should be Object");
2070
2071 let mut cur2 = &result.root;
2073 for i in 0..512 {
2074 match cur2 {
2075 Value::Object(map) => {
2076 let key = format!("level_{}", i);
2077 cur2 = map.get(&key).expect(&format!("key '{}' should exist", key));
2078 }
2079 _ => break,
2080 }
2081 }
2082 if let Value::Object(map) = cur2 {
2084 for v in map.values() {
2085 if let Value::String(s) = v {
2086 assert!(s.starts_with("NESTING_ERR:"), "expected NESTING_ERR at depth limit, got: {}", s);
2087 }
2088 }
2089 }
2090 }
2091
2092 #[test]
2093 fn test_circular_alias_returns_error() {
2094 let mut r = parse("!active\na:alias b\nb:alias a");
2095 resolve(&mut r, &Default::default());
2096 let root = r.root.as_object().unwrap();
2097 let a_val = root.get("a").unwrap();
2098 let b_val = root.get("b").unwrap();
2099 assert!(
2100 matches!(a_val, Value::String(s) if s.starts_with("ALIAS_ERR:")),
2101 "expected ALIAS_ERR for 'a', got: {:?}", a_val
2102 );
2103 assert!(
2104 matches!(b_val, Value::String(s) if s.starts_with("ALIAS_ERR:")),
2105 "expected ALIAS_ERR for 'b', got: {:?}", b_val
2106 );
2107 }
2108
2109 #[test]
2110 fn test_self_alias_returns_error() {
2111 let mut r = parse("!active\na:alias a");
2112 resolve(&mut r, &Default::default());
2113 let root = r.root.as_object().unwrap();
2114 let a_val = root.get("a").unwrap();
2115 assert!(
2116 matches!(a_val, Value::String(s) if s.starts_with("ALIAS_ERR:")),
2117 "expected ALIAS_ERR for self-alias, got: {:?}", a_val
2118 );
2119 }
2120
2121 #[test]
2122 fn test_valid_alias_still_works() {
2123 let mut r = parse("!active\nbase 42\ncopy:alias base");
2124 resolve(&mut r, &Default::default());
2125 let root = r.root.as_object().unwrap();
2126 assert_eq!(root.get("copy"), Some(&Value::Int(42)));
2127 }
2128
2129 #[test]
2130 fn test_alias_to_string_valued_key_no_false_positive() {
2131 let mut r = parse("!active\na b\nb:alias a");
2134 resolve(&mut r, &Default::default());
2135 let root = r.root.as_object().unwrap();
2136 assert_eq!(
2137 root.get("b"),
2138 Some(&Value::String("b".to_string())),
2139 "alias to a string-valued key should not produce ALIAS_ERR"
2140 );
2141 }
2142}