rubyzip 1.0.0 → 2.4.1

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 (52) hide show
  1. checksums.yaml +6 -14
  2. data/README.md +231 -46
  3. data/Rakefile +13 -5
  4. data/TODO +0 -1
  5. data/lib/zip/central_directory.rb +64 -29
  6. data/lib/zip/compressor.rb +1 -2
  7. data/lib/zip/constants.rb +59 -5
  8. data/lib/zip/crypto/decrypted_io.rb +40 -0
  9. data/lib/zip/crypto/encryption.rb +11 -0
  10. data/lib/zip/crypto/null_encryption.rb +43 -0
  11. data/lib/zip/crypto/traditional_encryption.rb +99 -0
  12. data/lib/zip/decompressor.rb +22 -4
  13. data/lib/zip/deflater.rb +11 -6
  14. data/lib/zip/dos_time.rb +27 -16
  15. data/lib/zip/entry.rb +299 -163
  16. data/lib/zip/entry_set.rb +22 -20
  17. data/lib/zip/errors.rb +17 -6
  18. data/lib/zip/extra_field/generic.rb +15 -14
  19. data/lib/zip/extra_field/ntfs.rb +94 -0
  20. data/lib/zip/extra_field/old_unix.rb +46 -0
  21. data/lib/zip/extra_field/universal_time.rb +46 -16
  22. data/lib/zip/extra_field/unix.rb +10 -9
  23. data/lib/zip/extra_field/zip64.rb +46 -6
  24. data/lib/zip/extra_field/zip64_placeholder.rb +15 -0
  25. data/lib/zip/extra_field.rb +32 -18
  26. data/lib/zip/file.rb +260 -160
  27. data/lib/zip/filesystem.rb +297 -276
  28. data/lib/zip/inflater.rb +23 -34
  29. data/lib/zip/input_stream.rb +130 -82
  30. data/lib/zip/ioextras/abstract_input_stream.rb +36 -22
  31. data/lib/zip/ioextras/abstract_output_stream.rb +4 -6
  32. data/lib/zip/ioextras.rb +4 -6
  33. data/lib/zip/null_compressor.rb +2 -2
  34. data/lib/zip/null_decompressor.rb +4 -12
  35. data/lib/zip/null_input_stream.rb +2 -1
  36. data/lib/zip/output_stream.rb +75 -45
  37. data/lib/zip/pass_thru_compressor.rb +6 -6
  38. data/lib/zip/pass_thru_decompressor.rb +14 -24
  39. data/lib/zip/streamable_directory.rb +3 -3
  40. data/lib/zip/streamable_stream.rb +21 -16
  41. data/lib/zip/version.rb +1 -1
  42. data/lib/zip.rb +42 -3
  43. data/samples/example.rb +30 -40
  44. data/samples/example_filesystem.rb +16 -18
  45. data/samples/example_recursive.rb +33 -28
  46. data/samples/gtk_ruby_zip.rb +84 -0
  47. data/samples/qtzip.rb +25 -34
  48. data/samples/write_simple.rb +10 -13
  49. data/samples/zipfind.rb +38 -45
  50. metadata +129 -28
  51. data/NEWS +0 -182
  52. data/samples/gtkRubyzip.rb +0 -86
data/lib/zip/entry.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'pathname'
1
2
  module Zip
2
3
  class Entry
3
4
  STORED = 0
@@ -7,6 +8,7 @@ module Zip
7
8
 
8
9
  attr_accessor :comment, :compressed_size, :crc, :extra, :compression_method,
9
10
  :name, :size, :local_header_offset, :zipfile, :fstype, :external_file_attributes,
11
+ :internal_file_attributes,
10
12
  :gp_flags, :header_signature, :follow_symlinks,
11
13
  :restore_times, :restore_permissions, :restore_ownership,
12
14
  :unix_uid, :unix_gid, :unix_perms,
@@ -15,13 +17,13 @@ module Zip
15
17
 
16
18
  def set_default_vars_values
17
19
  @local_header_offset = 0
18
- @local_header_size = 0
20
+ @local_header_size = nil # not known until local entry is created or read
19
21
  @internal_file_attributes = 1
20
22
  @external_file_attributes = 0
21
23
  @header_signature = ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
22
24
 
23
25
  @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT
24
- @version = 52 # this library's version
26
+ @version = VERSION_MADE_BY
25
27
 
26
28
  @ftype = nil # unspecified or unknown
27
29
  @filepath = nil
