use oxml::{Limits, parse, parse_with_external};
fn with(
doc: &str,
parts: &[(&str, &str)],
) -> Result<oxml::Document, oxml::Error> {
parse_with_external(doc, Limits::default(), &parts)
}
#[test]
fn without_a_source_an_external_reference_expands_to_nothing() {
let doc = r#"<!DOCTYPE d [<!ENTITY g SYSTEM "g.ent">]><d>&g;</d>"#;
let parsed = parse(doc).expect("well-formed");
assert_eq!(parsed.text(parsed.root()), "");
}
#[test]
fn with_a_source_the_content_is_used() {
let doc = r#"<!DOCTYPE d [<!ENTITY g SYSTEM "g.ent">]><d>&g;</d>"#;
let parsed = with(doc, &[("g.ent", "hello")]).expect("well-formed");
assert_eq!(parsed.text(parsed.root()), "hello");
}
#[test]
fn a_text_declaration_is_stripped_rather_than_inserted() {
let doc = r#"<!DOCTYPE d [<!ENTITY g SYSTEM "g.ent">]><d>&g;</d>"#;
let ent = r#"<?xml version="1.0" encoding="UTF-8"?>hello"#;
let parsed = with(doc, &[("g.ent", ent)]).expect("well-formed");
assert_eq!(parsed.text(parsed.root()), "hello");
}
#[test]
fn a_text_declaration_must_name_an_encoding() {
let doc = r#"<!DOCTYPE d [<!ENTITY g SYSTEM "g.ent">]><d>&g;</d>"#;
assert!(with(doc, &[("g.ent", r#"<?xml version="1.0"?>x"#)]).is_err());
let ok = with(doc, &[("g.ent", r#"<?xml encoding="UTF-8"?>x"#)]);
assert!(ok.is_ok(), "version is optional in a text declaration");
}
#[test]
fn a_text_declaration_may_not_be_standalone() {
let doc = r#"<!DOCTYPE d [<!ENTITY g SYSTEM "g.ent">]><d>&g;</d>"#;
let ent = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>x"#;
assert!(with(doc, &[("g.ent", ent)]).is_err());
}
#[test]
fn an_unreferenced_entity_is_never_read() {
let doc = r#"<!DOCTYPE d [<!ENTITY g SYSTEM "g.ent">]><d>text</d>"#;
let broken = r#"<?xml version="1.0"?>"#; let parsed = with(doc, &[("g.ent", broken)]).expect("never read");
assert_eq!(parsed.text(parsed.root()), "text");
}
#[test]
fn an_external_subsets_declarations_are_used() {
let doc = r#"<!DOCTYPE d SYSTEM "d.dtd"><d>&g;</d>"#;
let dtd = r#"<!ELEMENT d (#PCDATA)><!ENTITY g "from the subset">"#;
let parsed = with(doc, &[("d.dtd", dtd)]).expect("well-formed");
assert_eq!(parsed.text(parsed.root()), "from the subset");
}
#[test]
fn the_internal_subset_wins() {
let doc =
r#"<!DOCTYPE d SYSTEM "d.dtd" [<!ENTITY g "internal">]><d>&g;</d>"#;
let dtd = r#"<!ENTITY g "external">"#;
let parsed = with(doc, &[("d.dtd", dtd)]).expect("well-formed");
assert_eq!(parsed.text(parsed.root()), "internal");
}
#[test]
fn a_declaration_needing_a_parameter_entity_is_skipped_not_rejected() {
let doc = r#"<!DOCTYPE d SYSTEM "d.dtd"><d/>"#;
let dtd = r#"
<!ENTITY % choice "(a|b)">
<!ELEMENT d (a,%choice;,c)>
<!ENTITY g "still parsed">
"#;
assert!(with(doc, &[("d.dtd", dtd)]).is_ok());
}
#[test]
fn a_declaration_closed_by_a_parameter_entity_is_tolerated() {
let doc = r#"<!DOCTYPE d SYSTEM "d.dtd"><d></d>"#;
let dtd = "<!ENTITY % e \">\">\n<!ELEMENT d (#PCDATA) %e;\n";
assert!(with(doc, &[("d.dtd", dtd)]).is_ok());
}
#[test]
fn conditional_section_delimiters_must_balance() {
let doc = r#"<!DOCTYPE d SYSTEM "d.dtd"><d/>"#;
let dtd = r"<![IGNORE[ the delimiters '<![' and ']]>' must balance ]]>
<!ELEMENT d EMPTY>";
assert!(with(doc, &[("d.dtd", dtd)]).is_ok());
}
#[test]
fn an_external_entity_is_still_refused_in_an_attribute() {
let doc = r#"<!DOCTYPE d [<!ENTITY g SYSTEM "g.ent">]><d a="&g;"/>"#;
assert!(with(doc, &[("g.ent", "hello")]).is_err());
}
#[test]
fn parameter_entities_are_expanded_in_the_external_subset() {
let doc = r#"<!DOCTYPE d SYSTEM "d.dtd"><d/>"#;
let dtd = r#"
<!ENTITY % choice "(a|b)">
<!ELEMENT d (a,%choice;,c)>
"#;
assert!(with(doc, &[("d.dtd", dtd)]).is_ok());
}
#[test]
fn a_parameter_entity_can_expand_to_a_whole_declaration() {
let doc = "<?xml version='1.0'?>\n\
<!DOCTYPE test [\n\
<!ELEMENT test (#PCDATA) >\n\
<!ENTITY % xx '%zz;'>\n\
<!ENTITY % zz '<!ENTITY tricky \"error-prone\" >' >\n\
%xx;\n\
]>\n\
<test>This sample shows a &tricky; method.</test>";
let parsed = parse(doc).expect("well-formed");
assert!(
parsed.text(parsed.root()).contains("error-prone"),
"the entity `tricky` should have been declared by expansion"
);
}
#[test]
fn parameter_entity_recursion_is_bounded() {
let doc = r#"<!DOCTYPE d SYSTEM "d.dtd"><d/>"#;
let dtd = r#"<!ENTITY % a "%b;"><!ENTITY % b "%a;"><!ELEMENT d (%a;)>"#;
let _ = with(doc, &[("d.dtd", dtd)]);
}
#[test]
fn a_conditional_section_must_say_include_or_ignore() {
let doc = r#"<!DOCTYPE d SYSTEM "d.dtd"><d/>"#;
for keyword in ["MAYBE", "include", "Ignore", ""] {
let dtd = format!("<![{keyword}[ <!ELEMENT d EMPTY> ]]>");
assert!(
with(doc, &[("d.dtd", &dtd)]).is_err(),
"{keyword:?} is not a conditional section keyword"
);
}
for keyword in ["INCLUDE", "IGNORE"] {
let dtd = format!("<![{keyword}[ <!ELEMENT d EMPTY> ]]>");
assert!(with(doc, &[("d.dtd", &dtd)]).is_ok(), "{keyword}");
}
}
#[test]
fn an_include_sections_declarations_are_read() {
let doc = r#"<!DOCTYPE d SYSTEM "d.dtd"><d>&g;</d>"#;
let dtd = r#"<![INCLUDE[ <!ENTITY g "included"> ]]>"#;
let parsed = with(doc, &[("d.dtd", dtd)]).expect("well-formed");
assert_eq!(parsed.text(parsed.root()), "included");
}
#[test]
fn an_ignore_sections_declarations_are_not() {
let doc = r#"<!DOCTYPE d SYSTEM "d.dtd"><d>text</d>"#;
let dtd = r#"<![IGNORE[ <!ENTITY g "ignored"> ]]><!ELEMENT d (#PCDATA)>"#;
let parsed = with(doc, &[("d.dtd", dtd)]).expect("well-formed");
assert_eq!(parsed.text(parsed.root()), "text");
}
#[test]
fn the_keyword_may_come_from_a_parameter_entity() {
let doc = r#"<!DOCTYPE d SYSTEM "d.dtd"><d>&g;</d>"#;
let included = r#"<!ENTITY % yes "INCLUDE"><![%yes;[ <!ENTITY g "on"> ]]>"#;
let parsed = with(doc, &[("d.dtd", included)]).expect("well-formed");
assert_eq!(parsed.text(parsed.root()), "on");
let bogus = r#"<!ENTITY % maybe "PERHAPS"><![%maybe;[ <!ENTITY g "x"> ]]>"#;
assert!(with(doc, &[("d.dtd", bogus)]).is_err());
}
#[test]
fn a_text_declaration_must_come_first_or_not_at_all() {
let doc = r#"<!DOCTYPE d [<!ENTITY g SYSTEM "g.ent">]><d>&g;</d>"#;
for bad in [
"content first\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
"\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>content",
" <?xml version=\"1.0\" encoding=\"UTF-8\"?>",
] {
assert!(with(doc, &[("g.ent", bad)]).is_err(), "{bad:?}");
}
let good = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>content";
assert!(with(doc, &[("g.ent", good)]).is_ok());
}
#[test]
fn the_text_declaration_keyword_is_lower_case() {
let doc = r#"<!DOCTYPE d [<!ENTITY g SYSTEM "g.ent">]><d>&g;</d>"#;
assert!(with(doc, &[("g.ent", r#"<?XML encoding="UTF-8"?>x"#)]).is_err());
}
#[test]
fn an_entity_is_judged_by_its_own_declared_version() {
let doc = "<?xml version=\"1.1\"?>\
<!DOCTYPE d [<!ENTITY g SYSTEM \"g.ent\">]><d>&g;</d>";
let as_11 = "<?xml version='1.1' encoding='UTF-8'?>a\u{85}b";
let parsed = with(doc, &[("g.ent", as_11)]).expect("1.1 entity");
assert_eq!(parsed.text(parsed.root()), "a\nb", "NEL is a line ending");
let as_10 = "<?xml version='1.0' encoding='UTF-8'?>a\u{85}b";
let parsed = with(doc, &[("g.ent", as_10)]).expect("1.0 entity");
assert_eq!(
parsed.text(parsed.root()),
"a\u{85}b",
"in 1.0 it is an ordinary character, even inside a 1.1 document"
);
}