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