rubyzip 2.0.0 → 2.4.1

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -9
  3. data/Rakefile +3 -0
  4. data/lib/zip/central_directory.rb +9 -5
  5. data/lib/zip/constants.rb +52 -0
  6. data/lib/zip/crypto/decrypted_io.rb +40 -0
  7. data/lib/zip/crypto/traditional_encryption.rb +9 -9
  8. data/lib/zip/decompressor.rb +19 -1
  9. data/lib/zip/dos_time.rb +24 -12
  10. data/lib/zip/entry.rb +107 -49
  11. data/lib/zip/entry_set.rb +2 -0
  12. data/lib/zip/errors.rb +1 -0
  13. data/lib/zip/extra_field/generic.rb +10 -9
  14. data/lib/zip/extra_field/ntfs.rb +5 -1
  15. data/lib/zip/extra_field/old_unix.rb +3 -1
  16. data/lib/zip/extra_field/universal_time.rb +42 -12
  17. data/lib/zip/extra_field/unix.rb +3 -1
  18. data/lib/zip/extra_field/zip64.rb +5 -3
  19. data/lib/zip/extra_field.rb +11 -9
  20. data/lib/zip/file.rb +142 -65
  21. data/lib/zip/filesystem.rb +193 -177
  22. data/lib/zip/inflater.rb +24 -36
  23. data/lib/zip/input_stream.rb +50 -30
  24. data/lib/zip/ioextras/abstract_input_stream.rb +23 -12
  25. data/lib/zip/ioextras/abstract_output_stream.rb +1 -1
  26. data/lib/zip/ioextras.rb +3 -3
  27. data/lib/zip/null_decompressor.rb +1 -9
  28. data/lib/zip/output_stream.rb +28 -12
  29. data/lib/zip/pass_thru_compressor.rb +2 -2
  30. data/lib/zip/pass_thru_decompressor.rb +13 -22
  31. data/lib/zip/streamable_directory.rb +3 -3
  32. data/lib/zip/streamable_stream.rb +6 -10
  33. data/lib/zip/version.rb +1 -1
  34. data/lib/zip.rb +22 -2
  35. data/samples/example.rb +2 -2
  36. data/samples/example_filesystem.rb +1 -1
  37. data/samples/gtk_ruby_zip.rb +19 -19
  38. data/samples/qtzip.rb +6 -6
  39. data/samples/write_simple.rb +2 -4
  40. data/samples/zipfind.rb +23 -22
  41. metadata +52 -31
data/lib/zip/entry.rb CHANGED
@@ -34,7 +34,7 @@ module Zip
34
34
  end
35
35
  @follow_symlinks = false
36
36
 
37
- @restore_times = true
37
+ @restore_times = false
38
38
  @restore_permissions = false
39
39
  @restore_ownership = false
40
40
  # BUG: need an extra field to support uid/gid's
@@ -48,28 +48,52 @@ module Zip
48
48
 
49
49
  def check_name(name)
50
50
  return unless name.start_with?('/')
51
+
51
52
  raise ::Zip::EntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
52
53
  end
53
54
 
54
- def initialize(*args)
55
- name = args[1] || ''
55
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
56
+ def initialize(zipfile = nil, name = nil, *args)
57
+ name ||= ''
56
58
  check_name(name)
57
59
 
58
60
  set_default_vars_values
59
61
  @fstype = ::Zip::RUNNING_ON_WINDOWS ? ::Zip::FSTYPE_FAT : ::Zip::FSTYPE_UNIX
60
62
 
61
- @zipfile = args[0] || ''
63
+ @zipfile = zipfile || ''
62
64
  @name = name
