class Google::Cloud::Storage::File::SignerV4

@private Create a signed_url for a file.
#

def self.from_bucket bucket, file_name

def self.from_bucket bucket, file_name
  new bucket.name, file_name, bucket.service
end

def self.from_file file

def self.from_file file
  new file.bucket, file.name, file.service
end

def bucket_path path_style

The external path to the bucket, with trailing slash.
#
def bucket_path path_style
  "/#{@bucket_name}/" if path_style
end

def canonical_and_signed_headers headers, virtual_hosted_style, bucket_bound_hostname

def canonical_and_signed_headers headers, virtual_hosted_style, bucket_bound_hostname
  if virtual_hosted_style && bucket_bound_hostname
    raise "virtual_hosted_style: #{virtual_hosted_style} and bucket_bound_hostname: " \
          "#{bucket_bound_hostname} params cannot both be passed together"
  end
  canonical_headers = headers || {}
  headers_arr = canonical_headers.map do |k, v|
    [k.downcase, v.strip.gsub(/[^\S\t]+/, " ").gsub(/\t+/, " ")]
  end
  canonical_headers = headers_arr.to_h
  canonical_headers["host"] = host_name virtual_hosted_style, bucket_bound_hostname
  canonical_headers = canonical_headers.sort_by(&:first).to_h
  canonical_headers_str = canonical_headers.map { |k, v| "#{k}:#{v}\n" }.join
  signed_headers_str = canonical_headers.keys.join ";"
  [canonical_headers_str, signed_headers_str]
end

def canonical_query query, algorithm, credential, goog_date, expires, signed_headers_str

def canonical_query query, algorithm, credential, goog_date, expires, signed_headers_str
  query ||= {}
  query["X-Goog-Algorithm"] = algorithm
  query["X-Goog-Credential"] = credential
  query["X-Goog-Date"] = goog_date
  query["X-Goog-Expires"] = expires
  query["X-Goog-SignedHeaders"] = signed_headers_str
  query = query.map { |k, v| [escape_query_param(k), escape_query_param(v)] }.sort_by(&:first).to_h
  query.map { |k, v| "#{k}=#{v}" }.join "&"
end

def determine_expires expires

def determine_expires expires
  expires ||= 604_800 # Default is 7 days.
  if expires > 604_800
    raise ArgumentError, "Expiration time can't be longer than a week"
  end
  expires
end

def determine_issuer issuer, client_email

def determine_issuer issuer, client_email
  # Parse the Service Account and get client id and private key
  issuer = issuer || client_email || @service.credentials.issuer
  raise SignedUrlUnavailable, error_msg("issuer (client_email)") unless issuer
  issuer
end

def determine_signing_key signing_key, private_key, signer

def determine_signing_key signing_key, private_key, signer
  signing_key = signing_key || private_key || signer || @service.credentials.signing_key
  raise SignedUrlUnavailable, error_msg("signing_key (private_key, signer)") unless signing_key
  signing_key
end

def error_msg attr_name

def error_msg attr_name
  "Service account credentials '#{attr_name}' is missing. To generate service account credentials " \
    "see https://cloud.google.com/iam/docs/service-accounts"
end

def escape_characters str

rubocop:disable Style/StringLiterals
methods below are public visibility only for unit testing
def escape_characters str
  str.chars.map do |s|
    if s.ascii_only?
      case s
      when "\\"
        '\\'
      when "\b"
        '\b'
      when "\f"
        '\f'
      when "\n"
        '\n'
      when "\r"
        '\r'
      when "\t"
        '\t'
      when "\v"
        '\v'
      else
        s
      end
    else
      escape_special_unicode s
    end
  end.join
end

def escape_query_param str

percent-encoded using %XX UTF-8 style.
Only the characters in the regex set [A-Za-z0-9.~_-] must be left un-escaped; all others must be
#
def escape_query_param str
  CGI.escape(str.to_s).gsub("%7E", "~").gsub "+", "%20"
end

def escape_special_unicode str

def escape_special_unicode str
  str.unpack("U*").map { |i| "\\u#{i.to_s(16).rjust(4, '0')}" }.join
end

def ext_url scheme, virtual_hosted_style, bucket_bound_hostname

