Skip to content

Commit 8d800f6

Browse files
committed
fix(codgen): escape </script for template literals and comments
1 parent 33dcd44 commit 8d800f6

File tree

4 files changed

+67
-27
lines changed

4 files changed

+67
-27
lines changed

crates/oxc_codegen/src/comment.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,15 @@ impl Codegen<'_> {
128128
let comment_source = comment.span.source_text(source_text);
129129
match comment.kind {
130130
CommentKind::Line => {
131-
self.print_str(comment_source);
131+
self.print_str_escaping_slash_script(comment_source);
132132
}
133133
CommentKind::Block => {
134134
// Print block comments with our own indentation.
135135
for line in comment_source.split(is_line_terminator) {
136136
if !line.starts_with("/*") {
137137
self.print_indent();
138138
}
139-
self.print_str(line.trim_start());
139+
self.print_str_escaping_slash_script(line.trim_start());
140140
if !line.ends_with("*/") {
141141
self.print_hard_newline();
142142
}

crates/oxc_codegen/src/gen.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2082,7 +2082,7 @@ impl Gen for TemplateLiteral<'_> {
20822082

20832083
for quasi in &self.quasis {
20842084
p.add_source_mapping(quasi.span);
2085-
p.print_str(quasi.value.raw.as_str());
2085+
p.print_str_escaping_slash_script(quasi.value.raw.as_str());
20862086

20872087
if let Some(expr) = expressions.next() {
20882088
p.print_str("${");

crates/oxc_codegen/src/lib.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,62 @@ impl<'a> Codegen<'a> {
230230
self.code.print_str(s);
231231
}
232232

233+
/// Push str into the buffer, escaping `</script` to `<\/script`.
234+
#[inline]
235+
#[expect(clippy::missing_panics_doc, reason = "infallible")]
236+
pub fn print_str_escaping_slash_script(&mut self, s: &str) {
237+
let slice = s.as_bytes();
238+
let mut consumed = 0;
239+
let mut i = 0;
240+
241+
while i < slice.len() {
242+
if slice[i] != b'<' {
243+
i += 1;
244+
continue;
245+
}
246+
247+
// SAFETY:
248+
// The slice guarantees to be a valid UTF-8 string.
249+
// The consumed index is always pointed to a UTF-8 char boundary.
250+
// Current byte is `<`, thus i - 1 is also at a UTF-8 char boundary.
251+
unsafe {
252+
self.code.print_bytes_unchecked(&slice[consumed..i]);
253+
}
254+
consumed = i;
255+
256+
// We have to check 2nd byte separately as `next8_lower_case == *b"</script"`
257+
// would also match `<\x0Fscript` (0xF | 32 == b'/').
258+
if slice.len() >= 8 + i && slice[i + 1] == b'/' {
259+
// Compiler condenses these operations to an 8-byte read, u64 AND, and u64 compare.
260+
// https://godbolt.org/z/9ndYnbj53
261+
//
262+
// TODO: use the following expect after it is fixed in stable clippy.
263+
// https://github.com/rust-lang/rust-clippy/issues/14534
264+
// #[expect(clippy::missing_panics_doc, reason = "infallible")]
265+
let next8: [u8; 8] = slice[i..i + 8].try_into().unwrap();
266+
let mut next8_lower_case = [0; 8];
267+
for j in 0..8 {
268+
// `| 32` converts ASCII upper case letters to lower case. `<` and `/` are unaffected.
269+
next8_lower_case[j] = next8[j] | 32;
270+
}
271+
if next8_lower_case == *b"</script" {
272+
self.code.print_str("<\\/");
273+
consumed += 2;
274+
}
275+
i += 8;
276+
} else {
277+
i += 1;
278+
}
279+
}
280+
281+
// SAFETY:
282+
// The slice guarantees to be a valid UTF-8 string.
283+
// The consumed index is always pointed to a UTF-8 char boundary.
284+
unsafe {
285+
self.code.print_bytes_unchecked(&slice[consumed..]);
286+
}
287+
}
288+
233289
/// Print a single [`Expression`], adding it to the code generator's
234290
/// internal buffer. Unlike [`Codegen::build`], this does not consume `self`.
235291
#[inline]

crates/oxc_codegen/tests/integration/esbuild.rs

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,7 +1014,6 @@ fn test_jsx_single_line() {
10141014
}
10151015

10161016
#[test]
1017-
#[ignore]
10181017
fn test_avoid_slash_script() {
10191018
// Positive cases
10201019
test("x = '</script'", "x = \"<\\/script\";\n");
@@ -1034,29 +1033,14 @@ fn test_avoid_slash_script() {
10341033
test("//! </script\n//! >/script\n//! /script", "//! <\\/script\n//! >/script\n//! /script\n");
10351034
test("//! </SCRIPT\n//! >/SCRIPT\n//! /SCRIPT", "//! <\\/SCRIPT\n//! >/SCRIPT\n//! /SCRIPT\n");
10361035
test("//! </ScRiPt\n//! >/ScRiPt\n//! /ScRiPt", "//! <\\/ScRiPt\n//! >/ScRiPt\n//! /ScRiPt\n");
1037-
test("/*! </script \n </script */", "/*! <\\/script \n <\\/script */\n");
1038-
test("/*! </SCRIPT \n </SCRIPT */", "/*! <\\/SCRIPT \n <\\/SCRIPT */\n");
1039-
test("/*! </ScRiPt \n </ScRiPt */", "/*! <\\/ScRiPt \n <\\/ScRiPt */\n");
1040-
test(
1041-
"String.raw`</script`",
1042-
"import { __template } from \"<runtime>\";\nvar _a;\nString.raw(_a || (_a = __template([\"<\\/script\"])));\n",
1043-
);
1044-
test(
1045-
"String.raw`</script${a}`",
1046-
"import { __template } from \"<runtime>\";\nvar _a;\nString.raw(_a || (_a = __template([\"<\\/script\", \"\"])), a);\n",
1047-
);
1048-
test(
1049-
"String.raw`${a}</script`",
1050-
"import { __template } from \"<runtime>\";\nvar _a;\nString.raw(_a || (_a = __template([\"\", \"<\\/script\"])), a);\n",
1051-
);
1052-
test(
1053-
"String.raw`</SCRIPT`",
1054-
"import { __template } from \"<runtime>\";\nvar _a;\nString.raw(_a || (_a = __template([\"<\\/SCRIPT\"])));\n",
1055-
);
1056-
test(
1057-
"String.raw`</ScRiPt`",
1058-
"import { __template } from \"<runtime>\";\nvar _a;\nString.raw(_a || (_a = __template([\"<\\/ScRiPt\"])));\n",
1059-
);
1036+
test("/*! </script \n</script */", "/*! <\\/script \n<\\/script */");
1037+
test("/*! </SCRIPT \n</SCRIPT */", "/*! <\\/SCRIPT \n<\\/SCRIPT */");
1038+
test("/*! </ScRiPt \n</ScRiPt */", "/*! <\\/ScRiPt \n<\\/ScRiPt */");
1039+
test("String.raw`</script`", "String.raw`<\\/script`;\n");
1040+
test("String.raw`</script${a}`", "String.raw`<\\/script${a}`;\n");
1041+
test("String.raw`${a}</script`", "String.raw`${a}<\\/script`;\n");
1042+
test("String.raw`</SCRIPT`", "String.raw`<\\/SCRIPT`;\n");
1043+
test("String.raw`</ScRiPt`", "String.raw`<\\/ScRiPt`;\n");
10601044

10611045
// Negative cases
10621046
test("x = '</'", "x = \"</\";\n");

0 commit comments

Comments
 (0)