minitar 0.5.3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of minitar might be problematic. Click here for more details.

@@ -0,0 +1,14 @@
1
+ Revision history for Ruby library Archive::Tar::Minitar. Unless explicitly
2
+ noted otherwise, all changes are produced by Austin Ziegler
3
+ <austin@rubyforge.org>.
4
+
5
+ == 0.5.2
6
+ * Fixed a Ruby 1.9 compatibility error.
7
+
8
+ == 0.5.1
9
+ * Fixed a variable name error.
10
+
11
+ == Archive::Tar::Minitar 0.5.0
12
+ * Initial release. Does files and directories. Command does create, extract,
13
+ * and list.
14
+
data/Install ADDED
@@ -0,0 +1,6 @@
1
+ Installing this package is as simple as:
2
+
3
+ % ruby install.rb
4
+
5
+ Alternatively, you can use the RubyGem version of Archive::Tar::Minitar
6
+ available as archive-tar-minitar-0.5.2.gem from the usual sources.
data/README ADDED
@@ -0,0 +1,66 @@
1
+ Archive::Tar::Minitar README
2
+ ============================
3
+ Archive::Tar::Minitar is a pure-Ruby library and command-line utility that
4
+ provides the ability to deal with POSIX tar(1) archive files. The
5
+ implementation is based heavily on Mauricio Ferna'ndez's implementation in
6
+ rpa-base, but has been reorganised to promote reuse in other projects.
7
+
8
+ This release is version 0.5.2, offering a Ruby 1.9 compatibility bugfix over
9
+ version 0.5.1. The library can only handle files and directories at this
10
+ point. A future version will be expanded to handle symbolic links and hard
11
+ links in a portable manner. The command line utility, minitar, can only create
12
+ archives, extract from archives, and list archive contents.
13
+
14
+ Using this library is easy. The simplest case is:
15
+
16
+ require 'zlib'
17
+ require 'archive/tar/minitar'
18
+ include Archive::Tar
19
+
20
+ # Packs everything that matches Find.find('tests')
21
+ File.open('test.tar', 'wb') { |tar| Minitar.pack('tests', tar) }
22
+ # Unpacks 'test.tar' to 'x', creating 'x' if necessary.
23
+ Minitar.unpack('test.tar', 'x')
24
+
25
+ A gzipped tar can be written with:
26
+
27
+ tgz = Zlib::GzipWriter.new(File.open('test.tgz', 'wb'))
28
+ # Warning: tgz will be closed!
29
+ Minitar.pack('tests', tgz)
30
+
31
+ tgz = Zlib::GzipReader.new(File.open('test.tgz', 'rb'))
32
+ # Warning: tgz will be closed!
33
+ Minitar.unpack(tgz, 'x')
34
+
35
+ As the case above shows, one need not write to a file. However, it will
36
+ sometimes require that one dive a little deeper into the API, as in the case
37
+ of StringIO objects. Note that I'm not providing a block with Minitar::Output,
38
+ as Minitar::Output#close automatically closes both the Output object and the
39
+ wrapped data stream object.
40
+
41
+ begin
42
+ sgz = Zlib::GzipWriter.new(StringIO.new(""))
43
+ tar = Output.new(sgz)
44
+ Find.find('tests') do |entry|
45
+ Minitar.pack_file(entry, tar)
46
+ end
47
+ ensure
48
+ # Closes both tar and sgz.
49
+ tar.close
50
+ end
51
+
52
+ Copyright
53
+ =========
54
+ # Copyright 2004 Mauricio Julio Ferna'ndez Pradier and Austin Ziegler
55
+ #
56
+ # This program is based on and incorporates parts of RPA::Package from
57
+ # rpa-base (lib/rpa/package.rb and lib/rpa/util.rb) by Mauricio and has been
58
+ # adapted to be more generic by Austin.
59
+ #
60
+ # 'minitar' contains an adaptation of Ruby/ProgressBar by Satoru
61
+ # Takabayashi <satoru@namazu.org>, copyright 2001 - 2004.
62
+ #
63
+ # This program is free software. It may be redistributed and/or modified
64
+ # under the terms of the GPL version 2 (or later) or Ruby's licence.
65
+ #
66
+ # $Id$
@@ -0,0 +1,113 @@
1
+ #! /usr/bin/env rake
2
+ $LOAD_PATH.unshift('lib')
3
+
4
+ require 'rubygems'
5
+ require 'rake/gempackagetask'
6
+ require 'rake/contrib/rubyforgepublisher'
7
+ require 'archive/tar/minitar'
8
+ require 'zlib'
9
+
10
+ DISTDIR = "archive-tar-minitar-#{Archive::Tar::Minitar::VERSION}"
11
+ TARDIST = "../#{DISTDIR}.tar.gz"
12
+
13
+ DATE_RE = %r<(\d{4})[./-]?(\d{2})[./-]?(\d{2})(?:[\sT]?(\d{2})[:.]?(\d{2})[:.]?(\d{2})?)?>
14
+
15
+ if ENV['RELEASE_DATE']
16
+ year, month, day, hour, minute, second = DATE_RE.match(ENV['RELEASE_DATE']).captures
17
+ year ||= 0
18
+ month ||= 0
19
+ day ||= 0
20
+ hour ||= 0
21
+ minute ||= 0
22
+ second ||= 0
23
+ ReleaseDate = Time.mktime(year, month, day, hour, minute, second)
24
+ else
25
+ ReleaseDate = nil
26
+ end
27
+
28
+ task :test do |t|
29
+ require 'test/unit/testsuite'
30
+ require 'test/unit/ui/console/testrunner'
31
+
32
+ runner = Test::Unit::UI::Console::TestRunner
33
+
34
+ $LOAD_PATH.unshift('tests')
35
+ Dir['tests/tc_*.rb'].each do |testcase|
36
+ load testcase
37
+ end
38
+
39
+ suite = Test::Unit::TestSuite.new
40
+
41
+ ObjectSpace.each_object(Class) do |testcase|
42
+ suite << testcase.suite if testcase < Test::Unit::TestCase
43
+ end
44
+
45
+ runner.run(suite)
46
+ end
47
+
48
+ spec = eval(File.read("archive-tar-minitar.gemspec"))
49
+ desc "Build the RubyGem for Archive::Tar::Minitar."
50
+ task :gem => [ :test ]
51
+ Rake::GemPackageTask.new(spec) do |g|
52
+ g.need_tar = false
53
+ g.need_zip = false
54
+ g.package_dir = ".."
55
+ end
56
+
57
+ desc "Build an Archive::Tar::Minitar .tar.gz distribution."
58
+ task :tar => [ TARDIST ]
59
+ file TARDIST do |t|
60
+ current = File.basename(Dir.pwd)
61
+ Dir.chdir("..") do
62
+ begin
63
+ files = Dir["#{current}/**/*"].select { |dd| dd !~ %r{(?:/CVS/?|~$)} }
64
+ files.map! do |dd|
65
+ ddnew = dd.gsub(/^#{current}/, DISTDIR)
66
+ mtime = ReleaseDate || File.stat(dd).mtime
67
+ if File.directory?(dd)
68
+ { :name => ddnew, :mode => 0755, :dir => true, :mtime => mtime }
69
+ else
70
+ if dd =~ %r{bin/}
71
+ mode = 0755
72
+ else
73
+ mode = 0644
74
+ end
75
+ data = File.read(dd)
76
+ { :name => ddnew, :mode => mode, :data => data, :size => data.size,
77
+ :mtime => mtime }
78
+ end
79
+ end
80
+
81
+ ff = File.open(t.name.gsub(%r{^\.\./}o, ''), "wb")
82
+ gz = Zlib::GzipWriter.new(ff)
83
+ tw = Archive::Tar::Minitar::Writer.new(gz)
84
+
85
+ files.each do |entry|
86
+ if entry[:dir]
87
+ tw.mkdir(entry[:name], entry)
88
+ else
89
+ tw.add_file_simple(entry[:name], entry) { |os| os.write(entry[:data]) }
90
+ end
91
+ end
92
+ ensure
93
+ tw.close if tw
94
+ gz.close if gz
95
+ end
96
+ end
97
+ end
98
+ task TARDIST => [ :test ]
99
+
100
+ def sign(file)
101
+ sh %("C:\\Program Files\\Windows Privacy Tools\\GnuPG\\Gpg.exe" -ba #{file})
102
+ end
103
+
104
+ task :signtar => [ :tar ] do
105
+ sign TARDIST
106
+ end
107
+ task :signgem => [ :gem ] do
108
+ sign "../#{DISTDIR}.gem"
109
+ end
110
+
111
+ desc "Build everything."
112
+ task :default => [ :signtar, :signgem ] do
113
+ end
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ #--
3
+ # Archive::Tar::Minitar 0.5.2
4
+ # Copyright 2004 Mauricio Julio Ferna'ndez Pradier and Austin Ziegler
5
+ #
6
+ # This program is based on and incorporates parts of RPA::Package from
7
+ # rpa-base (lib/rpa/package.rb and lib/rpa/util.rb) by Mauricio and has been
8
+ # adapted to be more generic by Austin.
9
+ #
10
+ # It is licensed under the GNU General Public Licence or Ruby's licence.
11
+ #
12
+ # $Id$
13
+ #++
14
+
15
+ # 1) Try to load Archive::Tar::Minitar from the gem.
16
+ # 2) Try to load Archive::Tar::Minitar from $LOAD_PATH.
17
+ begin
18
+ require 'rubygems'
19
+ require_gem 'archive-tar-minitar', '= 0.5.2'
20
+ rescue LoadError
21
+ nil
22
+ end
23
+
24
+ require 'archive/tar/minitar'
25
+ require 'archive/tar/minitar/command'
26
+
27
+ exit Archive::Tar::Minitar::Command.run(ARGV)
@@ -0,0 +1,985 @@
1
+ #!/usr/bin/env ruby
2
+ #--
3
+ # Archive::Tar::Minitar 0.5.2
4
+ # Copyright 2004 Mauricio Julio Ferna'ndez Pradier and Austin Ziegler
5
+ #
6
+ # This program is based on and incorporates parts of RPA::Package from
7
+ # rpa-base (lib/rpa/package.rb and lib/rpa/util.rb) by Mauricio and has been
8
+ # adapted to be more generic by Austin.
9
+ #
10
+ # It is licensed under the GNU General Public Licence or Ruby's licence.
11
+ #
12
+ # $Id$
13
+ #++
14
+
15
+ module Archive; end
16
+ module Archive::Tar; end
17
+
18
+ # = Archive::Tar::PosixHeader
19
+ # Implements the POSIX tar header as a Ruby class. The structure of
20
+ # the POSIX tar header is:
21
+ #
22
+ # struct tarfile_entry_posix
23
+ # { // pack/unpack
24
+ # char name[100]; // ASCII (+ Z unless filled) a100/Z100
25
+ # char mode[8]; // 0 padded, octal, null a8 /A8
26
+ # char uid[8]; // ditto a8 /A8
27
+ # char gid[8]; // ditto a8 /A8
28
+ # char size[12]; // 0 padded, octal, null a12 /A12
29
+ # char mtime[12]; // 0 padded, octal, null a12 /A12
30
+ # char checksum[8]; // 0 padded, octal, null, space a8 /A8
31
+ # char typeflag[1]; // see below a /a
32
+ # char linkname[100]; // ASCII + (Z unless filled) a100/Z100
33
+ # char magic[6]; // "ustar\0" a6 /A6
34
+ # char version[2]; // "00" a2 /A2
35
+ # char uname[32]; // ASCIIZ a32 /Z32
36
+ # char gname[32]; // ASCIIZ a32 /Z32
37
+ # char devmajor[8]; // 0 padded, octal, null a8 /A8
38
+ # char devminor[8]; // 0 padded, octal, null a8 /A8
39
+ # char prefix[155]; // ASCII (+ Z unless filled) a155/Z155
40
+ # };
41
+ #
42
+ # The +typeflag+ may be one of the following known values:
43
+ #
44
+ # <tt>"0"</tt>:: Regular file. NULL should be treated as a synonym, for
45
+ # compatibility purposes.
46
+ # <tt>"1"</tt>:: Hard link.
47
+ # <tt>"2"</tt>:: Symbolic link.
48
+ # <tt>"3"</tt>:: Character device node.
49
+ # <tt>"4"</tt>:: Block device node.
50
+ # <tt>"5"</tt>:: Directory.
51
+ # <tt>"6"</tt>:: FIFO node.
52
+ # <tt>"7"</tt>:: Reserved.
53
+ #
54
+ # POSIX indicates that "A POSIX-compliant implementation must treat any
55
+ # unrecognized typeflag value as a regular file."
56
+ class Archive::Tar::PosixHeader
57
+ FIELDS = %w(name mode uid gid size mtime checksum typeflag linkname) +
58
+ %w(magic version uname gname devmajor devminor prefix)
59
+
60
+ FIELDS.each { |field| attr_reader field.intern }
61
+
62
+ HEADER_PACK_FORMAT = "a100a8a8a8a12a12a7aaa100a6a2a32a32a8a8a155"
63
+ HEADER_UNPACK_FORMAT = "Z100A8A8A8A12A12A8aZ100A6A2Z32Z32A8A8Z155"
64
+
65
+ # Creates a new PosixHeader from a data stream.
66
+ def self.new_from_stream(stream, long_name = nil)
67
+ data = stream.read(512)
68
+ fields = data.unpack(HEADER_UNPACK_FORMAT)
69
+ name = fields.shift
70
+ mode = fields.shift.oct
71
+ uid = fields.shift.oct
72
+ gid = fields.shift.oct
73
+ size = fields.shift.oct
74
+ mtime = fields.shift.oct
75
+ checksum = fields.shift.oct
76
+ typeflag = fields.shift
77
+ linkname = fields.shift
78
+ magic = fields.shift
79
+ version = fields.shift.oct
80
+ uname = fields.shift
81
+ gname = fields.shift
82
+ devmajor = fields.shift.oct
83
+ devminor = fields.shift.oct
84
+ prefix = fields.shift
85
+
86
+ empty = (data == "\0" * 512)
87
+
88
+ if typeflag == 'L' && name == '././@LongLink'
89
+ long_name = stream.read(512).rstrip
90
+ return new_from_stream(stream, long_name)
91
+ end
92
+
93
+ new(:name => long_name || name,
94
+ :mode => mode, :uid => uid, :gid => gid,
95
+ :size => size, :mtime => mtime, :checksum => checksum,
96
+ :typeflag => typeflag, :magic => magic, :version => version,
97
+ :uname => uname, :gname => gname, :devmajor => devmajor,
98
+ :devminor => devminor, :prefix => prefix, :empty => empty)
99
+ end
100
+
101
+ # Creates a new PosixHeader. A PosixHeader cannot be created unless the
102
+ # #name, #size, #prefix, and #mode are provided.
103
+ def initialize(vals)
104
+ unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode]
105
+ raise ArgumentError
106
+ end
107
+
108
+ vals[:mtime] ||= 0
109
+ vals[:checksum] ||= ""
110
+ vals[:typeflag] ||= "0"
111
+ vals[:magic] ||= "ustar"
112
+ vals[:version] ||= "00"
113
+
114
+ FIELDS.each do |field|
115
+ instance_variable_set("@#{field}", vals[field.intern])
116
+ end
117
+ @empty = vals[:empty]
118
+ end
119
+
120
+ def empty?
121
+ @empty
122
+ end
123
+
124
+ def to_s
125
+ update_checksum
126
+ header(@checksum)
127
+ end
128
+
129
+ # Update the checksum field.
130
+ def update_checksum
131
+ hh = header(" " * 8)
132
+ @checksum = oct(calculate_checksum(hh), 6)
133
+ end
134
+
135
+ private
136
+ def oct(num, len)
137
+ if num.nil?
138
+ "\0" * (len + 1)
139
+ else
140
+ "%0#{len}o" % num
141
+ end
142
+ end
143
+
144
+ def calculate_checksum(hdr)
145
+ hdr.unpack("C*").inject { |aa, bb| aa + bb }
146
+ end
147
+
148
+ def header(chksum)
149
+ arr = [name, oct(mode, 7), oct(uid, 7), oct(gid, 7), oct(size, 11),
150
+ oct(mtime, 11), chksum, " ", typeflag, linkname, magic, version,
151
+ uname, gname, oct(devmajor, 7), oct(devminor, 7), prefix]
152
+ str = arr.pack(HEADER_PACK_FORMAT)
153
+ str + "\0" * ((512 - str.size) % 512)
154
+ end
155
+ end
156
+
157
+ require 'fileutils'
158
+ require 'find'
159
+
160
+ # = Archive::Tar::Minitar 0.5.2
161
+ # Archive::Tar::Minitar is a pure-Ruby library and command-line
162
+ # utility that provides the ability to deal with POSIX tar(1) archive
163
+ # files. The implementation is based heavily on Mauricio Ferna'ndez's
164
+ # implementation in rpa-base, but has been reorganised to promote
165
+ # reuse in other projects.
166
+ #
167
+ # This tar class performs a subset of all tar (POSIX tape archive)
168
+ # operations. We can only deal with typeflags 0, 1, 2, and 5 (see
169
+ # Archive::Tar::PosixHeader). All other typeflags will be treated as
170
+ # normal files.
171
+ #
172
+ # NOTE::: support for typeflags 1 and 2 is not yet implemented in this
173
+ # version.
174
+ #
175
+ # This release is version 0.5.2. The library can only handle files and
176
+ # directories at this point. A future version will be expanded to
177
+ # handle symbolic links and hard links in a portable manner. The
178
+ # command line utility, minitar, can only create archives, extract
179
+ # from archives, and list archive contents.
180
+ #
181
+ # == Synopsis
182
+ # Using this library is easy. The simplest case is:
183
+ #
184
+ # require 'zlib'
185
+ # require 'archive/tar/minitar'
186
+ # include Archive::Tar
187
+ #
188
+ # # Packs everything that matches Find.find('tests')
189
+ # File.open('test.tar', 'wb') { |tar| Minitar.pack('tests', tar) }
190
+ # # Unpacks 'test.tar' to 'x', creating 'x' if necessary.
191
+ # Minitar.unpack('test.tar', 'x')
192
+ #
193
+ # A gzipped tar can be written with:
194
+ #
195
+ # tgz = Zlib::GzipWriter.new(File.open('test.tgz', 'wb'))
196
+ # # Warning: tgz will be closed!
197
+ # Minitar.pack('tests', tgz)
198
+ #
199
+ # tgz = Zlib::GzipReader.new(File.open('test.tgz', 'rb'))
200
+ # # Warning: tgz will be closed!
201
+ # Minitar.unpack(tgz, 'x')
202
+ #
203
+ # As the case above shows, one need not write to a file. However, it
204
+ # will sometimes require that one dive a little deeper into the API,
205
+ # as in the case of StringIO objects. Note that I'm not providing a
206
+ # block with Minitar::Output, as Minitar::Output#close automatically
207
+ # closes both the Output object and the wrapped data stream object.
208
+ #
209
+ # begin
210
+ # sgz = Zlib::GzipWriter.new(StringIO.new(""))
211
+ # tar = Output.new(sgz)
212
+ # Find.find('tests') do |entry|
213
+ # Minitar.pack_file(entry, tar)
214
+ # end
215
+ # ensure
216
+ # # Closes both tar and sgz.
217
+ # tar.close
218
+ # end
219
+ #
220
+ # == Copyright
221
+ # Copyright 2004 Mauricio Julio Ferna'ndez Pradier and Austin Ziegler
222
+ #
223
+ # This program is based on and incorporates parts of RPA::Package from
224
+ # rpa-base (lib/rpa/package.rb and lib/rpa/util.rb) by Mauricio and
225
+ # has been adapted to be more generic by Austin.
226
+ #
227
+ # 'minitar' contains an adaptation of Ruby/ProgressBar by Satoru
228
+ # Takabayashi <satoru@namazu.org>, copyright 2001 - 2004.
229
+ #
230
+ # This program is free software. It may be redistributed and/or
231
+ # modified under the terms of the GPL version 2 (or later) or Ruby's
232
+ # licence.
233
+ module Archive::Tar::Minitar
234
+ VERSION = "0.5.2"
235
+
236
+ # The exception raised when a wrapped data stream class is expected to
237
+ # respond to #rewind or #pos but does not.
238
+ class NonSeekableStream < StandardError; end
239
+ # The exception raised when a block is required for proper operation of
240
+ # the method.
241
+ class BlockRequired < ArgumentError; end
242
+ # The exception raised when operations are performed on a stream that has
243
+ # previously been closed.
244
+ class ClosedStream < StandardError; end
245
+ # The exception raised when a filename exceeds 256 bytes in length,
246
+ # the maximum supported by the standard Tar format.
247
+ class FileNameTooLong < StandardError; end
248
+ # The exception raised when a data stream ends before the amount of data
249
+ # expected in the archive's PosixHeader.
250
+ class UnexpectedEOF < StandardError; end
251
+
252
+ # The class that writes a tar format archive to a data stream.
253
+ class Writer
254
+ # A stream wrapper that can only be written to. Any attempt to read
255
+ # from this restricted stream will result in a NameError being thrown.
256
+ class RestrictedStream
257
+ def initialize(anIO)
258
+ @io = anIO
259
+ end
260
+
261
+ def write(data)
262
+ @io.write(data)
263
+ end
264
+ end
265
+
266
+ # A RestrictedStream that also has a size limit.
267
+ class BoundedStream < Archive::Tar::Minitar::Writer::RestrictedStream
268
+ # The exception raised when the user attempts to write more data to
269
+ # a BoundedStream than has been allocated.
270
+ class FileOverflow < RuntimeError; end
271
+
272
+ # The maximum number of bytes that may be written to this data
273
+ # stream.
274
+ attr_reader :limit
275
+ # The current total number of bytes written to this data stream.
276
+ attr_reader :written
277
+
278
+ def initialize(io, limit)
279
+ @io = io
280
+ @limit = limit
281
+ @written = 0
282
+ end
283
+
284
+ def write(data)
285
+ raise FileOverflow if (data.size + @written) > @limit
286
+ @io.write(data)
287
+ @written += data.size
288
+ data.size
289
+ end
290
+ end
291
+
292
+ # With no associated block, +Writer::open+ is a synonym for
293
+ # +Writer::new+. If the optional code block is given, it will be
294
+ # passed the new _writer_ as an argument and the Writer object will
295
+ # automatically be closed when the block terminates. In this instance,
296
+ # +Writer::open+ returns the value of the block.
297
+ def self.open(anIO)
298
+ writer = Writer.new(anIO)
299
+
300
+ return writer unless block_given?
301
+
302
+ begin
303
+ res = yield writer
304
+ ensure
305
+ writer.close
306
+ end
307
+
308
+ res
309
+ end
310
+
311
+ # Creates and returns a new Writer object.
312
+ def initialize(anIO)
313
+ @io = anIO
314
+ @closed = false
315
+ end
316
+
317
+ # Adds a file to the archive as +name+. +opts+ must contain the
318
+ # following values:
319
+ #
320
+ # <tt>:mode</tt>:: The Unix file permissions mode value.
321
+ # <tt>:size</tt>:: The size, in bytes.
322
+ #
323
+ # +opts+ may contain the following values:
324
+ #
325
+ # <tt>:uid</tt>: The Unix file owner user ID number.
326
+ # <tt>:gid</tt>: The Unix file owner group ID number.
327
+ # <tt>:mtime</tt>:: The *integer* modification time value.
328
+ #
329
+ # It will not be possible to add more than <tt>opts[:size]</tt> bytes
330
+ # to the file.
331
+ def add_file_simple(name, opts = {}) # :yields BoundedStream:
332
+ raise Archive::Tar::Minitar::BlockRequired unless block_given?
333
+ raise Archive::Tar::ClosedStream if @closed
334
+
335
+ name, prefix = split_name(name)
336
+
337
+ header = { :name => name, :mode => opts[:mode], :mtime => opts[:mtime],
338
+ :size => opts[:size], :gid => opts[:gid], :uid => opts[:uid],
339
+ :prefix => prefix }
340
+ header = Archive::Tar::PosixHeader.new(header).to_s
341
+ @io.write(header)
342
+
343
+ os = BoundedStream.new(@io, opts[:size])
344
+ yield os
345
+ # FIXME: what if an exception is raised in the block?
346
+
347
+ min_padding = opts[:size] - os.written
348
+ @io.write("\0" * min_padding)
349
+ remainder = (512 - (opts[:size] % 512)) % 512
350
+ @io.write("\0" * remainder)
351
+ end
352
+
353
+ # Adds a file to the archive as +name+. +opts+ must contain the
354
+ # following value:
355
+ #
356
+ # <tt>:mode</tt>:: The Unix file permissions mode value.
357
+ #
358
+ # +opts+ may contain the following values:
359
+ #
360
+ # <tt>:uid</tt>: The Unix file owner user ID number.
361
+ # <tt>:gid</tt>: The Unix file owner group ID number.
362
+ # <tt>:mtime</tt>:: The *integer* modification time value.
363
+ #
364
+ # The file's size will be determined from the amount of data written
365
+ # to the stream.
366
+ #
367
+ # For #add_file to be used, the Archive::Tar::Minitar::Writer must be
368
+ # wrapping a stream object that is seekable (e.g., it responds to
369
+ # #pos=). Otherwise, #add_file_simple must be used.
370
+ #
371
+ # +opts+ may be modified during the writing to the stream.
372
+ def add_file(name, opts = {}) # :yields RestrictedStream, +opts+:
373
+ raise Archive::Tar::Minitar::BlockRequired unless block_given?
374
+ raise Archive::Tar::Minitar::ClosedStream if @closed
375
+ raise Archive::Tar::Minitar::NonSeekableStream unless @io.respond_to?(:pos=)
376
+
377
+ name, prefix = split_name(name)
378
+ init_pos = @io.pos
379
+ @io.write("\0" * 512) # placeholder for the header
380
+
381
+ yield RestrictedStream.new(@io), opts
382
+ # FIXME: what if an exception is raised in the block?
383
+
384
+ size = @io.pos - (init_pos + 512)
385
+ remainder = (512 - (size % 512)) % 512
386
+ @io.write("\0" * remainder)
387
+
388
+ final_pos = @io.pos
389
+ @io.pos = init_pos
390
+
391
+ header = { :name => name, :mode => opts[:mode], :mtime => opts[:mtime],
392
+ :size => size, :gid => opts[:gid], :uid => opts[:uid],
393
+ :prefix => prefix }
394
+ header = Archive::Tar::PosixHeader.new(header).to_s
395
+ @io.write(header)
396
+ @io.pos = final_pos
397
+ end
398
+
399
+ # Creates a directory in the tar.
400
+ def mkdir(name, opts = {})
401
+ raise ClosedStream if @closed
402
+ name, prefix = split_name(name)
403
+ header = { :name => name, :mode => opts[:mode], :typeflag => "5",
404
+ :size => 0, :gid => opts[:gid], :uid => opts[:uid],
405
+ :mtime => opts[:mtime], :prefix => prefix }
406
+ header = Archive::Tar::PosixHeader.new(header).to_s
407
+ @io.write(header)
408
+ nil
409
+ end
410
+
411
+ # Passes the #flush method to the wrapped stream, used for buffered
412
+ # streams.
413
+ def flush
414
+ raise ClosedStream if @closed
415
+ @io.flush if @io.respond_to?(:flush)
416
+ end
417
+
418
+ # Closes the Writer.
419
+ def close
420
+ return if @closed
421
+ @io.write("\0" * 1024)
422
+ @closed = true
423
+ end
424
+
425
+ private
426
+ def split_name(name)
427
+ raise FileNameTooLong if name.size > 256
428
+ if name.size <= 100
429
+ prefix = ""
430
+ else
431
+ parts = name.split(/\//)
432
+ newname = parts.pop
433
+
434
+ nxt = ""
435
+
436
+ loop do
437
+ nxt = parts.pop
438
+ break if newname.size + 1 + nxt.size > 100
439
+ newname = "#{nxt}/#{newname}"
440
+ end
441
+
442
+ prefix = (parts + [nxt]).join("/")
443
+
444
+ name = newname
445
+
446
+ raise FileNameTooLong if name.size > 100 || prefix.size > 155
447
+ end
448
+ return name, prefix
449
+ end
450
+ end
451
+
452
+ # The class that reads a tar format archive from a data stream. The data
453
+ # stream may be sequential or random access, but certain features only work
454
+ # with random access data streams.
455
+ class Reader
456
+ # This marks the EntryStream closed for reading without closing the
457
+ # actual data stream.
458
+ module InvalidEntryStream
459
+ def read(len = nil); raise ClosedStream; end
460
+ def getc; raise ClosedStream; end
461
+ def rewind; raise ClosedStream; end
462
+ end
463
+
464
+ # EntryStreams are pseudo-streams on top of the main data stream.
465
+ class EntryStream
466
+ Archive::Tar::PosixHeader::FIELDS.each do |field|
467
+ attr_reader field.intern
468
+ end
469
+
470
+ def initialize(header, anIO)
471
+ @io = anIO
472
+ @name = header.name
473
+ @mode = header.mode
474
+ @uid = header.uid
475
+ @gid = header.gid
476
+ @size = header.size
477
+ @mtime = header.mtime
478
+ @checksum = header.checksum
479
+ @typeflag = header.typeflag
480
+ @linkname = header.linkname
481
+ @magic = header.magic
482
+ @version = header.version
483
+ @uname = header.uname
484
+ @gname = header.gname
485
+ @devmajor = header.devmajor
486
+ @devminor = header.devminor
487
+ @prefix = header.prefix
488
+ @read = 0
489
+ @orig_pos = @io.pos
490
+ end
491
+
492
+ # Reads +len+ bytes (or all remaining data) from the entry. Returns
493
+ # +nil+ if there is no more data to read.
494
+ def read(len = nil)
495
+ return nil if @read >= @size
496
+ len ||= @size - @read
497
+ max_read = [len, @size - @read].min
498
+ ret = @io.read(max_read)
499
+ @read += ret.size
500
+ ret
501
+ end
502
+
503
+ # Reads one byte from the entry. Returns +nil+ if there is no more data
504
+ # to read.
505
+ def getc
506
+ return nil if @read >= @size
507
+ ret = @io.getc
508
+ @read += 1 if ret
509
+ ret
510
+ end
511
+
512
+ # Returns +true+ if the entry represents a directory.
513
+ def directory?
514
+ @typeflag == "5"
515
+ end
516
+ alias_method :directory, :directory?
517
+
518
+ # Returns +true+ if the entry represents a plain file.
519
+ def file?
520
+ @typeflag == "0"
521
+ end
522
+ alias_method :file, :file?
523
+
524
+ # Returns +true+ if the current read pointer is at the end of the
525
+ # EntryStream data.
526
+ def eof?
527
+ @read >= @size
528
+ end
529
+
530
+ # Returns the current read pointer in the EntryStream.
531
+ def pos
532
+ @read
533
+ end
534
+
535
+ # Sets the current read pointer to the beginning of the EntryStream.
536
+ def rewind
537
+ raise NonSeekableStream unless @io.respond_to?(:pos=)
538
+ @io.pos = @orig_pos
539
+ @read = 0
540
+ end
541
+
542
+ def bytes_read
543
+ @read
544
+ end
545
+
546
+ # Returns the full and proper name of the entry.
547
+ def full_name
548
+ if @prefix != ""
549
+ File.join(@prefix, @name)
550
+ else
551
+ @name
552
+ end
553
+ end
554
+
555
+ # Closes the entry.
556
+ def close
557
+ invalidate
558
+ end
559
+
560
+ private
561
+ def invalidate
562
+ extend InvalidEntryStream
563
+ end
564
+ end
565
+
566
+ # With no associated block, +Reader::open+ is a synonym for
567
+ # +Reader::new+. If the optional code block is given, it will be passed
568
+ # the new _writer_ as an argument and the Reader object will
569
+ # automatically be closed when the block terminates. In this instance,
570
+ # +Reader::open+ returns the value of the block.
571
+ def self.open(anIO)
572
+ reader = Reader.new(anIO)
573
+
574
+ return reader unless block_given?
575
+
576
+ begin
577
+ res = yield reader
578
+ ensure
579
+ reader.close
580
+ end
581
+
582
+ res
583
+ end
584
+
585
+ # Creates and returns a new Reader object.
586
+ def initialize(anIO)
587
+ @io = anIO
588
+ @init_pos = anIO.pos
589
+ end
590
+
591
+ # Iterates through each entry in the data stream.
592
+ def each(&block)
593
+ each_entry(&block)
594
+ end
595
+
596
+ # Resets the read pointer to the beginning of data stream. Do not call
597
+ # this during a #each or #each_entry iteration. This only works with
598
+ # random access data streams that respond to #rewind and #pos.
599
+ def rewind
600
+ if @init_pos == 0
601
+ raise NonSeekableStream unless @io.respond_to?(:rewind)
602
+ @io.rewind
603
+ else
604
+ raise NonSeekableStream unless @io.respond_to?(:pos=)
605
+ @io.pos = @init_pos
606
+ end
607
+ end
608
+
609
+ # Iterates through each entry in the data stream.
610
+ def each_entry
611
+ loop do
612
+ return if @io.eof?
613
+
614
+ header = Archive::Tar::PosixHeader.new_from_stream(@io)
615
+ return if header.empty?
616
+
617
+ entry = EntryStream.new(header, @io)
618
+ size = entry.size
619
+
620
+ yield entry
621
+
622
+ skip = (512 - (size % 512)) % 512
623
+
624
+ if @io.respond_to?(:seek)
625
+ # avoid reading...
626
+ @io.seek(size - entry.bytes_read, IO::SEEK_CUR)
627
+ else
628
+ pending = size - entry.bytes_read
629
+ while pending > 0
630
+ bread = @io.read([pending, 4096].min).size
631
+ raise UnexpectedEOF if @io.eof?
632
+ pending -= bread
633
+ end
634
+ end
635
+ @io.read(skip) # discard trailing zeros
636
+ # make sure nobody can use #read, #getc or #rewind anymore
637
+ entry.close
638
+ end
639
+ end
640
+
641
+ def close
642
+ end
643
+ end
644
+
645
+ # Wraps a Archive::Tar::Minitar::Reader with convenience methods and
646
+ # wrapped stream management; Input only works with random access data
647
+ # streams. See Input::new for details.
648
+ class Input
649
+ include Enumerable
650
+
651
+ # With no associated block, +Input::open+ is a synonym for
652
+ # +Input::new+. If the optional code block is given, it will be passed
653
+ # the new _writer_ as an argument and the Input object will
654
+ # automatically be closed when the block terminates. In this instance,
655
+ # +Input::open+ returns the value of the block.
656
+ def self.open(input)
657
+ stream = Input.new(input)
658
+ return stream unless block_given?
659
+
660
+ begin
661
+ res = yield stream
662
+ ensure
663
+ stream.close
664
+ end
665
+
666
+ res
667
+ end
668
+
669
+ # Creates a new Input object. If +input+ is a stream object that responds
670
+ # to #read), then it will simply be wrapped. Otherwise, one will be
671
+ # created and opened using Kernel#open. When Input#close is called, the
672
+ # stream object wrapped will be closed.
673
+ def initialize(input)
674
+ if input.respond_to?(:read)
675
+ @io = input
676
+ else
677
+ @io = open(input, "rb")
678
+ end
679
+ @tarreader = Archive::Tar::Minitar::Reader.new(@io)
680
+ end
681
+
682
+ # Iterates through each entry and rewinds to the beginning of the stream
683
+ # when finished.
684
+ def each(&block)
685
+ @tarreader.each { |entry| yield entry }
686
+ ensure
687
+ @tarreader.rewind
688
+ end
689
+
690
+ # Extracts the current +entry+ to +destdir+. If a block is provided, it
691
+ # yields an +action+ Symbol, the full name of the file being extracted
692
+ # (+name+), and a Hash of statistical information (+stats+).
693
+ #
694
+ # The +action+ will be one of:
695
+ # <tt>:dir</tt>:: The +entry+ is a directory.
696
+ # <tt>:file_start</tt>:: The +entry+ is a file; the extract of the
697
+ # file is just beginning.
698
+ # <tt>:file_progress</tt>:: Yielded every 4096 bytes during the extract
699
+ # of the +entry+.
700
+ # <tt>:file_done</tt>:: Yielded when the +entry+ is completed.
701
+ #
702
+ # The +stats+ hash contains the following keys:
703
+ # <tt>:current</tt>:: The current total number of bytes read in the
704
+ # +entry+.
705
+ # <tt>:currinc</tt>:: The current number of bytes read in this read
706
+ # cycle.
707
+ # <tt>:entry</tt>:: The entry being extracted; this is a
708
+ # Reader::EntryStream, with all methods thereof.
709
+ def extract_entry(destdir, entry) # :yields action, name, stats:
710
+ stats = {
711
+ :current => 0,
712
+ :currinc => 0,
713
+ :entry => entry
714
+ }
715
+
716
+ if entry.directory?
717
+ dest = File.join(destdir, entry.full_name)
718
+
719
+ yield :dir, entry.full_name, stats if block_given?
720
+
721
+ if Archive::Tar::Minitar.dir?(dest)
722
+ begin
723
+ FileUtils.chmod(entry.mode, dest)
724
+ rescue Exception
725
+ nil
726
+ end
727
+ else
728
+ FileUtils.mkdir_p(dest, :mode => entry.mode)
729
+ FileUtils.chmod(entry.mode, dest)
730
+ end
731
+
732
+ fsync_dir(dest)
733
+ fsync_dir(File.join(dest, ".."))
734
+ return
735
+ else # it's a file
736
+ destdir = File.join(destdir, File.dirname(entry.full_name))
737
+ FileUtils.mkdir_p(destdir, :mode => 0755)
738
+
739
+ destfile = File.join(destdir, File.basename(entry.full_name))
740
+ FileUtils.chmod(0600, destfile) rescue nil # Errno::ENOENT
741
+
742
+ yield :file_start, entry.full_name, stats if block_given?
743
+
744
+ File.open(destfile, "wb", entry.mode) do |os|
745
+ loop do
746
+ data = entry.read(4096)
747
+ break unless data
748
+
749
+ stats[:currinc] = os.write(data)
750
+ stats[:current] += stats[:currinc]
751
+
752
+ yield :file_progress, entry.full_name, stats if block_given?
753
+ end
754
+ os.fsync
755
+ end
756
+
757
+ FileUtils.chmod(entry.mode, destfile)
758
+ fsync_dir(File.dirname(destfile))
759
+ fsync_dir(File.join(File.dirname(destfile), ".."))
760
+
761
+ yield :file_done, entry.full_name, stats if block_given?
762
+ end
763
+ end
764
+
765
+ # Returns the Reader object for direct access.
766
+ def tar
767
+ @tarreader
768
+ end
769
+
770
+ # Closes the Reader object and the wrapped data stream.
771
+ def close
772
+ @io.close
773
+ @tarreader.close
774
+ end
775
+
776
+ private
777
+ def fsync_dir(dirname)
778
+ # make sure this hits the disc
779
+ dir = open(dirname, 'rb')
780
+ dir.fsync
781
+ rescue # ignore IOError if it's an unpatched (old) Ruby
782
+ nil
783
+ ensure
784
+ dir.close if dir rescue nil
785
+ end
786
+ end
787
+
788
+ # Wraps a Archive::Tar::Minitar::Writer with convenience methods and
789
+ # wrapped stream management; Output only works with random access data
790
+ # streams. See Output::new for details.
791
+ class Output
792
+ # With no associated block, +Output::open+ is a synonym for
793
+ # +Output::new+. If the optional code block is given, it will be passed
794
+ # the new _writer_ as an argument and the Output object will
795
+ # automatically be closed when the block terminates. In this instance,
796
+ # +Output::open+ returns the value of the block.
797
+ def self.open(output)
798
+ stream = Output.new(output)
799
+ return stream unless block_given?
800
+
801
+ begin
802
+ res = yield stream
803
+ ensure
804
+ stream.close
805
+ end
806
+
807
+ res
808
+ end
809
+
810
+ # Creates a new Output object. If +output+ is a stream object that
811
+ # responds to #read), then it will simply be wrapped. Otherwise, one will
812
+ # be created and opened using Kernel#open. When Output#close is called,
813
+ # the stream object wrapped will be closed.
814
+ def initialize(output)
815
+ if output.respond_to?(:write)
816
+ @io = output
817
+ else
818
+ @io = ::File.open(output, "wb")
819
+ end
820
+ @tarwriter = Archive::Tar::Minitar::Writer.new(@io)
821
+ end
822
+
823
+ # Returns the Writer object for direct access.
824
+ def tar
825
+ @tarwriter
826
+ end
827
+
828
+ # Closes the Writer object and the wrapped data stream.
829
+ def close
830
+ @tarwriter.close
831
+ @io.close
832
+ end
833
+ end
834
+
835
+ class << self
836
+ # Tests if +path+ refers to a directory. Fixes an apparently
837
+ # corrupted <tt>stat()</tt> call on Windows.
838
+ def dir?(path)
839
+ File.directory?((path[-1] == ?/) ? path : "#{path}/")
840
+ end
841
+
842
+ # A convenience method for wrapping Archive::Tar::Minitar::Input.open
843
+ # (mode +r+) and Archive::Tar::Minitar::Output.open (mode +w+). No other
844
+ # modes are currently supported.
845
+ def open(dest, mode = "r", &block)
846
+ case mode
847
+ when "r"
848
+ Input.open(dest, &block)
849
+ when "w"
850
+ Output.open(dest, &block)
851
+ else
852
+ raise "Unknown open mode for Archive::Tar::Minitar.open."
853
+ end
854
+ end
855
+
856
+ # A convenience method to packs the file provided. +entry+ may either be
857
+ # a filename (in which case various values for the file (see below) will
858
+ # be obtained from <tt>File#stat(entry)</tt> or a Hash with the fields:
859
+ #
860
+ # <tt>:name</tt>:: The filename to be packed into the tarchive.
861
+ # *REQUIRED*.
862
+ # <tt>:mode</tt>:: The mode to be applied.
863
+ # <tt>:uid</tt>:: The user owner of the file. (Ignored on Windows.)
864
+ # <tt>:gid</tt>:: The group owner of the file. (Ignored on Windows.)
865
+ # <tt>:mtime</tt>:: The modification Time of the file.
866
+ #
867
+ # During packing, if a block is provided, #pack_file yields an +action+
868
+ # Symol, the full name of the file being packed, and a Hash of
869
+ # statistical information, just as with
870
+ # Archive::Tar::Minitar::Input#extract_entry.
871
+ #
872
+ # The +action+ will be one of:
873
+ # <tt>:dir</tt>:: The +entry+ is a directory.
874
+ # <tt>:file_start</tt>:: The +entry+ is a file; the extract of the
875
+ # file is just beginning.
876
+ # <tt>:file_progress</tt>:: Yielded every 4096 bytes during the extract
877
+ # of the +entry+.
878
+ # <tt>:file_done</tt>:: Yielded when the +entry+ is completed.
879
+ #
880
+ # The +stats+ hash contains the following keys:
881
+ # <tt>:current</tt>:: The current total number of bytes read in the
882
+ # +entry+.
883
+ # <tt>:currinc</tt>:: The current number of bytes read in this read
884
+ # cycle.
885
+ # <tt>:name</tt>:: The filename to be packed into the tarchive.
886
+ # *REQUIRED*.
887
+ # <tt>:mode</tt>:: The mode to be applied.
888
+ # <tt>:uid</tt>:: The user owner of the file. (+nil+ on Windows.)
889
+ # <tt>:gid</tt>:: The group owner of the file. (+nil+ on Windows.)
890
+ # <tt>:mtime</tt>:: The modification Time of the file.
891
+ def pack_file(entry, outputter) #:yields action, name, stats:
892
+ outputter = outputter.tar if outputter.kind_of?(Archive::Tar::Minitar::Output)
893
+
894
+ stats = {}
895
+
896
+ if entry.kind_of?(Hash)
897
+ name = entry[:name]
898
+
899
+ entry.each { |kk, vv| stats[kk] = vv unless vv.nil? }
900
+ else
901
+ name = entry
902
+ end
903
+
904
+ name = name.sub(%r{\./}, '')
905
+ stat = File.stat(name)
906
+ stats[:mode] ||= stat.mode
907
+ stats[:mtime] ||= stat.mtime
908
+ stats[:size] = stat.size
909
+
910
+ if RUBY_PLATFORM =~ /win32/
911
+ stats[:uid] = nil
912
+ stats[:gid] = nil
913
+ else
914
+ stats[:uid] ||= stat.uid
915
+ stats[:gid] ||= stat.gid
916
+ end
917
+
918
+ case
919
+ when File.file?(name)
920
+ outputter.add_file_simple(name, stats) do |os|
921
+ stats[:current] = 0
922
+ yield :file_start, name, stats if block_given?
923
+ File.open(name, "rb") do |ff|
924
+ until ff.eof?
925
+ stats[:currinc] = os.write(ff.read(4096))
926
+ stats[:current] += stats[:currinc]
927
+ yield :file_progress, name, stats if block_given?
928
+ end
929
+ end
930
+ yield :file_done, name, stats if block_given?
931
+ end
932
+ when dir?(name)
933
+ yield :dir, name, stats if block_given?
934
+ outputter.mkdir(name, stats)
935
+ else
936
+ raise "Don't yet know how to pack this type of file."
937
+ end
938
+ end
939
+
940
+ # A convenience method to pack files specified by +src+ into +dest+. If
941
+ # +src+ is an Array, then each file detailed therein will be packed into
942
+ # the resulting Archive::Tar::Minitar::Output stream; if +recurse_dirs+
943
+ # is true, then directories will be recursed.
944
+ #
945
+ # If +src+ is an Array, it will be treated as the argument to Find.find;
946
+ # all files matching will be packed.
947
+ def pack(src, dest, recurse_dirs = true, &block)
948
+ Output.open(dest) do |outp|
949
+ if src.kind_of?(Array)
950
+ src.each do |entry|
951
+ pack_file(entry, outp, &block)
952
+ if dir?(entry) and recurse_dirs
953
+ Dir["#{entry}/**/**"].each do |ee|
954
+ pack_file(ee, outp, &block)
955
+ end
956
+ end
957
+ end
958
+ else
959
+ Find.find(src) do |entry|
960
+ pack_file(entry, outp, &block)
961
+ end
962
+ end
963
+ end
964
+ end
965
+
966
+ # A convenience method to unpack files from +src+ into the directory
967
+ # specified by +dest+. Only those files named explicitly in +files+
968
+ # will be extracted.
969
+ def unpack(src, dest, files = [], &block)
970
+ Input.open(src) do |inp|
971
+ if File.exist?(dest) and (not dir?(dest))
972
+ raise "Can't unpack to a non-directory."
973
+ elsif not File.exist?(dest)
974
+ FileUtils.mkdir_p(dest)
975
+ end
976
+
977
+ inp.each do |entry|
978
+ if files.empty? or files.include?(entry.full_name)
979
+ inp.extract_entry(dest, entry, &block)
980
+ end
981
+ end
982
+ end
983
+ end
984
+ end
985
+ end