rubyzip 1.2.0 → 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 (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