• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

halostatue / minitar / 17531210856

07 Sep 2025 04:42PM UTC coverage: 91.933% (+7.2%) from 84.689%
17531210856

Pull #151

github

web-flow
Merge 6d1337f92 into f9fcc1649
Pull Request #151: chore: Fix GNU long filename handling bug

335 of 467 branches covered (71.73%)

73 of 77 new or added lines in 8 files covered. (94.81%)

5 existing lines in 1 file now uncovered.

547 of 595 relevant lines covered (91.93%)

174.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.46
/lib/minitar/input.rb
1
# frozen_string_literal: true
2

3
require "minitar/reader"
2✔
4

5
class Minitar
2✔
6
  # Wraps a Minitar::Reader with convenience methods and wrapped stream management; Input
7
  # only works with data streams that can be rewound.
8
  #
9
  # === Security Notice
10
  #
11
  # Constructing a Minitar::Input will use Kernel.open if the provided input is not
12
  # a readable stream object. Using an untrusted value for input may allow a malicious
13
  # user to execute arbitrary system commands. It is the caller's responsibility to ensure
14
  # that the input value is safe.
15
  #
16
  # * {CWE-073}[https://cwe.mitre.org/data/definitions/73.html]
17
  # * {CWE-078}[https://cwe.mitre.org/data/definitions/78.html]
18
  # * {CWE-088}[https://cwe.mitre.org/data/definitions/88.html]
19
  #
20
  # This notice applies to Minitar::Input.open, Minitar::Input.each_entry, and
21
  # Minitar::Input.new.
22
  class Input
2✔
23
    include Enumerable
2✔
24

25
    # With no associated block, +Input.open+ is a synonym for +Input.new+.
26
    #
27
    # If a block is given, the new Input will be yielded to the block as an argument and
28
    # the Input object will automatically be closed when the block terminates (this also
29
    # closes the wrapped stream object). The return value will be the value of the block.
30
    #
31
    # :call-seq:
32
    #    Minitar::Input.open(io) -> input
33
    #    Minitar::Input.open(io) { |input| block } -> obj
34
    def self.open(input)
2✔
35
      stream = new(input)
88✔
36

37
      if block_given?
88✔
38
        # This exception context must remain, otherwise the stream closes on open even if
39
        # a block is not given.
43✔
40
        begin
41
          yield stream
86✔
42
        ensure
43
          stream.close
86✔
44
        end
45
      else
1✔
46
        stream
2✔
47
      end
48
    end
49

50
    # Iterates over each entry in the provided input. This wraps the common pattern of:
51
    #
52
    #     Minitar::Input.open(io) do |i|
53
    #       inp.each do |entry|
54
    #         # ...
55
    #       end
56
    #     end
57
    #
58
    # If a block is not provided, an enumerator will be created with the same behaviour.
59
    #
60
    # :call-seq:
61
    #    Minitar::Input.each_entry(io) -> enumerator
62
    #    Minitar::Input.each_entry(io) { |entry| block } -> obj
63
    def self.each_entry(input)
2✔
64
      return to_enum(__method__, input) unless block_given?
×
65

66
      Input.open(input) do |stream|
×
67
        stream.each do |entry|
×
68
          yield entry
×
69
        end
70
      end
71
    end
72

73
    # Creates a new Input object. If +input+ is a stream object that responds to #read,
74
    # then it will simply be wrapped. Otherwise, one will be created and opened using
75
    # Kernel#open. When Input#close is called, the stream object wrapped will be closed.
76
    #
77
    # An exception will be raised if the stream that is wrapped does not support
78
    # rewinding.
79
    #
80
    # :call-seq:
81
    #    Minitar::Input.new(io) -> input
82
    #    Minitar::Input.new(path) -> input
83
    def initialize(input)
2✔
84
      @io =
85
        if input.respond_to?(:read)
88!
86
          input
88✔
87
        else
×
NEW
88
          ::Kernel.open(input, "rb")
×
89
        end
90

91
      raise Minitar::NonSeekableStream unless Minitar.seekable?(@io, :rewind)
88!
92

93
      @tar = Reader.new(@io)
88✔
94
    end
95

96
    # When provided a block, iterates through each entry in the archive. When finished,
97
    # rewinds to the beginning of the stream.
98
    #
99
    # If not provided a block, creates an enumerator with the same semantics.
100
    def each_entry
2✔
101
      return to_enum unless block_given?
92✔
102

103
      @tar.each do |entry|
86✔
104
        yield entry
322✔
105
      end
106
    ensure
107
      @tar.rewind
92✔
108
    end
109
    alias_method :each, :each_entry
2✔
110

111
    # Extracts the current +entry+ to +destdir+. If a block is provided, it yields an
112
    # +action+ Symbol, the full name of the file being extracted (+name+), and a Hash of
113
    # statistical information (+stats+).
114
    #
115
    # The +action+ will be one of:
116
    #
117
    # <tt>:dir</tt>::           The +entry+ is a directory.
118
    # <tt>:file_start</tt>::    The +entry+ is a file; the extract of the file is just
119
    #                           beginning.
