archive-zip 0.3.0 → 0.4.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.
Files changed (132) hide show
  1. data/HACKING +25 -42
  2. data/NEWS +25 -0
  3. data/README +2 -2
  4. data/Rakefile +202 -0
  5. data/TODO +5 -0
  6. data/default.mspec +8 -0
  7. data/lib/archive/support/binary_stringio.rb +23 -0
  8. data/lib/archive/support/integer.rb +13 -0
  9. data/lib/archive/support/io-like.rb +3 -1
  10. data/lib/archive/support/ioextensions.rb +16 -0
  11. data/lib/archive/support/iowindow.rb +10 -18
  12. data/lib/archive/support/time.rb +2 -0
  13. data/lib/archive/support/zlib.rb +298 -71
  14. data/lib/archive/zip.rb +161 -139
  15. data/lib/archive/zip/codec.rb +2 -0
  16. data/lib/archive/zip/codec/deflate.rb +59 -11
  17. data/lib/archive/zip/codec/null_encryption.rb +75 -14
  18. data/lib/archive/zip/codec/store.rb +75 -26
  19. data/lib/archive/zip/codec/traditional_encryption.rb +146 -35
  20. data/lib/archive/zip/data_descriptor.rb +6 -4
  21. data/lib/archive/zip/entry.rb +184 -132
  22. data/lib/archive/zip/error.rb +2 -0
  23. data/lib/archive/zip/extra_field.rb +20 -6
  24. data/lib/archive/zip/extra_field/extended_timestamp.rb +141 -60
  25. data/lib/archive/zip/extra_field/raw.rb +70 -12
  26. data/lib/archive/zip/extra_field/unix.rb +58 -16
  27. data/lib/archive/zip/version.rb +6 -0
  28. data/spec/archive/zip/codec/deflate/compress/checksum_spec.rb +42 -0
  29. data/spec/archive/zip/codec/deflate/compress/close_spec.rb +44 -0
  30. data/spec/archive/zip/codec/deflate/compress/crc32_spec.rb +21 -0
  31. data/spec/archive/zip/codec/deflate/compress/data_descriptor_spec.rb +67 -0
  32. data/spec/archive/zip/codec/deflate/compress/new_spec.rb +37 -0
  33. data/spec/archive/zip/codec/deflate/compress/open_spec.rb +46 -0
  34. data/spec/archive/zip/codec/deflate/compress/write_spec.rb +109 -0
  35. data/spec/archive/zip/codec/deflate/decompress/checksum_spec.rb +18 -0
  36. data/spec/archive/zip/codec/deflate/decompress/close_spec.rb +33 -0
  37. data/spec/archive/zip/codec/deflate/decompress/crc32_spec.rb +18 -0
  38. data/spec/archive/zip/codec/deflate/decompress/data_descriptor_spec.rb +67 -0
  39. data/spec/archive/zip/codec/deflate/decompress/new_spec.rb +14 -0
  40. data/spec/archive/zip/codec/deflate/decompress/open_spec.rb +27 -0
  41. data/spec/archive/zip/codec/deflate/fixtures/classes.rb +25 -0
  42. data/spec/archive/zip/codec/deflate/fixtures/compressed_file.bin +1 -0
  43. data/spec/archive/zip/codec/deflate/fixtures/compressed_file_nocomp.bin +0 -0
  44. data/spec/archive/zip/codec/deflate/fixtures/raw_file.txt +10 -0
  45. data/spec/archive/zip/codec/null_encryption/decrypt/close_spec.rb +33 -0
  46. data/spec/archive/zip/codec/null_encryption/decrypt/new_spec.rb +14 -0
  47. data/spec/archive/zip/codec/null_encryption/decrypt/open_spec.rb +27 -0
  48. data/spec/archive/zip/codec/null_encryption/decrypt/read_spec.rb +24 -0
  49. data/spec/archive/zip/codec/null_encryption/decrypt/rewind_spec.rb +25 -0
  50. data/spec/archive/zip/codec/null_encryption/decrypt/seek_spec.rb +57 -0
  51. data/spec/archive/zip/codec/null_encryption/decrypt/tell_spec.rb +21 -0
  52. data/spec/archive/zip/codec/null_encryption/encrypt/close_spec.rb +33 -0
  53. data/spec/archive/zip/codec/null_encryption/encrypt/new_spec.rb +14 -0
  54. data/spec/archive/zip/codec/null_encryption/encrypt/open_spec.rb +27 -0
  55. data/spec/archive/zip/codec/null_encryption/encrypt/rewind_spec.rb +26 -0
  56. data/spec/archive/zip/codec/null_encryption/encrypt/seek_spec.rb +50 -0
  57. data/spec/archive/zip/codec/null_encryption/encrypt/tell_spec.rb +29 -0
  58. data/spec/archive/zip/codec/null_encryption/encrypt/write_spec.rb +29 -0
  59. data/spec/archive/zip/codec/null_encryption/fixtures/classes.rb +12 -0
  60. data/spec/archive/zip/codec/null_encryption/fixtures/raw_file.txt +10 -0
  61. data/spec/archive/zip/codec/store/compress/close_spec.rb +33 -0
  62. data/spec/archive/zip/codec/store/compress/data_descriptor_spec.rb +68 -0
  63. data/spec/archive/zip/codec/store/compress/new_spec.rb +14 -0
  64. data/spec/archive/zip/codec/store/compress/open_spec.rb +27 -0
  65. data/spec/archive/zip/codec/store/compress/rewind_spec.rb +26 -0
  66. data/spec/archive/zip/codec/store/compress/seek_spec.rb +50 -0
  67. data/spec/archive/zip/codec/store/compress/tell_spec.rb +29 -0
  68. data/spec/archive/zip/codec/store/compress/write_spec.rb +29 -0
  69. data/spec/archive/zip/codec/store/decompress/close_spec.rb +33 -0
  70. data/spec/archive/zip/codec/store/decompress/data_descriptor_spec.rb +68 -0
  71. data/spec/archive/zip/codec/store/decompress/new_spec.rb +14 -0
  72. data/spec/archive/zip/codec/store/decompress/open_spec.rb +27 -0
  73. data/spec/archive/zip/codec/store/decompress/read_spec.rb +24 -0
  74. data/spec/archive/zip/codec/store/decompress/rewind_spec.rb +25 -0
  75. data/spec/archive/zip/codec/store/decompress/seek_spec.rb +57 -0
  76. data/spec/archive/zip/codec/store/decompress/tell_spec.rb +21 -0
  77. data/spec/archive/zip/codec/store/fixtures/classes.rb +12 -0
  78. data/spec/archive/zip/codec/store/fixtures/raw_file.txt +10 -0
  79. data/spec/archive/zip/codec/traditional_encryption/decrypt/close_spec.rb +64 -0
  80. data/spec/archive/zip/codec/traditional_encryption/decrypt/new_spec.rb +18 -0
  81. data/spec/archive/zip/codec/traditional_encryption/decrypt/open_spec.rb +39 -0
  82. data/spec/archive/zip/codec/traditional_encryption/decrypt/read_spec.rb +126 -0
  83. data/spec/archive/zip/codec/traditional_encryption/decrypt/rewind_spec.rb +38 -0
  84. data/spec/archive/zip/codec/traditional_encryption/decrypt/seek_spec.rb +82 -0
  85. data/spec/archive/zip/codec/traditional_encryption/decrypt/tell_spec.rb +25 -0
  86. data/spec/archive/zip/codec/traditional_encryption/encrypt/close_spec.rb +64 -0
  87. data/spec/archive/zip/codec/traditional_encryption/encrypt/new_spec.rb +18 -0
  88. data/spec/archive/zip/codec/traditional_encryption/encrypt/open_spec.rb +39 -0
  89. data/spec/archive/zip/codec/traditional_encryption/encrypt/rewind_spec.rb +41 -0
  90. data/spec/archive/zip/codec/traditional_encryption/encrypt/seek_spec.rb +75 -0
  91. data/spec/archive/zip/codec/traditional_encryption/encrypt/tell_spec.rb +42 -0
  92. data/spec/archive/zip/codec/traditional_encryption/encrypt/write_spec.rb +127 -0
  93. data/spec/archive/zip/codec/traditional_encryption/fixtures/classes.rb +27 -0
  94. data/spec/archive/zip/codec/traditional_encryption/fixtures/encrypted_file.bin +0 -0
  95. data/spec/archive/zip/codec/traditional_encryption/fixtures/raw_file.txt +10 -0
  96. data/spec/binary_stringio/new_spec.rb +34 -0
  97. data/spec/binary_stringio/set_encoding_spec.rb +14 -0
  98. data/spec/ioextensions/read_exactly_spec.rb +50 -0
  99. data/spec/zlib/fixtures/classes.rb +65 -0
  100. data/spec/zlib/fixtures/compressed_file.bin +1 -0
  101. data/spec/zlib/fixtures/compressed_file_gzip.bin +0 -0
  102. data/spec/zlib/fixtures/compressed_file_huffman.bin +2 -0
  103. data/spec/zlib/fixtures/compressed_file_minmem.bin +0 -0
  104. data/spec/zlib/fixtures/compressed_file_minwin.bin +1 -0
  105. data/spec/zlib/fixtures/compressed_file_nocomp.bin +0 -0
  106. data/spec/zlib/fixtures/compressed_file_raw.bin +1 -0
  107. data/spec/zlib/fixtures/raw_file.txt +10 -0
  108. data/spec/zlib/zreader/checksum_spec.rb +40 -0
  109. data/spec/zlib/zreader/close_spec.rb +14 -0
  110. data/spec/zlib/zreader/compressed_size_spec.rb +18 -0
  111. data/spec/zlib/zreader/new_spec.rb +41 -0
  112. data/spec/zlib/zreader/open_spec.rb +49 -0
  113. data/spec/zlib/zreader/read_spec.rb +47 -0
  114. data/spec/zlib/zreader/rewind_spec.rb +23 -0
  115. data/spec/zlib/zreader/seek_spec.rb +55 -0
  116. data/spec/zlib/zreader/tell_spec.rb +21 -0
  117. data/spec/zlib/zreader/uncompressed_size_spec.rb +18 -0
  118. data/spec/zlib/zwriter/checksum_spec.rb +41 -0
  119. data/spec/zlib/zwriter/close_spec.rb +14 -0
  120. data/spec/zlib/zwriter/compressed_size_spec.rb +19 -0
  121. data/spec/zlib/zwriter/new_spec.rb +64 -0
  122. data/spec/zlib/zwriter/open_spec.rb +68 -0
  123. data/spec/zlib/zwriter/rewind_spec.rb +26 -0
  124. data/spec/zlib/zwriter/seek_spec.rb +54 -0
  125. data/spec/zlib/zwriter/tell_spec.rb +29 -0
  126. data/spec/zlib/zwriter/uncompressed_size_spec.rb +19 -0
  127. data/spec/zlib/zwriter/write_spec.rb +28 -0
  128. data/spec_helper.rb +49 -0
  129. metadata +296 -74
  130. data/MANIFEST +0 -27
  131. data/lib/archive/support/io.rb +0 -14
  132. data/lib/archive/support/stringio.rb +0 -22
