rubyzip 2.4.rc1 → 3.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +368 -0
  3. data/README.md +112 -37
  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 +32 -3
  16. data/lib/zip/entry.rb +263 -199
  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 +143 -264
  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 +7 -5
  36. data/lib/zip/input_stream.rb +44 -39
  37. data/lib/zip/ioextras/abstract_input_stream.rb +14 -9
  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 +47 -48
  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 -16
  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 +81 -46
  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,59 +54,54 @@ 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
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
56
- def initialize(zipfile = nil, name = nil, *args)
57
- name ||= ''
58
- 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)
59
81
 
60
82
  set_default_vars_values
61
83
  @fstype = ::Zip::RUNNING_ON_WINDOWS ? ::Zip::FSTYPE_FAT : ::Zip::FSTYPE_UNIX
62
84
 
63
- @zipfile = zipfile || ''
64
- @name = name
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
85
-
86
- @ftype = name_is_directory? ? :directory : :file
87
- @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
88
104
  end
89
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
90
105
 
91
106
  def encrypted?
92
107
  gp_flags & 1 == 1
@@ -96,32 +111,76 @@ module Zip
96
111
  gp_flags & 8 == 8
97
112
  end
98
113
 
99
- def time
100
- if @extra['UniversalTime']
101
- @extra['UniversalTime'].mtime
102
- elsif @extra['NTFS']
103
- @extra['NTFS'].mtime
104
- else
105
- # Standard time field in central directory has local time
106
- # under archive creator. Then, we can't get timezone.
107
- @time
108
- 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)
109
129
  end
110
130
 
111
131
  alias mtime time
112
132
 
113
- 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
114
143
  unless @extra.member?('UniversalTime') || @extra.member?('NTFS')
115
144
  @extra.create('UniversalTime')
116
145
  end
117
- (@extra['UniversalTime'] || @extra['NTFS']).mtime = value
118
- @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?
119
176
  end
120
177
 
121
178
  def file_type_is?(type)
122
- raise InternalError, "current filetype is unknown: #{inspect}" unless @ftype
179
+ ftype == type
180
+ end
123
181
 
124
- @ftype == type
182
+ def ftype # :nodoc:
183
+ @ftype ||= name_is_directory? ? :directory : :file
125
184
  end
126
185
 
127
186
  # Dynamic checkers
@@ -143,8 +202,9 @@ module Zip
143
202
  return false unless cleanpath.relative?
144
203
 
145
204
  root = ::File::SEPARATOR
146
- naive_expanded_path = ::File.join(root, cleanpath.to_s)
147
- ::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)
148
208
  end
149
209
 
150
210
  def local_entry_offset #:nodoc:all
@@ -173,7 +233,10 @@ module Zip
173
233
  return if @local_header_size.nil?
174
234
 
175
235
  new_size = calculate_local_header_size
176
- 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})"
177
240
  end
178
241
 
179
242
  def cdir_header_size #:nodoc:all
@@ -182,27 +245,7 @@ module Zip
182
245
  end
183
246
 
184
247
  def next_header_offset #:nodoc:all
185
- local_entry_offset + compressed_size + data_descriptor_size
186
- end
187
-
188
- # Extracts entry to file dest_path (defaults to @name).
189
- # NB: The caller is responsible for making sure dest_path is safe, if it
190
- # is passed.
191
- def extract(dest_path = nil, &block)
192
- Zip.warn_about_v3_api('Zip::Entry#extract')
193
-
194
- if dest_path.nil? && !name_safe?
195
- warn "WARNING: skipped '#{@name}' as unsafe."
196
- return self
197
- end
198
-
199
- dest_path ||= @name
200
- block ||= proc { ::Zip.on_exists_proc }
201
-
202
- raise "unknown file type #{inspect}" unless directory? || file? || symlink?
203
-
204
- __send__("create_#{@ftype}", dest_path, &block)
205
- self
248
+ local_entry_offset + compressed_size
206
249
  end
207
250
 
208
251
  # Extracts this entry to a file at `entry_path`, with
@@ -210,7 +253,7 @@ module Zip
210
253
  #
211
254
  # NB: The caller is responsible for making sure `destination_directory` is
212
255
  # safe, if it is passed.
213
- def extract_v3(entry_path = @name, destination_directory: '.', &block)
256
+ def extract(entry_path = @name, destination_directory: '.', &block)
214
257
  dest_dir = ::File.absolute_path(destination_directory || '.')
