minitar 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +285 -0
  3. data/CONTRIBUTING.md +273 -0
  4. data/CONTRIBUTORS.md +27 -0
  5. data/LICENCE.md +39 -0
  6. data/Manifest.txt +29 -6
  7. data/README.md +70 -0
  8. data/Rakefile +74 -19
  9. data/SECURITY.md +64 -0
  10. data/docs/ruby.txt +3 -3
  11. data/lib/minitar/input.rb +69 -56
  12. data/lib/minitar/output.rb +34 -22
  13. data/lib/minitar/pax_header.rb +111 -0
  14. data/lib/minitar/posix_header.rb +96 -57
  15. data/lib/minitar/reader.rb +65 -70
  16. data/lib/minitar/version.rb +5 -0
  17. data/lib/minitar/writer.rb +50 -88
  18. data/lib/minitar.rb +60 -64
  19. data/licenses/bsdl.txt +20 -0
  20. data/licenses/dco.txt +34 -0
  21. data/licenses/ruby.txt +52 -0
  22. data/test/fixtures/issue_46.tar.gz +0 -0
  23. data/test/fixtures/issue_62.tar.gz +0 -0
  24. data/test/fixtures/tar_input.tgz +0 -0
  25. data/test/fixtures/test_input_non_strict_octal.tgz +0 -0
  26. data/test/fixtures/test_input_relative.tgz +0 -0
  27. data/test/fixtures/test_input_space_octal.tgz +0 -0
  28. data/test/fixtures/test_minitar.tar.gz +0 -0
  29. data/test/minitest_helper.rb +12 -1
  30. data/test/support/minitar_test_helpers/fixtures.rb +38 -0
  31. data/test/support/minitar_test_helpers/header.rb +130 -0
  32. data/test/support/minitar_test_helpers/tarball.rb +324 -0
  33. data/test/support/minitar_test_helpers.rb +36 -0
  34. data/test/test_filename_boundary_conditions.rb +74 -0
  35. data/test/test_gnu_tar_compatibility.rb +92 -0
  36. data/test/test_integration_pack_unpack_cycle.rb +38 -0
  37. data/test/test_issue_46.rb +5 -23
  38. data/test/test_issue_62.rb +50 -0
  39. data/test/test_minitar.rb +168 -39
  40. data/test/test_pax_header.rb +104 -0
  41. data/test/test_pax_support.rb +66 -0
  42. data/test/test_tar_header.rb +289 -75
  43. data/test/test_tar_input.rb +14 -61
  44. data/test/test_tar_output.rb +7 -9
  45. data/test/test_tar_reader.rb +17 -18
  46. data/test/test_tar_writer.rb +105 -126
  47. metadata +95 -89
  48. data/Contributing.md +0 -94
  49. data/History.md +0 -236
  50. data/Licence.md +0 -15
  51. data/README.rdoc +0 -92
  52. data/test/support/tar_test_helpers.rb +0 -134
  53. /data/{Code-of-Conduct.md → CODE_OF_CONDUCT.md} +0 -0
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Minitar
4
+ # Implements the PAX Extended Header as a Ruby class. The header consists of following
5
+ # format:
6
+ #
7
+ # <decimal-length><space><ascii-keyword>=<value><newline>
8
+ #
9
+ # Where:
10
+ #
11
+ # - +decimal-length+ is the total number of bytes for the PaxHeader record using ASCII
12
+ # decimal values; this includes the terminal newline (0x10).
13
+ # - +space+ is a single literal ASCII space (0x20).
14
+ # - +ascii-keyword+ is a PAX Extended Header keyword, which may be any ASCII character
15
+ # except newline (0x10) or equal sign (0x3D).
16
+ # - +=+ is the literal ASCII equal sign (0x3D).
17
+ # - +value+ is any series of bytes except newline (0x10).
18
+ # - +newline+ is the literal ASCII newline (0x10).
19
+ #
20
+ # There are several keywords defined in the POSIX standard; some of them are supported
21
+ # in this class, but may not be supported by Minitar as a whole.
22
+ #
23
+ # Primary support for PAX extended headers is for extracting size information for large
24
+ # file support. Other features may be added in the future.
25
+ class PaxHeader
26
+ BLOCK_SIZE = 512
27
+
28
+ attr_reader :attributes
29
+
30
+ class << self
31
+ # Creates a new PaxHeader from a data stream and posix header. Reads the PAX content
32
+ # based on the size specified in the posix header.
33
+ def from_stream(stream, posix_header)
34
+ raise ArgumentError, "Header must be a PAX header" unless posix_header.pax_header?
35
+
36
+ pax_block = (posix_header.size / BLOCK_SIZE.to_f).ceil * BLOCK_SIZE
37
+ pax_content = stream.read(pax_block)
38
+
39
+ raise Minitar::InvalidTarStream if pax_content.nil? || pax_content.bytesize < posix_header.size
40
+
41
+ actual_content = pax_content[0, posix_header.size]
42
+
43
+ from_data(actual_content)
44
+ end
45
+
46
+ # Creates a new PaxHeader from PAX content data.
47
+ def from_data(content) = new(parse_content(content))
48
+
49
+ private
50
+
51
+ def parse_content(content)
52
+ attributes = {}
53
+ offset = 0
54
+
55
+ while offset < content.bytesize
56
+ space_pos = content.index(" ", offset)
57
+ break unless space_pos
58
+
59
+ length_str = content[offset, space_pos - offset]
60
+
61
+ unless length_str.match?(/\A\d+\z/)
62
+ raise ArgumentError, "Invalid length format in PAX header: '#{length_str}'"
63
+ end
64
+
65
+ length = length_str.to_i
66
+ if offset + length > content.bytesize
67
+ raise ArgumentError, "Length beyond PAX header: '#{content[offset..]}'"
68
+ end
69
+ record = content[offset, length]
70
+
71
+ keyword_value = record[(space_pos - offset + 1)..-2]
72
+ if keyword_value.include?("=")
73
+ keyword, value = keyword_value.split("=", 2)
74
+ attributes[keyword] = value
75
+ end
76
+
77
+ offset += length
78
+ end
79
+ attributes
80
+ end
81
+ end
82
+
83
+ # Creates a new PaxHeader from attributes hash.
84
+ def initialize(attributes = {})
85
+ @attributes = attributes.transform_keys(&:to_s)
86
+ end
87
+
88
+ # The size value from PAX attributes
89
+ def size = @attributes["size"]&.to_i
90
+
91
+ # The path value from PAX attributes
92
+ def path = @attributes["path"]
93
+
94
+ # The mtime value from PAX attributes
95
+ def mtime = @attributes["mtime"]&.to_f
96
+
97
+ # Returns a string representation of the PAX header content.
98
+ def to_s
99
+ @attributes.map do |keyword, value|
100
+ keyword_value = " #{keyword}=#{value}\n"
101
+ record = keyword_value
102
+ begin
103
+ length = record.bytesize
104
+ length_str = length.to_s
105
+ record = "#{length_str}#{keyword_value}"
106
+ end while record.size != length # standard:disable Lint/Loop
107
+ record
108
+ end.join
109
+ end
110
+ end
111
+ end
@@ -1,34 +1,34 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Minitar
2
4
  # Implements the POSIX tar header as a Ruby class. The structure of
