rubyzip 1.2.1 → 1.3.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 (64) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +89 -35
  3. data/lib/zip/central_directory.rb +3 -3
  4. data/lib/zip/compressor.rb +1 -2
  5. data/lib/zip/constants.rb +3 -3
  6. data/lib/zip/crypto/null_encryption.rb +2 -4
  7. data/lib/zip/decompressor.rb +1 -1
  8. data/lib/zip/dos_time.rb +1 -1
  9. data/lib/zip/entry.rb +67 -57
  10. data/lib/zip/entry_set.rb +3 -3
  11. data/lib/zip/errors.rb +1 -0
  12. data/lib/zip/extra_field/generic.rb +1 -1
  13. data/lib/zip/extra_field/zip64_placeholder.rb +1 -2
  14. data/lib/zip/extra_field.rb +2 -2
  15. data/lib/zip/file.rb +47 -27
  16. data/lib/zip/filesystem.rb +17 -13
  17. data/lib/zip/inflater.rb +2 -2
  18. data/lib/zip/input_stream.rb +10 -7
  19. data/lib/zip/ioextras/abstract_input_stream.rb +1 -1
  20. data/lib/zip/ioextras/abstract_output_stream.rb +1 -1
  21. data/lib/zip/output_stream.rb +5 -5
  22. data/lib/zip/pass_thru_decompressor.rb +1 -1
  23. data/lib/zip/streamable_stream.rb +1 -1
  24. data/lib/zip/version.rb +1 -1
  25. data/lib/zip.rb +11 -1
  26. data/samples/example_recursive.rb +14 -15
  27. data/samples/gtk_ruby_zip.rb +1 -1
  28. data/samples/qtzip.rb +1 -1
  29. data/samples/zipfind.rb +2 -2
  30. data/test/central_directory_entry_test.rb +1 -1
  31. data/test/data/gpbit3stored.zip +0 -0
  32. data/test/data/path_traversal/Makefile +10 -0
  33. data/test/data/path_traversal/jwilk/README.md +5 -0
  34. data/test/data/path_traversal/jwilk/absolute1.zip +0 -0
  35. data/test/data/path_traversal/jwilk/absolute2.zip +0 -0
  36. data/test/data/path_traversal/jwilk/dirsymlink.zip +0 -0
  37. data/test/data/path_traversal/jwilk/dirsymlink2a.zip +0 -0
  38. data/test/data/path_traversal/jwilk/dirsymlink2b.zip +0 -0
  39. data/test/data/path_traversal/jwilk/relative0.zip +0 -0
  40. data/test/data/path_traversal/jwilk/relative2.zip +0 -0
  41. data/test/data/path_traversal/jwilk/symlink.zip +0 -0
  42. data/test/data/path_traversal/relative1.zip +0 -0
  43. data/test/data/path_traversal/tilde.zip +0 -0
  44. data/test/data/path_traversal/tuzovakaoff/README.md +3 -0
  45. data/test/data/path_traversal/tuzovakaoff/absolutepath.zip +0 -0
  46. data/test/data/path_traversal/tuzovakaoff/symlink.zip +0 -0
  47. data/test/data/rubycode.zip +0 -0
  48. data/test/errors_test.rb +1 -0
  49. data/test/file_extract_test.rb +62 -0
  50. data/test/file_permissions_test.rb +11 -15
  51. data/test/file_test.rb +92 -9
  52. data/test/filesystem/dir_iterator_test.rb +1 -1
  53. data/test/filesystem/directory_test.rb +29 -11
  54. data/test/filesystem/file_mutating_test.rb +3 -4
  55. data/test/filesystem/file_nonmutating_test.rb +31 -31
  56. data/test/filesystem/file_stat_test.rb +4 -4
  57. data/test/gentestfiles.rb +13 -13
  58. data/test/input_stream_test.rb +6 -6
  59. data/test/ioextras/abstract_output_stream_test.rb +2 -2
  60. data/test/path_traversal_test.rb +141 -0
  61. data/test/test_helper.rb +2 -2
  62. data/test/unicode_file_names_and_comments_test.rb +12 -0
  63. data/test/zip64_full_test.rb +2 -2
  64. metadata +103 -51
data/lib/zip/file.rb CHANGED
@@ -64,25 +64,38 @@ module Zip
64
64
 
