summaryrefslogtreecommitdiff
path: root/lib/gitlab/ci/build/artifacts/metadata.rb
blob: 375d8bc1ff558f0c2b9374093e947bebc3291a8e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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+)/
          INVALID_PATH_PATTERN = %r{(^\.?\.?/)|(/\.?\.?/)}

          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
                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')

                next unless path.valid_encoding? && meta.valid_encoding?
                next unless path =~ match_pattern
                next if path =~ INVALID_PATH_PATTERN

                entries[path] = 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.unpack('L>')[0] if binary
          end

          def read_string(gz)
            string_size = read_uint32(gz)
            return nil 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