rubyzip 2.4.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 +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 +262 -198
  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 +144 -265
  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 -20
  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 +84 -50
  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
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