65
65
  # Opens a zip archive. Pass true as the second parameter to create
66
66
  # a new archive if it doesn't exist already.
67
- def initialize(file_name, create = false, buffer = false, options = {})
67
+ def initialize(path_or_io, create = false, buffer = false, options = {})
68
68
  super()
69
- @name = file_name
69
+ @name = path_or_io.respond_to?(:path) ? path_or_io.path : path_or_io
70
70
  @comment = ''
71
71
  @create = create ? true : false # allow any truthy value to mean true
72
- case
73
- when !buffer && ::File.size?(file_name)
72
+
73
+ if ::File.size?(@name.to_s)
74
+ # There is a file, which exists, that is associated with this zip.
74
75
  @create = false
75
- @file_permissions = ::File.stat(file_name).mode
76
- ::File.open(name, 'rb') do |f|
77
- read_from_stream(f)
76
+ @file_permissions = ::File.stat(@name).mode
77
+
78
+ if buffer
79
+ read_from_stream(path_or_io)
80
+ else
81
+ ::File.open(@name, 'rb') do |f|
82
+ read_from_stream(f)
83
+ end
78
84
  end
79
- when @create
85
+ elsif buffer && path_or_io.size > 0
86
+ # This zip is probably a non-empty StringIO.
87
+ read_from_stream(path_or_io)
88
+ elsif @create
89
+ # This zip is completely new/empty and is to be created.
80
90
  @entry_set = EntrySet.new
81
- when ::File.zero?(file_name)
82
- raise Error, "File #{file_name} has zero size. Did you mean to pass the create flag?"
91
+ elsif ::File.zero?(@name)
92
+ # A file exists, but it is empty.
93
+ raise Error, "File #{@name} has zero size. Did you mean to pass the create flag?"
83
94
  else
84
- raise Error, "File #{file_name} not found"
95
+ # Everything is wrong.
96
+ raise Error, "File #{@name} not found"
85
97
  end
98
+
86
99
  @stored_entries = @entry_set.dup
87
100
  @stored_comment = @comment
88
101
  @restore_ownership = options[:restore_ownership] || false
@@ -120,17 +133,16 @@ module Zip
120
133
  unless IO_METHODS.map { |method| io.respond_to?(method) }.all? || io.is_a?(String)
121
134
  raise "Zip::File.open_buffer expects a String or IO-like argument (responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
122
135
  end
123
- if io.is_a?(::String)
124
- require 'stringio'
125
- io = ::StringIO.new(io)
126
- elsif io.respond_to?(:binmode)
127
- # https://github.com/rubyzip/rubyzip/issues/119
128
- io.binmode
129
- end
136
+
137
+ io = ::StringIO.new(io) if io.is_a?(::String)
138
+
139
+ # https://github.com/rubyzip/rubyzip/issues/119
140
+ io.binmode if io.respond_to?(:binmode)
141
+
130
142
  zf = ::Zip::File.new(io, true, true, options)
131
- zf.read_from_stream(io)
132
143
  return zf unless block_given?
133
144
  yield zf
145
+
134
146
  begin
135
147
  zf.write_buffer(io)
136
148
  rescue IOError => e
@@ -151,10 +163,9 @@ module Zip
151
163
  end
152
164
 
153
165
  def get_segment_size_for_split(segment_size)
154
- case
155
- when MIN_SEGMENT_SIZE > segment_size
166
+ if MIN_SEGMENT_SIZE > segment_size
156
167
  MIN_SEGMENT_SIZE
157
- when MAX_SEGMENT_SIZE < segment_size
168
+ elsif MAX_SEGMENT_SIZE < segment_size
158
169
  MAX_SEGMENT_SIZE
159
170
  else
160
171
  segment_size
@@ -162,8 +173,10 @@ module Zip
162
173
  end
163
174
 
164
175
  def get_partial_zip_file_name(zip_file_name, partial_zip_file_name)
165
- partial_zip_file_name = zip_file_name.sub(/#{::File.basename(zip_file_name)}\z/,
166
- partial_zip_file_name + ::File.extname(zip_file_name)) unless partial_zip_file_name.nil?
176
+ unless partial_zip_file_name.nil?
177
+ partial_zip_file_name = zip_file_name.sub(/#{::File.basename(zip_file_name)}\z/,
178
+ partial_zip_file_name + ::File.extname(zip_file_name))
179
+ end
167
180
  partial_zip_file_name ||= zip_file_name
