rubyzip 2.4.1 → 3.7.0

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +549 -0
  3. data/LICENSE.md +24 -0
  4. data/README.md +250 -44
  5. data/Rakefile +15 -13
  6. data/lib/rubyzip.rb +3 -0
  7. data/lib/zip/central_directory.rb +194 -125
  8. data/lib/zip/compressor.rb +3 -1
  9. data/lib/zip/constants.rb +30 -21
  10. data/lib/zip/crypto/aes_encryption.rb +213 -0
  11. data/lib/zip/crypto/decrypted_io.rb +27 -15
  12. data/lib/zip/crypto/encryption.rb +13 -2
  13. data/lib/zip/crypto/null_encryption.rb +5 -13
  14. data/lib/zip/crypto/traditional_encryption.rb +13 -7
  15. data/lib/zip/decompressor.rb +4 -3
  16. data/lib/zip/deflater.rb +12 -8
  17. data/lib/zip/dirtyable.rb +32 -0
  18. data/lib/zip/dos_time.rb +64 -6
  19. data/lib/zip/entry.rb +432 -270
  20. data/lib/zip/entry_set.rb +11 -9
  21. data/lib/zip/errors.rb +152 -16
  22. data/lib/zip/extra_field/aes.rb +50 -0
  23. data/lib/zip/extra_field/generic.rb +10 -11
  24. data/lib/zip/extra_field/ntfs.rb +6 -4
  25. data/lib/zip/extra_field/old_unix.rb +5 -3
  26. data/lib/zip/extra_field/universal_time.rb +3 -1
  27. data/lib/zip/extra_field/unix.rb +7 -5
  28. data/lib/zip/extra_field/unknown.rb +35 -0
  29. data/lib/zip/extra_field/zip64.rb +19 -5
  30. data/lib/zip/extra_field.rb +25 -23
  31. data/lib/zip/file.rb +261 -262
  32. data/lib/zip/file_split.rb +91 -0
  33. data/lib/zip/filesystem/dir.rb +86 -0
  34. data/lib/zip/filesystem/directory_iterator.rb +48 -0
  35. data/lib/zip/filesystem/file.rb +263 -0
  36. data/lib/zip/filesystem/file_stat.rb +110 -0
  37. data/lib/zip/filesystem/zip_file_name_mapper.rb +81 -0
  38. data/lib/zip/filesystem.rb +27 -596
  39. data/lib/zip/inflater.rb +13 -10
  40. data/lib/zip/input_stream.rb +102 -61
  41. data/lib/zip/ioextras/abstract_input_stream.rb +156 -58
  42. data/lib/zip/ioextras/abstract_output_stream.rb +15 -3
  43. data/lib/zip/ioextras/fake_io.rb +39 -0
  44. data/lib/zip/ioextras.rb +12 -16
  45. data/lib/zip/null_compressor.rb +3 -1
  46. data/lib/zip/null_decompressor.rb +6 -3
  47. data/lib/zip/null_input_stream.rb +3 -1
  48. data/lib/zip/output_stream.rb +76 -64
  49. data/lib/zip/pass_thru_compressor.rb +6 -3
  50. data/lib/zip/pass_thru_decompressor.rb +12 -9
  51. data/lib/zip/streamable_directory.rb +3 -1
  52. data/lib/zip/streamable_stream.rb +4 -1
  53. data/lib/zip/version.rb +4 -1
  54. data/lib/zip.rb +28 -23
  55. data/rubyzip.gemspec +39 -0
  56. data/samples/example.rb +8 -3
  57. data/samples/example_filesystem.rb +3 -2
  58. data/samples/example_recursive.rb +3 -1
  59. data/samples/gtk_ruby_zip.rb +5 -3
  60. data/samples/qtzip.rb +7 -6
  61. data/samples/write_simple.rb +2 -1
  62. data/samples/zipfind.rb +1 -0
  63. metadata +88 -52
  64. data/TODO +0 -15
  65. data/lib/zip/extra_field/zip64_placeholder.rb +0 -15
data/lib/zip/entry.rb CHANGED
@@ -1,21 +1,45 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pathname'
4
+
5
+ require_relative 'constants'
6
+ require_relative 'dirtyable'
7
+
2
8
  module Zip
9
+ # Zip::Entry represents an entry in a Zip archive.
3
10
  class Entry
4
- STORED = 0
5
- DEFLATED = 8
11
+ include Dirtyable
12
+
13
+ # Constant used to specify that the entry is stored (i.e., not compressed).
14
+ STORED = ::Zip::COMPRESSION_METHOD_STORE
15
+
16
+ # Constant used to specify that the entry is deflated (i.e., compressed).
17
+ DEFLATED = ::Zip::COMPRESSION_METHOD_DEFLATE
18
+
6
19
  # Language encoding flag (EFS) bit
7
- EFS = 0b100000000000
8
-
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:
17
-
18
- def set_default_vars_values
20
+ EFS = 0b100000000000 # :nodoc:
21
+
22
+ # Compression level flags (used as part of the gp flags).
23
+ COMPRESSION_LEVEL_SUPERFAST_GPFLAG = 0b110 # :nodoc:
24
+ COMPRESSION_LEVEL_FAST_GPFLAG = 0b100 # :nodoc:
25
+ COMPRESSION_LEVEL_MAX_GPFLAG = 0b010 # :nodoc:
26
+
27
+ attr_accessor :comment, :compressed_size, :follow_symlinks, :name,
28
+ :restore_ownership, :restore_permissions, :restore_times,
29
+ :unix_gid, :unix_perms, :unix_uid
30
+
31
+ attr_accessor :crc, :external_file_attributes, :fstype, :gp_flags,
32
+ :internal_file_attributes, :local_header_offset # :nodoc:
33
+
34
+ attr_reader :extra, :compression_level, :filepath # :nodoc:
35
+
36
+ attr_writer :size # :nodoc:
37
+
38
+ mark_dirty :comment=, :compressed_size=, :external_file_attributes=,
39
+ :fstype=, :gp_flags=, :name=, :size=,
40
+ :unix_gid=, :unix_perms=, :unix_uid=
41
+
42
+ def set_default_vars_values # :nodoc:
19
43
  @local_header_offset = 0
