minitar 0.5.4 → 0.6

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.
@@ -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