1use std::collections::{HashMap, HashSet};
29
30use sup_xml_tree::dom::{Document, Node, NodeKind};
31
32use super::{
33 model::{ContentModel, Group, GroupKind, Item, Occurrence, Particle},
34 AttDecl, AttDefault, AttType, Dtd,
35};
36
37#[derive(Debug, Clone)]
40pub struct DtdError {
41 pub element: String,
42 pub message: String,
43}
44
45impl std::fmt::Display for DtdError {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 write!(f, "DTD validation error on element <{}>: {}", self.element, self.message)
48 }
49}
50
51impl std::error::Error for DtdError {}
52
53pub fn validate(doc: &Document, dtd: &Dtd) -> Result<(), Vec<DtdError>> {
65 if dtd.is_empty() {
66 return Ok(());
67 }
68 let mut ids: HashSet<String> = HashSet::new();
69 let mut idrefs: Vec<(String, String)> = Vec::new();
70 let mut errors: Vec<DtdError> = Vec::new();
71 let root = doc.root();
72 walk(root, dtd, &mut ids, &mut idrefs, &mut errors);
73 for (element, idref) in idrefs {
74 if !ids.contains(&idref) {
75 errors.push(DtdError {
76 element,
77 message: format!("IDREF '{}' does not match any declared ID", idref),
78 });
79 }
80 }
81 if errors.is_empty() { Ok(()) } else { Err(errors) }
82}
83
84pub fn is_valid(doc: &Document, dtd: &Dtd) -> bool {
87 validate(doc, dtd).is_ok()
88}
89
90fn walk(
91 node: &Node<'_>,
92 dtd: &Dtd,
93 ids: &mut HashSet<String>,
94 idrefs: &mut Vec<(String, String)>,
95 errors: &mut Vec<DtdError>,
96) {
97 if !node.is_element() { return; }
98
99 let name = node.name();
100
101 let Some(decl) = dtd.elements.get(name) else {
103 errors.push(DtdError {
104 element: name.to_string(),
105 message: "no declaration found".into(),
106 });
107 for child in node.children() {
110 walk(child, dtd, ids, idrefs, errors);
111 }
112 return;
113 };
114
115 if let Err(e) = check_content(node, &decl.content) {
117 errors.push(e);
118 }
119
120 if let Some(attlist) = dtd.attlists.get(name) {
122 check_attrs(node, attlist, ids, idrefs, errors);
123 }
124
125 for child in node.children() {
127 walk(child, dtd, ids, idrefs, errors);
128 }
129}
130
131fn check_content(element: &Node<'_>, model: &ContentModel) -> Result<(), DtdError> {
136 match model {
137 ContentModel::Empty => {
138 for child in element.children() {
139 match child.kind {
140 NodeKind::Comment | NodeKind::Pi => continue,
141 NodeKind::Text | NodeKind::CData => {
142 if !child.content().chars().all(char::is_whitespace) {
145 return Err(DtdError {
146 element: element.name().to_string(),
147 message: "content not allowed in EMPTY element".into(),
148 });
149 }
150 }
151 _ => return Err(DtdError {
152 element: element.name().to_string(),
153 message: "content not allowed in EMPTY element".into(),
154 }),
155 }
156 }
157 Ok(())
158 }
159 ContentModel::Any => Ok(()),
160 ContentModel::Mixed { choices } => {
161 for child in element.children() {
162 match child.kind {
163 NodeKind::Comment | NodeKind::Pi
164 | NodeKind::Text | NodeKind::CData => continue,
165 NodeKind::Element => {
166 if !choices.iter().any(|n| n == child.name()) {
167 return Err(DtdError {
168 element: element.name().to_string(),
169 message: format!(
170 "child <{}> not allowed in mixed content (allowed: {})",
171 child.name(),
172 if choices.is_empty() { "#PCDATA only".into() }
173 else { choices.join(", ") }
174 ),
175 });
176 }
177 }
178 _ => return Err(DtdError {
179 element: element.name().to_string(),
180 message: "unexpected node kind in mixed content".into(),
181 }),
182 }
183 }
184 Ok(())
185 }
186 ContentModel::Children(group) => {
187 let mut child_names: Vec<&str> = Vec::new();
191 for child in element.children() {
192 match child.kind {
193 NodeKind::Element => child_names.push(child.name()),
194 NodeKind::Comment | NodeKind::Pi => continue,
195 NodeKind::Text | NodeKind::CData
196 if !child.content().chars().all(char::is_whitespace) =>
197 {
198 return Err(DtdError {
199 element: element.name().to_string(),
200 message: "character data not allowed in element-only content".into(),
201 });
202 }
203 NodeKind::Text | NodeKind::CData => {}
204 _ => {}
205 }
206 }
207 let mut pos = 0usize;
208 if !match_group(group, &child_names, &mut pos) || pos != child_names.len() {
209 return Err(DtdError {
210 element: element.name().to_string(),
211 message: format!(
212 "child sequence [{}] does not match content model",
213 child_names.join(", ")
214 ),
215 });
216 }
217 Ok(())
218 }
219 }
220}
221
222fn match_group(group: &Group, names: &[&str], pos: &mut usize) -> bool {
230 let start = *pos;
231 let min = match group.occur {
232 Occurrence::One | Occurrence::OneOrMore => 1,
233 Occurrence::ZeroOrOne | Occurrence::ZeroOrMore => 0,
234 };
235 let max = match group.occur {
236 Occurrence::One | Occurrence::ZeroOrOne => 1,
237 Occurrence::OneOrMore | Occurrence::ZeroOrMore => usize::MAX,
238 };
239 let mut matches = 0;
240 loop {
241 let try_pos = *pos;
242 if matches >= max || !match_group_one(group, names, pos) {
243 *pos = try_pos;
244 break;
245 }
246 matches += 1;
247 }
248 if matches < min {
249 *pos = start;
250 return false;
251 }
252 true
253}
254
255fn match_group_one(group: &Group, names: &[&str], pos: &mut usize) -> bool {
258 match group.kind {
259 GroupKind::Sequence => {
260 let start = *pos;
261 for item in &group.items {
262 if !match_particle(item, names, pos) {
263 *pos = start;
264 return false;
265 }
266 }
267 true
268 }
269 GroupKind::Choice => {
270 let start = *pos;
271 for item in &group.items {
272 let mut local = start;
273 if match_particle(item, names, &mut local) {
274 *pos = local;
275 return true;
276 }
277 }
278 *pos = start;
279 false
280 }
281 }
282}
283
284fn match_particle(p: &Particle, names: &[&str], pos: &mut usize) -> bool {
285 let start = *pos;
286 let min = match p.occur {
287 Occurrence::One | Occurrence::OneOrMore => 1,
288 Occurrence::ZeroOrOne | Occurrence::ZeroOrMore => 0,
289 };
290 let max = match p.occur {
291 Occurrence::One | Occurrence::ZeroOrOne => 1,
292 Occurrence::OneOrMore | Occurrence::ZeroOrMore => usize::MAX,
293 };
294 let mut matches = 0;
295 loop {
296 let try_pos = *pos;
297 let advanced = match &p.item {
298 Item::Name(n) => {
299 if names.get(*pos) == Some(&n.as_str()) { *pos += 1; true } else { false }
300 }
301 Item::Group(g) => match_group(g, names, pos),
302 };
303 if !advanced || matches >= max {
304 *pos = try_pos;
305 break;
306 }
307 matches += 1;
308 }
309 if matches < min { *pos = start; return false; }
310 true
311}
312
313fn check_attrs(
314 element: &Node<'_>,
315 attlist: &[AttDecl],
316 ids: &mut HashSet<String>,
317 idrefs: &mut Vec<(String, String)>,
318 errors: &mut Vec<DtdError>,
319) {
320 let mut present: HashMap<&str, &str> = HashMap::new();
322 for a in element.attributes() {
323 let name = a.name();
324 if name == "xmlns" || name.starts_with("xmlns:") { continue; }
328 present.insert(name, a.value());
329 }
330
331 for decl in attlist {
332 let value = present.get(decl.name.as_str()).copied();
333
334 match &decl.default {
336 AttDefault::Required => {
337 if value.is_none() {
338 errors.push(DtdError {
339 element: element.name().to_string(),
340 message: format!("required attribute '{}' missing", decl.name),
341 });
342 continue;
343 }
344 }
345 AttDefault::Fixed(want) => {
346 if let Some(got) = value {
347 if got != want {
348 errors.push(DtdError {
349 element: element.name().to_string(),
350 message: format!(
351 "attribute '{}' = '{}' violates #FIXED \"{}\"",
352 decl.name, got, want
353 ),
354 });
355 continue;
356 }
357 }
358 }
359 AttDefault::Implied | AttDefault::Default(_) => {}
360 }
361
362 if let Some(v) = value {
364 if let Err(e) = check_att_type(element.name(), &decl.name, &decl.att_type, v, ids, idrefs) {
365 errors.push(e);
366 }
367 }
368 }
369}
370
371fn check_att_type(
372 element: &str,
373 attr: &str,
374 ty: &AttType,
375 value: &str,
376 ids: &mut HashSet<String>,
377 idrefs: &mut Vec<(String, String)>,
378) -> Result<(), DtdError> {
379 match ty {
380 AttType::CData => Ok(()),
381 AttType::Id => {
382 if !is_valid_name(value) {
383 return Err(DtdError {
384 element: element.into(),
385 message: format!("attribute '{}' = '{}' is not a valid ID Name", attr, value),
386 });
387 }
388 if !ids.insert(value.to_string()) {
389 return Err(DtdError {
390 element: element.into(),
391 message: format!("duplicate ID '{}'", value),
392 });
393 }
394 Ok(())
395 }
396 AttType::IdRef => {
397 if !is_valid_name(value) {
398 return Err(DtdError {
399 element: element.into(),
400 message: format!("attribute '{}' = '{}' is not a valid IDREF Name", attr, value),
401 });
402 }
403 idrefs.push((element.to_string(), value.to_string()));
404 Ok(())
405 }
406 AttType::IdRefs => {
407 for tok in value.split_ascii_whitespace() {
408 if !is_valid_name(tok) {
409 return Err(DtdError {
410 element: element.into(),
411 message: format!(
412 "attribute '{}' IDREFS token '{}' is not a valid Name", attr, tok
413 ),
414 });
415 }
416 idrefs.push((element.to_string(), tok.to_string()));
417 }
418 Ok(())
419 }
420 AttType::Nmtoken => {
421 if !is_valid_nmtoken(value) {
422 return Err(DtdError {
423 element: element.into(),
424 message: format!("attribute '{}' = '{}' is not a valid NMTOKEN", attr, value),
425 });
426 }
427 Ok(())
428 }
429 AttType::Nmtokens => {
430 for tok in value.split_ascii_whitespace() {
431 if !is_valid_nmtoken(tok) {
432 return Err(DtdError {
433 element: element.into(),
434 message: format!(
435 "attribute '{}' NMTOKENS token '{}' invalid", attr, tok
436 ),
437 });
438 }
439 }
440 Ok(())
441 }
442 AttType::Entity | AttType::Entities => {
443 for tok in value.split_ascii_whitespace() {
446 if !is_valid_name(tok) {
447 return Err(DtdError {
448 element: element.into(),
449 message: format!(
450 "attribute '{}' ENTITY token '{}' is not a valid Name", attr, tok
451 ),
452 });
453 }
454 }
455 Ok(())
456 }
457 AttType::Notation(allowed) | AttType::Enumeration(allowed) => {
458 if !allowed.iter().any(|v| v == value) {
459 return Err(DtdError {
460 element: element.into(),
461 message: format!(
462 "attribute '{}' = '{}' not in enumeration ({})",
463 attr, value, allowed.join(" | ")
464 ),
465 });
466 }
467 Ok(())
468 }
469 }
470}
471
472fn is_valid_name(s: &str) -> bool {
476 let mut chars = s.chars();
477 let first = match chars.next() { Some(c) => c, None => return false };
478 if !is_name_start_char(first) { return false; }
479 chars.all(is_name_char)
480}
481
482fn is_valid_nmtoken(s: &str) -> bool {
483 !s.is_empty() && s.chars().all(is_name_char)
484}
485
486fn is_name_start_char(c: char) -> bool {
487 matches!(c, 'A'..='Z' | 'a'..='z' | '_' | ':')
488 || (c as u32) >= 0xC0
489}
490
491fn is_name_char(c: char) -> bool {
492 is_name_start_char(c) || matches!(c, '0'..='9' | '-' | '.')
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use crate::options::ParseOptions;
499 use crate::parser::parse_bytes_with_dtd;
500
501 fn parse(src: &str) -> (sup_xml_tree::dom::Document, Dtd) {
502 let opts = ParseOptions { namespace_aware: false, ..ParseOptions::default() };
503 parse_bytes_with_dtd(src.as_bytes(), &opts).expect("parse failed")
504 }
505
506 fn assert_err_contains(errs: &[DtdError], needle: &str) {
507 assert!(
508 errs.iter().any(|e| e.message.contains(needle)),
509 "no error matching {needle:?}, got: {errs:#?}",
510 );
511 }
512
513 #[test]
516 fn dtd_error_display_format() {
517 let e = DtdError {
518 element: "foo".into(),
519 message: "boom".into(),
520 };
521 assert_eq!(format!("{e}"), "DTD validation error on element <foo>: boom");
522 }
523
524 #[test]
525 fn empty_dtd_validates_anything() {
526 let opts = ParseOptions::default();
527 let doc = crate::parse_str("<r><a/><b/></r>", &opts).unwrap();
528 let dtd = Dtd::new();
529 assert!(dtd.is_empty());
530 assert!(validate(&doc, &dtd).is_ok());
531 assert!(is_valid(&doc, &dtd));
532 }
533
534 #[test]
535 fn is_valid_returns_false_on_failure() {
536 let src = r#"<?xml version="1.0"?>
538<!DOCTYPE r [
539 <!ELEMENT r ANY>
540]>
541<r><unknown/></r>"#;
542 let (doc, dtd) = parse(src);
543 assert!(!is_valid(&doc, &dtd));
544 }
545
546 #[test]
549 fn missing_element_decl_reports_error_but_keeps_walking() {
550 let src = r#"<?xml version="1.0"?>
551<!DOCTYPE r [
552 <!ELEMENT r ANY>
553 <!ELEMENT b ANY>
554]>
555<r><undeclared><b/></undeclared></r>"#;
556 let (doc, dtd) = parse(src);
560 let errs = validate(&doc, &dtd).unwrap_err();
561 assert_err_contains(&errs, "no declaration found");
562 }
563
564 #[test]
567 fn empty_model_accepts_whitespace_text_and_comments() {
568 let src = r#"<?xml version="1.0"?>
569<!DOCTYPE r [
570 <!ELEMENT r EMPTY>
571]>
572<r> <!-- comment --><?pi?></r>"#;
573 let (doc, dtd) = parse(src);
574 assert!(validate(&doc, &dtd).is_ok());
575 }
576
577 #[test]
578 fn empty_model_rejects_non_whitespace_text() {
579 let src = r#"<?xml version="1.0"?>
580<!DOCTYPE r [
581 <!ELEMENT r EMPTY>
582]>
583<r>text</r>"#;
584 let (doc, dtd) = parse(src);
585 let errs = validate(&doc, &dtd).unwrap_err();
586 assert_err_contains(&errs, "EMPTY");
587 }
588
589 #[test]
590 fn empty_model_rejects_child_elements() {
591 let src = r#"<?xml version="1.0"?>
592<!DOCTYPE r [
593 <!ELEMENT r EMPTY>
594 <!ELEMENT a EMPTY>
595]>
596<r><a/></r>"#;
597 let (doc, dtd) = parse(src);
598 let errs = validate(&doc, &dtd).unwrap_err();
599 assert_err_contains(&errs, "EMPTY");
600 }
601
602 #[test]
605 fn any_model_accepts_everything() {
606 let src = r#"<?xml version="1.0"?>
607<!DOCTYPE r [
608 <!ELEMENT r ANY>
609 <!ELEMENT a ANY>
610 <!ELEMENT b ANY>
611]>
612<r>text<a/>more<b><a/></b></r>"#;
613 let (doc, dtd) = parse(src);
614 assert!(validate(&doc, &dtd).is_ok(), "got {:?}", validate(&doc, &dtd));
615 }
616
617 #[test]
620 fn mixed_accepts_listed_children() {
621 let src = r#"<?xml version="1.0"?>
622<!DOCTYPE r [
623 <!ELEMENT r (#PCDATA | em | b)*>
624 <!ELEMENT em (#PCDATA)>
625 <!ELEMENT b (#PCDATA)>
626]>
627<r>text <em>emp</em> tail <b>bold</b></r>"#;
628 let (doc, dtd) = parse(src);
629 assert!(validate(&doc, &dtd).is_ok());
630 }
631
632 #[test]
633 fn mixed_rejects_unlisted_child() {
634 let src = r#"<?xml version="1.0"?>
635<!DOCTYPE r [
636 <!ELEMENT r (#PCDATA | em)*>
637 <!ELEMENT em (#PCDATA)>
638 <!ELEMENT bad (#PCDATA)>
639]>
640<r>text <bad/></r>"#;
641 let (doc, dtd) = parse(src);
642 let errs = validate(&doc, &dtd).unwrap_err();
643 assert_err_contains(&errs, "mixed content");
644 }
645
646 #[test]
647 fn mixed_pcdata_only_lists_pcdata_only_in_error() {
648 let src = r#"<?xml version="1.0"?>
649<!DOCTYPE r [
650 <!ELEMENT r (#PCDATA)>
651 <!ELEMENT bad (#PCDATA)>
652]>
653<r>text <bad/></r>"#;
654 let (doc, dtd) = parse(src);
655 let errs = validate(&doc, &dtd).unwrap_err();
656 assert_err_contains(&errs, "#PCDATA only");
657 }
658
659 #[test]
662 fn children_sequence_matches_in_order() {
663 let src = r#"<?xml version="1.0"?>
664<!DOCTYPE r [
665 <!ELEMENT r (a, b)>
666 <!ELEMENT a EMPTY>
667 <!ELEMENT b EMPTY>
668]>
669<r><a/><b/></r>"#;
670 let (doc, dtd) = parse(src);
671 assert!(validate(&doc, &dtd).is_ok());
672 }
673
674 #[test]
675 fn children_sequence_wrong_order_fails() {
676 let src = r#"<?xml version="1.0"?>
677<!DOCTYPE r [
678 <!ELEMENT r (a, b)>
679 <!ELEMENT a EMPTY>
680 <!ELEMENT b EMPTY>
681]>
682<r><b/><a/></r>"#;
683 let (doc, dtd) = parse(src);
684 let errs = validate(&doc, &dtd).unwrap_err();
685 assert_err_contains(&errs, "does not match content model");
686 }
687
688 #[test]
689 fn children_choice_accepts_either() {
690 let src = r#"<?xml version="1.0"?>
691<!DOCTYPE r [
692 <!ELEMENT r (a | b)>
693 <!ELEMENT a EMPTY>
694 <!ELEMENT b EMPTY>
695]>
696<r><b/></r>"#;
697 let (doc, dtd) = parse(src);
698 assert!(validate(&doc, &dtd).is_ok());
699 }
700
701 #[test]
702 fn children_choice_rejects_neither() {
703 let src = r#"<?xml version="1.0"?>
704<!DOCTYPE r [
705 <!ELEMENT r (a | b)>
706 <!ELEMENT a EMPTY>
707 <!ELEMENT b EMPTY>
708 <!ELEMENT c EMPTY>
709]>
710<r><c/></r>"#;
711 let (doc, dtd) = parse(src);
712 assert!(validate(&doc, &dtd).is_err());
713 }
714
715 #[test]
716 fn children_optional_allows_zero_or_one() {
717 let src = r#"<?xml version="1.0"?>
718<!DOCTYPE r [
719 <!ELEMENT r (a?)>
720 <!ELEMENT a EMPTY>
721]>
722<r></r>"#;
723 let (doc, dtd) = parse(src);
724 assert!(validate(&doc, &dtd).is_ok());
725
726 let src2 = r#"<?xml version="1.0"?>
727<!DOCTYPE r [
728 <!ELEMENT r (a?)>
729 <!ELEMENT a EMPTY>
730]>
731<r><a/></r>"#;
732 let (doc2, dtd2) = parse(src2);
733 assert!(validate(&doc2, &dtd2).is_ok());
734 }
735
736 #[test]
737 fn children_zero_or_more_allows_repetition() {
738 let src = r#"<?xml version="1.0"?>
739<!DOCTYPE r [
740 <!ELEMENT r (a*)>
741 <!ELEMENT a EMPTY>
742]>
743<r><a/><a/><a/></r>"#;
744 let (doc, dtd) = parse(src);
745 assert!(validate(&doc, &dtd).is_ok());
746 }
747
748 #[test]
749 fn children_one_or_more_requires_at_least_one() {
750 let src = r#"<?xml version="1.0"?>
751<!DOCTYPE r [
752 <!ELEMENT r (a+)>
753 <!ELEMENT a EMPTY>
754]>
755<r></r>"#;
756 let (doc, dtd) = parse(src);
757 assert!(validate(&doc, &dtd).is_err());
758
759 let src2 = r#"<?xml version="1.0"?>
760<!DOCTYPE r [
761 <!ELEMENT r (a+)>
762 <!ELEMENT a EMPTY>
763]>
764<r><a/><a/></r>"#;
765 let (doc2, dtd2) = parse(src2);
766 assert!(validate(&doc2, &dtd2).is_ok());
767 }
768
769 #[test]
770 fn children_nested_group() {
771 let src = r#"<?xml version="1.0"?>
772<!DOCTYPE r [
773 <!ELEMENT r ((a, b)+ , c?)>
774 <!ELEMENT a EMPTY>
775 <!ELEMENT b EMPTY>
776 <!ELEMENT c EMPTY>
777]>
778<r><a/><b/><a/><b/><c/></r>"#;
779 let (doc, dtd) = parse(src);
780 assert!(validate(&doc, &dtd).is_ok());
781 }
782
783 #[test]
784 fn children_text_rejected_unless_whitespace() {
785 let src = r#"<?xml version="1.0"?>
786<!DOCTYPE r [
787 <!ELEMENT r (a)>
788 <!ELEMENT a EMPTY>
789]>
790<r>nope<a/></r>"#;
791 let (doc, dtd) = parse(src);
792 let errs = validate(&doc, &dtd).unwrap_err();
793 assert_err_contains(&errs, "character data");
794 }
795
796 #[test]
797 fn children_whitespace_text_is_ignored() {
798 let src = r#"<?xml version="1.0"?>
799<!DOCTYPE r [
800 <!ELEMENT r (a)>
801 <!ELEMENT a EMPTY>
802]>
803<r>
804 <a/>
805</r>"#;
806 let (doc, dtd) = parse(src);
807 assert!(validate(&doc, &dtd).is_ok());
808 }
809
810 #[test]
813 fn required_attribute_missing_errors() {
814 let src = r#"<?xml version="1.0"?>
815<!DOCTYPE r [
816 <!ELEMENT r EMPTY>
817 <!ATTLIST r id CDATA #REQUIRED>
818]>
819<r/>"#;
820 let (doc, dtd) = parse(src);
821 let errs = validate(&doc, &dtd).unwrap_err();
822 assert_err_contains(&errs, "required attribute 'id'");
823 }
824
825 #[test]
826 fn required_attribute_present_ok() {
827 let src = r#"<?xml version="1.0"?>
828<!DOCTYPE r [
829 <!ELEMENT r EMPTY>
830 <!ATTLIST r id CDATA #REQUIRED>
831]>
832<r id="x"/>"#;
833 let (doc, dtd) = parse(src);
834 assert!(validate(&doc, &dtd).is_ok());
835 }
836
837 #[test]
838 fn fixed_attribute_mismatch_errors() {
839 let src = r#"<?xml version="1.0"?>
840<!DOCTYPE r [
841 <!ELEMENT r EMPTY>
842 <!ATTLIST r kind CDATA #FIXED "v1">
843]>
844<r kind="v2"/>"#;
845 let (doc, dtd) = parse(src);
846 let errs = validate(&doc, &dtd).unwrap_err();
847 assert_err_contains(&errs, "#FIXED");
848 }
849
850 #[test]
851 fn fixed_attribute_match_ok() {
852 let src = r#"<?xml version="1.0"?>
853<!DOCTYPE r [
854 <!ELEMENT r EMPTY>
855 <!ATTLIST r kind CDATA #FIXED "v1">
856]>
857<r kind="v1"/>"#;
858 let (doc, dtd) = parse(src);
859 assert!(validate(&doc, &dtd).is_ok());
860 }
861
862 #[test]
863 fn enumeration_accepts_listed_value() {
864 let src = r#"<?xml version="1.0"?>
865<!DOCTYPE r [
866 <!ELEMENT r EMPTY>
867 <!ATTLIST r role (admin|user|guest) "user">
868]>
869<r role="admin"/>"#;
870 let (doc, dtd) = parse(src);
871 assert!(validate(&doc, &dtd).is_ok());
872 }
873
874 #[test]
875 fn enumeration_rejects_unlisted_value() {
876 let src = r#"<?xml version="1.0"?>
877<!DOCTYPE r [
878 <!ELEMENT r EMPTY>
879 <!ATTLIST r role (admin|user|guest) "user">
880]>
881<r role="root"/>"#;
882 let (doc, dtd) = parse(src);
883 let errs = validate(&doc, &dtd).unwrap_err();
884 assert_err_contains(&errs, "not in enumeration");
885 }
886
887 #[test]
888 fn id_must_be_valid_name() {
889 let src = r#"<?xml version="1.0"?>
890<!DOCTYPE r [
891 <!ELEMENT r EMPTY>
892 <!ATTLIST r id ID #IMPLIED>
893]>
894<r id="0bad"/>"#;
895 let (doc, dtd) = parse(src);
896 let errs = validate(&doc, &dtd).unwrap_err();
897 assert_err_contains(&errs, "valid ID Name");
898 }
899
900 #[test]
901 fn duplicate_id_errors() {
902 let src = r#"<?xml version="1.0"?>
903<!DOCTYPE r [
904 <!ELEMENT r (x, y)>
905 <!ELEMENT x EMPTY>
906 <!ELEMENT y EMPTY>
907 <!ATTLIST x id ID #REQUIRED>
908 <!ATTLIST y id ID #REQUIRED>
909]>
910<r><x id="a"/><y id="a"/></r>"#;
911 let (doc, dtd) = parse(src);
912 let errs = validate(&doc, &dtd).unwrap_err();
913 assert_err_contains(&errs, "duplicate ID");
914 }
915
916 #[test]
917 fn idref_must_match_declared_id() {
918 let src = r#"<?xml version="1.0"?>
919<!DOCTYPE r [
920 <!ELEMENT r (x, y)>
921 <!ELEMENT x EMPTY>
922 <!ELEMENT y EMPTY>
923 <!ATTLIST x id ID #REQUIRED>
924 <!ATTLIST y target IDREF #REQUIRED>
925]>
926<r><x id="a"/><y target="ghost"/></r>"#;
927 let (doc, dtd) = parse(src);
928 let errs = validate(&doc, &dtd).unwrap_err();
929 assert_err_contains(&errs, "IDREF 'ghost'");
930 }
931
932 #[test]
933 fn idref_resolves_to_existing_id() {
934 let src = r#"<?xml version="1.0"?>
935<!DOCTYPE r [
936 <!ELEMENT r (x, y)>
937 <!ELEMENT x EMPTY>
938 <!ELEMENT y EMPTY>
939 <!ATTLIST x id ID #REQUIRED>
940 <!ATTLIST y target IDREF #REQUIRED>
941]>
942<r><x id="a"/><y target="a"/></r>"#;
943 let (doc, dtd) = parse(src);
944 assert!(validate(&doc, &dtd).is_ok());
945 }
946
947 #[test]
948 fn idrefs_validates_each_token() {
949 let src = r#"<?xml version="1.0"?>
950<!DOCTYPE r [
951 <!ELEMENT r (x, x, y)>
952 <!ELEMENT x EMPTY>
953 <!ELEMENT y EMPTY>
954 <!ATTLIST x id ID #REQUIRED>
955 <!ATTLIST y refs IDREFS #REQUIRED>
956]>
957<r><x id="a"/><x id="b"/><y refs="a b"/></r>"#;
958 let (doc, dtd) = parse(src);
959 assert!(validate(&doc, &dtd).is_ok());
960 }
961
962 #[test]
963 fn idrefs_rejects_invalid_token() {
964 let src = r#"<?xml version="1.0"?>
965<!DOCTYPE r [
966 <!ELEMENT r (x, y)>
967 <!ELEMENT x EMPTY>
968 <!ELEMENT y EMPTY>
969 <!ATTLIST x id ID #REQUIRED>
970 <!ATTLIST y refs IDREFS #REQUIRED>
971]>
972<r><x id="a"/><y refs="a 0bad"/></r>"#;
973 let (doc, dtd) = parse(src);
974 let errs = validate(&doc, &dtd).unwrap_err();
975 assert_err_contains(&errs, "valid Name");
976 }
977
978 #[test]
979 fn nmtoken_validation() {
980 let src = r#"<?xml version="1.0"?>
981<!DOCTYPE r [
982 <!ELEMENT r EMPTY>
983 <!ATTLIST r tok NMTOKEN #IMPLIED>
984]>
985<r tok="bad token"/>"#;
986 let (doc, dtd) = parse(src);
988 let errs = validate(&doc, &dtd).unwrap_err();
989 assert_err_contains(&errs, "NMTOKEN");
990 }
991
992 #[test]
993 fn nmtoken_accepts_digit_start() {
994 let src = r#"<?xml version="1.0"?>
995<!DOCTYPE r [
996 <!ELEMENT r EMPTY>
997 <!ATTLIST r tok NMTOKEN #IMPLIED>
998]>
999<r tok="123abc"/>"#;
1000 let (doc, dtd) = parse(src);
1001 assert!(validate(&doc, &dtd).is_ok());
1002 }
1003
1004 #[test]
1005 fn nmtokens_validates_each_token() {
1006 let src = r#"<?xml version="1.0"?>
1007<!DOCTYPE r [
1008 <!ELEMENT r EMPTY>
1009 <!ATTLIST r toks NMTOKENS #IMPLIED>
1010]>
1011<r toks="a 1b c-d"/>"#;
1012 let (doc, dtd) = parse(src);
1013 assert!(validate(&doc, &dtd).is_ok());
1014 }
1015
1016 #[test]
1017 fn nmtokens_rejects_invalid_token() {
1018 let src = r#"<?xml version="1.0"?>
1022<!DOCTYPE r [
1023 <!ELEMENT r EMPTY>
1024 <!ATTLIST r toks NMTOKENS #IMPLIED>
1025]>
1026<r toks="a b!c d"/>"#;
1027 let (doc, dtd) = parse(src);
1028 let errs = validate(&doc, &dtd).unwrap_err();
1029 assert_err_contains(&errs, "NMTOKENS");
1030 }
1031
1032 #[test]
1033 fn entity_attr_accepts_name() {
1034 let src = r#"<?xml version="1.0"?>
1035<!DOCTYPE r [
1036 <!ELEMENT r EMPTY>
1037 <!ATTLIST r ent ENTITY #IMPLIED>
1038]>
1039<r ent="picture"/>"#;
1040 let (doc, dtd) = parse(src);
1041 assert!(validate(&doc, &dtd).is_ok());
1042 }
1043
1044 #[test]
1045 fn entity_attr_rejects_invalid_name() {
1046 let src = r#"<?xml version="1.0"?>
1047<!DOCTYPE r [
1048 <!ELEMENT r EMPTY>
1049 <!ATTLIST r ent ENTITY #IMPLIED>
1050]>
1051<r ent="0bad"/>"#;
1052 let (doc, dtd) = parse(src);
1053 let errs = validate(&doc, &dtd).unwrap_err();
1054 assert_err_contains(&errs, "ENTITY token");
1055 }
1056
1057 #[test]
1058 fn xmlns_attributes_are_skipped_during_attribute_validation() {
1059 let src = r#"<?xml version="1.0"?>
1062<!DOCTYPE r [
1063 <!ELEMENT r EMPTY>
1064 <!ATTLIST r id CDATA #IMPLIED>
1065]>
1066<r xmlns:foo="urn:bar" id="x"/>"#;
1067 let (doc, dtd) = parse(src);
1068 assert!(validate(&doc, &dtd).is_ok());
1069 }
1070
1071 #[test]
1074 fn name_predicates_basic() {
1075 assert!(is_valid_name("foo"));
1076 assert!(is_valid_name("_bar"));
1077 assert!(is_valid_name(":ns"));
1078 assert!(is_valid_name("a1-2.3"));
1079 assert!(!is_valid_name(""));
1080 assert!(!is_valid_name("0foo"));
1081 assert!(!is_valid_name("a b"));
1082 }
1083
1084 #[test]
1085 fn nmtoken_predicate() {
1086 assert!(is_valid_nmtoken("1abc"));
1087 assert!(is_valid_nmtoken("foo-bar"));
1088 assert!(!is_valid_nmtoken(""));
1089 assert!(!is_valid_nmtoken("a b"));
1090 assert!(!is_valid_nmtoken("!"));
1091 }
1092}