rubyzip 2.3.2 → 3.2.2

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