All Files
(82.22%
covered at
38.26
hits/line)
6 files in total.
225 relevant lines.
185 lines covered and
40 lines missed
-
# frozen_string_literal: true
-
-
1
require 'openssl'
-
1
require 'base64'
-
1
require 'chronic_duration'
-
-
1
module BB
-
# Crypto utilities.
-
1
module Crypto
-
1
class << self
-
# Encrypt a String.
-
#
-
# @param [String] plaintext Input String (plaintext)
-
# @param [String] key Encryption key
-
# @param [String] cipher_type OpenSSL cipher
-
# @param [String] iv Initialization vector
-
# @return [String] When iv == nil: iv_length+iv+ciphertext
-
# @return [String] When iv != nil: ciphertext
-
1
def encrypt(plaintext, key, cipher_type = 'aes-256-cbc', iv = nil)
-
702
cipher = OpenSSL::Cipher.new(cipher_type)
-
702
cipher.encrypt
-
702
cipher.key = key
-
700
if iv.nil?
-
430
iv = cipher.random_iv
-
430
[iv.length].pack('C') + iv + cipher.update(plaintext) + cipher.final
-
else
-
270
cipher.iv = iv
-
270
cipher.update(plaintext) + cipher.final
-
end
-
end
-
-
# Decrypt a String.
-
#
-
# @param [String] ciphertext Input String (ciphertext)
-
# @param [String] key Encryption key
-
# @param [String] cipher_type OpenSSL cipher
-
# @param [String] iv Initialization vector
-
# @return [String] Plaintext
-
1
def decrypt(ciphertext, key, cipher_type = 'aes-256-cbc', iv = nil)
-
372
cipher = OpenSSL::Cipher.new(cipher_type)
-
372
cipher.decrypt
-
372
cipher.key = key
-
372
if iv.nil?
-
282
iv_len = ciphertext.slice!(0).unpack('C')[0]
-
282
cipher.iv = ciphertext.slice!(0..iv_len - 1) unless iv_len == 0
-
else
-
90
cipher.iv = iv
-
end
-
372
cipher.update(ciphertext) + cipher.final
-
end
-
-
# Encrypt a String and encode the resulting ciphertext to Base64.
-
#
-
# @param [String] plaintext Input String (plaintext)
-
# @param [String] key Encryption key
-
# @param [String] cipher_type OpenSSL cipher
-
# @param [String] iv Initialization vector
-
# @return [String] When iv == nil: base64(iv_length+iv+ciphertext)
-
# @return [String] When iv != nil: base64(ciphertext)
-
1
def encrypt_base64(plaintext, key, cipher_type = 'aes-256-cbc', iv = nil)
-
246
Base64.strict_encode64(encrypt(plaintext, key, cipher_type, iv))
-
end
-
-
# Decode and Decrypt a Base64-String.
-
#
-
# @param [String] ciphertext Input String (base64(ciphertext))
-
# @param [String] key Encryption key
-
# @param [String] cipher_type OpenSSL cipher
-
# @param [String] iv Initialization vector
-
# @return [String] Plaintext
-
1
def decrypt_base64(ciphertext, key, cipher_type = 'aes-256-cbc', iv = nil)
-
123
decrypt(Base64.decode64(ciphertext), key, cipher_type, iv)
-
end
-
-
# Encrypt a String and encode the resulting ciphertext to urlsafe Base64.
-
#
-
# @param [String] plaintext Input String (plaintext)
-
# @param [String] key Encryption key
-
# @param [String] cipher_type OpenSSL cipher
-
# @param [String] iv Initialization vector
-
# @return [String] When iv == nil: urlsafe_base64(iv_length+iv+ciphertext)
-
# @return [String] When iv != nil: urlsafe_base64(ciphertext)
-
1
def encrypt_urlsafe_base64(plaintext, key, cipher_type = 'aes-256-cbc', iv = nil)
-
251
Base64.urlsafe_encode64(encrypt(plaintext, key, cipher_type, iv))
-
end
-
-
# Decode and Decrypt an urlsafe Base64-String.
-
#
-
# @param [String] ciphertext Input String (urlsafe_base64(ciphertext))
-
# @param [String] key Encryption key
-
# @param [String] cipher_type OpenSSL cipher
-
# @param [String] iv Initialization vector
-
# @return [String] Plaintext
-
1
def decrypt_urlsafe_base64(ciphertext, key, cipher_type = 'aes-256-cbc', iv = nil)
-
126
decrypt(Base64.urlsafe_decode64(ciphertext), key, cipher_type, iv)
-
end
-
end
-
-
# Secure Control Token.
-
1
class ControlToken
-
1
class << self
-
# Encode and encrypt an urlsafe ControlToken.
-
#
-
# @param [String] op Operation id
-
# @param [Array] args Arguments (Strings)
-
# @param [Fixnum] expire_in
-
# @param [String] key Encryption key
-
# @param [String] cipher_type OpenSSL cipher
-
# @return [String] ControlToken (urlsafe base64)
-
1
def create(op, args, expire_in = 900, key = ENV['CONTROLTOKEN_SECRET'], cipher_type = 'aes-256-cbc')
-
6
raise ArgumentError, 'key can not be blank' if key.nil? || key.empty?
-
# If you're reading this in the year 2038: Hi there! :-)
-
5
[Time.now.to_i + expire_in].pack('l<')
-
5
body = ([[Time.now.to_i + expire_in].pack('l<'), op] + args).join("\x00")
-
5
BB::Crypto.encrypt_urlsafe_base64(body, key, cipher_type)
-
end
-
-
# Decrypt and parse an urlsafe ControlToken.
-
#
-
# @param [String] token Input String (urlsafe base64)
-
# @param [String] key Encryption key
-
# @param [Boolean] force Decode expired token (suppress ArgumentError)
-
# @param [String] cipher_type OpenSSL cipher
-
# @return [Hash] Token payload
-
1
def parse(token, key = ENV['CONTROLTOKEN_SECRET'], force = false, cipher_type = 'aes-256-cbc')
-
3
raise ArgumentError, 'key can not be blank' if key.nil? || key.empty?
-
3
body = BB::Crypto.decrypt_urlsafe_base64(token, key, cipher_type)
-
3
valid_until, op, *args = body.split("\x00")
-
3
valid_until = valid_until.unpack('l<')[0]
-
3
expired = Time.now.to_i > valid_until
-
3
raise ArgumentError, "Token expired at #{Time.at(valid_until)} (#{ChronicDuration.output(Time.now.to_i - valid_until)} ago)" if expired && !force
-
2
{ valid_until: valid_until,
-
op: op,
-
args: args,
-
expired: expired }
-
end
-
end
-
end # /BB::Crypto::Token
-
end # /BB::Crypto
-
end
-
# frozen_string_literal: true
-
-
1
require 'versionomy'
-
1
module BB
-
# Gem utilities.
-
1
module Gem
-
1
class << self
-
# Return information about the currently installed gem
-
# version and the latest available version on rubygems.org.
-
#
-
# @param [Hash] opts the options to create a message with.
-
# @option opts [Fixnum] :check_interval how frequently to query rubygems.org (default: 3600)
-
# @option opts [String] :disabling_env_var (default: #{GEMNAME}_DISABLE_VERSION_CHECK)
-
# @option opts [] :from ('nobody') From address
-
# @return [Hash] result
-
# * :gem_name => name of current gem
-
# * :gem_installed_version => installed version
-
# * :gem_latest_version => latest version on rubygems.org
-
# * :last_checked_for_update => timestamp of last query to rubygems.org
-
# * :next_check_for_update => timestamp of next query to rubygems.org
-
# * :gem_update_available => update available?
-
# * :installed_is_latest => is installed version == latest available version?
-
1
def version_info(*_, **opts)
-
8
ret = {
-
gem_name: :unknown,
-
gem_installed_version: :unknown,
-
gem_latest_version: :unknown,
-
gem_update_available: false,
-
last_checked_for_update: :unknown,
-
next_check_for_update: :unknown,
-
installed_is_latest: true
-
}
-
-
8
calling_file = caller[0].split(':')[0]
-
8
spec = ::Gem::Specification.find do |s|
-
96
File.fnmatch(File.join(s.full_gem_path, '*'), calling_file)
-
end
-
-
8
ret[:gem_installed_version] = spec&.version&.to_s || :unknown
-
8
ret[:gem_name] = spec&.name || :unknown
-
-
8
opts = { # defaults
-
check_interval: 3600,
-
disabling_env_var: "#{ret[:gem_name].upcase}_DISABLE_VERSION_CHECK"
-
}.merge(opts)
-
-
8
return ret if ret[:gem_name] == :unknown
-
8
return ret if ret[:gem_installed_version] == :unknown
-
8
if opts[:disabling_env_var] && ENV.include?(opts[:disabling_env_var])
-
1
ret[:next_check_for_update] = :never
-
1
return ret
-
end
-
-
7
require 'gem_update_checker'
-
7
require 'tmpdir'
-
7
require 'fileutils'
-
-
7
statefile_path = File.join(Dir.tmpdir, "#{ret[:gem_name]}-#{ret[:gem_installed_version]}.last_update_check")
-
-
7
last_check_at = nil
-
7
begin
-
7
last_check_at = File.stat(statefile_path).mtime
-
rescue StandardError
-
1
last_check_at = Time.at(0)
-
end
-
-
7
ret.merge!(
-
last_checked_for_update: last_check_at,
-
next_check_for_update: last_check_at + opts[:check_interval]
-
)
-
-
7
return ret if last_check_at + opts[:check_interval] > Time.now && !opts[:force_check]
-
-
6
checker = GemUpdateChecker::Client.new(ret[:gem_name], ret[:gem_installed_version])
-
6
last_check_at = Time.now
-
-
6
ret.merge!(
-
gem_latest_version: checker.latest_version,
-
last_checked_for_update: last_check_at,
-
next_check_for_update: last_check_at + opts[:check_interval],
-
installed_is_latest: ret[:gem_installed_version] == checker.latest_version,
-
gem_update_available: Versionomy.parse(ret[:gem_installed_version]) < Versionomy.parse(checker.latest_version)
-
)
-
-
6
if ret[:installed_is_latest] || opts[:force_check]
-
6
FileUtils.touch(statefile_path, mtime: Time.now)
-
else
-
ret[:next_check_for_update] = Time.now
-
end
-
-
6
ret
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module BB
-
# Hash utilities.
-
1
module Hash
-
1
class << self
-
# Symbolize all top level keys.
-
#
-
# @param [Hash] hash Input hash
-
# @return [Hash] Output hash (with symbolized keys)
-
1
def symbolize_keys(hash)
-
112
hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
-
end
-
-
# Recursively flatten a hash to property-style format.
-
# This is a lossy conversion and should only be used for display-purposes.
-
#
-
# @example
-
# input = { :a => { :b => :c } }
-
# BB::Hash.flatten_prop_style(input)
-
# => {"a.b"=>:c}
-
#
-
# @example
-
# input = { :a => { :b => [:c, :d, :e] } }
-
# BB::Hash.flatten_prop_style(input)
-
# => {"a.b"=>"c,d,e"}
-
#
-
# @param [Hash] input Input hash
-
# @param [Hash] opts Options
-
# @option opts [String] :delimiter
-
# Key delimiter (Default: '.')
-
# @param [Hash] output (leave this blank)
-
# @return [Hash] Output hash (flattened)
-
1
def flatten_prop_style(input = {}, opts = {}, output = {})
-
10
input.each do |key, value|
-
22
key = opts[:prefix].nil? ? key.to_s : "#{opts[:prefix]}#{opts[:delimiter] || '.'}#{key}"
-
22
case value
-
when ::Hash
-
9
flatten_prop_style(value, opts.merge(prefix: key), output)
-
when Array
-
4
output[key] = value.join(',')
-
else
-
9
output[key] = value
-
end
-
end
-
10
output
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require 'blackbox/hash'
-
-
1
module BB
-
# String utilities.
-
1
module Number
-
1
class << self
-
1
STORAGE_UNITS = %w[byte k M G T P E Z Y].freeze
-
-
##
-
# Formats the bytes in +number+ into a more understandable representation
-
# (e.g., giving it 1500 yields 1.5k). This method is useful for
-
# reporting file sizes to users. This method returns nil if
-
# +number+ cannot be converted into a number. You can customize the
-
# format in the +options+ hash.
-
#
-
# @overload to_human_size(number, options={})
-
# @param [Fixnum] number
-
# Number value to format.
-
# @param [Hash] options
-
# Options for formatter.
-
# @option options [Fixnum] :precision (1)
-
# Sets the level of precision.
-
# @option options [String] :separator (".")
-
# Sets the separator between the units.
-
# @option options [String] :delimiter ("")
-
# Sets the thousands delimiter.
-
# @option options [String] :kilo (1024)
-
# Sets the number of bytes in a kilobyte.
-
# @option options [String] :format ("%n%u")
-
# Sets the display format.
-
#
-
# @return [String] The formatted representation of bytes
-
#
-
# @example
-
# to_human_size(123) # => 123
-
# to_human_size(1234) # => 1.2k
-
# to_human_size(12345) # => 12.1k
-
# to_human_size(1234567) # => 1.2M
-
# to_human_size(1234567890) # => 1.1G
-
# to_human_size(1234567890123) # => 1.1T
-
# to_human_size(1234567, :precision => 2) # => 1.18M
-
# to_human_size(483989, :precision => 0) # => 473k
-
# to_human_size(1234567, :precision => 2, :separator => ',') # => 1,18M
-
#
-
1
def to_human_size(number, args = {})
-
13
begin
-
13
Float(number)
-
rescue StandardError
-
1
return nil
-
end
-
-
12
options = BB::Hash.symbolize_keys(args)
-
-
12
precision ||= (options[:precision] || 1)
-
12
separator ||= (options[:separator] || '.')
-
12
delimiter ||= (options[:delimiter] || '')
-
12
kilo ||= (options[:kilo] || 1024)
-
12
storage_units_format ||= (options[:format] || '%n%u')
-
-
12
begin
-
12
if number.to_i < kilo
-
1
storage_units_format.gsub(/%n/, number.to_i.to_s).gsub(/%u/, '')
-
else
-
10
max_exp = STORAGE_UNITS.size - 1
-
10
number = Float(number)
-
10
exponent = (Math.log(number) / Math.log(kilo)).to_i # Convert to base
-
10
exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit
-
10
number /= kilo**exponent
-
-
10
unit = STORAGE_UNITS[exponent]
-
-
10
escaped_separator = Regexp.escape(separator)
-
10
formatted_number = with_precision(number,
-
precision: precision,
-
separator: separator,
-
delimiter: delimiter).sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '')
-
10
storage_units_format.gsub(/%n/, formatted_number).gsub(/%u/, unit)
-
end
-
1
rescue StandardError
-
1
number
-
end
-
end
-
-
##
-
# Formats a +number+ with the specified level of <tt>:precision</tt> (e.g., 112.32 has a precision of 2).
-
# This method returns nil if +number+ cannot be converted into a number.
-
# You can customize the format in the +options+ hash.
-
#
-
# @overload with_precision(number, options={})
-
# @param [Fixnum, Float] number
-
# Number value to format.
-
# @param [Hash] options
-
# Options for formatter.
-
# @option options [Fixnum] :precision (3)
-
# Sets the level of precision.
-
# @option options [String] :separator (".")
-
# Sets the separator between the units.
-
# @option options [String] :delimiter ("")
-
# Sets the thousands delimiter.
-
#
-
# @return [String] The formatted representation of the number.
-
#
-
# @example
-
# with_precision(111.2345) # => 111.235
-
# with_precision(111.2345, :precision => 2) # => 111.23
-
# with_precision(13, :precision => 5) # => 13.00000
-
# with_precision(389.32314, :precision => 0) # => 389
-
# with_precision(1111.2345, :precision => 2, :separator => ',', :delimiter => '.')
-
# # => 1.111,23
-
#
-
1
def with_precision(number, args)
-
14
begin
-
14
Float(number)
-
rescue StandardError
-
1
return nil
-
end
-
-
13
options = BB::Hash.symbolize_keys(args)
-
-
13
precision ||= (options[:precision] || 3)
-
13
separator ||= (options[:separator] || '.')
-
13
delimiter ||= (options[:delimiter] || '')
-
-
13
begin
-
13
rounded_number = (Float(number) * (10**precision)).round.to_f / 10**precision
-
12
with_delimiter("%01.#{precision}f" % rounded_number,
-
separator: separator,
-
delimiter: delimiter)
-
1
rescue StandardError
-
1
number
-
end
-
end
-
-
##
-
# Formats a +number+ with grouped thousands using +delimiter+ (e.g., 12,324).
-
# This method returns nil if +number+ cannot be converted into a number.
-
# You can customize the format in the +options+ hash.
-
#
-
# @overload with_delimiter(number, options={})
-
# @param [Fixnum, Float] number
-
# Number value to format.
-
# @param [Hash] options
-
# Options for formatter.
-
# @option options [String] :delimiter (", ")
-
# Sets the thousands delimiter.
-
# @option options [String] :separator (".")
-
# Sets the separator between the units.
-
#
-
# @return [String] The formatted representation of the number.
-
#
-
# @example
-
# with_delimiter(12345678) # => 12,345,678
-
# with_delimiter(12345678.05) # => 12,345,678.05
-
# with_delimiter(12345678, :delimiter => ".") # => 12.345.678
-
# with_delimiter(12345678, :separator => ",") # => 12,345,678
-
# with_delimiter(98765432.98, :delimiter => " ", :separator => ",")
-
# # => 98 765 432,98
-
#
-
1
def with_delimiter(number, args = {})
-
17
begin
-
17
Float(number)
-
rescue StandardError
-
1
return nil
-
end
-
16
options = BB::Hash.symbolize_keys(args)
-
-
16
delimiter ||= (options[:delimiter] || ',')
-
16
separator ||= (options[:separator] || '.')
-
-
16
begin
-
16
parts = number.to_s.split('.')
-
16
parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}")
-
16
parts.join(separator)
-
1
rescue StandardError
-
1
number
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module BB
-
# String utilities.
-
1
module String
-
1
class << self
-
# Strip ANSI escape sequences from String.
-
#
-
# @param [String] text Input string (dirty)
-
# @return [String] Output string (cleaned)
-
1
def strip_ansi(text)
-
text.gsub(/\x1b(\[|\(|\))[;?0-9]*[0-9A-Za-z]/, '')
-
.gsub(/\x1b(\[|\(|\))[;?0-9]*[0-9A-Za-z]/, '')
-
1
.gsub(/(\x03|\x1a)/, '')
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require 'lolcat/lol'
-
1
require 'open3'
-
1
require 'pty'
-
1
require 'rainbow/ext/string'
-
-
1
module BB
-
# Unix utilities.
-
1
module Unix
-
1
class << self
-
# Run each line of a script in a shell.
-
#
-
# @param [Hash] script Script
-
# @return [Hash] Exit status (depends on mode)
-
1
def run_each(script, opts = {})
-
4
opts = {
-
quiet: false,
-
failfast: true,
-
spinner: nil,
-
stream: false
-
}.merge(opts)
-
-
4
@minispinlock ||= Mutex.new
-
4
script.lines.each_with_index do |line, i|
-
8
line.chomp!
-
8
case line[0]
-
when '#'
-
puts "\n" + line.bright unless opts[:quiet]
-
when ':'
-
4
opts[:quiet] = true if line == ':quiet'
-
4
opts[:failfast] = false if line == ':return'
-
4
opts[:spinner] = nil if line == ':nospinner'
-
4
if line == ':stream'
-
opts[:stream] = true
-
opts[:quiet] = false
-
end
-
end
-
8
next if line.empty? || ['#', ':'].include?(line[0])
-
-
4
status = nil
-
4
if opts[:stream]
-
puts "\n> ".color(:green) + line.color(:black).bright
-
rows, cols = STDIN.winsize
-
@minispin_disable = false
-
@minispin_last_char_at = Time.now
-
@tspin ||= Thread.new do
-
i = 0
-
loop do
-
break if @minispin_last_char_at == :end
-
if Time.now - @minispin_last_char_at < 0.23 || @minispin_disable
-
sleep 0.1
-
next
-
end
-
@minispinlock.synchronize do
-
next if @minispin_disable
-
print "\e[?25l"
-
print Paint[' ', '#000', Lol.rainbow(1, i / 3.0)]
-
sleep 0.12
-
print 8.chr
-
print ' '
-
print 8.chr
-
i += 1
-
print "\e[?25h"
-
end
-
end
-
end
-
-
PTY.spawn("stty rows #{rows} cols #{cols}; " + line) do |r, _w, pid|
-
begin
-
until r.eof?
-
c = r.getc
-
@minispinlock.synchronize do
-
print c
-
@minispin_last_char_at = Time.now
-
c = c.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: "\e") # barf.
-
# hold on when we are (likely) inside an escape sequence
-
@minispin_disable = true if c.ord == 27 || c.ord < 9
-
@minispin_disable = false if c =~ /[A-Za-z]/ || [13, 10].include?(c.ord)
-
end
-
end
-
rescue Errno::EIO
-
# Linux raises EIO on EOF, cf.
-
# https://github.com/ruby/ruby/blob/57fb2199059cb55b632d093c2e64c8a3c60acfbb/ext/pty/pty.c#L519
-
nil
-
end
-
-
_pid, status = Process.wait2(pid)
-
@minispin_last_char_at = :end
-
@tspin.join
-
@tspin = nil
-
end
-
else
-
4
opts[:spinner]&.call(true)
-
4
output, status = Open3.capture2e(line)
-
4
opts[:spinner]&.call(false)
-
4
color = status.exitstatus == 0 ? :green : :red
-
4
if status.exitstatus != 0 || !opts[:quiet]
-
3
puts "\n> ".color(color) + line.color(:black).bright
-
3
puts output
-
end
-
end
-
4
next unless status.exitstatus != 0
-
2
puts "Error, exit #{status.exitstatus}: #{line} (L#{i})".color(:red).bright
-
-
2
exit status.exitstatus if opts[:failfast]
-
1
return status.exitstatus
-
end
-
2
0
-
end
-
end
-
end
-
end