use std::fmt::Write as _;
use oxml::{ErrorKind, Limits, parse, parse_with};
fn many_attributes(n: usize) -> String {
(0..n).fold(String::new(), |mut acc, i| {
let _ = write!(acc, " a{i}=\"v\"");
acc
})
}
fn err_kind(src: &str, limits: Limits) -> ErrorKind {
parse_with(src, limits)
.expect_err("should exceed a limit")
.kind
}
#[test]
fn the_defaults_accept_an_ordinary_document() {
let doc = "<library><book id=\"1\" lang=\"en\">\
<title>Dune</title></book></library>";
assert!(parse(doc).is_ok());
assert!(parse_with(doc, Limits::default()).is_ok());
assert!(parse_with(doc, Limits::strict()).is_ok());
assert!(parse_with(doc, Limits::permissive()).is_ok());
}
#[test]
fn parse_and_parse_with_default_agree() {
for src in ["<a/>", "<a><b>x</b></a>", "<a x=\"1\"/>", "<a><!--c--></a>"] {
assert_eq!(
parse(src).is_ok(),
parse_with(src, Limits::default()).is_ok(),
"{src}"
);
}
}
#[test]
fn max_depth_is_enforced_at_its_boundary() {
let mut l = Limits::default();
l.max_depth = 8;
let ok = format!("{}{}", "<a>".repeat(8), "</a>".repeat(8));
assert!(parse_with(&ok, l).is_ok(), "8 deep with a limit of 8");
let over = format!("{}{}", "<a>".repeat(9), "</a>".repeat(9));
assert_eq!(err_kind(&over, l), ErrorKind::DepthLimitExceeded);
}
#[test]
fn max_attributes_per_element_is_enforced() {
let mut l = Limits::default();
l.max_attributes_per_element = 4;
let attrs = |n: usize| format!("<e{}/>", many_attributes(n));
assert!(parse_with(&attrs(4), l).is_ok(), "4 with a limit of 4");
assert_eq!(err_kind(&attrs(5), l), ErrorKind::TooManyAttributes);
}
#[test]
fn max_attribute_size_is_enforced() {
let mut l = Limits::default();
l.max_attribute_size = 16;
let ok = format!("<e a=\"{}\"/>", "x".repeat(16));
assert!(parse_with(&ok, l).is_ok());
let over = format!("<e a=\"{}\"/>", "x".repeat(17));
assert_eq!(err_kind(&over, l), ErrorKind::AttributeTooLarge);
}
#[test]
fn max_name_length_is_enforced_on_elements_and_attributes() {
let mut l = Limits::default();
l.max_name_length = 8;
assert!(parse_with(&format!("<{}/>", "e".repeat(8)), l).is_ok());
assert_eq!(
err_kind(&format!("<{}/>", "e".repeat(9)), l),
ErrorKind::NameTooLong,
"element name"
);
assert_eq!(
err_kind(&format!("<e {}=\"v\"/>", "a".repeat(9)), l),
ErrorKind::NameTooLong,
"attribute name"
);
}
#[test]
fn max_nodes_is_enforced() {
let mut l = Limits::default();
l.max_nodes = Some(10);
assert!(parse_with("<a><b/></a>", l).is_ok());
let many = format!("<a>{}</a>", "<b/>".repeat(1000));
assert_eq!(err_kind(&many, l), ErrorKind::TooManyNodes);
}
#[test]
fn max_text_length_is_enforced() {
let mut l = Limits::default();
l.max_text_length = Some(16);
assert!(parse_with(&format!("<a>{}</a>", "x".repeat(16)), l).is_ok());
assert_eq!(
err_kind(&format!("<a>{}</a>", "x".repeat(17)), l),
ErrorKind::TextTooLong
);
}
#[test]
fn text_length_counts_the_whole_coalesced_run() {
let mut l = Limits::default();
l.max_text_length = Some(16);
let split = format!("<a>{}&{}</a>", "x".repeat(10), "y".repeat(10));
assert_eq!(err_kind(&split, l), ErrorKind::TextTooLong);
}
#[test]
fn strict_limits_reject_what_defaults_accept() {
let deep = format!("{}{}", "<a>".repeat(100), "</a>".repeat(100));
assert!(parse_with(&deep, Limits::default()).is_ok());
assert_eq!(
err_kind(&deep, Limits::strict()),
ErrorKind::DepthLimitExceeded
);
}
#[test]
fn permissive_limits_accept_what_defaults_reject() {
let wide = (0..5_000).fold(String::from("<e"), |mut acc, i| {
let _ = write!(acc, " a{i}=\"v\"");
acc
}) + "/>";
assert_eq!(
err_kind(&wide, Limits::default()),
ErrorKind::TooManyAttributes
);
assert!(parse_with(&wide, Limits::permissive()).is_ok());
}
#[test]
fn permissive_does_not_raise_the_depth_limit() {
assert_eq!(
Limits::permissive().max_depth,
Limits::default().max_depth,
"permissive() must not raise max_depth past the stack ceiling"
);
}
#[test]
fn the_default_depth_limit_stays_under_the_debug_stack_ceiling() {
assert!(
Limits::default().max_depth <= 280,
"default max_depth {} exceeds the measured debug ceiling",
Limits::default().max_depth
);
let at_limit = format!(
"{}{}",
"<a>".repeat(Limits::default().max_depth),
"</a>".repeat(Limits::default().max_depth)
);
assert!(parse(&at_limit).is_ok(), "the default must be reachable");
}
#[test]
fn limits_are_copy_and_adjustable_from_default() {
let mut l = Limits::default();
l.max_depth = 4;
let copy = l;
assert_eq!(copy.max_depth, 4);
assert_eq!(l, copy);
}
#[test]
fn a_hostile_document_fails_fast_under_strict_limits() {
let cases = [
format!("{}{}", "<a>".repeat(100_000), "</a>".repeat(100_000)),
format!("<e{}/>", many_attributes(100_000)),
format!("<a>{}</a>", "<b/>".repeat(1_000_000)),
format!("<a>{}</a>", "x".repeat(50_000_000)),
];
for (i, src) in cases.iter().enumerate() {
assert!(
parse_with(src, Limits::strict()).is_err(),
"case {i} was accepted"
);
}
}
#[test]
fn the_two_xml_1_0_editions_disagree_and_both_are_available() {
let src = "<a\u{0E5C}/>";
assert!(parse(src).is_ok(), "5th edition accepts it");
let mut fourth = Limits::default();
fourth.edition = oxml::Edition::Fourth;
assert!(parse_with(src, fourth).is_err(), "4th edition rejects it");
assert!(parse("<abc/>").is_ok());
assert!(parse_with("<abc/>", fourth).is_ok());
}
#[test]
fn the_fifth_edition_is_the_default() {
assert_eq!(Limits::default().edition, oxml::Edition::Fifth);
assert_eq!(Limits::strict().edition, oxml::Edition::Fifth);
assert_eq!(Limits::permissive().edition, oxml::Edition::Fifth);
}
#[test]
fn a_parameter_entity_width_bomb_is_rejected_by_the_budget() {
let a = format!("<!-- {} -->", "x".repeat(1024));
let doc = format!(
"<!DOCTYPE d SYSTEM \"d.dtd\" [\
<!ENTITY % a \"{a}\">\
<!ENTITY % b \"{}\">\
<!ENTITY % c \"{}\">\
%c;\
]><d/>",
"%a;".repeat(100),
"%b;".repeat(100),
);
assert_eq!(
err_kind(&doc, Limits::default()),
ErrorKind::EntityLimitExceeded
);
}
#[test]
fn a_name_declared_in_the_dtd_is_bounded_by_max_name_length() {
let long = "n".repeat(Limits::default().max_name_length + 1);
for decl in [
format!("<!ELEMENT {long} ANY>"),
format!("<!ATTLIST d {long} CDATA #IMPLIED>"),
format!("<!ENTITY {long} \"v\">"),
format!("<!NOTATION {long} SYSTEM \"s\">"),
] {
let doc = format!("<!DOCTYPE d [{decl}]><d/>");
assert_eq!(
err_kind(&doc, Limits::default()),
ErrorKind::NameTooLong,
"{}",
&decl[..12]
);
}
let fine = format!(
"<!DOCTYPE d [<!ELEMENT {} ANY>]><d/>",
"n".repeat(Limits::default().max_name_length)
);
assert!(parse(&fine).is_ok(), "a name at the limit is accepted");
}
#[test]
fn an_attlist_default_is_bounded_by_max_attribute_size() {
let value = "v".repeat(600 * 1024);
for default in [format!("\"{value}\""), format!("#FIXED \"{value}\"")] {
let doc = format!("<!DOCTYPE d [<!ATTLIST d a CDATA {default}>]><d/>");
assert_eq!(
err_kind(&doc, Limits::default()),
ErrorKind::AttributeTooLarge
);
}
}
#[test]
fn max_dtd_bytes_bounds_the_internal_subset() {
let comment = format!("<!-- {} -->", "c".repeat(1_000));
let subset = comment.repeat(17 * 1024 * 1024 / comment.len() + 1);
let doc = format!("<!DOCTYPE d [{subset}]><d/>");
assert_eq!(err_kind(&doc, Limits::default()), ErrorKind::DtdTooLarge);
}
#[test]
fn permissive_limits_do_not_bound_the_doctype() {
let comment = format!("<!-- {} -->", "c".repeat(1_000));
let subset = comment.repeat(17 * 1024 * 1024 / comment.len() + 1);
let doc = format!("<!DOCTYPE d [{subset}]><d/>");
assert!(
parse_with(&doc, Limits::permissive()).is_ok(),
"permissive() leaves max_dtd_bytes unbounded"
);
let mut tight = Limits::default();
tight.max_pe_expansion = 64;
let doc = "<!DOCTYPE d SYSTEM \"d.dtd\" [\
<!ENTITY % a \"<!-- thirty-two bytes of comment -->\">\
%a;%a;%a;]><d/>";
assert_eq!(err_kind(doc, tight), ErrorKind::EntityLimitExceeded);
assert!(parse(doc).is_ok(), "and the default budget is ample");
}
#[test]
fn the_profiles_order_the_dtd_and_stream_bounds() {
let (s, d, p) = (Limits::strict(), Limits::default(), Limits::permissive());
assert!(s.max_pe_expansion < d.max_pe_expansion);
assert!(d.max_pe_expansion < p.max_pe_expansion);
assert!(s.max_dtd_bytes < d.max_dtd_bytes);
assert_eq!(p.max_dtd_bytes, None);
assert!(s.max_construct_bytes < d.max_construct_bytes);
assert_eq!(p.max_construct_bytes, None);
}