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,237 @@
1
+ # coding: utf-8
2
+
3
+ module Archive::Tar::Minitar
4
+ # The class that reads a tar format archive from a data stream. The data
5
+ # stream may be sequential or random access, but certain features only work
6
+ # with random access data streams.
7
+ class Reader
8
+ # This marks the EntryStream closed for reading without closing the
9
+ # actual data stream.
10
+ module InvalidEntryStream
11
+ # rubocop:disable Style/SingleLineMethods
12
+ # rubocop:disable Style/EmptyLineBetweenDefs
13
+ def read(*); raise ClosedStream; end
14
+ def getc; raise ClosedStream; end
15
+ def rewind; raise ClosedStream; end
16
+ # rubocop:enable Style/EmptyLineBetweenDefs
17
+ # rubocop:enable Style/SingleLineMethods Style/EmptyLineBetweenDefs
18
+ end
19
+
20
+ # EntryStreams are pseudo-streams on top of the main data stream.
21
+ class EntryStream
22
+ Archive::Tar::Minitar::PosixHeader::FIELDS.each do |field|
23
+ attr_reader field.to_sym
24
+ end
25
+
26
+ def initialize(header, io)
27
+ @io = io
28
+ @name = header.name
29
+ @mode = header.mode
30
+ @uid = header.uid
31
+ @gid = header.gid
32
+ @size = header.size
33
+ @mtime = header.mtime
34
+ @checksum = header.checksum
35
+ @typeflag = header.typeflag
36
+ @linkname = header.linkname
37
+ @magic = header.magic
38
+ @version = header.version
39
+ @uname = header.uname
40
+ @gname = header.gname
41
+ @devmajor = header.devmajor
42
+ @devminor = header.devminor
43
+ @prefix = header.prefix
44
+ @read = 0
45
+ @orig_pos =
46
+ if Archive::Tar::Minitar.seekable?(@io)
47
+ @io.pos
48
+ else
49
+ 0
50
+ end
51
+ end
52
+
53
+ # Reads +len+ bytes (or all remaining data) from the entry. Returns
54
+ # +nil+ if there is no more data to read.
55
+ def read(len = nil)
56
+ return nil if @read >= @size
57
+ len ||= @size - @read
58
+ max_read = [len, @size - @read].min
59
+ ret = @io.read(max_read)
60
+ @read += ret.size
61
+ ret
62
+ end
63
+
64
+ # Reads one byte from the entry. Returns +nil+ if there is no more data
65
+ # to read.
66
+ def getc
67
+ return nil if @read >= @size
68
+ ret = @io.getc
69
+ @read += 1 if ret
70
+ ret
71
+ end
72
+
73
+ # Returns +true+ if the entry represents a directory.
74
+ def directory?
75
+ @typeflag == '5'
76
+ end
77
+ alias directory directory?
78
+
79
+ # Returns +true+ if the entry represents a plain file.
80
+ def file?
81
+ @typeflag == '0' || @typeflag == "\0"
82
+ end
83
+ alias file file?
84
+
85
+ # Returns +true+ if the current read pointer is at the end of the
86
+ # EntryStream data.
87
+ def eof?
88
+ @read >= @size
89
+ end
90
+
91
+ # Returns the current read pointer in the EntryStream.
92
+ def pos
93
+ @read
94
+ end
95
+
96
+ # Sets the current read pointer to the beginning of the EntryStream.
97
+ def rewind
98
+ unless Archive::Tar::Minitar.seekable?(@io, :pos=)
99
+ raise Archive::Tar::Minitar::NonSeekableStream
100
+ end
101
+ @io.pos = @orig_pos
102
+ @read = 0
103
+ end
104
+
105
+ def bytes_read
106
+ @read
107
+ end
108
+
109
+ # Returns the full and proper name of the entry.
110
+ def full_name
111
+ if @prefix != ''
112
+ File.join(@prefix, @name)
113
+ else
114
+ @name
115
+ end
116
+ end
117
+
118
+ # Closes the entry.
119
+ def close
120
+ invalidate
121
+ end
122
+
123
+ private
124
+
125
+ def invalidate
126
+ extend InvalidEntryStream
127
+ end
128
+ end
129
+
130
+ # With no associated block, +Reader::open+ is a synonym for
131
+ # +Reader::new+. If the optional code block is given, it will be passed
132
+ # the new _writer_ as an argument and the Reader object will
133
+ # automatically be closed when the block terminates. In this instance,
134
+ # +Reader::open+ returns the value of the block.
135
+ def self.open(io)
136
+ reader = new(io)
137
+ return reader unless block_given?
138
+ yield reader
139
+ ensure
140
+ reader.close
141
+ end
142
+
143
+ # Iterates over each entry in the provided input. This wraps the common
144
+ # pattern of:
145
+ #
146
+ # Archive::Tar::Minitar::Input.open(io) do |i|
147
+ # inp.each do |entry|
148
+ # # ...
149
+ # end
150
+ # end
151
+ #
152
+ # If a block is not provided, an enumerator will be created with the same
153
+ # behaviour.
154
+ #
155
+ # call-seq:
156
+ # Archive::Tar::Minitar::Reader.each_entry(io) -> enumerator
157
+ # Archive::Tar::Minitar::Reader.each_entry(io) { |entry| block } -> obj
158
+ def self.each_entry(io)
159
+ return to_enum(__method__, io) unless block_given?
160
+
161
+ open(io) do |reader|
162
+ reader.each_entry do |entry|
163
+ yield entry
164
+ end
165
+ end
166
+ end
167
+
168
+ # Creates and returns a new Reader object.
169
+ def initialize(io)
170
+ @io = io
171
+ @init_pos = io.pos
172
+ end
173
+
174
+ # Resets the read pointer to the beginning of data stream. Do not call
175
+ # this during a #each or #each_entry iteration. This only works with
176
+ # random access data streams that respond to #rewind and #pos.
177
+ def rewind
178
+ if @init_pos.zero?
179
+ unless Archive::Tar::Minitar.seekable?(@io, :rewind)
180
+ raise Archive::Tar::Minitar::NonSeekableStream
181
+ end
182
+ @io.rewind
183
+ else
184
+ unless Archive::Tar::Minitar.seekable?(@io, :pos=)
185
+ raise Archive::Tar::Minitar::NonSeekableStream
186
+ end
187
+ @io.pos = @init_pos
188
+ end
189
+ end
190
+
191
+ # Iterates through each entry in the data stream.
192
+ def each_entry
193
+ return to_enum unless block_given?
194
+
195
+ loop do
196
+ return if @io.eof?
197
+
198
+ header = Archive::Tar::Minitar::PosixHeader.from_stream(@io)
199
+ return if header.empty?
200
+
201
+ if header.long_name?
202
+ name = @io.read(512).rstrip
203
+ header = PosixHeader.from_stream(@io)
204
+ return if header.empty?
205
+ header.name = name
206
+ end
207
+
208
+ entry = EntryStream.new(header, @io)
209
+ size = entry.size
210
+
211
+ yield entry
212
+
213
+ skip = (512 - (size % 512)) % 512
214
+
215
+ if Archive::Tar::Minitar.seekable?(@io, :seek)
216
+ # avoid reading...
217
+ @io.seek(size - entry.bytes_read, IO::SEEK_CUR)
218
+ else
219
+ pending = size - entry.bytes_read
220
+ while pending > 0
221
+ bread = @io.read([pending, 4096].min).size
222
+ raise UnexpectedEOF if @io.eof?
223
+ pending -= bread
224
+ end
225
+ end
226
+
227
+ @io.read(skip) # discard trailing zeros
228
+ # make sure nobody can use #read, #getc or #rewind anymore
229
+ entry.close
230
+ end
231
+ end
232
+ alias each each_entry
233
+
234
+ def close
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,297 @@
1
+ # coding: utf-8
2
+
3
+ module Archive::Tar::Minitar
4
+ # The class that writes a tar format archive to a data stream.
5
+ class Writer
6
+ # The exception raised when the user attempts to write more data to a
7
+ # BoundedWriteStream than has been allocated.
8
+ WriteBoundaryOverflow = Class.new(StandardError)
9
+
10
+ # A stream wrapper that can only be written to. Any attempt to read
11
+ # from this restricted stream will result in a NameError being thrown.
12
+ class WriteOnlyStream
13
+ def initialize(io)
14
+ @io = io
15
+ end
16
+
17
+ def write(data)
18
+ @io.write(data)
19
+ end
20
+ end
21
+
22
+ private_constant :WriteOnlyStream if respond_to?(:private_constant)
23
+
24
+ # A WriteOnlyStream that also has a size limit.
25
+ class BoundedWriteStream < WriteOnlyStream
26
+ def self.const_missing(c)
27
+ case c
28
+ when :FileOverflow
29
+ warn 'Writer::BoundedWriteStream::FileOverflow has been renamed ' \
30
+ 'to Writer::WriteBoundaryOverflow'
31
+ const_set :FileOverflow,
32
+ Archive::Tar::Minitar::Writer::WriteBoundaryOverflow
33
+ else
34
+ super
35
+ end
36
+ end
37
+
38
+ # The maximum number of bytes that may be written to this data
39
+ # stream.
40
+ attr_reader :limit
41
+ # The current total number of bytes written to this data stream.
42
+ attr_reader :written
43
+
44
+ def initialize(io, limit)
45
+ @io = io
46
+ @limit = limit
47
+ @written = 0
48
+ end
49
+
50
+ def write(data)
51
+ raise WriteBoundaryOverflow if (data.size + @written) > @limit
52
+ @io.write(data)
53
+ @written += data.size
54
+ data.size
55
+ end
56
+ end
57
+
58
+ private_constant :BoundedWriteStream if respond_to?(:private_constant)
59
+
60
+ def self.const_missing(c)
61
+ case c
62
+ when :BoundedStream
63
+ warn 'BoundedStream has been renamed to BoundedWriteStream'
64
+ const_set(:BoundedStream, BoundedWriteStream)
65
+ else
66
+ super
67
+ end
68
+ end
69
+
70
+ # With no associated block, +Writer::open+ is a synonym for +Writer::new+.
71
+ # If the optional code block is given, it will be passed the new _writer_
72
+ # as an argument and the Writer object will automatically be closed when
73
+ # the block terminates. In this instance, +Writer::open+ returns the value
74
+ # of the block.
75
+ #
76
+ # call-seq:
77
+ # w = Archive::Tar::Minitar::Writer.open(STDOUT)
78
+ # w.add_file_simple('foo.txt', :size => 3)
79
+ # w.close
80
+ #
81
+ # Archive::Tar::Minitar::Writer.open(STDOUT) do |w|
82
+ # w.add_file_simple('foo.txt', :size => 3)
83
+ # end
84
+ def self.open(io) # :yields Writer:
85
+ writer = new(io)
86
+ return writer unless block_given?
87
+ yield writer
88
+ ensure
89
+ writer.close
90
+ end
91
+
92
+ # Creates and returns a new Writer object.
93
+ def initialize(io)
94
+ @io = io
95
+ @closed = false
96
+ end
97
+
98
+ # Adds a file to the archive as +name+. The data can be provided in the
99
+ # <tt>opts[:data]</tt> or provided to a BoundedWriteStream that is
100
+ # yielded to the provided block.
101
+ #
102
+ # If <tt>opts[:data]</tt> is provided, all other values to +opts+ are
103
+ # optional. If the data is provided to the yielded BoundedWriteStream,
104
+ # <tt>opts[:size]</tt> must be provided.
105
+ #
106
+ # Valid parameters to +opts+ are:
107
+ #
108
+ # <tt>:data</tt>:: Optional. The data to write to the archive.
109
+ # <tt>:mode</tt>:: The Unix file permissions mode value. If not
110
+ # provided, defaults to 0644.
111
+ # <tt>:size</tt>:: The size, in bytes. If <tt>:data</tt> is provided,
112
+ # this parameter may be ignored (if it is less than
113
+ # the size of the data provided) or used to add
114
+ # padding (if it is greater than the size of the data
115
+ # provided).
116
+ # <tt>:uid</tt>:: The Unix file owner user ID number.
117
+ # <tt>:gid</tt>:: The Unix file owner group ID number.
118
+ # <tt>:mtime</tt>:: File modification time, interpreted as an integer.
119
+ #
120
+ # An exception will be raised if the Writer is already closed, or if
121
+ # more data is written to the BoundedWriteStream than expected.
122
+ #
123
+ # call-seq:
124
+ # writer.add_file_simple('foo.txt', :data => "bar")
125
+ # writer.add_file_simple('foo.txt', :size => 3) do |w|
126
+ # w.write("bar")
127
+ # end
128
+ def add_file_simple(name, opts = {}) # :yields BoundedWriteStream:
129
+ raise ClosedStream if @closed
130
+ name, prefix = split_name(name)
131
+
132
+ header = {
133
+ :prefix => prefix,
134
+ :name => name,
135
+ :mode => opts.fetch(:mode, 0o644),
136
+ :mtime => opts.fetch(:mtime, nil),
137
+ :gid => opts.fetch(:gid, nil),
138
+ :uid => opts.fetch(:uid, nil)
139
+ }
140
+
141
+ data = opts.fetch(:data, nil)
142
+ size = opts.fetch(:size, nil)
143
+
144
+ if block_given?
145
+ if data
146
+ raise ArgumentError,
147
+ 'Too much data (opts[:data] and block_given?).'
148
+ end
149
+
150
+ raise ArgumentError, 'No size provided' unless size
151
+ else
152
+ raise ArgumentError, 'No data provided' unless data
153
+
154
+ size = data.size if size < data.size
155
+ end
156
+
157
+ header[:size] = size
158
+
159
+ @io.write(PosixHeader.new(header))
160
+
161
+ os = BoundedWriteStream.new(@io, opts[:size])
162
+ if block_given?
163
+ yield os
164
+ else
165
+ os.write(data)
166
+ end
167
+
168
+ min_padding = opts[:size] - os.written
169
+ @io.write("\0" * min_padding)
170
+ remainder = (512 - (opts[:size] % 512)) % 512
171
+ @io.write("\0" * remainder)
172
+ end
173
+
174
+ # Adds a file to the archive as +name+. The data can be provided in the
175
+ # <tt>opts[:data]</tt> or provided to a yielded +WriteOnlyStream+. The
176
+ # size of the file will be determined from the amount of data written
177
+ # to the stream.
178
+ #
179
+ # Valid parameters to +opts+ are:
180
+ #
181
+ # <tt>:mode</tt>:: The Unix file permissions mode value. If not
182
+ # provided, defaults to 0644.
183
+ # <tt>:uid</tt>:: The Unix file owner user ID number.
184
+ # <tt>:gid</tt>:: The Unix file owner group ID number.
185
+ # <tt>:mtime</tt>:: File modification time, interpreted as an integer.
186
+ # <tt>:data</tt>:: Optional. The data to write to the archive.
187
+ #
188
+ # If <tt>opts[:data]</tt> is provided, this acts the same as
189
+ # #add_file_simple. Otherwise, the file's size will be determined from
190
+ # the amount of data written to the stream.
191
+ #
192
+ # For #add_file to be used without <tt>opts[:data]</tt>, the Writer
193
+ # must be wrapping a stream object that is seekable. Otherwise,
194
+ # #add_file_simple must be used.
195
+ #
196
+ # +opts+ may be modified during the writing of the file to the stream.
197
+ def add_file(name, opts = {}, &block) # :yields WriteOnlyStream, +opts+:
198
+ raise ClosedStream if @closed
199
+
200
+ return add_file_simple(name, opts, &block) if opts[:data]
201
+
202
+ unless Archive::Tar::Minitar.seekable?(@io)
203
+ raise Archive::Tar::Minitar::NonSeekableStream
204
+ end
205
+
206
+ name, prefix = split_name(name)
207
+
208
+ init_pos = @io.pos
209
+ @io.write("\0" * 512) # placeholder for the header
210
+
211
+ yield WriteOnlyStream.new(@io), opts
212
+
213
+ size = @io.pos - (init_pos + 512)
214
+ remainder = (512 - (size % 512)) % 512
215
+ @io.write("\0" * remainder)
216
+
217
+ final_pos, @io.pos = @io.pos, init_pos
218
+
219
+ header = {
220
+ :name => name,
221
+ :mode => opts[:mode],
222
+ :mtime => opts[:mtime],
223
+ :size => size,
224
+ :gid => opts[:gid],
225
+ :uid => opts[:uid],
226
+ :prefix => prefix
227
+ }
228
+ @io.write(PosixHeader.new(header))
229
+ @io.pos = final_pos
230
+ end
231
+
232
+ # Creates a directory entry in the tar.
233
+ def mkdir(name, opts = {})
234
+ raise ClosedStream if @closed
235
+
236
+ name, prefix = split_name(name)
237
+ header = {
238
+ :name => name,
239
+ :mode => opts[:mode],
240
+ :typeflag => '5',
241
+ :size => 0,
242
+ :gid => opts[:gid],
243
+ :uid => opts[:uid],
244
+ :mtime => opts[:mtime],
245
+ :prefix => prefix
246
+ }
247
+ @io.write(PosixHeader.new(header))
248
+ nil
249
+ end
250
+
251
+ # Passes the #flush method to the wrapped stream, used for buffered
252
+ # streams.
253
+ def flush
254
+ raise ClosedStream if @closed
255
+ @io.flush if @io.respond_to?(:flush)
256
+ end
257
+
258
+ # Closes the Writer. This does not close the underlying wrapped output
259
+ # stream.
260
+ def close
261
+ return if @closed
262
+ @io.write("\0" * 1024)
263
+ @closed = true
264
+ end
265
+
266
+ private
267
+
268
+ def split_name(name)
269
+ # TODO: Enable long-filename write support.
270
+
271
+ raise FileNameTooLong if name.size > 256
272
+
273
+ if name.size <= 100
274
+ prefix = ''
275
+ else
276
+ parts = name.split(/\//)
277
+ newname = parts.pop
278
+
279
+ nxt = ''
280
+
281
+ loop do
282
+ nxt = parts.pop || ''
283
+ break if newname.size + 1 + nxt.size >= 100
284
+ newname = "#{nxt}/#{newname}"
285
+ end
286
+
287
+ prefix = (parts + [nxt]).join('/')
288
+
289
+ name = newname
290
+
291
+ raise FileNameTooLong if name.size > 100 || prefix.size > 155
292
+ end
293
+
294
+ [ name, prefix ]
295
+ end
296
+ end
297
+ end