215
258
  extract_path = ::File.absolute_path(::File.join(dest_dir, entry_path))
216
259
 
@@ -223,7 +266,7 @@ module Zip
223
266
 
224
267
  raise "unknown file type #{inspect}" unless directory? || file? || symlink?
225
268
 
226
- __send__(:"create_#{ftype}", extract_path, &block)
269
+ __send__("create_#{ftype}", extract_path, &block)
227
270
  self
228
271
  end
229
272
 
@@ -232,18 +275,6 @@ module Zip
232
275
  end
233
276
 
234
277
  class << self
235
- def read_zip_short(io) # :nodoc:
236
- io.read(2).unpack1('v')
237
- end
238
-
239
- def read_zip_long(io) # :nodoc:
240
- io.read(4).unpack1('V')
241
- end
242
-
243
- def read_zip_64_long(io) # :nodoc:
244
- io.read(8).unpack1('Q<')
245
- end
246
-
247
278
  def read_c_dir_entry(io) #:nodoc:all
248
279
  path = if io.respond_to?(:path)
249
280
  io.path
@@ -261,6 +292,8 @@ module Zip
261
292
  entry = new(io)
262
293
  entry.read_local_entry(io)
263
294
  entry
295
+ rescue SplitArchiveError
296
+ raise
264
297
  rescue Error
265
298
  nil
266
299
  end
@@ -282,6 +315,7 @@ module Zip
282
315
  end
283
316
 
284
317
  def read_local_entry(io) #:nodoc:all
318
+ @dirty = false # No changes at this point.
285
319
  @local_header_offset = io.tell
286
320
 
287
321
  static_sized_fields_buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
@@ -292,30 +326,32 @@ module Zip
292
326
 
293
327
  unpack_local_entry(static_sized_fields_buf)
294
328
 
295
- unless @header_signature == ::Zip::LOCAL_ENTRY_SIGNATURE
296
- 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}'"
297
335
  end
298
336
 
299
337
  set_time(@last_mod_date, @last_mod_time)
300
338
 
301
339
  @name = io.read(@name_length)
302
- extra = io.read(@extra_length)
303
-
304
- @name.tr!('\\', '/')
305
340
  if ::Zip.force_entry_names_encoding
306
341
  @name.force_encoding(::Zip.force_entry_names_encoding)
307
342
  end
343
+ @name.tr!('\\', '/') # Normalise filepath separators after encoding set.
344
+
345
+ # We need to do this here because `initialize` has so many side-effects.
346
+ # :-(
347
+ @ftype = name_is_directory? ? :directory : :file
308
348
 
349
+ extra = io.read(@extra_length)
309
350
  if extra && extra.bytesize != @extra_length
310
351
  raise ::Zip::Error, 'Truncated local zip entry header'
311
352
  end
312
353
 
313
- if @extra.kind_of?(::Zip::ExtraField)
314
- @extra.merge(extra) if extra
315
- else
316
- @extra = ::Zip::ExtraField.new(extra)
317
- end
318
-
354
+ read_extra_field(extra, local: true)
319
355
  parse_zip64_extra(true)
320
356
  @local_header_size = calculate_local_header_size
321
357
  end
@@ -325,18 +361,18 @@ module Zip
325
361
  [::Zip::LOCAL_ENTRY_SIGNATURE,
326
362
  @version_needed_to_extract, # version needed to extract
327
363
  @gp_flags, # @gp_flags
328
- @compression_method,
364
+ compression_method,
329
365
  @time.to_binary_dos_time, # @last_mod_time
330
366
  @time.to_binary_dos_date, # @last_mod_date
331
367
  @crc,
332
368
  zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
333
- zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
369
+ zip64 && zip64.original_size ? 0xFFFFFFFF : (@size || 0),
334
370
  name_size,
335
371
  @extra ? @extra.local_size : 0].pack('VvvvvvVVVvv')
336
372
  end
337
373
 
338
- def write_local_entry(io, rewrite = false) #:nodoc:all
339
- prep_zip64_extra(true)
374
+ def write_local_entry(io, rewrite: false) #:nodoc:all
375
+ prep_local_zip64_extra
340
376
  verify_local_header_size! if rewrite
341
377
  @local_header_offset = io.tell
342
378
 
@@ -383,8 +419,9 @@ module Zip
383
419
  when ::Zip::FILE_TYPE_SYMLINK
384
420
  :symlink
