diff --git a/crates/engine/src/dtd.rs b/crates/engine/src/dtd.rs index f72bed311..30a573c23 100644 --- a/crates/engine/src/dtd.rs +++ b/crates/engine/src/dtd.rs @@ -196,8 +196,12 @@ fn expand_parameter_entities(src: &str, ents: &[(String, String)]) -> Result`) never gets anywhere. @@ -300,37 +304,63 @@ pub fn parse(src: &str) -> Result { Ok(dtd) } +/// What `ends` has worked out for one sequence of children: the end positions of a group (by the +/// address of its node, which lives as long as the model) from a position. +type EndsMemo = HashMap<(usize, usize), Vec>; + /// End positions after `cp` matches a prefix of `seq[i..]`. -fn ends(cp: &Cp, seq: &[&str], i: usize) -> Vec { +/// +/// The positions of each group are worked out once per start position (`memo`): nested repetitions +/// such as `((a*)*)*` ask the inner group for the same positions again and again, which is +/// exponential in their depth without it. +fn ends(cp: &Cp, seq: &[&str], i: usize, memo: &mut EndsMemo) -> Vec { + if let Cp::Name(n) = cp { + return if seq.get(i) == Some(&n.as_str()) { vec![i + 1] } else { vec![] }; + } + let key = (std::ptr::from_ref(cp) as usize, i); + if let Some(done) = memo.get(&key) { + return done.clone(); + } let mut out: Vec = match cp { - Cp::Name(n) => { - if seq.get(i) == Some(&n.as_str()) { - vec![i + 1] - } else { - vec![] - } - } + Cp::Name(_) => vec![], // Keep the positions after each item a set: as a list of every path, `(a?, a?, …)` doubles per item. - Cp::Seq(items) => items.iter().fold(vec![i], |at, c| { - let mut next: Vec = at.into_iter().flat_map(|p| ends(c, seq, p)).collect(); - next.sort_unstable(); - next.dedup(); - next - }), - Cp::Choice(items) => items.iter().flat_map(|c| ends(c, seq, i)).collect(), + Cp::Seq(items) => { + let mut at = vec![i]; + for c in items { + let mut next = Vec::new(); + for p in at { + next.extend(ends(c, seq, p, memo)); + } + next.sort_unstable(); + next.dedup(); + at = next; + } + at + } + Cp::Choice(items) => { + let mut all = Vec::new(); + for c in items { + all.extend(ends(c, seq, i, memo)); + } + all + } Cp::Rep(c, r) => { let mut all = if *r == '+' { vec![] } else { vec![i] }; - let mut frontier = ends(c, seq, i); + let mut frontier = ends(c, seq, i, memo); if *r == '?' { all.extend(frontier); } else { while !frontier.is_empty() { let new: Vec = frontier.iter().copied().filter(|p| !all.contains(p)).collect(); all.extend(&new); - frontier = new.into_iter().filter(|&p| p > i).flat_map(|p| ends(c, seq, p)).filter(|p| !all.contains(p)).collect(); + let mut next = Vec::new(); + for p in new.into_iter().filter(|&p| p > i) { + next.extend(ends(c, seq, p, memo).into_iter().filter(|p| !all.contains(p))); + } // Two paths to one position are one position: kept apart they multiply per round. - frontier.sort_unstable(); - frontier.dedup(); + next.sort_unstable(); + next.dedup(); + frontier = next; } } all @@ -338,6 +368,7 @@ fn ends(cp: &Cp, seq: &[&str], i: usize) -> Vec { }; out.sort_unstable(); out.dedup(); + memo.insert(key, out.clone()); out } @@ -425,7 +456,7 @@ fn check(dtd: &Dtd, n: &Node, path: &str, out: &mut Vec) { problem(format!("`{}` can't contain text", n.name)); } let seq: Vec<&str> = n.children.iter().map(|c| c.name.as_str()).collect(); - if !ends(cp, &seq, 0).contains(&seq.len()) { + if !ends(cp, &seq, 0, &mut EndsMemo::new()).contains(&seq.len()) { let got = if seq.is_empty() { "nothing".to_string() } else { seq.join(", ") }; problem(format!("`{}` contains {got}; the DTD asks for {}", n.name, show(cp))); } @@ -592,6 +623,37 @@ mod tests { assert_eq!(expand_parameter_entities("%%%model;%%", &entities), Ok("%%EMPTY%%".to_string())); } + #[test] + fn parameter_entity_with_a_long_name_expands() { + // A name can be any length: the search for the closing `;` has no window. + let name = "n".repeat(300); + let dtd = parse(&format!("")).unwrap(); + assert_eq!(dtd.model("root"), Some(&Model::Empty)); + } + + #[test] + fn percent_in_text_without_a_reference_is_left_alone() { + // `100% sure` (a space ends the search) and a `%` followed by 1 MiB without a `;` are text. + let text = format!("100% sure, %{}", "a".repeat(1 << 20)); + let entities = [("a".to_string(), "X".to_string())]; + assert_eq!(expand_parameter_entities(&text, &entities), Ok(text.clone())); + } + + /// `a` inside `levels` groups, each repeated: `((a)*)*…`. + fn nested_repetitions(levels: usize) -> String { + format!("{}a{}", "(".repeat(levels), ")*".repeat(levels)) + } + + #[test] + fn nested_repetitions_validate_without_going_exponential() { + // Ten levels over 100 children: each level asked the one below for the same positions again + // and again, about 10^14 steps without the memo of `ends`. + let dtd = parse(&format!("", nested_repetitions(10))).unwrap(); + assert_eq!(validate(&dtd, &format!("{}", "".repeat(100))), vec![]); + let problems = validate(&dtd, &format!("{}", "".repeat(100))); + assert_eq!(problems.len(), 1, "{problems:?}"); + } + #[test] fn repetition_of_a_sequence_of_optional_elements_validates() { // `(a, a?, a?, a?, a?, a?)*` over 90 children: every way to split the children among the