63
- @comment = args[2] || ''
64
- @extra = args[3] || ''
65
- @compressed_size = args[4] || 0
66
- @crc = args[5] || 0
67
- @compression_method = args[6] || ::Zip::Entry::DEFLATED
68
- @size = args[7] || 0
69
- @time = args[8] || ::Zip::DOSTime.now
65
+
66
+ if (args_hash = args.first).kind_of?(::Hash)
67
+ @comment = args_hash[:comment] || ''
68
+ @extra = args_hash[:extra] || ''
69
+ @compressed_size = args_hash[:compressed_size] || 0
70
+ @crc = args_hash[:crc] || 0
71
+ @compression_method = args_hash[:compression_method] || ::Zip::Entry::DEFLATED
72
+ @size = args_hash[:size] || 0
73
+ @time = args_hash[:time] || ::Zip::DOSTime.now
74
+ else
75
+ Zip.warn_about_v3_api('Zip::Entry.new') unless args.empty?
76
+
77
+ @comment = args[0] || ''
78
+ @extra = args[1] || ''
79
+ @compressed_size = args[2] || 0
80
+ @crc = args[3] || 0
81
+ @compression_method = args[4] || ::Zip::Entry::DEFLATED
82
+ @size = args[5] || 0
83
+ @time = args[6] || ::Zip::DOSTime.now
84
+ end
70
85
 
71
86
  @ftype = name_is_directory? ? :directory : :file
72
- @extra = ::Zip::ExtraField.new(@extra.to_s) unless @extra.is_a?(::Zip::ExtraField)
87
+ @extra = ::Zip::ExtraField.new(@extra.to_s) unless @extra.kind_of?(::Zip::ExtraField)
88
+ end
89
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
90
+
91
+ def encrypted?
92
+ gp_flags & 1 == 1
93
+ end
94
+
95
+ def incomplete?
96
+ gp_flags & 8 == 8
73
97
  end
74
98
 
75
99
  def time
@@ -91,11 +115,12 @@ module Zip
91
115
  @extra.create('UniversalTime')
92
116
  end
93
117
  (@extra['UniversalTime'] || @extra['NTFS']).mtime = value
94
- @time = value
118
+ @time = value
95
119
  end
96
120
 
97
121
  def file_type_is?(type)
98
122
  raise InternalError, "current filetype is unknown: #{inspect}" unless @ftype
123
+
99
124
  @ftype == type
100
125
  end
101
126
 
@@ -116,6 +141,7 @@ module Zip
116
141
  def name_safe?
117
142
  cleanpath = Pathname.new(@name).cleanpath
118
143
  return false unless cleanpath.relative?
144
+
119
145
  root = ::File::SEPARATOR
120
146
  naive_expanded_path = ::File.join(root, cleanpath.to_s)
121
147
  ::File.absolute_path(cleanpath.to_s, root) == naive_expanded_path
@@ -145,6 +171,7 @@ module Zip
145
171
  # that we didn't change the header size (and thus clobber file data or something)
146
172
  def verify_local_header_size!
147
173
  return if @local_header_size.nil?
174
+
148
175
  new_size = calculate_local_header_size
149
176
  raise Error, "local header size changed (#{@local_header_size} -> #{new_size})" if @local_header_size != new_size
150
177
  end
@@ -162,20 +189,41 @@ module Zip
162
189
  # NB: The caller is responsible for making sure dest_path is safe, if it
163
190
  # is passed.
164
191
  def extract(dest_path = nil, &block)
192
+ Zip.warn_about_v3_api('Zip::Entry#extract')
193
+
165
194
  if dest_path.nil? && !name_safe?
166
- puts "WARNING: skipped #{@name} as unsafe"
195
+ warn "WARNING: skipped '#{@name}' as unsafe."
167
196
  return self
168
197
  end
169
198
 
170
199
  dest_path ||= @name
171
200
  block ||= proc { ::Zip.on_exists_proc }
172
201
 