@@ -32,48 +34,73 @@ module Zip
32
34
  end
33
35
  @follow_symlinks = false
34
36
 
35
- @restore_times = true
37
+ @restore_times = false
36
38
  @restore_permissions = false
37
39
  @restore_ownership = false
38
40
  # BUG: need an extra field to support uid/gid's
39
41
  @unix_uid = nil
40
42
  @unix_gid = nil
41
43
  @unix_perms = nil
42
- #@posix_acl = nil
43
- #@ntfs_acl = nil
44
+ # @posix_acl = nil
45
+ # @ntfs_acl = nil
44
46
  @dirty = false
45
47
  end
46
48
 
47
49
  def check_name(name)
48
- if name.start_with?('/')
49
- raise ::Zip::ZipEntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
50
- end
50
+ return unless name.start_with?('/')
51
+
52
+ raise ::Zip::EntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
51
53
  end
52
54
 
53
- def initialize(*args)
54
- name = args[1] || ''
55
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
56
+ def initialize(zipfile = nil, name = nil, *args)
57
+ name ||= ''
55
58
  check_name(name)
56
59
 
57
60
  set_default_vars_values
58
61
  @fstype = ::Zip::RUNNING_ON_WINDOWS ? ::Zip::FSTYPE_FAT : ::Zip::FSTYPE_UNIX
59
62
 
60
- @zipfile = args[0] || ''
63
+ @zipfile = zipfile || ''
61
64
  @name = name
62
- @comment = args[2] || ''
63
- @extra = args[3] || ''
64
- @compressed_size = args[4] || 0
65
- @crc = args[5] || 0
66
- @compression_method = args[6] || ::Zip::Entry::DEFLATED
67
- @size = args[7] || 0
68
- @time = args[8] || ::Zip::DOSTime.now
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
69
85
 
70
86
  @ftype = name_is_directory? ? :directory : :file
71
- @extra = ::Zip::ExtraField.new(@extra.to_s) unless ::Zip::ExtraField === @extra
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
+
91
+ def encrypted?
92
+ gp_flags & 1 == 1
93
+ end
94
+
95
+ def incomplete?
96
+ gp_flags & 8 == 8
72
97
  end
73
98
 
74
99
  def time
75
100
  if @extra['UniversalTime']
76
101
  @extra['UniversalTime'].mtime
102
+ elsif @extra['NTFS']
103
+ @extra['NTFS'].mtime
77
104
  else
78
105
  # Standard time field in central directory has local time
79
106
  # under archive creator. Then, we can't get timezone.
@@ -81,23 +108,24 @@ module Zip
81
108
  end
82
109
  end
83
110
 
84
- alias :mtime :time
111
+ alias mtime time
85
112
 
86
113
  def time=(value)
87
- unless @extra.member?('UniversalTime')
114
+ unless @extra.member?('UniversalTime') || @extra.member?('NTFS')
88
115
  @extra.create('UniversalTime')
89
116
  end
90
- @extra['UniversalTime'].mtime = value
91
- @time = value
117
+ (@extra['UniversalTime'] || @extra['NTFS']).mtime = value
118
+ @time = value
92
119
  end
93
120
 
94
121
  def file_type_is?(type)
95
- raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype
122
+ raise InternalError, "current filetype is unknown: #{inspect}" unless @ftype
123
+
96
124
  @ftype == type
97
125
  end
98
126
 
99
127
  # Dynamic checkers
100
- %w(directory file symlink).each do |k|
128
+ %w[directory file symlink].each do |k|
101
129
  define_method "#{k}?" do
102
130
  file_type_is?(k.to_sym)
103
131
  end
@@ -107,6 +135,18 @@ module Zip
107
135
  @name.end_with?('/')
108
136
  end
109
137
 
138
+ # Is the name a relative path, free of `..` patterns that could lead to
139
+ # path traversal attacks? This does NOT handle symlinks; if the path
140
+ # contains symlinks, this check is NOT enough to guarantee safety.
141
+ def name_safe?
142
+ cleanpath = Pathname.new(@name).cleanpath
143
+ return false unless cleanpath.relative?
144
+
145
+ root = ::File::SEPARATOR
146
+ naive_expanded_path = ::File.join(root, cleanpath.to_s)
147
+ ::File.absolute_path(cleanpath.to_s, root) == naive_expanded_path
148
+ end
149
+
110
150
  def local_entry_offset #:nodoc:all
111
151
  local_header_offset + @local_header_size
112
152
  end
