use oxml::{ErrorKind, parse};
#[test]
fn replacement_text_that_is_not_content_is_refused() {
let cases: &[(&str, &str)] = &[
(
"closes an element it did not open",
r#"<!DOCTYPE doc [<!ENTITY e "</foo><foo>">]><doc><foo>&e;</foo></doc>"#,
),
(
"opens an element it does not close",
r#"<!DOCTYPE doc [<!ENTITY e "<foo>">]><doc>&e;</doc>"#,
),
(
"a `<` in an attribute value",
r#"<!DOCTYPE doc [<!ENTITY e "<foo a='<'></foo>">]><doc>&e;</doc>"#,
),
(
"an `&` in an attribute value starting no reference",
r#"<!DOCTYPE doc [<!ENTITY e "<foo a='&'></foo>">]><doc>&e;</doc>"#,
),
(
"a bare `&` in content",
r#"<!DOCTYPE doc [<!ENTITY e "&">]><doc>&e;</doc>"#,
),
(
"a bare `&` in an attribute value",
r#"<!DOCTYPE doc [<!ENTITY e "&">]><doc a="&e;"></doc>"#,
),
(
"a reference assembled across the entity boundary",
r#"<!DOCTYPE doc [<!ENTITY e "&">]><doc>&e;#97;</doc>"#,
),
(
"a character reference assembled across the boundary",
r#"<!DOCTYPE doc [<!ENTITY e "&#9">]><doc>&e;7;</doc>"#,
),
(
"a reserved processing-instruction target",
r#"<!DOCTYPE doc [<!ENTITY e "<?xml encoding='UTF-8'?>">]><doc>&e;</doc>"#,
),
(
"a comment the entity opens and does not close",
r#"<!DOCTYPE doc [<!ENTITY e "<!--">]><doc>&e;--></doc>"#,
),
];
for (why, doc) in cases {
assert!(
parse(doc).is_err(),
"accepted a document that is not well-formed: {why}\n {doc}"
);
}
}
#[test]
fn a_bypassed_reference_is_not_an_included_character() {
let included = r#"<!DOCTYPE doc [<!ENTITY e "&">]><doc>&e;</doc>"#;
assert!(parse(included).is_err(), "a bare `&` is not content");
let bypassed = r#"<!DOCTYPE doc [<!ENTITY e "&">]><doc>&e;</doc>"#;
let doc = parse(bypassed).expect("a reference resolves");
assert_eq!(doc.text(doc.root()), "&");
}
#[test]
fn direct_references_are_characters_not_markup() {
for (doc, expected) in [
("<doc>&</doc>", "&"),
("<doc>&</doc>", "&"),
("<doc><</doc>", "<"),
("<doc>a < b</doc>", "a < b"),
] {
let parsed = parse(doc).unwrap_or_else(|e| {
panic!("{doc:?} is well-formed but was refused: {e}")
});
assert_eq!(parsed.text(parsed.root()), expected, "{doc:?}");
}
for (doc, expected) in [
("<doc a=\"&\"></doc>", "&"),
("<doc a=\"&\"></doc>", "&"),
("<doc a=\"<\"></doc>", "<"),
] {
let parsed = parse(doc).unwrap_or_else(|e| {
panic!("{doc:?} is well-formed but was refused: {e}")
});
let root = parsed.root_element().expect("a root element");
assert_eq!(parsed.attribute(root, "a"), Some(expected), "{doc:?}");
}
}
#[test]
fn well_formed_replacement_text_is_accepted() {
for doc in [
r#"<!DOCTYPE doc [<!ENTITY e "<b/>">]><doc>&e;</doc>"#,
r#"<!DOCTYPE doc [<!ENTITY e "<b>text</b>">]><doc>&e;</doc>"#,
r#"<!DOCTYPE doc [<!ENTITY e "plain text">]><doc>&e;</doc>"#,
r#"<!DOCTYPE doc [<!ENTITY e "<b a='1'/>">]><doc>&e;</doc>"#,
r#"<!DOCTYPE doc [<!ENTITY e "<!-- a comment -->">]><doc>&e;</doc>"#,
r#"<!DOCTYPE doc [<!ENTITY e "<![CDATA[<not markup>]]>">]><doc>&e;</doc>"#,
r#"<!DOCTYPE doc [<!ENTITY a "<b/>"><!ENTITY e "&a;">]><doc>&e;</doc>"#,
] {
let parsed = parse(doc).unwrap_or_else(|e| {
panic!("{doc:?} is well-formed but was refused: {e}")
});
assert!(parsed.root_element().is_some(), "{doc:?} has a root");
}
}
#[test]
fn the_error_points_at_the_reference_not_inside_the_entity() {
let doc = r#"<!DOCTYPE doc [<!ENTITY e "&">]><doc>&e;</doc>"#;
let error = parse(doc).expect_err("not well-formed");
let at = doc.find("&e;").expect("the reference is in the document");
assert_eq!(
error.offset, at,
"offset {} should point at the reference at {at}",
error.offset
);
assert!(
error.offset < doc.len(),
"offset must be inside the document"
);
}
#[test]
fn a_recursive_entity_is_bounded() {
let doc = r#"<!DOCTYPE doc [<!ENTITY e "<b>&e;</b>">]><doc>&e;</doc>"#;
let error = parse(doc).expect_err("must not recurse without bound");
assert!(
matches!(
error.kind,
ErrorKind::EntityLimitExceeded | ErrorKind::UnknownEntity(_)
),
"unexpected: {:?}",
error.kind
);
}