168
181
  partial_zip_file_name
169
182
  end
@@ -237,7 +250,7 @@ module Zip
237
250
  # specified. If a block is passed the stream object is passed to the block and
238
251
  # the stream is automatically closed afterwards just as with ruby's builtin
239
252
  # File.open method.
240
- def get_output_stream(entry, permission_int = nil, comment = nil, extra = nil, compressed_size = nil, crc = nil, compression_method = nil, size = nil, time = nil, &aProc)
253
+ def get_output_stream(entry, permission_int = nil, comment = nil, extra = nil, compressed_size = nil, crc = nil, compression_method = nil, size = nil, time = nil, &aProc)
241
254
  new_entry =
242
255
  if entry.kind_of?(Entry)
243
256
  entry
@@ -274,6 +287,13 @@ module Zip
274
287
  @entry_set << new_entry
275
288
  end
276
289
 
290
+ # Convenience method for adding the contents of a file to the archive
291
+ # in Stored format (uncompressed)
292
+ def add_stored(entry, src_path, &continue_on_exists_proc)
293
+ entry = ::Zip::Entry.new(@name, entry.to_s, nil, nil, nil, nil, ::Zip::Entry::STORED)
294
+ add(entry, src_path, &continue_on_exists_proc)
295
+ end
296
+
277
297
  # Removes the specified entry.
278
298
  def remove(entry)
279
299
  @entry_set.delete(get_entry(entry))
@@ -306,7 +326,7 @@ module Zip
306
326
  # Commits changes that has been made since the previous commit to
307
327
  # the zip archive.
308
328
  def commit
309
- return unless commit_required?
329
+ return if name.is_a?(StringIO) || !commit_required?
310
330
  on_success_replace do |tmp_file|
311
331
  ::Zip::OutputStream.open(tmp_file) do |zos|
312
332
  @entry_set.each do |e|
@@ -366,7 +386,7 @@ module Zip
366
386
  end
367
387
 
368
388
  # Creates a directory
369
- def mkdir(entryName, permissionInt = 0755)
389
+ def mkdir(entryName, permissionInt = 0o755)
370
390
  raise Errno::EEXIST, "File exists - #{entryName}" if find_entry(entryName)
371
391
  entryName = entryName.dup.to_s
372
392
  entryName << '/' unless entryName.end_with?('/')
@@ -142,9 +142,9 @@ module Zip
142
142
 
143
143
  def ftype
144
144
  if file?
145
- return 'file'
145
+ 'file'
146
146
  elsif directory?
147
- return 'directory'
147
+ 'directory'
148
148
  else
149
149
  raise StandardError, 'Unknown file type'
150
150
  end
@@ -198,30 +198,30 @@ module Zip
198
198
  alias grpowned? exists?
199
199
 
200
200
  def readable?(fileName)
201
- unix_mode_cmp(fileName, 0444)
201
+ unix_mode_cmp(fileName, 0o444)
202
202
  end
203
203
  alias readable_real? readable?
204
204
 
205
205
  def writable?(fileName)
206
- unix_mode_cmp(fileName, 0222)
206
+ unix_mode_cmp(fileName, 0o222)
207
207
  end
208
208
  alias writable_real? writable?
209
209
 
210
210
  def executable?(fileName)
211
- unix_mode_cmp(fileName, 0111)
211
+ unix_mode_cmp(fileName, 0o111)
212
212
  end
213
213
  alias executable_real? executable?
214
214
 
215
215
  def setuid?(fileName)
216
- unix_mode_cmp(fileName, 04000)
216
+ unix_mode_cmp(fileName, 0o4000)
217
217
  end
218
218
 
219
219
  def setgid?(fileName)
220
- unix_mode_cmp(fileName, 02000)
220
+ unix_mode_cmp(fileName, 0o2000)
221
221
  end
222
222
 
223
223
  def sticky?(fileName)
224
- unix_mode_cmp(fileName, 01000)
224
+ unix_mode_cmp(fileName, 0o1000)
225
225
  end
226
226
 
227
227
  def umask(*args)
@@ -237,8 +237,8 @@ module Zip
237
237
  expand_path(fileName) == '/' || (!entry.nil? && entry.directory?)