20
44
  @local_header_size = nil # not known until local entry is created or read
21
45
  @internal_file_attributes = 1
@@ -34,175 +58,242 @@ module Zip
34
58
  end
35
59
  @follow_symlinks = false
36
60
 
37
- @restore_times = false
38
- @restore_permissions = false
39
- @restore_ownership = false
61
+ @restore_times = DEFAULT_RESTORE_OPTIONS[:restore_times]
62
+ @restore_permissions = DEFAULT_RESTORE_OPTIONS[:restore_permissions]
63
+ @restore_ownership = DEFAULT_RESTORE_OPTIONS[:restore_ownership]
40
64
  # BUG: need an extra field to support uid/gid's
41
65
  @unix_uid = nil
42
66
  @unix_gid = nil
43
67
  @unix_perms = nil
44
- # @posix_acl = nil
45
- # @ntfs_acl = nil
46
- @dirty = false
47
68
  end
48
69
 
49
- def check_name(name)
50
- return unless name.start_with?('/')
51
-
52
- raise ::Zip::EntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
70
+ def check_name(name) # :nodoc:
71
+ raise EntryNameError, name if name.start_with?('/')
72
+ raise EntryNameError if name.length > 65_535
53
73
  end
54
74
 
55
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
56
- def initialize(zipfile = nil, name = nil, *args)
57
- name ||= ''
58
- check_name(name)
75
+ # Create a new Zip::Entry.
76
+ def initialize(
77
+ zipfile = '', name = '',
78
+ comment: '', size: nil, compressed_size: 0, crc: 0,
79
+ compression_method: DEFLATED,
80
+ compression_level: ::Zip.default_compression,
81
+ time: ::Zip::DOSTime.now, extra: ::Zip::ExtraField.new
82
+ )
83
+ super()
84
+ @name = name
85
+ check_name(@name)
59
86
 
60
87
  set_default_vars_values
61
88
  @fstype = ::Zip::RUNNING_ON_WINDOWS ? ::Zip::FSTYPE_FAT : ::Zip::FSTYPE_UNIX
62
89
 
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?
90
+ @zipfile = zipfile
91
+ @comment = comment || ''
92
+ @compression_method = compression_method || DEFLATED
93
+ @compression_level = compression_level || ::Zip.default_compression
94
+ @compressed_size = compressed_size || 0
95
+ @crc = crc || 0
96
+ @size = size
97
+ @time = case time
98
+ when ::Zip::DOSTime
99
+ time
100
+ when Time
101
+ ::Zip::DOSTime.from_time(time)
102
+ else
103
+ ::Zip::DOSTime.now
104
+ end
105
+ @extra =
106
+ extra.kind_of?(ExtraField) ? extra : ExtraField.new(extra.to_s)
107
+
108
+ set_compression_level_flags
109
+ end
110
+
111
+ # Is this entry encrypted?
112
+ def encrypted?
113
+ gp_flags & 1 == 1
114
+ end
76
115
 
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
116
+ def incomplete? # :nodoc:
117
+ (gp_flags & 8 == 8) && (crc == 0 || size == 0 || compressed_size == 0)
118
+ end
85
119
 
86
- @ftype = name_is_directory? ? :directory : :file
87
- @extra = ::Zip::ExtraField.new(@extra.to_s) unless @extra.kind_of?(::Zip::ExtraField)
120
+ # The uncompressed size of the entry.
121
+ def size
122
+ @size || 0
88
123
  end
89
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
90
124
 
91
- def encrypted?
92
- gp_flags & 1 == 1
125
+ # Get a timestamp component of this entry.
126
+ #
127
+ # Returns modification time by default.
128
+ def time(component: :mtime)
129
+ time =
130
+ if @extra[:universaltime]
131
+ @extra[:universaltime].send(component)
132
+ elsif @extra[:ntfs]
133
+ @extra[:ntfs].send(component)
134
+ end
135
+
136
+ # Standard time field in central directory has local time
137
+ # under archive creator. Then, we can't get timezone.
138
+ time || (@time if component == :mtime)
139
+ end
140
+
141
+ alias mtime time
142
+
143
+ # Get the last access time of this entry, if available.
144
+ def atime
145
+ time(component: :atime)
93
146
  end
94
147
 
95
- def incomplete?
96
- gp_flags & 8 == 8
148
+ # Get the creation time of this entry, if available.
149
+ def ctime
150
+ time(component: :ctime)
97
151
  end
98
152
 
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
153
+ # Set a timestamp component of this entry.
154
+ #
155
+ # Sets modification time by default.
156
+ def time=(value, component: :mtime)
157
+ @dirty = true
158
+ unless @extra.member?(:universaltime) || @extra.member?(:ntfs)
159
+ @extra.create(:universaltime)
108
160
  end
161
+
162
+ value = DOSTime.from_time(value)
163
+ comp = "#{component}=" unless component.to_s.end_with?('=')
164
+ (@extra[:universaltime] || @extra[:ntfs]).send(comp, value)
165
+ @time = value if component == :mtime
109
166
  end
110
167
 
111
- alias mtime time
168
+ alias mtime= time=
112
169
 
113
- def time=(value)
114
- unless @extra.member?('UniversalTime') || @extra.member?('NTFS')
115
- @extra.create('UniversalTime')
116
- end
117
- (@extra['UniversalTime'] || @extra['NTFS']).mtime = value
118
- @time = value
170
+ # Set the last access time of this entry.
171
+ def atime=(value)
172
+ send(:time=, value, component: :atime)
119
173
  end
