• 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

82.96
/lib/minitar/reader.rb
1
# frozen_string_literal: true
2

3
class Minitar
2✔
4
  # The class that reads a tar format archive from a data stream. The data stream may be
5
  # sequential or random access, but certain features only work with random access data
6
  # streams.
7
  class Reader
2✔
8
    include Enumerable
2✔
9

10
    # This marks the EntryStream closed for reading without closing the actual data
11
    # stream.
12
    module InvalidEntryStream
2✔
13
      def read(*) = raise ClosedStream # :nodoc:
2✔
14

15
      def getc = raise ClosedStream # :nodoc:
2✔
16

17
      def rewind = raise ClosedStream # :nodoc:
2✔
18

19
      def closed? = true # :nodoc:
2✔
20
    end
21

22
    # EntryStreams are pseudo-streams on top of the main data stream.
23
    class EntryStream
2✔
24
      Minitar::PosixHeader::FIELDS.each do |field|
2✔
25
        attr_reader field.to_sym
32✔
26
      end
27

28
      def initialize(header, io)
2✔
29
        @io = io
336✔
30
        @name = header.name
336✔
31
        @mode = header.mode
336✔
32
        @uid = header.uid
336✔
33
        @gid = header.gid
336✔
34
        @size = header.size
336✔
35
        @mtime = header.mtime
336✔
36
        @checksum = header.checksum
336✔
37
        @typeflag = header.typeflag
336✔
38
        @linkname = header.linkname
336✔
39
        @magic = header.magic
336✔
40
        @version = header.version
336✔
41
        @uname = header.uname
336✔
42
        @gname = header.gname
336✔
43
        @devmajor = header.devmajor
336✔
44
        @devminor = header.devminor
336✔
45
        @prefix = header.prefix
336✔
46
        @read = 0
336✔
47
        @orig_pos =
48
          if Minitar.seekable?(@io)
336✔
49
            @io.pos
286✔
50
          else
25✔
51
            0
50✔
52
          end
53
      end
54

55
      # Reads +len+ bytes (or all remaining data) from the entry. Returns +nil+ if there
56
      # is no more data to read.
57
      def read(len = nil)
2✔
58
        return nil if @read >= @size
332✔
59
        len ||= @size - @read
226✔
60
        max_read = [len, @size - @read].min
226✔
61
        ret = @io.read(max_read)
226✔
62
        @read += ret.bytesize
226✔
63
        ret
226✔
64
      end
65

66
      # Reads one byte from the entry. Returns +nil+ if there is no more data to read.
67
      def getc
2✔
68
        return nil if @read >= @size
×
69
        ret = @io.getc
×
70
        @read += 1 if ret
×
71
        ret
×
72
      end
73

74
      # Returns +true+ if the entry represents a directory.
75
      def directory?
2✔
76
        case @typeflag
308!
77
        when "5"
40✔
78
          true
80✔
79
        when "0", "\0"
80
          # If the name ends with a slash, treat it as a directory. This is what other
81
          # major tar implementations do for interoperability and compatibility with older
82
          # tar variants and some new ones.
114✔
83
          @name.end_with?("/")
228✔
84
        else
×
85
          false
×
86
        end
87
      end
88
      alias_method :directory, :directory?
2✔
89

90
      # Returns +true+ if the entry represents a plain file.
91
      def file?
2✔
92
        (@typeflag == "0" || @typeflag == "\0") && !@name.end_with?("/")
×
93
      end
94
      alias_method :file, :file?
2✔
95

96
      # Returns +true+ if the current read pointer is at the end of the EntryStream data.
97
      def eof? = @read >= @size
2✔
98

99
      # Returns the current read pointer in the EntryStream.
100
      def pos = @read
2✔
101

102
      alias_method :bytes_read, :pos
2✔
103

104
      # Sets the current read pointer to the beginning of the EntryStream.
105
      def rewind
2✔
106
        unless Minitar.seekable?(@io, :pos=)
×
107
          raise Minitar::NonSeekableStream
×
108
        end
109
        @io.pos = @orig_pos
×
110
        @read = 0
×
111
      end
112

113
      # Returns the full and proper name of the entry.
114
      def full_name
2✔
115
        if @prefix != ""
298✔
116
          File.join(@prefix, @name)
20✔
117
        else
139✔
118
          @name
278✔
119
        end
120
      end
121

122
      # Returns false if the entry stream is valid.
123
      def closed? = false
2✔
124

125
      # Closes the entry.
126
      def close = invalidate
2✔
127

128
      private
2✔
129

130
      def invalidate
2✔
131
        extend InvalidEntryStream
336✔
132
      end
133
    end
134

135
    # With no associated block, +Reader::open+ is a synonym for +Reader::new+. If the
136
    # optional code block is given, it will be passed the new _writer_ as an argument and
137
    # the Reader object will automatically be closed when the block terminates. In this
138
    # instance, +Reader::open+ returns the value of the block.
