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

halostatue / minitar / 18974075762

31 Oct 2025 01:30PM UTC coverage: 93.478% (+1.0%) from 92.437%
18974075762

Pull #166

github

web-flow
Merge 1a7512720 into afa9ce24d
Pull Request #166: Add `out_string` parameter to `EntryStream#read`

359 of 471 branches covered (76.22%)

4 of 5 new or added lines in 1 file covered. (80.0%)

6 existing lines in 1 file now uncovered.

559 of 598 relevant lines covered (93.48%)

215.08 hits per line

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

88.41
/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
372✔
30
        @name = header.name
372✔
31
        @mode = header.mode
372✔
32
        @uid = header.uid
372✔
33
        @gid = header.gid
372✔
34
        @size = header.size
372✔
35
        @mtime = header.mtime
372✔
36
        @checksum = header.checksum
372✔
37
        @typeflag = header.typeflag
372✔
38
        @linkname = header.linkname
372✔
39
        @magic = header.magic
372✔
40
        @version = header.version
372✔
41
        @uname = header.uname
372✔
42
        @gname = header.gname
372✔
43
        @devmajor = header.devmajor
372✔
44
        @devminor = header.devminor
372✔
45
        @prefix = header.prefix
372✔
46
        @read = 0
372✔
47
        @orig_pos =
48
          if Minitar.seekable?(@io)
372✔
49
            @io.pos
312✔
50
          else
30✔
51
            0
60✔
52
          end
53
      end
54

55
      # Reads +len+ bytes (or all remaining data) from the entry. If
56
      # +out_string+ is provided, the read data is stored there instead of
57
      # allocating a new string. Returns +nil+ if there is no more data to read.
58
      def read(len = nil, out_string = nil)
2✔
59
        return nil if @read >= @size
390✔
60
        len ||= @size - @read
248✔
61
        max_read = [len, @size - @read].min
248✔
62

63
        ret = if out_string
248✔
64
          begin
65
            @io.read(max_read, out_string)
2✔
66
          rescue ArgumentError # If +@io+ does not support +out_string+
NEW
67
            out_string.replace(@io.read(max_read))
×
68
          end
69
        else
123✔
70
          @io.read(max_read)
246✔
71
        end
72

73
        @read += ret.bytesize
248✔
74
        ret
248✔
75
      end
76

77
      # Reads one byte from the entry. Returns +nil+ if there is no more data to read.
78
      def getc
2✔
79
        return nil if @read >= @size
5,012✔
80
        ret = @io.getc
5,000✔
81
        @read += 1 if ret
5,000!
82
        ret
5,000✔
83
      end
84

85
      # Returns +true+ if the entry represents a directory.
86
      def directory?
2✔
87
        case @typeflag
318!
88
        when "5"
42✔
89
          true
84✔
90
        when "0", "\0"
91
          # If the name ends with a slash, treat it as a directory. This is what other
92
          # major tar implementations do for interoperability and compatibility with older
93
          # tar variants and some new ones.
117✔
94
          @name.end_with?("/")
234✔
95
        else
×
UNCOV
96
          false
×
97
        end
98
      end
99
      alias_method :directory, :directory?
2✔
100

101
      # Returns +true+ if the entry represents a plain file.
102
      def file?
2✔
103
        (@typeflag == "0" || @typeflag == "\0") && !@name.end_with?("/")
16✔
104
      end
105
      alias_method :file, :file?
2✔
106

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

110
      # Returns the current read pointer in the EntryStream.
111
      def pos = @read
2✔
112

113
      alias_method :bytes_read, :pos
2✔
114

115
      # Sets the current read pointer to the beginning of the EntryStream.
116
      def rewind
2✔
117
        unless Minitar.seekable?(@io, :pos=)
6!
118
          raise Minitar::NonSeekableStream
×
119
        end
120
        @io.pos = @orig_pos
6✔
121
        @read = 0
6✔
122
      end
123

124
      # Returns the full and proper name of the entry.
125
      def full_name
2✔
126
        if @prefix != ""
302✔
127
          File.join(@prefix, @name)
24✔
128
        else
139✔
129
          @name
278✔
130
        end
131
      end
132

133
      # Returns false if the entry stream is valid.
134
      def closed? = false
2✔
135

136
      # Closes the entry.
137
      def close = invalidate
2✔
138

139
      private
2✔
140

141
      def invalidate
2✔
142
        extend InvalidEntryStream
372✔
143
      end
144
    end
145

146
    # With no associated block, +Reader::open+ is a synonym for +Reader::new+. If the
147
    # optional code block is given, it will be passed the new _writer_ as an argument and
148
    # the Reader object will automatically be closed when the block terminates. In this
149
    # instance, +Reader::open+ returns the value of the block.
150
    def self.open(io)
2✔
151
      reader = new(io)
32✔
152
      return reader unless block_given?
