1use std::fmt;
14
15use crate::{
16 schema::{AttributeRule, Definition, DEFINITIONS, GLOBAL_ROOTS},
17 Document, Element, Node, NAMESPACE,
18};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25#[non_exhaustive]
26pub enum SchemaViolationCode {
27 UnknownRoot,
29 UnexpectedElement,
31 MissingRequiredChild,
33 TooManyOccurrences,
35 OutOfOrderChild,
37 MissingChoiceBranch,
39 MissingRequiredAttribute,
41 UnknownAttribute,
43 InvalidLexicalValue,
45 InvalidEnumerationValue,
47 PatternMismatch,
49 UnexpectedText,
51 ForeignNamespace,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct SchemaViolation {
58 code: SchemaViolationCode,
59 path: String,
61 message: String,
62}
63
64impl SchemaViolation {
65 #[must_use]
66 pub const fn code(&self) -> SchemaViolationCode {
67 self.code
68 }
69
70 #[must_use]
71 pub fn path(&self) -> &str {
72 &self.path
73 }
74
75 #[must_use]
76 pub fn message(&self) -> &str {
77 &self.message
78 }
79}
80
81impl fmt::Display for SchemaViolation {
82 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83 write!(
84 formatter,
85 "{:?} at {}: {}",
86 self.code, self.path, self.message
87 )
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Default)]
93pub struct SchemaReport {
94 violations: Vec<SchemaViolation>,
95}
96
97impl SchemaReport {
98 #[must_use]
100 pub fn is_conforming(&self) -> bool {
101 self.violations.is_empty()
102 }
103
104 #[must_use]
105 pub fn violations(&self) -> &[SchemaViolation] {
106 &self.violations
107 }
108
109 fn push(&mut self, code: SchemaViolationCode, path: &str, message: impl Into<String>) {
110 self.violations.push(SchemaViolation {
111 code,
112 path: path.to_owned(),
113 message: message.into(),
114 });
115 }
116}
117
118impl Document {
119 #[must_use]
126 pub fn validate_schema(&self) -> SchemaReport {
127 let mut report = SchemaReport::default();
128 let root = self.root();
129
130 if root.namespace_uri() != Some(NAMESPACE) {
131 report.push(
132 SchemaViolationCode::ForeignNamespace,
133 &format!("/{}", root.local_name()),
134 format!(
135 "root element is in namespace {:?}, not the ISO 23387 namespace",
136 root.namespace_uri().unwrap_or("(none)")
137 ),
138 );
139 return report;
140 }
141
142 let Some(definition) = global_root_definition(root.local_name()) else {
143 report.push(
144 SchemaViolationCode::UnknownRoot,
145 &format!("/{}", root.local_name()),
146 format!(
147 "`{}` is not a global root element declared by the schema",
148 root.local_name()
149 ),
150 );
151 return report;
152 };
153
154 let path = format!("/{}", root.local_name());
155 validate_element(root, definition, &path, &mut report);
156 report
157 }
158}
159
160fn global_root_definition(local_name: &str) -> Option<&'static Definition> {
161 GLOBAL_ROOTS
162 .iter()
163 .find(|(name, _)| *name == local_name)
164 .map(|(_, index)| &DEFINITIONS[*index])
165}
166
167fn validate_element(
168 element: &Element,
169 definition: &'static Definition,
170 path: &str,
171 report: &mut SchemaReport,
172) {
173 validate_attributes(element, definition, path, report);
174 validate_content(element, definition, path, report);
175}
176
177fn validate_attributes(
178 element: &Element,
179 definition: &'static Definition,
180 path: &str,
181 report: &mut SchemaReport,
182) {
183 for attribute in element.attributes() {
184 if attribute.qname() == "xmlns" || attribute.prefix() == Some("xmlns") {
186 continue;
187 }
188 if matches!(
190 attribute.namespace_uri(),
191 Some("http://www.w3.org/XML/1998/namespace")
192 | Some("http://www.w3.org/2001/XMLSchema-instance")
193 ) {
194 continue;
195 }
196
197 let applies =
199 attribute.namespace_uri().is_none() || attribute.namespace_uri() == Some(NAMESPACE);
200 if !applies {
201 continue;
202 }
203
204 match find_attribute(definition, attribute.local_name()) {
205 Some(rule) => validate_simple_value(
206 attribute.value(),
207 rule.data_type,
208 rule.pattern,
209 rule.enum_values,
210 &format!("{path}/@{}", attribute.local_name()),
211 report,
212 ),
213 None => report.push(
214 SchemaViolationCode::UnknownAttribute,
215 path,
216 format!(
217 "attribute `{}` is not declared on `{}`",
218 attribute.local_name(),
219 definition.handle
220 ),
221 ),
222 }
223 }
224
225 for rule in definition.attributes.iter().filter(|rule| rule.required) {
226 let present = element.attributes().iter().any(|attribute| {
227 attribute.local_name() == rule.name
228 && (attribute.namespace_uri().is_none()
229 || attribute.namespace_uri() == Some(NAMESPACE))
230 });
231 if !present {
232 report.push(
233 SchemaViolationCode::MissingRequiredAttribute,
234 path,
235 format!("required attribute `{}` is missing", rule.name),
236 );
237 }
238 }
239}
240
241fn find_attribute(definition: &'static Definition, name: &str) -> Option<&'static AttributeRule> {
242 definition.attributes.iter().find(|rule| rule.name == name)
243}
244
245fn validate_content(
246 element: &Element,
247 definition: &'static Definition,
248 path: &str,
249 report: &mut SchemaReport,
250) {
251 let element_only = !definition.children.is_empty();
252
253 if element_only {
254 for node in element.nodes() {
255 let has_text = match node {
256 Node::Text(text) => !text.chars().all(is_xml_whitespace),
257 Node::CData(text) => !text.is_empty(),
258 _ => false,
259 };
260 if has_text {
261 report.push(
262 SchemaViolationCode::UnexpectedText,
263 path,
264 format!(
265 "`{}` has element-only content but carries character data",
266 definition.handle
267 ),
268 );
269 break;
270 }
271 }
272 } else if let Some(data_type) = definition.data_type {
273 validate_simple_value(
274 &element.direct_text(),
275 data_type,
276 definition.pattern,
277 definition.enum_values,
278 path,
279 report,
280 );
281 }
282
283 let mut counts = vec![0_u32; definition.children.len()];
287 let mut cursor = 0_usize;
288
289 for child in element.children() {
290 if child.namespace_uri() != Some(NAMESPACE) {
291 continue;
294 }
295
296 let child_path = format!("{path}/{}", child.local_name());
297 let Some(position) = definition
298 .children
299 .iter()
300 .position(|rule| rule.name == child.local_name())
301 else {
302 report.push(
303 SchemaViolationCode::UnexpectedElement,
304 &child_path,
305 format!(
306 "`{}` is not declared as a child of `{}`",
307 child.local_name(),
308 definition.handle
309 ),
310 );
311 continue;
312 };
313
314 let rule = &definition.children[position];
315
316 if position < cursor && !shares_choice_group(definition, position, cursor) {
317 report.push(
318 SchemaViolationCode::OutOfOrderChild,
319 &child_path,
320 format!(
321 "`{}` appears after content the schema declares later in the sequence",
322 child.local_name()
323 ),
324 );
325 } else {
326 cursor = position;
327 }
328
329 counts[position] += 1;
330 if let Some(max) = rule.max_occurs {
331 if counts[position] == max + 1 {
332 report.push(
333 SchemaViolationCode::TooManyOccurrences,
334 &child_path,
335 format!(
336 "`{}` occurs more than the permitted {max} time(s)",
337 child.local_name()
338 ),
339 );
340 }
341 }
342
343 validate_element(child, &DEFINITIONS[rule.definition], &child_path, report);
344 }
345
346 for (position, rule) in definition.children.iter().enumerate() {
347 if rule.choice_group.is_some() {
349 continue;
350 }
351 if counts[position] < rule.min_occurs {
352 report.push(
353 SchemaViolationCode::MissingRequiredChild,
354 path,
355 format!(
356 "`{}` requires at least {} `{}` child element(s) but found {}",
357 definition.handle, rule.min_occurs, rule.name, counts[position]
358 ),
359 );
360 }
361 }
362
363 for (group_index, group) in definition.choice_groups.iter().enumerate() {
364 let total: u32 = definition
365 .children
366 .iter()
367 .enumerate()
368 .filter(|(_, rule)| rule.choice_group == Some(group_index))
369 .map(|(position, _)| counts[position])
370 .sum();
371
372 if total < group.min_occurs {
373 report.push(
374 SchemaViolationCode::MissingChoiceBranch,
375 path,
376 format!(
377 "`{}` requires at least {} of ({}) but found {total}",
378 definition.handle,
379 group.min_occurs,
380 group.members.join(" | ")
381 ),
382 );
383 }
384 if let Some(max) = group.max_occurs {
385 if total > max {
386 report.push(
387 SchemaViolationCode::TooManyOccurrences,
388 path,
389 format!(
390 "`{}` permits at most {max} of ({}) but found {total}",
391 definition.handle,
392 group.members.join(" | ")
393 ),
394 );
395 }
396 }
397 }
398}
399
400fn shares_choice_group(definition: &'static Definition, left: usize, right: usize) -> bool {
403 let left_group = definition.children.get(left).and_then(|r| r.choice_group);
404 let right_group = definition.children.get(right).and_then(|r| r.choice_group);
405 left_group.is_some() && left_group == right_group
406}
407
408const fn is_xml_whitespace(character: char) -> bool {
411 matches!(character, '\u{0009}' | '\u{000A}' | '\u{000D}' | '\u{0020}')
412}
413
414fn collapse(value: &str) -> String {
415 value
416 .split(is_xml_whitespace)
417 .filter(|part| !part.is_empty())
418 .collect::<Vec<_>>()
419 .join(" ")
420}
421
422fn validate_simple_value(
423 raw: &str,
424 data_type: &str,
425 pattern: Option<&str>,
426 enum_values: &[&str],
427 path: &str,
428 report: &mut SchemaReport,
429) {
430 let value = if data_type == "xs:string" && pattern.is_none() && enum_values.is_empty() {
433 raw.to_owned()
434 } else {
435 collapse(raw)
436 };
437
438 if !enum_values.is_empty() && !enum_values.contains(&value.as_str()) {
439 report.push(
440 SchemaViolationCode::InvalidEnumerationValue,
441 path,
442 format!(
443 "`{value}` is not one of the permitted values ({})",
444 enum_values.join(", ")
445 ),
446 );
447 return;
448 }
449
450 if let Some(pattern) = pattern {
451 if !matches_pattern(pattern, &value) {
452 report.push(
453 SchemaViolationCode::PatternMismatch,
454 path,
455 format!("`{value}` does not match the declared pattern"),
456 );
457 return;
458 }
459 }
460
461 if !lexical_space_admits(data_type, &value) {
462 report.push(
463 SchemaViolationCode::InvalidLexicalValue,
464 path,
465 format!("`{value}` is not a valid `{data_type}` value"),
466 );
467 }
468}
469
470fn lexical_space_admits(data_type: &str, value: &str) -> bool {
476 match data_type {
477 "xs:string" | "xs:anySimpleType" | "xs:anyURI" => true,
478 "xs:boolean" => matches!(value, "true" | "false" | "1" | "0"),
479 "xs:int" => value.parse::<i32>().is_ok(),
480 "xs:integer" => is_integer(value),
481 "xs:positiveInteger" => is_integer(value) && !is_non_positive(value),
482 "xs:decimal" => is_decimal(value),
483 "xs:double" | "xs:float" => {
484 matches!(value, "INF" | "-INF" | "NaN") || value.parse::<f64>().is_ok()
485 }
486 "xs:dateTime" => is_date_time(value),
487 "xs:language" => is_language(value),
488 _ => true,
489 }
490}
491
492fn is_integer(value: &str) -> bool {
493 let digits = value.strip_prefix(['+', '-']).unwrap_or(value);
494 !digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit())
495}
496
497fn is_non_positive(value: &str) -> bool {
498 let (negative, digits) = match value.strip_prefix('-') {
499 Some(rest) => (true, rest),
500 None => (false, value.strip_prefix('+').unwrap_or(value)),
501 };
502 negative || digits.bytes().all(|byte| byte == b'0')
503}
504
505fn is_decimal(value: &str) -> bool {
506 let body = value.strip_prefix(['+', '-']).unwrap_or(value);
507 let (whole, fraction) = match body.split_once('.') {
508 Some((whole, fraction)) => (whole, Some(fraction)),
509 None => (body, None),
510 };
511 if !whole.bytes().all(|byte| byte.is_ascii_digit()) {
512 return false;
513 }
514 match fraction {
515 Some(fraction) => {
517 fraction.bytes().all(|byte| byte.is_ascii_digit())
518 && !(whole.is_empty() && fraction.is_empty())
519 }
520 None => !whole.is_empty(),
521 }
522}
523
524fn is_language(value: &str) -> bool {
526 let mut parts = value.split('-');
527 let Some(primary) = parts.next() else {
528 return false;
529 };
530 if primary.is_empty() || primary.len() > 8 || !primary.bytes().all(|b| b.is_ascii_alphabetic())
531 {
532 return false;
533 }
534 parts.all(|part| {
535 !part.is_empty() && part.len() <= 8 && part.bytes().all(|b| b.is_ascii_alphanumeric())
536 })
537}
538
539fn is_date_time(value: &str) -> bool {
541 let body = value.strip_prefix('-').unwrap_or(value);
542 let Some((date, rest)) = body.split_once('T') else {
543 return false;
544 };
545
546 let date_parts: Vec<&str> = date.split('-').collect();
547 if date_parts.len() != 3 {
548 return false;
549 }
550 let (year, month, day) = (date_parts[0], date_parts[1], date_parts[2]);
551 if year.len() < 4 || !year.bytes().all(|b| b.is_ascii_digit()) {
552 return false;
553 }
554 if year.len() > 4 && year.starts_with('0') {
556 return false;
557 }
558 if !is_fixed_digits(month, 2) || !is_fixed_digits(day, 2) {
559 return false;
560 }
561 let month_value: u32 = month.parse().unwrap_or(0);
562 let day_value: u32 = day.parse().unwrap_or(0);
563 if !(1..=12).contains(&month_value) || day_value < 1 {
564 return false;
565 }
566 let year_value: i64 = year.parse().unwrap_or(0);
567 if day_value > days_in_month(year_value, month_value) {
568 return false;
569 }
570
571 let (time, zone) = if let Some(stripped) = rest.strip_suffix('Z') {
573 (stripped, None)
574 } else if let Some(position) = rest.rfind(['+', '-']) {
575 (&rest[..position], Some(&rest[position + 1..]))
576 } else {
577 (rest, None)
578 };
579
580 if let Some(zone) = zone {
581 let Some((hours, minutes)) = zone.split_once(':') else {
582 return false;
583 };
584 if !is_fixed_digits(hours, 2) || !is_fixed_digits(minutes, 2) {
585 return false;
586 }
587 let hour_value: u32 = hours.parse().unwrap_or(99);
588 let minute_value: u32 = minutes.parse().unwrap_or(99);
589 if hour_value > 14 || minute_value > 59 || (hour_value == 14 && minute_value != 0) {
590 return false;
591 }
592 }
593
594 let time_parts: Vec<&str> = time.split(':').collect();
595 if time_parts.len() != 3 {
596 return false;
597 }
598 if !is_fixed_digits(time_parts[0], 2) || !is_fixed_digits(time_parts[1], 2) {
599 return false;
600 }
601 let (seconds, fraction) = match time_parts[2].split_once('.') {
602 Some((seconds, fraction)) => (seconds, Some(fraction)),
603 None => (time_parts[2], None),
604 };
605 if !is_fixed_digits(seconds, 2) {
606 return false;
607 }
608 if let Some(fraction) = fraction {
609 if fraction.is_empty() || !fraction.bytes().all(|b| b.is_ascii_digit()) {
610 return false;
611 }
612 }
613
614 let hour_value: u32 = time_parts[0].parse().unwrap_or(99);
615 let minute_value: u32 = time_parts[1].parse().unwrap_or(99);
616 let second_value: u32 = seconds.parse().unwrap_or(99);
617 if minute_value > 59 || second_value > 59 {
618 return false;
619 }
620 if hour_value == 24 {
622 return minute_value == 0 && second_value == 0;
623 }
624 hour_value <= 23
625}
626
627fn is_fixed_digits(value: &str, count: usize) -> bool {
628 value.len() == count && value.bytes().all(|byte| byte.is_ascii_digit())
629}
630
631const fn is_leap_year(year: i64) -> bool {
632 (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
633}
634
635const fn days_in_month(year: i64, month: u32) -> u32 {
636 match month {
637 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
638 4 | 6 | 9 | 11 => 30,
639 2 if is_leap_year(year) => 29,
640 2 => 28,
641 _ => 0,
642 }
643}
644
645fn matches_pattern(pattern: &str, value: &str) -> bool {
655 let tokens: Vec<char> = pattern.chars().collect();
656 let input: Vec<char> = value.chars().collect();
657 match parse_alternation(&tokens, 0) {
658 Some((node, next)) if next == tokens.len() => node
659 .match_at(&input, 0)
660 .into_iter()
661 .any(|end| end == input.len()),
662 _ => false,
663 }
664}
665
666#[derive(Debug, Clone)]
667enum Pattern {
668 Repeat {
670 inner: Box<Pattern>,
671 min: usize,
672 max: Option<usize>,
673 },
674 Sequence(Vec<Pattern>),
675 Alternation(Vec<Pattern>),
676 Class {
677 negated: bool,
678 items: Vec<ClassItem>,
679 },
680 Literal(char),
681}
682
683#[derive(Debug, Clone, Copy)]
684enum ClassItem {
685 Single(char),
686 Range(char, char),
687}
688
689impl Pattern {
690 fn match_at(&self, input: &[char], start: usize) -> Vec<usize> {
695 match self {
696 Self::Literal(expected) => match input.get(start) {
697 Some(actual) if actual == expected => vec![start + 1],
698 _ => Vec::new(),
699 },
700 Self::Class { negated, items } => match input.get(start) {
701 Some(actual) => {
702 let hit = items.iter().any(|item| match *item {
703 ClassItem::Single(character) => character == *actual,
704 ClassItem::Range(low, high) => (low..=high).contains(actual),
705 });
706 if hit != *negated {
707 vec![start + 1]
708 } else {
709 Vec::new()
710 }
711 }
712 None => Vec::new(),
713 },
714 Self::Sequence(parts) => {
715 let mut positions = vec![start];
716 for part in parts {
717 let mut next = Vec::new();
718 for position in positions {
719 for end in part.match_at(input, position) {
720 if !next.contains(&end) {
721 next.push(end);
722 }
723 }
724 }
725 if next.is_empty() {
726 return Vec::new();
727 }
728 positions = next;
729 }
730 positions
731 }
732 Self::Alternation(branches) => {
733 let mut ends = Vec::new();
734 for branch in branches {
735 for end in branch.match_at(input, start) {
736 if !ends.contains(&end) {
737 ends.push(end);
738 }
739 }
740 }
741 ends
742 }
743 Self::Repeat { inner, min, max } => {
744 let mut ends = Vec::new();
745 let mut frontier = vec![start];
746 let mut count = 0usize;
747 if *min == 0 {
748 ends.push(start);
749 }
750 while !frontier.is_empty() {
751 if let Some(max) = max {
752 if count >= *max {
753 break;
754 }
755 }
756 let mut next = Vec::new();
757 for position in &frontier {
758 for end in inner.match_at(input, *position) {
759 if end != *position && !next.contains(&end) {
761 next.push(end);
762 }
763 }
764 }
765 count += 1;
766 if next.is_empty() {
767 break;
768 }
769 if count >= *min {
770 for end in &next {
771 if !ends.contains(end) {
772 ends.push(*end);
773 }
774 }
775 }
776 frontier = next;
777 }
778 ends
779 }
780 }
781 }
782}
783
784type Parsed = Option<(Pattern, usize)>;
785
786fn parse_alternation(tokens: &[char], mut position: usize) -> Parsed {
787 let mut branches = Vec::new();
788 loop {
789 let (branch, next) = parse_sequence(tokens, position)?;
790 branches.push(branch);
791 position = next;
792 if tokens.get(position) == Some(&'|') {
793 position += 1;
794 } else {
795 break;
796 }
797 }
798 let node = if branches.len() == 1 {
799 branches.remove(0)
800 } else {
801 Pattern::Alternation(branches)
802 };
803 Some((node, position))
804}
805
806fn parse_sequence(tokens: &[char], mut position: usize) -> Parsed {
807 let mut parts = Vec::new();
808 while position < tokens.len() && !matches!(tokens[position], '|' | ')') {
809 let (atom, next) = parse_atom(tokens, position)?;
810 let (atom, next) = parse_quantifier(atom, tokens, next)?;
811 parts.push(atom);
812 position = next;
813 }
814 Some((Pattern::Sequence(parts), position))
815}
816
817fn parse_atom(tokens: &[char], position: usize) -> Parsed {
818 match tokens.get(position)? {
819 '(' => {
820 let (inner, next) = parse_alternation(tokens, position + 1)?;
821 if tokens.get(next) != Some(&')') {
822 return None;
823 }
824 Some((inner, next + 1))
825 }
826 '[' => parse_class(tokens, position + 1),
827 '\\' => {
828 let escaped = *tokens.get(position + 1)?;
831 if escaped.is_ascii_alphanumeric() {
832 return None;
833 }
834 Some((Pattern::Literal(escaped), position + 2))
835 }
836 '.' | '*' | '+' | '?' | '{' | '}' | ']' => None,
838 literal => Some((Pattern::Literal(*literal), position + 1)),
839 }
840}
841
842fn parse_class(tokens: &[char], mut position: usize) -> Parsed {
843 let negated = tokens.get(position) == Some(&'^');
844 if negated {
845 position += 1;
846 }
847 let mut items = Vec::new();
848 while let Some(token) = tokens.get(position) {
849 if *token == ']' {
850 if items.is_empty() {
851 return None;
852 }
853 return Some((Pattern::Class { negated, items }, position + 1));
854 }
855 let low = if *token == '\\' {
856 position += 1;
857 *tokens.get(position)?
858 } else {
859 *token
860 };
861 position += 1;
862
863 if tokens.get(position) == Some(&'-') && tokens.get(position + 1) != Some(&']') {
865 let high = *tokens.get(position + 1)?;
866 items.push(ClassItem::Range(low, high));
867 position += 2;
868 } else {
869 items.push(ClassItem::Single(low));
870 }
871 }
872 None
873}
874
875fn parse_quantifier(atom: Pattern, tokens: &[char], position: usize) -> Parsed {
876 let (min, max, next) = match tokens.get(position) {
877 Some('?') => (0, Some(1), position + 1),
878 Some('*') => (0, None, position + 1),
879 Some('+') => (1, None, position + 1),
880 Some('{') => {
881 let close = tokens[position..].iter().position(|token| *token == '}')? + position;
882 let body: String = tokens[position + 1..close].iter().collect();
883 let (min, max) = match body.split_once(',') {
884 None => {
885 let exact = body.parse().ok()?;
886 (exact, Some(exact))
887 }
888 Some((low, "")) => (low.parse().ok()?, None),
889 Some((low, high)) => (low.parse().ok()?, Some(high.parse().ok()?)),
890 };
891 (min, max, close + 1)
892 }
893 _ => return Some((atom, position)),
894 };
895 Some((
896 Pattern::Repeat {
897 inner: Box::new(atom),
898 min,
899 max,
900 },
901 next,
902 ))
903}