173
- if directory? || file? || symlink?
174
- __send__("create_#{@ftype}", dest_path, &block)
175
- else
176
- raise "unknown file type #{inspect}"
202
+ raise "unknown file type #{inspect}" unless directory? || file? || symlink?
203
+
204
+ __send__("create_#{@ftype}", dest_path, &block)
205
+ self
206
+ end
207
+
208
+ # Extracts this entry to a file at `entry_path`, with
209
+ # `destination_directory` as the base location in the filesystem.
210
+ #
211
+ # NB: The caller is responsible for making sure `destination_directory` is
212
+ # safe, if it is passed.
213
+ def extract_v3(entry_path = @name, destination_directory: '.', &block)
214
+ dest_dir = ::File.absolute_path(destination_directory || '.')
215
+ extract_path = ::File.absolute_path(::File.join(dest_dir, entry_path))
216
+
217
+ unless extract_path.start_with?(dest_dir)
218
+ warn "WARNING: skipped extracting '#{@name}' to '#{extract_path}' as unsafe."
219
+ return self
177
220
  end
178
221
 
222
+ block ||= proc { ::Zip.on_exists_proc }
223
+
224
+ raise "unknown file type #{inspect}" unless directory? || file? || symlink?
225
+
226
+ __send__(:"create_#{ftype}", extract_path, &block)
179
227
  self
180
228
  end
181
229
 
@@ -185,15 +233,15 @@ module Zip
185
233
 
186
234
  class << self
187
235
  def read_zip_short(io) # :nodoc:
188
- io.read(2).unpack('v')[0]
236
+ io.read(2).unpack1('v')
189
237
  end
190
238
 
191
239
  def read_zip_long(io) # :nodoc:
192
- io.read(4).unpack('V')[0]
240
+ io.read(4).unpack1('V')
193
241
  end
194
242
 
195
243
  def read_zip_64_long(io) # :nodoc:
196
- io.read(8).unpack('Q<')[0]
244
+ io.read(8).unpack1('Q<')
197
245
  end
198
246
 
199
247
  def read_c_dir_entry(io) #:nodoc:all
@@ -218,8 +266,6 @@ module Zip
218
266
  end
219
267
  end
220
268
 
221
- public
222
-
223
269
  def unpack_local_entry(buf)
224
270
  @header_signature,
225
271
  @version,
@@ -249,6 +295,7 @@ module Zip
249
295
  unless @header_signature == ::Zip::LOCAL_ENTRY_SIGNATURE
250
296
  raise ::Zip::Error, "Zip local header magic not found at location '#{local_header_offset}'"
251
297
  end
298
+
252
299
  set_time(@last_mod_date, @last_mod_time)
253
300
 
254
301
  @name = io.read(@name_length)
@@ -261,13 +308,14 @@ module Zip
261
308
 
262
309
  if extra && extra.bytesize != @extra_length
263
310
  raise ::Zip::Error, 'Truncated local zip entry header'
311
+ end
312
+
313
+ if @extra.kind_of?(::Zip::ExtraField)
314
+ @extra.merge(extra) if extra
264
315
  else
265
- if @extra.is_a?(::Zip::ExtraField)
266
- @extra.merge(extra) if extra
267
- else
268
- @extra = ::Zip::ExtraField.new(extra)
269
- end
316
+ @extra = ::Zip::ExtraField.new(extra)
270
317
  end
318
+
271
319
  parse_zip64_extra(true)
272
320
  @local_header_size = calculate_local_header_size
273
321
  end
@@ -354,21 +402,24 @@ module Zip
354
402
 
355
403
  def check_c_dir_entry_static_header_length(buf)
356
404
  return if buf.bytesize == ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
405
+
357
406
  raise Error, 'Premature end of file. Not enough data for zip cdir entry header'
358
407
  end
359
408
 
360
409
  def check_c_dir_entry_signature
361
410
  return if header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
411
+
362
412
  raise Error, "Zip local header magic not found at location '#{local_header_offset}'"
363
413
  end
364
414
 
365
415
  def check_c_dir_entry_comment_size
366
416
  return if @comment && @comment.bytesize == @comment_length
417
+
367
418
  raise ::Zip::Error, 'Truncated cdir zip entry header'
368
419
  end
369
420
 
370
421
  def read_c_dir_extra_field(io)