@@ -124,29 +164,66 @@ module Zip
124
164
  end
125
165
 
126
166
  def calculate_local_header_size #:nodoc:all
127
- fix_zip64_sizes!
128
167
  LOCAL_ENTRY_STATIC_HEADER_LENGTH + name_size + extra_size
129
168
  end
130
169
 
170
+ # check before rewriting an entry (after file sizes are known)
171
+ # that we didn't change the header size (and thus clobber file data or something)
172
+ def verify_local_header_size!
173
+ return if @local_header_size.nil?
174
+
175
+ new_size = calculate_local_header_size
176
+ raise Error, "local header size changed (#{@local_header_size} -> #{new_size})" if @local_header_size != new_size
177
+ end
178
+
131
179
  def cdir_header_size #:nodoc:all
132
180
  CDIR_ENTRY_STATIC_HEADER_LENGTH + name_size +
133
181
  (@extra ? @extra.c_dir_size : 0) + comment_size
134
182
  end
135
183
 
136
184
  def next_header_offset #:nodoc:all
137
- local_entry_offset + self.compressed_size
185
+ local_entry_offset + compressed_size + data_descriptor_size
138
186
  end
139
187
 
140
188
  # Extracts entry to file dest_path (defaults to @name).
141
- def extract(dest_path = @name, &block)
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
142
200
  block ||= proc { ::Zip.on_exists_proc }
143
201
 
144
- if directory? || file? || symlink?
145
- self.__send__("create_#{@ftype}", dest_path, &block)
146
- else
147
- raise RuntimeError, "unknown file type #{self.inspect}"
202
+ raise "unknown file type #{inspect}" unless directory? || file? || symlink?
203
+
204
+ __send__("create_#{@ftype}", dest_path, &block)
205
+ self
206
+ end
207
+
208
+ # Extracts this entry to a file at `entry_path`, with
209
+ # `destination_directory` as the base location in the filesystem.
210
+ #
211
+ # NB: The caller is responsible for making sure `destination_directory` is
212
+ # safe, if it is passed.
213
+ def extract_v3(entry_path = @name, destination_directory: '.', &block)
214
+ dest_dir = ::File.absolute_path(destination_directory || '.')
215
+ extract_path = ::File.absolute_path(::File.join(dest_dir, entry_path))
216
+
217
+ unless extract_path.start_with?(dest_dir)
218
+ warn "WARNING: skipped extracting '#{@name}' to '#{extract_path}' as unsafe."
219
+ return self
148
220
  end
149
221
 
222
+ block ||= proc { ::Zip.on_exists_proc }
223
+
224
+ raise "unknown file type #{inspect}" unless directory? || file? || symlink?
225
+
226
+ __send__(:"create_#{ftype}", extract_path, &block)
150
227
  self
151
228
  end
152
229
 
@@ -154,41 +231,41 @@ module Zip
154
231
  @name
155
232
  end
156
233
 
157
- protected
158
-
159
234
  class << self
160
235
  def read_zip_short(io) # :nodoc:
161
- io.read(2).unpack('v')[0]
236
+ io.read(2).unpack1('v')
162
237
  end
163
238
 
164
239
  def read_zip_long(io) # :nodoc:
165
- io.read(4).unpack('V')[0]
240
+ io.read(4).unpack1('V')
166
241
  end
167
242
 
168
243
  def read_zip_64_long(io) # :nodoc:
169
- io.read(8).unpack('V')[0]
244
+ io.read(8).unpack1('Q<')
170
245
  end
171
246
 
172
247
  def read_c_dir_entry(io) #:nodoc:all
173
- entry = new(io.path)
248
+ path = if io.respond_to?(:path)
249
+ io.path
250
+ else
251
+ io
252
+ end
253
+ entry = new(path)
174
254
  entry.read_c_dir_entry(io)
175
255
  entry
176
- rescue ZipError
256
+ rescue Error
177
257
  nil
178
258
  end
179
259
 
180
260
  def read_local_entry(io)
181
- entry = new(io.path)
261
+ entry = new(io)
182
262
  entry.read_local_entry(io)
183
263
  entry
184
- rescue ZipError
264
+ rescue Error
185
265
  nil
186
266
  end
187
-
188
267
  end
189
268
 
190
- public
191
-
192
269
  def unpack_local_entry(buf)
193
270
  @header_signature,
194
271
  @version,
@@ -207,57 +284,67 @@ module Zip
207
284
  def read_local_entry(io) #:nodoc:all