The external url to the file.
#
def ext_url scheme, virtual_hosted_style, bucket_bound_hostname
  url = @service.service.root_url.chomp "/"
  if virtual_hosted_style
    parts = url.split "//"
    parts[1] = "#{@bucket_name}.#{parts[1]}"
    parts.join "//"
  elsif bucket_bound_hostname
    raise ArgumentError, "scheme is required" unless scheme
    URI "#{scheme.to_s.downcase}://#{bucket_bound_hostname}"
  else
    url
  end
end

def file_path path_style

The URI-encoded (percent encoded) external path to the file.
#
def file_path path_style
  path = []
  path << "/#{@bucket_name}" if path_style
  path << "/#{String(@file_name)}" if @file_name && !@file_name.empty?
  CGI.escape(path.join).gsub("%2F", "/").gsub "+", "%20"
end

def generate_signature signing_key, data

def generate_signature signing_key, data
  packed_signature = nil
  if signing_key.is_a? Proc
    packed_signature = signing_key.call data
  else
    unless signing_key.respond_to? :sign
      signing_key = OpenSSL::PKey::RSA.new signing_key
    end
    packed_signature = signing_key.sign OpenSSL::Digest::SHA256.new, data
  end
  packed_signature.unpack1("H*").force_encoding "utf-8"
end

def host_name virtual_hosted_style, bucket_bound_hostname

def host_name virtual_hosted_style, bucket_bound_hostname
  return bucket_bound_hostname if bucket_bound_hostname
  virtual_hosted_style ? "#{@bucket_name}.storage.googleapis.com" : "storage.googleapis.com"
end

def initialize bucket_name, file_name, service

def initialize bucket_name, file_name, service
  @bucket_name = bucket_name
  @file_name = file_name
  @service = service
end

def issuer_and_signer issuer, client_email, signing_key, private_key, signer

def issuer_and_signer issuer, client_email, signing_key, private_key, signer
  issuer = determine_issuer issuer, client_email
  signing_key = determine_signing_key signing_key, private_key, signer
  signer = service_account_signer signing_key
  [issuer, signer]
end

def path_style? virtual_hosted_style, bucket_bound_hostname

def path_style? virtual_hosted_style, bucket_bound_hostname
  !(virtual_hosted_style || bucket_bound_hostname)
end

def policy_conditions base_fields, user_conditions, user_fields

def policy_conditions base_fields, user_conditions, user_fields
  # Convert each pair in base_fields hash to a single-entry hash in an array.
  conditions = base_fields.to_a.map { |f| Hash[*f] }
  # Add the bucket to the head of the base_fields. This is not returned in the PostObject fields.
  conditions.unshift "bucket" => @bucket_name
  # Add user-provided conditions to the head of the conditions array.
  conditions = user_conditions + conditions if user_conditions
  if user_fields
    # Convert each pair in fields hash to a single-entry hash and add it to the head of the conditions array.
    user_fields.to_a.reverse.each { |f| conditions.unshift Hash[*f] }
  end
  conditions.freeze
end

def post_object issuer: nil,

def post_object issuer: nil,
                client_email: nil,
                signing_key: nil,
                private_key: nil,
                signer: nil,
                expires: nil,
                fields: nil,
                conditions: nil,
                scheme: "https",
                virtual_hosted_style: nil,
                bucket_bound_hostname: nil
  i = determine_issuer issuer, client_email
  s = determine_signing_key signing_key, private_key, signer
  now = Time.now.utc
  base_fields = required_fields i, now
  post_fields = fields.dup || {}
  post_fields.merge! base_fields
  p = {}
  p["conditions"] = policy_conditions base_fields, conditions, fields
  expires ||= 60 * 60 * 24
  p["expiration"] = (now + expires).strftime "%Y-%m-%dT%H:%M:%SZ"
  policy_str = escape_characters p.to_json
  policy = Base64.strict_encode64(policy_str).force_encoding "utf-8"
  signature = generate_signature s, policy
  post_fields["x-goog-signature"] = signature
  post_fields["policy"] = policy
  url = post_object_ext_url scheme, virtual_hosted_style, bucket_bound_hostname
  hostname = "#{url}#{bucket_path path_style?(virtual_hosted_style, bucket_bound_hostname)}"
  Google::Cloud::Storage::PostObject.new hostname, post_fields
end

def post_object_ext_path