3
5
  # the POSIX tar header is:
4
6
  #
5
7
  # struct tarfile_entry_posix
6
- # { // pack/unpack
7
- # char name[100]; // ASCII (+ Z unless filled) a100/Z100
8
- # char mode[8]; // 0 padded, octal, null a8 /A8
9
- # char uid[8]; // 0 padded, octal, null a8 /A8
10
- # char gid[8]; // 0 padded, octal, null a8 /A8
11
- # char size[12]; // 0 padded, octal, null a12 /A12
12
- # char mtime[12]; // 0 padded, octal, null a12 /A12
13
- # char checksum[8]; // 0 padded, octal, null, space a8 /A8
14
- # char typeflag[1]; // see below a /a
15
- # char linkname[100]; // ASCII + (Z unless filled) a100/Z100
16
- # char magic[6]; // "ustar\0" a6 /A6
17
- # char version[2]; // "00" a2 /A2
18
- # char uname[32]; // ASCIIZ a32 /Z32
19
- # char gname[32]; // ASCIIZ a32 /Z32
20
- # char devmajor[8]; // 0 padded, octal, null a8 /A8
21
- # char devminor[8]; // 0 padded, octal, null a8 /A8
22
- # char prefix[155]; // ASCII (+ Z unless filled) a155/Z155
8
+ # { // pack unpack
9
+ # char name[100]; // ASCII (+ Z unless filled) a100 Z100
10
+ # char mode[8]; // 0 padded, octal, null a8 A8
11
+ # char uid[8]; // 0 padded, octal, null a8 A8
12
+ # char gid[8]; // 0 padded, octal, null a8 A8
13
+ # char size[12]; // 0 padded, octal, null a12 A12
14
+ # char mtime[12]; // 0 padded, octal, null a12 A12
15
+ # char checksum[8]; // 0 padded, octal, null, space a8 A8
16
+ # char typeflag[1]; // see below a a
17
+ # char linkname[100]; // ASCII + (Z unless filled) a100 Z100
18
+ # char magic[6]; // "ustar\0" a6 A6
19
+ # char version[2]; // "00" a2 A2
20
+ # char uname[32]; // ASCIIZ a32 Z32
21
+ # char gname[32]; // ASCIIZ a32 Z32
22
+ # char devmajor[8]; // 0 padded, octal, null a8 A8
23
+ # char devminor[8]; // 0 padded, octal, null a8 A8
24
+ # char prefix[155]; // ASCII (+ Z unless filled) a155 Z155
23
25
  # };
24
26
  #
25
- # The #typeflag is one of several known values.
26
- #
27
- # POSIX indicates that "A POSIX-compliant implementation must treat any
28
- # unrecognized typeflag value as a regular file."
27
+ # The #typeflag is one of several known values. POSIX indicates that "A POSIX-compliant
28
+ # implementation must treat any unrecognized typeflag value as a regular file."
29
29
  class PosixHeader
30
30
  BLOCK_SIZE = 512
31
- MAGIC_BYTES = "ustar".freeze
31
+ MAGIC_BYTES = "ustar"
32
32
 
33
33
  GNU_EXT_LONG_LINK = "././@LongLink"
34
34
 
@@ -36,38 +36,33 @@ class Minitar
36
36
  REQUIRED_FIELDS = [:name, :size, :prefix, :mode].freeze
37
37
  # Fields that may be set in a POSIX tar(1) header.
38
38
  OPTIONAL_FIELDS = [
39
- :uid, :gid, :mtime, :checksum, :typeflag, :linkname, :magic, :version,
40
- :uname, :gname, :devmajor, :devminor
39
+ :uid, :gid, :mtime, :checksum, :typeflag, :linkname, :magic, :version, :uname,
40
+ :gname, :devmajor, :devminor
41
41
  ].freeze
42
42
 
43
43
  # All fields available in a POSIX tar(1) header.
44
44
  FIELDS = (REQUIRED_FIELDS + OPTIONAL_FIELDS).freeze
45
45
 
46
46
  FIELDS.each do |f|
47
- attr_reader f.to_sym unless f.to_sym == :name
47
+ attr_reader f.to_sym
48
+ end
49
+
50
+ ##
51
+ def name=(value)
52
+ valid_name!(value)
53
+ @name = value
48
54
  end
49
55
 
50
- # The name of the file. By default, limited to 100 bytes. Required. May be
51
- # longer (up to BLOCK_SIZE bytes) if using the GNU long name tar extension.
52
- attr_accessor :name
56
+ attr_writer :size
53
57
 
54
58
  # The pack format passed to Array#pack for encoding a header.
55
- HEADER_PACK_FORMAT = "a100a8a8a8a12a12a7aaa100a6a2a32a32a8a8a155".freeze
59
+ HEADER_PACK_FORMAT = "a100a8a8a8a12a12a7aaa100a6a2a32a32a8a8a155"
56
60
  # The unpack format passed to String#unpack for decoding a header.
57
- HEADER_UNPACK_FORMAT = "Z100A8A8A8A12A12A8aZ100A6A2Z32Z32A8A8Z155".freeze
61
+ HEADER_UNPACK_FORMAT = "Z100A8A8A8a12A12A8aZ100A6A2Z32Z32A8A8Z155"
58
62
 
59
63
  class << self
60
64
  # Creates a new PosixHeader from a data stream.
61
- def from_stream(stream)
62
- from_data(stream.read(BLOCK_SIZE))
63
- end
64
-
65
- # Creates a new PosixHeader from a data stream. Deprecated; use
66
- # PosixHeader.from_stream instead.
67
- def new_from_stream(stream)
68
- warn "#{__method__} has been deprecated; use from_stream instead."
69
- from_stream(stream)
70
- end
65
+ def from_stream(stream) = from_data(stream.read(BLOCK_SIZE))
71
66
 
72
67
  # Creates a new PosixHeader from a BLOCK_SIZE-byte data buffer.
73
68
  def from_data(data)
@@ -76,7 +71,7 @@ class Minitar
76
71
  mode = fields.shift.oct
77
72
  uid = fields.shift.oct
78
73
  gid = fields.shift.oct
79
- size = strict_oct(fields.shift)
74
+ size = parse_numeric_field(fields.shift)
80
75
  mtime = fields.shift.oct
81
76
  checksum = fields.shift.oct
82
77
  typeflag = fields.shift
@@ -114,14 +109,30 @@ class Minitar
114
109
 
115
110
  private
116
111
 
117
- def strict_oct(string)
118
- return string.oct if /\A[0-7 ]*\z/.match?(string)
119
- raise ArgumentError, "#{string.inspect} is not a valid octal string"
112
+ def parse_numeric_field(string)
113
+ return string.oct if /\A[0-7 \0]*\z/.match?(string) # \0 appears as a padding
114
+ return parse_base256(string) if string.bytes.first == 0x80 || string.bytes.first == 0xff
115
+ raise ArgumentError, "#{string.inspect} is not a valid numeric field"
116
+ end
117
+
118
+ def parse_base256(string)
119
+ # https://www.gnu.org/software/tar/manual/html_node/Extensions.html
120
+ bytes = string.bytes
121
+ case bytes.first
122
+ when 0x80 # Positive number: *non-leading* bytes, number in big-endian order
123
+ bytes[1..].inject(0) { |r, byte| (r << 8) | byte }
124
+ when 0xff # Negative number: *all* bytes, two's complement in big-endian order
125
+ result = bytes.inject(0) { |r, byte| (r << 8) | byte }
126
+ bit_length = bytes.size * 8
127
+ result - (1 << bit_length)
128
+ else
129
+ raise ArgumentError, "Invalid binary field format"
130
+ end
120
131
  end
121
132
  end
122
133
 
123
- # Creates a new PosixHeader. A PosixHeader cannot be created unless
124
- # +name+, +size+, +prefix+, and +mode+ are provided.
134
+ # Creates a new PosixHeader. A PosixHeader cannot be created unless +name+, +size+,
135
+ # +prefix+, and +mode+ are provided.
125
136
  def initialize(v)
126
137
  REQUIRED_FIELDS.each do |f|
127
138
  raise ArgumentError, "Field #{f} is required." unless v.key?(f)
@@ -138,22 +149,35 @@ class Minitar
138
149
  end
139
150
 
140
151
  @empty = v[:empty]
152
+
153
+ valid_name!(v[:name]) unless v[:empty]
141
154
  end
142
155
 
143
156
  # Indicates if the header was an empty header.
144
- def empty?
145
- @empty
146
- end
157
+ def empty? = @empty
147
158
 
148
159
  # Indicates if the header has a valid magic value.
149
- def valid?
150
- empty? || @magic == MAGIC_BYTES
151
- end
160
+ def valid? = empty? || @magic == MAGIC_BYTES
152
161
 
153
162
  # Returns +true+ if the header is a long name special header which indicates
154
163
  # that the next block of data is the filename.
155
- def long_name?
156
- typeflag == "L" && name == GNU_EXT_LONG_LINK
164
+ def long_name? = typeflag == "L" && name == GNU_EXT_LONG_LINK
165
+
166
+ # Returns +true+ if the header is a PAX extended header which contains
167
+ # metadata for the next file entry.
168
+ def pax_header? = typeflag == "x"
169
+
170
+ # Sets the +name+ to the +value+ provided and clears +prefix+.
171
+ #
172
+ # Used by Minitar::Reader#each_entry to set the long name when processing GNU long
173
+ # filename extensions.
174
+ #
175
+ # The +value+ must be the complete name, including leading directory components.
176
+ def long_name=(value)
177
+ valid_name!(value)
178
+
179
+ @prefix = ""
180
+ @name = value
157
181
  end
158
182
 
159
183
  # A string representation of the header.
@@ -163,6 +187,8 @@ class Minitar
163
187
  end
164
188
  alias_method :to_str, :to_s
165
189
 
190
+ # TODO: In Minitar 2, PosixHeader#to_str will be removed.
191
+
166
192
  # Update the checksum field.
167
193
  def update_checksum
168
194
  hh = header(" " * 8)
@@ -171,6 +197,11 @@ class Minitar
171
197
 
172
198
  private
173
199
 
200
+ def valid_name!(value)
201
+ return if value.is_a?(String) && !value.empty?
202
+ raise ArgumentError, "Field name must be a non-empty string"
203
+ end
204
+
174
205
  def oct(num, len)
175
206
  if num.nil?
176
207
  "\0" * (len + 1)
@@ -180,7 +211,7 @@ class Minitar
180
211
  end
181
212
 
182
213
  def calculate_checksum(hdr)
183
- hdr.unpack("C*").inject { |a, e| a + e }
214
+ hdr.unpack("C*").inject(:+)
184
215
  end
185
216
 
186
217
  def header(chksum)
@@ -192,7 +223,14 @@ class Minitar
192
223
  end
193
224
 
194
225
  ##
195
- # :attr_reader: size
226
+ # :attr_accessor: name
227
+ # The name of the file. Required.
228
+ #
229
+ # By default, limited to 100 bytes, but may be up to BLOCK_SIZE bytes if using the
230
+ # GNU long name tar extension.
231
+
232
+ ##
233
+ # :attr_accessor: size
196
234
  # The size of the file. Required.
197
235
 
198
236
  ##
@@ -243,10 +281,11 @@ class Minitar
243
281
  # +5+:: Directory.
244
282
  # +6+:: FIFO node.
245
283
  # +7+:: Reserved.
284
+ # +L+:: GNU extension for long filenames when #name is <tt>././@LongLink</tt>.
246
285
 
247
286
  ##
248
287
  # :attr_reader: linkname
249
- # The name of the link stored. Not currently used.
288
+ # The target of the symbolic link.
250
289
 
251
290
  ##
252
291
  # :attr_reader: magic
@@ -1,28 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Minitar
2
- # The class that reads a tar format archive from a data stream. The data
3
- # stream may be sequential or random access, but certain features only work
4
- # with random access data streams.
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.
5
7
  class Reader
6
8
  include Enumerable
7
9
 
8
- # This marks the EntryStream closed for reading without closing the
9
- # actual data stream.
10
+ # This marks the EntryStream closed for reading without closing the actual data
11
+ # stream.
10
12
  module InvalidEntryStream
11
- def read(*)
12
- raise ClosedStream
13
- end
13
+ def read(*) = raise ClosedStream # :nodoc:
14
14
 
15
- def getc
16
- raise ClosedStream
17
- end
15
+ def getc = raise ClosedStream # :nodoc:
18
16
 
19
- def rewind
20
- raise ClosedStream
21
- end
17
+ def rewind = raise ClosedStream # :nodoc:
22
18
 
23
- def closed?
24
- true
25
- end
19
+ def closed? = true # :nodoc:
26
20
  end
27
21
 
28
22
  # EntryStreams are pseudo-streams on top of the main data stream.
@@ -58,8 +52,8 @@ class Minitar
58
52
  end
59
53
  end
60
54
 
61
- # Reads +len+ bytes (or all remaining data) from the entry. Returns
62
- # +nil+ if there is no more data to read.
55
+ # Reads +len+ bytes (or all remaining data) from the entry. Returns +nil+ if there
56
+ # is no more data to read.
63
57
  def read(len = nil)
64
58
  return nil if @read >= @size
65
59
  len ||= @size - @read
@@ -69,8 +63,7 @@ class Minitar
69
63
  ret
70
64
  end
71
65
 
72
- # Reads one byte from the entry. Returns +nil+ if there is no more data
73
- # to read.
66
+ # Reads one byte from the entry. Returns +nil+ if there is no more data to read.
74
67
  def getc
75
68
  return nil if @read >= @size
76
69
  ret = @io.getc
@@ -84,10 +77,9 @@ class Minitar
84
77
  when "5"
85
78
  true
86
79
  when "0", "\0"
87
- # If the name ends with a slash, treat it as a directory.
88
- # This is what other major tar implementations do for
89
- # interoperability and compatibility with older tar variants
90
- # and some new ones.
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.
91
83
  @name.end_with?("/")
92
84
  else
93
85
  false
@@ -101,16 +93,13 @@ class Minitar
101
93
  end
102
94
  alias_method :file, :file?
103
95
 
104
- # Returns +true+ if the current read pointer is at the end of the
105
- # EntryStream data.
106
- def eof?
107
- @read >= @size
108
- end
96
+ # Returns +true+ if the current read pointer is at the end of the EntryStream data.
97
+ def eof? = @read >= @size
109
98
 
110
99
  # Returns the current read pointer in the EntryStream.
111
- def pos
112
- @read
113
- end
100
+ def pos = @read
101
+
102
+ alias_method :bytes_read, :pos
114
103
 
115
104
  # Sets the current read pointer to the beginning of the EntryStream.
116
105
  def rewind
@@ -121,10 +110,6 @@ class Minitar
121
110
  @read = 0
122
111
  end
123
112
 
124
- def bytes_read
125
- @read
126
- end
127
-
128
113
  # Returns the full and proper name of the entry.
129
114
  def full_name
130
115
  if @prefix != ""
@@ -135,14 +120,10 @@ class Minitar
135
120
  end
136
121
 
137
122
  # Returns false if the entry stream is valid.
138
- def closed?
139
- false
140
- end
123
+ def closed? = false
141
124
 
142
125
  # Closes the entry.
143
- def close
144
- invalidate
145
- end
126
+ def close = invalidate
146
127
 
147
128
  private
148
129
 
@@ -151,17 +132,16 @@ class Minitar
151
132
  end
152
133
  end
153
134
 
154
- # With no associated block, +Reader::open+ is a synonym for
155
- # +Reader::new+. If the optional code block is given, it will be passed
156
- # the new _writer_ as an argument and the Reader object will
157
- # automatically be closed when the block terminates. In this instance,
158
- # +Reader::open+ returns the value of the block.
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.
159
139
  def self.open(io)
160
140
  reader = new(io)
161
141
  return reader unless block_given?
162
142
 
163
- # This exception context must remain, otherwise the stream closes on open
164
- # even if a block is not given.
143
+ # This exception context must remain, otherwise the stream closes on open even if
144
+ # a block is not given.
165
145
  begin
166
146
  yield reader
