use crate::MAX_TEMPLATE_NESTING;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TemplateEnd {
Closed(usize),
Unterminated(usize),
}
#[must_use]
pub fn template_end(src: &str, open: usize) -> TemplateEnd {
debug_assert_eq!(src.as_bytes().get(open), Some(&b'`'));
match run(src.as_bytes(), open, 1) {
Ok(end) => TemplateEnd::Closed(end),
Err(stopped) => TemplateEnd::Unterminated(stopped),
}
}
#[must_use]
pub fn string_end(src: &str, open: usize) -> Option<usize> {
debug_assert_eq!(src.as_bytes().get(open), Some(&b'"'));
quoted_run(src.as_bytes(), open, b'"').ok()
}
fn run(bytes: &[u8], open: usize, level: usize) -> Result<usize, usize> {
let mut pos = open + 1; let mut braces = 0usize;
while pos < bytes.len() {
match bytes[pos] {
b'\n' => return Err(pos),
b'\r' if bytes.get(pos + 1) == Some(&b'\n') => return Err(pos),
b'\\' => {
if matches!(bytes.get(pos + 1), Some(b'\n') | None)
|| (bytes.get(pos + 1) == Some(&b'\r') && bytes.get(pos + 2) == Some(&b'\n'))
{
return Err(pos + 1);
}
pos = skip_scalar(bytes, pos + 1);
}
b'"' if braces > 0 => pos = quoted_run(bytes, pos, b'"')?,
b'{' => {
braces += 1;
pos += 1;
}
b'}' => {
braces = braces.saturating_sub(1);
pos += 1;
}
b'`' => {
if braces == 0 || level >= MAX_TEMPLATE_NESTING {
return Ok(pos + 1);
}
pos = run(bytes, pos, level + 1)?;
}
_ => pos = skip_scalar(bytes, pos),
}
}
Err(bytes.len())
}
pub(crate) fn quoted_run(bytes: &[u8], open: usize, terminator: u8) -> Result<usize, usize> {
let mut pos = open + 1;
while pos < bytes.len() {
match bytes[pos] {
b'\n' => return Err(pos),
b'\r' if bytes.get(pos + 1) == Some(&b'\n') => return Err(pos),
b'\\' => {
if matches!(bytes.get(pos + 1), Some(b'\n') | None)
|| (bytes.get(pos + 1) == Some(&b'\r') && bytes.get(pos + 2) == Some(&b'\n'))
{
return Err(pos + 1);
}
pos = skip_scalar(bytes, pos + 1);
}
b if b == terminator => return Ok(pos + 1),
_ => pos = skip_scalar(bytes, pos),
}
}
Err(bytes.len())
}
pub(crate) fn skip_scalar(bytes: &[u8], pos: usize) -> usize {
if pos >= bytes.len() {
return bytes.len();
}
let mut next = pos + 1;
while next < bytes.len() && (bytes[next] & 0xC0) == 0x80 {
next += 1;
}
next
}
#[cfg(test)]
mod tests {
use super::*;
fn nested(n: usize) -> String {
let mut s = String::new();
for _ in 0..n {
s.push_str("`{a:");
}
s.push_str("int");
for _ in 0..n {
s.push_str("}`");
}
s
}
fn nested_quote(n: usize) -> String {
let mut s = String::new();
for _ in 0..n - 1 {
s.push_str("`{a:");
}
s.push_str("`\"`");
for _ in 0..n - 1 {
s.push_str("}`");
}
s
}
fn closed(src: &str) -> bool {
template_end(src, 0) == TemplateEnd::Closed(src.len())
}
#[test]
fn a_delimiter_inside_a_string_is_text() {
for src in [
r#"`{c:one_of("{")}`"#,
r#"`{c:one_of("}")}`"#,
r#"`{s:sep("{", int)}`"#,
r#"`{c:one_of("`")}`"#,
r#"`{c:one_of("\"")}`"#,
r#"`{c:one_of("{{{")}`"#,
] {
assert!(closed(src), "{src}");
}
}
#[test]
fn a_quote_in_literal_text_is_not_a_string() {
assert!(closed(r#"`He said "hi`"#));
assert!(closed(r#"`" {x:int}`"#));
}
#[test]
fn a_nested_template_is_part_of_the_run() {
assert!(closed("`{g:choice(A: `{x:int}`, B: word)}`"));
assert!(closed("`{a:choice(A: `{b:choice(C: `{c:int}`)}`)}`"));
assert!(closed(r"`a\`b`"));
assert!(closed(r"`{a:choice(A: `x\`y`)}`"));
}
#[test]
fn a_run_that_never_closes_is_unterminated() {
assert_eq!(
template_end("`never closes", 0),
TemplateEnd::Unterminated("`never closes".len())
);
assert_eq!(
template_end("`{g:choice(A: `{x:int}`)}", 0),
TemplateEnd::Unterminated("`{g:choice(A: `{x:int}`)}".len())
);
assert_eq!(
template_end(r#"`{c:one_of("abc)}`"#, 0),
TemplateEnd::Unterminated(r#"`{c:one_of("abc)}`"#.len())
);
}
#[test]
fn a_template_ends_at_the_line_it_opens_on() {
assert_eq!(
template_end("`{int\n}\n", 0),
TemplateEnd::Unterminated(5),
"the token is the first line's template, not the rest of the file"
);
assert_eq!(template_end("`{a:int}`\nrest", 0), TemplateEnd::Closed(9));
assert_eq!(template_end("`{int\r\n}", 0), TemplateEnd::Unterminated(5));
assert_eq!(
template_end("`abc\\\ndef`", 0),
TemplateEnd::Unterminated(5)
);
assert_eq!(
template_end("`{g:choice(A: `{x:int}\n)}`", 0),
TemplateEnd::Unterminated(22)
);
assert_eq!(
template_end("`{c:one_of(\"ab\n)}`", 0),
TemplateEnd::Unterminated(14)
);
assert_eq!(
template_end("`{c:one_of(\"ab\\\ncd\")}`", 0),
TemplateEnd::Unterminated(15)
);
assert_eq!(
template_end("`{c:one_of(\"ab\\\r\ncd\")}`", 0),
TemplateEnd::Unterminated(15)
);
}
#[test]
fn nesting_is_bounded_at_max_template_nesting() {
let at_the_bound = nested(MAX_TEMPLATE_NESTING);
assert_eq!(
template_end(&at_the_bound, 0),
TemplateEnd::Closed(at_the_bound.len()),
"a run nested exactly to the bound still closes at its own backtick"
);
let entered = nested_quote(MAX_TEMPLATE_NESTING);
assert_eq!(
template_end(&entered, 0),
TemplateEnd::Closed(entered.len()),
"the {MAX_TEMPLATE_NESTING}th template is entered, so its `\"` is literal text"
);
let not_entered = nested_quote(MAX_TEMPLATE_NESTING + 1);
assert_eq!(
template_end(¬_entered, 0),
TemplateEnd::Unterminated(not_entered.len()),
"one past the bound that template is not entered, so its `\"` is a string"
);
let deep = "`{a:".repeat(5_000);
assert_eq!(
template_end(&deep, 0),
TemplateEnd::Unterminated(deep.len())
);
}
#[test]
fn a_multibyte_scalar_after_a_backslash_is_stepped_over_whole() {
let src = "`a\\λb`";
assert_eq!(template_end(src, 0), TemplateEnd::Closed(src.len()));
assert!(src.is_char_boundary(src.len()));
}
#[test]
fn a_string_ends_at_its_own_unescaped_quote() {
assert_eq!(string_end(r#""ab" rest"#, 0), Some(4));
assert_eq!(string_end(r#""a\"b" rest"#, 0), Some(6));
assert_eq!(string_end(r#""unterminated"#, 0), None);
}
}