Module: Gitlab::EncodingHelper

Constant Summary collapse

ENCODING_CONFIDENCE_THRESHOLD =

This threshold is carefully tweaked to prevent usage of encodings detected by CharlockHolmes with low confidence. If CharlockHolmes confidence is low, we're better off sticking with utf8 encoding. Reason: git diff can return strings with invalid utf8 byte sequences if it truncates a diff in the middle of a multibyte character. In this case CharlockHolmes will try to guess the encoding and will likely suggest an obscure encoding with low confidence. There is a lot more info with this merge request: gitlab.com/gitlab-org/gitlab_git/merge_requests/77#note_4754193

50
UNICODE_REPLACEMENT_CHARACTER =
""
ESCAPED_CHARS =
{
  "a" => "\a", "b" => "\b", "e" => "\e", "f" => "\f",
  "n" => "\n", "r" => "\r", "t" => "\t", "v" => "\v",
  "\"" => "\""
}.freeze

Instance Method Summary collapse

Instance Method Details

#binary_io(str_or_io) ⇒ Object


101
102
103
104
105
106
# File 'lib/gitlab/encoding_helper.rb', line 101

def binary_io(str_or_io)
  io = str_or_io.to_io.dup if str_or_io.respond_to?(:to_io)
  io ||= StringIO.new(str_or_io.to_s.freeze)

  io.tap { |io| io.set_encoding(Encoding::ASCII_8BIT) }
end

#detect_binary?(data, detect = nil) ⇒ Boolean

Returns:

  • (Boolean)

48
49
50
51
# File 'lib/gitlab/encoding_helper.rb', line 48

def detect_binary?(data, detect = nil)
  detect ||= detect_encoding(data)
  detect && detect[:type] == :binary && detect[:confidence] == 100
end

#detect_encoding(data, limit: CharlockHolmes::EncodingDetector::DEFAULT_BINARY_SCAN_LEN, cache_key: nil) ⇒ Object


42
43
44
45
46
# File 'lib/gitlab/encoding_helper.rb', line 42

def detect_encoding(data, limit: CharlockHolmes::EncodingDetector::DEFAULT_BINARY_SCAN_LEN, cache_key: nil)
  return if data.nil?

  CharlockHolmes::EncodingDetector.new(limit).detect(data)
end

#detect_libgit2_binary?(data, cache_key: nil) ⇒ Boolean

EncodingDetector checks the first 1024 * 1024 bytes for NUL byte, libgit2 checks only the first 8000 (github.com/libgit2/libgit2/blob/2ed855a9e8f9af211e7274021c2264e600c0f86b/src/filter.h#L15), which is what we use below to keep a consistent behavior.

Returns:

  • (Boolean)

56
57
58
59
# File 'lib/gitlab/encoding_helper.rb', line 56

def detect_libgit2_binary?(data, cache_key: nil)
  detect = detect_encoding(data, limit: 8000, cache_key: cache_key)
  detect && detect[:type] == :binary
end

#encode!(message) ⇒ Object


20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/gitlab/encoding_helper.rb', line 20

def encode!(message)
  message = force_encode_utf8(message)
  return message if message.valid_encoding?

  # return message if message type is binary
  detect = detect_encoding(message)
  return message.force_encoding("BINARY") if detect_binary?(message, detect)

  if detect && detect[:encoding] && detect[:confidence] > ENCODING_CONFIDENCE_THRESHOLD
    # force detected encoding if we have sufficient confidence.
    message.force_encoding(detect[:encoding])
  end

  # encode and clean the bad chars
  message.replace clean(message)
rescue ArgumentError => e
  return unless e.message.include?('unknown encoding name')

  encoding = detect ? detect[:encoding] : "unknown"
  "--broken encoding: #{encoding}"
end

#encode_binary(str) ⇒ Object


95
96
97
98
99
# File 'lib/gitlab/encoding_helper.rb', line 95

def encode_binary(str)
  return "" if str.nil?

  str.dup.force_encoding(Encoding::ASCII_8BIT)
end

#encode_utf8(message, replace: "") ⇒ Object


74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/gitlab/encoding_helper.rb', line 74

def encode_utf8(message, replace: "")
  message = force_encode_utf8(message)
  return message if message.valid_encoding?

  detect = detect_encoding(message)

  if detect && detect[:encoding]
    begin
      CharlockHolmes::Converter.convert(message, detect[:encoding], 'UTF-8')
    rescue ArgumentError => e
      Gitlab::AppLogger.warn("Ignoring error converting #{detect[:encoding]} into UTF8: #{e.message}")

      ''
    end
  else
    clean(message, replace: replace)
  end
rescue ArgumentError
  nil
end

#encode_utf8_no_detect(message) ⇒ Object

This is like encode_utf8 except we skip autodetection of the encoding. We assume the data must be interpreted as UTF-8.


63
64
65
66
67
68
# File 'lib/gitlab/encoding_helper.rb', line 63

def encode_utf8_no_detect(message)
  message = force_encode_utf8(message)
  return message if message.valid_encoding?

  message.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
end

#encode_utf8_with_replacement_character(data) ⇒ Object


70
71
72
# File 'lib/gitlab/encoding_helper.rb', line 70

def encode_utf8_with_replacement_character(data)
  encode_utf8(data, replace: UNICODE_REPLACEMENT_CHARACTER)
end

#unquote_path(filename) ⇒ Object

rubocop:disable Style/AsciiComments `unquote_path` decode filepaths that are returned by some git commands. The path may be returned in double-quotes if it contains special characters, that are encoded in octal. Also, some characters (see `ESCAPED_CHARS`) are escaped. eg. “311240304253305247305200310247306200” (quotes included) is decoded as ɠīŧŀȧƀ

Based on `unquote_c_style` from git source github.com/git/git/blob/v2.35.1/quote.c#L399 rubocop:enable Style/AsciiComments


123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/gitlab/encoding_helper.rb', line 123

def unquote_path(filename)
  return filename unless filename[0] == '"'

  filename = filename[1..-2].gsub(/\\(?:([#{ESCAPED_CHARS.keys.join}\\])|(\d{3}))/) do
    if c = Regexp.last_match(1)
      c == "\\" ? "\\" : ESCAPED_CHARS[c]
    elsif c = Regexp.last_match(2)
      c.to_i(8).chr
    end
  end

  filename.force_encoding("UTF-8")
end