From 2326a220ffe2b04465147acf7371891ca1103a6a Mon Sep 17 00:00:00 2001 From: Gabriel Silva Date: Wed, 28 Oct 2026 15:41:09 -0300 Subject: [PATCH 1/2] fix(parser): stop the lexers from hanging on a NUL character --- .../src/parser/expression/lexer.rs | 7 +++--- .../src/parser/html/lexer.rs | 4 ++++ .../tests/expression_lexer_test.rs | 10 ++++++++ .../tests/html_lexer_test.rs | 24 +++++++++++++++++++ 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/crates/oxc_angular_compiler/src/parser/expression/lexer.rs b/crates/oxc_angular_compiler/src/parser/expression/lexer.rs index f0680c00e..187e0dfc4 100644 --- a/crates/oxc_angular_compiler/src/parser/expression/lexer.rs +++ b/crates/oxc_angular_compiler/src/parser/expression/lexer.rs @@ -454,7 +454,8 @@ impl<'a> Lexer<'a> { /// Advances the index by the current character's byte length and returns the character. fn advance(&mut self) -> char { let ch = self.peek(); - if ch != chars::EOF { + // By the index, not the character: a NUL in the input reads as `EOF` too. + if !self.is_eof() { self.index += ch.len_utf8() as u32; } ch @@ -476,8 +477,8 @@ impl<'a> Lexer<'a> { fn scan_token(&mut self, start: u32) { let ch = self.advance(); - // Whitespace - if chars::is_whitespace(ch) { + // Whitespace. Angular's `scanToken` skips every character up to the space, NUL among them. + if ch <= chars::SPACE || chars::is_whitespace(ch) { return; } diff --git a/crates/oxc_angular_compiler/src/parser/html/lexer.rs b/crates/oxc_angular_compiler/src/parser/html/lexer.rs index ea9e69c8d..1a0027bb9 100644 --- a/crates/oxc_angular_compiler/src/parser/html/lexer.rs +++ b/crates/oxc_angular_compiler/src/parser/html/lexer.rs @@ -706,6 +706,10 @@ impl<'a> HtmlLexer<'a> { /// Tokenizes the input. pub fn tokenize(mut self) -> HtmlTokenizeResult { + // Angular's cursor reads NUL as `$EOF` (0), so the input ends at the first one. + if let Some(nul) = self.input[self.index as usize..self.length as usize].find('\0') { + self.length = self.index + nul as u32; + } while self.index < self.length { self.scan_token(); } diff --git a/crates/oxc_angular_compiler/tests/expression_lexer_test.rs b/crates/oxc_angular_compiler/tests/expression_lexer_test.rs index 8fcdbd6b2..e3c02ba92 100644 --- a/crates/oxc_angular_compiler/tests/expression_lexer_test.rs +++ b/crates/oxc_angular_compiler/tests/expression_lexer_test.rs @@ -1176,6 +1176,16 @@ mod regular_expressions { mod errors { use super::*; + #[test] + fn should_skip_a_nul_character_like_whitespace() { + // Angular's `scanToken` skips every character up to the space. + let tokens = lex("a\0b"); + assert_eq!(tokens.len(), 2); + expect_identifier_token(&tokens[0], 0, 1, "a"); + expect_identifier_token(&tokens[1], 2, 3, "b"); + assert!(lex("'a\0b'").iter().any(super::TokenInfo::is_error)); + } + #[test] fn should_report_unterminated_string() { let tokens = lex(r#""hello"#); diff --git a/crates/oxc_angular_compiler/tests/html_lexer_test.rs b/crates/oxc_angular_compiler/tests/html_lexer_test.rs index b950f3ce6..a278f1f55 100644 --- a/crates/oxc_angular_compiler/tests/html_lexer_test.rs +++ b/crates/oxc_angular_compiler/tests/html_lexer_test.rs @@ -1900,6 +1900,30 @@ mod positions { mod edge_cases { use super::*; + #[test] + fn should_end_the_input_at_a_nul_character() { + // Angular's cursor reads NUL as `$EOF`, so nothing after it is read. + assert_eq!( + tokenize_and_humanize_parts("a\0{{ b }}"), + vec![ + (HtmlTokenType::TagOpenStart, vec![String::new(), "b".to_string()]), + (HtmlTokenType::TagOpenEnd, vec![]), + (HtmlTokenType::Text, vec!["a".to_string()]), + (HtmlTokenType::TagClose, vec![String::new(), "b".to_string()]), + (HtmlTokenType::Eof, vec![]), + ] + ); + assert_eq!( + tokenize_and_humanize_parts("{{ a\0 }}"), + vec![ + (HtmlTokenType::Text, vec![String::new()]), + (HtmlTokenType::Interpolation, vec!["{{".to_string(), " a".to_string()]), + (HtmlTokenType::Text, vec![String::new()]), + (HtmlTokenType::Eof, vec![]), + ] + ); + } + #[test] fn should_handle_empty_input() { let result = tokenize(""); From ecb086085bec83d1162ee91b266dd55e3c7c81c7 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Fri, 30 Oct 2026 14:30:21 +0800 Subject: [PATCH 2/2] fix(parser): stop at a NUL only where Angular's lexers do Angular's cursor peeks a NUL as `$EOF`, but `advance()` still moves past it. So only the loops that check `peek() === $EOF` stop there: the main loop, text, interpolation, tag and attribute names, unquoted values, and block parameters outside quotes. Comments, CDATA, DOCTYPE, raw text (`