238
238
  end
239
239
 
240
- def open(fileName, openMode = 'r', permissionInt = 0644, &block)
241
- openMode.gsub!('b', '') # ignore b option
240
+ def open(fileName, openMode = 'r', permissionInt = 0o644, &block)
241
+ openMode.delete!('b') # ignore b option
242
242
  case openMode
243
243
  when 'r'
244
244
  @mappedZip.get_input_stream(fileName, &block)
@@ -260,7 +260,7 @@ module Zip
260
260
  # Returns nil for not found and nil for directories
261
261
  def size?(fileName)
262
262
  entry = @mappedZip.find_entry(fileName)
263
- (entry.nil? || entry.directory?) ? nil : entry.size
263
+ entry.nil? || entry.directory? ? nil : entry.size
264
264
  end
265
265
 
266
266
  def chown(ownerInt, groupInt, *filenames)
@@ -498,7 +498,7 @@ module Zip
498
498
  alias rmdir delete
499
499
  alias unlink delete
500
500
 
501
- def mkdir(entryName, permissionInt = 0755)
501
+ def mkdir(entryName, permissionInt = 0o755)
502
502
  @mappedZip.mkdir(entryName, permissionInt)
503
503
  end
504
504
 
@@ -573,6 +573,10 @@ module Zip
573
573
  @zipFile.get_output_stream(expand_to_entry(fileName), permissionInt, &aProc)
574
574
  end
575
575
 
576
+ def glob(pattern, *flags, &block)
577
+ @zipFile.glob(expand_to_entry(pattern), *flags, &block)
578
+ end
579
+
576
580
  def read(fileName)
577
581
  @zipFile.read(expand_to_entry(fileName))
578
582
  end
@@ -586,7 +590,7 @@ module Zip
586
590
  &continueOnExistsProc)
587
591
  end
588
592
 
589
- def mkdir(fileName, permissionInt = 0755)
593
+ def mkdir(fileName, permissionInt = 0o755)
590
594
  @zipFile.mkdir(expand_to_entry(fileName), permissionInt)
591
595
  end
592
596
 
data/lib/zip/inflater.rb CHANGED
@@ -3,9 +3,9 @@ module Zip
3
3
  def initialize(input_stream, decrypter = NullDecrypter.new)
4
4
  super(input_stream)
5
5
  @zlib_inflater = ::Zlib::Inflate.new(-Zlib::MAX_WBITS)
6
- @output_buffer = ''
6
+ @output_buffer = ''.dup
7
7
  @has_returned_empty_string = false
8
- @decrypter = decrypter
8
+ @decrypter = decrypter
9
9
  end
10
10
 
11
11
  def sysread(number_of_bytes = nil, buf = '')
@@ -129,23 +129,26 @@ module Zip
129
129
  end
130
130
  if @current_entry && @current_entry.gp_flags & 8 == 8 && @current_entry.crc == 0 \
131
131
  && @current_entry.compressed_size == 0 \
132
- && @current_entry.size == 0 && !@internal
132
+ && @current_entry.size == 0 && !@complete_entry
133
133
  raise GPFBit3Error,
134
134
  'General purpose flag Bit 3 is set so not possible to get proper info from local header.' \
135
135
  'Please use ::Zip::File instead of ::Zip::InputStream'
136
136
  end
137
- @decompressor = get_decompressor
137
+ @decompressor = get_decompressor
138
138
  flush
139
139
  @current_entry
140
140
  end
141
141
 
142
142
  def get_decompressor
143
- case
144
- when @current_entry.nil?
143
+ if @current_entry.nil?
145
144
  ::Zip::NullDecompressor
146
- when @current_entry.compression_method == ::Zip::Entry::STORED
147
- ::Zip::PassThruDecompressor.new(@archive_io, @current_entry.size)
148
- when @current_entry.compression_method == ::Zip::Entry::DEFLATED
145
+ elsif @current_entry.compression_method == ::Zip::Entry::STORED
146
+ if @current_entry.gp_flags & 8 == 8 && @current_entry.crc == 0 && @current_entry.size == 0 && @complete_entry
147
+ ::Zip::PassThruDecompressor.new(@archive_io, @complete_entry.size)
148
+ else
149
+ ::Zip::PassThruDecompressor.new(@archive_io, @current_entry.size)
150
+ end
151
+ elsif @current_entry.compression_method == ::Zip::Entry::DEFLATED
149
152
  header = @archive_io.read(@decrypter.header_bytesize)
150
153
  @decrypter.reset!(header)
151
154
  ::Zip::Inflater.new(@archive_io, @decrypter)
@@ -33,7 +33,7 @@ module Zip
33
33
  sysread(number_of_bytes, buf)
34
34
  end
35
35
 
36
- if tbuf.nil? || tbuf.length == 0
36
+ if tbuf.nil? || tbuf.empty?
37
37
  return nil if number_of_bytes
38
38
  return ''
39
39
  end
@@ -15,7 +15,7 @@ module Zip
15
15
  end
16
16
 
17
17
  def printf(a_format_string, *params)
18
- self << sprintf(a_format_string, *params)
18
+ self << format(a_format_string, *params)
19
19
  end
20
20
 
21
21
  def putc(an_object)
@@ -87,11 +87,11 @@ module Zip
87
87
  # +entry+ can be a ZipEntry object or a string.
88
88
  def put_next_entry(entry_name, comment = nil, extra = nil, compression_method = Entry::DEFLATED, level = Zip.default_compression)
89
89
  raise Error, 'zip stream is closed' if @closed
90
- if entry_name.kind_of?(Entry)
91
- new_entry = entry_name
92
- else
93
- new_entry = Entry.new(@file_name, entry_name.to_s)
94
- end
90
+ new_entry = if entry_name.kind_of?(Entry)
91
+ entry_name
92
+ else
93
+ Entry.new(@file_name, entry_name.to_s)
94
+ end
95
95
  new_entry.comment = comment unless comment.nil?
96
96
  unless extra.nil?
97
97
  new_entry.extra = extra.is_a?(ExtraField) ? extra : ExtraField.new(extra.to_s)
@@ -1,5 +1,5 @@
1
1
  module Zip
2
- class PassThruDecompressor < Decompressor #:nodoc:all
2
+ class PassThruDecompressor < Decompressor #:nodoc:all
3
3
  def initialize(input_stream, chars_to_read)
4
4
  super(input_stream)
5
5
  @chars_to_read = chars_to_read
@@ -5,7 +5,7 @@ module Zip
5
5
  dirname = if zipfile.is_a?(::String)
6
6
  ::File.dirname(zipfile)
7
7
  else
8
- '.'
8
+ nil
9
9
  end
10
10
  @temp_file = Tempfile.new(::File.basename(name), dirname)
11
11
  @temp_file.binmode
data/lib/zip/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Zip
2
- VERSION = '1.2.1'
2
+ VERSION = '1.3.0'
3
3
  end
data/lib/zip.rb CHANGED
@@ -34,7 +34,16 @@ require 'zip/errors'
34
34
 
35
35
  module Zip
36
36
  extend self
37
- attr_accessor :unicode_names, :on_exists_proc, :continue_on_exists_proc, :sort_entries, :default_compression, :write_zip64_support, :warn_invalid_date, :case_insensitive_match
37
+ attr_accessor :unicode_names,
38
+ :on_exists_proc,
39
+ :continue_on_exists_proc,
40
+ :sort_entries,
41
+ :default_compression,
42
+ :write_zip64_support,
43
+ :warn_invalid_date,
44
+ :case_insensitive_match,
45
+ :force_entry_names_encoding,
46
+ :validate_entry_sizes
38
47
 
39
48
  def reset!
40
49
  @_ran_once = false
@@ -46,6 +55,7 @@ module Zip
46
55
  @write_zip64_support = false
47
56
  @warn_invalid_date = true
48
57
  @case_insensitive_match = false
58
+ @validate_entry_sizes = false
49
59
  end
50
60
 
51
61
  def setup
@@ -19,37 +19,36 @@ class ZipFileGenerator
19
19
 
20
20
  # Zip the input directory.
21
21
  def write
22
- entries = Dir.entries(@input_dir) - %w(. ..)
22
+ entries = Dir.entries(@input_dir) - %w[. ..]
23
23
 
24
- ::Zip::File.open(@output_file, ::Zip::File::CREATE) do |io|
25
- write_entries entries, '', io
24
+ ::Zip::File.open(@output_file, ::Zip::File::CREATE) do |zipfile|
25
+ write_entries entries, '', zipfile
26
26
  end