120
174
 
121
- def file_type_is?(type)
122
- raise InternalError, "current filetype is unknown: #{inspect}" unless @ftype
175
+ # Set the creation time of this entry.
176
+ def ctime=(value)
177
+ send(:time=, value, component: :ctime)
178
+ end
123
179
 
124
- @ftype == type
180
+ # Does this entry return time fields with accurate timezone information?
181
+ def absolute_time?
182
+ @extra.member?(:universaltime) || @extra.member?(:ntfs)
183
+ end
184
+
185
+ # Return the compression method for this entry.
186
+ #
187
+ # Returns STORED if the entry is a directory or if the compression
188
+ # level is 0.
189
+ def compression_method
190
+ return STORED if ftype == :directory || @compression_level == 0
191
+
192
+ @compression_method
193
+ end
194
+
195
+ # Set the compression method for this entry.
196
+ def compression_method=(method)
197
+ @dirty = true
198
+ @compression_method = (ftype == :directory ? STORED : method)
199
+ end
200
+
201
+ # Does this entry use the ZIP64 extensions?
202
+ def zip64?
203
+ !@extra[:zip64].nil?
204
+ end
205
+
206
+ # Is this entry encrypted with AES encryption?
207
+ def aes?
208
+ !@extra[:aes].nil?
209
+ end
210
+
211
+ # Called by `AESEncrypter#prepare_entry` before the local header is first
212
+ # written, so the extra field is in place before its size is measured.
213
+ # Directories have no content to encrypt, so they are left untouched.
214
+ def prep_aes_extra(vendor_version, strength) # :nodoc:
215
+ return if directory?
216
+
217
+ @version_needed_to_extract =
218
+ [@version_needed_to_extract, Zip::VERSION_NEEDED_TO_EXTRACT_AES].max
219
+ aes = @extra[:aes] || @extra.create(:aes)
220
+ aes.vendor_version = vendor_version
221
+ aes.vendor_id = 'AE'
222
+ aes.encryption_strength = strength
223
+ aes.compression_method = compression_method
224
+ end
225
+
226
+ def file_type_is?(type) # :nodoc:
227
+ ftype == type
228
+ end
229
+
230
+ def ftype # :nodoc:
231
+ @ftype ||= name_is_directory? ? :directory : :file
125
232
  end
126
233
 
127
234
  # Dynamic checkers
128
235
  %w[directory file symlink].each do |k|
129
- define_method "#{k}?" do
236
+ define_method :"#{k}?" do
130
237
  file_type_is?(k.to_sym)
131
238
  end
132
239
  end
133
240
 
134
- def name_is_directory? #:nodoc:all
241
+ def name_is_directory? # :nodoc:
135
242
  @name.end_with?('/')
136
243
  end
137
244
 
138
245
  # Is the name a relative path, free of `..` patterns that could lead to
139
246
  # path traversal attacks? This does NOT handle symlinks; if the path
140
247
  # contains symlinks, this check is NOT enough to guarantee safety.
141
- def name_safe?
248
+ def name_safe? # :nodoc:
142
249
  cleanpath = Pathname.new(@name).cleanpath
143
250
  return false unless cleanpath.relative?
144
251
 
145
252
  root = ::File::SEPARATOR
