rubyzip 2.3.1 → 3.0.0.alpha

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +368 -0
  3. data/README.md +122 -39
  4. data/Rakefile +11 -7
  5. data/lib/zip/central_directory.rb +164 -118
  6. data/lib/zip/compressor.rb +3 -1
  7. data/lib/zip/constants.rb +25 -21
  8. data/lib/zip/crypto/decrypted_io.rb +3 -1
  9. data/lib/zip/crypto/encryption.rb +4 -2
  10. data/lib/zip/crypto/null_encryption.rb +5 -3
  11. data/lib/zip/crypto/traditional_encryption.rb +5 -3
  12. data/lib/zip/decompressor.rb +4 -3
  13. data/lib/zip/deflater.rb +10 -8
  14. data/lib/zip/dirtyable.rb +32 -0
  15. data/lib/zip/dos_time.rb +41 -5
  16. data/lib/zip/entry.rb +273 -170
  17. data/lib/zip/entry_set.rb +9 -7
  18. data/lib/zip/errors.rb +115 -16
  19. data/lib/zip/extra_field/generic.rb +3 -10
  20. data/lib/zip/extra_field/ntfs.rb +4 -2
  21. data/lib/zip/extra_field/old_unix.rb +3 -1
  22. data/lib/zip/extra_field/universal_time.rb +3 -1
  23. data/lib/zip/extra_field/unix.rb +5 -3
  24. data/lib/zip/extra_field/unknown.rb +33 -0
  25. data/lib/zip/extra_field/zip64.rb +12 -5
  26. data/lib/zip/extra_field.rb +15 -21
  27. data/lib/zip/file.rb +152 -221
  28. data/lib/zip/file_split.rb +97 -0
  29. data/lib/zip/filesystem/dir.rb +86 -0
  30. data/lib/zip/filesystem/directory_iterator.rb +48 -0
  31. data/lib/zip/filesystem/file.rb +262 -0
  32. data/lib/zip/filesystem/file_stat.rb +110 -0
  33. data/lib/zip/filesystem/zip_file_name_mapper.rb +81 -0
  34. data/lib/zip/filesystem.rb +26 -595
  35. data/lib/zip/inflater.rb +6 -4
  36. data/lib/zip/input_stream.rb +43 -25
  37. data/lib/zip/ioextras/abstract_input_stream.rb +13 -8
  38. data/lib/zip/ioextras/abstract_output_stream.rb +5 -3
  39. data/lib/zip/ioextras.rb +6 -6
  40. data/lib/zip/null_compressor.rb +3 -1
  41. data/lib/zip/null_decompressor.rb +3 -1
  42. data/lib/zip/null_input_stream.rb +3 -1
  43. data/lib/zip/output_stream.rb +45 -39
  44. data/lib/zip/pass_thru_compressor.rb +3 -1
  45. data/lib/zip/pass_thru_decompressor.rb +4 -2
  46. data/lib/zip/streamable_directory.rb +3 -1
  47. data/lib/zip/streamable_stream.rb +3 -0
  48. data/lib/zip/version.rb +3 -1
  49. data/lib/zip.rb +15 -22
  50. data/rubyzip.gemspec +38 -0
  51. data/samples/example.rb +8 -3
  52. data/samples/example_filesystem.rb +2 -1
  53. data/samples/example_recursive.rb +3 -1
  54. data/samples/gtk_ruby_zip.rb +4 -2
  55. data/samples/qtzip.rb +6 -5
  56. data/samples/write_simple.rb +1 -0
  57. data/samples/zipfind.rb +1 -0
  58. metadata +83 -35
  59. data/TODO +0 -15
  60. data/lib/zip/extra_field/zip64_placeholder.rb +0 -15
data/lib/zip/entry.rb CHANGED
@@ -1,19 +1,39 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pathname'
4
+
5
+ require_relative 'dirtyable'
6
+
2
7
  module Zip
8
+ # Zip::Entry represents an entry in a Zip archive.
3
9
  class Entry
4
- STORED = 0
5
- DEFLATED = 8
10
+ include Dirtyable
11
+
12
+ STORED = ::Zip::COMPRESSION_METHOD_STORE
13
+ DEFLATED = ::Zip::COMPRESSION_METHOD_DEFLATE
14
+
6
15
  # Language encoding flag (EFS) bit
7
16
  EFS = 0b100000000000
8
17
 
