archive-zip 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -21,17 +21,19 @@ module Archive; class Zip
21
21
  # A count of the number of bytes of a set of uncompressed data.
22
22
  attr_reader :uncompressed_size
23
23
 
24
- # Compares the attributes of this object with like-named attributes of
25
- # _other_ and raises Archive::Zip::Error for any mismatches.
24
+ # Compares the crc32 and uncompressed_size attributes of this object with
25
+ # like-named attributes of _other_ and raises Archive::Zip::Error for any
26
+ # mismatches.
27
+ #
28
+ # NOTE: The compressed_size attribute is not checked because encrypted
29
+ # entries may have misleading compressed sizes. Checking only the CRC32 and
30
+ # uncompressed size of the data should be sufficient to ensure that an entry
31
+ # has been successfully extracted.
26
32
  def verify(other)
27
33
  unless crc32 == other.crc32 then
28
34
  raise Zip::Error,
29
35
  "CRC32 mismatch: #{crc32.to_s(16)} vs. #{other.crc32.to_s(16)}"
30
36
  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
37
  unless uncompressed_size == other.uncompressed_size then
36
38
  raise Zip::Error,
37
39
  "uncompressed size mismatch: #{uncompressed_size} vs. #{other.uncompressed_size}"
@@ -1,7 +1,10 @@
1
+ require 'archive/zip/codec/deflate'
2
+ require 'archive/zip/codec/null_encryption'
1
3
  require 'archive/zip/codec/store'
4
+ require 'archive/zip/codec/traditional_encryption'
5
+ require 'archive/zip/data_descriptor'
2
6
  require 'archive/zip/error'
3
- require 'archive/zip/extrafield'
4
- require 'archive/zip/datadescriptor'
7
+ require 'archive/zip/extra_field'
5
8
 
6
9
  module Archive; class Zip
7
10
  # The Archive::Zip::Entry mixin provides classes with methods implementing
@@ -12,8 +15,8 @@ module Archive; class Zip
12
15
  # overridden to provide sensible information for the new entry type.
13
16
  #
14
17
  # 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:
18
+ # _dump_file_data_. _extract_ should be a public method with the following
19
+ # signature:
17
20
  #
18
21
  # def extract(options = {})
19
22
  # ...
@@ -26,10 +29,9 @@ module Archive; class Zip
26
29
  # Archive::Zip::Entry::Directory#extract for examples of the options currently
27
30
  # supported.
28
31
  #
29
- # _dump_compressed_data_ should be a private method with the following
30
- # signature:
32
+ # _dump_file_data_ should be a private method with the following signature:
31
33
  #
32
- # def dump_compressed_data(io)
34
+ # def dump_file_data(io)
33
35
  # ...
34
36
  # end
35
37
  #
@@ -74,6 +76,10 @@ module Archive; class Zip
74
76
  :compressed_data
75
77
  )
76
78
 
79
+ # When this flag is set in the general purpose flags, it indicates that the
80
+ # entry's file data is encrypted using the original (weak) algorithm.
81
+ FLAG_ENCRYPTED = 0b0001
82
+
77
83
  # When this flag is set in the general purpose flags, it indicates that the
78
84
  # read data descriptor record for a local file record is located after the
79
85
  # entry's file data.
@@ -108,9 +114,18 @@ module Archive; class Zip
108
114
  # <b>:follow_symlinks</b>::
109
115
  # When set to +true+ (the default), symlinks are treated as the files or
110
116
  # 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.
117
+ # <b>:compression_codec</b>::
118
+ # Specifies a proc, lambda, or class. If a proc or lambda is used, it
119
+ # must take a single argument containing a zip entry and return a
120
+ # compression codec class to be instantiated and used with the entry.
121
+ # Otherwise, a compression codec class must be specified directly. When
122
+ # unset, the default compression codec for each entry type is used.
123
+ # <b>:encryption_codec</b>::
124
+ # Specifies a proc, lambda, or class. If a proc or lambda is used, it
125
+ # must take a single argument containing a zip entry and return an
126
+ # encryption codec class to be instantiated and used with the entry.
127
+ # Otherwise, an encryption codec class must be specified directly. When
128
+ # unset, the default encryption codec for each entry type is used.
114
129
  #
