archive-zip 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|