rubyzip 2.3.1 → 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 +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