diff --git a/modules/charset/escape.go b/modules/charset/escape.go index d2e8fb0d870..9883700e886 100644 --- a/modules/charset/escape.go +++ b/modules/charset/escape.go @@ -63,6 +63,7 @@ func EscapeControlBytes(text []byte) (EscapeStatus, []byte) { func EscapeControlReader(text io.Reader, output io.Writer) (escaped EscapeStatus, err error) { buf := make([]byte, 4096) readStart := 0 + runeCount := 0 var n int var writePos int @@ -79,6 +80,8 @@ readingloop: for i < len(bs) { r, size := utf8.DecodeRune(bs[i:]) + runeCount++ + // Now handle the codepoints switch { case r == utf8.RuneError: @@ -113,6 +116,8 @@ readingloop: lineHasRTLScript = false lineHasLTRScript = false + case runeCount == 1 && r == 0xFEFF: // UTF BOM + // the first BOM is safe case r == '\r' || r == '\t' || r == ' ': // These are acceptable control characters and space characters case unicode.IsSpace(r): @@ -144,7 +149,8 @@ readingloop: return } writePos = i + size - case unicode.Is(unicode.C, r): + // 65279 == BOM rune. + case unicode.Is(unicode.C, r) && r != rune(65279): escaped.Escaped = true escaped.HasControls = true if writePos < i { diff --git a/modules/charset/escape_test.go b/modules/charset/escape_test.go index 1804381413b..01ccca77249 100644 --- a/modules/charset/escape_test.go +++ b/modules/charset/escape_test.go @@ -129,6 +129,14 @@ then resh (ר), and finally heh (ה) (which should appear leftmost).`, "\n" + `if access_level != "user` + "\u202e" + ` ` + "\u2066" + `// Check if admin` + "\u2069" + ` ` + "\u2066" + `" {` + "\n", status: EscapeStatus{Escaped: true, HasBIDI: true, BadBIDI: true, HasLTRScript: true, HasRTLScript: true}, }, + { + // UTF-8/16/32 all use the same codepoint for BOM + // Gitea could read UTF-16/32 content and convert into UTF-8 internally then render it, so we only process UTF-8 internally + name: "UTF BOM", + text: "\xef\xbb\xbftest", + result: "\xef\xbb\xbftest", + status: EscapeStatus{HasLTRScript: true}, + }, } func TestEscapeControlString(t *testing.T) { @@ -163,10 +171,18 @@ func TestEscapeControlReader(t *testing.T) { // lets add some control characters to the tests tests := make([]escapeControlTest, 0, len(escapeControlTests)*3) copy(tests, escapeControlTests) + + // if there is a BOM, we should keep the BOM + addPrefix := func(prefix, s string) string { + if strings.HasPrefix(s, "\xef\xbb\xbf") { + return s[:3] + prefix + s[3:] + } + return prefix + s + } for _, test := range escapeControlTests { test.name += " (+Control)" - test.text = "\u001E" + test.text - test.result = `` + "\u001e" + `` + test.result + test.text = addPrefix("\u001E", test.text) + test.result = addPrefix(``+"\u001e"+``, test.result) test.status.Escaped = true test.status.HasControls = true tests = append(tests, test) @@ -174,8 +190,8 @@ func TestEscapeControlReader(t *testing.T) { for _, test := range escapeControlTests { test.name += " (+Mark)" - test.text = "\u0300" + test.text - test.result = `` + "\u0300" + `` + test.result + test.text = addPrefix("\u0300", test.text) + test.result = addPrefix(``+"\u0300"+``, test.result) test.status.Escaped = true test.status.HasMarks = true tests = append(tests, test)