1pub mod inject;
32pub mod model;
33pub mod validate;
34
35use std::collections::HashMap;
36
37pub use inject::{inject_defaults, inject_defaults_from};
38pub use model::{
39 AttDecl, AttDefault, AttType, ContentModel, DeclRef, ElementDecl, EntityDecl, Group, GroupKind,
40 Item, Occurrence, Particle,
41};
42pub use validate::{validate, DtdError};
43
44pub fn collect_id_attrs(dtd: &Dtd) -> HashMap<String, Vec<String>> {
51 let mut out: HashMap<String, Vec<String>> = HashMap::new();
52 for (elem, decls) in &dtd.attlists {
53 let ids: Vec<String> = decls.iter()
54 .filter(|d| matches!(d.att_type, AttType::Id))
55 .map(|d| d.name.clone())
56 .collect();
57 if !ids.is_empty() {
58 out.insert(elem.clone(), ids);
59 }
60 }
61 out
62}
63
64pub fn collect_idref_attrs(dtd: &Dtd) -> HashMap<String, Vec<String>> {
68 let mut out: HashMap<String, Vec<String>> = HashMap::new();
69 for (elem, decls) in &dtd.attlists {
70 let refs: Vec<String> = decls.iter()
71 .filter(|d| matches!(d.att_type, AttType::IdRef | AttType::IdRefs))
72 .map(|d| d.name.clone())
73 .collect();
74 if !refs.is_empty() {
75 out.insert(elem.clone(), refs);
76 }
77 }
78 out
79}
80
81#[derive(Debug, Clone, Default)]
85pub struct Dtd {
86 pub elements: HashMap<String, ElementDecl>,
88 pub element_order: Vec<String>,
92 pub attlists: HashMap<String, Vec<AttDecl>>,
96 pub root_name: String,
101 pub public_id: Option<String>,
104 pub system_id: Option<String>,
108 pub unparsed_entities: HashMap<String, sup_xml_tree::UnparsedEntity>,
115 pub entities: Vec<crate::dtd::model::EntityDecl>,
121 pub decl_order: Vec<crate::dtd::model::DeclRef>,
124 pub internal_decls: Vec<String>,
132 pub internal_subset_prolog_index: u32,
140}
141
142impl Dtd {
143 pub fn new() -> Self { Self::default() }
145
146 pub fn is_empty(&self) -> bool {
149 self.elements.is_empty() && self.attlists.is_empty()
150 }
151
152 pub fn add_element(&mut self, decl: ElementDecl) {
158 if !self.elements.contains_key(&decl.name) {
160 self.element_order.push(decl.name.clone());
161 self.decl_order.push(crate::dtd::model::DeclRef::Element(decl.name.clone()));
162 self.elements.insert(decl.name.clone(), decl);
163 }
164 }
165
166 pub fn add_attlist(&mut self, element: String, mut decls: Vec<AttDecl>) {
169 if !self.attlists.contains_key(&element) {
172 self.decl_order.push(crate::dtd::model::DeclRef::Attlist(element.clone()));
173 }
174 let entry = self.attlists.entry(element).or_insert_with(Vec::new);
175 entry.append(&mut decls);
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::options::ParseOptions;
183 use crate::parser::{parse_bytes_with_dtd, parse_external_subset};
184
185 fn parse(src: &str) -> (sup_xml_tree::dom::Document, Dtd) {
186 let opts = ParseOptions { namespace_aware: false, ..ParseOptions::default() };
187 parse_bytes_with_dtd(src.as_bytes(), &opts).expect("parse failed")
188 }
189
190 #[test]
191 fn external_subset_captures_element_and_attlist() {
192 let dtd = parse_external_subset(
195 b"<!ELEMENT a (b)>\n<!ATTLIST a x CDATA #IMPLIED>\n<!ELEMENT b EMPTY>",
196 &ParseOptions::default(),
197 ).expect("external subset should parse");
198 assert!(dtd.elements.contains_key("a"));
199 assert!(dtd.elements.contains_key("b"));
200 assert_eq!(dtd.attlists.get("a").map(Vec::len), Some(1));
201 }
202
203 #[test]
204 fn external_subset_allows_conditional_sections() {
205 let dtd = parse_external_subset(
208 b"<![INCLUDE[ <!ELEMENT keep EMPTY> ]]>\n<![IGNORE[ <!ELEMENT drop EMPTY> ]]>",
209 &ParseOptions::default(),
210 ).expect("conditional sections should parse");
211 assert!(dtd.elements.contains_key("keep"));
212 assert!(!dtd.elements.contains_key("drop"));
213 }
214
215 #[test]
216 fn captures_empty_element_decl() {
217 let src = r#"<?xml version="1.0"?>
218<!DOCTYPE r [
219 <!ELEMENT r EMPTY>
220]>
221<r/>"#;
222 let (_doc, dtd) = parse(src);
223 assert!(matches!(dtd.elements.get("r").map(|e| &e.content), Some(ContentModel::Empty)));
224 }
225
226 #[test]
227 fn captures_mixed_content() {
228 let src = r#"<?xml version="1.0"?>
229<!DOCTYPE r [
230 <!ELEMENT r (#PCDATA | em | b)*>
231 <!ELEMENT em (#PCDATA)>
232 <!ELEMENT b (#PCDATA)>
233]>
234<r>text <em>emp</em> tail</r>"#;
235 let (_doc, dtd) = parse(src);
236 match &dtd.elements.get("r").unwrap().content {
237 ContentModel::Mixed { choices } => {
238 assert_eq!(choices, &vec!["em".to_string(), "b".to_string()]);
239 }
240 other => panic!("expected Mixed, got {:?}", other),
241 }
242 }
243
244 #[test]
245 fn captures_children_with_quantifier() {
246 let src = r#"<?xml version="1.0"?>
247<!DOCTYPE r [
248 <!ELEMENT r (a, b+, c?)>
249 <!ELEMENT a EMPTY>
250 <!ELEMENT b EMPTY>
251 <!ELEMENT c EMPTY>
252]>
253<r><a/><b/><b/></r>"#;
254 let (_doc, dtd) = parse(src);
255 match &dtd.elements.get("r").unwrap().content {
256 ContentModel::Children(g) => {
257 assert_eq!(g.kind, GroupKind::Sequence);
258 assert_eq!(g.items.len(), 3);
259 assert_eq!(g.items[0].occur, Occurrence::One);
260 assert_eq!(g.items[1].occur, Occurrence::OneOrMore);
261 assert_eq!(g.items[2].occur, Occurrence::ZeroOrOne);
262 }
263 other => panic!("expected Children, got {:?}", other),
264 }
265 }
266
267 #[test]
268 fn captures_attlist() {
269 let src = r#"<?xml version="1.0"?>
270<!DOCTYPE r [
271 <!ELEMENT r EMPTY>
272 <!ATTLIST r
273 id ID #REQUIRED
274 type (a | b) "a"
275 note CDATA #IMPLIED>
276]>
277<r id="x1" type="b"/>"#;
278 let (_doc, dtd) = parse(src);
279 let attrs = dtd.attlists.get("r").expect("attrs");
280 assert_eq!(attrs.len(), 3);
281 assert!(matches!(attrs[0].att_type, AttType::Id));
282 assert!(matches!(attrs[0].default, AttDefault::Required));
283 assert!(matches!(&attrs[1].att_type, AttType::Enumeration(v) if v == &vec!["a".to_string(), "b".to_string()]));
284 assert!(matches!(&attrs[1].default, AttDefault::Default(s) if s == "a"));
285 assert!(matches!(attrs[2].att_type, AttType::CData));
286 assert!(matches!(attrs[2].default, AttDefault::Implied));
287 }
288
289 #[test]
290 fn validates_valid_document() {
291 let src = r#"<?xml version="1.0"?>
292<!DOCTYPE r [
293 <!ELEMENT r (a+, b?)>
294 <!ELEMENT a EMPTY>
295 <!ELEMENT b (#PCDATA)>
296 <!ATTLIST a id ID #REQUIRED>
297]>
298<r>
299 <a id="x1"/>
300 <a id="x2"/>
301 <b>hi</b>
302</r>"#;
303 let (doc, dtd) = parse(src);
304 validate(&doc, &dtd).expect("should validate");
305 }
306
307 #[test]
308 fn rejects_missing_required_attr() {
309 let src = r#"<?xml version="1.0"?>
310<!DOCTYPE r [
311 <!ELEMENT r EMPTY>
312 <!ATTLIST r id ID #REQUIRED>
313]>
314<r/>"#;
315 let (doc, dtd) = parse(src);
316 let errs = validate(&doc, &dtd).unwrap_err();
317 assert!(errs.iter().any(|e| e.message.contains("required")), "got: {:?}", errs);
318 }
319
320 #[test]
321 fn rejects_bad_enum_value() {
322 let src = r#"<?xml version="1.0"?>
323<!DOCTYPE r [
324 <!ELEMENT r EMPTY>
325 <!ATTLIST r type (a | b) "a">
326]>
327<r type="c"/>"#;
328 let (doc, dtd) = parse(src);
329 let errs = validate(&doc, &dtd).unwrap_err();
330 assert!(errs.iter().any(|e| e.message.contains("enumeration")), "got: {:?}", errs);
331 }
332
333 #[test]
334 fn rejects_duplicate_id() {
335 let src = r#"<?xml version="1.0"?>
336<!DOCTYPE r [
337 <!ELEMENT r (a, a)>
338 <!ELEMENT a EMPTY>
339 <!ATTLIST a id ID #REQUIRED>
340]>
341<r><a id="x1"/><a id="x1"/></r>"#;
342 let (doc, dtd) = parse(src);
343 let errs = validate(&doc, &dtd).unwrap_err();
344 assert!(errs.iter().any(|e| e.message.contains("duplicate")), "got: {:?}", errs);
345 }
346
347 #[test]
348 fn rejects_unresolved_idref() {
349 let src = r#"<?xml version="1.0"?>
350<!DOCTYPE r [
351 <!ELEMENT r (a, b)>
352 <!ELEMENT a EMPTY>
353 <!ELEMENT b EMPTY>
354 <!ATTLIST a id ID #REQUIRED>
355 <!ATTLIST b ref IDREF #REQUIRED>
356]>
357<r><a id="x1"/><b ref="missing"/></r>"#;
358 let (doc, dtd) = parse(src);
359 let errs = validate(&doc, &dtd).unwrap_err();
360 assert!(errs.iter().any(|e| e.message.contains("IDREF")), "got: {:?}", errs);
361 }
362
363 #[test]
364 fn rejects_bad_content_model() {
365 let src = r#"<?xml version="1.0"?>
366<!DOCTYPE r [
367 <!ELEMENT r (a, b)>
368 <!ELEMENT a EMPTY>
369 <!ELEMENT b EMPTY>
370]>
371<r><b/><a/></r>"#;
372 let (doc, dtd) = parse(src);
373 let errs = validate(&doc, &dtd).unwrap_err();
374 assert!(errs.iter().any(|e| e.message.contains("does not match")), "got: {:?}", errs);
375 }
376
377 #[test]
378 fn rejects_deeply_nested_content_model() {
379 let n = 400usize;
384 let src = format!(
385 "<?xml version=\"1.0\"?>\n<!DOCTYPE r [\n <!ELEMENT r {}a{}>\n]>\n<r/>",
386 "(".repeat(n),
387 ")".repeat(n),
388 );
389 let opts = ParseOptions { namespace_aware: false, ..ParseOptions::default() };
390 let err = parse_bytes_with_dtd(src.as_bytes(), &opts)
391 .expect_err("deeply nested content model should be rejected");
392 assert!(
393 err.message.contains("nesting depth exceeds limit"),
394 "expected depth-limit error, got: {err}"
395 );
396 }
397
398 #[test]
399 fn loads_external_subset() {
400 use std::io::Write;
401 let mut tmp = std::env::temp_dir();
402 tmp.push("sup_xml_dtd_external.dtd");
403 let mut f = std::fs::File::create(&tmp).unwrap();
404 f.write_all(b"<!ELEMENT r (a+)>\n<!ELEMENT a EMPTY>\n<!ATTLIST a id ID #REQUIRED>\n").unwrap();
405 drop(f);
406
407 let src = format!(
408 r#"<?xml version="1.0"?>
409<!DOCTYPE r SYSTEM "{}">
410<r><a id="x1"/></r>"#,
411 tmp.display()
412 );
413 let opts = ParseOptions {
414 namespace_aware: false,
415 load_external_dtd: true,
416 ..ParseOptions::default()
417 };
418 let (doc, dtd) = parse_bytes_with_dtd(src.as_bytes(), &opts).unwrap();
419 assert!(dtd.elements.contains_key("r"), "external <!ELEMENT r> missing");
420 assert!(dtd.elements.contains_key("a"), "external <!ELEMENT a> missing");
421 assert!(dtd.attlists.contains_key("a"), "external <!ATTLIST a> missing");
422 validate(&doc, &dtd).expect("valid doc against external DTD");
424
425 let _ = std::fs::remove_file(&tmp);
426 }
427
428 #[test]
429 fn external_subset_off_by_default() {
430 let src = r#"<?xml version="1.0"?>
434<!DOCTYPE r SYSTEM "/nonexistent/path.dtd">
435<r/>"#;
436 let opts = ParseOptions { namespace_aware: false, ..ParseOptions::default() };
437 let (_, dtd) = parse_bytes_with_dtd(src.as_bytes(), &opts).unwrap();
439 assert!(dtd.is_empty(), "external DTD should not have been loaded");
440 }
441
442 #[test]
443 fn missing_external_subset_is_non_fatal() {
444 let src = r#"<?xml version="1.0"?>
446<!DOCTYPE r SYSTEM "/nonexistent/path-that-does-not-exist.dtd">
447<r/>"#;
448 let opts = ParseOptions {
449 namespace_aware: false,
450 load_external_dtd: true,
451 ..ParseOptions::default()
452 };
453 let (_, dtd) = parse_bytes_with_dtd(src.as_bytes(), &opts).unwrap();
454 assert!(dtd.is_empty(), "missing file should leave dtd empty");
455 }
456
457 #[test]
458 fn internal_and_external_subsets_merge() {
459 use std::io::Write;
460 let mut tmp = std::env::temp_dir();
461 tmp.push("sup_xml_dtd_merge.dtd");
462 let mut f = std::fs::File::create(&tmp).unwrap();
463 f.write_all(b"<!ELEMENT a EMPTY>\n").unwrap();
464 drop(f);
465
466 let src = format!(
467 r#"<?xml version="1.0"?>
468<!DOCTYPE r SYSTEM "{}" [
469 <!ELEMENT r (a+)>
470]>
471<r><a/></r>"#,
472 tmp.display()
473 );
474 let opts = ParseOptions {
475 namespace_aware: false,
476 load_external_dtd: true,
477 ..ParseOptions::default()
478 };
479 let (_, dtd) = parse_bytes_with_dtd(src.as_bytes(), &opts).unwrap();
480 assert!(dtd.elements.contains_key("r"), "internal <!ELEMENT r> missing");
481 assert!(dtd.elements.contains_key("a"), "external <!ELEMENT a> missing");
482 let _ = std::fs::remove_file(&tmp);
483 }
484
485 #[test]
486 fn collects_every_error_in_one_pass() {
487 let src = r#"<?xml version="1.0"?>
490<!DOCTYPE r [
491 <!ELEMENT r (a, a)>
492 <!ELEMENT a EMPTY>
493 <!ATTLIST a id ID #REQUIRED>
494]>
495<r><a/><a id="dup"/></r>"#;
496 let (doc, dtd) = parse(src);
497 let errs = validate(&doc, &dtd).unwrap_err();
498 assert!(errs.len() >= 1, "expected at least 1 error, got {:?}", errs);
499 assert!(errs.iter().any(|e| e.message.contains("required")),
500 "missing 'required' error: {:?}", errs);
501 }
502}