371
- if @extra.is_a?(::Zip::ExtraField)
422
+ if @extra.kind_of?(::Zip::ExtraField)
372
423
  @extra.merge(io.read(@extra_length))
373
424
  else
374
425
  @extra = ::Zip::ExtraField.new(io.read(@extra_length))
@@ -402,20 +453,25 @@ module Zip
402
453
 
403
454
  def get_extra_attributes_from_path(path) # :nodoc:
404
455
  return if Zip::RUNNING_ON_WINDOWS
456
+
405
457
  stat = file_stat(path)
406
458
  @unix_uid = stat.uid
407
459
  @unix_gid = stat.gid
408
460
  @unix_perms = stat.mode & 0o7777
461
+ @time = ::Zip::DOSTime.from_time(stat.mtime)
409
462
  end
410
463
 
411
- def set_unix_permissions_on_path(dest_path)
412
- # BUG: does not update timestamps into account
464
+ def set_unix_attributes_on_path(dest_path)
413
465
  # ignore setuid/setgid bits by default. honor if @restore_ownership
414
466
  unix_perms_mask = 0o1777
415
467
  unix_perms_mask = 0o7777 if @restore_ownership
416
468
  ::FileUtils.chmod(@unix_perms & unix_perms_mask, dest_path) if @restore_permissions && @unix_perms
417
469
  ::FileUtils.chown(@unix_uid, @unix_gid, dest_path) if @restore_ownership && @unix_uid && @unix_gid && ::Process.egid == 0
418
- # File::utimes()
470
+
471
+ # Restore the timestamp on a file. This will either have come from the
472
+ # original source file that was copied into the archive, or from the
473
+ # creation date of the archive if there was no original source file.
474
+ ::FileUtils.touch(dest_path, mtime: time) if @restore_times
419
475
  end
420
476
 
421
477
  def set_extra_attributes_on_path(dest_path) # :nodoc:
@@ -423,7 +479,7 @@ module Zip
423
479
 
424
480
  case @fstype
425
481
  when ::Zip::FSTYPE_UNIX
426
- set_unix_permissions_on_path(dest_path)
482
+ set_unix_attributes_on_path(dest_path)
427
483
  end
428
484
  end
429
485
 
@@ -484,11 +540,12 @@ module Zip
484
540
 
485
541
  def ==(other)
486
542
  return false unless other.class == self.class
543
+
487
544
  # Compares contents of local entry and exposed fields
488
545
  keys_equal = %w[compression_method crc compressed_size size name extra filepath].all? do |k|
489
546
  other.__send__(k.to_sym) == __send__(k.to_sym)
490
547
  end
491
- keys_equal && time.dos_equals(other.time)
548
+ keys_equal && time == other.time
492
549
  end
493
550
 
494
551
  def <=>(other)
@@ -514,7 +571,7 @@ module Zip
514
571
  raise "unknown @file_type #{@ftype}"
515
572
  end
516
573
  else
517
- zis = ::Zip::InputStream.new(@zipfile, local_header_offset)
574
+ zis = ::Zip::InputStream.new(@zipfile, offset: local_header_offset)
518
575
  zis.instance_variable_set(:@complete_entry, self)
519
576
  zis.get_next_entry
520
577
  if block_given?
@@ -591,7 +648,7 @@ module Zip
591
648
  def set_time(binary_dos_date, binary_dos_time)
592
649
  @time = ::Zip::DOSTime.parse_binary_dos_format(binary_dos_date, binary_dos_time)
593
650
  rescue ArgumentError
594
- warn 'Invalid date/time in zip entry' if ::Zip.warn_invalid_date
651
+ warn 'WARNING: invalid date/time in zip entry.' if ::Zip.warn_invalid_date
595
652
  end
596
653
 
597
654
  def create_file(dest_path, _continue_on_exists_proc = proc { Zip.continue_on_exists_proc })
@@ -601,30 +658,29 @@ module Zip
601
658
  end
602
659
  ::File.open(dest_path, 'wb') do |os|
603
660
  get_input_stream do |is|