https://cloud.google.com/storage/docs/xml-api/post-object
"You can also use the ${filename} variable..."
Will not URI encode the special `${filename}` variable.
The external path to the file, URI-encoded.
#
def post_object_ext_path
  path = "/#{@bucket_name}/#{@file_name}"
  escaped = Addressable::URI.encode_component path, Addressable::URI::CharacterClasses::PATH
  special_var = "${filename}"
  # Restore the unencoded `${filename}` variable, if present.
  if path.include? special_var
    return escaped.gsub "$%7Bfilename%7D", special_var
  end
  escaped
end

def post_object_ext_url scheme, virtual_hosted_style, bucket_bound_hostname

The external url to the file.
#
def post_object_ext_url scheme, virtual_hosted_style, bucket_bound_hostname
  url = GOOGLEAPIS_URL.dup
  if virtual_hosted_style
    parts = url.split "//"
    parts[1] = "#{@bucket_name}.#{parts[1]}/"
    parts.join "//"
  elsif bucket_bound_hostname
    raise ArgumentError, "scheme is required" unless scheme
    URI "#{scheme.to_s.downcase}://#{bucket_bound_hostname}/"
  else
    url
  end
end

def required_fields issuer, time

def required_fields issuer, time
  {
    "key" => @file_name,
    "x-goog-date" => time.strftime("%Y%m%dT%H%M%SZ"),
    "x-goog-credential" => "#{issuer}/#{time.strftime '%Y%m%d'}/auto/storage/goog4_request",
    "x-goog-algorithm" => "GOOG4-RSA-SHA256"
  }.freeze
end

def service_account_signer signer

def service_account_signer signer
  if signer.is_a? Proc
    lambda do |string_to_sign|
      sig = signer.call string_to_sign
      sig.unpack1 "H*"
    end
  else
    signer = OpenSSL::PKey::RSA.new signer unless signer.respond_to? :sign
    # Sign string to sign
    lambda do |string_to_sign|
      sig = signer.sign OpenSSL::Digest::SHA256.new, string_to_sign
      sig.unpack1 "H*"
    end
  end
end

def signed_url method: "GET",

def signed_url method: "GET",
               expires: nil,
               headers: nil,
               issuer: nil,
               client_email: nil,
               signing_key: nil,
               private_key: nil,
               signer: nil,
               query: nil,
               scheme: "https",
               virtual_hosted_style: nil,
               bucket_bound_hostname: nil
  raise ArgumentError, "method is required" unless method
  issuer, signer = issuer_and_signer issuer, client_email, signing_key, private_key, signer
  datetime_now = Time.now.utc
  goog_date = datetime_now.strftime "%Y%m%dT%H%M%SZ"
  datestamp = datetime_now.strftime "%Y%m%d"
  # goog4_request is not checked.
  scope = "#{datestamp}/auto/storage/goog4_request"
  canonical_headers_str, signed_headers_str = canonical_and_signed_headers \
    headers, virtual_hosted_style, bucket_bound_hostname
  algorithm = "GOOG4-RSA-SHA256"
  expires = determine_expires expires
  credential = "#{issuer}/#{scope}"
  canonical_query_str = canonical_query query, algorithm, credential, goog_date, expires, signed_headers_str
  # From AWS: You don't include a payload hash in the Canonical
  # Request, because when you create a presigned URL, you don't know
  # the payload content because the URL is used to upload an arbitrary
  # payload. Instead, you use a constant string UNSIGNED-PAYLOAD.
  payload = headers&.key?("X-Goog-Content-SHA256") ? headers["X-Goog-Content-SHA256"] : "UNSIGNED-PAYLOAD"
  canonical_request = [method,
                       file_path(!(virtual_hosted_style || bucket_bound_hostname)),
                       canonical_query_str,
                       canonical_headers_str,
                       signed_headers_str,
                       payload].join("\n")
  # Construct string to sign
  req_sha = Digest::SHA256.hexdigest canonical_request
  string_to_sign = [algorithm, goog_date, scope, req_sha].join "\n"
  # Sign string
  signature = signer.call string_to_sign
  # Construct signed URL
  hostname = signed_url_hostname scheme, virtual_hosted_style, bucket_bound_hostname
  "#{hostname}?#{canonical_query_str}&X-Goog-Signature=#{signature}"
end

def signed_url_hostname scheme, virtual_hosted_style, bucket_bound_hostname

def signed_url_hostname scheme, virtual_hosted_style, bucket_bound_hostname
  url = ext_url scheme, virtual_hosted_style, bucket_bound_hostname
  "#{url}#{file_path path_style?(virtual_hosted_style, bucket_bound_hostname)}"
end