Skip to content
Open
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
240 changes: 224 additions & 16 deletions src/parser/lex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ pub enum Error {
#[error("invalid escape character: \\{}", .ch.unwrap_or_default())]
EscapeChar { start: usize, ch: Option<char> },

#[error("invalid unicode escape sequence")]
UnicodeEscape { start: usize, end: usize },

#[error("unexpected parse error")]
UnexpectedParseError(String),
}
Expand All @@ -49,7 +52,7 @@ impl DiagnosticMessage for Error {
fn code(&self) -> usize {
use Error::{
EscapeChar, Literal, NumericLiteral, ParseError, ReservedKeyword, StringLiteral,
UnexpectedParseError,
UnexpectedParseError, UnicodeEscape,
};

match self {
Expand All @@ -66,13 +69,14 @@ impl DiagnosticMessage for Error {
Literal { .. } => 208,
EscapeChar { .. } => 209,
UnexpectedParseError(..) => 210,
UnicodeEscape { .. } => 211,
}
}

fn labels(&self) -> Vec<Label> {
use Error::{
EscapeChar, Literal, NumericLiteral, ParseError, ReservedKeyword, StringLiteral,
UnexpectedParseError,
UnexpectedParseError, UnicodeEscape,
};

fn update_expected(expected: Vec<String>) -> Vec<String> {
Expand Down Expand Up @@ -190,6 +194,11 @@ impl DiagnosticMessage for Error {
)],

UnexpectedParseError(string) => vec![Label::primary(string, Span::default())],

UnicodeEscape { start, end } => vec![Label::primary(
"invalid unicode escape sequence",
Span::new(*start, *end),
)],
}
}
}
Expand Down Expand Up @@ -1224,13 +1233,62 @@ impl<'input> Lexer<'input> {
fn escape_code(&mut self, start: usize) -> Result<(), Error> {
match self.bump() {
Some((_, '\n' | '\'' | '"' | '\\' | 'n' | 'r' | 't' | '{' | '}' | '0')) => Ok(()),
Some((_, 'u')) => self.unicode_escape(start),
Some((start, ch)) => Err(Error::EscapeChar {
start,
ch: Some(ch),
}),
None => Err(Error::EscapeChar { start, ch: None }),
}
}

/// Validates a `\u{HEX}` Unicode escape sequence after the `u` has been consumed.
///
/// `start` is the byte position of the leading `\`. All `UnicodeEscape` errors
/// span from `start` to the current position so the entire `\u{...}` sequence
/// is highlighted in diagnostics.
fn unicode_escape(&mut self, start: usize) -> Result<(), Error> {
match self.bump() {
Some((_, '{')) => {}
Some((s, ch)) => {
return Err(Error::EscapeChar {
start: s,
ch: Some(ch),
});
}
None => return Err(Error::EscapeChar { start, ch: None }),
}
let hex_start = self.next_index();
let mut count = 0usize;
loop {
match self.peek() {
Some((_, '}')) => {
let hex_end = self.next_index();
self.bump();
let end = self.next_index();
if count == 0 {
return Err(Error::UnicodeEscape { start, end });
}
let hex = &self.input[hex_start..hex_end];
let codepoint = u32::from_str_radix(hex, 16)
.map_err(|_| Error::UnicodeEscape { start, end })?;
char::from_u32(codepoint).ok_or(Error::UnicodeEscape { start, end })?;
return Ok(());
}
Some((_, ch)) if ch.is_ascii_hexdigit() => {
self.bump();
count += 1;
}
Some((pos, ch)) => {
return Err(Error::EscapeChar {
start: pos,
ch: Some(ch),
});
}
None => return Err(Error::EscapeChar { start, ch: None }),
}
}
}
}

// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -1269,7 +1327,14 @@ pub(crate) fn is_operator(ch: char) -> bool {
fn unescape_string_literal(mut s: &str) -> String {
let mut string = String::with_capacity(s.len());
while let Some(i) = s.bytes().position(|b| b == b'\\') {
let next = s.as_bytes()[i + 1];
// The lexer never emits a literal ending in a lone backslash (it would
// have escaped the closing quote), so there is always an escape
// character here. Treat a trailing backslash as literal text rather
// than indexing out of bounds if that ever stops holding.
let Some(&next) = s.as_bytes().get(i + 1) else {
debug_assert!(false, "string literal ended with a lone backslash");
break;
};
if next == b'\n' {
// Remove the \n and any ensuing spaces or tabs
string.push_str(&s[..i]);
Expand All @@ -1280,22 +1345,59 @@ fn unescape_string_literal(mut s: &str) -> String {
.map(char::len_utf8)
.sum();
s = &s[i + whitespace + 2..];
} else if next == b'u' {
// `\u{HEX}`: the lexer has already validated the syntax and that the
// codepoint is a legal `char`, so every step below is expected to
// succeed. It is nonetheless written to degrade gracefully rather
// than abort the process, because `template()` rewrites literal
// content before it reaches this function — the invariant spans two
// distant pieces of code, and a future edit there must not become a
// crash here.
let decoded = s.get(i + 3..).and_then(|rest| {
// skipped past `\u{`
let close = rest.find('}')?;
let codepoint = u32::from_str_radix(&rest[..close], 16).ok()?;
let ch = char::from_u32(codepoint)?;
Some((ch, &rest[close + 1..]))
});

if let Some((ch, remainder)) = decoded {
string.push_str(&s[..i]);
string.push(ch);
s = remainder;
} else {
debug_assert!(false, "lexer should have validated this \\u{{..}} escape");
// Keep the backslash as literal text and resume just after it,
// so the loop always makes progress.
string.push_str(&s[..=i]);
s = &s[i + 1..];
}
} else {
let c = match next {
b'\'' => '\'',
b'"' => '"',
b'\\' => '\\',
b'n' => '\n',
b'r' => '\r',
b't' => '\t',
b'0' => '\0',
b'{' => '{',
_ => unimplemented!("invalid escape"),
let unescaped = match next {
b'\'' => Some('\''),
b'"' => Some('"'),
b'\\' => Some('\\'),
b'n' => Some('\n'),
b'r' => Some('\r'),
b't' => Some('\t'),
b'0' => Some('\0'),
b'{' => Some('{'),
b'}' => Some('}'),
_ => None,
};

string.push_str(&s[..i]);
string.push(c);
s = &s[i + 2..];
if let Some(c) = unescaped {
string.push_str(&s[..i]);
string.push(c);
s = &s[i + 2..];
} else {
debug_assert!(false, "lexer should have rejected this escape");
// Unknown escape. Emit the backslash literally and resume after
// it rather than guessing at the intended character (`next` may
// be one byte of a multi-byte character).
string.push_str(&s[..=i]);
s = &s[i + 1..];
}
}
}

Expand Down Expand Up @@ -2146,6 +2248,112 @@ mod test {
);
}

// OBE-10734: \} must unescape to } rather than hitting unimplemented!()
#[test]
fn escaped_close_brace_unescapes_cleanly() {
let token = StringLiteralToken("\\}");
assert_eq!(token.unescape(), "}");
}

#[test]
fn escaped_close_brace_in_string_literal() {
let token = StringLiteralToken("hello\\}world");
assert_eq!(token.unescape(), "hello}world");
}

// OBE-10734 (upstream sync): \u{HEX} unicode escape support
#[test]
fn unicode_escape_basic() {
let token = StringLiteralToken("\\u{41}");
assert_eq!(token.unescape(), "A");
}

#[test]
fn unicode_escape_multibyte() {
// U+1F600 GRINNING FACE
let token = StringLiteralToken("\\u{1F600}");
assert_eq!(token.unescape(), "\u{1F600}");
}

#[test]
fn unicode_escape_in_string() {
let token = StringLiteralToken("hello\\u{20}world");
assert_eq!(token.unescape(), "hello world");
}

#[test]
fn unicode_escape_null() {
let token = StringLiteralToken("\\u{0}");
assert_eq!(token.unescape(), "\0");
}

#[test]
fn unicode_escape_invalid_codepoint_rejected_by_lexer() {
// D800 is a surrogate — escape_code/unicode_escape returns Err so the
// string never reaches unescape_string_literal. Verify via tokenization.
let src = r#""hello\u{D800}world""#;
let mut lexer = Lexer::new(src);
let tokens: Vec<_> = lexer.by_ref().collect();
assert!(
tokens.iter().any(|t| t.is_err()),
"expected a lex error for surrogate codepoint"
);
}

#[test]
fn unicode_escape_empty_braces_rejected_by_lexer() {
let src = r#""\u{}""#;
let mut lexer = Lexer::new(src);
let tokens: Vec<_> = lexer.by_ref().collect();
assert!(
tokens.iter().any(|t| t.is_err()),
"expected a lex error for empty unicode escape"
);
}

#[test]
fn unicode_escape_missing_open_brace_rejected_by_lexer() {
let src = r#""\u41""#;
let mut lexer = Lexer::new(src);
let tokens: Vec<_> = lexer.by_ref().collect();
assert!(
tokens.iter().any(|t| t.is_err()),
"expected a lex error for missing open brace in unicode escape"
);
}

// The other positive `\u{..}` tests construct a `StringLiteralToken` by hand,
// which skips the lexer. `unescape_string_literal` assumes the lexer already
// validated the escape, so lock in that the two really do agree by driving a
// literal through tokenization and *then* unescaping the resulting token.
#[test]
fn unicode_escape_tokenizes_and_unescapes_end_to_end() {
for (src, want) in [
(r#""\u{41}""#, "A"),
(r#""\u{1F600}""#, "\u{1F600}"),
(r#""a\u{20}b\u{9}c""#, "a b\tc"),
(r#""\u{10FFFF}""#, "\u{10FFFF}"), // highest legal codepoint
(r#""\u{000041}""#, "A"), // leading zeros
] {
let tokens: Vec<_> = Lexer::new(src).collect();
assert!(
tokens.iter().all(|t| t.is_ok()),
"{src} should tokenize cleanly, got {tokens:?}"
);

let literal = tokens
.into_iter()
.filter_map(|t| match t {
Ok((_, Token::StringLiteral(literal), _)) => Some(literal),
_ => None,
})
.next()
.unwrap_or_else(|| panic!("{src} should produce a string literal token"));

assert_eq!(literal.unescape(), want, "unescaping {src}");
}
}

#[test]
fn function_closure_no_arg() {
test(
Expand Down
13 changes: 10 additions & 3 deletions src/parsing/xml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,14 @@ fn process_node(node: Node, config: &ParseXmlConfig) -> Value {
);

Value::Object(map)
} else {
// Otherwise, 'flatten' the object by continuing processing.
} else if node.is_text() {
// 'Flatten' the object by continuing processing.
process_node(node, config)
} else {
// Comment or PI as the sole child — return empty object
// rather than forwarding into process_node where it would
// hit an unreachable arm.
Value::Object(BTreeMap::new())
}
}
// For 2+ nodes, expand.
Expand All @@ -186,7 +191,9 @@ fn process_node(node: Node, config: &ParseXmlConfig) -> Value {
}
}
NodeType::Text => process_text(node.text().expect("expected XML text node"), config),
_ => unreachable!("shouldn't be other XML nodes"),
// Comment and PI nodes are skipped by the multi-child filter; reaching here
// means a caller forwarded one directly. Return empty object rather than panic.
NodeType::Comment | NodeType::PI => Value::Object(BTreeMap::new()),
}
}

Expand Down
33 changes: 32 additions & 1 deletion src/stdlib/decrypt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ macro_rules! decrypt_stream {
($algorithm:ty, $plaintext:expr, $key:expr, $iv:expr) => {{
<$algorithm>::new(&GenericArray::from(get_key_bytes($key)?))
.decrypt(&GenericArray::from(get_iv_bytes($iv)?), $plaintext.as_ref())
.expect("key/iv sizes were already checked")
.map_err(|_| "decryption failed (authentication tag mismatch)")?
}};
}

Expand Down Expand Up @@ -383,5 +383,36 @@ mod tests {
want: Ok(value!("morethan1blockofdata")),
tdef: TypeDef::bytes().fallible(),
}

// OBE-10720: bad AEAD ciphertext must return a VRL error, not panic
chacha20_poly1305_bad_tag {
args: func_args![ciphertext: value!(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"), algorithm: "CHACHA20-POLY1305", key: "32_bytes_xxxxxxxxxxxxxxxxxxxxxxx", iv: "12_bytes_xxx"],
want: Err("decryption failed (authentication tag mismatch)"),
tdef: TypeDef::bytes().fallible(),
}

xchacha20_poly1305_bad_tag {
args: func_args![ciphertext: value!(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"), algorithm: "XCHACHA20-POLY1305", key: "32_bytes_xxxxxxxxxxxxxxxxxxxxxxx", iv: "24_bytes_xxxxxxxxxxxxxxx"],
want: Err("decryption failed (authentication tag mismatch)"),
tdef: TypeDef::bytes().fallible(),
}

xsalsa20_poly1305_bad_tag {
args: func_args![ciphertext: value!(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"), algorithm: "XSALSA20-POLY1305", key: "32_bytes_xxxxxxxxxxxxxxxxxxxxxxx", iv: "24_bytes_xxxxxxxxxxxxxxx"],
want: Err("decryption failed (authentication tag mismatch)"),
tdef: TypeDef::bytes().fallible(),
}

aes_128_siv_bad_tag {
args: func_args![ciphertext: value!(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"), algorithm: "AES-128-SIV", key: "32_bytes_xxxxxxxxxxxxxxxxxxxxxxx", iv: "16_bytes_xxxxxxx"],
want: Err("decryption failed (authentication tag mismatch)"),
tdef: TypeDef::bytes().fallible(),
}

aes_256_siv_bad_tag {
args: func_args![ciphertext: value!(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"), algorithm: "AES-256-SIV", key: "64_bytes_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", iv: "16_bytes_xxxxxxx"],
want: Err("decryption failed (authentication tag mismatch)"),
tdef: TypeDef::bytes().fallible(),
}
];
}
Loading