385
421
  else
386
- # best case guess for whether it is a file or not
387
- # 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.
388
425
  if name_is_directory?
389
426
  :directory
390
427
  else
@@ -401,13 +438,13 @@ module Zip
401
438
  end
402
439
 
403
440
  def check_c_dir_entry_static_header_length(buf)
404
- return if buf.bytesize == ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
441
+ return unless buf.nil? || buf.bytesize != ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
405
442
 
406
443
  raise Error, 'Premature end of file. Not enough data for zip cdir entry header'
407
444
  end
408
445
 
409
446
  def check_c_dir_entry_signature
410
- return if header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
447
+ return if @header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
411
448
 
412
449
  raise Error, "Zip local header magic not found at location '#{local_header_offset}'"
413
450
  end
@@ -418,25 +455,29 @@ module Zip
418
455
  raise ::Zip::Error, 'Truncated cdir zip entry header'
419
456
  end
420
457
 
421
- def read_c_dir_extra_field(io)
458
+ def read_extra_field(buf, local: false)
422
459
  if @extra.kind_of?(::Zip::ExtraField)
423
- @extra.merge(io.read(@extra_length))
460
+ @extra.merge(buf, local: local) if buf
424
461
  else
425
- @extra = ::Zip::ExtraField.new(io.read(@extra_length))
462
+ @extra = ::Zip::ExtraField.new(buf, local: local)
426
463
  end
427
464
  end
428
465
 
429
466
  def read_c_dir_entry(io) #:nodoc:all
467
+ @dirty = false # No changes at this point.
430
468
  static_sized_fields_buf = io.read(::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH)
431
469
  check_c_dir_entry_static_header_length(static_sized_fields_buf)
432
470
  unpack_c_dir_entry(static_sized_fields_buf)
433
471
  check_c_dir_entry_signature
434
472
  set_time(@last_mod_date, @last_mod_time)
473
+
435
474
  @name = io.read(@name_length)
436
475
  if ::Zip.force_entry_names_encoding
437
476
  @name.force_encoding(::Zip.force_entry_names_encoding)
438
477
  end
439
- read_c_dir_extra_field(io)
478
+ @name.tr!('\\', '/') # Normalise filepath separators after encoding set.
479
+
480
+ read_extra_field(io.read(@extra_length))
440
481
  @comment = io.read(@comment_length)
441
482
  check_c_dir_entry_comment_size
442
483
  set_ftype_from_c_dir_entry
@@ -452,27 +493,27 @@ module Zip
452
493
  end
453
494
 
454
495
  def get_extra_attributes_from_path(path) # :nodoc:
455
- 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
456
499
 
457
- stat = file_stat(path)
458
500
  @unix_uid = stat.uid
459
501
  @unix_gid = stat.gid
460
502
  @unix_perms = stat.mode & 0o7777
461
- @time = ::Zip::DOSTime.from_time(stat.mtime)
462
503
  end
463
504
 
505
+ # rubocop:disable Style/GuardClause
464
506
  def set_unix_attributes_on_path(dest_path)
465
- # ignore setuid/setgid bits by default. honor if @restore_ownership
466
- unix_perms_mask = 0o1777
467
- unix_perms_mask = 0o7777 if @restore_ownership
468
- ::FileUtils.chmod(@unix_perms & unix_perms_mask, dest_path) if @restore_permissions && @unix_perms
469
- ::FileUtils.chown(@unix_uid, @unix_gid, dest_path) if @restore_ownership && @unix_uid && @unix_gid && ::Process.egid == 0
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
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
475
515
  end
516
+ # rubocop:enable Style/GuardClause
476
517
 
477
518
  def set_extra_attributes_on_path(dest_path) # :nodoc:
478
519
  return unless file? || directory?
@@ -481,6 +522,11 @@ module Zip
481
522
  when ::Zip::FSTYPE_UNIX
482
523
  set_unix_attributes_on_path(dest_path)
483
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
484
530
  end
485
531
 
486
532
  def pack_c_dir_entry
@@ -491,12 +537,12 @@ module Zip
491
537
  @fstype, # filesystem type
492
538
  @version_needed_to_extract, # @versionNeededToExtract
493
539
  @gp_flags, # @gp_flags
494
- @compression_method,
540
+ compression_method,
495
541
  @time.to_binary_dos_time, # @last_mod_time
496
542
  @time.to_binary_dos_date, # @last_mod_date