9
- attr_accessor :comment, :compressed_size, :crc, :extra, :compression_method,
10
- :name, :size, :local_header_offset, :zipfile, :fstype, :external_file_attributes,
11
- :internal_file_attributes,
12
- :gp_flags, :header_signature, :follow_symlinks,
13
- :restore_times, :restore_permissions, :restore_ownership,
14
- :unix_uid, :unix_gid, :unix_perms,
15
- :dirty
16
- attr_reader :ftype, :filepath # :nodoc:
18
+ # Compression level flags (used as part of the gp flags).
19
+ COMPRESSION_LEVEL_SUPERFAST_GPFLAG = 0b110
20
+ COMPRESSION_LEVEL_FAST_GPFLAG = 0b100
21
+ COMPRESSION_LEVEL_MAX_GPFLAG = 0b010
22
+
23
+ attr_accessor :comment, :compressed_size, :follow_symlinks, :name,
24
+ :restore_ownership, :restore_permissions, :restore_times,
25
+ :unix_gid, :unix_perms, :unix_uid
26
+
27
+ attr_accessor :crc, :external_file_attributes, :fstype, :gp_flags,
28
+ :internal_file_attributes, :local_header_offset # :nodoc:
29
+
30
+ attr_reader :extra, :compression_level, :filepath # :nodoc:
31
+
32
+ attr_writer :size # :nodoc:
33
+
34
+ mark_dirty :comment=, :compressed_size=, :external_file_attributes=,
35
+ :fstype=, :gp_flags=, :name=, :size=,
36
+ :unix_gid=, :unix_perms=, :unix_uid=
17
37
 
18
38
  def set_default_vars_values
19
39
  @local_header_offset = 0
@@ -34,43 +54,53 @@ module Zip
34
54
  end
35
55
  @follow_symlinks = false
36
56
 
37
- @restore_times = false
38
- @restore_permissions = false
39
- @restore_ownership = false
57
+ @restore_times = DEFAULT_RESTORE_OPTIONS[:restore_times]
58
+ @restore_permissions = DEFAULT_RESTORE_OPTIONS[:restore_permissions]
59
+ @restore_ownership = DEFAULT_RESTORE_OPTIONS[:restore_ownership]
40
60
  # BUG: need an extra field to support uid/gid's
41
61
  @unix_uid = nil
42
62
  @unix_gid = nil
43
63
  @unix_perms = nil
44
- # @posix_acl = nil
45
- # @ntfs_acl = nil
46
- @dirty = false
47
64
  end
48
65
 
49
66
  def check_name(name)
50
- return unless name.start_with?('/')
51
-
52
- raise ::Zip::EntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
67
+ raise EntryNameError, name if name.start_with?('/')
68
+ raise EntryNameError if name.length > 65_535
53
69
  end
54
70
 
55
- def initialize(*args)
56
- name = args[1] || ''
57
- check_name(name)
71
+ def initialize(
72
+ zipfile = '', name = '',
73
+ comment: '', size: nil, compressed_size: 0, crc: 0,
74
+ compression_method: DEFLATED,
75
+ compression_level: ::Zip.default_compression,
76
+ time: ::Zip::DOSTime.now, extra: ::Zip::ExtraField.new
77
+ )
78
+ super()
79
+ @name = name
80
+ check_name(@name)
58
81
 
59
82
  set_default_vars_values
60
83
  @fstype = ::Zip::RUNNING_ON_WINDOWS ? ::Zip::FSTYPE_FAT : ::Zip::FSTYPE_UNIX
61
84
 
62
- @zipfile = args[0] || ''
63
- @name = name
64
- @comment = args[2] || ''
65
- @extra = args[3] || ''
66
- @compressed_size = args[4] || 0
67
- @crc = args[5] || 0
68
- @compression_method = args[6] || ::Zip::Entry::DEFLATED
69
- @size = args[7] || 0
70
- @time = args[8] || ::Zip::DOSTime.now
71
-
72
- @ftype = name_is_directory? ? :directory : :file
73
- @extra = ::Zip::ExtraField.new(@extra.to_s) unless @extra.kind_of?(::Zip::ExtraField)
85
+ @zipfile = zipfile
86
+ @comment = comment || ''
87
+ @compression_method = compression_method || DEFLATED
88
+ @compression_level = compression_level || ::Zip.default_compression
89
+ @compressed_size = compressed_size || 0
90
+ @crc = crc || 0
91
+ @size = size
92
+ @time = case time
93
+ when ::Zip::DOSTime
94
+ time
95
+ when Time
96
+ ::Zip::DOSTime.from_time(time)
97
+ else
98
+ ::Zip::DOSTime.now
99
+ end
100
+ @extra =
101
+ extra.kind_of?(ExtraField) ? extra : ExtraField.new(extra.to_s)
102
+
103
+ set_compression_level_flags
74
104
  end