120
    # <tt>:file_progress</tt>:: Yielded every 4096 bytes during the extract of the
121
    #                           +entry+.
122
    # <tt>:file_done</tt>::     Yielded when the +entry+ is completed.
123
    #
124
    # The +stats+ hash contains the following keys:
125
    #
126
    # <tt>:current</tt>:: The current total number of bytes read in the +entry+.
127
    # <tt>:currinc</tt>:: The current number of bytes read in this read cycle.
128
    # <tt>:entry</tt>::   The entry being extracted; this is a Reader::EntryStream, with
129
    #                     all methods thereof.
130
    def extract_entry(destdir, entry, options = {}, &) # :yields: action, name, stats
2✔
131
      stats = {
182✔
132
        current: 0,
133
        currinc: 0,
134
        entry: entry
135
      }
136

137
      # extract_entry is not vulnerable to prefix '/' vulnerabilities, but it is
138
      # vulnerable to relative path directories. This code will break this vulnerability.
139
      # For this version, we are breaking relative paths HARD by throwing an exception.
140
      #
141
      # Future versions may permit relative paths as long as the file does not leave
142
      # +destdir+.
143
      #
144
      # However, squeeze consecutive '/' characters together.
145
      full_name = entry.full_name.squeeze("/")
182✔
146

147
      if /\.{2}(?:\/|\z)/.match?(full_name)
182✔
148
        raise SecureRelativePathError, "Path contains '..'"
2✔
149
      end
150

151
      if entry.directory?
180✔
152
        extract_directory(destdir, full_name, entry, stats, options, &)
76✔
153
      else # it's a file
52✔
154
        extract_file(destdir, full_name, entry, stats, options, &)
104✔
155
      end
156
    end
157

158
    # Returns false if the wrapped data stream is open.
159
    def closed? = @io.closed?
2✔
160

161
    # Returns the Reader object for direct access.
162
    attr_reader :tar
2✔
163

164
    # Closes both the Reader object and the wrapped data stream.
165
    def close
2✔
166
      @io.close
88✔
167
      @tar.close
88✔
168
    end
169

170
    private
2✔
171

172
    def fsync_dir(dirname)
2✔
173
      # make sure this hits the disc
174
      dir = IO.open(dirname, "rb")
352✔
175
      dir.fsync
×
176
    rescue # ignore IOError if it's an unpatched (old) Ruby
177
      nil
352✔
178
    ensure
179
      dir&.close rescue nil # standard:disable Style/RescueModifier
352!
180
    end
181

182
    def extract_directory(destdir, full_name, entry, stats, options)
2✔
183
      dest = File.join(destdir, full_name)
76✔
184

185
      yield :dir, full_name, stats if block_given?
76!
186

187
      if Minitar.dir?(dest)
76✔
188
        begin
189
          FileUtils.chmod(entry.mode, dest)
8✔
190
        rescue
191
          nil
×
192
        end
193
      else
34✔
194
        File.unlink(dest.chomp("/")) if File.symlink?(dest.chomp("/"))
68✔
195

196
        FileUtils.mkdir_p(dest, mode: entry.mode)
68✔
197
        FileUtils.chmod(entry.mode, dest)
68✔
198
      end
199

200
      if options.fetch(:fsync, true)
76!
201
        fsync_dir(dest)
76✔
202
        fsync_dir(File.join(dest, ".."))
76✔
203
      end
204
    end
205

206
    def extract_file(destdir, full_name, entry, stats, options)
2✔
207
      destdir = File.join(destdir, File.dirname(full_name))
104✔
208
      FileUtils.mkdir_p(destdir, mode: 0o755)
104✔
209

210
      destfile = File.join(destdir, File.basename(full_name))
104✔
211

212
      File.unlink(destfile) if File.symlink?(destfile)
104✔
213

214
      # Errno::ENOENT
215
      begin
216
        FileUtils.chmod(0o600, destfile)
104✔
217
      rescue
218
        nil
104✔
219
      end
220

221
      yield :file_start, full_name, stats if block_given?
104✔
222

223
      File.open(destfile, "wb", entry.mode) do |os|
104✔
224
        loop do
104✔
225
          data = entry.read(4096)
208✔
226
          break unless data
208✔
227

228
          stats[:currinc] = os.write(data)
104✔
229
          stats[:current] += stats[:currinc]
104✔
230

231
          yield :file_progress, full_name, stats if block_given?
104✔
232
        end
233

234
        if options.fetch(:fsync, true)
104✔
235
          yield :file_fsync, full_name, stats if block_given?
100!
236
          os.fsync
100✔
237
        end
238
      end
239

240
      FileUtils.chmod(entry.mode, destfile)
104✔
241

242
      if options.fetch(:fsync, true)
104✔
243
        yield :dir_fsync, full_name, stats if block_given?
100!
244

245
        fsync_dir(File.dirname(destfile))
100✔
246
        fsync_dir(File.join(File.dirname(destfile), ".."))
100✔
247
      end
248

249
      yield :file_done, full_name, stats if block_given?
104✔
250
    end
251
  end
252
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc