minitar 0.5.4 → 0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,212 @@
1
+ # coding: utf-8
2
+
3
+ require 'archive/tar/minitar/reader'
4
+
5
+ module Archive::Tar::Minitar
6
+ # Wraps a Archive::Tar::Minitar::Reader with convenience methods and wrapped
7
+ # stream management; Input only works with data streams that can be rewound.
8
+ class Input
9
+ include Enumerable
10
+
11
+ # With no associated block, +Input.open+ is a synonym for +Input.new+. If
12
+ # the optional code block is given, it will be given the new Input as an
13
+ # argument and the Input object will automatically be closed when the block
14
+ # terminates (this also closes the wrapped stream object). In this
15
+ # instance, +Input.open+ returns the value of the block.
16
+ #
17
+ # call-seq:
18
+ # Archive::Tar::Minitar::Input.open(io) -> input
19
+ # Archive::Tar::Minitar::Input.open(io) { |input| block } -> obj
20
+ def self.open(input)
21
+ stream = new(input)
22
+ return stream unless block_given?
23
+ yield stream
24
+ ensure
25
+ stream.close
26
+ end
27
+
28
+ # Iterates over each entry in the provided input. This wraps the common
29
+ # pattern of:
30
+ #
31
+ # Archive::Tar::Minitar::Input.open(io) do |i|
32
+ # inp.each do |entry|
33
+ # # ...
34
+ # end
35
+ # end
36
+ #
37
+ # If a block is not provided, an enumerator will be created with the same
38
+ # behaviour.
39
+ #
40
+ # call-seq:
41
+ # Archive::Tar::Minitar::Input.each_entry(io) -> enumerator
42
+ # Archive::Tar::Minitar::Input.each_entry(io) { |entry| block } -> obj
43
+ def self.each_entry(input)
44
+ return to_enum(__method__, input) unless block_given?
45
+
46
+ open(input) do |stream|
47
+ stream.each do |entry|
48
+ yield entry
49
+ end
50
+ end
51
+ end
52
+
53
+ # Creates a new Input object. If +input+ is a stream object that responds
54
+ # to #read, then it will simply be wrapped. Otherwise, one will be created
55
+ # and opened using Kernel#open. When Input#close is called, the stream
56
+ # object wrapped will be closed.
57
+ #
58
+ # An exception will be raised if the stream that is wrapped does not
59
+ # support rewinding.
60
+ #
61
+ # call-seq:
62
+ # Archive::Tar::Minitar::Input.new(io) -> input
63
+ # Archive::Tar::Minitar::Input.new(path) -> input
64
+ def initialize(input)
65
+ @io = if input.respond_to?(:read)
66
+ input
67
+ else
68
+ ::Kernel.open(input, 'rb')
69
+ end
70
+
71
+ unless Archive::Tar::Minitar.seekable?(@io, :rewind)
72
+ raise Archive::Tar::Minitar::NonSeekableStream
73
+ end
74
+
75
+ @tar = Reader.new(@io)
76
+ end
77
+
78
+ # When provided a block, iterates through each entry in the archive. When
79
+ # finished, rewinds to the beginning of the stream.
80
+ #
81
+ # If not provided a block, creates an enumerator with the same semantics.
82
+ def each_entry
83
+ return to_enum unless block_given?
84
+
85
+ @tar.each do |entry|
86
+ yield entry
87
+ end
88
+ ensure
89
+ @tar.rewind
90
+ end
91
+ alias each each_entry
92
+
93
+ # Extracts the current +entry+ to +destdir+. If a block is provided, it
94
+ # yields an +action+ Symbol, the full name of the file being extracted
95
+ # (+name+), and a Hash of statistical information (+stats+).
96
+ #
97
+ # The +action+ will be one of:
98
+ # <tt>:dir</tt>:: The +entry+ is a directory.
99
+ # <tt>:file_start</tt>:: The +entry+ is a file; the extract of the
100
+ # file is just beginning.
101
+ # <tt>:file_progress</tt>:: Yielded every 4096 bytes during the extract
102
+ # of the +entry+.
103
+ # <tt>:file_done</tt>:: Yielded when the +entry+ is completed.
104
+ #
105
+ # The +stats+ hash contains the following keys:
106
+ # <tt>:current</tt>:: The current total number of bytes read in the
107
+ # +entry+.
108
+ # <tt>:currinc</tt>:: The current number of bytes read in this read
109
+ # cycle.
110
+ # <tt>:entry</tt>:: The entry being extracted; this is a
111
+ # Reader::EntryStream, with all methods thereof.
112
+ def extract_entry(destdir, entry) # :yields action, name, stats:
113
+ stats = {
114
+ :current => 0,
115
+ :currinc => 0,
116
+ :entry => entry
117
+ }
118
+
119
+ # extract_entry is not vulnerable to prefix '/' vulnerabilities, but it
120
+ # is vulnerable to relative path directories. This code will break this
121
+ # vulnerability. For this version, we are breaking relative paths HARD by
122
+ # throwing an exception.
123
+ #
124
+ # Future versions may permit relative paths as long as the file does not
125
+ # leave +destdir+.
126
+ #
127
+ # However, squeeze consecutive '/' characters together.
128
+ full_name = entry.full_name.squeeze('/')
129
+
130
+ if full_name =~ /\.{2}(?:\/|\z)/
131
+ raise SecureRelativePathError, %q(Path contains '..')
132
+ end
133
+
134
+ if entry.directory?
135
+ dest = File.join(destdir, full_name)
136
+
137
+ yield :dir, full_name, stats if block_given?
138
+
139
+ if Archive::Tar::Minitar.dir?(dest)
140
+ begin
141
+ FileUtils.chmod(entry.mode, dest)
142
+ rescue
143
+ nil
144
+ end
145
+ else
146
+ File.unlink(dest.chomp('/')) if File.symlink?(dest.chomp('/'))
147
+
148
+ FileUtils.mkdir_p(dest, :mode => entry.mode)
149
+ FileUtils.chmod(entry.mode, dest)
150
+ end
151
+
152
+ fsync_dir(dest)
153
+ fsync_dir(File.join(dest, '..'))
154
+ return
155
+ else # it's a file
156
+ destdir = File.join(destdir, File.dirname(full_name))
157
+ FileUtils.mkdir_p(destdir, :mode => 0o755)
158
+
159
+ destfile = File.join(destdir, File.basename(full_name))
160
+
161
+ File.unlink(destfile) if File.symlink?(destfile)
162
+
163
+ # Errno::ENOENT
164
+ # rubocop:disable Style/RescueModifier
165
+ FileUtils.chmod(0o600, destfile) rescue nil
166
+ # rubocop:enable Style/RescueModifier
167
+
168
+ yield :file_start, full_name, stats if block_given?
169
+
170
+ File.open(destfile, 'wb', entry.mode) do |os|
171
+ loop do
172
+ data = entry.read(4096)
173
+ break unless data
174
+
175
+ stats[:currinc] = os.write(data)
176
+ stats[:current] += stats[:currinc]
177
+
178
+ yield :file_progress, full_name, stats if block_given?
179
+ end
180
+ os.fsync
181
+ end
182
+
183
+ FileUtils.chmod(entry.mode, destfile)
184
+ fsync_dir(File.dirname(destfile))
185
+ fsync_dir(File.join(File.dirname(destfile), '..'))
186
+
187
+ yield :file_done, full_name, stats if block_given?
188
+ end
189
+ end
190
+
191
+ # Returns the Reader object for direct access.
192
+ attr_reader :tar
193
+
194
+ # Closes both the Reader object and the wrapped data stream.
195
+ def close
196
+ @io.close
197
+ @tar.close
198
+ end
199
+
200
+ private
201
+
202
+ def fsync_dir(dirname)
203
+ # make sure this hits the disc
204
+ dir = open(dirname, 'rb')
205
+ dir.fsync
206
+ rescue # ignore IOError if it's an unpatched (old) Ruby
207
+ nil
208
+ ensure
209
+ dir.close if dir rescue nil # rubocop:disable Style/RescueModifier
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,69 @@
1
+ # coding: utf-8
2
+
3
+ require 'archive/tar/minitar/writer'
4
+
5
+ module Archive::Tar::Minitar
6
+ # Wraps a Archive::Tar::Minitar::Writer with convenience methods and wrapped
7
+ # stream management. If the stream provided to Output does not support random
8
+ # access, only Writer#add_file_simple and Writer#mkdir are guaranteed to
9
+ # work.
10
+ class Output
11
+ # With no associated block, +Output.open+ is a synonym for +Output.new+. If
12
+ # the optional code block is given, it will be given the new Output as an
13
+ # argument and the Output object will automatically be closed when the
14
+ # block terminates (this also closes the wrapped stream object). In this
15
+ # instance, +Output.open+ returns the value of the block.
16
+ #
17
+ # call-seq:
18
+ # Archive::Tar::Minitar::Output.open(io) -> output
19
+ # Archive::Tar::Minitar::Output.open(io) { |output| block } -> obj
20
+ def self.open(output)
21
+ stream = new(output)
22
+ return stream unless block_given?
23
+ yield stream
24
+ ensure
25
+ stream.close
26
+ end
27
+
28
+ # Output.tar is a wrapper for Output.open that yields the owned tar object
29
+ # instead of the Output object. If a block is not provided, an enumerator
30
+ # will be created with the same behaviour.
31
+ #
32
+ # call-seq:
33
+ # Archive::Tar::Minitar::Output.tar(io) -> enumerator
34
+ # Archive::Tar::Minitar::Output.tar(io) { |tar| block } -> obj
35
+ def self.tar(output)
36
+ return to_enum(__method__, output) unless block_given?
37
+
38
+ open(output) do |stream|
39
+ yield stream.tar
40
+ end
41
+ end
42
+
43
+ # Creates a new Output object. If +output+ is a stream object that responds
44
+ # to #write, then it will simply be wrapped. Otherwise, one will be created
45
+ # and opened using Kernel#open. When Output#close is called, the stream
46
+ # object wrapped will be closed.
47
+ #
48
+ # call-seq:
49
+ # Archive::Tar::Minitar::Output.new(io) -> output
50
+ # Archive::Tar::Minitar::Output.new(path) -> output
51
+ def initialize(output)
52
+ @io = if output.respond_to?(:write)
53
+ output
54
+ else
55
+ ::Kernel.open(output, 'wb')
56
+ end
57
+ @tar = Archive::Tar::Minitar::Writer.new(@io)
58
+ end
59
+
60
+ # Returns the Writer object for direct access.
61
+ attr_reader :tar
62
+
63
+ # Closes the Writer object and the wrapped data stream.
64
+ def close
65
+ @tar.close
66
+ @io.close
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,259 @@
1
+ # coding: utf-8
2
+
3
+ ##
4
+ module Archive; end
5
+ ##
6
+ module Archive::Tar; end
7
+ ##
8
+ module Archive::Tar::Minitar; end
9
+
10
+ # Implements the POSIX tar header as a Ruby class. The structure of
11
+ # the POSIX tar header is:
12
+ #
13
+ # struct tarfile_entry_posix
14
+ # { // pack/unpack
15
+ # char name[100]; // ASCII (+ Z unless filled) a100/Z100
16
+ # char mode[8]; // 0 padded, octal, null a8 /A8
17
+ # char uid[8]; // 0 padded, octal, null a8 /A8
18
+ # char gid[8]; // 0 padded, octal, null a8 /A8
19
+ # char size[12]; // 0 padded, octal, null a12 /A12
20
+ # char mtime[12]; // 0 padded, octal, null a12 /A12
21
+ # char checksum[8]; // 0 padded, octal, null, space a8 /A8
22
+ # char typeflag[1]; // see below a /a
23
+ # char linkname[100]; // ASCII + (Z unless filled) a100/Z100
24
+ # char magic[6]; // "ustar\0" a6 /A6
25
+ # char version[2]; // "00" a2 /A2
26
+ # char uname[32]; // ASCIIZ a32 /Z32
27
+ # char gname[32]; // ASCIIZ a32 /Z32
28
+ # char devmajor[8]; // 0 padded, octal, null a8 /A8
29
+ # char devminor[8]; // 0 padded, octal, null a8 /A8
30
+ # char prefix[155]; // ASCII (+ Z unless filled) a155/Z155
31
+ # };
32
+ #
33
+ # The #typeflag is one of several known values.
34
+ #
35
+ # POSIX indicates that "A POSIX-compliant implementation must treat any
36
+ # unrecognized typeflag value as a regular file."
37
+ class Archive::Tar::Minitar::PosixHeader
38
+ BLOCK_SIZE = 512
39
+
40
+ # Fields that must be set in a POSIX tar(1) header.
41
+ REQUIRED_FIELDS = [ :name, :size, :prefix, :mode ].freeze
42
+ # Fields that may be set in a POSIX tar(1) header.
43
+ OPTIONAL_FIELDS = [
44
+ :uid, :gid, :mtime, :checksum, :typeflag, :linkname, :magic, :version,
45
+ :uname, :gname, :devmajor, :devminor
46
+ ].freeze
47
+
48
+ # All fields available in a POSIX tar(1) header.
49
+ FIELDS = (REQUIRED_FIELDS + OPTIONAL_FIELDS).freeze
50
+
51
+ FIELDS.each do |f|
52
+ attr_reader f.to_sym unless f.to_sym == :name
53
+ end
54
+
55
+ # The name of the file. By default, limited to 100 bytes. Required. May be
56
+ # longer (up to BLOCK_SIZE bytes) if using the GNU long name tar extension.
57
+ attr_accessor :name
58
+
59
+ # The pack format passed to Array#pack for encoding a header.
60
+ HEADER_PACK_FORMAT = 'a100a8a8a8a12a12a7aaa100a6a2a32a32a8a8a155'.freeze
61
+ # The unpack format passed to String#unpack for decoding a header.
62
+ HEADER_UNPACK_FORMAT = 'Z100A8A8A8A12A12A8aZ100A6A2Z32Z32A8A8Z155'.freeze
63
+
64
+ class << self
65
+ # Creates a new PosixHeader from a data stream.
66
+ def from_stream(stream)
67
+ from_data(stream.read(BLOCK_SIZE))
68
+ end
69
+
70
+ # Creates a new PosixHeader from a data stream. Deprecated; use
71
+ # PosixHeader.from_stream instead.
72
+ def new_from_stream(stream)
73
+ warn "#{__method__} has been deprecated; use from_stream instead."
74
+ from_stream(stream)
75
+ end
76
+
77
+ # Creates a new PosixHeader from a BLOCK_SIZE-byte data buffer.
78
+ def from_data(data)
79
+ fields = data.unpack(HEADER_UNPACK_FORMAT)
80
+ name = fields.shift
81
+ mode = fields.shift.oct
82
+ uid = fields.shift.oct
83
+ gid = fields.shift.oct
84
+ size = fields.shift.oct
85
+ mtime = fields.shift.oct
86
+ checksum = fields.shift.oct
87
+ typeflag = fields.shift
88
+ linkname = fields.shift
89
+ magic = fields.shift
90
+ version = fields.shift.oct
91
+ uname = fields.shift
92
+ gname = fields.shift
93
+ devmajor = fields.shift.oct
94
+ devminor = fields.shift.oct
95
+ prefix = fields.shift
96
+
97
+ empty = !data.each_byte.any?(&:nonzero?)
98
+
99
+ new(
100
+ :name => name,
101
+ :mode => mode,
102
+ :uid => uid,
103
+ :gid => gid,
104
+ :size => size,
105
+ :mtime => mtime,
106
+ :checksum => checksum,
107
+ :typeflag => typeflag,
108
+ :magic => magic,
109
+ :version => version,
110
+ :uname => uname,
111
+ :gname => gname,
112
+ :devmajor => devmajor,
113
+ :devminor => devminor,
114
+ :prefix => prefix,
115
+ :empty => empty,
116
+ :linkname => linkname
117
+ )
118
+ end
119
+ end
120
+
121
+ # Creates a new PosixHeader. A PosixHeader cannot be created unless
122
+ # +name+, +size+, +prefix+, and +mode+ are provided.
123
+ def initialize(v)
124
+ REQUIRED_FIELDS.each do |f|
125
+ raise ArgumentError, "Field #{f} is required." unless v.key?(f)
126
+ end
127
+
128
+ v[:mtime] = v[:mtime].to_i
129
+ v[:checksum] ||= ''
130
+ v[:typeflag] ||= '0'
131
+ v[:magic] ||= 'ustar'
132
+ v[:version] ||= '00'
133
+
134
+ FIELDS.each do |f|
135
+ instance_variable_set("@#{f}", v[f])
136
+ end
137
+
138
+ @empty = v[:empty]
139
+ end
140
+
141
+ # Indicates if the header was an empty header.
142
+ def empty?
143
+ @empty
144
+ end
145
+
146
+ # Returns +true+ if the header is a long name special header which indicates
147
+ # that the next block of data is the filename.
148
+ def long_name?
149
+ typeflag == 'L' && name == '././@LongLink'
150
+ end
151
+
152
+ # A string representation of the header.
153
+ def to_s
154
+ update_checksum
155
+ header(@checksum)
156
+ end
157
+ alias to_str to_s
158
+
159
+ # Update the checksum field.
160
+ def update_checksum
161
+ hh = header(' ' * 8)
162
+ @checksum = oct(calculate_checksum(hh), 6)
163
+ end
164
+
165
+ private
166
+
167
+ def oct(num, len)
168
+ if num.nil?
169
+ "\0" * (len + 1)
170
+ else
171
+ "%0#{len}o" % num
172
+ end
173
+ end
174
+
175
+ def calculate_checksum(hdr)
176
+ hdr.unpack('C*').inject { |a, e| a + e }
177
+ end
178
+
179
+ def header(chksum)
180
+ arr = [name, oct(mode, 7), oct(uid, 7), oct(gid, 7), oct(size, 11),
181
+ oct(mtime, 11), chksum, ' ', typeflag, linkname, magic, version,
182
+ uname, gname, oct(devmajor, 7), oct(devminor, 7), prefix]
183
+ str = arr.pack(HEADER_PACK_FORMAT)
184
+ str + "\0" * ((BLOCK_SIZE - str.size) % BLOCK_SIZE)
185
+ end
186
+
187
+ ##
188
+ # :attr_reader: size
189
+ # The size of the file. Required.
190
+
191
+ ##
192
+ # :attr_reader: prefix
193
+ # The prefix of the file; the path before #name. Limited to 155 bytes.
194
+ # Required.
195
+
196
+ ##
197
+ # :attr_reader: mode
198
+ # The Unix file mode of the file. Stored as an octal integer. Required.
199
+
200
+ ##
201
+ # :attr_reader: uid
202
+ # The Unix owner user ID of the file. Stored as an octal integer.
203
+
204
+ ##
205
+ # :attr_reader: uname
206
+ # The user name of the Unix owner of the file.
207
+
208
+ ##
209
+ # :attr_reader: gid
210
+ # The Unix owner group ID of the file. Stored as an octal integer.
211
+
212
+ ##
213
+ # :attr_reader: gname
214
+ # The group name of the Unix owner of the file.
215
+
216
+ ##
217
+ # :attr_reader: mtime
218
+ # The modification time of the file in epoch seconds. Stored as an
219
+ # octal integer.
220
+
221
+ ##
222
+ # :attr_reader: checksum
223
+ # The checksum of the file. Stored as an octal integer. Calculated
224
+ # before encoding the header as a string.
225
+
226
+ ##
227
+ # :attr_reader: typeflag
228
+ # The type of record in the file.
229
+ #
230
+ # +0+:: Regular file. NULL should be treated as a synonym, for compatibility
231
+ # purposes.
232
+ # +1+:: Hard link.
233
+ # +2+:: Symbolic link.
234
+ # +3+:: Character device node.
235
+ # +4+:: Block device node.
236
+ # +5+:: Directory.
237
+ # +6+:: FIFO node.
238
+ # +7+:: Reserved.
239
+
240
+ ##
241
+ # :attr_reader: linkname
242
+ # The name of the link stored. Not currently used.
243
+
244
+ ##
245
+ # :attr_reader: magic
246
+ # Always "ustar\0".
247
+
248
+ ##
249
+ # :attr_reader: version
250
+ # Always "00"
251
+
252
+ ##
253
+ # :attr_reader: devmajor
254
+ # The major device ID. Not currently used.
255
+
256
+ ##
257
+ # :attr_reader: devminor
258
+ # The minor device ID. Not currently used.
259
+ end