Module: Gitlab::Utils

Extended by:
Utils
Included in:
API::Helpers, Utils
Defined in:
lib/gitlab/utils.rb,
lib/gitlab/utils/gzip.rb,
lib/gitlab/utils/markdown.rb,
lib/gitlab/utils/override.rb,
lib/gitlab/utils/deep_size.rb,
lib/gitlab/utils/measuring.rb,
lib/gitlab/utils/merge_hash.rb,
lib/gitlab/utils/usage_data.rb,
lib/gitlab/utils/inline_hash.rb,
lib/gitlab/utils/strong_memoize.rb,
lib/gitlab/utils/lazy_attributes.rb,
lib/gitlab/utils/safe_inline_hash.rb,
lib/gitlab/utils/log_limited_array.rb,
lib/gitlab/utils/sanitize_node_link.rb,
lib/gitlab/utils/json_size_estimator.rb

Defined Under Namespace

Modules: Gzip, InlineHash, LazyAttributes, LogLimitedArray, Markdown, MergeHash, Override, SanitizeNodeLink, StrongMemoize, UsageData Classes: DeepSize, JsonSizeEstimator, Measuring, SafeInlineHash

Instance Method Summary collapse

Instance Method Details

#allowlisted?(absolute_path, allowlist) ⇒ Boolean

Returns:

  • (Boolean)

25
26
27
28
29
30
31
# File 'lib/gitlab/utils.rb', line 25

def allowlisted?(absolute_path, allowlist)
  path = absolute_path.downcase

  allowlist.map(&:downcase).any? do |allowed_path|
    path.start_with?(allowed_path)
  end
end

#append_path(host, path) ⇒ Object

Append path to host, making sure there's one single / in between


69
70
71
# File 'lib/gitlab/utils.rb', line 69

def append_path(host, path)
  "#{host.to_s.sub(%r{\/+$}, '')}/#{path.to_s.sub(%r{^\/+}, '')}"
end

#boolean_to_yes_no(bool) ⇒ Object


109
110
111
112
113
114
115
# File 'lib/gitlab/utils.rb', line 109

def boolean_to_yes_no(bool)
  if bool
    'Yes'
  else
    'No'
  end
end

#bytes_to_megabytes(bytes) ⇒ Object


144
145
146
# File 'lib/gitlab/utils.rb', line 144

def bytes_to_megabytes(bytes)
  bytes.to_f / Numeric::MEGABYTE
end

#check_allowed_absolute_path!(path, allowlist) ⇒ Object

Raises:

  • (StandardError)

33
34
35
36
37
38
# File 'lib/gitlab/utils.rb', line 33

def check_allowed_absolute_path!(path, allowlist)
  return unless Pathname.new(path).absolute?
  return if allowlisted?(path, allowlist)

  raise StandardError, "path #{path} is not allowed"
end

#check_path_traversal!(path) ⇒ Object

Ensure that the relative path will not traverse outside the base directory We url decode the path to avoid passing invalid paths forward in url encoded format. Also see gitlab.com/gitlab-org/gitlab/-/merge_requests/24223#note_284122580 It also checks for ALT_SEPARATOR aka '' (forward slash)


12
13
14
15
16
17
18
19
20
21
22
23
# File 'lib/gitlab/utils.rb', line 12

def check_path_traversal!(path)
  return unless path.is_a?(String)

  path = decode_path(path)
  path_regex = /(\A(\.{1,2})\z|\A\.\.[\/\\]|[\/\\]\.\.\z|[\/\\]\.\.[\/\\]|\n)/

  if path.match?(path_regex)
    raise PathTraversalAttackError.new('Invalid path')
  end

  path
end

#decode_path(encoded_path) ⇒ Object


40
41
42
43
44
45
46
47
# File 'lib/gitlab/utils.rb', line 40

def decode_path(encoded_path)
  decoded = CGI.unescape(encoded_path)
  if decoded != CGI.unescape(decoded)
    raise StandardError, "path #{encoded_path} is not allowed"
  end

  decoded
end

#deep_indifferent_access(data) ⇒ Object


160
161
162
163
164
165
166
167
168
# File 'lib/gitlab/utils.rb', line 160

def deep_indifferent_access(data)
  if data.is_a?(Array)
    data.map(&method(:deep_indifferent_access))
  elsif data.is_a?(Hash)
    data.with_indifferent_access
  else
    data
  end
end

#ensure_array_from_string(string_or_array) ⇒ Object

Used in EE Accepts either an Array or a String and returns an array


154
155
156
157
158
# File 'lib/gitlab/utils.rb', line 154

def ensure_array_from_string(string_or_array)
  return string_or_array if string_or_array.is_a?(Array)

  string_or_array.split(',').map(&:strip)
end

#ensure_utf8_size(str, bytes:) ⇒ Object

Raises:

  • (ArgumentError)

53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/gitlab/utils.rb', line 53

def ensure_utf8_size(str, bytes:)
  raise ArgumentError, 'Empty string provided!' if str.empty?
  raise ArgumentError, 'Negative string size provided!' if bytes < 0

  truncated = str.each_char.each_with_object(+'') do |char, object|
    if object.bytesize + char.bytesize > bytes
      break object
    else
      object.concat(char)
    end
  end

  truncated + ('0' * (bytes - truncated.bytesize))
end

#force_utf8(str) ⇒ Object


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

def force_utf8(str)
  str.dup.force_encoding(Encoding::UTF_8)
end

#ms_to_round_sec(ms) ⇒ Object


148
149
150
# File 'lib/gitlab/utils.rb', line 148

def ms_to_round_sec(ms)
  (ms.to_f / 1000).round(6)
end

#multiple_key_invert(hash) ⇒ Object

