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,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