497
543
  @crc,
498
544
  zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
499
- zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
545
+ zip64 && zip64.original_size ? 0xFFFFFFFF : (@size || 0),
500
546
  name_size,
501
547
  @extra ? @extra.c_dir_size : 0,
502
548
  comment_size,
@@ -511,10 +557,11 @@ module Zip
511
557
  end
512
558
 
513
559
  def write_c_dir_entry(io) #:nodoc:all
514
- prep_zip64_extra(false)
560
+ prep_cdir_zip64_extra
561
+
515
562
  case @fstype
516
563
  when ::Zip::FSTYPE_UNIX
517
- ft = case @ftype
564
+ ft = case ftype
518
565
  when :file
519
566
  @unix_perms ||= 0o644
520
567
  ::Zip::FILE_TYPE_FILE
@@ -542,10 +589,9 @@ module Zip
542
589
  return false unless other.class == self.class
543
590
 
544
591
  # Compares contents of local entry and exposed fields
545
- 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|
546
593
  other.__send__(k.to_sym) == __send__(k.to_sym)
547
594
  end
548
- keys_equal && time == other.time
549
595
  end
550
596
 
551
597
  def <=>(other)
@@ -555,26 +601,26 @@ module Zip
555
601
  # Returns an IO like object for the given ZipEntry.
556
602
  # Warning: may behave weird with symlinks.
557
603
  def get_input_stream(&block)
558
- if @ftype == :directory
559
- yield ::Zip::NullInputStream if block_given?
604
+ if ftype == :directory
605
+ yield ::Zip::NullInputStream if block
560
606
  ::Zip::NullInputStream
561
607
  elsif @filepath
562
- case @ftype
608
+ case ftype
563
609
  when :file
564
610
  ::File.open(@filepath, 'rb', &block)
565
611
  when :symlink
566
612
  linkpath = ::File.readlink(@filepath)
567
613
  stringio = ::StringIO.new(linkpath)
568
- yield(stringio) if block_given?
614
+ yield(stringio) if block
569
615
  stringio
570
616
  else
571
- raise "unknown @file_type #{@ftype}"
617
+ raise "unknown @file_type #{ftype}"
572
618
  end
573
619
  else
574
- zis = ::Zip::InputStream.new(@zipfile, local_header_offset)
620
+ zis = ::Zip::InputStream.new(@zipfile, offset: local_header_offset)
575
621
  zis.instance_variable_set(:@complete_entry, self)
576
622
  zis.get_next_entry
577
- if block_given?
623
+ if block
578
624
  begin
579
625
  yield(zis)
580
626
  ensure
@@ -611,15 +657,18 @@ module Zip
611
657
  end
612
658
 
613
659
  @filepath = src_path
660
+ @size = stat.size
614
661
  get_extra_attributes_from_path(@filepath)
615
662
  end
616
663
 
617
664
  def write_to_zip_output_stream(zip_output_stream) #:nodoc:all
618
- if @ftype == :directory
619
- zip_output_stream.put_next_entry(self, nil, nil, ::Zip::Entry::STORED)
665
+ if ftype == :directory
666
+ zip_output_stream.put_next_entry(self)
620
667
  elsif @filepath
621
- zip_output_stream.put_next_entry(self, nil, nil, compression_method || ::Zip::Entry::DEFLATED)
622
- 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
623
672
  else
624
673
  zip_output_stream.copy_raw_entry(self)
625
674
  end
@@ -640,7 +689,7 @@ module Zip
640
689
  end
641
690
 
642
691
  def clean_up
643
- # By default, do nothing
692
+ @dirty = false # Any changes are written at this point.
644
693
  end
645
694
 
646
695
  private
@@ -653,9 +702,9 @@ module Zip
653
702
 
654
703
  def create_file(dest_path, _continue_on_exists_proc = proc { Zip.continue_on_exists_proc })
655
704
  if ::File.exist?(dest_path) && !yield(self, dest_path)
656
- raise ::Zip::DestinationFileExistsError,
657
- "Destination '#{dest_path}' already exists"
705
+ raise ::Zip::DestinationExistsError, dest_path
658
706
  end
707
+
659
708
  ::File.open(dest_path, 'wb') do |os|
660
709
  get_input_stream do |is|
661
710
  bytes_written = 0
@@ -666,10 +715,10 @@ module Zip
666
715
  bytes_written += buf.bytesize
667
716
  next unless bytes_written > size && !warned