208
285
  @local_header_offset = io.tell
209
286
 
210
- static_sized_fields_buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH)
287
+ static_sized_fields_buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
211
288
 
212
289
  unless static_sized_fields_buf.bytesize == ::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH
213
- raise ZipError, "Premature end of file. Not enough data for zip entry local header"
290
+ raise Error, 'Premature end of file. Not enough data for zip entry local header'
214
291
  end
215
292
 
216
293
  unpack_local_entry(static_sized_fields_buf)
217
294
 
218
295
  unless @header_signature == ::Zip::LOCAL_ENTRY_SIGNATURE
219
- raise ::Zip::ZipError, "Zip local header magic not found at location '#{local_header_offset}'"
296
+ raise ::Zip::Error, "Zip local header magic not found at location '#{local_header_offset}'"
220
297
  end
298
+
221
299
  set_time(@last_mod_date, @last_mod_time)
222
300
 
223
301
  @name = io.read(@name_length)
224
302
  extra = io.read(@extra_length)
225
303
 
226
- @name.gsub!('\\', '/')
304
+ @name.tr!('\\', '/')
305
+ if ::Zip.force_entry_names_encoding
306
+ @name.force_encoding(::Zip.force_entry_names_encoding)
307
+ end
227
308
 
228
309
  if extra && extra.bytesize != @extra_length
229
- raise ::Zip::ZipError, "Truncated local zip entry header"
310
+ raise ::Zip::Error, 'Truncated local zip entry header'
311
+ end
312
+
313
+ if @extra.kind_of?(::Zip::ExtraField)
314
+ @extra.merge(extra) if extra
230
315
  else
231
- if ::Zip::ExtraField === @extra
232
- @extra.merge(extra)
233
- else
234
- @extra = ::Zip::ExtraField.new(extra)
235
- end
316
+ @extra = ::Zip::ExtraField.new(extra)
236
317
  end
318
+
319
+ parse_zip64_extra(true)
237
320
  @local_header_size = calculate_local_header_size
238
321
  end
239
322
 
240
323
  def pack_local_entry
324
+ zip64 = @extra['Zip64']
241
325
  [::Zip::LOCAL_ENTRY_SIGNATURE,
242
326
  @version_needed_to_extract, # version needed to extract
243
- @gp_flags, # @gp_flags ,
327
+ @gp_flags, # @gp_flags
244
328
  @compression_method,
245
- @time.to_binary_dos_time, # @last_mod_time ,
246
- @time.to_binary_dos_date, # @last_mod_date ,
329
+ @time.to_binary_dos_time, # @last_mod_time
330
+ @time.to_binary_dos_date, # @last_mod_date
247
331
  @crc,
248
- @compressed_size,
249
- @size,
332
+ zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
333
+ zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
250
334
  name_size,
251
335
  @extra ? @extra.local_size : 0].pack('VvvvvvVVVvv')
252
336
  end
253
337
 
254
- def write_local_entry(io) #:nodoc:all
338
+ def write_local_entry(io, rewrite = false) #:nodoc:all
339
+ prep_zip64_extra(true)
340
+ verify_local_header_size! if rewrite
255
341
  @local_header_offset = io.tell
256
342
 
257
343
  io << pack_local_entry
258
344
 
259
345
  io << @name
260
- io << (@extra ? @extra.to_local_bin : '')
346
+ io << @extra.to_local_bin if @extra
347
+ @local_header_size = io.tell - @local_header_offset
261
348
  end
262
349
 
263
350
  def unpack_c_dir_entry(buf)
@@ -287,7 +374,7 @@ module Zip
287
374
  def set_ftype_from_c_dir_entry
288
375
  @ftype = case @fstype
289
376
  when ::Zip::FSTYPE_UNIX
290
- @unix_perms = (@external_file_attributes >> 16) & 07777
377
+ @unix_perms = (@external_file_attributes >> 16) & 0o7777
291
378
  case (@external_file_attributes >> 28)
292
379
  when ::Zip::FILE_TYPE_DIR
293
380
  :directory
@@ -296,8 +383,8 @@ module Zip
296
383
  when ::Zip::FILE_TYPE_SYMLINK
297
384
  :symlink
298
385
  else
299
- #best case guess for whether it is a file or not
300
- #Otherwise this would be set to unknown and that entry would never be able to extracted
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
301
388
  if name_is_directory?
302
389
  :directory
303
390
  else
@@ -314,25 +401,25 @@ module Zip
314
401
  end
