rubyzip 2.4.1 → 3.0.0.rc1

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