archive-zip 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CONTRIBUTORS +13 -0
- data/GPL +676 -0
- data/HACKING +122 -0
- data/LEGAL +8 -0
- data/LICENSE +57 -0
- data/MANIFEST +26 -0
- data/NEWS +22 -0
- data/README +130 -0
- data/lib/archive/support/io-like.rb +12 -0
- data/lib/archive/support/io.rb +14 -0
- data/lib/archive/support/iowindow.rb +123 -0
- data/lib/archive/support/stringio.rb +22 -0
- data/lib/archive/support/time.rb +85 -0
- data/lib/archive/support/zlib.rb +211 -0
- data/lib/archive/zip.rb +643 -0
- data/lib/archive/zip/codec.rb +30 -0
- data/lib/archive/zip/codec/deflate.rb +206 -0
- data/lib/archive/zip/codec/store.rb +241 -0
- data/lib/archive/zip/datadescriptor.rb +54 -0
- data/lib/archive/zip/entry.rb +991 -0
- data/lib/archive/zip/error.rb +22 -0
- data/lib/archive/zip/extrafield.rb +23 -0
- data/lib/archive/zip/extrafield/extendedtimestamp.rb +101 -0
- data/lib/archive/zip/extrafield/raw.rb +32 -0
- data/lib/archive/zip/extrafield/unix.rb +101 -0
- data/test/test_archive.rb +8 -0
- metadata +98 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
module Archive; class Zip
|
2
|
+
# Archive::Zip::Entry::DataDescriptor is a convenience class which bundles
|
3
|
+
# imporant information concerning the compressed data in a ZIP archive entry
|
4
|
+
# and allows easy comparisons between instances of itself.
|
5
|
+
class DataDescriptor
|
6
|
+
# Create a new instance of this class where <em>crc32</em>,
|
7
|
+
# _compressed_size_, and _uncompressed_size_ are all integers representing a
|
8
|
+
# CRC32 checksum of uncompressed data, the size of compressed data, and the
|
9
|
+
# size of uncompressed data respectively.
|
10
|
+
def initialize(crc32, compressed_size, uncompressed_size)
|
11
|
+
@crc32 = crc32
|
12
|
+
@compressed_size = compressed_size
|
13
|
+
@uncompressed_size = uncompressed_size
|
14
|
+
end
|
15
|
+
|
16
|
+
# A CRC32 checksum over some set of uncompressed data.
|
17
|
+
attr_reader :crc32
|
18
|
+
# A count of the number of bytes of compressed data associated with a set of
|
19
|
+
# uncompressed data.
|
20
|
+
attr_reader :compressed_size
|
21
|
+
# A count of the number of bytes of a set of uncompressed data.
|
22
|
+
attr_reader :uncompressed_size
|
23
|
+
|
24
|
+
# Compares the attributes of this object with like-named attributes of
|
25
|
+
# _other_ and raises Archive::Zip::Error for any mismatches.
|
26
|
+
def verify(other)
|
27
|
+
unless crc32 == other.crc32 then
|
28
|
+
raise Zip::Error,
|
29
|
+
"CRC32 mismatch: #{crc32.to_s(16)} vs. #{other.crc32.to_s(16)}"
|
30
|
+
end
|
31
|
+
unless compressed_size == other.compressed_size then
|
32
|
+
raise Zip::Error,
|
33
|
+
"compressed size mismatch: #{compressed_size} vs. #{other.compressed_size}"
|
34
|
+
end
|
35
|
+
unless uncompressed_size == other.uncompressed_size then
|
36
|
+
raise Zip::Error,
|
37
|
+
"uncompressed size mismatch: #{uncompressed_size} vs. #{other.uncompressed_size}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Writes the data wrapped in this object to _io_ which must be a writable,
|
42
|
+
# IO-like object prividing a _write_ method. Returns the number of bytes
|
43
|
+
# written.
|
44
|
+
def dump(io)
|
45
|
+
io.write(
|
46
|
+
[
|
47
|
+
crc32,
|
48
|
+
compressed_size,
|
49
|
+
uncompressed_size
|
50
|
+
].pack('VVV')
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end; end
|
@@ -0,0 +1,991 @@
|
|
1
|
+
require 'archive/zip/codec/store'
|
2
|
+
require 'archive/zip/error'
|
3
|
+
require 'archive/zip/extrafield'
|
4
|
+
require 'archive/zip/datadescriptor'
|
5
|
+
|
6
|
+
module Archive; class Zip
|
7
|
+
# The Archive::Zip::Entry mixin provides classes with methods implementing
|
8
|
+
# many of the common features of all entry types. Some of these methods, such
|
9
|
+
# as _dump_local_file_record_ and _dump_central_file_record_, are required by
|
10
|
+
# Archive::Zip in order to store the entry into an archive. Those should be
|
11
|
+
# left alone. Others, such as _ftype_ and <i>mode=</i>, are expected to be
|
12
|
+
# overridden to provide sensible information for the new entry type.
|
13
|
+
#
|
14
|
+
# A class using this mixin must provide 2 methods: _extract_ and
|
15
|
+
# _dump_compressed_data_. _extract_ should be a public method with the
|
16
|
+
# following signature:
|
17
|
+
#
|
18
|
+
# def extract(options = {})
|
19
|
+
# ...
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# This method should extract the contents of the entry to the filesystem.
|
23
|
+
# _options_ should be an optional Hash containing a mapping of option names to
|
24
|
+
# option values. Please refer to Archive::Zip::Entry::File#extract,
|
25
|
+
# Archive::Zip::Entry::Symlink#extract, and
|
26
|
+
# Archive::Zip::Entry::Directory#extract for examples of the options currently
|
27
|
+
# supported.
|
28
|
+
#
|
29
|
+
# _dump_compressed_data_ should be a private method with the following
|
30
|
+
# signature:
|
31
|
+
#
|
32
|
+
# def dump_compressed_data(io)
|
33
|
+
# ...
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# This method should use the _write_ method of _io_ to write all file data.
|
37
|
+
# _io_ will be a writable, IO-like object.
|
38
|
+
#
|
39
|
+
# The class methods from_file and parse are factories for creating the 3 kinds
|
40
|
+
# of concrete entries currently implemented: File, Directory, and Symlink.
|
41
|
+
# While it is possible to create new archives using custom entry
|
42
|
+
# implementations, it is not possible to load those same entries from the
|
43
|
+
# archive since the parse factory method does not know about them. Patches
|
44
|
+
# to support new entry types are welcome.
|
45
|
+
module Entry
|
46
|
+
CFHRecord = Struct.new(
|
47
|
+
:made_by_version,
|
48
|
+
:extraction_version,
|
49
|
+
:general_purpose_flags,
|
50
|
+
:compression_method,
|
51
|
+
:mtime,
|
52
|
+
:crc32,
|
53
|
+
:compressed_size,
|
54
|
+
:uncompressed_size,
|
55
|
+
:disk_number_start,
|
56
|
+
:internal_file_attributes,
|
57
|
+
:external_file_attributes,
|
58
|
+
:local_header_position,
|
59
|
+
:zip_path,
|
60
|
+
:extra_fields,
|
61
|
+
:comment
|
62
|
+
)
|
63
|
+
|
64
|
+
LFHRecord = Struct.new(
|
65
|
+
:extraction_version,
|
66
|
+
:general_purpose_flags,
|
67
|
+
:compression_method,
|
68
|
+
:mtime,
|
69
|
+
:crc32,
|
70
|
+
:compressed_size,
|
71
|
+
:uncompressed_size,
|
72
|
+
:zip_path,
|
73
|
+
:extra_fields,
|
74
|
+
:compressed_data
|
75
|
+
)
|
76
|
+
|
77
|
+
# When this flag is set in the general purpose flags, it indicates that the
|
78
|
+
# read data descriptor record for a local file record is located after the
|
79
|
+
# entry's file data.
|
80
|
+
FLAG_DATA_DESCRIPTOR_FOLLOWS = 0b1000
|
81
|
+
|
82
|
+
# Cleans up and returns _zip_path_ by eliminating . and .. references,
|
83
|
+
# leading and trailing <tt>/</tt>'s, and runs of <tt>/</tt>'s.
|
84
|
+
def self.expand_path(zip_path)
|
85
|
+
result = []
|
86
|
+
source = zip_path.split('/')
|
87
|
+
|
88
|
+
source.each do |e|
|
89
|
+
next if e.empty? || e == '.'
|
90
|
+
|
91
|
+
if e == '..' && ! (result.last.nil? || result.last == '..') then
|
92
|
+
result.pop
|
93
|
+
else
|
94
|
+
result.push(e)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
result.shift while result.first == '..'
|
98
|
+
|
99
|
+
result.join('/')
|
100
|
+
end
|
101
|
+
|
102
|
+
# Creates a new Entry based upon a file, symlink, or directory. _file_path_
|
103
|
+
# points to the source item. _options_ is a Hash optionally containing the
|
104
|
+
# following:
|
105
|
+
# <b>:zip_path</b>::
|
106
|
+
# The path for the entry in the archive where `/' is the file separator
|
107
|
+
# character. This defaults to the basename of _file_path_ if unspecified.
|
108
|
+
# <b>:follow_symlinks</b>::
|
109
|
+
# When set to +true+ (the default), symlinks are treated as the files or
|
110
|
+
# directories to which they point.
|
111
|
+
# <b>:codec</b>::
|
112
|
+
# When unset, the default codec for file entries is used; otherwise, a
|
113
|
+
# file entry which is created will use the codec set with this option.
|
114
|
+
#
|
115
|
+
# Raises Archive::Zip::EntryError if processing the given file path results
|
116
|
+
# in a file not found error.
|
117
|
+
def self.from_file(file_path, options = {})
|
118
|
+
zip_path = options.has_key?(:zip_path) ?
|
119
|
+
expand_path(options[:zip_path]) :
|
120
|
+
::File.basename(file_path)
|
121
|
+
follow_symlinks = options.has_key?(:follow_symlinks) ?
|
122
|
+
options[:follow_symlinks] :
|
123
|
+
true
|
124
|
+
|
125
|
+
# Avoid repeatedly stat'ing the file by storing the stat structure once.
|
126
|
+
begin
|
127
|
+
stat = follow_symlinks ?
|
128
|
+
::File.stat(file_path) :
|
129
|
+
::File.lstat(file_path)
|
130
|
+
rescue Errno::ENOENT
|
131
|
+
if ::File.symlink?(file_path) then
|
132
|
+
raise Zip::EntryError,
|
133
|
+
"symlink at `#{file_path}' points to a non-existent file `#{::File.readlink(file_path)}'"
|
134
|
+
else
|
135
|
+
raise Zip::EntryError, "no such file or directory `#{file_path}'"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Ensure that zip paths for directories end with '/'.
|
140
|
+
if stat.directory? then
|
141
|
+
zip_path += '/'
|
142
|
+
end
|
143
|
+
|
144
|
+
if stat.symlink? then
|
145
|
+
entry = Entry::Symlink.new(zip_path)
|
146
|
+
entry.link_target = ::File.readlink(file_path)
|
147
|
+
elsif stat.file? then
|
148
|
+
entry = Entry::File.new(zip_path)
|
149
|
+
entry.file_path = file_path
|
150
|
+
entry.codec = options[:codec] unless options[:codec].nil?
|
151
|
+
elsif stat.directory? then
|
152
|
+
entry = Entry::Directory.new(zip_path)
|
153
|
+
else
|
154
|
+
raise Zip::EntryError,
|
155
|
+
"unsupported file type `#{stat.ftype}' for file `#{file_path}'"
|
156
|
+
end
|
157
|
+
entry.uid = stat.uid
|
158
|
+
entry.gid = stat.gid
|
159
|
+
entry.mtime = stat.mtime
|
160
|
+
entry.atime = stat.atime
|
161
|
+
entry.mode = stat.mode
|
162
|
+
|
163
|
+
entry
|
164
|
+
end
|
165
|
+
|
166
|
+
# Creates and returns a new entry object by parsing from the current
|
167
|
+
# position of _io_. _io_ must be a readable, IO-like object which provides
|
168
|
+
# a _readbytes_ method, and it must be positioned at the start of a central
|
169
|
+
# file record following the signature for that record.
|
170
|
+
#
|
171
|
+
# <b>NOTE:</b> For now _io_ MUST be seekable and report such by returning
|
172
|
+
# +true+ from its <i>seekable?</i> method. See IO#seekable?.
|
173
|
+
#
|
174
|
+
# Currently, the only entry objects returned are instances of
|
175
|
+
# Archive::Zip::Entry::File, Archive::Zip::Entry::Directory, and
|
176
|
+
# Archive::Zip::Entry::Symlink. Any other kind of entry will be mapped into
|
177
|
+
# an instance of Archive::Zip::Entry::File.
|
178
|
+
#
|
179
|
+
# Raises Archive::Zip::IOError if _io_ is not seekable. Raises
|
180
|
+
# Archive::Zip::EntryError for any other errors related to processing the
|
181
|
+
# entry.
|
182
|
+
def self.parse(io)
|
183
|
+
# Error out if the IO object is not confirmed seekable.
|
184
|
+
unless io.respond_to?(:seekable?) and io.seekable? then
|
185
|
+
raise Zip::IOError, 'non-seekable IO object given'
|
186
|
+
end
|
187
|
+
|
188
|
+
# Parse the central file record and then use the information found there
|
189
|
+
# to locate and parse the corresponding local file record.
|
190
|
+
cfr = parse_central_file_record(io)
|
191
|
+
next_record_position = io.pos
|
192
|
+
io.seek(cfr.local_header_position)
|
193
|
+
unless io.readbytes(4) == LFH_SIGNATURE then
|
194
|
+
raise Zip::EntryError, 'bad local file header signature'
|
195
|
+
end
|
196
|
+
lfr = parse_local_file_record(io, cfr.compressed_size)
|
197
|
+
|
198
|
+
# Check to ensure that the contents of the central file record and the
|
199
|
+
# local file record which are supposed to be duplicated are in fact the
|
200
|
+
# same.
|
201
|
+
compare_file_records(lfr, cfr)
|
202
|
+
|
203
|
+
# Raise an error if the codec is not supported.
|
204
|
+
unless Codec.supported?(cfr.compression_method) then
|
205
|
+
raise Zip::EntryError,
|
206
|
+
"`#{cfr.zip_path}': unsupported compression method"
|
207
|
+
end
|
208
|
+
|
209
|
+
# Load the correct codec.
|
210
|
+
codec = Codec.create(cfr.compression_method, cfr.general_purpose_flags)
|
211
|
+
# Set up a data descriptor with expected values for later comparison.
|
212
|
+
data_descriptor = DataDescriptor.new(
|
213
|
+
cfr.crc32,
|
214
|
+
cfr.compressed_size,
|
215
|
+
cfr.uncompressed_size
|
216
|
+
)
|
217
|
+
# Create the entry.
|
218
|
+
expanded_path = expand_path(cfr.zip_path)
|
219
|
+
if cfr.zip_path[-1..-1] == '/' then
|
220
|
+
# This is a directory entry.
|
221
|
+
begin
|
222
|
+
data_descriptor.verify(DataDescriptor.new(0, 0, 0))
|
223
|
+
rescue => e
|
224
|
+
raise Zip::EntryError, "`#{cfr.zip_path}': #{e.message}"
|
225
|
+
end
|
226
|
+
entry = Entry::Directory.new(expanded_path)
|
227
|
+
elsif (cfr.external_file_attributes >> 16) & 0770000 == 0120000 then
|
228
|
+
# This is a symlink entry.
|
229
|
+
entry = Entry::Symlink.new(expanded_path)
|
230
|
+
decompressor = codec.decompressor(
|
231
|
+
IOWindow.new(io, io.pos, cfr.compressed_size)
|
232
|
+
)
|
233
|
+
entry.link_target = decompressor.read
|
234
|
+
begin
|
235
|
+
data_descriptor.verify(decompressor.data_descriptor)
|
236
|
+
rescue => e
|
237
|
+
raise Zip::EntryError, "`#{cfr.zip_path}': #{e.message}"
|
238
|
+
end
|
239
|
+
decompressor.close
|
240
|
+
else
|
241
|
+
# Anything else is a file entry.
|
242
|
+
entry = Entry::File.new(expanded_path)
|
243
|
+
entry.file_data = codec.decompressor(
|
244
|
+
IOWindow.new(io, io.pos, cfr.compressed_size)
|
245
|
+
)
|
246
|
+
entry.expected_data_descriptor = data_descriptor
|
247
|
+
end
|
248
|
+
|
249
|
+
# Set some entry metadata.
|
250
|
+
entry.mtime = cfr.mtime
|
251
|
+
# Only set mode bits for the entry if the external file attributes are
|
252
|
+
# Unix-compatible.
|
253
|
+
if cfr.made_by_version & 0xFF00 == 0x0300 then
|
254
|
+
entry.mode = cfr.external_file_attributes >> 16
|
255
|
+
end
|
256
|
+
entry.comment = cfr.comment
|
257
|
+
cfr.extra_fields.each { |ef| entry.add_extra_field(ef) }
|
258
|
+
lfr.extra_fields.each { |ef| entry.add_extra_field(ef) }
|
259
|
+
|
260
|
+
# Return to the beginning of the next central directory record.
|
261
|
+
io.seek(next_record_position)
|
262
|
+
|
263
|
+
entry
|
264
|
+
end
|
265
|
+
|
266
|
+
private
|
267
|
+
|
268
|
+
# Parses a central file record and returns a CFHRecord instance containing
|
269
|
+
# the parsed data. _io_ must be a readable, IO-like object which provides a
|
270
|
+
# _readbytes_ method, and it must be positioned at the start of a central
|
271
|
+
# file record following the signature for that record.
|
272
|
+
def self.parse_central_file_record(io)
|
273
|
+
cfr = CFHRecord.new
|
274
|
+
|
275
|
+
cfr.made_by_version,
|
276
|
+
cfr.extraction_version,
|
277
|
+
cfr.general_purpose_flags,
|
278
|
+
cfr.compression_method,
|
279
|
+
dos_mtime,
|
280
|
+
cfr.crc32,
|
281
|
+
cfr.compressed_size,
|
282
|
+
cfr.uncompressed_size,
|
283
|
+
file_name_length,
|
284
|
+
extra_fields_length,
|
285
|
+
comment_length,
|
286
|
+
cfr.disk_number_start,
|
287
|
+
cfr.internal_file_attributes,
|
288
|
+
cfr.external_file_attributes,
|
289
|
+
cfr.local_header_position = io.readbytes(42).unpack('vvvvVVVVvvvvvVV')
|
290
|
+
|
291
|
+
cfr.zip_path = io.readbytes(file_name_length)
|
292
|
+
cfr.extra_fields = parse_extra_fields(io.readbytes(extra_fields_length))
|
293
|
+
cfr.comment = io.readbytes(comment_length)
|
294
|
+
|
295
|
+
# Convert from MSDOS time to Unix time.
|
296
|
+
cfr.mtime = DOSTime.new(dos_mtime).to_time
|
297
|
+
|
298
|
+
cfr
|
299
|
+
rescue EOFError, TruncatedDataError
|
300
|
+
raise Zip::EntryError, 'unexpected end of file'
|
301
|
+
end
|
302
|
+
|
303
|
+
# Parses a local file record and returns a LFHRecord instance containing the
|
304
|
+
# parsed data. _io_ must be a readable, IO-like object which provides a
|
305
|
+
# readbytes method, and it must be positioned at the start of a local file
|
306
|
+
# record following the signature for that record.
|
307
|
+
#
|
308
|
+
# If the record to be parsed is flagged to have a trailing data descriptor
|
309
|
+
# record, _expected_compressed_size_ must be set to an integer counting the
|
310
|
+
# number of bytes of compressed data to skip in order to find the trailing
|
311
|
+
# data descriptor record, and _io_ must be seekable by providing _pos_ and
|
312
|
+
# <i>pos=</i> methods.
|
313
|
+
def self.parse_local_file_record(io, expected_compressed_size = nil)
|
314
|
+
lfr = LFHRecord.new
|
315
|
+
|
316
|
+
lfr.extraction_version,
|
317
|
+
lfr.general_purpose_flags,
|
318
|
+
lfr.compression_method,
|
319
|
+
dos_mtime,
|
320
|
+
lfr.crc32,
|
321
|
+
lfr.compressed_size,
|
322
|
+
lfr.uncompressed_size,
|
323
|
+
file_name_length,
|
324
|
+
extra_fields_length = io.readbytes(26).unpack('vvvVVVVvv')
|
325
|
+
|
326
|
+
lfr.zip_path = io.readbytes(file_name_length)
|
327
|
+
lfr.extra_fields = parse_extra_fields(io.readbytes(extra_fields_length))
|
328
|
+
|
329
|
+
# Convert from MSDOS time to Unix time.
|
330
|
+
lfr.mtime = DOSTime.new(dos_mtime).to_time
|
331
|
+
|
332
|
+
if lfr.general_purpose_flags & FLAG_DATA_DESCRIPTOR_FOLLOWS > 0 then
|
333
|
+
saved_pos = io.pos
|
334
|
+
io.pos += expected_compressed_size
|
335
|
+
# Because the ZIP specification has a history of murkiness, some
|
336
|
+
# libraries create trailing data descriptor records with a preceding
|
337
|
+
# signature while others do not.
|
338
|
+
# This handles both cases.
|
339
|
+
possible_signature = io.readbytes(4)
|
340
|
+
if possible_signature == DD_SIGNATURE then
|
341
|
+
lfr.crc32,
|
342
|
+
lfr.compressed_size,
|
343
|
+
lfr.uncompressed_size = io.readbytes(12).unpack('VVV')
|
344
|
+
else
|
345
|
+
lfr.crc32 = possible_signature.unpack('V')[0]
|
346
|
+
lfr.compressed_size,
|
347
|
+
lfr.uncompressed_size = io.readbytes(8).unpack('VV')
|
348
|
+
end
|
349
|
+
io.pos = saved_pos
|
350
|
+
end
|
351
|
+
|
352
|
+
lfr
|
353
|
+
rescue EOFError, TruncatedDataError
|
354
|
+
raise Zip::EntryError, 'unexpected end of file'
|
355
|
+
end
|
356
|
+
|
357
|
+
# Parses the extra fields for local and central file records and returns an
|
358
|
+
# array of extra field objects. _bytes_ must be a String containing all of
|
359
|
+
# the extra field data to be parsed.
|
360
|
+
def self.parse_extra_fields(bytes)
|
361
|
+
StringIO.open(bytes) do |io|
|
362
|
+
extra_fields = []
|
363
|
+
while ! io.eof? do
|
364
|
+
header_id, data_size = io.readbytes(4).unpack('vv')
|
365
|
+
data = io.readbytes(data_size)
|
366
|
+
|
367
|
+
extra_fields << ExtraField.parse(header_id, data)
|
368
|
+
end
|
369
|
+
extra_fields
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
# Compares the local and the central file records found in _lfr_ and _cfr
|
374
|
+
# respectively. Raises Archive::Zip::EntryError if the comparison fails.
|
375
|
+
def self.compare_file_records(lfr, cfr)
|
376
|
+
# Exclude the extra fields from the comparison since some implementations,
|
377
|
+
# such as InfoZip, are known to have differences in the extra fields used
|
378
|
+
# in local file records vs. central file records.
|
379
|
+
if lfr.zip_path != cfr.zip_path then
|
380
|
+
raise Zip::EntryError, "zip path differs between local and central file records: `#{lfr.zip_path}' != `#{cfr.zip_path}'"
|
381
|
+
end
|
382
|
+
if lfr.extraction_version != cfr.extraction_version then
|
383
|
+
raise Zip::EntryError, "`#{cfr.zip_path}': extraction version differs between local and central file records"
|
384
|
+
end
|
385
|
+
if lfr.crc32 != cfr.crc32 then
|
386
|
+
raise Zip::EntryError, "`#{cfr.zip_path}': CRC32 differs between local and central file records"
|
387
|
+
end
|
388
|
+
if lfr.compressed_size != cfr.compressed_size then
|
389
|
+
raise Zip::EntryError, "`#{cfr.zip_path}': compressed size differs between local and central file records"
|
390
|
+
end
|
391
|
+
if lfr.uncompressed_size != cfr.uncompressed_size then
|
392
|
+
raise Zip::EntryError, "`#{cfr.zip_path}': uncompressed size differs between local and central file records"
|
393
|
+
end
|
394
|
+
if lfr.general_purpose_flags != cfr.general_purpose_flags then
|
395
|
+
raise Zip::EntryError, "`#{cfr.zip_path}': general purpose flag differs between local and central file records"
|
396
|
+
end
|
397
|
+
if lfr.compression_method != cfr.compression_method then
|
398
|
+
raise Zip::EntryError, "`#{cfr.zip_path}': compression method differs between local and central file records"
|
399
|
+
end
|
400
|
+
if lfr.mtime != cfr.mtime then
|
401
|
+
raise Zip::EntryError, "`#{cfr.zip_path}': last modified time differs between local and central file records"
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
public
|
406
|
+
|
407
|
+
# Creates a new, uninitialized Entry instance using the Store compression
|
408
|
+
# method. The zip path is initialized to _zip_path_.
|
409
|
+
def initialize(zip_path)
|
410
|
+
self.zip_path = zip_path
|
411
|
+
self.mtime = Time.now
|
412
|
+
self.atime = @mtime
|
413
|
+
self.uid = nil
|
414
|
+
self.gid = nil
|
415
|
+
self.mode = 0777
|
416
|
+
self.comment = ''
|
417
|
+
self.codec = Zip::Codec::Store.new
|
418
|
+
@extra_fields = []
|
419
|
+
end
|
420
|
+
|
421
|
+
# The path for this entry in the ZIP archive.
|
422
|
+
attr_reader :zip_path
|
423
|
+
# The last accessed time.
|
424
|
+
attr_accessor :atime
|
425
|
+
# The last modified time.
|
426
|
+
attr_accessor :mtime
|
427
|
+
# The user ID of the owner of this entry.
|
428
|
+
attr_accessor :uid
|
429
|
+
# The group ID of the owner of this entry.
|
430
|
+
attr_accessor :gid
|
431
|
+
# The the file mode/permission bits for this entry.
|
432
|
+
attr_accessor :mode
|
433
|
+
# The comment associated with this entry.
|
434
|
+
attr_accessor :comment
|
435
|
+
# The selected compression codec.
|
436
|
+
attr_accessor :codec
|
437
|
+
|
438
|
+
# Sets the path in the archive for this entry to _zip_path_ after passing it
|
439
|
+
# through Archive::Zip::Entry.expand_path and ensuring that the result is
|
440
|
+
# not empty.
|
441
|
+
def zip_path=(zip_path)
|
442
|
+
@zip_path = Archive::Zip::Entry.expand_path(zip_path)
|
443
|
+
if @zip_path.empty? then
|
444
|
+
raise ArgumentError, "zip path expands to empty string"
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
# Returns the file type of this entry as the symbol <tt>:unknown</tt>.
|
449
|
+
#
|
450
|
+
# Override this in concrete subclasses to return an appropriate symbol.
|
451
|
+
def ftype
|
452
|
+
:unknown
|
453
|
+
end
|
454
|
+
|
455
|
+
# Returns false.
|
456
|
+
def file?
|
457
|
+
false
|
458
|
+
end
|
459
|
+
|
460
|
+
# Returns false.
|
461
|
+
def symlink?
|
462
|
+
false
|
463
|
+
end
|
464
|
+
|
465
|
+
# Returns false.
|
466
|
+
def directory?
|
467
|
+
false
|
468
|
+
end
|
469
|
+
|
470
|
+
# Override this method in descendent classes. It should cause the entry to
|
471
|
+
# be extracted from the archive. This implementation does nothing.
|
472
|
+
# _options_ should be a hash used for specifying extraction options, the
|
473
|
+
# keys of which should not collide with keys used by Archive::Zip#extract.
|
474
|
+
def extract(options = {})
|
475
|
+
end
|
476
|
+
|
477
|
+
# Adds _extra_field_ as an extra field specification to this entry. If
|
478
|
+
# _extra_field_ is an instance of
|
479
|
+
# Archive::Zip::Entry::ExtraField::ExtendedTimestamp, the values of that
|
480
|
+
# field are used to set mtime and atime for this entry. If _extra_field_ is
|
481
|
+
# an instance of Archive::Zip::Entry::ExtraField::Unix, the values of that
|
482
|
+
# field are used to set mtime, atime, uid, and gid for this entry.
|
483
|
+
def add_extra_field(extra_field)
|
484
|
+
@extra_field_data = nil
|
485
|
+
@extra_fields << extra_field
|
486
|
+
|
487
|
+
if extra_field.kind_of?(ExtraField::ExtendedTimestamp) then
|
488
|
+
self.mtime = extra_field.mtime
|
489
|
+
self.atime = extra_field.atime
|
490
|
+
elsif extra_field.kind_of?(ExtraField::Unix) then
|
491
|
+
self.mtime = extra_field.mtime
|
492
|
+
self.atime = extra_field.atime
|
493
|
+
self.uid = extra_field.uid
|
494
|
+
self.gid = extra_field.gid
|
495
|
+
end
|
496
|
+
self
|
497
|
+
end
|
498
|
+
|
499
|
+
# Writes the local file record for this entry to _io_, a writable, IO-like
|
500
|
+
# object which provides a _write_ method. _local_file_record_position_ is
|
501
|
+
# the offset within _io_ at which writing will begin. This is used so that
|
502
|
+
# when writing to a non-seekable IO object it is possible to avoid calling
|
503
|
+
# the _pos_ method of _io_. Returns the number of bytes written.
|
504
|
+
#
|
505
|
+
# <b>NOTE:</b> This method should only be called by Archive::Zip.
|
506
|
+
def dump_local_file_record(io, local_file_record_position)
|
507
|
+
@local_file_record_position = local_file_record_position
|
508
|
+
bytes_written = 0
|
509
|
+
|
510
|
+
general_purpose_flags = codec.general_purpose_flags
|
511
|
+
# Flag that the data descriptor record will follow the compressed file
|
512
|
+
# data of this entry unless the IO object can be access randomly.
|
513
|
+
general_purpose_flags |= 0b1000 unless io.seekable?
|
514
|
+
|
515
|
+
bytes_written += io.write(LFH_SIGNATURE)
|
516
|
+
bytes_written += io.write(
|
517
|
+
[
|
518
|
+
codec.version_needed_to_extract,
|
519
|
+
general_purpose_flags,
|
520
|
+
codec.compression_method,
|
521
|
+
mtime.to_dos_time.to_i,
|
522
|
+
0,
|
523
|
+
0,
|
524
|
+
0,
|
525
|
+
zip_path.length,
|
526
|
+
extra_field_data.length
|
527
|
+
].pack('vvvVVVVvv')
|
528
|
+
)
|
529
|
+
bytes_written += io.write(zip_path)
|
530
|
+
bytes_written += io.write(extra_field_data)
|
531
|
+
|
532
|
+
# Get a compressor, write all the file data to it, and get a data
|
533
|
+
# descriptor from it.
|
534
|
+
codec.compressor(io) do |c|
|
535
|
+
dump_compressed_data(c)
|
536
|
+
c.close(false)
|
537
|
+
@data_descriptor = c.data_descriptor
|
538
|
+
end
|
539
|
+
|
540
|
+
bytes_written += @data_descriptor.compressed_size
|
541
|
+
if io.seekable? then
|
542
|
+
saved_position = io.pos
|
543
|
+
io.pos = @local_file_record_position + 14
|
544
|
+
@data_descriptor.dump(io)
|
545
|
+
io.pos = saved_position
|
546
|
+
else
|
547
|
+
bytes_written += io.write(DD_SIGNATURE)
|
548
|
+
bytes_written += @data_descriptor.dump(io)
|
549
|
+
end
|
550
|
+
|
551
|
+
bytes_written
|
552
|
+
end
|
553
|
+
|
554
|
+
# Writes the central file record for this entry to _io_, a writable, IO-like
|
555
|
+
# object which provides a _write_ method. Returns the number of bytes
|
556
|
+
# written.
|
557
|
+
#
|
558
|
+
# <b>NOTE:</b> This method should only be called by Archive::Zip.
|
559
|
+
def dump_central_file_record(io)
|
560
|
+
bytes_written = 0
|
561
|
+
|
562
|
+
general_purpose_flags = codec.general_purpose_flags
|
563
|
+
# Flag that the data descriptor record will follow the compressed file
|
564
|
+
# data of this entry unless the IO object can be access randomly.
|
565
|
+
general_purpose_flags |= FLAG_DATA_DESCRIPTOR_FOLLOWS unless io.seekable?
|
566
|
+
|
567
|
+
bytes_written += io.write(CFH_SIGNATURE)
|
568
|
+
bytes_written += io.write(
|
569
|
+
[
|
570
|
+
version_made_by,
|
571
|
+
codec.version_needed_to_extract,
|
572
|
+
general_purpose_flags,
|
573
|
+
codec.compression_method,
|
574
|
+
mtime.to_dos_time.to_i
|
575
|
+
].pack('vvvvV')
|
576
|
+
)
|
577
|
+
bytes_written += @data_descriptor.dump(io)
|
578
|
+
bytes_written += io.write(
|
579
|
+
[
|
580
|
+
zip_path.length,
|
581
|
+
extra_field_data.length,
|
582
|
+
comment.length,
|
583
|
+
0,
|
584
|
+
internal_file_attributes,
|
585
|
+
external_file_attributes,
|
586
|
+
@local_file_record_position
|
587
|
+
].pack('vvvvvVV')
|
588
|
+
)
|
589
|
+
bytes_written += io.write(zip_path)
|
590
|
+
bytes_written += io.write(extra_field_data)
|
591
|
+
bytes_written += io.write(comment)
|
592
|
+
|
593
|
+
bytes_written
|
594
|
+
end
|
595
|
+
|
596
|
+
private
|
597
|
+
|
598
|
+
def version_made_by
|
599
|
+
0x0314
|
600
|
+
end
|
601
|
+
|
602
|
+
def extra_field_data
|
603
|
+
return @extra_field_data unless @extra_field_data.nil?
|
604
|
+
|
605
|
+
@extra_field_data = @extra_fields.collect do |extra_field|
|
606
|
+
unless extra_field.kind_of?(ExtraField::ExtendedTimestamp) ||
|
607
|
+
extra_field.kind_of?(ExtraField::Unix) then
|
608
|
+
extra_field.dump
|
609
|
+
else
|
610
|
+
''
|
611
|
+
end
|
612
|
+
end.join +
|
613
|
+
ExtraField::ExtendedTimestamp.new(mtime, atime, nil).dump +
|
614
|
+
ExtraField::Unix.new(mtime, atime, uid, gid).dump
|
615
|
+
end
|
616
|
+
|
617
|
+
def internal_file_attributes
|
618
|
+
0x0000
|
619
|
+
end
|
620
|
+
|
621
|
+
def external_file_attributes
|
622
|
+
# Put Unix attributes into the high word and DOS attributes into the low
|
623
|
+
# word.
|
624
|
+
(mode << 16) + (directory? ? 0x10 : 0)
|
625
|
+
end
|
626
|
+
end
|
627
|
+
end; end
|
628
|
+
|
629
|
+
module Archive; class Zip; module Entry
|
630
|
+
# Archive::Zip::Entry::Directory represents a directory entry within a Zip
|
631
|
+
# archive.
|
632
|
+
class Directory
|
633
|
+
include Archive::Zip::Entry
|
634
|
+
|
635
|
+
# Inherits the behavior of Archive::Zip::Entry#zip_path= but ensures that
|
636
|
+
# there is a trailing slash (<tt>/</tt>) on the end of the path.
|
637
|
+
def zip_path=(zip_path)
|
638
|
+
super(zip_path)
|
639
|
+
@zip_path += '/'
|
640
|
+
end
|
641
|
+
|
642
|
+
# Returns the file type of this entry as the symbol <tt>:directory</tt>.
|
643
|
+
def ftype
|
644
|
+
:directory
|
645
|
+
end
|
646
|
+
|
647
|
+
# Returns +true+.
|
648
|
+
def directory?
|
649
|
+
true
|
650
|
+
end
|
651
|
+
|
652
|
+
# Overridden in order to ensure that the proper mode bits are set for a
|
653
|
+
# directory.
|
654
|
+
def mode=(mode)
|
655
|
+
super(040000 | (mode & 07777))
|
656
|
+
end
|
657
|
+
|
658
|
+
# Extracts this entry.
|
659
|
+
#
|
660
|
+
# _options_ is a Hash optionally containing the following:
|
661
|
+
# <b>:file_path</b>::
|
662
|
+
# Specifies the path to which this entry will be extracted. Defaults to
|
663
|
+
# the zip path of this entry.
|
664
|
+
# <b>:permissions</b>::
|
665
|
+
# When set to +false+ (the default), POSIX mode/permission bits will be
|
666
|
+
# ignored. Otherwise, they will be restored if possible.
|
667
|
+
# <b>:ownerships</b>::
|
668
|
+
# When set to +false+ (the default), user and group ownerships will be
|
669
|
+
# ignored. On most systems, only a superuser is able to change
|
670
|
+
# ownerships, so setting this option to +true+ as a regular user may have
|
671
|
+
# no effect.
|
672
|
+
# <b>:times</b>::
|
673
|
+
# When set to +false+ (the default), last accessed and last modified times
|
674
|
+
# will be ignored. Otherwise, they will be restored if possible.
|
675
|
+
def extract(options = {})
|
676
|
+
# Ensure that unspecified options have default values.
|
677
|
+
file_path = options.has_key?(:file_path) ?
|
678
|
+
options[:file_path].to_s :
|
679
|
+
@zip_path
|
680
|
+
restore_permissions = options.has_key?(:permissions) ?
|
681
|
+
options[:permissions] :
|
682
|
+
false
|
683
|
+
restore_ownerships = options.has_key?(:ownerships) ?
|
684
|
+
options[:ownerships] :
|
685
|
+
false
|
686
|
+
restore_times = options.has_key?(:times) ?
|
687
|
+
options[:times] :
|
688
|
+
false
|
689
|
+
|
690
|
+
# Make the directory.
|
691
|
+
FileUtils.mkdir_p(file_path)
|
692
|
+
|
693
|
+
# Restore the metadata.
|
694
|
+
::File.chmod(mode, file_path) if restore_permissions
|
695
|
+
::File.chown(uid, gid, file_path) if restore_ownerships
|
696
|
+
::File.utime(atime, mtime, file_path) if restore_times
|
697
|
+
|
698
|
+
nil
|
699
|
+
end
|
700
|
+
|
701
|
+
private
|
702
|
+
|
703
|
+
# Directory entries do not have compressed data to write, so do nothing.
|
704
|
+
def dump_compressed_data(io)
|
705
|
+
end
|
706
|
+
end
|
707
|
+
end; end; end
|
708
|
+
|
709
|
+
module Archive; class Zip; module Entry
|
710
|
+
# Archive::Zip::Entry::Symlink represents a symlink entry withing a Zip
|
711
|
+
# archive.
|
712
|
+
class Symlink
|
713
|
+
include Archive::Zip::Entry
|
714
|
+
|
715
|
+
# A string indicating the target of a symlink.
|
716
|
+
attr_accessor :link_target
|
717
|
+
|
718
|
+
# Returns the file type of this entry as the symbol <tt>:symlink</tt>.
|
719
|
+
def ftype
|
720
|
+
:symlink
|
721
|
+
end
|
722
|
+
|
723
|
+
# Returns +true+.
|
724
|
+
def symlink?
|
725
|
+
true
|
726
|
+
end
|
727
|
+
|
728
|
+
# Overridden in order to ensure that the proper mode bits are set for a
|
729
|
+
# symlink.
|
730
|
+
def mode=(mode)
|
731
|
+
super(0120000 | (mode & 07777))
|
732
|
+
end
|
733
|
+
|
734
|
+
# Extracts this entry.
|
735
|
+
#
|
736
|
+
# _options_ is a Hash optionally containing the following:
|
737
|
+
# <b>:file_path</b>::
|
738
|
+
# Specifies the path to which this entry will be extracted. Defaults to
|
739
|
+
# the zip path of this entry.
|
740
|
+
# <b>:permissions</b>::
|
741
|
+
# When set to +false+ (the default), POSIX mode/permission bits will be
|
742
|
+
# ignored. Otherwise, they will be restored if possible.
|
743
|
+
# <b>:ownerships</b>::
|
744
|
+
# When set to +false+ (the default), user and group ownerships will be
|
745
|
+
# ignored. On most systems, only a superuser is able to change
|
746
|
+
# ownerships, so setting this option to +true+ as a regular user may have
|
747
|
+
# no effect.
|
748
|
+
#
|
749
|
+
# Raises Archive::Zip::ExtractError if the link_target attribute is not
|
750
|
+
# specified.
|
751
|
+
def extract(options = {})
|
752
|
+
raise Zip::ExtractError, 'link_target is nil' if link_target.nil?
|
753
|
+
|
754
|
+
# Ensure that unspecified options have default values.
|
755
|
+
file_path = options.has_key?(:file_path) ?
|
756
|
+
options[:file_path].to_s :
|
757
|
+
@zip_path
|
758
|
+
restore_permissions = options.has_key?(:permissions) ?
|
759
|
+
options[:permissions] :
|
760
|
+
false
|
761
|
+
restore_ownerships = options.has_key?(:ownerships) ?
|
762
|
+
options[:ownerships] :
|
763
|
+
false
|
764
|
+
|
765
|
+
# Create the containing directory tree if necessary.
|
766
|
+
parent_dir = ::File.dirname(file_path)
|
767
|
+
FileUtils.mkdir_p(parent_dir) unless ::File.exist?(parent_dir)
|
768
|
+
|
769
|
+
# Create the symlink.
|
770
|
+
::File.symlink(link_target, file_path)
|
771
|
+
|
772
|
+
# Restore the metadata.
|
773
|
+
# NOTE: Ruby does not have the ability to restore atime and mtime on
|
774
|
+
# symlinks at this time (version 1.8.6).
|
775
|
+
begin
|
776
|
+
::File.lchmod(mode, file_path) if restore_permissions
|
777
|
+
rescue NotImplementedError
|
778
|
+
# Ignore on platforms that do not support lchmod.
|
779
|
+
end
|
780
|
+
begin
|
781
|
+
::File.lchown(uid, gid, file_path) if restore_ownerships
|
782
|
+
rescue NotImplementedError
|
783
|
+
# Ignore on platforms that do not support lchown.
|
784
|
+
end
|
785
|
+
|
786
|
+
nil
|
787
|
+
end
|
788
|
+
|
789
|
+
private
|
790
|
+
|
791
|
+
# Write the link target to _io_ as the file data for the entry.
|
792
|
+
def dump_compressed_data(io)
|
793
|
+
io.write(@link_target)
|
794
|
+
end
|
795
|
+
end
|
796
|
+
end; end; end
|
797
|
+
|
798
|
+
module Archive; class Zip; module Entry
|
799
|
+
# Archive::Zip::Entry::File represents a file entry within a Zip archive.
|
800
|
+
class File
|
801
|
+
include Archive::Zip::Entry
|
802
|
+
|
803
|
+
# Creates a new file entry where _zip_path_ is the path to the entry in the
|
804
|
+
# ZIP archive. The Archive::Zip::Codec::Deflate codec with the default
|
805
|
+
# compression level set (NORMAL) is used by default for compression.
|
806
|
+
def initialize(zip_path)
|
807
|
+
super(zip_path)
|
808
|
+
@file_path = nil
|
809
|
+
@file_data = nil
|
810
|
+
@expected_data_descriptor = nil
|
811
|
+
@codec = Zip::Codec::Deflate.new
|
812
|
+
end
|
813
|
+
|
814
|
+
# An Archive::Zip::Entry::DataDescriptor instance which should contain the
|
815
|
+
# expected CRC32 checksum, compressed size, and uncompressed size for the
|
816
|
+
# file data. When not +nil+, this is used by #extract to confirm that the
|
817
|
+
# data extraction was successful.
|
818
|
+
attr_accessor :expected_data_descriptor
|
819
|
+
|
820
|
+
# Returns a readable, IO-like object containing uncompressed file data. If
|
821
|
+
# the file data has not been explicitly set previously, this will return a
|
822
|
+
# Archive::Zip::Codec::Store::Unstore instance wrapping either a File
|
823
|
+
# instance based on the +file_path+ attribute, if set, or an empty StringIO
|
824
|
+
# instance otherwise.
|
825
|
+
#
|
826
|
+
# <b>NOTE:</b> It is the responsibility of the user of this attribute to
|
827
|
+
# ensure that the #close method of the returned IO-like object is called
|
828
|
+
# when the object is no longer needed.
|
829
|
+
def file_data
|
830
|
+
if @file_data.nil? || @file_data.closed? then
|
831
|
+
if @file_path.nil? then
|
832
|
+
@file_data = StringIO.new
|
833
|
+
else
|
834
|
+
@file_data = ::File.new(@file_path, 'rb')
|
835
|
+
end
|
836
|
+
# Ensure that the IO-like object can return CRC32 and data size
|
837
|
+
# information so that it's possible to verify extraction later if
|
838
|
+
# desired.
|
839
|
+
@file_data = Zip::Codec::Store.new.decompressor(@file_data)
|
840
|
+
end
|
841
|
+
@file_data
|
842
|
+
end
|
843
|
+
|
844
|
+
# Sets the +file_data+ attribute of this object to _file_data_. If
|
845
|
+
# _file_data_ is a String, it will be wrapped in a StringIO instance;
|
846
|
+
# otherwise, _file_data_ must be a readable, IO-like object. _file_data_ is
|
847
|
+
# then wrapped inside an Archive::Zip::Codec::Store::Unstore instance before
|
848
|
+
# finally setting the +file_data+ attribute.
|
849
|
+
#
|
850
|
+
# <b>NOTE:</b> As a side effect, the +file_path+ attribute for this object
|
851
|
+
# will be set to +nil+.
|
852
|
+
def file_data=(file_data)
|
853
|
+
@file_path = nil
|
854
|
+
if file_data.kind_of?(String)
|
855
|
+
@file_data = StringIO.new(file_data)
|
856
|
+
else
|
857
|
+
@file_data = file_data
|
858
|
+
end
|
859
|
+
# Ensure that the IO-like object can return CRC32 and data size
|
860
|
+
# information so that it's possible to verify extraction later if desired.
|
861
|
+
unless @file_data.respond_to?(:data_descriptor) then
|
862
|
+
@file_data = Zip::Codec::Store.new.decompressor(@file_data)
|
863
|
+
end
|
864
|
+
@file_data
|
865
|
+
end
|
866
|
+
|
867
|
+
# The path to a file whose contents are to be used for uncompressed file
|
868
|
+
# data. This will be +nil+ if the +file_data+ attribute is set directly.
|
869
|
+
attr_reader :file_path
|
870
|
+
|
871
|
+
# Sets the +file_path+ attribute to _file_path_ which should be a String
|
872
|
+
# usable with File#new to open a file for reading which will provide the
|
873
|
+
# IO-like object for the +file_data+ attribute.
|
874
|
+
def file_path=(file_path)
|
875
|
+
@file_data = nil
|
876
|
+
@file_path = file_path
|
877
|
+
end
|
878
|
+
|
879
|
+
# Returns the file type of this entry as the symbol <tt>:file</tt>.
|
880
|
+
def ftype
|
881
|
+
:file
|
882
|
+
end
|
883
|
+
|
884
|
+
# Returns +true+.
|
885
|
+
def file?
|
886
|
+
true
|
887
|
+
end
|
888
|
+
|
889
|
+
# Overridden in order to ensure that the proper mode bits are set for a
|
890
|
+
# file.
|
891
|
+
def mode=(mode)
|
892
|
+
super(0100000 | (mode & 07777))
|
893
|
+
end
|
894
|
+
|
895
|
+
# Extracts this entry.
|
896
|
+
#
|
897
|
+
# _options_ is a Hash optionally containing the following:
|
898
|
+
# <b>:file_path</b>::
|
899
|
+
# Specifies the path to which this entry will be extracted. Defaults to
|
900
|
+
# the zip path of this entry.
|
901
|
+
# <b>:permissions</b>::
|
902
|
+
# When set to +false+ (the default), POSIX mode/permission bits will be
|
903
|
+
# ignored. Otherwise, they will be restored if possible.
|
904
|
+
# <b>:ownerships</b>::
|
905
|
+
# When set to +false+ (the default), user and group ownerships will be
|
906
|
+
# ignored. On most systems, only a superuser is able to change
|
907
|
+
# ownerships, so setting this option to +true+ as a regular user may have
|
908
|
+
# no effect.
|
909
|
+
# <b>:times</b>::
|
910
|
+
# When set to +false+ (the default), last accessed and last modified times
|
911
|
+
# will be ignored. Otherwise, they will be restored if possible.
|
912
|
+
#
|
913
|
+
# Raises Archive::Zip::ExtractError if the extracted file data appears
|
914
|
+
# corrupt.
|
915
|
+
def extract(options = {})
|
916
|
+
# Ensure that unspecified options have default values.
|
917
|
+
file_path = options.has_key?(:file_path) ?
|
918
|
+
options[:file_path].to_s :
|
919
|
+
@zip_path
|
920
|
+
restore_permissions = options.has_key?(:permissions) ?
|
921
|
+
options[:permissions] :
|
922
|
+
false
|
923
|
+
restore_ownerships = options.has_key?(:ownerships) ?
|
924
|
+
options[:ownerships] :
|
925
|
+
false
|
926
|
+
restore_times = options.has_key?(:times) ?
|
927
|
+
options[:times] :
|
928
|
+
false
|
929
|
+
|
930
|
+
# Create the containing directory tree if necessary.
|
931
|
+
parent_dir = ::File.dirname(file_path)
|
932
|
+
FileUtils.mkdir_p(parent_dir) unless ::File.exist?(parent_dir)
|
933
|
+
|
934
|
+
# Dump the file contents.
|
935
|
+
::File.open(file_path, 'wb') do |f|
|
936
|
+
while buffer = file_data.read(4096) do
|
937
|
+
f.write(buffer)
|
938
|
+
end
|
939
|
+
end
|
940
|
+
|
941
|
+
# Verify that the extracted data is good.
|
942
|
+
begin
|
943
|
+
unless expected_data_descriptor.nil? then
|
944
|
+
expected_data_descriptor.verify(file_data.data_descriptor)
|
945
|
+
end
|
946
|
+
rescue => e
|
947
|
+
raise Zip::ExtractError, "`#{zip_path}': #{e.message}"
|
948
|
+
end
|
949
|
+
|
950
|
+
# Restore the metadata.
|
951
|
+
::File.chmod(mode, file_path) if restore_permissions
|
952
|
+
::File.chown(uid, gid, file_path) if restore_ownerships
|
953
|
+
::File.utime(atime, mtime, file_path) if restore_times
|
954
|
+
|
955
|
+
# Attempt to rewind the file data back to the beginning, but ignore
|
956
|
+
# errors.
|
957
|
+
begin
|
958
|
+
file_data.rewind
|
959
|
+
rescue
|
960
|
+
# Ignore.
|
961
|
+
end
|
962
|
+
|
963
|
+
nil
|
964
|
+
end
|
965
|
+
|
966
|
+
private
|
967
|
+
|
968
|
+
# Write the file data to _io_.
|
969
|
+
def dump_compressed_data(io)
|
970
|
+
while buffer = file_data.read(4096) do io.write(buffer) end
|
971
|
+
|
972
|
+
# Attempt to ensure that the file data will still be in a readable state
|
973
|
+
# at the beginning of the data for the next user, but close it if possible
|
974
|
+
# in order to conserve resources.
|
975
|
+
if file_path.nil? then
|
976
|
+
# When the file_path attribute is not set, the file_data method cannot
|
977
|
+
# reinitialize the IO object it returns, so attempt to rewind the file
|
978
|
+
# data back to the beginning, but ignore errors.
|
979
|
+
begin
|
980
|
+
file_data.rewind
|
981
|
+
rescue
|
982
|
+
# Ignore.
|
983
|
+
end
|
984
|
+
else
|
985
|
+
# Since the file_path attribute is set, the file_data method will
|
986
|
+
# reinitialize the IO object it returns if we close the object here.
|
987
|
+
file_data.close
|
988
|
+
end
|
989
|
+
end
|
990
|
+
end
|
991
|
+
end; end; end
|