146
- naive_expanded_path = ::File.join(root, cleanpath.to_s)
147
- ::File.absolute_path(cleanpath.to_s, root) == naive_expanded_path
253
+ naive = Regexp.escape(::File.join(root, cleanpath.to_s))
254
+ # Allow for Windows drive mappings at the root.
255
+ ::File.absolute_path(cleanpath.to_s, root).match?(/([A-Z]:)?#{naive}/i)
148
256
  end
149
257
 
150
- def local_entry_offset #:nodoc:all
258
+ def local_entry_offset # :nodoc:
151
259
  local_header_offset + @local_header_size
152
260
  end
153
261
 
154
- def name_size
262
+ def name_size # :nodoc:
155
263
  @name ? @name.bytesize : 0
156
264
  end
157
265
 
158
- def extra_size
266
+ def extra_size # :nodoc:
159
267
  @extra ? @extra.local_size : 0
160
268
  end
161
269
 
162
- def comment_size
270
+ def comment_size # :nodoc:
163
271
  @comment ? @comment.bytesize : 0
164
272
  end
165
273
 
166
- def calculate_local_header_size #:nodoc:all
274
+ def calculate_local_header_size # :nodoc:
167
275
  LOCAL_ENTRY_STATIC_HEADER_LENGTH + name_size + extra_size
168
276
  end
169
277
 
170
278
  # check before rewriting an entry (after file sizes are known)
171
279
  # that we didn't change the header size (and thus clobber file data or something)
172
- def verify_local_header_size!
280
+ def verify_local_header_size! # :nodoc:
173
281
  return if @local_header_size.nil?
174
282
 
175
283
  new_size = calculate_local_header_size
176
- raise Error, "local header size changed (#{@local_header_size} -> #{new_size})" if @local_header_size != new_size
284
+ return unless @local_header_size != new_size
285
+
286
+ raise Error,
287
+ "Local header size changed (#{@local_header_size} -> #{new_size})"
177
288
  end
178
289
 
179
- def cdir_header_size #:nodoc:all
290
+ def cdir_header_size # :nodoc:
180
291
  CDIR_ENTRY_STATIC_HEADER_LENGTH + name_size +
181
292
  (@extra ? @extra.c_dir_size : 0) + comment_size
182
293
  end
183
294
 
184
- 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
295
+ def next_header_offset # :nodoc:
296
+ local_entry_offset + compressed_size
206
297
  end
207
298
 
208
299
  # Extracts this entry to a file at `entry_path`, with
@@ -210,11 +301,20 @@ module Zip
210
301
  #
211
302
  # NB: The caller is responsible for making sure `destination_directory` is
212
303
  # safe, if it is passed.
213
- def extract_v3(entry_path = @name, destination_directory: '.', &block)
304
+ #
305
+ # `create_parent_directories`, if true, creates any missing intermediate
306
+ # directories for a file entry before writing it. This defaults to
307
+ # false: a missing intermediate directory usually means an earlier
308
+ # entry (e.g. a symlink) was deliberately skipped as unsafe, and letting
309
+ # a later entry silently succeed by re-creating that path as a plain
310
+ # directory would mask that. Bulk operations such as `extract_all`,
311
+ # which have no such earlier-entry to skip past, opt into this.
312
+ def extract(entry_path = @name, destination_directory: '.',
313
+ create_parent_directories: false, &block)
214
314
  dest_dir = ::File.absolute_path(destination_directory || '.')
215
315
  extract_path = ::File.absolute_path(::File.join(dest_dir, entry_path))
216
316
 
217
- unless extract_path.start_with?(dest_dir)
317
+ unless extract_path.start_with?(dest_dir + ::File::SEPARATOR) || dest_dir == ::File::SEPARATOR
218
318
  warn "WARNING: skipped extracting '#{@name}' to '#{extract_path}' as unsafe."
219
319
  return self
220
320
  end
@@ -223,28 +323,18 @@ module Zip
223
323
 
224
324
  raise "unknown file type #{inspect}" unless directory? || file? || symlink?
225
325
 
326
+ ::FileUtils.mkdir_p(::File.dirname(extract_path)) if create_parent_directories && file?
327
+
226
328
  __send__(:"create_#{ftype}", extract_path, &block)
227
329
  self
228
330
  end
229
331
 
230
- def to_s
332
+ def to_s # :nodoc:
231
333
  @name
232
334
  end
233
335
 
234
336
  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
- def read_c_dir_entry(io) #:nodoc:all
337
+ def read_c_dir_entry(io) # :nodoc:
248
338
  path = if io.respond_to?(:path)
249
339
  io.path
250
340
  else
@@ -257,16 +347,18 @@ module Zip
257
347
  nil
258
348
  end
259
349
 
260
- def read_local_entry(io)
350
+ def read_local_entry(io) # :nodoc:
261
351
  entry = new(io)
262
352
  entry.read_local_entry(io)
263
353
  entry
354
+ rescue SplitArchiveError
355
+ raise
264
356
  rescue Error
265
357
  nil
266
358
  end
267
359
  end
268
360
 
269
- def unpack_local_entry(buf)
361
+ def unpack_local_entry(buf) # :nodoc:
270
362
  @header_signature,
271
363
  @version,
272
364
  @fstype,
@@ -281,63 +373,76 @@ module Zip
281
373
  @extra_length = buf.unpack('VCCvvvvVVVvv')
282
374
  end
283
375
 
284
- def read_local_entry(io) #:nodoc:all
285
- @local_header_offset = io.tell
376
+ def read_local_entry(io) # :nodoc:
377
+ @dirty = false # No changes at this point.
378
+ current_offset = io.tell
286
379
 
287
- static_sized_fields_buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
380
+ read_local_header_fields(io)
288
381
 
289
- unless static_sized_fields_buf.bytesize == ::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH
290
- raise Error, 'Premature end of file. Not enough data for zip entry local header'
291
- end
382
+ if @header_signature == SPLIT_FILE_SIGNATURE
383
+ raise SplitArchiveError if current_offset.zero?
292
384
 
293
- unpack_local_entry(static_sized_fields_buf)
385
+ # Rewind, skipping the data descriptor, then try to read the local header again.
386
+ current_offset += 16
387
+ io.seek(current_offset)
388
+ read_local_header_fields(io)
389
+ end
294
390
 
295
- unless @header_signature == ::Zip::LOCAL_ENTRY_SIGNATURE
296
- raise ::Zip::Error, "Zip local header magic not found at location '#{local_header_offset}'"
391
+ unless @header_signature == LOCAL_ENTRY_SIGNATURE
392
+ raise Error, "Zip local header magic not found at location '#{current_offset}'"
297
393
  end
298
394
 
395
+ @local_header_offset = current_offset
396
+
299
397
  set_time(@last_mod_date, @last_mod_time)
300
398
 
301
399
  @name = io.read(@name_length)
302
- extra = io.read(@extra_length)
303
-
304
- @name.tr!('\\', '/')
305
400
  if ::Zip.force_entry_names_encoding
306
401
  @name.force_encoding(::Zip.force_entry_names_encoding)
307
402
  end
403
+ @name.tr!('\\', '/') # Normalise filepath separators after encoding set.
404
+
405
+ # We need to do this here because `initialize` has so many side-effects.
406
+ # :-(
407
+ @ftype = name_is_directory? ? :directory : :file
308
408
 
409
+ extra = io.read(@extra_length)
309
410
  if extra && extra.bytesize != @extra_length
310
411
  raise ::Zip::Error, 'Truncated local zip entry header'
311
412
  end
312
413
 
313
- if @extra.kind_of?(::Zip::ExtraField)
314
- @extra.merge(extra) if extra
315
- else
316
- @extra = ::Zip::ExtraField.new(extra)
317
- end
318
-
414
+ read_extra_field(extra, local: true)
319
415
  parse_zip64_extra(true)
416
+ parse_aes_extra
320
417
  @local_header_size = calculate_local_header_size
321
418
  end
322
419
 
323
- def pack_local_entry
324
- zip64 = @extra['Zip64']
420
+ def pack_local_entry # :nodoc:
421
+ zip64 = @extra[:zip64]
325
422
  [::Zip::LOCAL_ENTRY_SIGNATURE,
326
423
  @version_needed_to_extract, # version needed to extract
327
424
  @gp_flags, # @gp_flags
328
- @compression_method,
425
+ stored_compression_method,
329
426
  @time.to_binary_dos_time, # @last_mod_time
330
427
  @time.to_binary_dos_date, # @last_mod_date
331
428
  @crc,
332
429
  zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
333
- zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
430
+ zip64 && zip64.original_size ? 0xFFFFFFFF : (@size || 0),
334
431
  name_size,
335
432
  @extra ? @extra.local_size : 0].pack('VvvvvvVVVvv')
336
433
  end
337
434
 
338
- def write_local_entry(io, rewrite = false) #:nodoc:all
339
- prep_zip64_extra(true)
340
- verify_local_header_size! if rewrite
435
+ def write_local_entry(io, suppress_extra_fields: false, rewrite: false) # :nodoc:
436
+ prep_local_zip64_extra
437
+
438
+ # If we are rewriting the local header, then we verify that we haven't changed
439
+ # its size. At this point we have to keep extra fields if they are present.
440
+ if rewrite
441
+ verify_local_header_size!
442
+ elsif suppress_extra_fields
443
+ @extra.suppress_fields!(suppress_extra_fields)
444
+ end
445
+
341
446
  @local_header_offset = io.tell
342
447
 
343
448
  io << pack_local_entry
@@ -347,7 +452,7 @@ module Zip
347
452
  @local_header_size = io.tell - @local_header_offset
348
453
  end
349
454
 
350
- def unpack_c_dir_entry(buf)
455
+ def unpack_c_dir_entry(buf) # :nodoc:
351
456
  @header_signature,
352
457
  @version, # version of encoding software
353
458
  @fstype, # filesystem type
@@ -365,13 +470,10 @@ module Zip
365
470
  _, # diskNumberStart
366
471
  @internal_file_attributes,
367
472
  @external_file_attributes,
368
- @local_header_offset,
369
- @name,
370
- @extra,
371
- @comment = buf.unpack('VCCvvvvvVVVvvvvvVV')
473
+ @local_header_offset = buf.unpack('VCCvvvvvVVVvvvvvVV')
372
474
  end
373
475
 
374
- def set_ftype_from_c_dir_entry
476
+ def set_ftype_from_c_dir_entry # :nodoc:
375
477
  @ftype = case @fstype
376
478
  when ::Zip::FSTYPE_UNIX
377
479
  @unix_perms = (@external_file_attributes >> 16) & 0o7777
@@ -383,8 +485,9 @@ module Zip
383
485
  when ::Zip::FILE_TYPE_SYMLINK
384
486
  :symlink
385
487
  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
488
+ # Best case guess for whether it is a file or not.
489
+ # Otherwise this would be set to unknown and that
490
+ # entry would never be able to be extracted.
388
491
  if name_is_directory?
389
492
  :directory
390
493
  else
@@ -400,47 +503,52 @@ module Zip
400
503
  end
401
504
  end
402
505
 
403
- def check_c_dir_entry_static_header_length(buf)
404
- return if buf.bytesize == ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
506
+ def check_c_dir_entry_static_header_length(buf) # :nodoc:
507
+ return unless buf.nil? || buf.bytesize != ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
405
508
 
406
509
  raise Error, 'Premature end of file. Not enough data for zip cdir entry header'
407
510
  end
408
511
 
409
- def check_c_dir_entry_signature
410
- return if header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
512
+ def check_c_dir_entry_signature # :nodoc:
513
+ return if @header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
411
514
 
412
515
  raise Error, "Zip local header magic not found at location '#{local_header_offset}'"
413
516
  end
414
517
 
415
- def check_c_dir_entry_comment_size
518
+ def check_c_dir_entry_comment_size # :nodoc:
416
519
  return if @comment && @comment.bytesize == @comment_length
417
520
 
418
521
  raise ::Zip::Error, 'Truncated cdir zip entry header'
419
522
  end
420
523
 
421
- def read_c_dir_extra_field(io)
524
+ def read_extra_field(buf, local: false) # :nodoc:
422
525
  if @extra.kind_of?(::Zip::ExtraField)
423
- @extra.merge(io.read(@extra_length))
526
+ @extra.merge(buf, local: local) if buf
424
527
  else
425
- @extra = ::Zip::ExtraField.new(io.read(@extra_length))
528
+ @extra = ::Zip::ExtraField.new(buf, local: local)
426
529
  end
427
530
  end
428
531
 
429
- def read_c_dir_entry(io) #:nodoc:all
532
+ def read_c_dir_entry(io) # :nodoc:
533
+ @dirty = false # No changes at this point.
430
534
  static_sized_fields_buf = io.read(::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH)
431
535
  check_c_dir_entry_static_header_length(static_sized_fields_buf)
432
536
  unpack_c_dir_entry(static_sized_fields_buf)
433
537
  check_c_dir_entry_signature
434
538
  set_time(@last_mod_date, @last_mod_time)
539
+
435
540
  @name = io.read(@name_length)
436
541
  if ::Zip.force_entry_names_encoding
437
542
  @name.force_encoding(::Zip.force_entry_names_encoding)
438
543
  end
439
- read_c_dir_extra_field(io)
544
+ @name.tr!('\\', '/') # Normalise filepath separators after encoding set.
545
+
546
+ read_extra_field(io.read(@extra_length))
440
547
  @comment = io.read(@comment_length)
441
548
  check_c_dir_entry_comment_size
442
549
  set_ftype_from_c_dir_entry
443
550
  parse_zip64_extra(false)
551
+ parse_aes_extra
444
552
  end
445
553
 
446
554
  def file_stat(path) # :nodoc:
@@ -452,27 +560,27 @@ module Zip
452
560
  end
453
561
 
454
562
  def get_extra_attributes_from_path(path) # :nodoc:
455
- return if Zip::RUNNING_ON_WINDOWS
563
+ stat = file_stat(path)
564
+ @time = DOSTime.from_time(stat.mtime)
565
+ return if ::Zip::RUNNING_ON_WINDOWS
456
566
 
457
- stat = file_stat(path)
458
567
  @unix_uid = stat.uid
459
568
  @unix_gid = stat.gid
460
569
  @unix_perms = stat.mode & 0o7777
461
- @time = ::Zip::DOSTime.from_time(stat.mtime)
462
570
  end
463
571
 
464
- 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
572
+ # rubocop:disable Style/GuardClause
573
+ def set_unix_attributes_on_path(dest_path) # :nodoc:
574
+ # Ignore setuid/setgid bits by default. Honour if @restore_ownership.
575
+ unix_perms_mask = (@restore_ownership ? 0o7777 : 0o1777)
576
+ if @restore_permissions && @unix_perms
577
+ ::FileUtils.chmod(@unix_perms & unix_perms_mask, dest_path)
578
+ end
579
+ if @restore_ownership && @unix_uid && @unix_gid && ::Process.egid == 0
580
+ ::FileUtils.chown(@unix_uid, @unix_gid, dest_path)
581
+ end
475
582
  end
583
+ # rubocop:enable Style/GuardClause
476
584
 
477
585
  def set_extra_attributes_on_path(dest_path) # :nodoc:
478
586
  return unless file? || directory?
@@ -481,40 +589,43 @@ module Zip
481
589
  when ::Zip::FSTYPE_UNIX
482
590
  set_unix_attributes_on_path(dest_path)
483
591
  end
592
+
593
+ # Restore the timestamp on a file. This will either have come from the
594
+ # original source file that was copied into the archive, or from the
595
+ # creation date of the archive if there was no original source file.
596
+ ::FileUtils.touch(dest_path, mtime: time) if @restore_times
484
597
  end
485
598
 
486
- def pack_c_dir_entry
487
- zip64 = @extra['Zip64']
599
+ def pack_c_dir_entry # :nodoc:
600
+ zip64 = @extra[:zip64]
488
601
  [
489
602
  @header_signature,
490
603
  @version, # version of encoding software
491
604
  @fstype, # filesystem type
492
605
  @version_needed_to_extract, # @versionNeededToExtract
493
606
  @gp_flags, # @gp_flags
494
- @compression_method,
607
+ stored_compression_method,
495
608
  @time.to_binary_dos_time, # @last_mod_time
496
609
  @time.to_binary_dos_date, # @last_mod_date
497
610
  @crc,
498
611
  zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
499
- zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
612
+ zip64 && zip64.original_size ? 0xFFFFFFFF : (@size || 0),
500
613
  name_size,
501
614
  @extra ? @extra.c_dir_size : 0,
502
615
  comment_size,
503
616
  zip64 && zip64.disk_start_number ? 0xFFFF : 0, # disk number start
504
617
  @internal_file_attributes, # file type (binary=0, text=1)
505
618
  @external_file_attributes, # native filesystem attributes
506
- zip64 && zip64.relative_header_offset ? 0xFFFFFFFF : @local_header_offset,
507
- @name,
508
- @extra,
509
- @comment
619
+ zip64 && zip64.relative_header_offset ? 0xFFFFFFFF : @local_header_offset
510
620
  ].pack('VCCvvvvvVVVvvvvvVV')
511
621
  end
512
622
 
513
- def write_c_dir_entry(io) #:nodoc:all
514
- prep_zip64_extra(false)
623
+ def write_c_dir_entry(io, suppress_extra_fields: false) # :nodoc:
624
+ prep_cdir_zip64_extra
625
+
515
626
  case @fstype
516
627
  when ::Zip::FSTYPE_UNIX
517
- ft = case @ftype
628
+ ft = case ftype
518
629
  when :file
519
630
  @unix_perms ||= 0o644
520
631
  ::Zip::FILE_TYPE_FILE
@@ -527,10 +638,11 @@ module Zip
527
638
  end
528
639
 
529
640
  unless ft.nil?
530
- @external_file_attributes = (ft << 12 | (@unix_perms & 0o7777)) << 16
641
+ @external_file_attributes = ((ft << 12) | (@unix_perms & 0o7777)) << 16
531
642
  end
532
643
  end
533
644
 
645
+ @extra.suppress_fields!(suppress_extra_fields) if suppress_extra_fields
534
646
  io << pack_c_dir_entry
535
647
 
536
648
  io << @name
@@ -538,43 +650,43 @@ module Zip
538
650
  io << @comment
539
651
  end
540
652
 
541
- def ==(other)
653
+ def ==(other) # :nodoc:
542
654
  return false unless other.class == self.class
543
655
 
544
656
  # Compares contents of local entry and exposed fields
545
- keys_equal = %w[compression_method crc compressed_size size name extra filepath].all? do |k|
657
+ %w[compression_method crc compressed_size size name extra filepath time].all? do |k|
546
658
  other.__send__(k.to_sym) == __send__(k.to_sym)
547
659
  end
548
- keys_equal && time == other.time
549
660
  end
550
661
 
551
- def <=>(other)
662
+ def <=>(other) # :nodoc:
552
663
  to_s <=> other.to_s
553
664
  end
554
665
 
555
666
  # Returns an IO like object for the given ZipEntry.
667
+ # Optional decrypter can be provided for an encrypted entry
556
668
  # Warning: may behave weird with symlinks.
557
- def get_input_stream(&block)
558
- if @ftype == :directory
559
- yield ::Zip::NullInputStream if block_given?
669
+ def get_input_stream(decrypter: nil, &block)
670
+ if ftype == :directory
671
+ yield ::Zip::NullInputStream if block
560
672
  ::Zip::NullInputStream
561
673
  elsif @filepath
562
- case @ftype
674
+ case ftype
563
675
  when :file
564
676
  ::File.open(@filepath, 'rb', &block)
565
677
  when :symlink
566
678
  linkpath = ::File.readlink(@filepath)
567
679
  stringio = ::StringIO.new(linkpath)
568
- yield(stringio) if block_given?
680
+ yield(stringio) if block
569
681
  stringio
570
682
  else
571
- raise "unknown @file_type #{@ftype}"
683
+ raise "unknown @file_type #{ftype}"
572
684
  end
573
685
  else
574
- zis = ::Zip::InputStream.new(@zipfile, offset: local_header_offset)
686
+ zis = ::Zip::InputStream.new(@zipfile, offset: local_header_offset, decrypter: decrypter)
575
687
  zis.instance_variable_set(:@complete_entry, self)
576
688
  zis.get_next_entry
577
- if block_given?
689
+ if block
578
690
  begin
579
691
  yield(zis)
580
692
  ensure
@@ -592,8 +704,8 @@ module Zip
592
704
  when 'file'
593
705
  if name_is_directory?
594
706
  raise ArgumentError,
595
- "entry name '#{newEntry}' indicates directory entry, but " \
596
- "'#{src_path}' is not a directory"
707
+ "entry name '#{@name}' indicates a directory entry, but " \
708
+ "'#{src_path}' is not a directory"
597
709
  end
598
710
  :file
599
711
  when 'directory'
@@ -602,8 +714,8 @@ module Zip
602
714
  when 'link'
603
715
  if name_is_directory?
604
716
  raise ArgumentError,
605
- "entry name '#{newEntry}' indicates directory entry, but " \
606
- "'#{src_path}' is not a directory"
717
+ "entry name '#{@name}' indicates a directory entry, but " \
718
+ "'#{src_path}' is not a directory"
607
719
  end
608
720
  :symlink
609
721
  else
@@ -611,27 +723,30 @@ module Zip
611
723
  end
612
724
 
613
725
  @filepath = src_path
726
+ @size = stat.size
614
727
  get_extra_attributes_from_path(@filepath)
615
728
  end
616
729
 
617
- 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)
730
+ def write_to_zip_output_stream(zip_output_stream) # :nodoc:
731
+ if ftype == :directory
732
+ zip_output_stream.put_next_entry(self)
620
733
  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) }