604
- set_extra_attributes_on_path(dest_path)
605
-
606
661
  bytes_written = 0
607
662
  warned = false
608
- buf = ''.dup
663
+ buf = +''
609
664
  while (buf = is.sysread(::Zip::Decompressor::CHUNK_SIZE, buf))
610
665
  os << buf
611
666
  bytes_written += buf.bytesize
612
- if bytes_written > size && !warned
613
- message = "Entry #{name} should be #{size}B but is larger when inflated"
614
- if ::Zip.validate_entry_sizes
615
- raise ::Zip::EntrySizeError, message
616
- else
617
- puts "WARNING: #{message}"
618
- warned = true
619
- end
620
- end
667
+ next unless bytes_written > size && !warned
668
+
669
+ message = "entry '#{name}' should be #{size}B, but is larger when inflated."
670
+ raise ::Zip::EntrySizeError, message if ::Zip.validate_entry_sizes
671
+
672
+ warn "WARNING: #{message}"
673
+ warned = true
621
674
  end
622
675
  end
623
676
  end
677
+
678
+ set_extra_attributes_on_path(dest_path)
624
679
  end
625
680
 
626
681
  def create_directory(dest_path)
627
682
  return if ::File.directory?(dest_path)
683
+
628
684
  if ::File.exist?(dest_path)
629
685
  if block_given? && yield(self, dest_path)
630
686
  ::FileUtils.rm_f dest_path
@@ -642,13 +698,14 @@ module Zip
642
698
  def create_symlink(dest_path)
643
699
  # TODO: Symlinks pose security challenges. Symlink support temporarily
644
700
  # removed in view of https://github.com/rubyzip/rubyzip/issues/369 .
645
- puts "WARNING: skipped symlink #{dest_path}"
701
+ warn "WARNING: skipped symlink '#{dest_path}'."
646
702
  end
647
703
 
648
704
  # apply missing data from the zip64 extra information field, if present
649
705
  # (required when file sizes exceed 2**32, but can be used for all files)
650
706
  def parse_zip64_extra(for_local_header) #:nodoc:all
651
707
  return if @extra['Zip64'].nil?
708
+
652
709
  if for_local_header
653
710
  @size, @compressed_size = @extra['Zip64'].parse(@size, @compressed_size)
654
711
  else
@@ -663,6 +720,7 @@ module Zip
663
720
  # create a zip64 extra information field if we need one
664
721
  def prep_zip64_extra(for_local_header) #:nodoc:all
665
722
  return unless ::Zip.write_zip64_support
723
+
666
724
  need_zip64 = @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
667
725
  need_zip64 ||= @local_header_offset >= 0xFFFFFFFF unless for_local_header
668
726
  if need_zip64
data/lib/zip/entry_set.rb CHANGED
@@ -50,6 +50,7 @@ module Zip
50
50
 
51
51
  def ==(other)
52
52
  return false unless other.kind_of?(EntrySet)
53
+
53
54
  @entry_set.values == other.entry_set.values
54
55
  end
55
56
 
@@ -60,6 +61,7 @@ module Zip
60
61
  def glob(pattern, flags = ::File::FNM_PATHNAME | ::File::FNM_DOTMATCH | ::File::FNM_EXTGLOB)
61
62
  entries.map do |entry|
62
63
  next nil unless ::File.fnmatch(pattern, entry.name.chomp('/'), flags)
64
+
63
65
  yield(entry) if block_given?
64
66
  entry
65
67
  end.compact
data/lib/zip/errors.rb CHANGED
@@ -7,6 +7,7 @@ module Zip
7
7
  class EntrySizeError < Error; end
8
8
  class InternalError < Error; end
9
9
  class GPFBit3Error < Error; end
10
+ class DecompressionError < Error; end
10
11
 
11
12
  # Backwards compatibility with v1 (delete in v2)
12
13
  ZipError = Error
@@ -1,9 +1,9 @@
1
1
  module Zip
2
2
  class ExtraField::Generic
3
3
  def self.register_map
