Fix unterminated loop in parse_record (#15246)

Fixes #15243

# Description

As noted in #15243, a record with more characters after it (e.g.,
`{a:b}/`) will cause an OOM due to an infinite loop, introduced by
#15023. This happens because the entire string `{a:b}/` is lexed as one
token and passed to `parse_record`, where it repeatedly lexes until it
hits the closing `}`. This PR detects such extra characters and reports
an error.

# User-Facing Changes

`{a:b}/` and other such constructions will no longer cause infinite
loops. Before #15023, you would've seen an "Unclosed delimiter" error
message, but this PR changes that to "Invalid characters."

```
Error: nu::parser::extra_token_after_closing_delimiter

  × Invalid characters after closing delimiter
   ╭─[entry #5:1:7]
 1 │  {a:b}/
   ·       ┬
   ·       ╰── invalid characters
   ╰────
  help: Try removing them.
```

# Tests + Formatting

# After Submitting
This commit is contained in:
Yash Thakur 2025-03-05 15:02:03 -05:00 committed by GitHub
parent 122bcff356
commit b1e591f84c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 30 additions and 2 deletions

View File

@ -5995,10 +5995,12 @@ pub fn parse_record(working_set: &mut StateWorkingSet, span: Span) -> Expression
return garbage(working_set, span);
}
let mut unclosed = false;
let mut extra_tokens = false;
if bytes.ends_with(b"}") {
end -= 1;
} else {
working_set.error(ParseError::Unclosed("}".into(), Span::new(end, end)));
unclosed = true;
}
let inner_span = Span::new(start, end);
@ -6009,7 +6011,12 @@ pub fn parse_record(working_set: &mut StateWorkingSet, span: Span) -> Expression
error: None,
span_offset: start,
};
loop {
while !lex_state.input.is_empty() {
if lex_state.input[0] == b'}' {
extra_tokens = true;
unclosed = false;
break;
}
let additional_whitespace = &[b'\n', b'\r', b','];
if lex_n_tokens(&mut lex_state, additional_whitespace, &[b':'], true, 1) < 1 {
break;
@ -6038,6 +6045,15 @@ pub fn parse_record(working_set: &mut StateWorkingSet, span: Span) -> Expression
}
let (tokens, err) = (lex_state.output, lex_state.error);
if unclosed {
working_set.error(ParseError::Unclosed("}".into(), Span::new(end, end)));
} else if extra_tokens {
working_set.error(ParseError::ExtraTokensAfterClosingDelimiter(Span::new(
lex_state.span_offset + 1,
end,
)));
}
if let Some(err) = err {
working_set.error(err);
}

View File

@ -2818,4 +2818,16 @@ mod record {
_ => panic!("Expected full cell path"),
}
}
/// Regression test for https://github.com/nushell/nushell/issues/15243
#[test]
fn record_terminate_loop() {
let engine_state = EngineState::new();
let mut working_set = StateWorkingSet::new(&engine_state);
parse(&mut working_set, None, b"{a:b}/", false);
assert_eq!(
working_set.parse_errors.first().map(|e| e.to_string()),
Some("Invalid characters after closing delimiter".to_string())
);
}
}