You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

charset.go 5.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  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. package charset
  5. import (
  6. "bytes"
  7. "fmt"
  8. "io"
  9. "io/ioutil"
  10. "strings"
  11. "unicode/utf8"
  12. "code.gitea.io/gitea/modules/log"
  13. "code.gitea.io/gitea/modules/setting"
  14. "github.com/gogs/chardet"
  15. "golang.org/x/net/html/charset"
  16. "golang.org/x/text/transform"
  17. )
  18. // UTF8BOM is the utf-8 byte-order marker
  19. var UTF8BOM = []byte{'\xef', '\xbb', '\xbf'}
  20. // ToUTF8WithFallbackReader detects the encoding of content and coverts to UTF-8 reader if possible
  21. func ToUTF8WithFallbackReader(rd io.Reader) io.Reader {
  22. var buf = make([]byte, 2048)
  23. n, err := rd.Read(buf)
  24. if err != nil {
  25. return rd
  26. }
  27. charsetLabel, err := DetectEncoding(buf[:n])
  28. if err != nil || charsetLabel == "UTF-8" {
  29. return io.MultiReader(bytes.NewReader(RemoveBOMIfPresent(buf[:n])), rd)
  30. }
  31. encoding, _ := charset.Lookup(charsetLabel)
  32. if encoding == nil {
  33. return io.MultiReader(bytes.NewReader(buf[:n]), rd)
  34. }
  35. return transform.NewReader(
  36. io.MultiReader(
  37. bytes.NewReader(RemoveBOMIfPresent(buf[:n])),
  38. rd,
  39. ),
  40. encoding.NewDecoder(),
  41. )
  42. }
  43. // ToUTF8WithErr converts content to UTF8 encoding
  44. func ToUTF8WithErr(content []byte) (string, error) {
  45. charsetLabel, err := DetectEncoding(content)
  46. if err != nil {
  47. return "", err
  48. } else if charsetLabel == "UTF-8" {
  49. return string(RemoveBOMIfPresent(content)), nil
  50. }
  51. encoding, _ := charset.Lookup(charsetLabel)
  52. if encoding == nil {
  53. return string(content), fmt.Errorf("Unknown encoding: %s", charsetLabel)
  54. }
  55. // If there is an error, we concatenate the nicely decoded part and the
  56. // original left over. This way we won't lose much data.
  57. result, n, err := transform.Bytes(encoding.NewDecoder(), content)
  58. if err != nil {
  59. result = append(result, content[n:]...)
  60. }
  61. result = RemoveBOMIfPresent(result)
  62. return string(result), err
  63. }
  64. // ToUTF8WithFallback detects the encoding of content and coverts to UTF-8 if possible
  65. func ToUTF8WithFallback(content []byte) []byte {
  66. bs, _ := ioutil.ReadAll(ToUTF8WithFallbackReader(bytes.NewReader(content)))
  67. return bs
  68. }
  69. // ToUTF8 converts content to UTF8 encoding and ignore error
  70. func ToUTF8(content string) string {
  71. res, _ := ToUTF8WithErr([]byte(content))
  72. return res
  73. }
  74. // ToUTF8DropErrors makes sure the return string is valid utf-8; attempts conversion if possible
  75. func ToUTF8DropErrors(content []byte) []byte {
  76. charsetLabel, err := DetectEncoding(content)
  77. if err != nil || charsetLabel == "UTF-8" {
  78. return RemoveBOMIfPresent(content)
  79. }
  80. encoding, _ := charset.Lookup(charsetLabel)
  81. if encoding == nil {
  82. return content
  83. }
  84. // We ignore any non-decodable parts from the file.
  85. // Some parts might be lost
  86. var decoded []byte
  87. decoder := encoding.NewDecoder()
  88. idx := 0
  89. for {
  90. result, n, err := transform.Bytes(decoder, content[idx:])
  91. decoded = append(decoded, result...)
  92. if err == nil {
  93. break
  94. }
  95. decoded = append(decoded, ' ')
  96. idx = idx + n + 1
  97. if idx >= len(content) {
  98. break
  99. }
  100. }
  101. return RemoveBOMIfPresent(decoded)
  102. }
  103. // RemoveBOMIfPresent removes a UTF-8 BOM from a []byte
  104. func RemoveBOMIfPresent(content []byte) []byte {
  105. if len(content) > 2 && bytes.Equal(content[0:3], UTF8BOM) {
  106. return content[3:]
  107. }
  108. return content
  109. }
  110. // DetectEncoding detect the encoding of content
  111. func DetectEncoding(content []byte) (string, error) {
  112. if utf8.Valid(content) {
  113. log.Debug("Detected encoding: utf-8 (fast)")
  114. return "UTF-8", nil
  115. }
  116. textDetector := chardet.NewTextDetector()
  117. var detectContent []byte
  118. if len(content) < 1024 {
  119. // Check if original content is valid
  120. if _, err := textDetector.DetectBest(content); err != nil {
  121. return "", err
  122. }
  123. times := 1024 / len(content)
  124. detectContent = make([]byte, 0, times*len(content))
  125. for i := 0; i < times; i++ {
  126. detectContent = append(detectContent, content...)
  127. }
  128. } else {
  129. detectContent = content
  130. }
  131. // Now we can't use DetectBest or just results[0] because the result isn't stable - so we need a tie break
  132. results, err := textDetector.DetectAll(detectContent)
  133. if err != nil {
  134. if err == chardet.NotDetectedError && len(setting.Repository.AnsiCharset) > 0 {
  135. log.Debug("Using default AnsiCharset: %s", setting.Repository.AnsiCharset)
  136. return setting.Repository.AnsiCharset, nil
  137. }
  138. return "", err
  139. }
  140. topConfidence := results[0].Confidence
  141. topResult := results[0]
  142. priority, has := setting.Repository.DetectedCharsetScore[strings.ToLower(strings.TrimSpace(topResult.Charset))]
  143. for _, result := range results {
  144. // As results are sorted in confidence order - if we have a different confidence
  145. // we know it's less than the current confidence and can break out of the loop early
  146. if result.Confidence != topConfidence {
  147. break
  148. }
  149. // Otherwise check if this results is earlier in the DetectedCharsetOrder than our current top guesss
  150. resultPriority, resultHas := setting.Repository.DetectedCharsetScore[strings.ToLower(strings.TrimSpace(result.Charset))]
  151. if resultHas && (!has || resultPriority < priority) {
  152. topResult = result
  153. priority = resultPriority
  154. has = true
  155. }
  156. }
  157. // FIXME: to properly decouple this function the fallback ANSI charset should be passed as an argument
  158. if topResult.Charset != "UTF-8" && len(setting.Repository.AnsiCharset) > 0 {
  159. log.Debug("Using default AnsiCharset: %s", setting.Repository.AnsiCharset)
  160. return setting.Repository.AnsiCharset, err
  161. }
  162. log.Debug("Detected encoding: %s", topResult.Charset)
  163. return topResult.Charset, err
  164. }