rubyzip 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +95 -43
  3. data/lib/zip.rb +11 -1
  4. data/lib/zip/central_directory.rb +3 -3
  5. data/lib/zip/compressor.rb +1 -2
  6. data/lib/zip/constants.rb +3 -3
  7. data/lib/zip/crypto/null_encryption.rb +2 -4
  8. data/lib/zip/decompressor.rb +1 -1
  9. data/lib/zip/dos_time.rb +1 -1
  10. data/lib/zip/entry.rb +70 -54
  11. data/lib/zip/entry_set.rb +4 -4
  12. data/lib/zip/errors.rb +1 -0
  13. data/lib/zip/extra_field.rb +2 -2
  14. data/lib/zip/extra_field/generic.rb +1 -1
  15. data/lib/zip/extra_field/zip64_placeholder.rb +1 -2
  16. data/lib/zip/file.rb +62 -51
  17. data/lib/zip/filesystem.rb +17 -13
  18. data/lib/zip/inflater.rb +2 -2
  19. data/lib/zip/input_stream.rb +10 -7
  20. data/lib/zip/ioextras/abstract_input_stream.rb +1 -1
  21. data/lib/zip/ioextras/abstract_output_stream.rb +3 -3
  22. data/lib/zip/output_stream.rb +5 -5
  23. data/lib/zip/pass_thru_decompressor.rb +1 -1
  24. data/lib/zip/streamable_stream.rb +1 -1
  25. data/lib/zip/version.rb +1 -1
  26. data/samples/example_recursive.rb +15 -18
  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 +2 -2
  31. data/test/crypto/null_encryption_test.rb +6 -2
  32. data/test/data/gpbit3stored.zip +0 -0
  33. data/test/data/path_traversal/Makefile +10 -0
  34. data/test/data/path_traversal/jwilk/README.md +5 -0
  35. data/test/data/path_traversal/jwilk/absolute1.zip +0 -0
  36. data/test/data/path_traversal/jwilk/absolute2.zip +0 -0
  37. data/test/data/path_traversal/jwilk/dirsymlink.zip +0 -0
  38. data/test/data/path_traversal/jwilk/dirsymlink2a.zip +0 -0
  39. data/test/data/path_traversal/jwilk/dirsymlink2b.zip +0 -0
  40. data/test/data/path_traversal/jwilk/relative0.zip +0 -0
  41. data/test/data/path_traversal/jwilk/relative2.zip +0 -0
  42. data/test/data/path_traversal/jwilk/symlink.zip +0 -0
  43. data/test/data/path_traversal/relative1.zip +0 -0
  44. data/test/data/path_traversal/tilde.zip +0 -0
  45. data/test/data/path_traversal/tuzovakaoff/README.md +3 -0
  46. data/test/data/path_traversal/tuzovakaoff/absolutepath.zip +0 -0
  47. data/test/data/path_traversal/tuzovakaoff/symlink.zip +0 -0
  48. data/test/data/rubycode.zip +0 -0
  49. data/test/entry_set_test.rb +13 -2
  50. data/test/entry_test.rb +3 -12
  51. data/test/errors_test.rb +1 -0
  52. data/test/file_extract_test.rb +62 -0
  53. data/test/file_permissions_test.rb +39 -43
  54. data/test/file_test.rb +115 -12
  55. data/test/filesystem/dir_iterator_test.rb +1 -1
  56. data/test/filesystem/directory_test.rb +29 -11
  57. data/test/filesystem/file_mutating_test.rb +3 -4
  58. data/test/filesystem/file_nonmutating_test.rb +34 -34
  59. data/test/filesystem/file_stat_test.rb +5 -5
  60. data/test/gentestfiles.rb +17 -13
  61. data/test/input_stream_test.rb +10 -10
  62. data/test/ioextras/abstract_input_stream_test.rb +1 -1
  63. data/test/ioextras/abstract_output_stream_test.rb +2 -2
  64. data/test/ioextras/fake_io_test.rb +1 -1
  65. data/test/local_entry_test.rb +1 -1
  66. data/test/path_traversal_test.rb +141 -0
  67. data/test/test_helper.rb +16 -3
  68. data/test/unicode_file_names_and_comments_test.rb +12 -0
  69. data/test/zip64_full_test.rb +2 -2
  70. metadata +103 -51
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Zip
2
- VERSION = '1.2.0'
2
+ VERSION = '1.3.0'
3
3
  end
@@ -6,7 +6,7 @@ require 'zip'
6
6
  # included in the archive, rather just its contents.
7
7
  #
8
8
  # Usage:
9
- # directoryToZip = "/tmp/input"
9
+ # directory_to_zip = "/tmp/input"
10
10
  # output_file = "/tmp/out.zip"
11
11
  # zf = ZipFileGenerator.new(directory_to_zip, output_file)
12
12
  # zf.write()
@@ -19,39 +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.get_output_stream(zip_file_path) do |f|
54
- f.write(File.open(disk_file_path, 'rb').read)
55
- end
51
+ def put_into_archive(disk_file_path, zipfile, zipfile_path)
52
+ zipfile.add(zipfile_path, disk_file_path)
56
53
  end
57
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)
@@ -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)
@@ -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)
@@ -37,7 +37,7 @@ class ZipCentralDirectoryEntryTest < MiniTest::Test
37
37
  assert_equal('', entry.comment)
38
38
 
39
39
  entry = ::Zip::Entry.read_c_dir_entry(file)
40
- assert_equal(nil, entry)
40
+ assert_nil(entry)
41
41
  # Fields that are not check by this test:
42
42
  # version made by 2 bytes
43
43
  # version needed to extract 2 bytes
@@ -18,7 +18,9 @@ class NullEncrypterTest < MiniTest::Test
18
18
  end
19
19
 
20
20
  def test_encrypt
21
- [nil, '', 'a' * 10, 0xffffffff].each do |data|
21
+ assert_nil @encrypter.encrypt(nil)
22
+
23
+ ['', 'a' * 10, 0xffffffff].each do |data|
22
24
  assert_equal data, @encrypter.encrypt(data)
23
25
  end
24
26
  end
@@ -42,7 +44,9 @@ class NullDecrypterTest < MiniTest::Test
42
44
  end
43
45
 
44
46
  def test_decrypt
45
- [nil, '', 'a' * 10, 0xffffffff].each do |data|
47
+ assert_nil @decrypter.decrypt(nil)
48
+
49
+ ['', 'a' * 10, 0xffffffff].each do |data|
46
50
  assert_equal data, @decrypter.decrypt(data)
47
51
  end
48
52
  end
@@ -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
@@ -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
@@ -76,7 +76,7 @@ class ZipEntrySetTest < MiniTest::Test
76
76
  ::Zip.case_insensitive_match = false
77
77
  zipEntrySet = ::Zip::EntrySet.new(entries)
78
78
  assert_equal(entries[0], zipEntrySet.find_entry('MiXeDcAsEnAmE'))
79
- assert_equal(nil, zipEntrySet.find_entry('mixedcasename'))
79
+ assert_nil(zipEntrySet.find_entry('mixedcasename'))
80
80
  end
81
81
 
82
82
  def test_entries_with_sort
@@ -123,7 +123,7 @@ class ZipEntrySetTest < MiniTest::Test
123
123
  ]
124
124
  entrySet = ::Zip::EntrySet.new(entries)
125
125
 
126
- assert_equal(nil, entrySet.parent(entries[0]))
126
+ assert_nil(entrySet.parent(entries[0]))
127
127
  assert_equal(entries[0], entrySet.parent(entries[1]))
128
128
  assert_equal(entries[1], entrySet.parent(entries[2]))
129
129
  end
@@ -149,4 +149,15 @@ class ZipEntrySetTest < MiniTest::Test
149
149
  # assert_equal(entries.size, res.size)
150
150
  # assert_equal(entrySet.map { |e| e.name }, res.map { |e| e.name })
151
151
  end
152
+
153
+ def test_glob3
154
+ entries = [
155
+ ::Zip::Entry.new('zf.zip', 'a/a'),
156
+ ::Zip::Entry.new('zf.zip', 'a/b'),
157
+ ::Zip::Entry.new('zf.zip', 'a/c')
158
+ ]
159
+ entrySet = ::Zip::EntrySet.new(entries)
160
+
161
+ assert_equal(entries[0, 2].sort, entrySet.glob('a/{a,b}').sort)
162
+ end
152
163
  end
@@ -1,16 +1,7 @@
1
1
  require 'test_helper'
2
2
 
3
3
  class ZipEntryTest < MiniTest::Test
4
- TEST_ZIPFILE = 'someZipFile.zip'
5
- TEST_COMMENT = 'a comment'
6
- TEST_COMPRESSED_SIZE = 1234
7
- TEST_CRC = 325_324
8
- TEST_EXTRA = 'Some data here'
9
- TEST_COMPRESSIONMETHOD = ::Zip::Entry::DEFLATED
10
- TEST_NAME = 'entry name'
11
- TEST_SIZE = 8432
12
- TEST_ISDIRECTORY = false
13
- TEST_TIME = Time.now
4
+ include ZipEntryData
14
5
 