734
+ zip_output_stream.put_next_entry(self)
735
+ get_input_stream do |is|
736
+ ::Zip::IOExtras.copy_stream(zip_output_stream, is)
737
+ end
623
738
  else
624
739
  zip_output_stream.copy_raw_entry(self)
625
740
  end
626
741
  end
627
742
 
628
- def parent_as_string
743
+ def parent_as_string # :nodoc:
629
744
  entry_name = name.chomp('/')
630
745
  slash_index = entry_name.rindex('/')
631
746
  slash_index ? entry_name.slice(0, slash_index + 1) : nil
632
747
  end
633
748
 
634
- def get_raw_input_stream(&block)
749
+ def get_raw_input_stream(&block) # :nodoc:
635
750
  if @zipfile.respond_to?(:seek) && @zipfile.respond_to?(:read)
636
751
  yield @zipfile
637
752
  else
@@ -639,12 +754,22 @@ module Zip
639
754
  end
640
755
  end
641
756
 
642
- def clean_up
643
- # By default, do nothing
757
+ def clean_up # :nodoc:
758
+ @dirty = false # Any changes are written at this point.
644
759
  end
645
760
 
646
761
  private
647
762
 
763
+ def read_local_header_fields(io) # :nodoc:
764
+ static_sized_fields_buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
765
+
766
+ unless static_sized_fields_buf.bytesize == ::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH
767
+ raise Error, 'Premature end of file. Not enough data for zip entry local header'
768
+ end
769
+
770
+ unpack_local_entry(static_sized_fields_buf)
771
+ end
772
+
648
773
  def set_time(binary_dos_date, binary_dos_time)