4
- if const_defined?(:HEADER_ID)
5
- ::Zip::ExtraField::ID_MAP[const_get(:HEADER_ID)] = self
6
- end
4
+ return unless const_defined?(:HEADER_ID)
5
+
6
+ ::Zip::ExtraField::ID_MAP[const_get(:HEADER_ID)] = self
7
7
  end
8
8
 
9
9
  def self.name
@@ -12,18 +12,19 @@ module Zip
12
12
 
13
13
  # return field [size, content] or false
14
14
  def initial_parse(binstr)
15
- if !binstr
16
- # If nil, start with empty.
17
- return false
18
- elsif binstr[0, 2] != self.class.const_get(:HEADER_ID)
19
- $stderr.puts 'Warning: weired extra feild header ID. skip parsing'
15
+ return false unless binstr
16
+
17
+ if binstr[0, 2] != self.class.const_get(:HEADER_ID)
18
+ warn 'WARNING: weird extra field header ID. Skip parsing it.'
20
19
  return false
21
20
  end
22
- [binstr[2, 2].unpack('v')[0], binstr[4..-1]]
21
+
22
+ [binstr[2, 2].unpack1('v'), binstr[4..-1]]
23
23
  end
24
24
 
25
25
  def ==(other)
26
26
  return false if self.class != other.class
27
+
27
28
  each do |k, v|
28
29
  return false if v != other[k]
29
30
  end
@@ -19,6 +19,7 @@ module Zip
19
19
 
20
20
  def merge(binstr)
21
21
  return if binstr.empty?
22
+
22
23
  size, content = initial_parse(binstr)
23
24
  (size && content) || return
24
25
 
@@ -27,6 +28,7 @@ module Zip
27
28
 
28
29
  tag1 = tags[1]
29
30
  return unless tag1
31
+
30
32
  ntfs_mtime, ntfs_atime, ntfs_ctime = tag1.unpack('Q<Q<Q<')
31
33
  ntfs_mtime && @mtime ||= from_ntfs_time(ntfs_mtime)
32
34
  ntfs_atime && @atime ||= from_ntfs_time(ntfs_atime)
@@ -49,7 +51,7 @@ module Zip
49
51
  # reserved 0 and tag 1
50
52
  s = [0, 1].pack('Vv')
51
53
 
52
- tag1 = ''.force_encoding(Encoding::BINARY)
54
+ tag1 = ''.b
53
55
  if @mtime
54
56
  tag1 << [to_ntfs_time(@mtime)].pack('Q<')
55
57
  if @atime
@@ -65,12 +67,14 @@ module Zip
65
67
 
66
68
  def parse_tags(content)
67
69
  return {} if content.nil?
70
+
68
71
  tags = {}
69
72
  i = 0
70
73
  while i < content.bytesize
71
74
  tag, size = content[i, 4].unpack('vv')
72
75
  i += 4
73
76
  break unless tag && size
77
+
74
78
  value = content[i, size]
75
79
  i += size
76
80
  tags[tag] = value
@@ -16,14 +16,16 @@ module Zip
16
16
 
17
17
  def merge(binstr)
18
18
  return if binstr.empty?
19
+
19
20
  size, content = initial_parse(binstr)
20
21
  # size: 0 for central directory. 4 for local header
21
22
  return if !size || size == 0
23
+
22
24
  atime, mtime, uid, gid = content.unpack('VVvv')
23
25
  @uid ||= uid
24
26
  @gid ||= gid
25
27
  @atime ||= atime
26
- @mtime ||= mtime
28
+ @mtime ||= mtime # rubocop:disable Naming/MemoizedInstanceVariableName
27
29
  end
28
30
 
29
31
  def ==(other)
@@ -4,24 +4,54 @@ module Zip
4
4
  HEADER_ID = 'UT'
5
5
  register_map
6
6
 
7
+ ATIME_MASK = 0b010
8
+ CTIME_MASK = 0b100
9
+ MTIME_MASK = 0b001
10
+
7
11
  def initialize(binstr = nil)