@@ -1,7 +1,9 @@
1
+ # encoding: UTF-8
2
+
1
3
  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.
4
+ # Archive::Zip::DataDescriptor is a convenience class which bundles important
5
+ # information concerning the compressed data in a ZIP archive entry and allows
6
+ # easy comparisons between instances of itself.
5
7
  class DataDescriptor
6
8
  # Create a new instance of this class where <em>crc32</em>,
7
9
  # _compressed_size_, and _uncompressed_size_ are all integers representing a
@@ -41,7 +43,7 @@ module Archive; class Zip
41
43
  end
42
44
 
43
45
  # Writes the data wrapped in this object to _io_ which must be a writable,
44
- # IO-like object prividing a _write_ method. Returns the number of bytes
46
+ # IO-like object providing a _write_ method. Returns the number of bytes
45
47
  # written.
46
48
  def dump(io)
47
49
  io.write(
@@ -1,3 +1,7 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'archive/support/ioextensions'
4
+ require 'archive/support/binary_stringio'
1
5
  require 'archive/zip/codec/deflate'
2
6
  require 'archive/zip/codec/null_encryption'
3
7
  require 'archive/zip/codec/store'
@@ -197,33 +201,26 @@ module Archive; class Zip
197
201
  end
198
202
 
199
203
  # Creates and returns a new entry object by parsing from the current
200
- # position of _io_. _io_ must be a readable, IO-like object which provides
201
- # a _readbytes_ method, and it must be positioned at the start of a central
202
- # file record following the signature for that record.
204
+ # position of _io_. _io_ must be a readable, IO-like object which is
205
+ # positioned at the start of a central file record following the signature
206
+ # for that record.
203
207
  #
204
- # <b>NOTE:</b> For now _io_ MUST be seekable and report such by returning
205
- # +true+ from its <i>seekable?</i> method. See IO#seekable?.
208
+ # <b>NOTE:</b> For now _io_ MUST be seekable.
206
209
  #
207
210
  # Currently, the only entry objects returned are instances of
208
211
  # Archive::Zip::Entry::File, Archive::Zip::Entry::Directory, and
209
212
  # Archive::Zip::Entry::Symlink. Any other kind of entry will be mapped into
210
213
  # an instance of Archive::Zip::Entry::File.
211
214
  #
212
- # Raises Archive::Zip::IOError if _io_ is not seekable. Raises
213
- # Archive::Zip::EntryError for any other errors related to processing the
214
- # entry.
215
+ # Raises Archive::Zip::EntryError for any other errors related to processing
216
+ # the entry.
215
217
  def self.parse(io)
216
- # Error out if the IO object is not confirmed seekable.
217
- unless io.respond_to?(:seekable?) and io.seekable? then
218
- raise Zip::IOError, 'non-seekable IO object given'
219
- end
220
-
221
218
  # Parse the central file record and then use the information found there
222
219
  # to locate and parse the corresponding local file record.
223
220
  cfr = parse_central_file_record(io)
224
221
  next_record_position = io.pos
225
222
  io.seek(cfr.local_header_position)
226
- unless io.readbytes(4) == LFH_SIGNATURE then
223
+ unless IOExtensions.read_exactly(io, 4) == LFH_SIGNATURE then
227
224
  raise Zip::EntryError, 'bad local file header signature'
228
225
  end
229
226
  lfr = parse_local_file_record(io, cfr.compressed_size)
@@ -275,8 +272,6 @@ module Archive; class Zip
275
272
 
276
273
  # Set the expected data descriptor so that extraction can be verified.
277
274
  entry.expected_data_descriptor = expected_data_descriptor
278
- # Record the raw file data for the entry.
279
- entry.raw_data = IOWindow.new(io, io.pos, cfr.compressed_size)
280
275
  # Record the compression codec.
281
276
  entry.compression_codec = compression_codec
282
277
  # Record the encryption codec.
@@ -301,9 +296,9 @@ module Archive; class Zip
301
296
  private
302
297
 
303
298
  # Parses a central file record and returns a CFHRecord instance containing
304
- # the 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 central
306
- # file record following the signature for that record.
299
+ # the parsed data. _io_ must be a readable, IO-like object which is
300
+ # positioned at the start of a central file record following the signature
301
+ # for that record.
307
302
  def self.parse_central_file_record(io)
308
303
  cfr = CFHRecord.new
309
304
 
@@ -321,24 +316,27 @@ module Archive; class Zip
321
316
  cfr.disk_number_start,
322
317
  cfr.internal_file_attributes,
323
318
  cfr.external_file_attributes,
324
- cfr.local_header_position = io.readbytes(42).unpack('vvvvVVVVvvvvvVV')
319
+ cfr.local_header_position =
320
+ IOExtensions.read_exactly(io, 42).unpack('vvvvVVVVvvvvvVV')
325
321
 
326
- cfr.zip_path = io.readbytes(file_name_length)
327
- cfr.extra_fields = parse_extra_fields(io.readbytes(extra_fields_length))
328
- cfr.comment = io.readbytes(comment_length)
322
+ cfr.zip_path = IOExtensions.read_exactly(io, file_name_length)
323
+ cfr.extra_fields = parse_central_extra_fields(
324
+ IOExtensions.read_exactly(io, extra_fields_length)
325
+ )
326
+ cfr.comment = IOExtensions.read_exactly(io, comment_length)
329
327
 
330
328
  # Convert from MSDOS time to Unix time.
331
329
  cfr.mtime = DOSTime.new(dos_mtime).to_time
332
330
 
333
331
  cfr
334
- rescue EOFError, TruncatedDataError
332
+ rescue EOFError
335
333
  raise Zip::EntryError, 'unexpected end of file'
336
334
  end
337
335
 
338
336
  # Parses a local file record and returns a LFHRecord instance containing the
339
- # parsed data. _io_ must be a readable, IO-like object which provides a
340
- # readbytes method, and it must be positioned at the start of a local file
341
- # record following the signature for that record.
337
+ # parsed data. _io_ must be a readable, IO-like object which is positioned
338
+ # at the start of a local file record following the signature for that
339
+ # record.
342
340
  #
343
341
  # If the record to be parsed is flagged to have a trailing data descriptor
344
342
  # record, _expected_compressed_size_ must be set to an integer counting the
@@ -356,10 +354,13 @@ module Archive; class Zip
356
354
  lfr.compressed_size,
357
355
  lfr.uncompressed_size,
358
356
  file_name_length,
359
- extra_fields_length = io.readbytes(26).unpack('vvvVVVVvv')
357
+ extra_fields_length =
358
+ IOExtensions.read_exactly(io, 26).unpack('vvvVVVVvv')
360
359
 
361
- lfr.zip_path = io.readbytes(file_name_length)
362
- lfr.extra_fields = parse_extra_fields(io.readbytes(extra_fields_length))
360
+ lfr.zip_path = IOExtensions.read_exactly(io, file_name_length)
361
+ lfr.extra_fields = parse_local_extra_fields(
362
+ IOExtensions.read_exactly(io, extra_fields_length)
363
+ )
363
364
 
364
365
  # Convert from MSDOS time to Unix time.
365
366
  lfr.mtime = DOSTime.new(dos_mtime).to_time
@@ -371,35 +372,60 @@ module Archive; class Zip
371
372
  # libraries create trailing data descriptor records with a preceding
372
373
  # signature while others do not.
373
374
  # This handles both cases.
374
- possible_signature = io.readbytes(4)
375
+ possible_signature = IOExtensions.read_exactly(io, 4)
375
376
  if possible_signature == DD_SIGNATURE then
376
377
  lfr.crc32,
377
378
  lfr.compressed_size,
378
- lfr.uncompressed_size = io.readbytes(12).unpack('VVV')
379
+ lfr.uncompressed_size =
380
+ IOExtensions.read_exactly(io, 12).unpack('VVV')
379
381
  else
380
382
  lfr.crc32 = possible_signature.unpack('V')[0]
381
383
  lfr.compressed_size,
382
- lfr.uncompressed_size = io.readbytes(8).unpack('VV')
384
+ lfr.uncompressed_size = IOExtensions.read_exactly(io, 8).unpack('VV')
383
385
  end
384
386
  io.pos = saved_pos
385
387
  end
386
388
 
387
389
  lfr
388
- rescue EOFError, TruncatedDataError
390
+ rescue EOFError
389
391
  raise Zip::EntryError, 'unexpected end of file'
390
392
  end
391
393
 
392
- # Parses the extra fields for local and central file records and returns an
393
- # array of extra field objects. _bytes_ must be a String containing all of
394
- # the extra field data to be parsed.
395
- def self.parse_extra_fields(bytes)
396
- StringIO.open(bytes) do |io|
394
+ # Parses the extra fields for central file records and returns an array of
395
+ # extra field objects. _bytes_ must be a String containing all of the extra
396
+ # field data to be parsed.
397
+ def self.parse_central_extra_fields(bytes)
398
+ BinaryStringIO.open(bytes) do |io|
397
399
  extra_fields = []
398
400
  while ! io.eof? do
399
- header_id, data_size = io.readbytes(4).unpack('vv')
400
- data = io.readbytes(data_size)
401
+ begin
402
+ header_id, data_size = IOExtensions.read_exactly(io, 4).unpack('vv')
403
+ data = IOExtensions.read_exactly(io, data_size)
404
+ rescue ::EOFError
405
+ raise EntryError, 'insufficient data available'
406
+ end
407
+
408
+ extra_fields << ExtraField.parse_central(header_id, data)
409
+ end
410
+ extra_fields
411
+ end
412
+ end
413
+
414
+ # Parses the extra fields for local file records and returns an array of
415
+ # extra field objects. _bytes_ must be a String containing all of the extra
416
+ # field data to be parsed.
417
+ def self.parse_local_extra_fields(bytes)
418
+ BinaryStringIO.open(bytes) do |io|
419
+ extra_fields = []
420
+ while ! io.eof? do
421
+ begin
422
+ header_id, data_size = IOExtensions.read_exactly(io, 4).unpack('vv')
423
+ data = IOExtensions.read_exactly(io, data_size)
424
+ rescue ::EOFError
425
+ raise EntryError, 'insufficient data available'
426
+ end
401
427
 
402
- extra_fields << ExtraField.parse(header_id, data)
428
+ extra_fields << ExtraField.parse_local(header_id, data)
403
429
  end
404
430
  extra_fields
405
431
  end
@@ -474,10 +500,10 @@ module Archive; class Zip
474
500
  attr_accessor :mode
475
501
  # The comment associated with this entry.
476
502
  attr_accessor :comment
477
- # An Archive::Zip::Entry::DataDescriptor instance which should contain the
478
- # expected CRC32 checksum, compressed size, and uncompressed size for the
479
- # file data. When not +nil+, this is used by #extract to confirm that the
480
- # data extraction was successful.
503
+ # An Archive::Zip::DataDescriptor instance which should contain the expected
504
+ # CRC32 checksum, compressed size, and uncompressed size for the file data.
505
+ # When not +nil+, this is used by #extract to confirm that the data
506
+ # extraction was successful.
481
507
  attr_accessor :expected_data_descriptor
482
508
  # The selected compression codec.
483
509
  attr_accessor :compression_codec
@@ -521,24 +547,37 @@ module Archive; class Zip
521
547
  false
522
548
  end
523
549
 
524
- # Adds _extra_field_ as an extra field specification to this entry. If
525
- # _extra_field_ is an instance of
550
+ # Adds _extra_field_ as an extra field specification to *both* the central
551
+ # file record and the local file record of this entry.
552
+ #
553
+ # If _extra_field_ is an instance of
526
554
  # Archive::Zip::Entry::ExtraField::ExtendedTimestamp, the values of that
527
555
  # field are used to set mtime and atime for this entry. If _extra_field_ is
528
556
  # an instance of Archive::Zip::Entry::ExtraField::Unix, the values of that
529
557
  # field are used to set mtime, atime, uid, and gid for this entry.
530
558
  def add_extra_field(extra_field)
531
- @extra_field_data = nil
532
- @extra_fields << extra_field
559
+ # Try to find an extra field with the same header ID already in the list
560
+ # and merge the new one with that if one exists; otherwise, add the new
561
+ # one to the list.
562
+ existing_extra_field = @extra_fields.find do |ef|
563
+ ef.header_id == extra_field.header_id
564
+ end
565
+ if existing_extra_field.nil? then
566
+ @extra_fields << extra_field
567
+ else
568
+ extra_field = existing_extra_field.merge(extra_field)
569
+ end
533
570
 
571
+ # Set some attributes of this entry based on the settings in select types
572
+ # of extra fields.
534
573
  if extra_field.kind_of?(ExtraField::ExtendedTimestamp) then
535
- self.mtime = extra_field.mtime
536
- self.atime = extra_field.atime
574
+ self.mtime = extra_field.mtime unless extra_field.mtime.nil?
575
+ self.atime = extra_field.atime unless extra_field.atime.nil?
537
576
  elsif extra_field.kind_of?(ExtraField::Unix) then
538
- self.mtime = extra_field.mtime
539
- self.atime = extra_field.atime
540
- self.uid = extra_field.uid
541
- self.gid = extra_field.gid
577
+ self.mtime = extra_field.mtime unless extra_field.mtime.nil?
578
+ self.atime = extra_field.atime unless extra_field.atime.nil?
579
+ self.uid = extra_field.uid unless extra_field.uid.nil?
580
+ self.gid = extra_field.gid unless extra_field.uid.nil?
542
581
  end
543
582
  self
544
583
  end
@@ -554,21 +593,34 @@ module Archive; class Zip
554
593
  @local_file_record_position = local_file_record_position
555
594
  bytes_written = 0
556
595
 
596
+ # Assume that no trailing data descriptor will be necessary.
597
+ need_trailing_data_descriptor = false
598
+ begin
599
+ io.pos
600
+ rescue Errno::ESPIPE
601
+ # A trailing data descriptor is required for non-seekable IO.
602
+ need_trailing_data_descriptor = true
603
+ end
604
+ if encryption_codec.class == Codec::TraditionalEncryption then
605
+ # HACK:
606
+ # According to the ZIP specification, a trailing data descriptor should
607
+ # only be required when writing to non-seekable IO , but InfoZIP
608
+ # *always* does this when using traditional encryption even though it
609
+ # will also write the data descriptor in the usual place if possible.
610
+ # Failure to emulate InfoZIP in this behavior will prevent InfoZIP
611
+ # compatibility with traditionally encrypted entries.
612
+ need_trailing_data_descriptor = true
613
+ # HACK:
614
+ # The InfoZIP implementation of traditional encryption requires that the
615
+ # the last modified file time be used as part of the encryption header.
616
+ # This is a deviation from the ZIP specification.
617
+ encryption_codec.mtime = mtime
618
+ end
619
+
620
+ # Set the general purpose flags.
557
621
  general_purpose_flags = compression_codec.general_purpose_flags
558
622
  general_purpose_flags |= encryption_codec.general_purpose_flags
559
-
560
- if ! io.seekable? ||
561
- encryption_codec.class == Codec::TraditionalEncryption then
562
- # Flag that the data descriptor record will follow the compressed file
563
- # data of this entry.
564
- #
565
- # HACK:
566
- # According to the ZIP specification, this should only be done if the IO
567
- # object cannot be accessed randomly, but InfoZIP *always* sets this
568
- # flag when using traditional encryption even though it will also write
569
- # the data descriptor in the usual place if possible. Failure to
570
- # emulate InfoZIP in this behavior will prevent InfoZIP compatibility
571
- # with traditionally encrypted entries.
623
+ if need_trailing_data_descriptor then
572
624
  general_purpose_flags |= FLAG_DATA_DESCRIPTOR_FOLLOWS
573
625
  end
574
626
 
@@ -579,7 +631,9 @@ module Archive; class Zip
579
631
  version_needed_to_extract = encryption_codec.version_needed_to_extract
580
632
  end
581
633
 
634
+ # Write the data.
582
635
  bytes_written += io.write(LFH_SIGNATURE)
636
+ extra_field_data = local_extra_field_data
583
637
  bytes_written += io.write(
584
638
  [
585
639
  version_needed_to_extract,
@@ -596,15 +650,6 @@ module Archive; class Zip
596
650
  bytes_written += io.write(zip_path)
597
651
  bytes_written += io.write(extra_field_data)
598
652
 
599
- if encryption_codec.class == Codec::TraditionalEncryption then
600
- # HACK:
601
- # Traditional encryption requires that the last 2 bytes of the header it
602
- # uses are the 2 low order bytes of the last modified file time in DOS
603
- # format. This is different than stated in the ZIP specification and
604
- # rather comes from the InfoZIP implementation.
605
- encryption_codec.mtime = mtime
606
- end
607
-
608
653
  # Pipeline a compressor into an encryptor, write all the file data to the
609
654
  # compressor, and get a data descriptor from it.
610
655
  encryption_codec.encryptor(io, password) do |e|
@@ -619,31 +664,23 @@ module Archive; class Zip
619
664
  end
620
665
  e.close(false)
621
666
  end
622
-
623
667
  bytes_written += @data_descriptor.compressed_size
624
- if io.seekable? then
668
+
669
+ # Write the trailing data descriptor if necessary.
670
+ if need_trailing_data_descriptor then
671
+ bytes_written += io.write(DD_SIGNATURE)
672
+ bytes_written += @data_descriptor.dump(io)
673
+ end
674
+
675
+ begin
625
676
  # Update the data descriptor located before the compressed data for the
626
677
  # entry.
627
678
  saved_position = io.pos
628
679
  io.pos = @local_file_record_position + 14
629
680
  @data_descriptor.dump(io)
630
681
  io.pos = saved_position
631
- end
632
-
633
- if ! io.seekable? ||
634
- encryption_codec.class == Codec::TraditionalEncryption then
635
- # Write the data descriptor after the compressed file data for the
636
- # entry.
637
- #
638
- # HACK:
639
- # According to the ZIP specification, this should only be done if the IO
640
- # object cannot be accessed randomly, but InfoZIP *always* does this
641
- # when using traditional encryption even though it will also write the
642
- # data descriptor in the usual place if possible. Failure to emulate
643
- # InfoZIP in this behavior will prevent InfoZIP compatibility with
644
- # traditionally encrypted entries.
645
- bytes_written += io.write(DD_SIGNATURE)
646
- bytes_written += @data_descriptor.dump(io)
682
+ rescue Errno::ESPIPE
683
+ # Ignore a failed attempt to update the data descriptor.
647
684
  end
648
685
 
649
686
  bytes_written
@@ -657,21 +694,29 @@ module Archive; class Zip
657
694
  def dump_central_file_record(io)
658
695
  bytes_written = 0
659
696
 
697
+ # Assume that no trailing data descriptor will be necessary.
698
+ need_trailing_data_descriptor = false
699
+ begin
700
+ io.pos
701
+ rescue Errno::ESPIPE
702
+ # A trailing data descriptor is required for non-seekable IO.
703
+ need_trailing_data_descriptor = true
704
+ end
705
+ if encryption_codec.class == Codec::TraditionalEncryption then
706
+ # HACK:
707
+ # According to the ZIP specification, a trailing data descriptor should
708
+ # only be required when writing to non-seekable IO , but InfoZIP
709
+ # *always* does this when using traditional encryption even though it
710
+ # will also write the data descriptor in the usual place if possible.
711
+ # Failure to emulate InfoZIP in this behavior will prevent InfoZIP
712
+ # compatibility with traditionally encrypted entries.
713
+ need_trailing_data_descriptor = true
714
+ end
715
+
716
+ # Set the general purpose flags.
660
717
  general_purpose_flags = compression_codec.general_purpose_flags
661
718
  general_purpose_flags |= encryption_codec.general_purpose_flags
662
-
663
- if ! io.seekable? ||
664
- encryption_codec.class == Codec::TraditionalEncryption then
665
- # Flag that the data descriptor record will follow the compressed file
666
- # data of this entry.
667
- #
668
- # HACK:
669
- # According to the ZIP specification, this should only be done if the IO
670
- # object cannot be accessed randomly, but InfoZIP *always* sets this
671
- # flag when using traditional encryption even though it will also write
672
- # the data descriptor in the usual place if possible. Failure to
673
- # emulate InfoZIP in this behavior will prevent InfoZIP compatibility
674
- # with encrypted entries.
719
+ if need_trailing_data_descriptor then
675
720
  general_purpose_flags |= FLAG_DATA_DESCRIPTOR_FOLLOWS
676
721
  end
677
722
 
@@ -682,6 +727,7 @@ module Archive; class Zip
682
727
  version_needed_to_extract = encryption_codec.version_needed_to_extract
683
728
  end
684
729
 
730
+ # Write the data.
685
731
  bytes_written += io.write(CFH_SIGNATURE)
686
732
  bytes_written += io.write(
687
733
  [
@@ -693,6 +739,7 @@ module Archive; class Zip
693
739
  ].pack('vvvvV')
694
740
  )
695
741
  bytes_written += @data_descriptor.dump(io)
742
+ extra_field_data = central_extra_field_data
696
743
  bytes_written += io.write(
697
744
  [
698
745
  zip_path.length,
@@ -717,19 +764,31 @@ module Archive; class Zip
717
764
  0x0314
718
765
  end
719
766
 
720
- def extra_field_data
721
- return @extra_field_data unless @extra_field_data.nil?
767
+ def central_extra_field_data
768
+ @central_extra_field_data = @extra_fields.collect do |extra_field|
769
+ extra_field.dump_central
770
+ end.join
771
+ end
722
772
 
723
- @extra_field_data = @extra_fields.collect do |extra_field|
724
- unless extra_field.kind_of?(ExtraField::ExtendedTimestamp) ||
725
- extra_field.kind_of?(ExtraField::Unix) then
726
- extra_field.dump
727
- else
728
- ''
729
- end
730
- end.join +
731
- ExtraField::ExtendedTimestamp.new(mtime, atime, nil).dump +
732
- ExtraField::Unix.new(mtime, atime, uid, gid).dump
773
+ def dummy
774
+ # Add fields for time data if available.
775
+ unless mtime.nil? && atime.nil? then
776
+ @central_extra_field_data +=
777
+ ExtraField::ExtendedTimestamp.new(mtime, atime, nil).dump_central
778
+ end
779
+
780
+ # Add fields for user and group ownerships if available.
781
+ unless uid.nil? || gid.nil? || mtime.nil? || atime.nil? then
782
+ @central_extra_field_data += ExtraField::Unix.new(
783
+ mtime, atime, uid, gid
784
+ ).dump_central
785
+ end
786
+ end
787
+
788
+ def local_extra_field_data
789
+ @local_extra_field_data = @extra_fields.collect do |extra_field|
790
+ extra_field.dump_local
791
+ end.join
733
792
  end
734
793
 
735
794
  def internal_file_attributes
@@ -1012,7 +1071,7 @@ module Archive; class Zip; module Entry
1012
1071
  )
1013
1072
  else
1014
1073
  if @file_path.nil? then
1015
- simulated_raw_data = StringIO.new
1074
+ simulated_raw_data = BinaryStringIO.new
1016
1075
  else
1017
1076
  simulated_raw_data = ::File.new(@file_path, 'rb')
1018
1077
  end
@@ -1023,22 +1082,15 @@ module Archive; class Zip; module Entry
1023
1082
  @file_data
1024
1083
  end
1025
1084
 
1026
- # Sets the +file_data+ attribute of this object to _file_data_. If
1027
- # _file_data_ is a String, it will be wrapped in a StringIO instance;
1028
- # otherwise, _file_data_ must be a readable, IO-like object. _file_data_ is
1029
- # then wrapped inside an Archive::Zip::Codec::Store::Unstore instance before
1030
- # finally setting the +file_data+ attribute.
1085
+ # Sets the +file_data+ attribute of this object to _file_data_. _file_data_
1086
+ # must be a readable, IO-like object.
1031
1087
  #
1032
1088
  # <b>NOTE:</b> As a side effect, the +file_path+ and +raw_data+ attributes
1033
1089
  # for this object will be set to +nil+.
1034
1090
  def file_data=(file_data)
1035
1091
  @file_path = nil
1036
1092
  self.raw_data = nil
1037
- if file_data.kind_of?(String)
1038
- @file_data = StringIO.new(file_data)
1039
- else
1040
- @file_data = file_data
1041
- end
1093
+ @file_data = file_data
1042
1094
  # Ensure that the IO-like object can return CRC32 and data size
1043
1095
  # information so that it's possible to verify extraction later if desired.
1044
1096
  unless @file_data.respond_to?(:data_descriptor) then