649
774
  @time = ::Zip::DOSTime.parse_binary_dos_format(binary_dos_date, binary_dos_time)
650
775
  rescue ArgumentError
@@ -653,23 +778,22 @@ module Zip
653
778
 
654
779
  def create_file(dest_path, _continue_on_exists_proc = proc { Zip.continue_on_exists_proc })
655
780
  if ::File.exist?(dest_path) && !yield(self, dest_path)
656
- raise ::Zip::DestinationFileExistsError,
657
- "Destination '#{dest_path}' already exists"
781
+ raise DestinationExistsError, dest_path
658
782
  end
783
+
659
784
  ::File.open(dest_path, 'wb') do |os|
660
785
  get_input_stream do |is|
661
786
  bytes_written = 0
662
787
  warned = false
663
- buf = +''
664
- while (buf = is.sysread(::Zip::Decompressor::CHUNK_SIZE, buf))
788
+ while (buf = is.sysread(Decompressor::CHUNK_SIZE))
665
789
  os << buf
666
790
  bytes_written += buf.bytesize
667
791
  next unless bytes_written > size && !warned
668
792
 
669
- message = "entry '#{name}' should be #{size}B, but is larger when inflated."
670
- raise ::Zip::EntrySizeError, message if ::Zip.validate_entry_sizes
793
+ error = EntrySizeError.new(self)
794
+ raise error if Zip.validate_entry_sizes
671
795
 
