Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'gems/gitlab-utils/lib/gitlab/utils.rb')
-rw-r--r--gems/gitlab-utils/lib/gitlab/utils.rb263
1 files changed, 263 insertions, 0 deletions
diff --git a/gems/gitlab-utils/lib/gitlab/utils.rb b/gems/gitlab-utils/lib/gitlab/utils.rb
new file mode 100644
index 00000000000..4e08ee8fcaf
--- /dev/null
+++ b/gems/gitlab-utils/lib/gitlab/utils.rb
@@ -0,0 +1,263 @@
+# frozen_string_literal: true
+
+require "addressable/uri"
+require "active_support/all"
+require "action_view"
+
+module Gitlab
+ module Utils
+ extend self
+ DoubleEncodingError = Class.new(StandardError)
+
+ def allowlisted?(absolute_path, allowlist)
+ path = absolute_path.downcase
+
+ allowlist.map(&:downcase).any? do |allowed_path|
+ path.start_with?(allowed_path)
+ end
+ end
+
+ def decode_path(encoded_path)
+ decoded = CGI.unescape(encoded_path)
+ if decoded != CGI.unescape(decoded) # rubocop:disable Style/IfUnlessModifier
+ raise DoubleEncodingError, "path #{encoded_path} is not allowed"
+ end
+
+ decoded
+ end
+
+ def force_utf8(str)
+ str.dup.force_encoding(Encoding::UTF_8)
+ end
+
+ 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 # rubocop:disable Style/GuardClause
+ break object
+ else
+ object.concat(char)
+ end
+ end
+
+ truncated + ('0' * (bytes - truncated.bytesize))
+ end
+
+ # Append path to host, making sure there's one single / in between
+ def append_path(host, path)
+ "#{host.to_s.sub(%r{\/+$}, '')}/#{remove_leading_slashes(path)}" # rubocop:disable Style/RedundantRegexpEscape
+ end
+
+ def remove_leading_slashes(str)
+ str.to_s.sub(%r{^/+}, '')
+ end
+
+ # 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
+ def slugify(str)
+ str.downcase
+ .gsub(/[^a-z0-9]/, '-')[0..62]
+ .gsub(/(\A-+|-+\z)/, '')
+ end
+
+ # Converts newlines into HTML line break elements
+ def nlbr(str)
+ ActionView::Base.full_sanitizer.sanitize(+str, tags: []).gsub(/\r?\n/, '<br>').html_safe
+ end
+
+ def remove_line_breaks(str)
+ str.gsub(/\r?\n/, '')
+ end
+
+ def to_boolean(value, default: nil)
+ value = value.to_s if [0, 1].include?(value)
+
+ 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
+
+ def boolean_to_yes_no(bool)
+ if bool
+ 'Yes'
+ else
+ 'No'
+ end
+ end
+
+ # Behaves like `which` on Linux machines: given PATH, try to resolve the given
+ # executable name to an absolute path, or return nil.
+ #
+ # which('ruby') #=> /usr/bin/ruby
+ def which(filename)
+ ENV['PATH']&.split(File::PATH_SEPARATOR)&.each do |path|
+ full_path = File.join(path, filename)
+ return full_path if File.executable?(full_path)
+ end
+
+ nil
+ end
+
+ def try_megabytes_to_bytes(size)
+ Integer(size).megabytes
+ rescue ArgumentError
+ size
+ end
+
+ def bytes_to_megabytes(bytes)
+ bytes.to_f / Numeric::MEGABYTE
+ end
+
+ def ms_to_round_sec(ms)
+ (ms.to_f / 1000).round(6)
+ end
+
+ # Used in EE
+ # Accepts either an Array or a String and returns an array
+ 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
+
+ def deep_indifferent_access(data)
+ case data
+ when Array
+ data.map { |item| deep_indifferent_access(item) }
+ when Hash
+ data.with_indifferent_access
+ else
+ data
+ end
+ end
+
+ def deep_symbolized_access(data)
+ case data
+ when Array
+ data.map { |item| deep_symbolized_access(item) }
+ when Hash
+ data.deep_symbolize_keys
+ else
+ data
+ end
+ end
+
+ def string_to_ip_object(str)
+ return unless str
+
+ IPAddr.new(str)
+ rescue IPAddr::InvalidAddressError
+ end
+
+ # A safe alternative to String#downcase!
+ #
+ # This will make copies of frozen strings but downcase unfrozen
+ # strings in place, reducing allocations.
+ def safe_downcase!(str)
+ if str.frozen?
+ str.downcase
+ else
+ str.downcase! || str
+ end
+ end
+
+ # 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.
+ def parse_url(uri_string)
+ Addressable::URI.parse(uri_string)
+ rescue Addressable::URI::InvalidURIError, TypeError
+ end
+
+ def add_url_parameters(url, params)
+ uri = parse_url(url.to_s)
+ uri.query_values = uri.query_values.to_h.merge(params.to_h.stringify_keys)
+ uri.query_values = nil if uri.query_values.empty?
+ uri.to_s
+ end
+
+ def removes_sensitive_data_from_url(uri_string)
+ uri = parse_url(uri_string)
+
+ return unless uri
+ return uri_string unless uri.fragment
+
+ stripped_params = CGI.parse(uri.fragment)
+ if stripped_params['access_token']
+ stripped_params['access_token'] = 'filtered'
+ filtered_query = Addressable::URI.new
+ filtered_query.query_values = stripped_params
+
+ uri.fragment = filtered_query.query
+ end
+
+ uri.to_s
+ end
+
+ # 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[k]]`, and the original
+ # hash can always be reconstructed.
+ #
+ # example:
+ #
+ # multiple_key_invert({ a: 1, b: 2, c: 1 })
+ # # => { 1 => [:a, :c], 2 => [:b] }
+ #
+ 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
+
+ # This sort is stable (see https://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.
+ def stable_sort_by(list)
+ list.sort_by.with_index { |x, idx| [yield(x), idx] }
+ end
+
+ # 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
+ 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?(']]') # rubocop:disable Style/SoleNestedConditional
+ 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
+ end
+end