27
27
  end
28
28
 
29
29
  private
30
30
 
31
31
  # A helper method to make the recursion work.
32
- def write_entries(entries, path, io)
32
+ def write_entries(entries, path, zipfile)
33
33
  entries.each do |e|
34
- zip_file_path = path == '' ? e : File.join(path, e)
35
- disk_file_path = File.join(@input_dir, zip_file_path)
36
- puts "Deflating #{disk_file_path}"
34
+ zipfile_path = path == '' ? e : File.join(path, e)
35
+ disk_file_path = File.join(@input_dir, zipfile_path)
37
36
 
38
37
  if File.directory? disk_file_path
39
- recursively_deflate_directory(disk_file_path, io, zip_file_path)
38
+ recursively_deflate_directory(disk_file_path, zipfile, zipfile_path)
40
39
  else
41
- put_into_archive(disk_file_path, io, zip_file_path)
40
+ put_into_archive(disk_file_path, zipfile, zipfile_path)
42
41
  end
43
42
  end
44
43
  end
45
44
 
46
- def recursively_deflate_directory(disk_file_path, io, zip_file_path)
47
- io.mkdir zip_file_path
48
- subdir = Dir.entries(disk_file_path) - %w(. ..)
49
- write_entries subdir, zip_file_path, io
45
+ def recursively_deflate_directory(disk_file_path, zipfile, zipfile_path)
46
+ zipfile.mkdir zipfile_path
47
+ subdir = Dir.entries(disk_file_path) - %w[. ..]
48
+ write_entries subdir, zipfile_path, zipfile
50
49
  end
51
50
 
52
- def put_into_archive(disk_file_path, io, zip_file_path)
53
- io.add(zip_file_path, disk_file_path)
51
+ def put_into_archive(disk_file_path, zipfile, zipfile_path)
52
+ zipfile.add(zipfile_path, disk_file_path)
54
53
  end
55
54
  end
@@ -31,7 +31,7 @@ class MainApp < Gtk::Window
31
31
  sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
32
32
  box.pack_start(sw, true, true, 0)
33
33
 
34
- @clist = Gtk::CList.new(%w(Name Size Compression))
34
+ @clist = Gtk::CList.new(%w[Name Size Compression])
35
35
  @clist.set_selection_mode(Gtk::SELECTION_BROWSE)
36
36
  @clist.set_column_width(0, 120)
37
37
  @clist.set_column_width(1, 120)
data/samples/qtzip.rb CHANGED
@@ -65,7 +65,7 @@ class ZipDialog < ZipDialogUI
65
65
  end
66
66
  puts "selected_items.size = #{selected_items.size}"
67
67
  puts "unselected_items.size = #{unselected_items.size}"
68
- items = selected_items.size > 0 ? selected_items : unselected_items
68
+ items = !selected_items.empty? ? selected_items : unselected_items
69
69
  puts "items.size = #{items.size}"
70
70
 
71
71
  d = Qt::FileDialog.get_existing_directory(nil, self)
data/samples/zipfind.rb CHANGED
@@ -31,7 +31,7 @@ module Zip
31
31
  end
32
32
  end
33
33
 
34
- if __FILE__ == $0
34
+ if $0 == __FILE__
35
35
  module ZipFindConsoleRunner
36
36
  PATH_ARG_INDEX = 0
37
37
  FILENAME_PATTERN_ARG_INDEX = 1
@@ -47,7 +47,7 @@ if __FILE__ == $0
47
47
  end
48
48
 
49
49
  def self.check_args(args)
50
- if (args.size != 3)
50
+ if args.size != 3
51
51
  usage
52
52
  exit
53
53
  end
@@ -2,7 +2,7 @@ require 'test_helper'
2
2
 
3
3
  class ZipCentralDirectoryEntryTest < MiniTest::Test
4
4
  def test_read_from_stream
5
- File.open('test/data/testDirectory.bin', 'rb') do |file|
5
+ File.open('test/data/testDirectory.bin', 'rb') do |file|
6
6
  entry = ::Zip::Entry.read_c_dir_entry(file)
7
7
 
8
8
  assert_equal('longAscii.txt', entry.name)