8
12
  @ctime = nil
9
13
  @mtime = nil
10
14
  @atime = nil
11
- @flag = nil
12
- binstr && merge(binstr)
15
+ @flag = 0
16
+
17
+ merge(binstr) unless binstr.nil?
18
+ end
19
+
20
+ attr_reader :atime, :ctime, :mtime, :flag
21
+
22
+ def atime=(time)
23
+ @flag = time.nil? ? @flag & ~ATIME_MASK : @flag | ATIME_MASK
24
+ @atime = time
25
+ end
26
+
27
+ def ctime=(time)
28
+ @flag = time.nil? ? @flag & ~CTIME_MASK : @flag | CTIME_MASK
29
+ @ctime = time
13
30
  end
14
31
 
15
- attr_accessor :atime, :ctime, :mtime, :flag
32
+ def mtime=(time)
33
+ @flag = time.nil? ? @flag & ~MTIME_MASK : @flag | MTIME_MASK
34
+ @mtime = time
35
+ end
16
36
 
17
37
  def merge(binstr)
18
38
  return if binstr.empty?
39
+
19
40
  size, content = initial_parse(binstr)
20
- size || return
21
- @flag, mtime, atime, ctime = content.unpack('CVVV')
22
- mtime && @mtime ||= ::Zip::DOSTime.at(mtime)
23
- atime && @atime ||= ::Zip::DOSTime.at(atime)
24
- ctime && @ctime ||= ::Zip::DOSTime.at(ctime)
41
+ return if !size || size <= 0
42
+
43
+ @flag, *times = content.unpack('Cl<l<l<')
44
+
45
+ # Parse the timestamps, in order, based on which flags are set.
46
+ return if times[0].nil?
47
+
48
+ @mtime ||= ::Zip::DOSTime.at(times.shift) unless @flag & MTIME_MASK == 0
49
+ return if times[0].nil?
50
+
51
+ @atime ||= ::Zip::DOSTime.at(times.shift) unless @flag & ATIME_MASK == 0
52
+ return if times[0].nil?
53
+
54
+ @ctime ||= ::Zip::DOSTime.at(times.shift) unless @flag & CTIME_MASK == 0
25
55
  end
26
56
 
27
57
  def ==(other)
@@ -32,15 +62,15 @@ module Zip
32
62
 
33
63
  def pack_for_local
34
64
  s = [@flag].pack('C')
35
- @flag & 1 != 0 && s << [@mtime.to_i].pack('V')
36
- @flag & 2 != 0 && s << [@atime.to_i].pack('V')
37
- @flag & 4 != 0 && s << [@ctime.to_i].pack('V')
65
+ s << [@mtime.to_i].pack('l<') unless @flag & MTIME_MASK == 0
66
+ s << [@atime.to_i].pack('l<') unless @flag & ATIME_MASK == 0
67
+ s << [@ctime.to_i].pack('l<') unless @flag & CTIME_MASK == 0
38
68
  s
39
69
  end
40
70
 
41
71
  def pack_for_c_dir
42
72
  s = [@flag].pack('C')
43
- @flag & 1 == 1 && s << [@mtime.to_i].pack('V')
73
+ s << [@mtime.to_i].pack('l<') unless @flag & MTIME_MASK == 0
44
74
  s
45
75
  end
46
76
  end
@@ -14,12 +14,14 @@ module Zip
14
14
 
15
15
  def merge(binstr)
16
16
  return if binstr.empty?
17
+
17
18
  size, content = initial_parse(binstr)
18
19
  # size: 0 for central directory. 4 for local header
19
20
  return if !size || size == 0
21
+
20
22
  uid, gid = content.unpack('vv')
21
23
  @uid ||= uid
22
- @gid ||= gid
24
+ @gid ||= gid # rubocop:disable Naming/MemoizedInstanceVariableName
23
25
  end
24
26
 
25
27
  def ==(other)
@@ -9,7 +9,7 @@ module Zip
9
9
  # unparsed binary; we don't actually know what this contains
