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