167
147
  ensure
@@ -169,8 +149,7 @@ class Minitar
169
149
  end
170
150
  end
171
151
 
172
- # Iterates over each entry in the provided input. This wraps the common
173
- # pattern of:
152
+ # Iterates over each entry in the provided input. This wraps the common pattern of:
174
153
  #
175
154
  # Minitar::Input.open(io) do |i|
176
155
  # inp.each do |entry|
@@ -178,10 +157,9 @@ class Minitar
178
157
  # end
179
158
  # end
180
159
  #
181
- # If a block is not provided, an enumerator will be created with the same
182
- # behaviour.
160
+ # If a block is not provided, an enumerator will be created with the same behaviour.
183
161
  #
184
- # call-seq:
162
+ # :call-seq:
185
163
  # Minitar::Reader.each_entry(io) -> enumerator
186
164
  # Minitar::Reader.each_entry(io) { |entry| block } -> obj
187
165
  def self.each_entry(io)
@@ -204,19 +182,15 @@ class Minitar
204
182
  end
205
183
  end
206
184
 
207
- # Resets the read pointer to the beginning of data stream. Do not call
208
- # this during a #each or #each_entry iteration. This only works with
209
- # random access data streams that respond to #rewind and #pos.
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.
210
188
  def rewind
211
189
  if @init_pos.zero?
212
- unless Minitar.seekable?(@io, :rewind)
213
- raise Minitar::NonSeekableStream
214
- end
190
+ raise Minitar::NonSeekableStream unless Minitar.seekable?(@io, :rewind)
215
191
  @io.rewind
216
192
  else
217
- unless Minitar.seekable?(@io, :pos=)
218
- raise Minitar::NonSeekableStream
219
- end
193
+ raise Minitar::NonSeekableStream unless Minitar.seekable?(@io, :pos=)
220
194
  @io.pos = @init_pos
221
195
  end
222
196
  end
@@ -237,11 +211,18 @@ class Minitar
237
211
  if header.long_name?
238
212
  name_block = (header.size / 512.0).ceil * 512
239
213
 
240
- name = @io.read(name_block).rstrip
214
+ long_name = @io.read(name_block).rstrip
241
215
  header = PosixHeader.from_stream(@io)
242
216
 
243
217
  return if header.empty?
244
- header.name = name
218
+ header.long_name = long_name
219
+ elsif header.pax_header?
220
+ pax_header = PaxHeader.from_stream(@io, header)
221
+
222
+ header = PosixHeader.from_stream(@io)
223
+ return if header.empty?
224
+
225
+ header.size = pax_header.size if pax_header.size
245
226
  end
246
227
 
247
228
  entry = EntryStream.new(header, @io)
@@ -253,7 +234,7 @@ class Minitar
253
234
 
254
235
  if Minitar.seekable?(@io, :seek)
255
236
  # avoid reading...
256
- @io.seek(size - entry.bytes_read, IO::SEEK_CUR)
237
+ try_seek(size - entry.bytes_read)
257
238
  else
258
239
  pending = size - entry.bytes_read
259
240
  while pending > 0
@@ -271,11 +252,25 @@ class Minitar
271
252
  alias_method :each, :each_entry
272
253
 
273
254
  # Returns false if the reader is open (it never closes).
274
- def closed?
275
- false
276
- end
255
+ def closed? = false
277
256
 
278
257
  def close
279
258
  end
259
+
260
+ private
261
+
262
+ def try_seek(bytes)
263
+ @io.seek(bytes, IO::SEEK_CUR)
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).
267
+ max = RbConfig::LIMITS.fetch("INT_MAX", 2147483647)
268
+ skipped = 0
269
+ while skipped < bytes
270
+ to_skip = [bytes - skipped, max].min
271
+ @io.seek(to_skip, IO::SEEK_CUR)
272
+ skipped += to_skip
273
+ end
274
+ end
280
275
  end
281
276
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Minitar
4
+ VERSION = "1.1.0"
5
+ end