139
    def self.open(io)
2✔
140
      reader = new(io)
12✔
141
      return reader unless block_given?
12✔
142

143
      # This exception context must remain, otherwise the stream closes on open even if
144
      # a block is not given.
145
      begin
146
        yield reader
10✔
147
      ensure
148
        reader.close
10✔
149
      end
150
    end
151

152
    # Iterates over each entry in the provided input. This wraps the common pattern of:
153
    #
154
    #     Minitar::Input.open(io) do |i|
155
    #       inp.each do |entry|
156
    #         # ...
157
    #       end
158
    #     end
159
    #
160
    # If a block is not provided, an enumerator will be created with the same behaviour.
161
    #
162
    # :call-seq:
163
    #    Minitar::Reader.each_entry(io) -> enumerator
164
    #    Minitar::Reader.each_entry(io) { |entry| block } -> obj
165
    def self.each_entry(io)
2✔
166
      return to_enum(__method__, io) unless block_given?
×
167

168
      Input.open(io) do |reader|
×
169
        reader.each_entry do |entry|
×
170
          yield entry
×
171
        end
172
      end
173
    end
174

175
    # Creates and returns a new Reader object.
176
    def initialize(io)
2✔
177
      @io = io
118✔
178
      @init_pos = begin
179
        io.pos
118✔
180
      rescue
181
        nil
×
182
      end
183
    end
184

185
    # Resets the read pointer to the beginning of data stream. Do not call this during
186
    # a #each or #each_entry iteration. This only works with random access data streams
187
    # that respond to #rewind and #pos.
188
    def rewind
2✔
189
      if @init_pos.zero?
92!
190
        raise Minitar::NonSeekableStream unless Minitar.seekable?(@io, :rewind)
92!
191
        @io.rewind
92✔
192
      else
×
NEW
193
        raise Minitar::NonSeekableStream unless Minitar.seekable?(@io, :pos=)
×
194
        @io.pos = @init_pos
×
195
      end
196
    end
197

198
    # Iterates through each entry in the data stream.
199
    def each_entry
2✔
200
      return to_enum unless block_given?
96!
201

202
      loop do
96✔
203
        return if @io.eof?
432✔
204

205
        header = Minitar::PosixHeader.from_stream(@io)
426✔
206
        raise Minitar::InvalidTarStream unless header.valid?
424✔
207
        return if header.empty?
422✔
208

209
        raise Minitar::InvalidTarStream if header.size < 0
336!
210

211
        if header.long_name?
336✔
212
          name_block = (header.size / 512.0).ceil * 512
128✔
213

214
          long_name = @io.read(name_block).rstrip
128✔
215
          header = PosixHeader.from_stream(@io)
128✔
216

217
          return if header.empty?
128!
218
          header.long_name = long_name
128✔
219
        elsif header.pax_header?
208✔
220
          pax_header = PaxHeader.from_stream(@io, header)
6✔
221

222
          header = PosixHeader.from_stream(@io)
6✔
223
          return if header.empty?
6!
224

225
          header.size = pax_header.size if pax_header.size
6✔
226
        end
227

228
        entry = EntryStream.new(header, @io)
336✔
229
        size = entry.size
336✔
230

231
        yield entry
336✔
232

233
        skip = (512 - (size % 512)) % 512
336✔
234

235
        if Minitar.seekable?(@io, :seek)
336✔
236
          # avoid reading...
143✔
237
          try_seek(size - entry.bytes_read)
286✔
238
        else
25✔
239
          pending = size - entry.bytes_read
50✔
240
          while pending > 0
50✔
241
            bread = @io.read([pending, 4096].min).bytesize
8✔
242
            raise UnexpectedEOF if @io.eof?
8!
243
            pending -= bread
8✔
244
          end
245
        end
246

247
        @io.read(skip) # discard trailing zeros
336✔
248
        # make sure nobody can use #read, #getc or #rewind anymore
249
        entry.close
336✔
250
      end
251
    end
252
    alias_method :each, :each_entry
2✔
253

254
    # Returns false if the reader is open (it never closes).
255
    def closed? = false
2✔
256

257
    def close
2✔
258
    end
259

260
    private
2✔
261

262
    def try_seek(bytes)
2✔
263
      @io.seek(bytes, IO::SEEK_CUR)
286✔
264
    rescue RangeError
265
      # This happens when skipping the large entry and the skipping entry size exceeds
266
      # maximum allowed size (varies by platform and underlying IO object).
NEW
267
      max = RbConfig::LIMITS.fetch("INT_MAX", 2147483647)
×
UNCOV
268
      skipped = 0
×
UNCOV
269
      while skipped < bytes
×
UNCOV
270
        to_skip = [bytes - skipped, max].min
×
UNCOV
271
        @io.seek(to_skip, IO::SEEK_CUR)
×
UNCOV
272
        skipped += to_skip
×
273
      end
274
    end
275
  end
276
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