315
402
 
316
403
  def check_c_dir_entry_static_header_length(buf)
317
- unless buf.bytesize == ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
318
- raise ZipError, 'Premature end of file. Not enough data for zip cdir entry header'
319
- end
404
+ return if buf.bytesize == ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
405
+
406
+ raise Error, 'Premature end of file. Not enough data for zip cdir entry header'
320
407
  end
321
408
 
322
409
  def check_c_dir_entry_signature
323
- unless header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
324
- raise ZipError, "Zip local header magic not found at location '#{local_header_offset}'"
325
- end
410
+ return if header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
411
+
412
+ raise Error, "Zip local header magic not found at location '#{local_header_offset}'"
326
413
  end
327
414
 
328
415
  def check_c_dir_entry_comment_size
329
- unless @comment && @comment.bytesize == @comment_length
330
- raise ::Zip::ZipError, "Truncated cdir zip entry header"
331
- end
416
+ return if @comment && @comment.bytesize == @comment_length
417
+
418
+ raise ::Zip::Error, 'Truncated cdir zip entry header'
332
419
  end
333
420
 
334
421
  def read_c_dir_extra_field(io)
335
- if @extra.is_a?(::Zip::ExtraField)
422
+ if @extra.kind_of?(::Zip::ExtraField)
336
423
  @extra.merge(io.read(@extra_length))
337
424
  else
338
425
  @extra = ::Zip::ExtraField.new(io.read(@extra_length))
@@ -345,70 +432,78 @@ module Zip
345
432
  unpack_c_dir_entry(static_sized_fields_buf)
346
433
  check_c_dir_entry_signature
347
434
  set_time(@last_mod_date, @last_mod_time)
348
- @name = io.read(@name_length).gsub('\\', '/')
435
+ @name = io.read(@name_length)
436
+ if ::Zip.force_entry_names_encoding
437
+ @name.force_encoding(::Zip.force_entry_names_encoding)
438
+ end
349
439
  read_c_dir_extra_field(io)
350
440
  @comment = io.read(@comment_length)
351
441
  check_c_dir_entry_comment_size
352
442
  set_ftype_from_c_dir_entry
353
- @local_header_size = calculate_local_header_size
443
+ parse_zip64_extra(false)
354
444
  end
355
445
 
356
446
  def file_stat(path) # :nodoc:
357
447
  if @follow_symlinks
358
- ::File::stat(path)
448
+ ::File.stat(path)
359
449
  else
360
- ::File::lstat(path)
450
+ ::File.lstat(path)
361
451
  end
362
452
  end
363
453
 
364
454
  def get_extra_attributes_from_path(path) # :nodoc:
365
- unless Zip::RUNNING_ON_WINDOWS
366
- stat = file_stat(path)
367
- @unix_uid = stat.uid
368
- @unix_gid = stat.gid
369
- @unix_perms = stat.mode & 07777
370
- end
455
+ return if Zip::RUNNING_ON_WINDOWS
456
+
457
+ stat = file_stat(path)
458
+ @unix_uid = stat.uid
459
+ @unix_gid = stat.gid
460
+ @unix_perms = stat.mode & 0o7777
461
+ @time = ::Zip::DOSTime.from_time(stat.mtime)
371
462
  end
372
463
 
373
- def set_unix_permissions_on_path(dest_path)
374
- # BUG: does not update timestamps into account
464
+ def set_unix_attributes_on_path(dest_path)
375
465
  # ignore setuid/setgid bits by default. honor if @restore_ownership
376
- unix_perms_mask = 01777
377
- unix_perms_mask = 07777 if @restore_ownership
466
+ unix_perms_mask = 0o1777
467
+ unix_perms_mask = 0o7777 if @restore_ownership
378
468
  ::FileUtils.chmod(@unix_perms & unix_perms_mask, dest_path) if @restore_permissions && @unix_perms
379
469
  ::FileUtils.chown(@unix_uid, @unix_gid, dest_path) if @restore_ownership && @unix_uid && @unix_gid && ::Process.egid == 0
380
- # File::utimes()
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
381
475
  end
382
476
 
383
477
  def set_extra_attributes_on_path(dest_path) # :nodoc:
384
- return unless (file? || directory?)
478
+ return unless file? || directory?
385
479
 
386
480
  case @fstype
387
481
  when ::Zip::FSTYPE_UNIX
388
- set_unix_permissions_on_path(dest_path)
482
+ set_unix_attributes_on_path(dest_path)
389
483
  end
