archive-zip 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,22 @@
1
+ require 'stringio'
2
+
3
+ class StringIO
4
+ unless StringIO.method_defined?(:readbytes)
5
+ # Copied from IO#readbytes.
6
+ def readbytes(n)
7
+ str = read(n)
8
+ if str == nil
9
+ raise EOFError, "end of file reached"
10
+ end
11
+ if str.size < n
12
+ raise TruncatedDataError.new("data truncated", str)
13
+ end
14
+ str
15
+ end
16
+ end
17
+
18
+ # Always returns +true+. Added for compatibility with IO#seekable?.
19
+ def seekable?
20
+ true
21
+ end
22
+ end
@@ -0,0 +1,85 @@
1
+ class Time
2
+ # Returns a DOSTime representing this time object as a DOS date-time
3
+ # structure. Times are bracketed by the limits of the ability of the DOS
4
+ # date-time structure to represent them. Accuracy is 2 seconds and years
5
+ # range from 1980 to 2099. The returned structure represents as closely as
6
+ # possible the time of this object.
7
+ #
8
+ # See DOSTime#new for a description of this structure.
9
+ def to_dos_time
10
+ dos_sec = sec/2
11
+ dos_year = year - 1980
12
+ dos_year = 0 if dos_year < 0
13
+ dos_year = 119 if dos_year > 119
14
+
15
+ DOSTime.new(
16
+ (dos_sec ) |
17
+ (min << 5) |
18
+ (hour << 11) |
19
+ (day << 16) |
20
+ (month << 21) |
21
+ (dos_year << 25)
22
+ )
23
+ end
24
+ end
25
+
26
+ # A representation of the DOS time structure which can be converted into
27
+ # instances of Time.
28
+ class DOSTime
29
+ include Comparable
30
+
31
+ # Creates a new instance of DOSTime. _dos_time_ is a 4 byte String or
32
+ # unsigned number (Integer) representing an MS-DOS time structure where:
33
+ # Bits 0-4:: 2 second increments (0-29)
34
+ # Bits 5-10:: minutes (0-59)
35
+ # Bits 11-15:: hours (0-24)
36
+ # Bits 16-20:: day (1-31)
37
+ # Bits 21-24:: month (1-12)
38
+ # Bits 25-31:: four digit year minus 1980 (0-119)
39
+ #
40
+ # If _dos_time_ is ommitted or +nil+, a new instance is created based on the
41
+ # current time.
42
+ def initialize(dos_time = nil)
43
+ case dos_time
44
+ when nil
45
+ @dos_time = Time.now.to_dos_time.dos_time
46
+ when Integer
47
+ @dos_time = dos_time
48
+ else
49
+ unless dos_time.length == 4 then
50
+ raise ArgumentError, 'length of DOS time structure is not 4'
51
+ end
52
+ @dos_time = dos_time.unpack('V')[0]
53
+ end
54
+ end
55
+
56
+ # Returns -1 if _other_ is a time earlier than this one, 0 if _other_ is the
57
+ # same time, and 1 if _other_ is a later time.
58
+ def cmp(other)
59
+ @dos_time <=> other.dos_time
60
+ end
61
+ alias :<=> :cmp
62
+
63
+ # Returns the time value of this object as an integer representing the DOS
64
+ # time structure.
65
+ def to_i
66
+ @dos_time
67
+ end
68
+
69
+ # Returns a Time instance which is equivalent to the time represented by this
70
+ # object.
71
+ def to_time
72
+ second = ((0b11111 & @dos_time) ) * 2
73
+ minute = ((0b111111 << 5 & @dos_time) >> 5)
74
+ hour = ((0b11111 << 11 & @dos_time) >> 11)
75
+ day = ((0b11111 << 16 & @dos_time) >> 16)
76
+ month = ((0b1111 << 21 & @dos_time) >> 21)
77
+ year = ((0b1111111 << 25 & @dos_time) >> 25) + 1980
78
+ return Time.local(year, month, day, hour, minute, second)
79
+ end
80
+
81
+ protected
82
+
83
+ # Used by _cmp_ to read another time stored in another DOSTime instance.
84
+ attr_reader :dos_time # :nodoc:
85
+ end
@@ -0,0 +1,211 @@
1
+ require 'zlib'
2
+
3
+ require 'archive/support/io-like'
4
+
5
+ module Zlib # :nodoc:
6
+ if ! const_defined?(:MAX_WBITS) then
7
+ MAX_WBITS = Deflate::MAX_WBITS
8
+ end
9
+
10
+ # Zlib::ZWriter is a writable, IO-like object (includes IO::Like) which wraps
11
+ # other writable, IO-like objects in order to facilitate writing data to those
12
+ # objects using the deflate method of compression.
13
+ class ZWriter
14
+ include IO::Like
15
+
16
+ # Creates a new instance of this class with the given arguments using #new
17
+ # and then passes the instance to the given block. The #close method is
18
+ # guaranteed to be called after the block completes.
19
+ #
20
+ # Equivalent to #new if no block is given.
21
+ def self.open(io, level = Zlib::DEFAULT_COMPRESSION, window_bits = nil, mem_level = nil, strategy = nil)
22
+ zw = new(io, level, window_bits, mem_level, strategy)
23
+ return zw unless block_given?
24
+
25
+ begin
26
+ yield(zw)
27
+ ensure
28
+ zw.close unless zw.closed?
29
+ end
30
+ end
31
+
32
+ # Creates a new instance of this class. _io_ must respond to the _write_
33
+ # method as an instance of IO would. _level_, _window_bits_, _mem_level_,
34
+ # and _strategy_ are all passed directly to Zlib::Deflate.new(). See the
35
+ # documentation of that method for their meanings.
36
+ #
37
+ # NOTE: Due to limitations in Ruby's finalization capabilities, the #close
38
+ # method is _not_ automatically called when this object is garbage
39
+ # collected. Make sure to call #close when finished with this object.
40
+ def initialize(io, level = Zlib::DEFAULT_COMPRESSION, window_bits = nil, mem_level = nil, strategy = nil)
41
+ @delegate = io
42
+ @deflater = Zlib::Deflate.new(level, window_bits, mem_level, strategy)
43
+ @deflate_buffer = ''
44
+ @crc32 = 0
45
+ end
46
+
47
+ # The CRC32 checksum of the uncompressed data written using this object.
48
+ #
49
+ # NOTE: Anything still in the internal write buffer has not been processed,
50
+ # so calling #flush prior to examining this attribute may be necessary for
51
+ # an accurate computation.
52
+ attr_reader :crc32
53
+
54
+ protected
55
+
56
+ # The delegate object to which compressed data is written.
57
+ attr_reader :delegate
58
+
59
+ public
60
+
61
+ # Closes the writer by finishing the compressed data and flushing it to the
62
+ # delegate.
63
+ #
64
+ # Raises IOError if called more than once.
65
+ def close
66
+ super()
67
+ delegate.write(@deflater.finish)
68
+ nil
69
+ end
70
+
71
+ # Returns the number of bytes of compressed data produced so far.
72
+ #
73
+ # NOTE: Anything still in the internal write buffer has not been processed,
74
+ # so calling #flush prior to calling this method may be necessary for an
75
+ # accurate count.
76
+ def compressed_size
77
+ @deflater.total_out
78
+ end
79
+
80
+ # Returns the number of bytes sent to be compressed so far.
81
+ #
82
+ # NOTE: Anything still in the internal write buffer has not been processed,
83
+ # so calling #flush prior to calling this method may be necessary for an
84
+ # accurate count.
85
+ def uncompressed_size
86
+ @deflater.total_in
87
+ end
88
+
89
+ private
90
+
91
+ def unbuffered_write(string)
92
+ until @deflate_buffer.empty? do
93
+ @deflate_buffer.slice!(0, delegate.write(@deflate_buffer))
94
+ end
95
+ @deflate_buffer = @deflater.deflate(string)
96
+
97
+ begin
98
+ @deflate_buffer.slice!(0, delegate.write(@deflate_buffer))
99
+ rescue Errno::EINTR, Errno::EAGAIN
100
+ # Ignore this because everything is in the deflate buffer and will be
101
+ # attempted again the next time this method is called.
102
+ end
103
+ @crc32 = Zlib.crc32(string, @crc32)
104
+ string.length
105
+ end
106
+ end
107
+
108
+ # Zlib::ZReader is a readable, IO-like object (includes IO::Like) which wraps
109
+ # other readable, IO-like objects in order to facilitate reading data from
110
+ # those objects using the inflate method of decompression.
111
+ class ZReader
112
+ include IO::Like
113
+
114
+ # Creates a new instance of this class with the given arguments using #new
115
+ # and then passes the instance to the given block. The #close method is
116
+ # guaranteed to be called after the block completes.
117
+ #
118
+ # Equivalent to #new if no block is given.
119
+ def self.open(io, window_bits = nil)
120
+ zr = new(io, window_bits)
121
+ return zr unless block_given?
122
+
123
+ begin
124
+ yield(zr)
125
+ ensure
126
+ zr.close unless zr.closed?
127
+ end
128
+ end
129
+
130
+ # Creates a new instance of this class. _io_ must respond to the _read_
131
+ # method as an IO instance would. _window_bits_ is passed directly to
132
+ # Zlib::Inflate.new(). See the documentation of that method for its
133
+ # meaning. If _io_ also responds to _rewind_, then the _rewind_ method of
134
+ # this class can be used to reset the whole stream back to the beginning.
135
+ #
136
+ # NOTE: Due to limitations in Ruby's finalization capabilities, the #close
137
+ # method is _not_ automatically called when this object is garbage
138
+ # collected. Make sure to call #close when finished with this object.
139
+ def initialize(io, window_bits = nil)
140
+ @delegate = io
141
+ @window_bits = window_bits
142
+ @inflater = Zlib::Inflate.new(@window_bits)
143
+ @crc32 = 0
144
+ @decompress_buffer = ''
145
+ end
146
+
147
+ # The CRC32 checksum of the uncompressed data read using this object.
148
+ #
149
+ # NOTE: The contents of the internal read buffer are immediately processed
150
+ # any time the buffer is filled, so this count is only accurate if all data
151
+ # has been read out of this object.
152
+ attr_reader :crc32
153
+
154
+ protected
155
+
156
+ # The delegate object from which compressed data is read.
157
+ attr_reader :delegate
158
+
159
+ public
160
+
161
+ # Closes the reader.
162
+ #
163
+ # Raises IOError if called more than once.
164
+ def close
165
+ super()
166
+ @inflater.close
167
+ nil
168
+ end
169
+
170
+ # Returns the number of bytes sent to be decompressed so far.
171
+ def compressed_size
172
+ @inflater.total_in
173
+ end
174
+
175
+ # Returns the number of bytes of decompressed data produced so far.
176
+ def uncompressed_size
177
+ @inflater.total_out
178
+ end
179
+
180
+ private
181
+
182
+ def unbuffered_read(length)
183
+ raise EOFError, 'end of file reached' if @inflater.finished?
184
+
185
+ begin
186
+ while @decompress_buffer.length < length && ! @inflater.finished? do
187
+ @decompress_buffer << @inflater.inflate(delegate.read(1))
188
+ end
189
+ rescue Errno::EINTR, Errno::EAGAIN
190
+ raise if @decompress_buffer.empty?
191
+ end
192
+ buffer = @decompress_buffer.slice!(0, length)
193
+ @crc32 = Zlib.crc32(buffer, @crc32)
194
+ buffer
195
+ end
196
+
197
+ def unbuffered_seek(offset, whence = IO::SEEK_SET)
198
+ unless offset == 0 && whence == IO::SEEK_SET then
199
+ raise Errno::EINVAL, 'Invalid argument'
200
+ end
201
+ unless delegate.respond_to?(:rewind) then
202
+ raise Errno::ESPIPE, 'Illegal seek'
203
+ end
204
+ delegate.rewind
205
+ @inflater.close
206
+ @inflater = Zlib::Inflate.new(@window_bits)
207
+ @crc32 = 0
208
+ @decompress_buffer = ''
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,643 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'fileutils'
4
+ require 'set'
5
+ require 'tempfile'
6
+
7
+ require 'archive/support/io'
8
+ require 'archive/support/iowindow'
9
+ require 'archive/support/stringio'
10
+ require 'archive/support/time'
11
+ require 'archive/support/zlib'
12
+ require 'archive/zip/codec'
13
+ require 'archive/zip/entry'
14
+ require 'archive/zip/error'
15
+
16
+ module Archive # :nodoc:
17
+ # Archive::Zip represents a ZIP archive compatible with InfoZip tools and the
18
+ # archives they generate. It currently supports both stored and deflated ZIP
19
+ # entries, directory entries, file entries, and symlink entries. File and
20
+ # directory accessed and modified times, POSIX permissions, and ownerships can
21
+ # be archived and restored as well depending on platform support for such
22
+ # metadata.
23
+ #
24
+ # Zip64, digital signatures, and encryption are not supported. ZIP archives
25
+ # can only be read from seekable kinds of IO, such as files; reading archives
26
+ # from pipes or any other non-seekable kind of IO is not supported. However,
27
+ # writing to such IO objects <b><em>IS</em></b> supported.
28
+ class Zip
29
+ include Enumerable
30
+
31
+ # The lead-in marker for the end of central directory record.
32
+ EOCD_SIGNATURE = "PK\x5\x6" # 0x06054b50
33
+ # The lead-in marker for the digital signature record.
34
+ DS_SIGNATURE = "PK\x5\x5" # 0x05054b50
35
+ # The lead-in marker for the ZIP64 end of central directory record.
36
+ Z64EOCD_SIGNATURE = "PK\x6\x6" # 0x06064b50
37
+ # The lead-in marker for the ZIP64 end of central directory locator record.
38
+ Z64EOCDL_SIGNATURE = "PK\x6\x7" # 0x07064b50
39
+ # The lead-in marker for a central file record.
40
+ CFH_SIGNATURE = "PK\x1\x2" # 0x02014b50
41
+ # The lead-in marker for a local file record.
42
+ LFH_SIGNATURE = "PK\x3\x4" # 0x04034b50
43
+ # The lead-in marker for data descriptor record.
44
+ DD_SIGNATURE = "PK\x7\x8" # 0x08074b50
45
+
46
+
47
+ # A convenience method which opens a new or existing archive located in the
48
+ # path indicated by _archive_path_, adds and updates entries based on the
49
+ # paths given in _paths_, and then saves and closes the archive. See the
50
+ # instance method #archive for more information about _paths_ and _options_.
51
+ def self.archive(archive_path, paths, options = {})
52
+ open(archive_path) { |z| z.archive(paths, options) }
53
+ end
54
+
55
+ # A convenience method which opens an archive located in the path indicated
56
+ # by _archive_path_, extracts the entries to the path indicated by
57
+ # _destination_, and then closes the archive. See the instance method
58
+ # #extract for more information about _destination_ and _options_.
59
+ def self.extract(archive_path, destination, options = {})
60
+ open(archive_path) { |z| z.extract(destination, options) }
61
+ end
62
+
63
+ # Calls #new with the given arguments and yields the resulting Zip instance
64
+ # to the given block. Returns the result of the block and ensures that the
65
+ # Zip instance is closed.
66
+ #
67
+ # This is a synonym for #new if no block is given.
68
+ def self.open(archive_path, archive_out = nil)
69
+ zf = new(archive_path, archive_out)
70
+ return zf unless block_given?
71
+
72
+ begin
73
+ yield(zf)
74
+ ensure
75
+ zf.close unless zf.closed?
76
+ end
77
+ end
78
+
79
+ # Open and parse the file located at the path indicated by _archive_path_ if
80
+ # _archive_path_ is not +nil+ and the path exists. If _archive_out_ is
81
+ # unspecified or +nil+, any changes made will be saved in place, replacing
82
+ # the current archive with a new one having the same name. If _archive_out_
83
+ # is a String, it points to a file which will recieve the new archive's
84
+ # contents. Otherwise, _archive_out_ is assumed to be a writable, IO-like
85
+ # object operating in *binary* mode which will recieve the new archive's
86
+ # contents.
87
+ #
88
+ # At least one of _archive_path_ and _archive_out_ must be specified and
89
+ # non-nil; otherwise, an error will be raised.
90
+ def initialize(archive_path, archive_out = nil)
91
+ if (archive_path.nil? || archive_path.empty?) &&
92
+ (archive_out.nil? ||
93
+ archive_out.kind_of?(String) && archive_out.empty?) then
94
+ raise ArgumentError, 'No valid source or destination archive specified'
95
+ end
96
+ @archive_path = archive_path
97
+ @archive_out = archive_out
98
+ @entries = {}
99
+ @dirty = false
100
+ @comment = ''
101
+ @closed = false
102
+ if ! @archive_path.nil? && File.exist?(@archive_path) then
103
+ @archive_in = File.new(@archive_path, 'rb')
104
+ parse(@archive_in)
105
+ end
106
+ end
107
+
108
+ # A comment string for the ZIP archive.
109
+ attr_accessor :comment
110
+
111
+ # Close the archive. It is at this point that any changes made to the
112
+ # archive will be persisted to an output stream.
113
+ #
114
+ # Raises Archive::Zip::IOError if called more than once.
115
+ def close
116
+ raise IOError, 'closed archive' if closed?
117
+
118
+ if @dirty then
119
+ # There is something to write...
120
+ if @archive_out.nil? then
121
+ # Update the archive "in place".
122
+ tmp_archive_path = nil
123
+ Tempfile.open(*File.split(@archive_path).reverse) do |archive_out|
124
+ # Ensure the file is in binary mode for Windows.
125
+ archive_out.binmode
126
+ # Save off the path so that the temporary file can be renamed to the
127
+ # archive file later.
128
+ tmp_archive_path = archive_out.path
129
+ dump(archive_out)
130
+ end
131
+ File.chmod(0666 & ~File.umask, tmp_archive_path)
132
+ elsif @archive_out.kind_of?(String) then
133
+ # Open a new archive to receive the data.
134
+ File.open(@archive_out, 'wb') do |archive_out|
135
+ dump(archive_out)
136
+ end
137
+ else
138
+ # Assume the given object is an IO-like object and dump the archive
139
+ # contents to it.
140
+ dump(@archive_out)
141
+ end
142
+ @archive_in.close unless @archive_in.nil?
143
+ # The rename must happen after the original archive is closed when
144
+ # running on Windows since that platform does not allow a file which is
145
+ # in use to be replaced as is required when trying to update the archive
146
+ # "in place".
147
+ File.rename(tmp_archive_path, @archive_path) if @archive_out.nil?
148
+ end
149
+
150
+ closed = true
151
+ nil
152
+ end
153
+
154
+ # Returns +true+ if the ZIP archive is closed, false otherwise.
155
+ def closed?
156
+ @closed
157
+ end
158
+
159
+ # When the ZIP archive is open, this method iterates through each entry in
160
+ # turn yielding each one to the given block. Since Zip includes Enumerable,
161
+ # Zip instances are enumerables of Entry instances.
162
+ #
163
+ # Raises Archive::Zip::IOError if called after #close.
164
+ def each(&b)
165
+ raise IOError, 'closed archive' if @closed
166
+
167
+ @entries.each_value(&b)
168
+ end
169
+
170
+ # Add _entry_ into the ZIP archive replacing any existing entry with the
171
+ # same zip path.
172
+ #
173
+ # Raises Archive::Zip::IOError if called after #close.
174
+ def add_entry(entry)
175
+ raise IOError, 'closed archive' if @closed
176
+ unless entry.kind_of?(Entry) then
177
+ raise ArgumentError, 'Archive::Zip::Entry instance required'
178
+ end
179
+
180
+ @entries[entry.zip_path] = entry
181
+ @dirty = true
182
+ self
183
+ end
184
+ alias :<< :add_entry
185
+
186
+ # Look up an entry based on the zip path located in _zip_path_. Returns
187
+ # +nil+ if no entry is found.
188
+ def get_entry(zip_path)
189
+ @entries[zip_path]
190
+ end
191
+ alias :[] :get_entry
192
+
193
+ # Removes an entry from the ZIP file and returns the entry or +nil+ if no
194
+ # entry was found to remove. If _entry_ is an instance of
195
+ # Archive::Zip::Entry, the zip_path attribute is used to find the entry to
196
+ # remove; otherwise, _entry_ is assumed to be a zip path matching an entry
197
+ # in the ZIP archive.
198
+ #
199
+ # Raises Archive::Zip::IOError if called after #close.
200
+ def remove_entry(entry)
201
+ raise IOError, 'closed archive' if @closed
202
+
203
+ zip_path = entry
204
+ zip_path = entry.zip_path if entry.kind_of?(Entry)
205
+ entry = @entries.delete(zip_path)
206
+ entry = entry[1] unless entry.nil?
207
+ @dirty ||= ! entry.nil?
208
+ entry
209
+ end
210
+
211
+ # Adds _paths_ to the archive. _paths_ may be either a single path or an
212
+ # Array of paths. The files and directories referenced by _paths_ are added
213
+ # using their respective basenames as their zip paths. The exception to
214
+ # this is when the basename for a path is either <tt>"."</tt> or
215
+ # <tt>".."</tt>. In this case, the path is replaced with the paths to the
216
+ # contents of the directory it references.
217
+ #
218
+ # _options_ is a Hash optionally containing the following:
219
+ # <b>:path_prefix</b>::
220
+ # Specifies a prefix to be added to the zip_path attribute of each entry
221
+ # where `/' is the file separator character. This defaults to the empty
222
+ # string. All values are passed through Archive::Zip::Entry.expand_path
223
+ # before use.
224
+ # <b>:recursion</b>::
225
+ # When set to +true+ (the default), the contents of directories are
226
+ # recursively added to the archive.
227
+ # <b>:directories</b>::
228
+ # When set to +true+ (the default), entries are added to the archive for
229
+ # directories. Otherwise, the entries for directories will not be added;
230
+ # however, the contents of the directories will still be considered if the
231
+ # <b>:recursion</b> option is +true+.
232
+ # <b>:symlinks</b>::
233
+ # When set to +false+ (the default), entries for symlinks are excluded
234
+ # from the archive. Otherwise, they are included. <b>NOTE:</b> Unless
235
+ # <b>:follow_symlinks</b> is explicitly set, it will be set to the logical
236
+ # NOT of this option in calls to Archive::Zip::Entry.from_file. If
237
+ # symlinks should be completely ignored, set both this option and
238
+ # <b>:follow_symlinks</b> to +false+. See Archive::Zip::Entry.from_file
239
+ # for details regarding <b>:follow_symlinks</b>.
240
+ # <b>:flatten</b>::
241
+ # When set to +false+ (the default), the directory paths containing
242
+ # archived files will be included in the zip paths of entries representing
243
+ # the files. When set to +true+ files are archived without any containing
244
+ # directory structure in the zip paths. Setting to +true+ implies that
245
+ # <b>:directories</b> is +false+ and <b>:path_prefix</b> is empty.
246
+ # <b>:exclude</b>::
247
+ # Specifies a proc or lambda which takes a single argument containing a
248
+ # prospective zip entry and returns +true+ if the entry should be excluded
249
+ # from the archive and +false+ if it should be included. <b>NOTE:</b> If
250
+ # a directory is excluded in this way, the <b>:recursion</b> option has no
251
+ # effect for it.
252
+ # <b>:ignore_error</b>::
253
+ # When set to +false+ (the default), an error generated while creating an
254
+ # archive entry for a file will be raised. Otherwise, the bad file is
255
+ # skipped.
256
+ # Any other options which are supported by Archive::Zip::Entry.from_file are
257
+ # also supported.
258
+ #
259
+ # Raises Archive::Zip::IOError if called after #close. Raises
260
+ # Archive::Zip::EntryError if the <b>:ignore_error</b> option is +false+ and
261
+ # Archive::Zip::Entry.from_file raises an error.
262
+ #
263
+ # == Example
264
+ #
265
+ # A directory contains:
266
+ # zip-test
267
+ # +- dir1
268
+ # | +- file2.txt
269
+ # +- dir2
270
+ # +- file1.txt
271
+ #
272
+ # Create some archives:
273
+ # Archive::Zip.open('zip-test1.zip') do |z|
274
+ # z.archive('zip-test')
275
+ # end
276
+ #
277
+ # Archive::Zip.open('zip-test2.zip') do |z|
278
+ # z.archive('zip-test/.', :path_prefix => 'a/b/c/d')
279
+ # end
280
+ #
281
+ # Archive::Zip.open('zip-test3.zip') do |z|
282
+ # z.archive('zip-test', :directories => false)
283
+ # end
284
+ #
285
+ # Archive::Zip.open('zip-test4.zip') do |z|
286
+ # z.archive('zip-test', :exclude => lambda { |e| e.file? })
287
+ # end
288
+ #
289
+ # The archives contain:
290
+ # zip-test1.zip -> zip-test/
291
+ # zip-test/dir1/
292
+ # zip-test/dir1/file2.txt
293
+ # zip-test/dir2/
294
+ # zip-test/file1.txt
295
+ #
296
+ # zip-test2.zip -> a/b/c/d/dir1/
297
+ # a/b/c/d/dir1/file2.txt
298
+ # a/b/c/d/dir2/
299
+ # a/b/c/d/file1.txt
300
+ #
301
+ # zip-test3.zip -> zip-test/dir1/file2.txt
302
+ # zip-test/file1.txt
303
+ #
304
+ # zip-test4.zip -> zip-test/
305
+ # zip-test/dir1/
306
+ # zip-test/dir2/
307
+ def archive(paths, options = {})
308
+ raise IOError, 'closed archive' if @closed
309
+
310
+ # Ensure that paths is an enumerable.
311
+ paths = [paths] unless paths.kind_of?(Enumerable)
312
+ # If the basename of a path is '.' or '..', replace the path with the
313
+ # paths of all the entries contained within the directory referenced by
314
+ # the original path.
315
+ paths = paths.collect do |path|
316
+ basename = File.basename(path)
317
+ if basename == '.' || basename == '..' then
318
+ Dir.entries(path).reject do |e|
319
+ e == '.' || e == '..'
320
+ end.collect do |e|
321
+ File.join(path, e)
322
+ end
323
+ else
324
+ path
325
+ end
326
+ end.flatten.uniq
327
+
328
+ # Ensure that unspecified options have default values.
329
+ options[:path_prefix] = '' unless options.has_key?(:path_prefix)
330
+ options[:recursion] = true unless options.has_key?(:recursion)
331
+ options[:directories] = true unless options.has_key?(:directories)
332
+ options[:symlinks] = false unless options.has_key?(:symlinks)
333
+ options[:flatten] = false unless options.has_key?(:flatten)
334
+ options[:ignore_error] = false unless options.has_key?(:ignore_error)
335
+
336
+ # Flattening the directory structure implies that directories are skipped
337
+ # and that the path prefix should be ignored.
338
+ if options[:flatten] then
339
+ options[:path_prefix] = ''
340
+ options[:directories] = false
341
+ end
342
+
343
+ # Clean up the path prefix.
344
+ options[:path_prefix] = Entry.expand_path(options[:path_prefix].to_s)
345
+
346
+ paths.each do |path|
347
+ # Generate the zip path.
348
+ zip_entry_path = File.basename(path)
349
+ zip_entry_path += '/' if File.directory?(path)
350
+ unless options[:path_prefix].empty? then
351
+ zip_entry_path = "#{options[:path_prefix]}/#{zip_entry_path}"
352
+ end
353
+
354
+ begin
355
+ # Create the entry, but do not add it to the archive yet.
356
+ zip_entry = Zip::Entry.from_file(
357
+ path,
358
+ options.merge(
359
+ :zip_path => zip_entry_path,
360
+ :follow_symlinks => options.has_key?(:follow_symlinks) ?
361
+ options[:follow_symlinks] :
362
+ ! options[:symlinks]
363
+ )
364
+ )
365
+ rescue Zip::EntryError
366
+ # Ignore the error if requested.
367
+ if options[:ignore_error] then
368
+ next
369
+ else
370
+ raise
371
+ end
372
+ end
373
+
374
+ # Skip this entry if so directed.
375
+ if (zip_entry.symlink? && ! options[:symlinks]) ||
376
+ (! options[:exclude].nil? && options[:exclude].call(zip_entry)) then
377
+ next
378
+ end
379
+
380
+ # Add entries for directories (if requested) and files/symlinks.
381
+ if (! zip_entry.directory? || options[:directories]) then
382
+ add_entry(zip_entry)
383
+ end
384
+
385
+
386
+ # Recurse into subdirectories (if requested).
387
+ if zip_entry.directory? && options[:recursion] then
388
+ archive(
389
+ Dir.entries(path).reject do |e|
390
+ e == '.' || e == '..'
391
+ end.collect do |e|
392
+ File.join(path, e)
393
+ end,
394
+ options.merge(:path_prefix => zip_entry_path)
395
+ )
396
+ end
397
+ end
398
+
399
+ nil
400
+ end
401
+
402
+ # Extracts the contents of the archive to _destination_, where _destination_
403
+ # is a path to a directory which will contain the contents of the archive.
404
+ # The destination path will be created if it does not already exist.
405
+ #
406
+ # _options_ is a Hash optionally containing the following:
407
+ # <b>:directories</b>::
408
+ # When set to +true+ (the default), entries representing directories in
409
+ # the archive are extracted. This happens after all non-directory entries
410
+ # are extracted so that directory metadata can be properly updated.
411
+ # <b>:symlinks</b>::
412
+ # When set to +false+ (the default), entries representing symlinks in the
413
+ # archive are skipped. When set to +true+, such entries are extracted.
414
+ # Exceptions may be raised on plaforms/file systems which do not support
415
+ # symlinks.
416
+ # <b>:overwrite</b>::
417
+ # When set to <tt>:all</tt> (the default), files which already exist will
418
+ # be replaced. When set to <tt>:older</tt>, such files will only be
419
+ # replaced if they are older according to their last modified times than
420
+ # the zip entry which would replace them. When set to <tt>:none</tt>,
421
+ # such files will never be replaced. Any other value is the same as
422
+ # <tt>:all</tt>.
423
+ # <b>:create</b>::
424
+ # When set to +true+ (the default), files and directories which do not
425
+ # already exist will be extracted. When set to +false+ only files and
426
+ # directories which already exist will be extracted (depending on the
427
+ # setting of <b>:overwrite</b>).
428
+ # <b>:flatten</b>::
429
+ # When set to +false+ (the default), the directory paths containing
430
+ # extracted files will be created within +destination+ in order to contain
431
+ # the files. When set to +true+ files are extracted directly to
432
+ # +destination+ and directory entries are skipped.
433
+ # <b>:exclude</b>::
434
+ # Specifies a proc or lambda which takes a single argument containing a
435
+ # zip entry and returns +true+ if the entry should be skipped during
436
+ # extraction and +false+ if it should be extracted.
437
+ # Any other options which are supported by Archive::Zip::Entry#extract are
438
+ # also supported.
439
+ #
440
+ # Raises Archive::Zip::IOError if called after #close.
441
+ #
442
+ # == Example
443
+ #
444
+ # An archive, <tt>archive.zip</tt>, contains:
445
+ # zip-test/
446
+ # zip-test/dir1/
447
+ # zip-test/dir1/file2.txt
448
+ # zip-test/dir2/
449
+ # zip-test/file1.txt
450
+ #
451
+ # A directory, <tt>extract4</tt>, contains:
452
+ # zip-test
453
+ # +- dir1
454
+ # +- file1.txt
455
+ #
456
+ # Extract the archive:
457
+ # Archive::Zip.open('archive.zip') do |z|
458
+ # z.extract('extract1')
459
+ # end
460
+ #
461
+ # Archive::Zip.open('archive.zip') do |z|
462
+ # z.extract('extract2', :flatten => true)
463
+ # end
464
+ #
465
+ # Archive::Zip.open('archive.zip') do |z|
466
+ # z.extract('extract3', :create => false)
467
+ # end
468
+ #
469
+ # Archive::Zip.open('archive.zip') do |z|
470
+ # z.extract('extract3', :create => true)
471
+ # end
472
+ #
473
+ # Archive::Zip.open('archive.zip') do |z|
474
+ # z.extract( 'extract5', :exclude => lambda { |e| e.file? })
475
+ # end
476
+ #
477
+ # The directories contain:
478
+ # extract1 -> zip-test
479
+ # +- dir1
480
+ # | +- file2.txt
481
+ # +- dir2
482
+ # +- file1.txt
483
+ #
484
+ # extract2 -> file2.txt
485
+ # file1.txt
486
+ #
487
+ # extract3 -> <empty>
488
+ #
489
+ # extract4 -> zip-test
490
+ # +- dir2
491
+ # +- file1.txt <- from archive contents
492
+ #
493
+ # extract5 -> zip-test
494
+ # +- dir1
495
+ # +- dir2
496
+ def extract(destination, options = {})
497
+ raise IOError, 'closed archive' if @closed
498
+
499
+ # Ensure that unspecified options have default values.
500
+ options[:directories] = true unless options.has_key?(:directories)
501
+ options[:symlinks] = false unless options.has_key?(:symlinks)
502
+ options[:overwrite] = :all unless options[:overwrite] == :older ||
503
+ options[:overwrite] == :never
504
+ options[:create] = true unless options.has_key?(:create)
505
+ options[:flatten] = false unless options.has_key?(:flatten)
506
+
507
+ # Flattening the archive structure implies that directory entries are
508
+ # skipped.
509
+ options[:directories] = false if options[:flatten]
510
+
511
+ # First extract all non-directory entries.
512
+ directories = []
513
+ each do |entry|
514
+ # Compute the target file path.
515
+ file_path = entry.zip_path
516
+ file_path = File.basename(file_path) if options[:flatten]
517
+ file_path = File.join(destination, file_path)
518
+
519
+ # Cache some information about the file path.
520
+ file_exists = File.exist?(file_path)
521
+ file_mtime = File.mtime(file_path) if file_exists
522
+
523
+ # Skip this entry if so directed.
524
+ if (! file_exists && ! options[:create]) ||
525
+ (file_exists &&
526
+ (options[:overwrite] == :never ||
527
+ options[:overwrite] == :older && entry.mtime <= file_mtime)) ||
528
+ (! options[:exclude].nil? && options[:exclude].call(entry)) then
529
+ next
530
+ end
531
+
532
+ if entry.directory? then
533
+ # Record the directories as they are encountered.
534
+ directories << entry
535
+ elsif entry.file? || (entry.symlink? && options[:symlinks]) then
536
+ # Extract files and symlinks.
537
+ entry.extract(
538
+ options.merge(:file_path => file_path)
539
+ )
540
+ end
541
+ end
542
+
543
+ if options[:directories] then
544
+ # Then extract the directory entries in depth first order so that time
545
+ # stamps, ownerships, and permissions can be properly restored.
546
+ directories.sort { |a, b| b.zip_path <=> a.zip_path }.each do |entry|
547
+ entry.extract(
548
+ options.merge(:file_path => File.join(destination, entry.zip_path))
549
+ )
550
+ end
551
+ end
552
+
553
+ nil
554
+ end
555
+
556
+ private
557
+
558
+ # NOTE: For now _io_ MUST be seekable and report such by returning +true+
559
+ # from its seekable? method. See IO#seekable?.
560
+ #
561
+ # Raises Archive::Zip::IOError if _io_ is not seekable.
562
+ def parse(io)
563
+ # Error out if the IO object is not confirmed seekable.
564
+ raise Zip::IOError, 'non-seekable IO object given' unless io.respond_to?(:seekable?) and io.seekable?
565
+
566
+ socd_pos = find_central_directory(io)
567
+ io.seek(socd_pos)
568
+ # Parse each entry in the central directory.
569
+ loop do
570
+ signature = io.readbytes(4)
571
+ break unless signature == CFH_SIGNATURE
572
+ add_entry(Zip::Entry.parse(io))
573
+ end
574
+ @dirty = false
575
+ # Maybe add support for digital signatures and ZIP64 records... Later
576
+
577
+ nil
578
+ end
579
+
580
+ # Returns the file offset of the first record in the central directory.
581
+ # _io_ must be a seekable, readable, IO-like object.
582
+ #
583
+ # Raises Archive::Zip::UnzipError if the end of central directory signature
584
+ # is not found where expected or at all.
585
+ def find_central_directory(io)
586
+ # First find the offset to the end of central directory record.
587
+ # It is expected that the variable length comment field will usually be
588
+ # empty and as a result the initial value of eocd_offset is all that is
589
+ # necessary.
590
+ #
591
+ # NOTE: A cleverly crafted comment could throw this thing off if the
592
+ # comment itself looks like a valid end of central directory record.
593
+ eocd_offset = -22
594
+ loop do
595
+ io.seek(eocd_offset, IO::SEEK_END)
596
+ if io.readbytes(4) == EOCD_SIGNATURE then
597
+ io.seek(16, IO::SEEK_CUR)
598
+ break if io.readbytes(2).unpack('v')[0] == (eocd_offset + 22).abs
599
+ end
600
+ eocd_offset -= 1
601
+ end
602
+ # At this point, eocd_offset should point to the location of the end of
603
+ # central directory record relative to the end of the archive.
604
+ # Now, jump into the location in the record which contains a pointer to
605
+ # the start of the central directory record and return the value.
606
+ io.seek(eocd_offset + 16, IO::SEEK_END)
607
+ return io.readbytes(4).unpack('V')[0]
608
+ rescue Errno::EINVAL
609
+ raise Zip::UnzipError, 'unable to locate end-of-central-directory record'
610
+ end
611
+
612
+ # Writes all the entries of this archive to _io_. _io_ must be a writable,
613
+ # IO-like object providing a _write_ method. Returns the total number of
614
+ # bytes written.
615
+ def dump(io)
616
+ bytes_written = 0
617
+ entries = @entries.values
618
+ entries.each do |entry|
619
+ bytes_written += entry.dump_local_file_record(io, bytes_written)
620
+ end
621
+ central_directory_offset = bytes_written
622
+ entries.each do |entry|
623
+ bytes_written += entry.dump_central_file_record(io)
624
+ end
625
+ central_directory_length = bytes_written - central_directory_offset
626
+ bytes_written += io.write(EOCD_SIGNATURE)
627
+ bytes_written += io.write(
628
+ [
629
+ 0,
630
+ 0,
631
+ entries.length,
632
+ entries.length,
633
+ central_directory_length,
634
+ central_directory_offset,
635
+ comment.length
636
+ ].pack('vvvvVVv')
637
+ )
638
+ bytes_written += io.write(comment)
639
+
640
+ bytes_written
641
+ end
642
+ end
643
+ end