-
-
Notifications
You must be signed in to change notification settings - Fork 859
fix(codegen): escape backticks and ${} in template literal raw values
#18102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -386,6 +386,76 @@ impl<'a> Codegen<'a> { | |
| } | ||
| } | ||
|
|
||
| /// Print template literal content, escaping: | ||
| /// - `` ` `` → `` \` `` (unescaped backticks) | ||
| /// - `${` → `\${` (unescaped interpolation markers) | ||
| /// - `</script` → `<\/script` (HTML script close tag) | ||
| /// | ||
| /// For backticks and `${`, we only escape if they're not already escaped | ||
| /// (i.e., not preceded by an odd number of backslashes). | ||
| pub fn print_template_literal_str(&mut self, s: &str) { | ||
| let bytes = s.as_bytes(); | ||
| let len = bytes.len(); | ||
| let mut consumed = 0; | ||
|
|
||
| let mut i = 0; | ||
| while i < len { | ||
| let byte = bytes[i]; | ||
|
|
||
| match byte { | ||
| b'`' => { | ||
| // Check if preceded by odd number of backslashes (already escaped) | ||
| if !is_preceded_by_odd_backslashes(bytes, i) { | ||
| // Flush bytes up to (not including) the backtick | ||
| // SAFETY: `consumed` and `i` are valid indices | ||
| unsafe { | ||
| self.code.print_bytes_unchecked(bytes.get_unchecked(consumed..i)); | ||
| } | ||
| self.print_str("\\`"); | ||
| consumed = i + 1; | ||
| } | ||
| } | ||
| b'$' => { | ||
| // Check if followed by `{` and not already escaped | ||
| if i + 1 < len | ||
| && bytes[i + 1] == b'{' | ||
| && !is_preceded_by_odd_backslashes(bytes, i) | ||
| { | ||
| // Flush bytes up to (not including) the dollar sign | ||
| // SAFETY: `consumed` and `i` are valid indices | ||
| unsafe { | ||
| self.code.print_bytes_unchecked(bytes.get_unchecked(consumed..i)); | ||
| } | ||
| self.print_str("\\$"); | ||
| consumed = i + 1; | ||
| } | ||
| } | ||
| b'<' => { | ||
| // Check for `</script` (case-insensitive) | ||
| if i + 8 <= len && is_script_close_tag(&bytes[i..i + 8]) { | ||
|
||
| // Flush bytes up to and including `<`, then write `\/` | ||
| // SAFETY: `consumed` and `i + 1` are valid indices | ||
| unsafe { | ||
| self.code.print_bytes_unchecked(bytes.get_unchecked(consumed..=i)); | ||
| } | ||
| self.print_str("\\/"); | ||
| consumed = i + 2; // Skip past `</` | ||
| i += 1; // Extra increment to skip `/` | ||
| } | ||
| } | ||
|
Comment on lines
+433
to
+445
|
||
| _ => {} | ||
| } | ||
|
|
||
| i += 1; | ||
| } | ||
|
|
||
| // Flush remaining bytes | ||
| // SAFETY: `consumed` is a valid index | ||
| unsafe { | ||
| self.code.print_bytes_unchecked(bytes.get_unchecked(consumed..)); | ||
| } | ||
| } | ||
|
|
||
| /// Print a single [`Expression`], adding it to the code generator's | ||
| /// internal buffer. Unlike [`Codegen::build`], this does not consume `self`. | ||
| #[inline] | ||
|
|
@@ -394,6 +464,23 @@ impl<'a> Codegen<'a> { | |
| } | ||
| } | ||
|
|
||
| /// Returns true if position `i` in `bytes` is preceded by an odd number of backslashes. | ||
| /// This indicates the character at position `i` is escaped. | ||
| #[inline] | ||
| fn is_preceded_by_odd_backslashes(bytes: &[u8], i: usize) -> bool { | ||
| let mut count = 0; | ||
| let mut j = i; | ||
| while j > 0 { | ||
| j -= 1; | ||
| if bytes[j] == b'\\' { | ||
| count += 1; | ||
| } else { | ||
| break; | ||
| } | ||
| } | ||
| count % 2 == 1 | ||
| } | ||
|
|
||
| // Private APIs | ||
| impl<'a> Codegen<'a> { | ||
| fn code(&self) -> &CodeBuffer { | ||
|
|
@@ -938,3 +1025,99 @@ impl<'a> Codegen<'a> { | |
| #[inline] | ||
| fn add_source_mapping_for_name(&mut self, _span: Span, _name: &str) {} | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod template_literal_escaping_tests { | ||
| use super::*; | ||
|
|
||
| fn escape_template_str(input: &str) -> String { | ||
| let mut codegen = Codegen::new(); | ||
| codegen.print_template_literal_str(input); | ||
| codegen.into_source_text() | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_no_escaping_needed() { | ||
| // Regular strings should pass through unchanged | ||
| assert_eq!(escape_template_str("hello world"), "hello world"); | ||
| assert_eq!(escape_template_str("foo bar baz"), "foo bar baz"); | ||
| assert_eq!(escape_template_str(""), ""); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_backtick_escaping() { | ||
| // Unescaped backticks should be escaped | ||
| assert_eq!(escape_template_str("hello`world"), "hello\\`world"); | ||
| assert_eq!(escape_template_str("`"), "\\`"); | ||
| assert_eq!(escape_template_str("``"), "\\`\\`"); | ||
| assert_eq!(escape_template_str("a`b`c"), "a\\`b\\`c"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_already_escaped_backtick() { | ||
| // Already escaped backticks (preceded by odd backslashes) should NOT be double-escaped | ||
| assert_eq!(escape_template_str("hello\\`world"), "hello\\`world"); | ||
| assert_eq!(escape_template_str("\\`"), "\\`"); | ||
| assert_eq!(escape_template_str("a\\`b"), "a\\`b"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_double_backslash_before_backtick() { | ||
| // Double backslash + backtick: the backslashes escape each other, backtick is NOT escaped | ||
| // So we need to escape the backtick | ||
| assert_eq!(escape_template_str("\\\\`"), "\\\\\\`"); | ||
| assert_eq!(escape_template_str("a\\\\`b"), "a\\\\\\`b"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_triple_backslash_before_backtick() { | ||
| // Triple backslash + backtick: \\ (escaped backslash) + \` (escaped backtick) | ||
| // The backtick IS already escaped, so no change needed | ||
| assert_eq!(escape_template_str("\\\\\\`"), "\\\\\\`"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_dollar_brace_escaping() { | ||
| // Unescaped ${ should be escaped | ||
| assert_eq!(escape_template_str("hello${world"), "hello\\${world"); | ||
| assert_eq!(escape_template_str("${"), "\\${"); | ||
| assert_eq!(escape_template_str("${foo}${bar}"), "\\${foo}\\${bar}"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_dollar_without_brace() { | ||
| // $ not followed by { should NOT be escaped | ||
| assert_eq!(escape_template_str("$"), "$"); | ||
| assert_eq!(escape_template_str("$a"), "$a"); | ||
| assert_eq!(escape_template_str("$ {"), "$ {"); | ||
| assert_eq!(escape_template_str("$}"), "$}"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_already_escaped_dollar_brace() { | ||
| // Already escaped ${ should NOT be double-escaped | ||
| assert_eq!(escape_template_str("\\${"), "\\${"); | ||
| assert_eq!(escape_template_str("a\\${b"), "a\\${b"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_double_backslash_before_dollar_brace() { | ||
| // Double backslash + ${: the backslashes escape each other, ${ is NOT escaped | ||
| assert_eq!(escape_template_str("\\\\${"), "\\\\\\${"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_script_close_tag_escaping() { | ||
| // </script should be escaped to <\/script | ||
| assert_eq!(escape_template_str("</script"), "<\\/script"); | ||
| assert_eq!(escape_template_str("</SCRIPT"), "<\\/SCRIPT"); | ||
| assert_eq!(escape_template_str("</ScRiPt"), "<\\/ScRiPt"); | ||
| assert_eq!(escape_template_str("a</script>b"), "a<\\/script>b"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_combined_escaping() { | ||
| // Multiple escaping scenarios combined | ||
| assert_eq!(escape_template_str("a`b${c}</script>d"), "a\\`b\\${c}<\\/script>d"); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When escaping
${, only the$is being replaced with\\$, butconsumedis set to skip only the$character (i + 1), leaving the{to be printed in the next flush. This will result in\\${being output as\\$followed by{, which produces the incorrect output\\${when it should just be\\${. The logic should either skip both characters (setconsumed = i + 2) or only output\\and let the next iteration handle${.