Binary file
@@ -0,0 +1,10 @@
1
+ # Based on 'relative2' in https://github.com/jwilk/path-traversal-samples,
2
+ # but create the local `tmp` folder before adding the symlink. Otherwise
3
+ # we may bail out before we get to trying to create the file.
4
+ all: relative1.zip
5
+ relative1.zip:
6
+ rm -f $(@)
7
+ mkdir -p -m 755 tmp/tmp
8
+ umask 022 && echo moo > moo
9
+ cd tmp && zip -X ../$(@) tmp tmp/../../moo
10
+ rm -rf tmp moo
@@ -0,0 +1,5 @@
1
+ # Path Traversal Samples
2
+
3
+ Copied from https://github.com/jwilk/path-traversal-samples on 2018-08-26.
4
+
5
+ License: MIT
Binary file
@@ -0,0 +1,3 @@
1
+ # Path Traversal Samples
2
+
3
+ Copied from https://github.com/tuzovakaoff/zip_path_traversal on 2018-08-25.
Binary file
data/test/errors_test.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # encoding: utf-8
2
+
2
3
  require 'test_helper'
3
4
 
4
5
  class ErrorsTest < MiniTest::Test
@@ -10,6 +10,10 @@ class ZipFileExtractTest < MiniTest::Test
10
10
  ::File.delete(EXTRACTED_FILENAME) if ::File.exist?(EXTRACTED_FILENAME)
11
11
  end
12
12
 
13
+ def teardown
14
+ ::Zip.reset!
15
+ end
16
+
13
17
  def test_extract
14
18
  ::Zip::File.open(TEST_ZIP.zip_name) do |zf|
15
19
  zf.extract(ENTRY_TO_EXTRACT, EXTRACTED_FILENAME)
@@ -80,4 +84,62 @@ class ZipFileExtractTest < MiniTest::Test
80
84
  end
81
85
  assert(!File.exist?(outFile))
82
86
  end
87
+
88
+ def test_extract_incorrect_size
89
+ # The uncompressed size fields in the zip file cannot be trusted. This makes
90
+ # it harder for callers to validate the sizes of the files they are
91
+ # extracting, which can lead to denial of service. See also
92
+ # https://en.wikipedia.org/wiki/Zip_bomb
93
+ Dir.mktmpdir do |tmp|
94
+ real_zip = File.join(tmp, 'real.zip')
95
+ fake_zip = File.join(tmp, 'fake.zip')
96
+ file_name = 'a'
97
+ true_size = 500_000
98
+ fake_size = 1
99
+
100
+ ::Zip::File.open(real_zip, ::Zip::File::CREATE) do |zf|
101
+ zf.get_output_stream(file_name) do |os|
102
+ os.write 'a' * true_size
103
+ end
104
+ end
105
+
106
+ compressed_size = nil
107
+ ::Zip::File.open(real_zip) do |zf|
108
+ a_entry = zf.find_entry(file_name)
109
+ compressed_size = a_entry.compressed_size
110
+ assert_equal true_size, a_entry.size
111
+ end
112
+
113
+ true_size_bytes = [compressed_size, true_size, file_name.size].pack('LLS')
114
+ fake_size_bytes = [compressed_size, fake_size, file_name.size].pack('LLS')
115
+
116
+ data = File.binread(real_zip)
117
+ assert data.include?(true_size_bytes)
118
+ data.gsub! true_size_bytes, fake_size_bytes
119
+
120
+ File.open(fake_zip, 'wb') do |file|
121
+ file.write data
122
+ end
123
+
124
+ Dir.chdir tmp do
125
+ ::Zip::File.open(fake_zip) do |zf|
126
+ a_entry = zf.find_entry(file_name)
127
+ assert_equal fake_size, a_entry.size
128
+
129
+ ::Zip.validate_entry_sizes = false
130
+ a_entry.extract
131
+ assert_equal true_size, File.size(file_name)
132
+ FileUtils.rm file_name
133
+
134
+ ::Zip.validate_entry_sizes = true
135
+ error = assert_raises ::Zip::EntrySizeError do
136
+ a_entry.extract
137
+ end
138
+ assert_equal \
139
+ 'Entry a should be 1B but is larger when inflated',
140
+ error.message
141
+ end
142
+ end
143
+ end
144
+ end
83
145
  end