115
130
  # Raises Archive::Zip::EntryError if processing the given file path results
116
131
  # in a file not found error.
@@ -141,19 +156,37 @@ module Archive; class Zip
141
156
  zip_path += '/'
142
157
  end
143
158
 
159
+ # Instantiate the entry.
144
160
  if stat.symlink? then
145
161
  entry = Entry::Symlink.new(zip_path)
146
162
  entry.link_target = ::File.readlink(file_path)
147
163
  elsif stat.file? then
148
164
  entry = Entry::File.new(zip_path)
149
165
  entry.file_path = file_path
150
- entry.codec = options[:codec] unless options[:codec].nil?
151
166
  elsif stat.directory? then
152
167
  entry = Entry::Directory.new(zip_path)
153
168
  else
154
169
  raise Zip::EntryError,
155
170
  "unsupported file type `#{stat.ftype}' for file `#{file_path}'"
156
171
  end
172
+
173
+ # Set the compression and encryption codecs.
174
+ unless options[:compression_codec].nil? then
175
+ if options[:compression_codec].kind_of?(Proc) then
176
+ entry.compression_codec = options[:compression_codec][entry].new
177
+ else
178
+ entry.compression_codec = options[:compression_codec].new
179
+ end
180
+ end
181
+ unless options[:encryption_codec].nil? then
182
+ if options[:encryption_codec].kind_of?(Proc) then
183
+ entry.encryption_codec = options[:encryption_codec][entry].new
184
+ else
185
+ entry.encryption_codec = options[:encryption_codec].new
186
+ end
187
+ end
188
+
189
+ # Set the entry's metadata.
157
190
  entry.uid = stat.uid
158
191
  entry.gid = stat.gid
159
192
  entry.mtime = stat.mtime
@@ -200,52 +233,54 @@ module Archive; class Zip
200
233
  # same.
201
234
  compare_file_records(lfr, cfr)
202
235
 
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"
236
+ begin
237
+ # Load the correct compression codec.
238
+ compression_codec = Codec.create_compression_codec(
239
+ cfr.compression_method,
240
+ cfr.general_purpose_flags
241
+ )
242
+ rescue Zip::Error => e
243
+ raise Zip::EntryError, "`#{cfr.zip_path}': #{e.message}"
244
+ end
245
+
246
+ begin
247
+ # Load the correct encryption codec.
248
+ encryption_codec = Codec.create_encryption_codec(
249
+ cfr.general_purpose_flags
250
+ )
251
+ rescue Zip::Error => e
252
+ raise Zip::EntryError, "`#{cfr.zip_path}': #{e.message}"
207
253
  end
208
254
 
209
- # Load the correct codec.
210
- codec = Codec.create(cfr.compression_method, cfr.general_purpose_flags)
211
255
  # Set up a data descriptor with expected values for later comparison.
