use super::{Extras, Token};
use crate::lexer::{LexError, Lexer as _, LogosLexer, StringValueLexError};
use logos::Logos;
fn assert_lexes_safely_with_extras(input: &str, extras: Extras) -> usize {
let mut items = 0usize;
let mut previous_end = 0usize;
let mut lexer = Token::lexer_with_extras(input, extras);
while let Some(result) = lexer.next() {
let span = lexer.span();
items += 1;
assert!(
items <= input.len(),
"the lexer must make progress on {input:?}"
);
assert!(
span.start <= span.end && span.end <= input.len(),
"span {span:?} is out of bounds on {input:?}"
);
assert!(
input.is_char_boundary(span.start) && input.is_char_boundary(span.end),
"span {span:?} is not on a char boundary on {input:?}"
);
assert!(
previous_end <= span.start,
"span {span:?} moves backwards on {input:?}"
);
previous_end = span.end;
if let Err(LexError::StringValueInvalid(errors)) = result {
for error in errors {
let inner = match error {
StringValueLexError::InvalidUnicodeEscapeSequence(span)
| StringValueLexError::InvalidCharacters(span) => span.byte_range(),
};
assert!(
inner.start <= inner.end && inner.end <= input.len(),
"inner span {inner:?} is out of bounds on {input:?}"
);
assert!(
input.is_char_boundary(inner.start) && input.is_char_boundary(inner.end),
"inner span {inner:?} is not on a char boundary on {input:?}"
);
}
}
}
assert_eq!(
"",
lexer.remainder(),
"the lexer must consume the full input on {input:?}"
);
items
}
fn assert_lexes_safely(input: &str) -> usize {
assert_lexes_safely_with_extras(input, Extras::default())
}
fn adversarial_corpus() -> Vec<String> {
let mut corpus: Vec<String> = Vec::new();
for n in 0..=13 {
corpus.push("\"".repeat(n));
corpus.push(format!("{} name", "\"".repeat(n)));
corpus.push(format!("name {}", "\"".repeat(n)));
}
corpus.push("\\".repeat(9));
corpus.push(format!("\"{}\"", "\\".repeat(9)));
corpus.push(format!("\"{}", "\\".repeat(9)));
corpus.push("\"\\".into());
corpus.push("\"\\\"".into());
corpus.push(format!("\"\"\"{}", "\\\"\"\"".repeat(5)));
corpus.push(format!("\"\"\"{}\"\"\"", "\\\"\"\"".repeat(5)));
corpus.push("\"\"\"\\".into());
corpus.push("\"\"\"\\\"".into());
corpus.push("\"\"\"\\\"\"".into());
corpus.push("\"\"\"a\"\"a\"\"\"".into());
corpus.push(format!("\"\\u{{{}}}\"", "0".repeat(1_000)));
corpus.push("\"\\u".into());
corpus.push("\"\\u{".into());
corpus.push("\"\\u{12".into());
corpus.push("\"\\uD800".into());
corpus.push("\"\\uD800\"".into());
corpus.push("\"\\uD800\\u0041\"".into());
corpus.push("\"\\uD83D\\uD83D\"".into());
corpus.push("\"\\uDC00\\uD800\"".into());
corpus.push("\"\\uFFFF\\uFFFF\"".into());
corpus.push("\"\\u{110000}\\u{FFFFFFFF}\"".into());
corpus.push("-".into());
corpus.push("--1".into());
corpus.push("-.5".into());
corpus.push("+1".into());
corpus.push("1e".into());
corpus.push("1e+".into());
corpus.push("1e-".into());
corpus.push("1.".into());
corpus.push("1..2".into());
corpus.push("1.2.3.4".into());
corpus.push("00".into());
corpus.push("01".into());
corpus.push("-0".into());
corpus.push("9".repeat(200));
corpus.push(format!("-{0}.{0}e-{0}", "9".repeat(100)));
corpus.push("123abc_.456".into());
let control_characters: String = (0u8..0x20).map(char::from).collect();
corpus.push(control_characters.clone());
corpus.push(format!("\"{control_characters}\""));
corpus.push(format!("\"\"\"{control_characters}\"\"\""));
corpus.push("\u{7F}".into());
corpus.push("\0".into());
corpus.push("#\0\u{7F}".into());
corpus.push("é".into());
corpus.push("🔥".into());
corpus.push("a\u{FEFF}b".into());
corpus.push("1\u{FEFF}2".into());
corpus.push("\"🔥".into());
corpus.push("\"\"\"🔥".into());
corpus.push("\"\"\"é\né\r🔥\r\n é\"\"\"".into());
corpus.push("é123".into());
corpus.push("123é".into());
corpus.push("$é".into());
corpus.push("#é".into());
corpus.push("\"\"\"a\rb\r\"\"\"".into());
corpus.push("\"a\rb\"".into());
corpus.push("\r".into());
corpus.push("#".into());
corpus.push(format!("#{}", "#".repeat(100)));
corpus.push("$".repeat(100));
corpus.push(".".repeat(100));
corpus.push("..".into());
corpus.push("....".into());
corpus.push(".....!".into());
corpus.push("{\"\\%^&*\0é\"\"\"}".into());
corpus.push("query { field(arg: \"unterminated }".into());
corpus
}
fn kitchen_sink() -> String {
let mut source = String::from("\u{FEFF}");
source.push_str(
"query Kitchen($sink: [Int!] = -0) @dir(a: 1.5e-3, b: \"\\u0041\\u{1F525}\\uD83D\\uDD25\") {\n",
);
source.push_str(
" field(arg: \"\"\"\n block é\n indented\n \\\"\"\"\n \"\"\") # comment\r\n",
);
source.push_str(" ... on Thing { a, b }\r");
source.push_str(" \"string with escapes \\n \\t \\\" \\\\ /\"\n");
source.push_str("}\n");
source
}
#[test]
fn adversarial_corpus_lexes_safely() {
for input in adversarial_corpus() {
assert_lexes_safely(&input);
assert_lexes_safely_with_extras(
&input,
Extras {
graphql_ruby_compatibility: true,
},
);
}
}
#[test]
fn kitchen_sink_lexes_safely() {
let source = kitchen_sink();
let items = assert_lexes_safely(&source);
assert!(items > 0);
}
#[test]
fn truncated_documents_lex_safely() {
let source = kitchen_sink();
for index in 0..=source.len() {
if source.is_char_boundary(index) {
assert_lexes_safely(&source[..index]);
assert_lexes_safely(&source[index..]);
}
}
}
#[test]
fn large_pathological_inputs_terminate() {
let large_inputs = [
"\"".repeat(50_000),
"\\".repeat(50_000),
format!("\"{}\"", "a".repeat(100_000)),
format!("\"{}\"", "\\n".repeat(50_000)),
format!("\"{}", "\\u{1F5".repeat(20_000)),
format!("\"\\u{{{}}}\"", "F".repeat(100_000)),
format!("\"\"\"{}\"\"\"", "x é\n".repeat(25_000)),
format!("\"\"\"{}", "\\\"\"\"".repeat(25_000)),
format!("\"\"\"{}\"\"\"", " \n".repeat(50_000)),
"$".repeat(50_000),
"9 ".repeat(50_000),
"0 ".repeat(50_000),
".".repeat(50_000),
format!("#{}", "c".repeat(100_000)),
"9".repeat(100_000),
format!("-1.{0}e-{0}", "9".repeat(50_000)),
" ".repeat(100_000),
"\t \r\n,".repeat(20_000),
"\u{FEFF}\t \r\n,".repeat(1_000),
];
for input in large_inputs {
assert_lexes_safely(&input);
}
}
#[test]
fn long_token_stack_usage() {
std::thread::Builder::new()
.stack_size(2 * 1024 * 1024)
.spawn(|| {
assert_lexes_safely(&"9".repeat(1_000));
assert_lexes_safely(&format!("-1.{}e-9", "9".repeat(1_000)));
assert_lexes_safely(&" ".repeat(1_000));
assert_lexes_safely(&"\u{FEFF}\t \r\n,".repeat(200));
assert_lexes_safely(&format!("\"{}\"", "é".repeat(500)));
})
.unwrap()
.join()
.unwrap();
}
#[test]
#[ignore = "logos 0.16 overflows the stack on long runs of multi-byte characters in unoptimized builds; try to re-enable after the next logos upgrade"]
fn large_multibyte_runs_terminate() {
let large_inputs = [
format!("\"{}\"", "é".repeat(50_000)),
format!("\"{}\"", "🔥".repeat(25_000)),
"\u{FEFF}".repeat(30_000),
"\u{FEFF}\t \r\n,".repeat(20_000),
];
for input in large_inputs {
assert_lexes_safely(&input);
}
}
#[test]
fn max_tokens_bounds_adversarial_corpus() {
for input in adversarial_corpus() {
let mut lexer = LogosLexer::new(&input).with_max_tokens(Some(8));
let items = (&mut lexer).count();
assert!(
items <= input.len() + 1,
"the lexer must terminate on {input:?}"
);
assert!(
lexer.token_count() <= 9,
"the lexer must stop counting after the limit on {input:?}"
);
}
}