672
- warn "WARNING: #{message}"
796
+ warn "WARNING: #{error.message}"
673
797
  warned = true
674
798
  end
675
799
  end
@@ -682,14 +806,11 @@ module Zip
682
806
  return if ::File.directory?(dest_path)
683
807
 
684
808
  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
809
+ raise ::Zip::DestinationExistsError, dest_path unless block_given? && yield(self, dest_path)
810
+
811
+ ::FileUtils.rm_f dest_path
692
812
  end
813
+
693
814
  ::FileUtils.mkdir_p(dest_path)
694
815
  set_extra_attributes_on_path(dest_path)
695
816
  end
@@ -703,53 +824,94 @@ module Zip
703
824
 
704
825
  # apply missing data from the zip64 extra information field, if present
705
826
  # (required when file sizes exceed 2**32, but can be used for all files)
706
- def parse_zip64_extra(for_local_header) #:nodoc:all
707
- return if @extra['Zip64'].nil?
827
+ def parse_zip64_extra(for_local_header) # :nodoc:
828
+ return unless zip64?
708
829
 
709
830
  if for_local_header
710
- @size, @compressed_size = @extra['Zip64'].parse(@size, @compressed_size)
831
+ @size, @compressed_size = @extra[:zip64].parse(@size, @compressed_size)
711
832
  else