212
- data_descriptor = DataDescriptor.new(
256
+ expected_data_descriptor = DataDescriptor.new(
213
257
  cfr.crc32,
214
258
  cfr.compressed_size,
215
259
  cfr.uncompressed_size
216
260
  )
261
+
217
262
  # Create the entry.
218
263
  expanded_path = expand_path(cfr.zip_path)
264
+ io_window = IOWindow.new(io, io.pos, cfr.compressed_size)
219
265
  if cfr.zip_path[-1..-1] == '/' then
220
266
  # 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)
267
+ entry = Entry::Directory.new(expanded_path, io_window)
227
268
  elsif (cfr.external_file_attributes >> 16) & 0770000 == 0120000 then
228
269
  # 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
270
+ entry = Entry::Symlink.new(expanded_path, io_window)
240
271
  else
241
272
  # 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
273
+ entry = Entry::File.new(expanded_path, io_window)
247
274
  end
248
275
 
276
+ # Set the expected data descriptor so that extraction can be verified.
277
+ 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
+ # Record the compression codec.
281
+ entry.compression_codec = compression_codec
282
+ # Record the encryption codec.
283
+ entry.encryption_codec = encryption_codec
249
284
  # Set some entry metadata.
250
285
  entry.mtime = cfr.mtime
251
286
  # Only set mode bits for the entry if the external file attributes are
@@ -405,8 +440,11 @@ module Archive; class Zip
405
440
  public
406
441
 
407
442
  # 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)
443
+ # method. The zip path is initialized to _zip_path_. _raw_data_, if
444
+ # specified, must be a readable, IO-like object containing possibly
445
+ # compressed/encrypted file data for the entry. It is intended to be used
446
+ # primarily by the parse class method.
447
+ def initialize(zip_path, raw_data = nil)
410
448
  self.zip_path = zip_path
411
449
  self.mtime = Time.now
412
450
  self.atime = @mtime
@@ -414,7 +452,11 @@ module Archive; class Zip
414
452
  self.gid = nil
415
453
  self.mode = 0777
416
454
  self.comment = ''
417
- self.codec = Zip::Codec::Store.new
455
+ self.expected_data_descriptor = nil
456
+ self.compression_codec = Zip::Codec::Store.new
457
+ self.encryption_codec = Zip::Codec::NullEncryption.new
458
+ self.password = nil
459
+ @raw_data = raw_data
418
460
  @extra_fields = []
419
461
  end
420
462
 
@@ -428,12 +470,24 @@ module Archive; class Zip
428
470
  attr_accessor :uid
429
471
  # The group ID of the owner of this entry.
430
472
  attr_accessor :gid
431
- # The the file mode/permission bits for this entry.
473
+ # The file mode/permission bits for this entry.
432
474
  attr_accessor :mode
433
475
  # The comment associated with this entry.
434
476
  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.
481
+ attr_accessor :expected_data_descriptor
435
482
  # The selected compression codec.
436
- attr_accessor :codec
483
+ attr_accessor :compression_codec
484
+ # The selected encryption codec.
485
+ attr_accessor :encryption_codec
486
+ # The password used with the encryption codec to encrypt or decrypt the file
487
+ # data for an entry.
488
+ attr_accessor :password
489
+ # The raw, possibly compressed and/or encrypted file data for an entry.
490
+ attr_accessor :raw_data
437
491
 
438
492
  # Sets the path in the archive for this entry to _zip_path_ after passing it
439
493
  # through Archive::Zip::Entry.expand_path and ensuring that the result is
@@ -467,13 +521,6 @@ module Archive; class Zip
467
521
  false
468
522
  end
469
523
 
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
524
  # Adds _extra_field_ as an extra field specification to this entry. If
478
525
  # _extra_field_ is an instance of
479
526
  # Archive::Zip::Entry::ExtraField::ExtendedTimestamp, the values of that
@@ -507,17 +554,37 @@ module Archive; class Zip
507
554
  @local_file_record_position = local_file_record_position
508
555
  bytes_written = 0
509
556
 
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?
557
+ general_purpose_flags = compression_codec.general_purpose_flags
558
+ 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.
572
+ general_purpose_flags |= FLAG_DATA_DESCRIPTOR_FOLLOWS
573
+ end
574
+
575
+ # Select the minimum ZIP specification version needed to extract this
576
+ # entry.
577
+ version_needed_to_extract = compression_codec.version_needed_to_extract
578
+ if encryption_codec.version_needed_to_extract > version_needed_to_extract then
579
+ version_needed_to_extract = encryption_codec.version_needed_to_extract
580
+ end
514
581
 
515
582
  bytes_written += io.write(LFH_SIGNATURE)
516
583
  bytes_written += io.write(
517
584
  [
518
- codec.version_needed_to_extract,
585
+ version_needed_to_extract,
519
586
  general_purpose_flags,
520
- codec.compression_method,
587
+ compression_codec.compression_method,
521
588
  mtime.to_dos_time.to_i,
522
589
  0,
523
590
  0,
@@ -529,21 +596,52 @@ module Archive; class Zip
529
596
  bytes_written += io.write(zip_path)
530
597
  bytes_written += io.write(extra_field_data)
531
598
 
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
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
+ # Pipeline a compressor into an encryptor, write all the file data to the
609
+ # compressor, and get a data descriptor from it.
610
+ encryption_codec.encryptor(io, password) do |e|
611
+ compression_codec.compressor(e) do |c|
612
+ dump_file_data(c)
613
+ c.close(false)
614
+ @data_descriptor = DataDescriptor.new(
615
+ c.data_descriptor.crc32,
616
+ c.data_descriptor.compressed_size + encryption_codec.header_size,
617
+ c.data_descriptor.uncompressed_size
618
+ )
619
+ end
620
+ e.close(false)
538
621
  end
539
622
 
540
623
  bytes_written += @data_descriptor.compressed_size
541
624
  if io.seekable? then
625
+ # Update the data descriptor located before the compressed data for the
626
+ # entry.
542
627
  saved_position = io.pos
543
628
  io.pos = @local_file_record_position + 14
544
629
  @data_descriptor.dump(io)
545
630
  io.pos = saved_position
546
- else
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.
547
645
  bytes_written += io.write(DD_SIGNATURE)
548
646
  bytes_written += @data_descriptor.dump(io)
549
647
  end
@@ -559,18 +657,38 @@ module Archive; class Zip
559
657
  def dump_central_file_record(io)
560
658
  bytes_written = 0
561
659
 
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?
660
+ general_purpose_flags = compression_codec.general_purpose_flags
661
+ 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.
675
+ general_purpose_flags |= FLAG_DATA_DESCRIPTOR_FOLLOWS
676
+ end
677
+
678
+ # Select the minimum ZIP specification version needed to extract this
679
+ # entry.
680
+ version_needed_to_extract = compression_codec.version_needed_to_extract
681
+ if encryption_codec.version_needed_to_extract > version_needed_to_extract then
682
+ version_needed_to_extract = encryption_codec.version_needed_to_extract
683
+ end
566
684
 
567
685
  bytes_written += io.write(CFH_SIGNATURE)
568
686
  bytes_written += io.write(
569
687
  [
570
688
  version_made_by,
571
- codec.version_needed_to_extract,
689
+ version_needed_to_extract,
572
690
  general_purpose_flags,
573
- codec.compression_method,
691
+ compression_codec.compression_method,
574
692
  mtime.to_dos_time.to_i
575
693
  ].pack('vvvvV')
576
694
  )
@@ -700,8 +818,8 @@ module Archive; class Zip; module Entry
700
818
 
701
819
  private
702
820
 
703
- # Directory entries do not have compressed data to write, so do nothing.
704
- def dump_compressed_data(io)
821
+ # Directory entries do not have file data to write, so do nothing.
822
+ def dump_file_data(io)
705
823
  end
706
824
  end
707
825
  end; end; end
@@ -712,9 +830,6 @@ module Archive; class Zip; module Entry
712
830
  class Symlink
713
831
  include Archive::Zip::Entry
714
832
 
715
- # A string indicating the target of a symlink.
716
- attr_accessor :link_target
717
-
718
833
  # Returns the file type of this entry as the symbol <tt>:symlink</tt>.
719
834
  def ftype
720
835
  :symlink
@@ -731,6 +846,37 @@ module Archive; class Zip; module Entry
731
846
  super(0120000 | (mode & 07777))
732
847
  end
733
848
 
849
+ # Returns the link target for this entry.
850
+ #
851
+ # Raises Archive::Zip::EntryError if decoding the link target from an
852
+ # archive is required but fails.
853
+ def link_target
854
+ return @link_target unless @link_target.nil?
855
+
856
+ raw_data.rewind
857
+ encryption_codec.decryptor(raw_data, password) do |decryptor|
858
+ compression_codec.decompressor(decryptor) do |decompressor|
859
+ @link_target = decompressor.read
860
+ # Verify that the extracted data is good.
861
+ begin
862
+ unless expected_data_descriptor.nil? then
863
+ expected_data_descriptor.verify(decompressor.data_descriptor)
864
+ end
865
+ rescue => e
866
+ raise Zip::EntryError, "`#{zip_path}': #{e.message}"
867
+ end
868
+ end
869
+ end
870
+ @link_target
871
+ end
872
+
873
+ # Sets the link target for this entry. As a side effect, the raw_data
874
+ # attribute is set to +nil+.
875
+ def link_target=(link_target)
876
+ raw_data = nil
877
+ @link_target = link_target
878
+ end
879
+
734
880
  # Extracts this entry.
735
881
  #
736
882
  # _options_ is a Hash optionally containing the following:
@@ -739,17 +885,18 @@ module Archive; class Zip; module Entry
739
885
  # the zip path of this entry.
740
886
  # <b>:permissions</b>::
741
887
  # When set to +false+ (the default), POSIX mode/permission bits will be
742
- # ignored. Otherwise, they will be restored if possible.
888
+ # ignored. Otherwise, they will be restored if possible. Not supported
889
+ # on all platforms.
743
890
  # <b>:ownerships</b>::
744
891
  # When set to +false+ (the default), user and group ownerships will be
745
892
  # ignored. On most systems, only a superuser is able to change
746
893
  # ownerships, so setting this option to +true+ as a regular user may have
747
- # no effect.
894
+ # no effect. Not supported on all platforms.
748
895
  #
749
- # Raises Archive::Zip::ExtractError if the link_target attribute is not
896
+ # Raises Archive::Zip::EntryError if the link_target attribute is not
750
897
  # specified.
751
898
  def extract(options = {})
752
- raise Zip::ExtractError, 'link_target is nil' if link_target.nil?
899
+ raise Zip::EntryError, 'link_target is nil' if link_target.nil?
753
900
 
754
901
  # Ensure that unspecified options have default values.
755
902
  file_path = options.has_key?(:file_path) ?
@@ -789,7 +936,7 @@ module Archive; class Zip; module Entry
789
936
  private
790
937
 
791
938
  # Write the link target to _io_ as the file data for the entry.
792
- def dump_compressed_data(io)
939
+ def dump_file_data(io)
793
940
  io.write(@link_target)
794
941
  end
795
942
  end
@@ -803,40 +950,75 @@ module Archive; class Zip; module Entry
803
950
  # Creates a new file entry where _zip_path_ is the path to the entry in the
804
951
  # ZIP archive. The Archive::Zip::Codec::Deflate codec with the default
805
952
  # compression level set (NORMAL) is used by default for compression.
806
- def initialize(zip_path)
807
- super(zip_path)
953
+ # _raw_data_, if specified, must be a readable, IO-like object containing
954
+ # possibly compressed/encrypted file data for the entry. It is intended to
955
+ # be used primarily by the Archive::Zip::Entry.parse class method.
956
+ def initialize(zip_path, raw_data = nil)
957
+ super(zip_path, raw_data)
808
958
  @file_path = nil
809
959
  @file_data = nil
810
- @expected_data_descriptor = nil
811
- @codec = Zip::Codec::Deflate.new
960
+ @compression_codec = Zip::Codec::Deflate.new
812
961
  end
813
962
 
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
963
+ # Returns the file type of this entry as the symbol <tt>:file</tt>.
964
+ def ftype
965
+ :file
966
+ end
967
+
968
+ # Returns +true+.
969
+ def file?
970
+ true
971
+ end
972
+
973
+ # Overridden in order to ensure that the proper mode bits are set for a
974
+ # file.
975
+ def mode=(mode)
976
+ super(0100000 | (mode & 07777))
977
+ end
819
978
 
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.
979
+ # Sets the decryption password.
980
+ def password=(password)
981
+ unless @raw_data.nil? then
982
+ @file_data = nil
983
+ end
984
+ @password = password
985
+ end
986
+
987
+ # The path to a file whose contents are to be used for uncompressed file
988
+ # data. This will be +nil+ if the +file_data+ attribute is set directly.
989
+ attr_reader :file_path
990
+
991
+ # Sets the +file_path+ attribute to _file_path_ which should be a String
992
+ # usable with File#new to open a file for reading which will provide the
993
+ # IO-like object for the +file_data+ attribute.
994
+ def file_path=(file_path)
995
+ @file_data = nil
996
+ @raw_data = nil
997
+ @file_path = file_path
998
+ end
999
+
1000
+ # Returns a readable, IO-like object containing uncompressed file data.
825
1001
  #
826
1002
  # <b>NOTE:</b> It is the responsibility of the user of this attribute to
827
1003
  # ensure that the #close method of the returned IO-like object is called
828
1004
  # when the object is no longer needed.
829
1005
  def file_data
830
- if @file_data.nil? || @file_data.closed? then
1006
+ return @file_data unless @file_data.nil? || @file_data.closed?
1007
+
1008
+ unless raw_data.nil? then
1009
+ raw_data.rewind
1010
+ @file_data = compression_codec.decompressor(
1011
+ encryption_codec.decryptor(raw_data, password)
1012
+ )
1013
+ else
831
1014
  if @file_path.nil? then
832
- @file_data = StringIO.new
1015
+ simulated_raw_data = StringIO.new
833
1016
  else
834
- @file_data = ::File.new(@file_path, 'rb')
1017
+ simulated_raw_data = ::File.new(@file_path, 'rb')
835
1018
  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)
1019
+ # Ensure that the IO-like object can return a data descriptor so that
1020
+ # it's possible to verify extraction later if desired.
1021
+ @file_data = Zip::Codec::Store.new.decompressor(simulated_raw_data)
840
1022
  end
841
1023
  @file_data
842
1024
  end
@@ -847,10 +1029,11 @@ module Archive; class Zip; module Entry
847
1029
  # then wrapped inside an Archive::Zip::Codec::Store::Unstore instance before
848
1030
  # finally setting the +file_data+ attribute.
849
1031
  #
850
- # <b>NOTE:</b> As a side effect, the +file_path+ attribute for this object
851
- # will be set to +nil+.
1032
+ # <b>NOTE:</b> As a side effect, the +file_path+ and +raw_data+ attributes
1033
+ # for this object will be set to +nil+.
852
1034
  def file_data=(file_data)
853
1035
  @file_path = nil
1036
+ self.raw_data = nil
854
1037
  if file_data.kind_of?(String)
855
1038
  @file_data = StringIO.new(file_data)
856
1039
  else
@@ -864,34 +1047,6 @@ module Archive; class Zip; module Entry
864
1047
  @file_data
865
1048
  end
866
1049
 
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
1050
  # Extracts this entry.
896
1051
  #
897
1052
  # _options_ is a Hash optionally containing the following:
@@ -910,7 +1065,7 @@ module Archive; class Zip; module Entry
910
1065
  # When set to +false+ (the default), last accessed and last modified times
911
1066
  # will be ignored. Otherwise, they will be restored if possible.
912
1067
  #
913
- # Raises Archive::Zip::ExtractError if the extracted file data appears
1068
+ # Raises Archive::Zip::EntryError if the extracted file data appears
914
1069
  # corrupt.
915
1070
  def extract(options = {})
916
1071
  # Ensure that unspecified options have default values.
@@ -944,7 +1099,7 @@ module Archive; class Zip; module Entry
944
1099
  expected_data_descriptor.verify(file_data.data_descriptor)
945
1100
  end
946
1101
  rescue => e
947
- raise Zip::ExtractError, "`#{zip_path}': #{e.message}"
1102
+ raise Zip::EntryError, "`#{zip_path}': #{e.message}"
948
1103
  end
949
1104
 
950
1105
  # Restore the metadata.
@@ -966,7 +1121,7 @@ module Archive; class Zip; module Entry
966
1121
  private
967
1122
 
968
1123
  # Write the file data to _io_.
969
- def dump_compressed_data(io)
1124
+ def dump_file_data(io)
970
1125
  while buffer = file_data.read(4096) do io.write(buffer) end
971
1126
 
972
1127
  # Attempt to ensure that the file data will still be in a readable state