Skip to content

Commit 5a44be6

Browse files
guillep2klunny
authored andcommitted
Convert files to utf-8 for indexing (#7814)
* Convert files to utf-8 for indexing * Move utf8 functions to modules/base * Bump repoIndexerLatestVersion to 3 * Add tests for base/encoding.go * Changes to pass gosimple * Move UTF8 funcs into new modules/charset package
1 parent c2c35d1 commit 5a44be6

File tree

13 files changed

+371
-166
lines changed

13 files changed

+371
-166
lines changed

integrations/migration-test/migration_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"code.gitea.io/gitea/models"
2121
"code.gitea.io/gitea/models/migrations"
2222
"code.gitea.io/gitea/modules/base"
23+
"code.gitea.io/gitea/modules/charset"
2324
"code.gitea.io/gitea/modules/setting"
2425

2526
"github.com/go-xorm/xorm"
@@ -106,7 +107,7 @@ func readSQLFromFile(version string) (string, error) {
106107
if err != nil {
107108
return "", err
108109
}
109-
return string(base.RemoveBOMIfPresent(bytes)), nil
110+
return string(charset.RemoveBOMIfPresent(bytes)), nil
110111
}
111112

112113
func restoreOldDB(t *testing.T, version string) bool {

models/git_diff.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ import (
1919
"strconv"
2020
"strings"
2121

22-
"code.gitea.io/gitea/modules/base"
22+
"code.gitea.io/gitea/modules/charset"
2323
"code.gitea.io/gitea/modules/git"
2424
"code.gitea.io/gitea/modules/highlight"
2525
"code.gitea.io/gitea/modules/log"
2626
"code.gitea.io/gitea/modules/process"
2727
"code.gitea.io/gitea/modules/setting"
2828
"github.com/Unknwon/com"
2929
"github.com/sergi/go-diff/diffmatchpatch"
30-
"golang.org/x/net/html/charset"
30+
stdcharset "golang.org/x/net/html/charset"
3131
"golang.org/x/text/transform"
3232
)
3333

@@ -641,9 +641,9 @@ func ParsePatch(maxLines, maxLineCharacters, maxFiles int, reader io.Reader) (*D
641641
buf.WriteString("\n")
642642
}
643643
}
644-
charsetLabel, err := base.DetectEncoding(buf.Bytes())
644+
charsetLabel, err := charset.DetectEncoding(buf.Bytes())
645645
if charsetLabel != "UTF-8" && err == nil {
646-
encoding, _ := charset.Lookup(charsetLabel)
646+
encoding, _ := stdcharset.Lookup(charsetLabel)
647647
if encoding != nil {
648648
d := encoding.NewDecoder()
649649
for _, sec := range f.Sections {

models/repo_indexer.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111

1212
"code.gitea.io/gitea/modules/base"
13+
"code.gitea.io/gitea/modules/charset"
1314
"code.gitea.io/gitea/modules/git"
1415
"code.gitea.io/gitea/modules/indexer"
1516
"code.gitea.io/gitea/modules/log"
@@ -207,14 +208,15 @@ func addUpdate(update fileUpdate, repo *Repository, batch rupture.FlushingBatch)
207208
if err != nil {
208209
return err
209210
} else if !base.IsTextFile(fileContents) {
211+
// FIXME: UTF-16 files will probably fail here
210212
return nil
211213
}
212214
indexerUpdate := indexer.RepoIndexerUpdate{
213215
Filepath: update.Filename,
214216
Op: indexer.RepoIndexerOpUpdate,
215217
Data: &indexer.RepoIndexerData{
216218
RepoID: repo.ID,
217-
Content: string(fileContents),
219+
Content: string(charset.ToUTF8DropErrors(fileContents)),
218220
},
219221
}
220222
return indexerUpdate.AddToFlushingBatch(batch)

modules/base/tool.go

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
package base
66

77
import (
8-
"bytes"
98
"crypto/md5"
109
"crypto/rand"
1110
"crypto/sha1"
@@ -26,7 +25,6 @@ import (
2625
"strings"
2726
"time"
2827
"unicode"
29-
"unicode/utf8"
3028

3129
"code.gitea.io/gitea/modules/git"
3230
"code.gitea.io/gitea/modules/log"
@@ -35,12 +33,8 @@ import (
3533

3634
"github.com/Unknwon/com"
3735
"github.com/Unknwon/i18n"
38-
"github.com/gogits/chardet"
3936
)
4037

41-
// UTF8BOM is the utf-8 byte-order marker
42-
var UTF8BOM = []byte{'\xef', '\xbb', '\xbf'}
43-
4438
// EncodeMD5 encodes string to md5 hex value.
4539
func EncodeMD5(str string) string {
4640
m := md5.New()
@@ -68,49 +62,6 @@ func ShortSha(sha1 string) string {
6862
return TruncateString(sha1, 10)
6963
}
7064

71-
// DetectEncoding detect the encoding of content
72-
func DetectEncoding(content []byte) (string, error) {
73-
if utf8.Valid(content) {
74-
log.Debug("Detected encoding: utf-8 (fast)")
75-
return "UTF-8", nil
76-
}
77-
78-
textDetector := chardet.NewTextDetector()
79-
var detectContent []byte
80-
if len(content) < 1024 {
81-
// Check if original content is valid
82-
if _, err := textDetector.DetectBest(content); err != nil {
83-
return "", err
84-
}
85-
times := 1024 / len(content)
86-
detectContent = make([]byte, 0, times*len(content))
87-
for i := 0; i < times; i++ {
88-
detectContent = append(detectContent, content...)
89-
}
90-
} else {
91-
detectContent = content
92-
}
93-
result, err := textDetector.DetectBest(detectContent)
94-
if err != nil {
95-
return "", err
96-
}
97-
if result.Charset != "UTF-8" && len(setting.Repository.AnsiCharset) > 0 {
98-
log.Debug("Using default AnsiCharset: %s", setting.Repository.AnsiCharset)
99-
return setting.Repository.AnsiCharset, err
100-
}
101-
102-
log.Debug("Detected encoding: %s", result.Charset)
103-
return result.Charset, err
104-
}
105-
106-
// RemoveBOMIfPresent removes a UTF-8 BOM from a []byte
107-
func RemoveBOMIfPresent(content []byte) []byte {
108-
if len(content) > 2 && bytes.Equal(content[0:3], UTF8BOM) {
109-
return content[3:]
110-
}
111-
return content
112-
}
113-
11465
// BasicAuthDecode decode basic auth string
11566
func BasicAuthDecode(encoded string) (string, string, error) {
11667
s, err := base64.StdEncoding.DecodeString(encoded)

modules/base/tool_test.go

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -64,42 +64,6 @@ func TestShortSha(t *testing.T) {
6464
assert.Equal(t, "veryverylo", ShortSha("veryverylong"))
6565
}
6666

67-
func TestDetectEncoding(t *testing.T) {
68-
testSuccess := func(b []byte, expected string) {
69-
encoding, err := DetectEncoding(b)
70-
assert.NoError(t, err)
71-
assert.Equal(t, expected, encoding)
72-
}
73-
// utf-8
74-
b := []byte("just some ascii")
75-
testSuccess(b, "UTF-8")
76-
77-
// utf-8-sig: "hey" (with BOM)
78-
b = []byte{0xef, 0xbb, 0xbf, 0x68, 0x65, 0x79}
79-
testSuccess(b, "UTF-8")
80-
81-
// utf-16: "hey<accented G>"
82-
b = []byte{0xff, 0xfe, 0x68, 0x00, 0x65, 0x00, 0x79, 0x00, 0xf4, 0x01}
83-
testSuccess(b, "UTF-16LE")
84-
85-
// iso-8859-1: d<accented e>cor<newline>
86-
b = []byte{0x44, 0xe9, 0x63, 0x6f, 0x72, 0x0a}
87-
encoding, err := DetectEncoding(b)
88-
assert.NoError(t, err)
89-
// due to a race condition in `chardet` library, it could either detect
90-
// "ISO-8859-1" or "IS0-8859-2" here. Technically either is correct, so
91-
// we accept either.
92-
assert.Contains(t, encoding, "ISO-8859")
93-
94-
setting.Repository.AnsiCharset = "placeholder"
95-
testSuccess(b, "placeholder")
96-
97-
// invalid bytes
98-
b = []byte{0xfa}
99-
_, err = DetectEncoding(b)
100-
assert.Error(t, err)
101-
}
102-
10367
func TestBasicAuthDecode(t *testing.T) {
10468
_, _, err := BasicAuthDecode("?")
10569
assert.Equal(t, "illegal base64 data at input byte 0", err.Error())

modules/charset/charset.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright 2014 The Gogs Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package charset
6+
7+
import (
8+
"bytes"
9+
"fmt"
10+
"unicode/utf8"
11+
12+
"code.gitea.io/gitea/modules/log"
13+
"code.gitea.io/gitea/modules/setting"
14+
15+
"github.com/gogits/chardet"
16+
"golang.org/x/net/html/charset"
17+
"golang.org/x/text/transform"
18+
)
19+
20+
// UTF8BOM is the utf-8 byte-order marker
21+
var UTF8BOM = []byte{'\xef', '\xbb', '\xbf'}
22+
23+
// ToUTF8WithErr converts content to UTF8 encoding
24+
func ToUTF8WithErr(content []byte) (string, error) {
25+
charsetLabel, err := DetectEncoding(content)
26+
if err != nil {
27+
return "", err
28+
} else if charsetLabel == "UTF-8" {
29+
return string(RemoveBOMIfPresent(content)), nil
30+
}
31+
32+
encoding, _ := charset.Lookup(charsetLabel)
33+
if encoding == nil {
34+
return string(content), fmt.Errorf("Unknown encoding: %s", charsetLabel)
35+
}
36+
37+
// If there is an error, we concatenate the nicely decoded part and the
38+
// original left over. This way we won't lose data.
39+
result, n, err := transform.Bytes(encoding.NewDecoder(), content)
40+
if err != nil {
41+
result = append(result, content[n:]...)
42+
}
43+
44+
result = RemoveBOMIfPresent(result)
45+
46+
return string(result), err
47+
}
48+
49+
// ToUTF8WithFallback detects the encoding of content and coverts to UTF-8 if possible
50+
func ToUTF8WithFallback(content []byte) []byte {
51+
charsetLabel, err := DetectEncoding(content)
52+
if err != nil || charsetLabel == "UTF-8" {
53+
return RemoveBOMIfPresent(content)
54+
}
55+
56+
encoding, _ := charset.Lookup(charsetLabel)
57+
if encoding == nil {
58+
return content
59+
}
60+
61+
// If there is an error, we concatenate the nicely decoded part and the
62+
// original left over. This way we won't lose data.
63+
result, n, err := transform.Bytes(encoding.NewDecoder(), content)
64+
if err != nil {
65+
return append(result, content[n:]...)
66+
}
67+
68+
return RemoveBOMIfPresent(result)
69+
}
70+
71+
// ToUTF8 converts content to UTF8 encoding and ignore error
72+
func ToUTF8(content string) string {
73+
res, _ := ToUTF8WithErr([]byte(content))
74+
return res
75+
}
76+
77+
// ToUTF8DropErrors makes sure the return string is valid utf-8; attempts conversion if possible
78+
func ToUTF8DropErrors(content []byte) []byte {
79+
charsetLabel, err := DetectEncoding(content)
80+
if err != nil || charsetLabel == "UTF-8" {
81+
return RemoveBOMIfPresent(content)
82+
}
83+
84+
encoding, _ := charset.Lookup(charsetLabel)
85+
if encoding == nil {
86+
return content
87+
}
88+
89+
// We ignore any non-decodable parts from the file.
90+
// Some parts might be lost
91+
var decoded []byte
92+
decoder := encoding.NewDecoder()
93+
idx := 0
94+
for {
95+
result, n, err := transform.Bytes(decoder, content[idx:])
96+
decoded = append(decoded, result...)
97+
if err == nil {
98+
break
99+
}
100+
decoded = append(decoded, ' ')
101+
idx = idx + n + 1
102+
if idx >= len(content) {
103+
break
104+
}
105+
}
106+
107+
return RemoveBOMIfPresent(decoded)
108+
}
109+
110+
// RemoveBOMIfPresent removes a UTF-8 BOM from a []byte
111+
func RemoveBOMIfPresent(content []byte) []byte {
112+
if len(content) > 2 && bytes.Equal(content[0:3], UTF8BOM) {
113+
return content[3:]
114+
}
115+
return content
116+
}
117+
118+
// DetectEncoding detect the encoding of content
119+
func DetectEncoding(content []byte) (string, error) {
120+
if utf8.Valid(content) {
121+
log.Debug("Detected encoding: utf-8 (fast)")
122+
return "UTF-8", nil
123+
}
124+
125+
textDetector := chardet.NewTextDetector()
126+
var detectContent []byte
127+
if len(content) < 1024 {
128+
// Check if original content is valid
129+
if _, err := textDetector.DetectBest(content); err != nil {
130+
return "", err
131+
}
132+
times := 1024 / len(content)
133+
detectContent = make([]byte, 0, times*len(content))
134+
for i := 0; i < times; i++ {
135+
detectContent = append(detectContent, content...)
136+
}
137+
} else {
138+
detectContent = content
139+
}
140+
result, err := textDetector.DetectBest(detectContent)
141+
if err != nil {
142+
return "", err
143+
}
144+
// FIXME: to properly decouple this function the fallback ANSI charset should be passed as an argument
145+
if result.Charset != "UTF-8" && len(setting.Repository.AnsiCharset) > 0 {
146+
log.Debug("Using default AnsiCharset: %s", setting.Repository.AnsiCharset)
147+
return setting.Repository.AnsiCharset, err
148+
}
149+
150+
log.Debug("Detected encoding: %s", result.Charset)
151+
return result.Charset, err
152+
}

0 commit comments

Comments
 (0)