75
105
 
76
106
  def encrypted?
@@ -81,32 +111,76 @@ module Zip
81
111
  gp_flags & 8 == 8
82
112
  end
83
113
 
84
- def time
85
- if @extra['UniversalTime']
86
- @extra['UniversalTime'].mtime
87
- elsif @extra['NTFS']
88
- @extra['NTFS'].mtime
89
- else
90
- # Standard time field in central directory has local time
91
- # under archive creator. Then, we can't get timezone.
92
- @time
93
- end
114
+ def size
115
+ @size || 0
116
+ end
117
+
118
+ def time(component: :mtime)
119
+ time =
120
+ if @extra['UniversalTime']
121
+ @extra['UniversalTime'].send(component)
122
+ elsif @extra['NTFS']
123
+ @extra['NTFS'].send(component)
124
+ end
125
+
126
+ # Standard time field in central directory has local time
127
+ # under archive creator. Then, we can't get timezone.
128
+ time || (@time if component == :mtime)
94
129
  end
95
130
 
96
131
  alias mtime time
97
132
 
98
- def time=(value)
133
+ def atime
134
+ time(component: :atime)
135
+ end
136
+
137
+ def ctime
138
+ time(component: :ctime)
139
+ end
140
+
141
+ def time=(value, component: :mtime)
142
+ @dirty = true
99
143
  unless @extra.member?('UniversalTime') || @extra.member?('NTFS')
100
144
  @extra.create('UniversalTime')
101
145
  end
102
- (@extra['UniversalTime'] || @extra['NTFS']).mtime = value
103
- @time = value
146
+
147
+ value = DOSTime.from_time(value)
148
+ comp = "#{component}=" unless component.to_s.end_with?('=')
149
+ (@extra['UniversalTime'] || @extra['NTFS']).send(comp, value)
150
+ @time = value if component == :mtime
151
+ end
152
+
153
+ alias mtime= time=
154
+
155
+ def atime=(value)
156
+ send(:time=, value, component: :atime)
157
+ end
158
+
159
+ def ctime=(value)
160
+ send(:time=, value, component: :ctime)
161
+ end
162
+
163
+ def compression_method
164
+ return STORED if ftype == :directory || @compression_level == 0
165
+
166
+ @compression_method
167
+ end
168
+
169
+ def compression_method=(method)
170
+ @dirty = true
171
+ @compression_method = (ftype == :directory ? STORED : method)
172
+ end
173
+
174
+ def zip64?
175
+ !@extra['Zip64'].nil?
104
176
  end
105
177
 
106
178
  def file_type_is?(type)
107
- raise InternalError, "current filetype is unknown: #{inspect}" unless @ftype
179
+ ftype == type
180
+ end
108
181
 
109
- @ftype == type
182
+ def ftype # :nodoc:
183
+ @ftype ||= name_is_directory? ? :directory : :file
110
184
  end
111
185
 
112
186
  # Dynamic checkers
@@ -128,8 +202,9 @@ module Zip
128
202
  return false unless cleanpath.relative?
129
203
 
130
204
  root = ::File::SEPARATOR
