use velox_sfc::parse_sfc;
use velox_sfc::template_parse::MAX_TEMPLATE_DEPTH;
const CEILING: usize = 256;
const BOMB_DEPTH: usize = CEILING + 4;
fn just_over_the_ceiling() -> String {
let mut source = String::from("<template>\n");
for level in 0..BOMB_DEPTH {
source.push_str(&format!("<template v-slot:slot{level}><Slot{level}>\n"));
}
for level in (0..BOMB_DEPTH).rev() {
source.push_str(&format!("</Slot{level}></template>\n"));
}
source.push_str("</template>\n");
source
}
#[test]
fn a_recursion_bomb_is_refused_before_pest_sees_it() {
let source = just_over_the_ceiling();
let err = parse_sfc(&source).expect_err(
"a slot-fragment tree this deep drives one `template_body` frame per level \
in `grammar.pest`, with no ceiling of its own. parse_sfc must refuse it \
before handing it to pest.",
);
assert!(
err.contains("nested `<template` blocks exceed the limit of"),
"the refusal must name what was exceeded and by how much, or the reader \
cannot tell a parse bug from a depth limit: {err}"
);
assert!(
err.contains(&format!("{CEILING}")),
"the refusal must quote the ceiling ({CEILING}), so the reader knows what \
to aim for. A ceiling that does not match this file means the limit moved \
without the test noticing: {err}"
);
}
#[test]
fn a_source_with_multibyte_characters_is_scanned_safely() {
let source = "<template>\n<p>こんにちは 🎉 — em dash, and ünïcödé</p>\n<span>🎉🎉🎉</span>\n</template>\n";
parse_sfc(source).unwrap_or_else(|e| panic!("a Unicode template was refused: {e}"));
}
#[test]
fn a_multibyte_source_is_scanned_safely_even_when_the_guard_fires() {
let mut source = String::from("<template>\n<p>こんにちは 🎉</p>\n");
for level in 0..BOMB_DEPTH {
source.push_str(&format!(
"<template v-slot:slot{level}><Slot{level}>あ 🎉\n"
));
}
for level in (0..BOMB_DEPTH).rev() {
source.push_str(&format!("</Slot{level}></template>\n"));
}
source.push_str("</template>\n");
let err = parse_sfc(&source).expect_err("the depth guard did not fire on a Unicode bomb");
assert!(
err.contains(&format!(
"{} nested `<template` blocks exceed",
BOMB_DEPTH + 1
)),
"every opener must be counted even when the text between them is \
multi-byte: {err}"
);
}
#[test]
fn the_published_ceiling_is_the_one_the_guard_enforces() {
assert_eq!(
CEILING,
velox_sfc::sfc::MAX_NESTED_TEMPLATE_DEPTH,
"`sfc::MAX_NESTED_TEMPLATE_DEPTH` moved. It is part of the crate's API, and \
the bomb fixture and the refusal message are both pinned to it."
);
}
#[test]
fn the_refusal_says_what_to_do_instead() {
let source = just_over_the_ceiling();
let err = parse_sfc(&source).expect_err("the depth guard did not fire");
assert!(
err.contains("nest fewer than"),
"the refusal must offer a way forward, not just report the number: {err}"
);
}
#[test]
fn the_refusal_points_at_the_opening_tag_that_overran_the_limit() {
let source = just_over_the_ceiling();
let err = parse_sfc(&source).expect_err("the depth guard did not fire");
let top_level_line = 1;
let overrun_line = CEILING + 1;
assert!(
err.contains(&format!("{overrun_line} |")),
"expected a caret line for opener #{} — the one that takes the count past \
{CEILING} — but the diagnostic pointed somewhere else:\n{err}",
CEILING + 1
);
assert!(
!err.contains(&format!("{top_level_line} |")),
"the diagnostic points at line {top_level_line}, which is the top-level \
`<template>` every file has. That tag is not the problem and the reader \
can do nothing about it:\n{err}"
);
}
#[test]
fn a_file_that_merely_names_templates_is_not_penalised() {
let filler = "<p>the <template v-slot:x> block nests</p>\n".repeat(400);
let source =
format!("<template>\n{filler}</template>\n<script setup>\n// template\n</script>\n");
parse_sfc(&source).unwrap_or_else(|e| {
panic!("a file with 400 mentions of the word and ONE real opener was refused: {e}")
});
}
#[test]
fn tags_that_only_start_with_template_do_not_count_towards_the_ceiling() {
let filler = "<templates>x</templates>\n".repeat(400);
let source = format!("<template>\n{filler}</template>\n");
parse_sfc(&source).unwrap_or_else(|e| {
panic!("400 `<templates>` tags were counted as 400 nested blocks: {e}")
});
}
#[test]
fn the_grammar_ceiling_is_never_below_the_element_builder_ceiling() {
let grammar_ceiling = std::hint::black_box(velox_sfc::sfc::MAX_NESTED_TEMPLATE_DEPTH);
assert!(
grammar_ceiling >= MAX_TEMPLATE_DEPTH,
"the grammar's nesting ceiling ({grammar_ceiling}) is below the element builder's \
({MAX_TEMPLATE_DEPTH}), so the depth guard refuses files the builder is \
perfectly willing to take",
);
}