Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,16 @@ pub enum BasicParseErrorKind<'i> {
AtRuleBodyInvalid,
/// A qualified rule was encountered that was invalid.
QualifiedRuleInvalid,
/// We've gone over the nesting limit.
TooManyNestedBlocks,
}

impl fmt::Display for BasicParseErrorKind<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BasicParseErrorKind::TooManyNestedBlocks => {
write!(f, "nesting block limit reached")
}
BasicParseErrorKind::UnexpectedToken(token) => {
write!(f, "unexpected token: {token:?}")
}
Expand Down Expand Up @@ -230,6 +235,8 @@ impl<E: fmt::Display + fmt::Debug> std::error::Error for ParseError<'_, E> {}
pub struct ParserInput<'i> {
tokenizer: Tokenizer<'i>,
cached_token: Option<CachedToken<'i>>,
current_block_depth: u8,
nested_block_limit: u8,
}

struct CachedToken<'i> {
Expand All @@ -239,14 +246,26 @@ struct CachedToken<'i> {
}

impl<'i> ParserInput<'i> {
/// 75 nested blocks seems reasonable enough.
const REASONABLE_NESTED_BLOCK_LIMIT: u8 = 75;

/// Create a new input for a parser.
pub fn new(input: &'i str) -> ParserInput<'i> {
ParserInput {
tokenizer: Tokenizer::new(input),
nested_block_limit: Self::REASONABLE_NESTED_BLOCK_LIMIT,
current_block_depth: 0,
cached_token: None,
}
}

/// Sets a limit for how many nested blocks we're allowed to parse. This is useful to avoid
/// running out of stack space. By default, it's set to `REASONABLE_NESTED_BLOCK_LIMIT`, but it
/// can be overridden or cleared. A limit of 0 will be equivalent to no limit at all.
pub fn set_nested_block_limit(&mut self, limit: u8) {
self.nested_block_limit = limit;
}

#[inline]
fn cached_token_ref(&self) -> &Token<'i> {
&self.cached_token.as_ref().unwrap().token
Expand Down Expand Up @@ -1133,6 +1152,14 @@ where
token was just consumed.\
",
);
if parser.input.current_block_depth >= parser.input.nested_block_limit
&& parser.input.nested_block_limit != 0
{
return Err(parser.new_error(BasicParseErrorKind::TooManyNestedBlocks));
}
// Fine to use wrapping addition, overflow can only occur without a limit.
parser.input.current_block_depth = parser.input.current_block_depth.wrapping_add(1);

let closing_delimiter = match block_type {
BlockType::CurlyBracket => ClosingDelimiter::CloseCurlyBracket,
BlockType::SquareBracket => ClosingDelimiter::CloseSquareBracket,
Expand All @@ -1152,6 +1179,8 @@ where
}
}
consume_until_end_of_block(block_type, &mut parser.input.tokenizer);
// See above.
parser.input.current_block_depth = parser.input.current_block_depth.wrapping_sub(1);
result
}

Expand Down
2 changes: 1 addition & 1 deletion src/size_of_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ size_of_test!(std_cow_str, std::borrow::Cow<'static, str>, 24, 32);
size_of_test!(cow_rc_str, CowRcStr, 16);

size_of_test!(tokenizer, crate::tokenizer::Tokenizer, 96);
size_of_test!(parser_input, crate::parser::ParserInput, 160);
size_of_test!(parser_input, crate::parser::ParserInput, 168);
size_of_test!(parser, crate::parser::Parser, 16);
size_of_test!(source_position, crate::SourcePosition, 8);
size_of_test!(parser_state, crate::ParserState, 24);
Expand Down
45 changes: 45 additions & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,51 @@ fn no_stack_overflow_multiple_nested_blocks() {
while input.next().is_ok() {}
}

#[cfg_attr(all(miri, feature = "skip_long_tests"), ignore)]
#[test]
fn nested_block_limit() {
// Recursively descends into `calc(calc(calc(…1…)))`, which is the shape of expression that
// would blow the stack without a nesting limit.
fn parse_calc<'i>(input: &mut Parser<'i, '_>) -> Result<(), ParseError<'i, ()>> {
if input.try_parse(|input| input.expect_number()).is_ok() {
return Ok(());
}
input.expect_function_matching("calc")?;
input.parse_nested_block(parse_calc)
}

// Returns `Err(())` if (and only if) parsing bailed out due to the nesting limit.
fn parse(depth: usize, limit: Option<u8>) -> Result<(), ()> {
let css = format!("{}1{}", "calc(".repeat(depth), ")".repeat(depth));
let mut input = ParserInput::new(&css);
if let Some(limit) = limit {
input.set_nested_block_limit(limit);
}
Parser::new(&mut input)
.parse_entirely(parse_calc)
.map_err(|e| match e.kind {
ParseErrorKind::Basic(BasicParseErrorKind::TooManyNestedBlocks) => (),
other => panic!(
"Unexpected error parsing {} nested blocks: {:?}",
depth, other
),
})
}

// The default limit is 75 nested blocks.
assert_eq!(parse(75, None), Ok(()));
assert_eq!(parse(76, None), Err(()));
assert_eq!(parse(10_000, None), Err(()));

// The limit is configurable...
assert_eq!(parse(3, Some(3)), Ok(()));
assert_eq!(parse(4, Some(3)), Err(()));
assert_eq!(parse(100, Some(255)), Ok(()));

// ...and a limit of zero means no limit at all.
assert_eq!(parse(1000, Some(0)), Ok(()));
}

impl<'i> DeclarationParser<'i> for JsonParser {
type Declaration = Value;
type Error = ();
Expand Down
Loading