Invert a hash, collecting all keys that map to a given value in an array.

Unlike `Hash#invert`, where the last encountered pair wins, and which has the type `Hash[k, v] => Hash[v, k]`, `multiple_key_invert` does not lose any information, has the type `Hash[k, v] => Hash[v, Array]`, and the original hash can always be reconstructed.

example:

multiple_key_invert({ a: 1, b: 2, c: 1 })
# => { 1 => [:a, :c], 2 => [:b] }

198
199
200
201
202
# File 'lib/gitlab/utils.rb', line 198

def multiple_key_invert(hash)
  hash.flat_map { |k, v| Array.wrap(v).zip([k].cycle) }
    .group_by(&:first)
    .transform_values { |kvs| kvs.map(&:last) }
end

#nlbr(str) ⇒ Object

Converts newlines into HTML line break elements


93
94
95
# File 'lib/gitlab/utils.rb', line 93

def nlbr(str)
  ActionView::Base.full_sanitizer.sanitize(+str, tags: []).gsub(/\r?\n/, '<br>').html_safe
end

#parse_url(uri_string) ⇒ Object

Converts a string to an Addressable::URI object. If the string is not a valid URI, it returns nil. Param uri_string should be a String object. This method returns an Addressable::URI object or nil.


181
182
183
184
# File 'lib/gitlab/utils.rb', line 181

def parse_url(uri_string)
  Addressable::URI.parse(uri_string)
rescue Addressable::URI::InvalidURIError, TypeError
end

#random_stringObject


117
118
119
# File 'lib/gitlab/utils.rb', line 117

def random_string
  Random.rand(Float::MAX.to_i).to_s(36)
end

#remove_line_breaks(str) ⇒ Object


97
98
99
# File 'lib/gitlab/utils.rb', line 97

def remove_line_breaks(str)
  str.gsub(/\r?\n/, '')
end

#slugify(str) ⇒ Object

A slugified version of the string, suitable for inclusion in URLs and domain names. Rules:

* Lowercased
* Anything not matching [a-z0-9-] is replaced with a -
* Maximum length is 63 bytes
* First/Last Character is not a hyphen

80
81
82
83
84
# File 'lib/gitlab/utils.rb', line 80

def slugify(str)
  str.downcase
    .gsub(/[^a-z0-9]/, '-')[0..62]
    .gsub(/(\A-+|-+\z)/, '')
end

#stable_sort_by(list) ⇒ Object

This sort is stable (see en.wikipedia.org/wiki/Sorting_algorithm#Stability) contrary to the bare Ruby sort_by method. Using just sort_by leads to instability across different platforms (e.g., x86_64-linux and x86_64-darwin18) which in turn leads to different sorting results for the equal elements across these platforms. This method uses a list item's original index position to break ties.


210
211
212
# File 'lib/gitlab/utils.rb', line 210

def stable_sort_by(list)
  list.sort_by.with_index { |x, idx| [yield(x), idx] }
end

#string_to_ip_object(str) ⇒ Object


170
171
172
173
174
175
# File 'lib/gitlab/utils.rb', line 170

def string_to_ip_object(str)
  return unless str

  IPAddr.new(str)
rescue IPAddr::InvalidAddressError
end

#to_boolean(value, default: nil) ⇒ Object


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

def to_boolean(value, default: nil)
  return value if [true, false].include?(value)
  return true if value =~ /^(true|t|yes|y|1|on)$/i
  return false if value =~ /^(false|f|no|n|0|off)$/i

  default
end

#to_exclusive_sentence(array) ⇒ Object

Wraps ActiveSupport's Array#to_sentence to convert the given array to a comma-separated sentence joined with localized 'or' Strings instead of 'and'.


88
89
90
# File 'lib/gitlab/utils.rb', line 88

def to_exclusive_sentence(array)
  array.to_sentence(two_words_connector: _(' or '), last_word_connector: _(', or '))
end

#try_megabytes_to_bytes(size) ⇒ Object


138
139
140
141
142
# File 'lib/gitlab/utils.rb', line 138

def try_megabytes_to_bytes(size)
  Integer(size).megabytes
rescue ArgumentError
  size
end

#valid_brackets?(string = '', allow_nested: true) ⇒ Boolean

Check for valid brackets (`[` and `]`) in a string using this aspects:

  • open brackets count == closed brackets count

  • (optionally) reject nested brackets via `allow_nested: false`

  • open / close brackets coherence, eg. ][[] -> invalid

Returns:

  • (Boolean)

218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/gitlab/utils.rb', line 218

def valid_brackets?(string = '', allow_nested: true)
  # remove everything except brackets
  brackets = string.remove(/[^\[\]]/)

  return true if brackets.empty?
  # balanced counts check
  return false if brackets.size.odd?

  unless allow_nested
    # nested brackets check
    return false if brackets.include?('[[') || brackets.include?(']]')
  end

  # open / close brackets coherence check
  untrimmed = brackets
  loop do
    trimmed = untrimmed.gsub('[]', '')
    return true if trimmed.empty?
    return false if trimmed == untrimmed

    untrimmed = trimmed
  end
end

#which(cmd, env = ENV) ⇒ Object

See: stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby Cross-platform way of finding an executable in the $PATH.

which('ruby') #=> /usr/bin/ruby

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

def which(cmd, env = ENV)
  exts = env['PATHEXT'] ? env['PATHEXT'].split(';') : ['']

  env['PATH'].split(File::PATH_SEPARATOR).each do |path|
    exts.each do |ext|
      exe = File.join(path, "#{cmd}#{ext}")
      return exe if File.executable?(exe) && !File.directory?(exe)
    end
  end

  nil
end