Skip to content

Commit

Permalink
fix #1509: make </script escape case-insensitive
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Aug 12, 2021
1 parent a9456df commit b9445a5
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 13 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

This mirrors how the JS code generator similarly avoids the character sequence `</script`.

In addition, the check that escapes `</style` and `</script` is now case-insensitive to match how the browser's HTML parser behaves. So `</STYLE` and `</SCRIPT` are now escaped as well.

* Fix a TypeScript parsing edge case with ASI (Automatic Semicolon Insertion) ([#1512](https://github.com/evanw/esbuild/issues/1512))

This fixes a parsing bug where TypeScript types consisting of multiple identifiers joined together with a `.` could incorrectly extend onto the next line if the next line started with `<`. This problem was due to ASI; esbuild should be automatically inserting a semicolon at the end of the line:
Expand Down
2 changes: 1 addition & 1 deletion internal/css_printer/css_printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ func (p *printer) printQuotedWithQuote(text string, quote byte) {

case '/':
// Avoid generating the sequence "</style" in CSS code
if i >= 1 && text[i-1] == '<' && strings.HasPrefix(text[i+1:], "style") {
if i >= 1 && text[i-1] == '<' && i+6 <= len(text) && strings.EqualFold(text[i+1:i+6], "style") {
escape = escapeBackslash
}

Expand Down
4 changes: 4 additions & 0 deletions internal/css_printer/css_printer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,11 @@ func TestStringQuote(t *testing.T) {
expectPrintedString(t, "</script>", "\"</script>\"")
expectPrintedString(t, "</style>", "\"<\\/style>\"")
expectPrintedString(t, "</style", "\"<\\/style\"")
expectPrintedString(t, "</STYLE", "\"<\\/STYLE\"")
expectPrintedString(t, "</StYlE", "\"<\\/StYlE\"")
expectPrintedString(t, ">/style", "\">/style\"")
expectPrintedString(t, ">/STYLE", "\">/STYLE\"")
expectPrintedString(t, ">/StYlE", "\">/StYlE\"")
}

func TestURLQuote(t *testing.T) {
Expand Down
10 changes: 5 additions & 5 deletions internal/js_lexer/js_lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2840,7 +2840,7 @@ func StringToUTF16(text string) []uint16 {
}

func UTF16ToString(text []uint16) string {
temp := make([]byte, utf8.UTFMax)
var temp [utf8.UTFMax]byte
b := strings.Builder{}
n := len(text)
for i := 0; i < n; i++ {
Expand All @@ -2851,14 +2851,14 @@ func UTF16ToString(text []uint16) string {
i++
}
}
width := encodeWTF8Rune(temp, r1)
width := encodeWTF8Rune(temp[:], r1)
b.Write(temp[:width])
}
return b.String()
}

func UTF16ToStringWithValidation(text []uint16) (string, uint16, bool) {
temp := make([]byte, utf8.UTFMax)
var temp [utf8.UTFMax]byte
b := strings.Builder{}
n := len(text)
for i := 0; i < n; i++ {
Expand All @@ -2877,7 +2877,7 @@ func UTF16ToStringWithValidation(text []uint16) (string, uint16, bool) {
} else if r1 >= 0xDC00 && r1 <= 0xDFFF {
return "", uint16(r1), false
}
width := encodeWTF8Rune(temp, r1)
width := encodeWTF8Rune(temp[:], r1)
b.Write(temp[:width])
}
return b.String(), 0, true
Expand All @@ -2889,7 +2889,7 @@ func UTF16EqualsString(text []uint16, str string) bool {
// Strings can't be equal if UTF-16 encoding is longer than UTF-8 encoding
return false
}
temp := [utf8.UTFMax]byte{}
var temp [utf8.UTFMax]byte
n := len(text)
j := 0
for i := 0; i < n; i++ {
Expand Down
18 changes: 16 additions & 2 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -10515,6 +10515,20 @@ func (p *parser) mangleTemplate(loc logger.Loc, e *js_ast.ETemplate) js_ast.Expr
return js_ast.Expr{Loc: loc, Data: e}
}

func containsClosingScriptTag(text string) bool {
for {
i := strings.Index(text, "</")
if i < 0 {
break
}
text = text[i+2:]
if len(text) >= 6 && strings.EqualFold(text[:6], "script") {
return true
}
}
return false
}

// This function takes "exprIn" as input from the caller and produces "exprOut"
// for the caller to pass along extra data. This is mostly for optional chaining.
func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprOut) {
Expand Down Expand Up @@ -10785,11 +10799,11 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
// Lower tagged template literals that include "</script"
// since we won't be able to escape it without lowering it
if !shouldLowerTemplateLiteral && e.TagOrNil.Data != nil {
if strings.Contains(e.HeadRaw, "</script") {
if containsClosingScriptTag(e.HeadRaw) {
shouldLowerTemplateLiteral = true
} else {
for _, part := range e.Parts {
if strings.Contains(part.TailRaw, "</script") {
if containsClosingScriptTag(part.TailRaw) {
shouldLowerTemplateLiteral = true
break
}
Expand Down
44 changes: 40 additions & 4 deletions internal/js_printer/js_printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,23 @@ func (p *printer) printUnquotedUTF16(text []uint16, quote rune) {

case '/':
// Avoid generating the sequence "</script" in JS code
if i >= 2 && text[i-2] == '<' && i+6 <= len(text) && js_lexer.UTF16EqualsString(text[i:i+6], "script") {
js = append(js, '\\')
if i >= 2 && text[i-2] == '<' && i+6 <= len(text) {
script := "script"
matches := true
for j := 0; j < 6; j++ {
a := text[i+j]
b := uint16(script[j])
if a >= 'A' && a <= 'Z' {
a += 'a' - 'A'
}
if a != b {
matches = false
break
}
}
if matches {
js = append(js, '\\')
}
}
js = append(js, '/')

Expand Down Expand Up @@ -1781,7 +1796,7 @@ func (p *printer) printExpr(expr js_ast.Expr, level js_ast.L, flags printExprFla

if n > 0 {
// Avoid forming a single-line comment or "</script" sequence
if last := buffer[n-1]; last == '/' || (last == '<' && strings.HasPrefix(e.Value, "/script")) {
if last := buffer[n-1]; last == '/' || (last == '<' && len(e.Value) >= 7 && strings.EqualFold(e.Value[:7], "/script")) {
p.print(" ")
}
}
Expand Down Expand Up @@ -2405,9 +2420,30 @@ func (p *printer) printIf(s *js_ast.SIf) {
}
}

func escapeClosingScriptTag(text string) string {
i := strings.Index(text, "</")
if i < 0 {
return text
}
var b strings.Builder
for {
b.WriteString(text[:i+1])
text = text[i+1:]
if len(text) >= 7 && strings.EqualFold(text[:7], "/script") {
b.WriteByte('\\')
}
i = strings.Index(text, "</")
if i < 0 {
break
}
}
b.WriteString(text)
return b.String()
}

func (p *printer) printIndentedComment(text string) {
// Avoid generating a comment containing the character sequence "</script"
text = strings.ReplaceAll(text, "</script", "<\\/script")
text = escapeClosingScriptTag(text)

if strings.HasPrefix(text, "/*") {
// Re-indent multi-line comments
Expand Down
15 changes: 14 additions & 1 deletion internal/js_printer/js_printer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -910,17 +910,30 @@ func TestAvoidSlashScript(t *testing.T) {
// Positive cases
expectPrinted(t, "x = '</script'", "x = \"<\\/script\";\n")
expectPrinted(t, "x = `</script`", "x = `<\\/script`;\n")
expectPrinted(t, "x = `</SCRIPT`", "x = `<\\/SCRIPT`;\n")
expectPrinted(t, "x = `</ScRiPt`", "x = `<\\/ScRiPt`;\n")
expectPrinted(t, "x = `</script${y}`", "x = `<\\/script${y}`;\n")
expectPrinted(t, "x = `${y}</script`", "x = `${y}<\\/script`;\n")
expectPrintedMinify(t, "x = 1 < /script/.exec(y).length", "x=1< /script/.exec(y).length;")
expectPrintedMinify(t, "x = 1 < /SCRIPT/.exec(y).length", "x=1< /SCRIPT/.exec(y).length;")
expectPrintedMinify(t, "x = 1 < /ScRiPt/.exec(y).length", "x=1< /ScRiPt/.exec(y).length;")
expectPrintedMinify(t, "x = 1 << /script/.exec(y).length", "x=1<< /script/.exec(y).length;")
expectPrinted(t, "//! </script", "//! <\\/script\n")
expectPrinted(t, "//! </script\n//! >/script\n//! /script", "//! <\\/script\n//! >/script\n//! /script\n")
expectPrinted(t, "//! </SCRIPT\n//! >/SCRIPT\n//! /SCRIPT", "//! <\\/SCRIPT\n//! >/SCRIPT\n//! /SCRIPT\n")
expectPrinted(t, "//! </ScRiPt\n//! >/ScRiPt\n//! /ScRiPt", "//! <\\/ScRiPt\n//! >/ScRiPt\n//! /ScRiPt\n")
expectPrinted(t, "/*! </script \n </script */", "/*! <\\/script \n <\\/script */\n")
expectPrinted(t, "/*! </SCRIPT \n </SCRIPT */", "/*! <\\/SCRIPT \n <\\/SCRIPT */\n")
expectPrinted(t, "/*! </ScRiPt \n </ScRiPt */", "/*! <\\/ScRiPt \n <\\/ScRiPt */\n")
expectPrinted(t, "String.raw`</script`",
"var _a;\nString.raw(_a || (_a = __template([\"<\\/script\"])));\nimport {\n __template\n} from \"<runtime>\";\n")
expectPrinted(t, "String.raw`</script${a}`",
"var _a;\nString.raw(_a || (_a = __template([\"<\\/script\", \"\"])), a);\nimport {\n __template\n} from \"<runtime>\";\n")
expectPrinted(t, "String.raw`${a}</script`",
"var _a;\nString.raw(_a || (_a = __template([\"\", \"<\\/script\"])), a);\nimport {\n __template\n} from \"<runtime>\";\n")
expectPrinted(t, "String.raw`</SCRIPT`",
"var _a;\nString.raw(_a || (_a = __template([\"<\\/SCRIPT\"])));\nimport {\n __template\n} from \"<runtime>\";\n")
expectPrinted(t, "String.raw`</ScRiPt`",
"var _a;\nString.raw(_a || (_a = __template([\"<\\/ScRiPt\"])));\nimport {\n __template\n} from \"<runtime>\";\n")

// Negative cases
expectPrinted(t, "x = '</'", "x = \"</\";\n")
Expand Down

0 comments on commit b9445a5

Please sign in to comment.