128 lines
3.4 KiB
Ruby
128 lines
3.4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'zlib'
|
|
require 'json'
|
|
|
|
module Gitlab
|
|
module Ci
|
|
module Build
|
|
module Artifacts
|
|
class Metadata
|
|
ParserError = Class.new(StandardError)
|
|
InvalidStreamError = Class.new(StandardError)
|
|
|
|
VERSION_PATTERN = /^[\w\s]+(\d+\.\d+\.\d+)/.freeze
|
|
INVALID_PATH_PATTERN = %r{(^\.?\.?/)|(/\.?\.?/)}.freeze
|
|
|
|
attr_reader :stream, :path, :full_version
|
|
|
|
def initialize(stream, path, **opts)
|
|
@stream, @path, @opts = stream, path, opts
|
|
@full_version = read_version
|
|
end
|
|
|
|
def version
|
|
@full_version.match(VERSION_PATTERN)[1]
|
|
end
|
|
|
|
def errors
|
|
gzip do |gz|
|
|
read_string(gz) # version
|
|
errors = read_string(gz)
|
|
raise ParserError, 'Errors field not found!' unless errors
|
|
|
|
begin
|
|
Gitlab::Json.parse(errors)
|
|
rescue JSON::ParserError
|
|
raise ParserError, 'Invalid errors field!'
|
|
end
|
|
end
|
|
end
|
|
|
|
def find_entries!
|
|
gzip do |gz|
|
|
2.times { read_string(gz) } # version and errors fields
|
|
match_entries(gz)
|
|
end
|
|
end
|
|
|
|
def to_entry
|
|
entries = find_entries!
|
|
Entry.new(@path, entries)
|
|
end
|
|
|
|
private
|
|
|
|
def match_entries(gz)
|
|
entries = {}
|
|
|
|
child_pattern = '[^/]*/?$' unless @opts[:recursive]
|
|
match_pattern = /^#{Regexp.escape(@path)}#{child_pattern}/
|
|
|
|
until gz.eof?
|
|
begin
|
|
path = read_string(gz)&.force_encoding('UTF-8')
|
|
meta = read_string(gz)&.force_encoding('UTF-8')
|
|
|
|
# We might hit an EOF while reading either value, so we should
|
|
# abort if we don't get any data.
|
|
next unless path && meta
|
|
next unless path.valid_encoding? && meta.valid_encoding?
|
|
next unless path =~ match_pattern
|
|
next if path =~ INVALID_PATH_PATTERN
|
|
|
|
entries[path] = Gitlab::Json.parse(meta, symbolize_names: true)
|
|
rescue JSON::ParserError, Encoding::CompatibilityError
|
|
next
|
|
end
|
|
end
|
|
|
|
entries
|
|
end
|
|
|
|
def read_version
|
|
gzip do |gz|
|
|
version_string = read_string(gz)
|
|
|
|
unless version_string
|
|
raise ParserError, 'Artifacts metadata file empty!'
|
|
end
|
|
|
|
unless version_string =~ VERSION_PATTERN
|
|
raise ParserError, 'Invalid version!'
|
|
end
|
|
|
|
version_string.chomp
|
|
end
|
|
end
|
|
|
|
def read_uint32(gz)
|
|
binary = gz.read(4)
|
|
binary.unpack1('L>') if binary
|
|
end
|
|
|
|
def read_string(gz)
|
|
string_size = read_uint32(gz)
|
|
return unless string_size
|
|
|
|
gz.read(string_size)
|
|
end
|
|
|
|
def gzip(&block)
|
|
raise InvalidStreamError, "Invalid stream" unless @stream
|
|
|
|
# restart gzip reading
|
|
@stream.seek(0)
|
|
|
|
gz = Zlib::GzipReader.new(@stream)
|
|
yield(gz)
|
|
rescue Zlib::Error => e
|
|
raise InvalidStreamError, e.message
|
|
ensure
|
|
gz&.finish
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|