1use serde_json::{Map, Value};
14use std::collections::HashSet;
15
16use crate::expr::evaluator::{numbers_equal, relative_base};
17use crate::expr::Expression;
18
19use super::canonical::{canonical_text, number_text};
20use super::length::value_length;
21use super::membership::is_member;
22use super::numeric::{is_multiple, numeric_value};
23use super::parameters::Parameter;
24use super::references::reference_ids;
25use super::whitespace::{is_empty, trim};
26
27pub struct RuleContext<'a> {
32 pub value: &'a Value,
35 pub rule_param: &'a Value,
39 pub(crate) parameter: &'a Parameter,
41 pub messages: Option<&'a Value>,
43 pub rule_name: &'a str,
46 pub path_segments: &'a [String],
48 pub row_keys: &'a [usize],
50 pub form_data: &'a Value,
52 pub(crate) run: &'a super::validator::Run<'a>,
54}
55
56pub fn get_rule(name: &str) -> Option<fn(&RuleContext) -> Option<String>> {
59 match name {
60 "required" => Some(rule_required),
61 "email" => Some(rule_email),
62 "minlength" => Some(rule_minlength),
63 "maxlength" => Some(rule_maxlength),
64 "min" => Some(rule_min),
65 "max" => Some(rule_max),
66 "match" => Some(rule_match),
67 "pattern" => Some(rule_match),
69 "unique" => Some(rule_unique),
70 "in" => Some(rule_in),
71 "range" => Some(rule_range),
72 "rangelength" => Some(rule_rangelength),
73 "number" => Some(rule_number),
74 "digits" => Some(rule_digits),
75 "equalTo" => Some(rule_equal_to),
76 "containedIn" => Some(rule_contained_in),
77 "typeFrom" => Some(rule_type_from),
78 "minFrom" => Some(rule_min_from),
79 "maxFrom" => Some(rule_max_from),
80 "notEqual" => Some(rule_not_equal),
81 "date" => Some(rule_date),
82 "dateISO" => Some(rule_date_iso),
83 "enddate" => Some(rule_enddate),
84 "url" => Some(rule_url),
85 "accept" => Some(rule_accept),
86 "mincount" => Some(rule_mincount),
87 "maxcount" => Some(rule_maxcount),
88 "step" => Some(rule_step),
89 "conditionTree" => Some(rule_condition_tree),
90 "reference" => Some(rule_reference),
91 _ => None,
92 }
93}
94
95fn message_override<'a>(messages: Option<&'a Value>, key: &str) -> Option<&'a str> {
101 messages
102 .and_then(Value::as_object)
103 .and_then(|m| m.get(key))
104 .and_then(Value::as_str)
105}
106
107fn fill(message: &str, first: &str, second: Option<&str>) -> String {
109 let message = message.replace("{0}", first);
110 match second {
111 Some(second) => message.replace("{1}", second),
112 None => message,
113 }
114}
115
116fn js_string(value: &Value) -> String {
118 match value {
119 Value::Null => "null".to_string(),
120 Value::String(s) => s.clone(),
121 Value::Bool(b) => b.to_string(),
122 Value::Number(n) => n.as_f64().map(number_text).unwrap_or_default(),
123 Value::Array(_) | Value::Object(_) => value.to_string(),
126 }
127}
128
129fn rule_required(ctx: &RuleContext) -> Option<String> {
134 if ctx.rule_param != &Value::Bool(true) {
136 return None;
137 }
138 if is_empty(ctx.value) {
139 return Some(
140 message_override(ctx.messages, "required")
141 .unwrap_or("This field is required.")
142 .to_string(),
143 );
144 }
145 None
146}
147
148fn rule_email(ctx: &RuleContext) -> Option<String> {
153 if ctx.rule_param != &Value::Bool(true) {
154 return None;
155 }
156 if is_empty(ctx.value) {
157 return None;
158 }
159 if !is_valid_email(ctx.value) {
160 return Some(
161 message_override(ctx.messages, "email")
162 .unwrap_or("Please enter a valid email address.")
163 .to_string(),
164 );
165 }
166 None
167}
168
169fn is_valid_email(value: &Value) -> bool {
170 let s = match value {
171 Value::String(s) => s,
172 _ => return false,
173 };
174 let at = match s.find('@') {
175 Some(i) => i,
176 None => return false,
177 };
178 let local = &s[..at];
179 let domain = &s[at + 1..];
180
181 if !is_valid_email_local(local) || !is_valid_email_domain(domain) {
182 return false;
183 }
184 if local.starts_with('.') || local.ends_with('.') || local.contains("..") {
186 return false;
187 }
188 true
189}
190
191fn is_valid_email_local(local: &str) -> bool {
193 if local.is_empty() {
194 return false;
195 }
196 local.chars().all(|c| {
197 c.is_ascii_alphanumeric()
198 || matches!(
199 c,
200 '.' | '!'
201 | '#'
202 | '$'
203 | '%'
204 | '&'
205 | '\''
206 | '*'
207 | '+'
208 | '/'
209 | '='
210 | '?'
211 | '^'
212 | '_'
213 | '`'
214 | '{'
215 | '|'
216 | '}'
217 | '~'
218 | '-'
219 )
220 })
221}
222
223fn is_valid_email_domain(domain: &str) -> bool {
226 if domain.is_empty() {
227 return false;
228 }
229 for label in domain.split('.') {
230 if !is_valid_email_label(label) {
231 return false;
232 }
233 }
234 true
235}
236
237fn is_valid_email_label(label: &str) -> bool {
238 let bytes = label.as_bytes();
239 let n = bytes.len();
240 if n == 0 || n > 63 {
241 return false;
242 }
243 let is_alnum = |b: u8| b.is_ascii_alphanumeric();
244 if !is_alnum(bytes[0]) || !is_alnum(bytes[n - 1]) {
245 return false;
246 }
247 bytes
248 .get(1..n.saturating_sub(1))
249 .is_none_or(|middle| middle.iter().all(|&b| is_alnum(b) || b == b'-'))
250}
251
252fn rule_url(ctx: &RuleContext) -> Option<String> {
257 if ctx.rule_param == &Value::Bool(false) {
258 return None;
259 }
260 if is_empty(ctx.value) {
261 return None;
262 }
263 if !is_valid_url(ctx.value) {
264 return Some(
265 message_override(ctx.messages, "url")
266 .unwrap_or("Please enter a valid URL.")
267 .to_string(),
268 );
269 }
270 None
271}
272
273fn is_valid_url(value: &Value) -> bool {
277 let s = match value {
278 Value::String(s) => trim(s),
279 _ => return false,
280 };
281 if s.is_empty() {
282 return false;
283 }
284 let scheme_end = match s.find("://") {
285 Some(i) => i,
286 None => return false,
287 };
288 let scheme = s[..scheme_end].to_ascii_lowercase();
289 if !matches!(scheme.as_str(), "http" | "https" | "ftp") {
290 return false;
291 }
292 let rest = &s[scheme_end + 3..];
293 if rest.is_empty() {
294 return false;
295 }
296 if rest.chars().any(|c| c.is_whitespace()) {
297 return false;
298 }
299 true
300}
301
302fn rule_minlength(ctx: &RuleContext) -> Option<String> {
307 let Parameter::Limit(min) = *ctx.parameter else {
308 unreachable!("minlength receives a checked limit")
309 };
310 if is_empty(ctx.value) {
311 return None;
312 }
313 if value_length(ctx.value).is_some_and(|length| length >= min) {
314 return None;
315 }
316 let msg = message_override(ctx.messages, "minlength")
317 .map(str::to_string)
318 .unwrap_or_else(|| format!("Please enter at least {min} characters."));
319 Some(fill(&msg, &min.to_string(), None))
320}
321
322fn rule_maxlength(ctx: &RuleContext) -> Option<String> {
323 let Parameter::Limit(max) = *ctx.parameter else {
324 unreachable!("maxlength receives a checked limit")
325 };
326 if is_empty(ctx.value) {
327 return None;
328 }
329 if value_length(ctx.value).is_some_and(|length| length <= max) {
330 return None;
331 }
332 let msg = message_override(ctx.messages, "maxlength")
333 .map(str::to_string)
334 .unwrap_or_else(|| format!("Please enter no more than {max} characters."));
335 Some(fill(&msg, &max.to_string(), None))
336}
337
338fn rule_rangelength(ctx: &RuleContext) -> Option<String> {
339 let Parameter::Range(min, max) = *ctx.parameter else {
340 unreachable!("rangelength receives checked limits")
341 };
342 if is_empty(ctx.value) {
343 return None;
344 }
345 if value_length(ctx.value).is_some_and(|length| (min..=max).contains(&length)) {
346 return None;
347 }
348 let msg = message_override(ctx.messages, "rangelength")
349 .map(str::to_string)
350 .unwrap_or_else(|| format!("Please enter a value between {min} and {max} characters."));
351 Some(fill(&msg, &min.to_string(), Some(&max.to_string())))
352}
353
354fn rule_number(ctx: &RuleContext) -> Option<String> {
359 if ctx.rule_param != &Value::Bool(true) || is_empty(ctx.value) {
360 return None;
361 }
362 if numeric_value(ctx.value).is_some() {
363 return None;
364 }
365 Some(
366 message_override(ctx.messages, "number")
367 .unwrap_or("Please enter a valid number.")
368 .to_string(),
369 )
370}
371
372fn rule_digits(ctx: &RuleContext) -> Option<String> {
373 if ctx.rule_param != &Value::Bool(true) || is_empty(ctx.value) {
374 return None;
375 }
376 if is_digits_only(ctx.value) {
377 return None;
378 }
379 Some(
380 message_override(ctx.messages, "digits")
381 .unwrap_or("Please enter only digits.")
382 .to_string(),
383 )
384}
385
386fn rule_condition_tree(ctx: &RuleContext) -> Option<String> {
389 if !super::condition_tree::is_parameter(ctx.rule_param) || is_empty(ctx.value) {
390 return None;
391 }
392 let judge = ctx.rule_param.as_str() == Some("judge");
393 if super::condition_tree::is_condition(ctx.value, judge, 1) {
394 return None;
395 }
396 Some(
397 message_override(ctx.messages, "conditionTree")
398 .unwrap_or("Please enter a valid condition.")
399 .to_string(),
400 )
401}
402
403fn is_digits_only(value: &Value) -> bool {
405 let text = match value {
406 Value::String(text) => trim(text).to_string(),
407 Value::Number(_) => match canonical_text(value) {
408 Some(text) => text.into_owned(),
409 None => return false,
410 },
411 _ => return false,
412 };
413 !text.is_empty() && text.bytes().all(|b| b.is_ascii_digit())
414}
415
416fn rule_min(ctx: &RuleContext) -> Option<String> {
421 let Parameter::Bound(min) = *ctx.parameter else {
422 unreachable!("min receives a checked bound")
423 };
424 if is_empty(ctx.value) || numeric_value(ctx.value).is_some_and(|number| number >= min) {
425 return None;
426 }
427 let msg = message_override(ctx.messages, "min")
428 .unwrap_or("Please enter a value greater than or equal to {0}.");
429 Some(fill(msg, &number_text(min), None))
430}
431
432fn rule_max(ctx: &RuleContext) -> Option<String> {
433 let Parameter::Bound(max) = *ctx.parameter else {
434 unreachable!("max receives a checked bound")
435 };
436 if is_empty(ctx.value) || numeric_value(ctx.value).is_some_and(|number| number <= max) {
437 return None;
438 }
439 let msg = message_override(ctx.messages, "max")
440 .unwrap_or("Please enter a value less than or equal to {0}.");
441 Some(fill(msg, &number_text(max), None))
442}
443
444fn rule_range(ctx: &RuleContext) -> Option<String> {
445 let Parameter::Bounds(min, max) = *ctx.parameter else {
446 unreachable!("range receives checked bounds")
447 };
448 if is_empty(ctx.value)
449 || numeric_value(ctx.value).is_some_and(|number| min <= number && number <= max)
450 {
451 return None;
452 }
453 let msg = message_override(ctx.messages, "range")
454 .unwrap_or("Please enter a value between {0} and {1}.");
455 Some(fill(msg, &number_text(min), Some(&number_text(max))))
456}
457
458fn rule_step(ctx: &RuleContext) -> Option<String> {
459 let Parameter::Step(step) = *ctx.parameter else {
460 unreachable!("step receives a checked step")
461 };
462 if is_empty(ctx.value)
463 || numeric_value(ctx.value).is_some_and(|number| is_multiple(number, step))
464 {
465 return None;
466 }
467 let msg = message_override(ctx.messages, "step")
468 .unwrap_or("Please enter a value that is a multiple of {0}.");
469 Some(fill(msg, &number_text(step), None))
470}
471
472fn rule_match(ctx: &RuleContext) -> Option<String> {
477 let Parameter::Pattern(program) = ctx.parameter else {
478 unreachable!("pattern and match receive a compiled pattern")
479 };
480 if is_empty(ctx.value) {
481 return None;
482 }
483 if canonical_text(ctx.value).is_some_and(|text| program.is_match(&text)) {
485 return None;
486 }
487 let msg =
489 message_override(ctx.messages, ctx.rule_name).unwrap_or("Please enter a valid format.");
490 Some(msg.to_string())
491}
492
493fn rule_in(ctx: &RuleContext) -> Option<String> {
498 let Parameter::Members(members) = ctx.parameter else {
499 unreachable!("in receives checked members")
500 };
501 if is_empty(ctx.value) || is_member(ctx.value, members) {
502 return None;
503 }
504 Some(
505 message_override(ctx.messages, "in")
506 .unwrap_or("Please select a valid option.")
507 .to_string(),
508 )
509}
510
511fn rule_reference(ctx: &RuleContext) -> Option<String> {
518 let Value::String(path) = ctx.rule_param else {
519 unreachable!("reference receives a checked declaration path")
520 };
521 if is_empty(ctx.value) {
522 return None;
523 }
524 let mut cache = ctx.run.reference_ids.borrow_mut();
525 let ids = cache
526 .entry(path.clone())
527 .or_insert_with(|| reference_ids(ctx.run.properties, ctx.form_data, path));
528 if is_member(ctx.value, ids) {
529 return None;
530 }
531 Some(
532 message_override(ctx.messages, "reference")
533 .unwrap_or("Please select an existing value.")
534 .to_string(),
535 )
536}
537
538fn referenced_value<'a>(ctx: &RuleContext<'a>) -> Option<&'a Value> {
545 if ctx.rule_param.is_null() || is_empty(ctx.value) {
546 return None;
547 }
548 let param = match ctx.rule_param {
549 Value::String(s) => s.clone(),
550 other => js_string(other),
551 };
552 Some(resolve_field_reference(¶m, ctx.path_segments, ctx.row_keys, ctx.form_data))
553}
554
555fn rule_equal_to(ctx: &RuleContext) -> Option<String> {
556 let target = referenced_value(ctx)?;
557 if !strict_eq(ctx.value, target) {
558 return Some(
559 message_override(ctx.messages, "equalTo")
560 .unwrap_or("Please enter the same value again.")
561 .to_string(),
562 );
563 }
564 None
565}
566
567fn rule_not_equal(ctx: &RuleContext) -> Option<String> {
568 if ctx.rule_param.is_null() {
569 return None;
570 }
571 if is_empty(ctx.value) {
572 return None;
573 }
574 let compare = match ctx.rule_param {
575 Value::String(s) if s.starts_with('.') => {
576 resolve_field_reference(s, ctx.path_segments, ctx.row_keys, ctx.form_data)
577 }
578 other => other,
579 };
580 if strict_eq(ctx.value, compare) {
581 return Some(
582 message_override(ctx.messages, "notEqual")
583 .unwrap_or("Please enter a different value.")
584 .to_string(),
585 );
586 }
587 None
588}
589
590fn rule_contained_in(ctx: &RuleContext) -> Option<String> {
593 let target = referenced_value(ctx)?;
594 if let (Value::String(text), Value::String(within)) = (ctx.value, target) {
595 if within.contains(text.as_str()) {
596 return None;
597 }
598 }
599 Some(
600 message_override(ctx.messages, "containedIn")
601 .unwrap_or("Please enter a value that the referenced field contains.")
602 .to_string(),
603 )
604}
605
606fn rule_type_from(ctx: &RuleContext) -> Option<String> {
609 let declared = referenced_value(ctx)?;
610 let typed = match (declared, ctx.value) {
611 (Value::String(name), Value::Bool(_)) => name == "bool",
612 (Value::String(name), Value::Number(number)) => {
613 name == "int" && number.as_f64().is_some_and(|n| n.is_finite() && n.fract() == 0.0)
614 }
615 (Value::String(name), Value::String(_)) => name == "enum",
616 _ => false,
617 };
618 if typed {
619 return None;
620 }
621 Some(
622 message_override(ctx.messages, "typeFrom")
623 .unwrap_or("Please enter a value of the declared type.")
624 .to_string(),
625 )
626}
627
628fn rule_min_from(ctx: &RuleContext) -> Option<String> {
630 bound_from(ctx, "minFrom", "Please enter a value at or above the declared minimum.", |value, bound| value >= bound)
631}
632
633fn rule_max_from(ctx: &RuleContext) -> Option<String> {
635 bound_from(ctx, "maxFrom", "Please enter a value at or below the declared maximum.", |value, bound| value <= bound)
636}
637
638fn finite_bound(value: &Value) -> Option<f64> {
640 match value {
641 Value::Number(number) => number.as_f64().filter(|n| n.is_finite()),
642 _ => None,
643 }
644}
645
646fn bound_from(ctx: &RuleContext, rule: &str, fallback: &str, within: fn(f64, f64) -> bool) -> Option<String> {
647 let bound = referenced_value(ctx)?;
648 let number = finite_bound(ctx.value);
649 let holds = match (number, bound) {
650 (Some(_), Value::Null) => true,
651 (Some(value), other) => finite_bound(other).is_some_and(|limit| within(value, limit)),
652 (None, _) => false,
653 };
654 if holds {
655 return None;
656 }
657 Some(message_override(ctx.messages, rule).unwrap_or(fallback).to_string())
658}
659
660fn strict_eq(a: &Value, b: &Value) -> bool {
663 match (a, b) {
664 (Value::Number(x), Value::Number(y)) => numbers_equal(x, y),
665 _ => {
666 a == b
669 }
670 }
671}
672
673fn rule_date(ctx: &RuleContext) -> Option<String> {
678 if ctx.rule_param == &Value::Bool(false) {
679 return None;
680 }
681 if is_empty(ctx.value) {
682 return None;
683 }
684 if !is_valid_date(ctx.value) {
685 return Some(
686 message_override(ctx.messages, "date")
687 .unwrap_or("Please enter a valid date.")
688 .to_string(),
689 );
690 }
691 None
692}
693
694fn is_valid_date(value: &Value) -> bool {
697 match value {
698 Value::String(s) => is_explicit_date(trim(s)),
699 _ => false,
700 }
701}
702
703fn is_explicit_date(s: &str) -> bool {
706 match s.split_once('T') {
707 None => is_calendar_date(s),
708 Some((date, time)) => is_iso_calendar(date) && is_rfc3339_time(time),
709 }
710}
711
712fn is_calendar_date(s: &str) -> bool {
714 if is_iso_calendar(s) {
715 return true;
716 }
717 let parts: Vec<&str> = s.split('/').collect();
718 let digits = |part: &str| part.bytes().all(|b| b.is_ascii_digit());
719 match parts.as_slice() {
720 [y, m, d] if y.len() == 4 && m.len() == 2 && d.len() == 2 && [y, m, d].iter().all(|p| digits(p)) => {
721 let (year, month, day) = (parse_part(y), parse_part(m), parse_part(d));
722 valid_ymd(year, month, day)
723 }
724 [a, b, y] if a.len() == 2 && b.len() == 2 && y.len() == 4 && [a, b, y].iter().all(|p| digits(p)) => {
725 let year = parse_part(y);
726 valid_ymd(year, parse_part(a), parse_part(b)) || valid_ymd(year, parse_part(b), parse_part(a))
728 }
729 _ => false,
730 }
731}
732
733fn is_iso_calendar(s: &str) -> bool {
735 is_valid_date_iso(&Value::String(s.to_string()))
736}
737
738fn parse_part(part: &str) -> i64 {
740 part.parse().expect("the caller checked the part is ASCII digits")
741}
742
743fn valid_ymd(year: i64, month: i64, day: i64) -> bool {
744 (1..=12).contains(&month) && day >= 1 && day <= days_in_month(year, month)
745}
746
747fn is_rfc3339_time(time: &str) -> bool {
749 let bytes = time.as_bytes();
750 if bytes.len() < 9 {
751 return false;
752 }
753 let clock = &bytes[..8];
754 let shape = clock[2] == b':' && clock[5] == b':' && [0, 1, 3, 4, 6, 7].iter().all(|&i| clock[i].is_ascii_digit());
755 if !shape {
756 return false;
757 }
758 let (hour, minute, second) = (parse_bytes(&clock[0..2]), parse_bytes(&clock[3..5]), parse_bytes(&clock[6..8]));
759 if hour > 23 || minute > 59 || second > 59 {
760 return false;
761 }
762 match &bytes[8..] {
763 b"Z" => true,
764 [sign, rest @ ..] if (*sign == b'+' || *sign == b'-') && rest.len() == 5 && rest[2] == b':' => {
765 let digit = |i: usize| rest[i].is_ascii_digit();
766 digit(0) && digit(1) && digit(3) && digit(4) && parse_bytes(&rest[0..2]) <= 23 && parse_bytes(&rest[3..5]) <= 59
767 }
768 _ => false,
769 }
770}
771
772fn parse_bytes(digits: &[u8]) -> i64 {
774 digits.iter().fold(0, |value, &b| value * 10 + i64::from(b - b'0'))
775}
776
777fn parse_date(s: &str) -> Option<i64> {
783 if s.is_empty() {
784 return None;
785 }
786 let sep = if s.contains('-') {
788 '-'
789 } else if s.contains('/') {
790 '/'
791 } else {
792 return Some(0);
794 };
795 let parts: Vec<&str> = s.split(sep).collect();
796 if parts.len() == 3 {
797 if let (Ok(y), Ok(m), Ok(d)) = (
798 parts[0].parse::<i64>(),
799 parts[1].parse::<i64>(),
800 parts[2].parse::<i64>(),
801 ) {
802 if (1..=12).contains(&m) && (1..=31).contains(&d) {
803 return Some(y * 10000 + m * 100 + d);
804 }
805 }
806 }
807 Some(0)
809}
810
811fn rule_date_iso(ctx: &RuleContext) -> Option<String> {
812 if ctx.rule_param == &Value::Bool(false) {
813 return None;
814 }
815 if is_empty(ctx.value) {
816 return None;
817 }
818 if !is_valid_date_iso(ctx.value) {
819 return Some(
820 message_override(ctx.messages, "dateISO")
821 .unwrap_or("Please enter a valid date in ISO format (YYYY-MM-DD).")
822 .to_string(),
823 );
824 }
825 None
826}
827
828fn is_valid_date_iso(value: &Value) -> bool {
829 let s = match value {
830 Value::String(s) => trim(s),
831 _ => return false,
832 };
833 let bytes = s.as_bytes();
834 if bytes.len() != 10 {
835 return false;
836 }
837 let ok_shape = bytes[..4].iter().all(|b| b.is_ascii_digit())
839 && bytes[4] == b'-'
840 && bytes[5].is_ascii_digit()
841 && bytes[6].is_ascii_digit()
842 && bytes[7] == b'-'
843 && bytes[8].is_ascii_digit()
844 && bytes[9].is_ascii_digit();
845 if !ok_shape {
846 return false;
847 }
848 let year: i64 = s[0..4].parse().expect("four ASCII digits parse as i64");
850 let month: i64 = s[5..7].parse().expect("two ASCII digits parse as i64");
851 let day: i64 = s[8..10].parse().expect("two ASCII digits parse as i64");
852 if !(1..=12).contains(&month) {
853 return false;
854 }
855 let dim = days_in_month(year, month);
856 day >= 1 && day <= dim
857}
858
859fn days_in_month(year: i64, month: i64) -> i64 {
860 match month {
861 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
862 4 | 6 | 9 | 11 => 30,
863 2 => {
864 let leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
865 if leap {
866 29
867 } else {
868 28
869 }
870 }
871 _ => 0,
872 }
873}
874
875fn rule_enddate(ctx: &RuleContext) -> Option<String> {
876 if ctx.rule_param.is_null() {
877 return None;
878 }
879 if is_empty(ctx.value) {
880 return None;
881 }
882 let end = match ctx.value {
883 Value::String(s) => parse_date(trim(s))?,
884 Value::Number(_) => 0,
885 _ => return None,
886 };
887 let param = js_string(ctx.rule_param);
888 let start_value = if param.starts_with('.') {
889 resolve_field_reference(¶m, ctx.path_segments, ctx.row_keys, ctx.form_data)
891 } else {
892 let segments: Vec<String> = param
894 .split('.')
895 .filter(|s| !s.is_empty())
896 .map(String::from)
897 .collect();
898 get_value_by_path(ctx.form_data, &segments)
899 };
900 if is_empty(start_value) {
901 return None;
902 }
903 let start = match start_value {
904 Value::String(s) => parse_date(trim(s))?,
905 Value::Number(_) => 0,
906 _ => return None,
907 };
908 if end < start {
909 return Some(
910 message_override(ctx.messages, "enddate")
911 .unwrap_or("End date must be after the start date.")
912 .to_string(),
913 );
914 }
915 None
916}
917
918fn file_accepted(file: &Map<String, Value>, accept_list: &[String]) -> bool {
925 let mime = file
926 .get("type")
927 .or_else(|| file.get("mimeType"))
928 .and_then(Value::as_str)
929 .unwrap_or("");
930 let name = file
931 .get("name")
932 .or_else(|| file.get("filename"))
933 .and_then(Value::as_str)
934 .unwrap_or("");
935 (!mime.is_empty() && matches_mime_type(mime, accept_list))
936 || (!name.is_empty() && matches_extension(name, accept_list))
937}
938
939fn rule_accept(ctx: &RuleContext) -> Option<String> {
940 if ctx.rule_param.is_null() || ctx.rule_param == &Value::Bool(false) {
941 return None;
942 }
943 if is_empty(ctx.value) {
944 return None;
945 }
946 let accept_list = parse_accept_param(ctx.rule_param);
947 if accept_list.is_empty() {
948 return None;
949 }
950
951 let fail = || {
952 Some(
953 message_override(ctx.messages, "accept")
954 .unwrap_or("Please upload a file with a valid format.")
955 .to_string(),
956 )
957 };
958
959 match ctx.value {
960 Value::String(s) => {
961 if s.contains('/') {
962 if !matches_mime_type(s, &accept_list) {
963 return fail();
964 }
965 } else if !matches_extension(s, &accept_list) {
966 return fail();
967 }
968 None
969 }
970 Value::Array(items) => {
971 for file in items {
972 if let Value::Object(obj) = file {
973 if !file_accepted(obj, &accept_list) {
974 return fail();
975 }
976 }
977 }
978 None
979 }
980 Value::Object(obj) => {
981 if !file_accepted(obj, &accept_list) {
982 return fail();
983 }
984 None
985 }
986 _ => None,
987 }
988}
989
990fn extension_to_mime(ext: &str) -> Option<&'static [&'static str]> {
991 Some(match ext {
992 "jpg" | "jpeg" => &["image/jpeg"],
993 "png" => &["image/png"],
994 "gif" => &["image/gif"],
995 "webp" => &["image/webp"],
996 "svg" => &["image/svg+xml"],
997 "bmp" => &["image/bmp"],
998 "ico" => &["image/x-icon", "image/vnd.microsoft.icon"],
999 "pdf" => &["application/pdf"],
1000 "doc" => &["application/msword"],
1001 "docx" => &["application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
1002 "xls" => &["application/vnd.ms-excel"],
1003 "xlsx" => &["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],
1004 "ppt" => &["application/vnd.ms-powerpoint"],
1005 "pptx" => &["application/vnd.openxmlformats-officedocument.presentationml.presentation"],
1006 "txt" => &["text/plain"],
1007 "csv" => &["text/csv", "application/csv"],
1008 "mp3" => &["audio/mpeg", "audio/mp3"],
1009 "wav" => &["audio/wav", "audio/x-wav"],
1010 "ogg" => &["audio/ogg"],
1011 "flac" => &["audio/flac"],
1012 "mp4" => &["video/mp4"],
1013 "webm" => &["video/webm"],
1014 "avi" => &["video/x-msvideo"],
1015 "mov" => &["video/quicktime"],
1016 "mkv" => &["video/x-matroska"],
1017 "zip" => &["application/zip", "application/x-zip-compressed"],
1018 "rar" => &["application/x-rar-compressed", "application/vnd.rar"],
1019 "tar" => &["application/x-tar"],
1020 "gz" => &["application/gzip"],
1021 "7z" => &["application/x-7z-compressed"],
1022 "json" => &["application/json"],
1023 "xml" => &["application/xml", "text/xml"],
1024 "html" => &["text/html"],
1025 "css" => &["text/css"],
1026 "js" => &["application/javascript", "text/javascript"],
1027 _ => return None,
1028 })
1029}
1030
1031fn parse_accept_param(param: &Value) -> Vec<String> {
1032 let mut out = Vec::new();
1033 match param {
1034 Value::String(s) => {
1035 for raw in s.split(',') {
1036 let part = trim(raw).to_ascii_lowercase();
1037 if let Some(stripped) = part.strip_prefix('.') {
1038 if let Some(mimes) = extension_to_mime(stripped) {
1039 out.extend(mimes.iter().map(|m| m.to_string()));
1040 } else {
1041 out.push(part);
1042 }
1043 } else if part.contains('/') {
1044 out.push(part);
1045 }
1046 }
1047 }
1048 Value::Array(items) => {
1049 for item in items {
1050 out.extend(parse_accept_param(item));
1051 }
1052 }
1053 _ => {}
1054 }
1055 out
1056}
1057
1058fn matches_mime_type(mime: &str, accept_list: &[String]) -> bool {
1059 let normalized = mime.to_ascii_lowercase();
1060 for accept in accept_list {
1061 if accept == "*/*" {
1062 return true;
1063 }
1064 if let Some(prefix) = accept.strip_suffix('*') {
1065 if normalized.starts_with(prefix) {
1067 return true;
1068 }
1069 } else if accept.starts_with('.') {
1070 continue;
1071 } else if &normalized == accept {
1072 return true;
1073 }
1074 }
1075 false
1076}
1077
1078fn matches_extension(filename: &str, accept_list: &[String]) -> bool {
1079 let lower = filename.to_ascii_lowercase();
1080 let parts: Vec<&str> = lower.split('.').collect();
1081 if parts.len() < 2 {
1082 return false;
1083 }
1084 let ext = parts[parts.len() - 1];
1085 if ext.is_empty() {
1086 return false;
1087 }
1088 for accept in accept_list {
1089 if let Some(stripped) = accept.strip_prefix('.') {
1090 if stripped == ext {
1091 return true;
1092 }
1093 }
1094 }
1095 if let Some(mimes) = extension_to_mime(ext) {
1096 for mime in mimes {
1097 if matches_mime_type(mime, accept_list) {
1098 return true;
1099 }
1100 }
1101 }
1102 false
1103}
1104
1105fn rule_mincount(ctx: &RuleContext) -> Option<String> {
1110 let Parameter::Count(min) = *ctx.parameter else {
1111 unreachable!("mincount receives a checked limit")
1112 };
1113 if count(ctx.value) >= min {
1115 return None;
1116 }
1117 let msg =
1118 message_override(ctx.messages, "mincount").unwrap_or("Please select at least {0} items.");
1119 Some(fill(msg, &min.to_string(), None))
1120}
1121
1122fn rule_maxcount(ctx: &RuleContext) -> Option<String> {
1123 let Parameter::Count(max) = *ctx.parameter else {
1124 unreachable!("maxcount receives a checked limit")
1125 };
1126 if count(ctx.value) <= max {
1127 return None;
1128 }
1129 let msg = message_override(ctx.messages, "maxcount")
1130 .unwrap_or("Please select no more than {0} items.");
1131 Some(fill(msg, &max.to_string(), None))
1132}
1133
1134fn count(value: &Value) -> u64 {
1137 match value {
1138 Value::Array(elements) => elements.len() as u64,
1139 Value::Object(keys) => keys.len() as u64,
1140 Value::Null => 0,
1141 Value::String(text) if trim(text).is_empty() => 0,
1142 _ => 1,
1143 }
1144}
1145
1146fn rule_unique(ctx: &RuleContext) -> Option<String> {
1151 if ctx.rule_param == &Value::Bool(false) || ctx.rule_param.is_null() {
1152 return None;
1153 }
1154
1155 let filter_condition: Option<&str> = match ctx.rule_param {
1156 Value::String(s) if is_condition_expression(s) => Some(s.as_str()),
1157 _ => None,
1158 };
1159 let error_message = message_override(ctx.messages, "unique")
1160 .unwrap_or("Values must be unique.")
1161 .to_string();
1162
1163 if ctx.value.is_array() || ctx.value.is_object() {
1165 let entries: Vec<(String, &Value)> = match ctx.value {
1166 Value::Array(arr) => arr
1167 .iter()
1168 .enumerate()
1169 .map(|(i, v)| (i.to_string(), v))
1170 .collect(),
1171 Value::Object(map) => map.iter().map(|(k, v)| (k.clone(), v)).collect(),
1172 _ => unreachable!(),
1173 };
1174 let mut to_check: Vec<&Value> = Vec::new();
1175 if let Some(cond) = filter_condition {
1176 for (key, element) in &entries {
1177 let mut item_path = ctx.path_segments.to_vec();
1178 item_path.push(key.clone());
1179 let mut item_rows = ctx.row_keys.to_vec();
1180 item_rows.push(ctx.path_segments.len());
1181 if !item_passes_condition(cond, &item_path, &item_rows, ctx.form_data) {
1182 continue;
1183 }
1184 if !is_empty(element) {
1185 to_check.push(element);
1186 }
1187 }
1188 } else if let Value::String(field_name) = ctx.rule_param {
1189 let items: Vec<&Value> = entries.iter().map(|(_, value)| *value).collect();
1191 return unique_array_by_field(&items, field_name, &error_message);
1192 } else {
1193 for (_, element) in &entries {
1194 if !is_empty(element) {
1195 to_check.push(element);
1196 }
1197 }
1198 }
1199
1200 if to_check.is_empty() {
1201 return None;
1202 }
1203 if !are_all_unique(&to_check) {
1204 return Some(error_message);
1205 }
1206 return None;
1207 }
1208
1209 if ctx.path_segments.len() < 2 {
1212 return None;
1213 }
1214 let field_name = &ctx.path_segments[ctx.path_segments.len() - 1];
1215 let item_key = &ctx.path_segments[ctx.path_segments.len() - 2];
1216 let container_path = &ctx.path_segments[..ctx.path_segments.len() - 2];
1217 let container = get_value_by_path(ctx.form_data, container_path);
1218 if let Value::Array(_) = container {
1219 if item_key.is_empty() || !item_key.bytes().all(|b| b.is_ascii_digit()) {
1220 return None;
1221 }
1222 } else if !container.is_object() {
1223 return None;
1224 }
1225 if is_empty(ctx.value) {
1226 return None;
1227 }
1228 if let Some(cond) = filter_condition {
1230 if !item_passes_condition(cond, ctx.path_segments, ctx.row_keys, ctx.form_data) {
1231 return None;
1232 }
1233 }
1234
1235 let cache_key = format!(
1238 "{}\u{0}{}\u{0}{}",
1239 container_path.join("\u{0}"),
1240 field_name,
1241 filter_condition.unwrap_or("")
1242 );
1243 let mut memo = ctx.run.unique_duplicates.borrow_mut();
1244 let duplicate_set = memo.entry(cache_key).or_insert_with(|| {
1245 let rows: Vec<(String, &Value)> = match container {
1246 Value::Array(items) => items
1247 .iter()
1248 .enumerate()
1249 .map(|(i, item)| (i.to_string(), item))
1250 .collect(),
1251 Value::Object(rows) => rows.iter().map(|(key, item)| (key.clone(), item)).collect(),
1252 _ => Vec::new(),
1253 };
1254 let mut seen = HashSet::new();
1255 let mut duplicates = HashSet::new();
1256 for (key, item) in rows {
1257 let Value::Object(row) = item else { continue };
1258 let Some(item_value) = row.get(field_name.as_str()) else {
1259 continue;
1260 };
1261 if is_empty(item_value) {
1262 continue;
1263 }
1264 if let Some(cond) = filter_condition {
1265 let mut sibling_path: Vec<String> = container_path.to_vec();
1266 sibling_path.push(key.clone());
1267 sibling_path.push(field_name.clone());
1268 if !item_passes_condition(cond, &sibling_path, ctx.row_keys, ctx.form_data) {
1269 continue;
1270 }
1271 }
1272 if !seen.insert(comparison_key(item_value)) {
1273 duplicates.insert(key);
1274 }
1275 }
1276 duplicates
1277 });
1278
1279 if duplicate_set.contains(item_key) {
1281 return Some(error_message);
1282 }
1283
1284 None
1285}
1286
1287fn unique_array_by_field(items: &[&Value], field_name: &str, error: &str) -> Option<String> {
1289 let segs: Vec<String> = field_name
1290 .split('.')
1291 .filter(|s| !s.is_empty())
1292 .map(String::from)
1293 .collect();
1294 let mut values: Vec<&Value> = Vec::new();
1295 for item in items {
1296 if item.is_object() {
1297 let v = get_value_by_path(item, &segs);
1298 if !is_empty(v) {
1299 values.push(v);
1300 }
1301 }
1302 }
1303 if values.is_empty() {
1304 return None;
1305 }
1306 if !are_all_unique(&values) {
1307 return Some(error.to_string());
1308 }
1309 None
1310}
1311
1312fn comparison_key(value: &Value) -> String {
1316 match value {
1317 Value::Null => "null".to_string(),
1318 Value::Bool(b) => b.to_string(),
1319 Value::Number(n) => number_text(n.as_f64().unwrap_or(0.0)),
1320 Value::String(s) => Value::String(s.clone()).to_string(),
1321 Value::Array(items) => {
1322 let items: Vec<String> = items.iter().map(comparison_key).collect();
1323 format!("[{}]", items.join(","))
1324 }
1325 Value::Object(members) => {
1326 let mut names: Vec<&String> = members.keys().collect();
1327 names.sort();
1328 let members: Vec<String> = names
1329 .into_iter()
1330 .map(|name| {
1331 format!(
1332 "{}:{}",
1333 Value::String(name.clone()),
1334 comparison_key(&members[name])
1335 )
1336 })
1337 .collect();
1338 format!("{{{}}}", members.join(","))
1339 }
1340 }
1341}
1342
1343fn are_all_unique(values: &[&Value]) -> bool {
1344 let mut seen = std::collections::HashSet::new();
1345 for v in values {
1346 let key = comparison_key(v);
1347 if !seen.insert(key) {
1348 return false;
1349 }
1350 }
1351 true
1352}
1353
1354#[allow(clippy::disallowed_methods, reason = "a condition that does not parse is false")]
1357fn item_passes_condition(
1358 condition: &str,
1359 item_field_path: &[String],
1360 row_keys: &[usize],
1361 form_data: &Value,
1362) -> bool {
1363 Expression::evaluate(condition, form_data, item_field_path, row_keys).unwrap_or(false)
1364}
1365
1366pub fn is_condition_expression(value: &str) -> bool {
1372 let trimmed = trim(value);
1373 if trimmed.starts_with('.') {
1374 return true;
1375 }
1376 if starts_identifier_dot(trimmed) {
1378 return true;
1379 }
1380 if has_comparison_operator(trimmed) {
1381 return true;
1382 }
1383 if let Some(q) = trimmed.find('?') {
1385 if trimmed[q + 1..].contains(':') {
1386 return true;
1387 }
1388 }
1389 false
1390}
1391
1392fn starts_identifier_dot(s: &str) -> bool {
1393 let mut chars = s.char_indices();
1394 match chars.next() {
1395 Some((_, c)) if c.is_ascii_alphabetic() || c == '_' => {}
1396 _ => return false,
1397 }
1398 for (_, c) in chars {
1399 if c.is_ascii_alphanumeric() || c == '_' {
1400 continue;
1401 }
1402 return c == '.';
1404 }
1405 false
1406}
1407
1408fn has_comparison_operator(s: &str) -> bool {
1410 let ops = ["==", "!=", ">=", "<=", ">", "<", "&&", "||", "in", "not in"];
1411 let bytes = s.as_bytes();
1412 let is_ws =
1413 |b: u8| b == b' ' || b == b'\t' || b == b'\n' || b == b'\r' || b == 0x0c || b == 0x0b;
1414
1415 for op in ops {
1417 let mut search_from = 0;
1418 while let Some(pos) = s[search_from..].find(op) {
1419 let start = search_from + pos;
1420 let end = start + op.len();
1421 let before_ok = start > 0 && is_ws(bytes[start - 1]);
1423 let after_ok = end < bytes.len() && is_ws(bytes[end]);
1424 if before_ok && after_ok {
1425 return true;
1429 }
1430 search_from = start + 1;
1431 if search_from >= s.len() {
1432 break;
1433 }
1434 }
1435 }
1436 false
1437}
1438
1439pub fn get_value_by_path<'a>(data: &'a Value, path: &[String]) -> &'a Value {
1441 let mut current = data;
1442 for segment in path {
1443 match current {
1444 Value::Object(map) => match map.get(segment) {
1445 Some(v) => current = v,
1446 None => return &Value::Null,
1447 },
1448 Value::Array(arr) => match segment.parse::<usize>() {
1449 Ok(i) => match arr.get(i) {
1450 Some(v) => current = v,
1451 None => return &Value::Null,
1452 },
1453 Err(_) => return &Value::Null,
1454 },
1455 _ => return &Value::Null,
1456 }
1457 }
1458 current
1459}
1460
1461pub fn resolve_field_reference<'a>(
1465 expression: &str,
1466 current_path: &[String],
1467 row_keys: &[usize],
1468 form_data: &'a Value,
1469) -> &'a Value {
1470 let trimmed = expression.trim();
1471 if trimmed.is_empty() {
1472 return &Value::Null;
1473 }
1474
1475 let dots = trimmed.chars().take_while(|c| *c == '.').count();
1477
1478 if dots > 0 || !trimmed.contains('.') {
1479 let segments = trimmed[dots..]
1480 .split('.')
1481 .filter(|s| !s.is_empty())
1482 .map(String::from);
1483 let mut base = relative_base(current_path, row_keys, dots.saturating_sub(1));
1484 base.extend(segments);
1485 return get_value_by_path(form_data, &base);
1486 }
1487
1488 let segments: Vec<String> = trimmed
1490 .split('.')
1491 .filter(|s| !s.is_empty())
1492 .map(String::from)
1493 .collect();
1494 get_value_by_path(form_data, &segments)
1495}
1496
1497#[cfg(test)]
1498mod tests {
1499 use super::*;
1500
1501 #[test]
1502 fn date_rule_accepts_only_the_explicit_layouts() {
1503 for text in ["2026-02-28", "2026/02/28", "28/02/2026", "02/28/2026", "2026-02-28T10:30:00Z", "2026-02-28T10:30:00+01:00"] {
1504 assert!(is_valid_date(&serde_json::json!(text)), "{text} is a date");
1505 }
1506 for text in ["not a date", "2026-02-30", "02/30/2026", "2026-02-28T25:00:00Z", "2026-02-28T10:30:00", "2026-02-28 10:30"] {
1507 assert!(!is_valid_date(&serde_json::json!(text)), "{text} is no date");
1508 }
1509 assert!(!is_valid_date(&serde_json::json!(1_700_000_000_000_i64)), "a number is no date");
1510 }
1511
1512 #[test]
1513 fn test_comparison_key_canonical_json() {
1514 let val1 = serde_json::json!({"a": 1, "b": "test", "c": true});
1516 let val2 = serde_json::json!({"c": true, "a": 1, "b": "test"});
1517
1518 let key1 = comparison_key(&val1);
1519 let key2 = comparison_key(&val2);
1520
1521 assert_eq!(
1522 key1, key2,
1523 "Comparison keys for objects with different key order should be identical"
1524 );
1525 }
1526
1527 #[test]
1528 fn test_comparison_key_type_distinction() {
1529 let tests = vec![
1530 (serde_json::json!(1), serde_json::json!("1"), false), (serde_json::json!(true), serde_json::json!(1), false), (serde_json::json!(null), serde_json::json!("null"), false), (serde_json::json!([]), serde_json::json!("[]"), false), (serde_json::json!(1), serde_json::json!(1), true), (serde_json::json!("1"), serde_json::json!("1"), true), ];
1537
1538 for (val1, val2, should_match) in tests {
1539 let key1 = comparison_key(&val1);
1540 let key2 = comparison_key(&val2);
1541 let keys_match = key1 == key2;
1542
1543 assert_eq!(
1544 keys_match, should_match,
1545 "comparison_key({:?}) vs comparison_key({:?}): got {}, want {}",
1546 val1, val2, keys_match, should_match
1547 );
1548 }
1549 }
1550
1551 #[test]
1555 fn test_unique_many_values() {
1556 let mut values: Vec<Value> = (0..100_000)
1557 .map(|i| Value::String(format!("item-{i}")))
1558 .collect();
1559 assert!(are_all_unique(&values.iter().collect::<Vec<_>>()));
1560 values[99_999] = Value::String("item-0".into());
1561 assert!(!are_all_unique(&values.iter().collect::<Vec<_>>()));
1562 }
1563}
1564
1565#[cfg(test)]
1566mod email_label_tests {
1567 #[test]
1568 fn a_one_character_label_is_valid_after_the_boundaries() {
1569 assert!(super::is_valid_email_label("a"));
1572 assert!(super::is_valid_email_label("ab"));
1573 assert!(!super::is_valid_email_label("-"));
1574 assert!(!super::is_valid_email_label("a-"));
1575 }
1576}