668
717
 
669
- message = "entry '#{name}' should be #{size}B, but is larger when inflated."
670
- raise ::Zip::EntrySizeError, message if ::Zip.validate_entry_sizes
718
+ error = ::Zip::EntrySizeError.new(self)
719
+ raise error if ::Zip.validate_entry_sizes
671
720
 
672
- warn "WARNING: #{message}"
721
+ warn "WARNING: #{error.message}"
673
722
  warned = true
674
723
  end
675
724
  end
@@ -682,14 +731,11 @@ module Zip
682
731
  return if ::File.directory?(dest_path)
683
732
 
684
733
  if ::File.exist?(dest_path)
685
- if block_given? && yield(self, dest_path)
686
- ::FileUtils.rm_f dest_path
687
- else
688
- raise ::Zip::DestinationFileExistsError,
689
- "Cannot create directory '#{dest_path}'. " \
690
- 'A file already exists with that name'
691
- end
734
+ raise ::Zip::DestinationExistsError, dest_path unless block_given? && yield(self, dest_path)
735
+
736
+ ::FileUtils.rm_f dest_path
692
737
  end
738
+
693
739
  ::FileUtils.mkdir_p(dest_path)
694
740
  set_extra_attributes_on_path(dest_path)
695
741
  end
@@ -704,52 +750,70 @@ module Zip
704
750
  # apply missing data from the zip64 extra information field, if present
705
751
  # (required when file sizes exceed 2**32, but can be used for all files)
706
752
  def parse_zip64_extra(for_local_header) #:nodoc:all
707
- return if @extra['Zip64'].nil?
753
+ return unless zip64?
708
754
 
709
755
  if for_local_header
710
756
  @size, @compressed_size = @extra['Zip64'].parse(@size, @compressed_size)
711
757
  else
712
- @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
+ )
761
+ end
762
+ end
763
+
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
713
782
  end
714
783
  end
715
784
 
716
- def data_descriptor_size
717
- (@gp_flags & 0x0008) > 0 ? 16 : 0
785
+ # rubocop:disable Style/GuardClause
786
+ def prep_local_zip64_extra
787
+ return unless ::Zip.write_zip64_support
788
+ return if (!zip64? && @size && @size < 0xFFFFFFFF) || !file?
789
+
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
793
+ @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
794
+ zip64 = @extra['Zip64'] || @extra.create('Zip64')
795
+
796
+ # Local header always includes size and compressed size.
797
+ zip64.original_size = @size || 0
798
+ zip64.compressed_size = @compressed_size
799
+ end
718
800
  end
719
801
 
720
- # create a zip64 extra information field if we need one
721
- def prep_zip64_extra(for_local_header) #:nodoc:all
802
+ def prep_cdir_zip64_extra
722
803
  return unless ::Zip.write_zip64_support
723
804
 
724
- need_zip64 = @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
725
- need_zip64 ||= @local_header_offset >= 0xFFFFFFFF unless for_local_header
726
- if need_zip64
805
+ if (@size && @size >= 0xFFFFFFFF) || @compressed_size >= 0xFFFFFFFF ||
806
+ @local_header_offset >= 0xFFFFFFFF
727
807
  @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
728
- @extra.delete('Zip64Placeholder')
729
- zip64 = @extra.create('Zip64')
730
- if for_local_header
731
- # local header always includes size and compressed size
732
- zip64.original_size = @size
733
- zip64.compressed_size = @compressed_size
734
- else
735
- # central directory entry entries include whichever fields are necessary
736
- zip64.original_size = @size if @size >= 0xFFFFFFFF
737
- zip64.compressed_size = @compressed_size if @compressed_size >= 0xFFFFFFFF
738
- zip64.relative_header_offset = @local_header_offset if @local_header_offset >= 0xFFFFFFFF
739
- end
740
- else
741
- @extra.delete('Zip64')
808
+ zip64 = @extra['Zip64'] || @extra.create('Zip64')
742
809
 
743
- # if this is a local header entry, create a placeholder
744
- # so we have room to write a zip64 extra field afterward
745
- # (we won't know if it's needed until the file data is written)
746
- if for_local_header
747
- @extra.create('Zip64Placeholder')
748
- else
749
- @extra.delete('Zip64Placeholder')
750
- end
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
751
814
  end
752
815
  end
816
+ # rubocop:enable Style/GuardClause
753
817
  end
754
818
  end
755
819