712
- @size, @compressed_size, @local_header_offset = @extra['Zip64'].parse(@size, @compressed_size, @local_header_offset)
833
+ @size, @compressed_size, @local_header_offset = @extra[:zip64].parse(
834
+ @size, @compressed_size, @local_header_offset
835
+ )
836
+ end
837
+ end
838
+
839
+ def parse_aes_extra # :nodoc:
840
+ return unless aes?
841
+
842
+ if @extra[:aes].vendor_id != 'AE'
843
+ raise Error, "Unsupported encryption method #{@extra[:aes].vendor_id}"
844
+ end
845
+
846
+ unless ::Zip::AESEncryption::VERSIONS.include? @extra[:aes].vendor_version
847
+ raise Error, "Unsupported encryption style #{@extra[:aes].vendor_version}"
848
+ end
849
+
850
+ @compression_method = @extra[:aes].compression_method if ftype != :directory
851
+ end
852
+
853
+ # AES-encrypted entries always store `COMPRESSION_METHOD_AES` (99) on the
854
+ # wire; the real compression method lives in the AES extra field instead
855
+ # (see `parse_aes_extra`, which reverses this on read).
856
+ def stored_compression_method # :nodoc:
857
+ aes? ? COMPRESSION_METHOD_AES : compression_method
858
+ end
859
+
860
+ # For DEFLATED compression *only*: set the general purpose flags 1 and 2 to
861
+ # indicate compression level. This seems to be mainly cosmetic but they are
862
+ # generally set by other tools - including in docx files. It is these flags
863
+ # that are used by commandline tools (and elsewhere) to give an indication
864
+ # of how compressed a file is. See the PKWARE APPNOTE for more information:
865
+ # https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
866
+ #
867
+ # It's safe to simply OR these flags here as compression_level is read only.
868
+ def set_compression_level_flags
869
+ return unless compression_method == DEFLATED
870
+
871
+ case @compression_level
872
+ when 1
873
+ @gp_flags |= COMPRESSION_LEVEL_SUPERFAST_GPFLAG
874
+ when 2
875
+ @gp_flags |= COMPRESSION_LEVEL_FAST_GPFLAG
876
+ when 8, 9
877
+ @gp_flags |= COMPRESSION_LEVEL_MAX_GPFLAG
713
878
  end
714
879
  end
715
880
 
716
- def data_descriptor_size
717
- (@gp_flags & 0x0008) > 0 ? 16 : 0
881
+ # rubocop:disable Style/GuardClause
882
+ def prep_local_zip64_extra
883
+ return unless ::Zip.write_zip64_support
884
+ return if (!zip64? && @size && @size < 0xFFFFFFFF) || !file?
885
+
886
+ # Might not know size here, so need ZIP64 just in case.
887
+ # If we already have a ZIP64 extra (placeholder) then we must fill it in.
888
+ if zip64? || @size.nil? || @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
889
+ @version_needed_to_extract =
890
+ [@version_needed_to_extract, VERSION_NEEDED_TO_EXTRACT_ZIP64].max
891
+ zip64 = @extra[:zip64] || @extra.create(:zip64)
892
+
893
+ # Local header always includes size and compressed size.
894
+ zip64.original_size = @size || 0
895
+ zip64.compressed_size = @compressed_size
896
+ end
718
897
  end
719
898
 
720
- # create a zip64 extra information field if we need one
721
- def prep_zip64_extra(for_local_header) #:nodoc:all
899
+ def prep_cdir_zip64_extra
722
900
  return unless ::Zip.write_zip64_support
723
901
 
724
- need_zip64 = @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
725
- need_zip64 ||= @local_header_offset >= 0xFFFFFFFF unless for_local_header
726
- if need_zip64
727
- @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')
902
+ if (@size && @size >= 0xFFFFFFFF) || @compressed_size >= 0xFFFFFFFF ||
903
+ @local_header_offset >= 0xFFFFFFFF
904
+ @version_needed_to_extract =
905
+ [@version_needed_to_extract, VERSION_NEEDED_TO_EXTRACT_ZIP64].max
906
+ zip64 = @extra[:zip64] || @extra.create(:zip64)
742
907
 
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
908
+ # Central directory entry entries include whichever fields are necessary.
909
+ zip64.original_size = @size if @size && @size >= 0xFFFFFFFF
910
+ zip64.compressed_size = @compressed_size if @compressed_size >= 0xFFFFFFFF
911
+ zip64.relative_header_offset = @local_header_offset if @local_header_offset >= 0xFFFFFFFF
751
912
  end
752
913
  end
914
+ # rubocop:enable Style/GuardClause
753
915
  end
754
916
  end
755
917