131
- naive_expanded_path = ::File.join(root, cleanpath.to_s)
132
- ::File.absolute_path(cleanpath.to_s, root) == naive_expanded_path
205
+ naive = ::File.join(root, cleanpath.to_s)
206
+ # Allow for Windows drive mappings at the root.
207
+ ::File.absolute_path(cleanpath.to_s, root).match?(/([A-Z]:)?#{naive}/i)
133
208
  end
134
209
 
135
210
  def local_entry_offset #:nodoc:all
@@ -158,7 +233,10 @@ module Zip
158
233
  return if @local_header_size.nil?
159
234
 
160
235
  new_size = calculate_local_header_size
161
- raise Error, "local header size changed (#{@local_header_size} -> #{new_size})" if @local_header_size != new_size
236
+ return unless @local_header_size != new_size
237
+
238
+ raise Error,
239
+ "Local header size changed (#{@local_header_size} -> #{new_size})"
162
240
  end
163
241
 
164
242
  def cdir_header_size #:nodoc:all
@@ -167,24 +245,28 @@ module Zip
167
245
  end
168
246
 
169
247
  def next_header_offset #:nodoc:all
170
- local_entry_offset + compressed_size + data_descriptor_size
248
+ local_entry_offset + compressed_size
171
249
  end
172
250
 
173
- # Extracts entry to file dest_path (defaults to @name).
174
- # NB: The caller is responsible for making sure dest_path is safe, if it
175
- # is passed.
176
- def extract(dest_path = nil, &block)
177
- if dest_path.nil? && !name_safe?
178
- warn "WARNING: skipped '#{@name}' as unsafe."
251
+ # Extracts this entry to a file at `entry_path`, with
252
+ # `destination_directory` as the base location in the filesystem.
253
+ #
254
+ # NB: The caller is responsible for making sure `destination_directory` is
255
+ # safe, if it is passed.
256
+ def extract(entry_path = @name, destination_directory: '.', &block)
257
+ dest_dir = ::File.absolute_path(destination_directory || '.')
258
+ extract_path = ::File.absolute_path(::File.join(dest_dir, entry_path))
259
+
260
+ unless extract_path.start_with?(dest_dir)
261
+ warn "WARNING: skipped extracting '#{@name}' to '#{extract_path}' as unsafe."
179
262
  return self
180
263
  end
181
264
 
182
- dest_path ||= @name
183
265
  block ||= proc { ::Zip.on_exists_proc }
184
266
 
185
267
  raise "unknown file type #{inspect}" unless directory? || file? || symlink?
186
268
 
187
- __send__("create_#{@ftype}", dest_path, &block)
269
+ __send__("create_#{ftype}", extract_path, &block)
188
270
  self
189
271
  end
190
272
 
@@ -193,18 +275,6 @@ module Zip
193
275
  end
194
276
 
195
277
  class << self
196
- def read_zip_short(io) # :nodoc:
197
- io.read(2).unpack1('v')
198
- end
199
-
200
- def read_zip_long(io) # :nodoc:
201
- io.read(4).unpack1('V')
202
- end
203
-
204
- def read_zip_64_long(io) # :nodoc:
205
- io.read(8).unpack1('Q<')
206
- end
207
-
208
278
  def read_c_dir_entry(io) #:nodoc:all
209
279
  path = if io.respond_to?(:path)
210
280
  io.path
@@ -222,6 +292,8 @@ module Zip
222
292
  entry = new(io)
223
293
  entry.read_local_entry(io)
224
294
  entry
295
+ rescue SplitArchiveError
296
+ raise
225
297
  rescue Error
226
298
  nil
227
299
  end
@@ -243,6 +315,7 @@ module Zip
243
315
  end
244
316
 
245
317
  def read_local_entry(io) #:nodoc:all
318
+ @dirty = false # No changes at this point.
246
319
  @local_header_offset = io.tell
247
320
 
248
321
  static_sized_fields_buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
@@ -253,30 +326,32 @@ module Zip
253
326
 
254
327
  unpack_local_entry(static_sized_fields_buf)
255
328
 
256
- unless @header_signature == ::Zip::LOCAL_ENTRY_SIGNATURE
257
- raise ::Zip::Error, "Zip local header magic not found at location '#{local_header_offset}'"
329
+ unless @header_signature == LOCAL_ENTRY_SIGNATURE
330
+ if @header_signature == SPLIT_FILE_SIGNATURE
331
+ raise SplitArchiveError
332
+ end
333
+
334
+ raise Error, "Zip local header magic not found at location '#{local_header_offset}'"
258
335
  end
259
336
 
260
337
  set_time(@last_mod_date, @last_mod_time)
261
338
 
262
339
  @name = io.read(@name_length)
263
- extra = io.read(@extra_length)
264
-
265
- @name.tr!('\\', '/')
266
340
  if ::Zip.force_entry_names_encoding
267
341
  @name.force_encoding(::Zip.force_entry_names_encoding)
268
342
  end
343
+ @name.tr!('\\', '/') # Normalise filepath separators after encoding set.
269
344
 
345
+ # We need to do this here because `initialize` has so many side-effects.
346
+ # :-(
347
+ @ftype = name_is_directory? ? :directory : :file
348
+
349
+ extra = io.read(@extra_length)
270
350
  if extra && extra.bytesize != @extra_length
271
351
  raise ::Zip::Error, 'Truncated local zip entry header'
272
352
  end
273
353
 
274
- if @extra.kind_of?(::Zip::ExtraField)
275
- @extra.merge(extra) if extra
276
- else
277
- @extra = ::Zip::ExtraField.new(extra)
278
- end
279
-
354
+ read_extra_field(extra, local: true)
280
355
  parse_zip64_extra(true)
281
356
  @local_header_size = calculate_local_header_size
282
357
  end
@@ -286,18 +361,18 @@ module Zip
286
361
  [::Zip::LOCAL_ENTRY_SIGNATURE,
287
362
  @version_needed_to_extract, # version needed to extract
288
363
  @gp_flags, # @gp_flags
289
- @compression_method,
364
+ compression_method,
290
365
  @time.to_binary_dos_time, # @last_mod_time
291
366
  @time.to_binary_dos_date, # @last_mod_date
292
367
  @crc,
293
368
  zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
294
- zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
369
+ zip64 && zip64.original_size ? 0xFFFFFFFF : (@size || 0),
295
370
  name_size,
296
371
  @extra ? @extra.local_size : 0].pack('VvvvvvVVVvv')
297
372
  end
298
373
 
299
- def write_local_entry(io, rewrite = false) #:nodoc:all
300
- prep_zip64_extra(true)
374
+ def write_local_entry(io, rewrite: false) #:nodoc:all
375
+ prep_local_zip64_extra
301
376
  verify_local_header_size! if rewrite
302
377
  @local_header_offset = io.tell
303
378
 
@@ -344,8 +419,9 @@ module Zip
344
419
  when ::Zip::FILE_TYPE_SYMLINK
345
420
  :symlink
346
421
  else
347
- # best case guess for whether it is a file or not
348
- # Otherwise this would be set to unknown and that entry would never be able to extracted
422
+ # Best case guess for whether it is a file or not.
423
+ # Otherwise this would be set to unknown and that
424
+ # entry would never be able to be extracted.
349
425
  if name_is_directory?
350
426
  :directory
351
427
  else
@@ -362,13 +438,13 @@ module Zip
362
438
  end
363
439
 
364
440
  def check_c_dir_entry_static_header_length(buf)
365
- return if buf.bytesize == ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
441
+ return unless buf.nil? || buf.bytesize != ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
366
442
 
367
443
  raise Error, 'Premature end of file. Not enough data for zip cdir entry header'
368
444
  end
369
445
 
370
446
  def check_c_dir_entry_signature
371
- return if header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
447
+ return if @header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
372
448
 
373
449
  raise Error, "Zip local header magic not found at location '#{local_header_offset}'"
374
450
  end
@@ -379,25 +455,29 @@ module Zip
379
455
  raise ::Zip::Error, 'Truncated cdir zip entry header'
380
456
  end
381
457
 
382
- def read_c_dir_extra_field(io)
458
+ def read_extra_field(buf, local: false)
383
459
  if @extra.kind_of?(::Zip::ExtraField)
384
- @extra.merge(io.read(@extra_length))
460
+ @extra.merge(buf, local: local) if buf
385
461
  else
386
- @extra = ::Zip::ExtraField.new(io.read(@extra_length))
462
+ @extra = ::Zip::ExtraField.new(buf, local: local)
387
463
  end
388
464
  end
389
465
 
390
466
  def read_c_dir_entry(io) #:nodoc:all
467
+ @dirty = false # No changes at this point.
391
468
  static_sized_fields_buf = io.read(::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH)
392
469
  check_c_dir_entry_static_header_length(static_sized_fields_buf)
393
470
  unpack_c_dir_entry(static_sized_fields_buf)
394
471
  check_c_dir_entry_signature
395
472
  set_time(@last_mod_date, @last_mod_time)
473
+
396
474
  @name = io.read(@name_length)
397
475
  if ::Zip.force_entry_names_encoding
398
476
  @name.force_encoding(::Zip.force_entry_names_encoding)
399
477
  end
400
- read_c_dir_extra_field(io)
478
+ @name.tr!('\\', '/') # Normalise filepath separators after encoding set.
479
+
480
+ read_extra_field(io.read(@extra_length))
401
481
  @comment = io.read(@comment_length)
402
482
  check_c_dir_entry_comment_size
403
483
  set_ftype_from_c_dir_entry
@@ -413,27 +493,27 @@ module Zip
413
493
  end
414
494
 
415
495
  def get_extra_attributes_from_path(path) # :nodoc:
416
- return if Zip::RUNNING_ON_WINDOWS
496
+ stat = file_stat(path)
497
+ @time = DOSTime.from_time(stat.mtime)
498
+ return if ::Zip::RUNNING_ON_WINDOWS
417
499
 
418
- stat = file_stat(path)
419
500
  @unix_uid = stat.uid
420
501
  @unix_gid = stat.gid
421
502
  @unix_perms = stat.mode & 0o7777
422
- @time = ::Zip::DOSTime.from_time(stat.mtime)
423
503
  end
424
504
 
505
+ # rubocop:disable Style/GuardClause
425
506
  def set_unix_attributes_on_path(dest_path)
426
- # ignore setuid/setgid bits by default. honor if @restore_ownership
427
- unix_perms_mask = 0o1777
428
- unix_perms_mask = 0o7777 if @restore_ownership
429
- ::FileUtils.chmod(@unix_perms & unix_perms_mask, dest_path) if @restore_permissions && @unix_perms
430
- ::FileUtils.chown(@unix_uid, @unix_gid, dest_path) if @restore_ownership && @unix_uid && @unix_gid && ::Process.egid == 0
431
-
432
- # Restore the timestamp on a file. This will either have come from the
433
- # original source file that was copied into the archive, or from the
434
- # creation date of the archive if there was no original source file.
435
- ::FileUtils.touch(dest_path, mtime: time) if @restore_times
507
+ # Ignore setuid/setgid bits by default. Honour if @restore_ownership.
508
+ unix_perms_mask = (@restore_ownership ? 0o7777 : 0o1777)
509
+ if @restore_permissions && @unix_perms
510
+ ::FileUtils.chmod(@unix_perms & unix_perms_mask, dest_path)
511
+ end
512
+ if @restore_ownership && @unix_uid && @unix_gid && ::Process.egid == 0
513
+ ::FileUtils.chown(@unix_uid, @unix_gid, dest_path)
514
+ end
436
515
  end
516
+ # rubocop:enable Style/GuardClause
437
517
 
438
518
  def set_extra_attributes_on_path(dest_path) # :nodoc:
439
519
  return unless file? || directory?
@@ -442,6 +522,11 @@ module Zip
442
522
  when ::Zip::FSTYPE_UNIX
443
523
  set_unix_attributes_on_path(dest_path)
444
524
  end
525
+
526
+ # Restore the timestamp on a file. This will either have come from the
527
+ # original source file that was copied into the archive, or from the
528
+ # creation date of the archive if there was no original source file.
529
+ ::FileUtils.touch(dest_path, mtime: time) if @restore_times
445
530
  end
446
531
 
447
532
  def pack_c_dir_entry
@@ -452,12 +537,12 @@ module Zip
452
537
  @fstype, # filesystem type
453
538
  @version_needed_to_extract, # @versionNeededToExtract
454
539
  @gp_flags, # @gp_flags
455
- @compression_method,
540
+ compression_method,
456
541
  @time.to_binary_dos_time, # @last_mod_time
457
542
  @time.to_binary_dos_date, # @last_mod_date
458
543
  @crc,
459
544
  zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
460
- zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
545
+ zip64 && zip64.original_size ? 0xFFFFFFFF : (@size || 0),
461
546
  name_size,
462
547
  @extra ? @extra.c_dir_size : 0,
463
548
  comment_size,
@@ -472,10 +557,11 @@ module Zip
472
557
  end
473
558
 
474
559
  def write_c_dir_entry(io) #:nodoc:all
475
- prep_zip64_extra(false)
560
+ prep_cdir_zip64_extra
561
+
476
562
  case @fstype
477
563
  when ::Zip::FSTYPE_UNIX
478
- ft = case @ftype
564
+ ft = case ftype
479
565
  when :file
480
566
  @unix_perms ||= 0o644
481
567
  ::Zip::FILE_TYPE_FILE
@@ -503,10 +589,9 @@ module Zip
503
589
  return false unless other.class == self.class
504
590
 
505
591
  # Compares contents of local entry and exposed fields
506
- keys_equal = %w[compression_method crc compressed_size size name extra filepath].all? do |k|
592
+ %w[compression_method crc compressed_size size name extra filepath time].all? do |k|
507
593
  other.__send__(k.to_sym) == __send__(k.to_sym)
508
594
  end
509
- keys_equal && time.dos_equals(other.time)
510
595
  end
511
596
 
512
597
  def <=>(other)
@@ -516,26 +601,26 @@ module Zip
516
601
  # Returns an IO like object for the given ZipEntry.
517
602
  # Warning: may behave weird with symlinks.
518
603
  def get_input_stream(&block)
519
- if @ftype == :directory
520
- yield ::Zip::NullInputStream if block_given?
604
+ if ftype == :directory
605
+ yield ::Zip::NullInputStream if block
521
606
  ::Zip::NullInputStream
522
607
  elsif @filepath
523
- case @ftype
608
+ case ftype
524
609
  when :file
525
610
  ::File.open(@filepath, 'rb', &block)
526
611
  when :symlink
527
612
  linkpath = ::File.readlink(@filepath)
528
613
  stringio = ::StringIO.new(linkpath)
529
- yield(stringio) if block_given?
614
+ yield(stringio) if block
530
615
  stringio
531
616
  else
532
- raise "unknown @file_type #{@ftype}"
617
+ raise "unknown @file_type #{ftype}"
533
618
  end
534
619
  else
535
- zis = ::Zip::InputStream.new(@zipfile, local_header_offset)
620
+ zis = ::Zip::InputStream.new(@zipfile, offset: local_header_offset)
536
621
  zis.instance_variable_set(:@complete_entry, self)
537
622
  zis.get_next_entry
538
- if block_given?
623
+ if block
539
624
  begin
540
625
  yield(zis)
541
626
  ensure
@@ -572,15 +657,18 @@ module Zip
572
657
  end
573
658
 
574
659
  @filepath = src_path
660
+ @size = stat.size
575
661
  get_extra_attributes_from_path(@filepath)
576
662
  end
577
663
 
578
664
  def write_to_zip_output_stream(zip_output_stream) #:nodoc:all
579
- if @ftype == :directory
580
- zip_output_stream.put_next_entry(self, nil, nil, ::Zip::Entry::STORED)
665
+ if ftype == :directory
666
+ zip_output_stream.put_next_entry(self)
581
667
  elsif @filepath
582
- zip_output_stream.put_next_entry(self, nil, nil, compression_method || ::Zip::Entry::DEFLATED)
583
- get_input_stream { |is| ::Zip::IOExtras.copy_stream(zip_output_stream, is) }
668
+ zip_output_stream.put_next_entry(self)
669
+ get_input_stream do |is|
670
+ ::Zip::IOExtras.copy_stream(zip_output_stream, is)
671
+ end
584
672
  else
585
673
  zip_output_stream.copy_raw_entry(self)
586
674
  end
@@ -601,7 +689,7 @@ module Zip
601
689
  end
602
690
 
603
691
  def clean_up
604
- # By default, do nothing
692
+ @dirty = false # Any changes are written at this point.
605
693
  end
606
694
 
607
695
  private
@@ -614,9 +702,9 @@ module Zip
614
702
 
615
703
  def create_file(dest_path, _continue_on_exists_proc = proc { Zip.continue_on_exists_proc })
616
704
  if ::File.exist?(dest_path) && !yield(self, dest_path)
617
- raise ::Zip::DestinationFileExistsError,
618
- "Destination '#{dest_path}' already exists"
705
+ raise ::Zip::DestinationExistsError, dest_path
619
706
  end
707
+
620
708
  ::File.open(dest_path, 'wb') do |os|
621
709
  get_input_stream do |is|
622
710
  bytes_written = 0
@@ -627,10 +715,10 @@ module Zip
627
715
  bytes_written += buf.bytesize
628
716
  next unless bytes_written > size && !warned
629
717
 
630
- message = "entry '#{name}' should be #{size}B, but is larger when inflated."
631
- raise ::Zip::EntrySizeError, message if ::Zip.validate_entry_sizes
718
+ error = ::Zip::EntrySizeError.new(self)
719
+ raise error if ::Zip.validate_entry_sizes
632
720
 
633
- warn "WARNING: #{message}"
721
+ warn "WARNING: #{error.message}"
634
722
  warned = true
635
723
  end
636
724
  end
@@ -643,14 +731,11 @@ module Zip
643
731
  return if ::File.directory?(dest_path)
644
732
 
645
733
  if ::File.exist?(dest_path)
646
- if block_given? && yield(self, dest_path)
647
- ::FileUtils.rm_f dest_path
648
- else
649
- raise ::Zip::DestinationFileExistsError,
650
- "Cannot create directory '#{dest_path}'. " \
651
- 'A file already exists with that name'
652
- end
734
+ raise ::Zip::DestinationExistsError, dest_path unless block_given? && yield(self, dest_path)
735
+
736
+ ::FileUtils.rm_f dest_path
653
737
  end
738
+
654
739
  ::FileUtils.mkdir_p(dest_path)
655
740
  set_extra_attributes_on_path(dest_path)
656
741
  end
@@ -665,52 +750,70 @@ module Zip
665
750
  # apply missing data from the zip64 extra information field, if present
666
751
  # (required when file sizes exceed 2**32, but can be used for all files)
667
752
  def parse_zip64_extra(for_local_header) #:nodoc:all
668
- return if @extra['Zip64'].nil?
753
+ return unless zip64?
669
754
 
670
755
  if for_local_header
671
756
  @size, @compressed_size = @extra['Zip64'].parse(@size, @compressed_size)
672
757
  else
673
- @size, @compressed_size, @local_header_offset = @extra['Zip64'].parse(@size, @compressed_size, @local_header_offset)
758
+ @size, @compressed_size, @local_header_offset = @extra['Zip64'].parse(
759
+ @size, @compressed_size, @local_header_offset
760
+ )
674
761
  end
675
762
  end
676
763
 
677
- def data_descriptor_size
678
- (@gp_flags & 0x0008) > 0 ? 16 : 0
764
+ # For DEFLATED compression *only*: set the general purpose flags 1 and 2 to
765
+ # indicate compression level. This seems to be mainly cosmetic but they are
766
+ # generally set by other tools - including in docx files. It is these flags
767
+ # that are used by commandline tools (and elsewhere) to give an indication
768
+ # of how compressed a file is. See the PKWARE APPNOTE for more information:
769
+ # https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
770
+ #
771
+ # It's safe to simply OR these flags here as compression_level is read only.
772
+ def set_compression_level_flags
773
+ return unless compression_method == DEFLATED
774
+
775
+ case @compression_level
776
+ when 1
777
+ @gp_flags |= COMPRESSION_LEVEL_SUPERFAST_GPFLAG
778
+ when 2
779
+ @gp_flags |= COMPRESSION_LEVEL_FAST_GPFLAG
780
+ when 8, 9
781
+ @gp_flags |= COMPRESSION_LEVEL_MAX_GPFLAG
782
+ end
679
783
  end
680
784
 
681
- # create a zip64 extra information field if we need one
682
- def prep_zip64_extra(for_local_header) #:nodoc:all
785
+ # rubocop:disable Style/GuardClause
786
+ def prep_local_zip64_extra
683
787
  return unless ::Zip.write_zip64_support
788
+ return if (!zip64? && @size && @size < 0xFFFFFFFF) || !file?
684
789
 
685
- need_zip64 = @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
686
- need_zip64 ||= @local_header_offset >= 0xFFFFFFFF unless for_local_header
687
- if need_zip64
790
+ # Might not know size here, so need ZIP64 just in case.
791
+ # If we already have a ZIP64 extra (placeholder) then we must fill it in.
792
+ if zip64? || @size.nil? || @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
688
793
  @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
689
- @extra.delete('Zip64Placeholder')
690
- zip64 = @extra.create('Zip64')
691
- if for_local_header
692
- # local header always includes size and compressed size
693
- zip64.original_size = @size
694
- zip64.compressed_size = @compressed_size
695
- else
696
- # central directory entry entries include whichever fields are necessary
697
- zip64.original_size = @size if @size >= 0xFFFFFFFF
698
- zip64.compressed_size = @compressed_size if @compressed_size >= 0xFFFFFFFF
699
- zip64.relative_header_offset = @local_header_offset if @local_header_offset >= 0xFFFFFFFF
700
- end
701
- else
702
- @extra.delete('Zip64')
794
+ zip64 = @extra['Zip64'] || @extra.create('Zip64')
703
795
 
704
- # if this is a local header entry, create a placeholder
705
- # so we have room to write a zip64 extra field afterward
706
- # (we won't know if it's needed until the file data is written)
707
- if for_local_header
708
- @extra.create('Zip64Placeholder')
709
- else
710
- @extra.delete('Zip64Placeholder')
711
- end
796
+ # Local header always includes size and compressed size.
797
+ zip64.original_size = @size || 0
798
+ zip64.compressed_size = @compressed_size
799
+ end
800
+ end
801
+
802
+ def prep_cdir_zip64_extra
803
+ return unless ::Zip.write_zip64_support
804
+
805
+ if (@size && @size >= 0xFFFFFFFF) || @compressed_size >= 0xFFFFFFFF ||
806
+ @local_header_offset >= 0xFFFFFFFF
807
+ @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
808
+ zip64 = @extra['Zip64'] || @extra.create('Zip64')
809
+
810
+ # Central directory entry entries include whichever fields are necessary.
811
+ zip64.original_size = @size if @size && @size >= 0xFFFFFFFF
812
+ zip64.compressed_size = @compressed_size if @compressed_size >= 0xFFFFFFFF
813
+ zip64.relative_header_offset = @local_header_offset if @local_header_offset >= 0xFFFFFFFF
712
814
  end
713
815
  end
816
+ # rubocop:enable Style/GuardClause
714
817
  end
715
818
  end
716
819