10
10
  # without looking for FFs in the associated file header
11
11
  # call parse after initializing with a binary string
12
- @content = nil
12
+ @content = nil
13
13
  @original_size = nil
14
14
  @compressed_size = nil
15
15
  @relative_header_offset = nil
@@ -26,6 +26,7 @@ module Zip
26
26
 
27
27
  def merge(binstr)
28
28
  return if binstr.empty?
29
+
29
30
  _, @content = initial_parse(binstr)
30
31
  end
31
32
 
@@ -45,19 +46,20 @@ module Zip
45
46
  end
46
47
 
47
48
  def extract(size, format)
48
- @content.slice!(0, size).unpack(format)[0]
49
+ @content.slice!(0, size).unpack1(format)
49
50
  end
50
51
  private :extract
51
52
 
52
53
  def pack_for_local
53
54
  # local header entries must contain original size and compressed size; other fields do not apply
54
55
  return '' unless @original_size && @compressed_size
56
+
55
57
  [@original_size, @compressed_size].pack('Q<Q<')
56
58
  end
57
59
 
58
60
  def pack_for_c_dir
59
61
  # central directory entries contain only fields that didn't fit in the main entry part
60
- packed = ''.force_encoding('BINARY')
62
+ packed = ''.b
61
63
  packed << [@original_size].pack('Q<') if @original_size
62
64
  packed << [@compressed_size].pack('Q<') if @compressed_size
63
65
  packed << [@relative_header_offset].pack('Q<') if @relative_header_offset
@@ -6,27 +6,27 @@ module Zip
6
6
  merge(binstr) if binstr
7
7
  end
8
8
 
9
- def extra_field_type_exist(binstr, id, len, i)
9
+ def extra_field_type_exist(binstr, id, len, index)
10
10
  field_name = ID_MAP[id].name
11
11
  if member?(field_name)
12
- self[field_name].merge(binstr[i, len + 4])
12
+ self[field_name].merge(binstr[index, len + 4])
13
13
  else
14
- field_obj = ID_MAP[id].new(binstr[i, len + 4])
14
+ field_obj = ID_MAP[id].new(binstr[index, len + 4])
15
15
  self[field_name] = field_obj
16
16
  end
17
17
  end
18
18
 
19
- def extra_field_type_unknown(binstr, len, i)
19
+ def extra_field_type_unknown(binstr, len, index)
20
20
  create_unknown_item unless self['Unknown']
21
- if !len || len + 4 > binstr[i..-1].bytesize
22
- self['Unknown'] << binstr[i..-1]
21
+ if !len || len + 4 > binstr[index..-1].bytesize
22
+ self['Unknown'] << binstr[index..-1]
23
23
  return
24
24
  end
25
- self['Unknown'] << binstr[i, len + 4]
25
+ self['Unknown'] << binstr[index, len + 4]
26
26
  end
27
27
 
28
28
  def create_unknown_item
29
- s = ''.dup
29
+ s = +''
30
30
  class << s
31
31
  alias_method :to_c_dir_bin, :to_s
32
32
  alias_method :to_local_bin, :to_s
@@ -36,10 +36,11 @@ module Zip
36
36
 
37
37
  def merge(binstr)
38
38
  return if binstr.empty?
39
+
39
40
  i = 0
40
41
  while i < binstr.bytesize
41
42
  id = binstr[i, 2]
42
- len = binstr[i + 2, 2].to_s.unpack('v').first
43
+ len = binstr[i + 2, 2].to_s.unpack1('v')
43
44
  if id && ID_MAP.member?(id)
44
45
  extra_field_type_exist(binstr, id, len, i)
45
46
  elsif id
@@ -54,6 +55,7 @@ module Zip
54
55
  unless (field_class = ID_MAP.values.find { |k| k.name == name })
55
56
  raise Error, "Unknown extra field '#{name}'"
56
57
  end
58
+
57
59
  self[name] = field_class.new
58
60
  end
59
61