390
484
  end
391
485
 
392
486
  def pack_c_dir_entry
487
+ zip64 = @extra['Zip64']
393
488
  [
394
489
  @header_signature,
395
490
  @version, # version of encoding software
396
491
  @fstype, # filesystem type
397
- @version_needed_to_extract, # @versionNeededToExtract ,
398
- @gp_flags, # @gp_flags ,
492
+ @version_needed_to_extract, # @versionNeededToExtract
493
+ @gp_flags, # @gp_flags
399
494
  @compression_method,
400
- @time.to_binary_dos_time, # @last_mod_time ,
401
- @time.to_binary_dos_date, # @last_mod_date ,
495
+ @time.to_binary_dos_time, # @last_mod_time
496
+ @time.to_binary_dos_date, # @last_mod_date
402
497
  @crc,
403
- @compressed_size,
404
- @size,
498
+ zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
499
+ zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
405
500
  name_size,
406
501
  @extra ? @extra.c_dir_size : 0,
407
502
  comment_size,
408
- 0, # disk number start
503
+ zip64 && zip64.disk_start_number ? 0xFFFF : 0, # disk number start
409
504
  @internal_file_attributes, # file type (binary=0, text=1)
410
505
  @external_file_attributes, # native filesystem attributes
411
- @local_header_offset,
506
+ zip64 && zip64.relative_header_offset ? 0xFFFFFFFF : @local_header_offset,
412
507
  @name,
413
508
  @extra,
414
509
  @comment
@@ -416,22 +511,23 @@ module Zip
416
511
  end
417
512
 
418
513
  def write_c_dir_entry(io) #:nodoc:all
514
+ prep_zip64_extra(false)
419
515
  case @fstype
420
516
  when ::Zip::FSTYPE_UNIX
421
517
  ft = case @ftype
422
518
  when :file
423
- @unix_perms ||= 0644
519
+ @unix_perms ||= 0o644
424
520
  ::Zip::FILE_TYPE_FILE
425
521
  when :directory
426
- @unix_perms ||= 0755
522
+ @unix_perms ||= 0o755
427
523
  ::Zip::FILE_TYPE_DIR
428
524
  when :symlink
429
- @unix_perms ||= 0755
525
+ @unix_perms ||= 0o755
430
526
  ::Zip::FILE_TYPE_SYMLINK
431
527
  end
432
528
 
433
529
  unless ft.nil?
434
- @external_file_attributes = (ft << 12 | (@unix_perms & 07777)) << 16
530
+ @external_file_attributes = (ft << 12 | (@unix_perms & 0o7777)) << 16
435
531
  end
436
532
  end
437
533
 
@@ -444,23 +540,24 @@ module Zip
444
540
 
445
541
  def ==(other)
446
542
  return false unless other.class == self.class
543
+
447
544
  # Compares contents of local entry and exposed fields
448
- keys_equal = %w(compression_method crc compressed_size size name extra filepath).all? do |k|
449
- other.__send__(k.to_sym) == self.__send__(k.to_sym)
545
+ keys_equal = %w[compression_method crc compressed_size size name extra filepath].all? do |k|
546
+ other.__send__(k.to_sym) == __send__(k.to_sym)
450
547
  end
451
- keys_equal && self.time.dos_equals(other.time)
548
+ keys_equal && time == other.time
452
549
  end
453
550
 
454
- def <=> (other)
455
- self.to_s <=> other.to_s
551
+ def <=>(other)
552
+ to_s <=> other.to_s
456
553
  end
457
554
 
458
555
  # Returns an IO like object for the given ZipEntry.
459
556
  # Warning: may behave weird with symlinks.
460
557
  def get_input_stream(&block)
461
558
  if @ftype == :directory
462
- yield(::Zip::NullInputStream.instance) if block_given?
463
- ::Zip::NullInputStream.instance
559
+ yield ::Zip::NullInputStream if block_given?
560
+ ::Zip::NullInputStream
464
561
  elsif @filepath
465
562
  case @ftype
466
563
  when :file
@@ -474,7 +571,8 @@ module Zip
474
571
  raise "unknown @file_type #{@ftype}"
475
572
  end
476
573
  else
477
- zis = ::Zip::InputStream.new(@zipfile, local_header_offset)
574
+ zis = ::Zip::InputStream.new(@zipfile, offset: local_header_offset)
575
+ zis.instance_variable_set(:@complete_entry, self)
478
576
  zis.get_next_entry
479
577
  if block_given?
480
578
  begin
@@ -494,22 +592,22 @@ module Zip
494
592
  when 'file'
495
593
  if name_is_directory?
496
594
  raise ArgumentError,
497
- "entry name '#{newEntry}' indicates directory entry, but "+
498
- "'#{src_path}' is not a directory"
595
+ "entry name '#{newEntry}' indicates directory entry, but " \
596
+ "'#{src_path}' is not a directory"
499
597
  end
500
598
  :file
501
599
  when 'directory'
502
- @name += "/" unless name_is_directory?
600
+ @name += '/' unless name_is_directory?
503
601
  :directory
504
602
  when 'link'
505
603
  if name_is_directory?
506
604
  raise ArgumentError,
507
- "entry name '#{newEntry}' indicates directory entry, but "+
508
- "'#{src_path}' is not a directory"
605
+ "entry name '#{newEntry}' indicates directory entry, but " \
606
+ "'#{src_path}' is not a directory"
509
607
  end
510
608
  :symlink
511
609
  else
512
- raise RuntimeError, "unknown file type: #{src_path.inspect} #{stat.inspect}"
610
+ raise "unknown file type: #{src_path.inspect} #{stat.inspect}"
513
611
  end
514
612
 
515
613
  @filepath = src_path
@@ -518,9 +616,9 @@ module Zip
518
616
 
519
617
  def write_to_zip_output_stream(zip_output_stream) #:nodoc:all
520
618
  if @ftype == :directory
521
- zip_output_stream.put_next_entry(self)
619
+ zip_output_stream.put_next_entry(self, nil, nil, ::Zip::Entry::STORED)
522
620
  elsif @filepath
523
- zip_output_stream.put_next_entry(self, nil, nil, nil)
621
+ zip_output_stream.put_next_entry(self, nil, nil, compression_method || ::Zip::Entry::DEFLATED)
524
622
  get_input_stream { |is| ::Zip::IOExtras.copy_stream(zip_output_stream, is) }
525
623
  else
526
624
  zip_output_stream.copy_raw_entry(self)
@@ -530,11 +628,19 @@ module Zip
530
628
  def parent_as_string
531
629
  entry_name = name.chomp('/')
532
630
  slash_index = entry_name.rindex('/')
533
- slash_index ? entry_name.slice(0, slash_index+1) : nil
631
+ slash_index ? entry_name.slice(0, slash_index + 1) : nil
534
632
  end
535
633
 
536
634
  def get_raw_input_stream(&block)
537
- ::File.open(@zipfile, "rb", &block)
635
+ if @zipfile.respond_to?(:seek) && @zipfile.respond_to?(:read)
636
+ yield @zipfile
637
+ else
638
+ ::File.open(@zipfile, 'rb', &block)
639
+ end
640
+ end
641
+
642
+ def clean_up
643
+ # By default, do nothing
538
644
  end
539
645
 
540
646
  private
@@ -542,35 +648,46 @@ module Zip
542
648
  def set_time(binary_dos_date, binary_dos_time)
543
649
  @time = ::Zip::DOSTime.parse_binary_dos_format(binary_dos_date, binary_dos_time)
544
650
  rescue ArgumentError
545
- puts "Invalid date/time in zip entry"
651
+ warn 'WARNING: invalid date/time in zip entry.' if ::Zip.warn_invalid_date
546
652
  end
547
653
 
548
- def create_file(dest_path, continue_on_exists_proc = proc { Zip.continue_on_exists_proc })
549
- if ::File.exists?(dest_path) && !yield(self, dest_path)
550
- raise ::Zip::ZipDestinationFileExistsError,
654
+ def create_file(dest_path, _continue_on_exists_proc = proc { Zip.continue_on_exists_proc })
655
+ if ::File.exist?(dest_path) && !yield(self, dest_path)
656
+ raise ::Zip::DestinationFileExistsError,
551
657
  "Destination '#{dest_path}' already exists"
552
658
  end
553
- ::File.open(dest_path, "wb") do |os|
659
+ ::File.open(dest_path, 'wb') do |os|
554
660
  get_input_stream do |is|
555
- set_extra_attributes_on_path(dest_path)
556
-
557
- buf = ''
558
- while buf = is.sysread(::Zip::Decompressor::CHUNK_SIZE, buf)
661
+ bytes_written = 0
662
+ warned = false
663
+ buf = +''
664
+ while (buf = is.sysread(::Zip::Decompressor::CHUNK_SIZE, buf))
559
665
  os << buf
666
+ bytes_written += buf.bytesize
667
+ next unless bytes_written > size && !warned
668
+
669
+ message = "entry '#{name}' should be #{size}B, but is larger when inflated."
670
+ raise ::Zip::EntrySizeError, message if ::Zip.validate_entry_sizes
671
+
672
+ warn "WARNING: #{message}"
673
+ warned = true
560
674
  end
561
675
  end
562
676
  end
677
+
678
+ set_extra_attributes_on_path(dest_path)
563
679
  end
564
680
 
565
681
  def create_directory(dest_path)
566
682
  return if ::File.directory?(dest_path)
567
- if ::File.exists?(dest_path)
683
+
684
+ if ::File.exist?(dest_path)
568
685
  if block_given? && yield(self, dest_path)
569
- ::FileUtils::rm_f dest_path
686
+ ::FileUtils.rm_f dest_path
570
687
  else
571
- raise ::Zip::ZipDestinationFileExistsError,
572
- "Cannot create directory '#{dest_path}'. "+
573
- "A file already exists with that name"
688
+ raise ::Zip::DestinationFileExistsError,
689
+ "Cannot create directory '#{dest_path}'. " \
690
+ 'A file already exists with that name'
574
691
  end
575
692
  end
576
693
  ::FileUtils.mkdir_p(dest_path)
@@ -579,41 +696,60 @@ module Zip
579
696
 
580
697
  # BUG: create_symlink() does not use &block
581
698
  def create_symlink(dest_path)
582
- stat = nil
583
- begin
584
- stat = ::File.lstat(dest_path)
585
- rescue Errno::ENOENT
586
- end
699
+ # TODO: Symlinks pose security challenges. Symlink support temporarily
700
+ # removed in view of https://github.com/rubyzip/rubyzip/issues/369 .
701
+ warn "WARNING: skipped symlink '#{dest_path}'."
702
+ end
587
703
 
588
- io = get_input_stream
589
- linkto = io.read
590
-
591
- if stat
592
- if stat.symlink?
593
- if ::File.readlink(dest_path) == linkto
594
- return
595
- else
596
- raise ZipDestinationFileExistsError,
597
- "Cannot create symlink '#{dest_path}'. "+
598
- "A symlink already exists with that name"
599
- end
600
- else
601
- raise ZipDestinationFileExistsError,
602
- "Cannot create symlink '#{dest_path}'. "+
603
- "A file already exists with that name"
604
- end
704
+ # apply missing data from the zip64 extra information field, if present
705
+ # (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?
708
+
709
+ if for_local_header
710
+ @size, @compressed_size = @extra['Zip64'].parse(@size, @compressed_size)
711
+ else
712
+ @size, @compressed_size, @local_header_offset = @extra['Zip64'].parse(@size, @compressed_size, @local_header_offset)
605
713
  end
714
+ end
606
715
 
607
- ::File.symlink(linkto, dest_path)
716
+ def data_descriptor_size
717
+ (@gp_flags & 0x0008) > 0 ? 16 : 0
608
718
  end
609
719
 
610
- def fix_zip64_sizes! #:nodoc:all
611
- if zip64 = @extra["Zip64"]
612
- @size = zip64.original_size
613
- @compressed_size = zip64.compressed_size
720
+ # create a zip64 extra information field if we need one
721
+ def prep_zip64_extra(for_local_header) #:nodoc:all
722
+ return unless ::Zip.write_zip64_support
723
+
724
+ need_zip64 = @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
725
+ need_zip64 ||= @local_header_offset >= 0xFFFFFFFF unless for_local_header
726
+ if need_zip64
727
+ @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
728
+ @extra.delete('Zip64Placeholder')
729
+ zip64 = @extra.create('Zip64')
730
+ if for_local_header
731
+ # local header always includes size and compressed size
732
+ zip64.original_size = @size
733
+ zip64.compressed_size = @compressed_size
734
+ else
735
+ # central directory entry entries include whichever fields are necessary
736
+ zip64.original_size = @size if @size >= 0xFFFFFFFF
737
+ zip64.compressed_size = @compressed_size if @compressed_size >= 0xFFFFFFFF
738
+ zip64.relative_header_offset = @local_header_offset if @local_header_offset >= 0xFFFFFFFF
739
+ end
740
+ else
741
+ @extra.delete('Zip64')
742
+
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
614
751
  end
615
752
  end
616
-
617
753
  end
618
754
  end
619
755