32✔
153

154
      # This exception context must remain, otherwise the stream closes on open even if
155
      # a block is not given.
156
      begin
157
        yield reader
30✔
158
      ensure
159
        reader.close
30✔
160
      end
161
    end
162

163
    # Iterates over each entry in the provided input. This wraps the common pattern of:
164
    #
165
    #     Minitar::Input.open(io) do |i|
166
    #       inp.each do |entry|
167
    #         # ...
168
    #       end
169
    #     end
170
    #
171
    # If a block is not provided, an enumerator will be created with the same behaviour.
172
    #
173
    # :call-seq:
174
    #    Minitar::Reader.each_entry(io) -> enumerator
175
    #    Minitar::Reader.each_entry(io) { |entry| block } -> obj
176
    def self.each_entry(io)
2✔
UNCOV
177
      return to_enum(__method__, io) unless block_given?
×
178

UNCOV
179
      Input.open(io) do |reader|
×
180
        reader.each_entry do |entry|
×
181
          yield entry
×
182
        end
183
      end
184
    end
185

186
    # Creates and returns a new Reader object.
187
    def initialize(io)
2✔
188
      @io = io
120✔
189
      @init_pos = begin
190
        io.pos
120✔
191
      rescue
UNCOV
192
        nil
×
193
      end
194
    end
195

196
    # Resets the read pointer to the beginning of data stream. Do not call this during
197
    # a #each or #each_entry iteration. This only works with random access data streams
198
    # that respond to #rewind and #pos.
199
    def rewind
2✔
200
      if @init_pos.zero?
96!
201
        raise Minitar::NonSeekableStream unless Minitar.seekable?(@io, :rewind)
96!
202
        @io.rewind
96✔
203
      else
×
UNCOV
204
        raise Minitar::NonSeekableStream unless Minitar.seekable?(@io, :pos=)
×
205
        @io.pos = @init_pos
×
206
      end
207
    end
208

209
    # Iterates through each entry in the data stream.
210
    def each_entry
2✔
211
      return to_enum unless block_given?
120!
212

213
      loop do
120✔
214
        return if @io.eof?
492✔
215

216
        header = Minitar::PosixHeader.from_stream(@io)
472✔
217
        raise Minitar::InvalidTarStream unless header.valid?
470✔
218
        return if header.empty?
468✔
219

220
        raise Minitar::InvalidTarStream if header.size < 0
372!
221

222
        if header.long_name?
372✔
223
          name_block = (header.size / 512.0).ceil * 512
128✔
224

225
          long_name = @io.read(name_block).rstrip
128✔
226
          header = PosixHeader.from_stream(@io)
128✔
227

228
          return if header.empty?
128!
229
          header.long_name = long_name
128✔
230
        elsif header.pax_header?
244✔
231
          pax_header = PaxHeader.from_stream(@io, header)
6✔
232

233
          header = PosixHeader.from_stream(@io)
6✔
234
          return if header.empty?
6!
235

236
          header.size = pax_header.size if pax_header.size
6✔
237
        end
238

239
        entry = EntryStream.new(header, @io)
372✔
240
        size = entry.size
372✔
241

242
        yield entry
372✔
243

244
        skip = (512 - (size % 512)) % 512
372✔
245

246
        if Minitar.seekable?(@io, :seek)
372✔
247
          # avoid reading...
156✔
248
          try_seek(size - entry.bytes_read)
312✔
249
        else
30✔
250
          pending = size - entry.bytes_read
60✔
251
          while pending > 0
60✔
252
            bread = @io.read([pending, 4096].min).bytesize
8✔
253
            raise UnexpectedEOF if @io.eof?
8!
254
            pending -= bread
8✔
255
          end
256
        end
257

258
        @io.read(skip) # discard trailing zeros
372✔
259
        # make sure nobody can use #read, #getc or #rewind anymore
260
        entry.close
372✔
261
      end
262
    end
263
    alias_method :each, :each_entry
2✔
264

265
    # Returns false if the reader is open (it never closes).
266
    def closed? = false
2✔
267

268
    def close
2✔
269
    end
270

271
    private
2✔
272

273
    def try_seek(bytes)
2✔
274
      @io.seek(bytes, IO::SEEK_CUR)
312✔
275
    rescue RangeError
276
      # This happens when skipping the large entry and the skipping entry size exceeds
277
      # maximum allowed size (varies by platform and underlying IO object).
UNCOV
278
      max = RbConfig::LIMITS.fetch("INT_MAX", 2147483647)
×
279
      skipped = 0
×
280
      while skipped < bytes
×
281
        to_skip = [bytes - skipped, max].min
×
282
        @io.seek(to_skip, IO::SEEK_CUR)
×
283
        skipped += to_skip
×
284
      end
285
    end
286
  end
287
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

© 2026 Coveralls, Inc