15
6
  def test_constructor_and_getters
16
7
  entry = ::Zip::Entry.new(TEST_ZIPFILE,
@@ -118,8 +109,8 @@ class ZipEntryTest < MiniTest::Test
118
109
  entry5 = ::Zip::Entry.new('zf.zip', 'aa/bb/cc')
119
110
  entry6 = ::Zip::Entry.new('zf.zip', 'aa/bb/cc/')
120
111
 
121
- assert_equal(nil, entry1.parent_as_string)
122
- assert_equal(nil, entry2.parent_as_string)
112
+ assert_nil(entry1.parent_as_string)
113
+ assert_nil(entry2.parent_as_string)
123
114
  assert_equal('aa/', entry3.parent_as_string)
124
115
  assert_equal('aa/', entry4.parent_as_string)
125
116
  assert_equal('aa/bb/', entry5.parent_as_string)
@@ -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
@@ -1,69 +1,65 @@
1
1
  require 'test_helper'
2
2
 
3
3
  class FilePermissionsTest < MiniTest::Test
4
-
5
- FILENAME = File.join(File.dirname(__FILE__), "umask.zip")
4
+ ZIPNAME = File.join(File.dirname(__FILE__), 'umask.zip')
5
+ FILENAME = File.join(File.dirname(__FILE__), 'umask.txt')
6
6
 
7
7
  def teardown
8
+ ::File.unlink(ZIPNAME)
8
9
  ::File.unlink(FILENAME)
9
10
  end
10
11
 
11
- if ::Zip::RUNNING_ON_WINDOWS
12
- # Windows tests
13
-
14
- DEFAULT_PERMS = 0644
15
-
16
- def test_windows_perms
17
- create_file
12
+ def test_current_umask
13
+ create_files
14
+ assert_matching_permissions FILENAME, ZIPNAME
15
+ end
18
16
 
19
- assert_equal DEFAULT_PERMS, ::File.stat(FILENAME).mode
17
+ def test_umask_000
18
+ set_umask(0o000) do
19
+ create_files
20
20
  end
21
21
 
22
- else
23
- # Unix tests
24
-
25
- DEFAULT_PERMS = 0100666
26
-
27
- def test_current_umask
28
- umask = DEFAULT_PERMS - ::File.umask
29
- create_file
22
+ assert_matching_permissions FILENAME, ZIPNAME
23
+ end
30
24
 
31
- assert_equal umask, ::File.stat(FILENAME).mode
25
+ def test_umask_066
26
+ set_umask(0o066) do
27
+ create_files
32
28
  end
33
29
 
34
- def test_umask_000
35
- set_umask(0000) do
36
- create_file
37
- end
30
+ assert_matching_permissions FILENAME, ZIPNAME
31
+ end
38
32
 
39
- assert_equal DEFAULT_PERMS, ::File.stat(FILENAME).mode
33
+ def test_umask_027
34
+ set_umask(0o027) do
35
+ create_files
40
36
  end
41
37
 
42
- def test_umask_066
43
- umask = 0066
44
- set_umask(umask) do
45
- create_file
46
- end
47
-
48
- assert_equal((DEFAULT_PERMS - umask), ::File.stat(FILENAME).mode)
49
- end
38
+ assert_matching_permissions FILENAME, ZIPNAME
39
+ end
50
40
 
41
+ def assert_matching_permissions(expected_file, actual_file)
42
+ assert_equal(
43
+ ::File.stat(expected_file).mode.to_s(8).rjust(4, '0'),
44
+ ::File.stat(actual_file).mode.to_s(8).rjust(4, '0')
45
+ )
51
46
  end
52
47
 
53
- def create_file
54
- ::Zip::File.open(FILENAME, ::Zip::File::CREATE) do |zip|
55
- zip.comment = "test"
48
+ def create_files
49
+ ::Zip::File.open(ZIPNAME, ::Zip::File::CREATE) do |zip|
50
+ zip.comment = 'test'
56
51
  end
57
- end
58
52
 
59
- # If anything goes wrong, make sure the umask is restored.
60
- def set_umask(umask, &block)
61
- begin
62
- saved_umask = ::File.umask(umask)
63
- yield
64
- ensure
65
- ::File.umask(saved_umask)
53
+ ::File.open(FILENAME, 'w') do |file|
54
+ file << 'test'
66
55
  end
67
56
  end
68
57
 
58
+ # If anything goes wrong, make sure the umask is restored.
59
+ def set_umask(umask)
60
+ saved_umask = ::File.umask(umask)
61
+ yield
62
+ ensure
63
+ ::File.umask(saved_umask)
64
+ end
69
65
  end