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
@@ -5,7 +5,7 @@ module Zip
5
5
 
6
6
  def initialize(an_enumerable = [])
7
7
  super()
8
- @entry_set = {}
8
+ @entry_set = {}
9
9
  an_enumerable.each { |o| push(o) }
10
10
  end
11
11
 
@@ -33,9 +33,9 @@ module Zip
33
33
  entry if @entry_set.delete(to_key(entry))
34
34
  end
35
35
 
36
- def each(&block)
36
+ def each
37
37
  @entry_set = sorted_entries.dup.each do |_, value|
38
- block.call(value)
38
+ yield(value)
39
39
  end
40
40
  end
41
41
 
@@ -57,7 +57,7 @@ module Zip
57
57
  @entry_set[to_key(entry.parent_as_string)]
58
58
  end
59
59
 
60
- def glob(pattern, flags = ::File::FNM_PATHNAME | ::File::FNM_DOTMATCH)
60
+ def glob(pattern, flags = ::File::FNM_PATHNAME | ::File::FNM_DOTMATCH | ::File::FNM_EXTGLOB)
61
61
  entries.map do |entry|
62
62
  next nil unless ::File.fnmatch(pattern, entry.name.chomp('/'), flags)
63
63
  yield(entry) if block_given?
@@ -4,6 +4,7 @@ module Zip
4
4
  class DestinationFileExistsError < Error; end
5
5
  class CompressionMethodError < Error; end
6
6
  class EntryNameError < Error; end
7
+ class EntrySizeError < Error; end
7
8
  class InternalError < Error; end
8
9
  class GPFBit3Error < Error; end
9
10
 
@@ -8,7 +8,7 @@ module Zip
8
8
 
9
9
  def extra_field_type_exist(binstr, id, len, i)
10
10
  field_name = ID_MAP[id].name
11
- if self.member?(field_name)
11
+ if member?(field_name)
12
12
  self[field_name].merge(binstr[i, len + 4])
13
13
  else
14
14
  field_obj = ID_MAP[id].new(binstr[i, len + 4])
@@ -26,7 +26,7 @@ module Zip
26
26
  end
27
27
 
28
28
  def create_unknown_item
29
- s = ''
29
+ s = ''.dup
30
30
  class << s
31
31
  alias_method :to_c_dir_bin, :to_s
32
32
  alias_method :to_local_bin, :to_s
@@ -1,7 +1,7 @@
1
1
  module Zip
2
2
  class ExtraField::Generic
3
3
  def self.register_map
4
- if self.const_defined?(:HEADER_ID)
4
+ if const_defined?(:HEADER_ID)
5
5
  ::Zip::ExtraField::ID_MAP[const_get(:HEADER_ID)] = self
6
6
  end
7
7
  end
@@ -6,8 +6,7 @@ module Zip
6
6
  HEADER_ID = ['9999'].pack('H*') # this ID is used by other libraries such as .NET's Ionic.zip
7
7
  register_map
8
8
 
9
- def initialize(_binstr = nil)
10
- end
9
+ def initialize(_binstr = nil); end
11
10
 
12
11
  def pack_for_local
13
12
  "\x00" * 16
@@ -43,7 +43,7 @@ module Zip
43
43
  # interface for accessing the filesystem, ie. the File and Dir classes.
44
44
 
45
45
  class File < CentralDirectory
46
- CREATE = 1
46
+ CREATE = true
47
47
  SPLIT_SIGNATURE = 0x08074b50
48
48
  ZIP64_EOCD_SIGNATURE = 0x06064b50
49
49
  MAX_SEGMENT_SIZE = 3_221_225_472
@@ -64,26 +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 = nil, 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
- @create = create
72
- case
73
- when !buffer && ::File.size?(file_name)
74
- @create = nil
75
- @file_permissions = ::File.stat(file_name).mode
76
- ::File.open(name, 'rb') do |f|
77
- read_from_stream(f)
71
+ @create = create ? true : false # allow any truthy value to mean true
72
+
73
+ if ::File.size?(@name.to_s)
74
+ # There is a file, which exists, that is associated with this zip.
75
+ @create = false
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
80
- @file_permissions = create_file_permissions
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.
81
90
  @entry_set = EntrySet.new
82
- when ::File.zero?(file_name)
83
- 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?"
84
94
  else
85
- raise Error, "File #{file_name} not found"
95
+ # Everything is wrong.
96
+ raise Error, "File #{@name} not found"
86
97
  end
98
+
87
99
  @stored_entries = @entry_set.dup
88
100
  @stored_comment = @comment
89
101
  @restore_ownership = options[:restore_ownership] || false
@@ -95,7 +107,7 @@ module Zip
95
107
  # Same as #new. If a block is passed the ZipFile object is passed
96
108
  # to the block and is automatically closed afterwards just as with
97
109
  # ruby's builtin File.open method.
98
- def open(file_name, create = nil)
110
+ def open(file_name, create = false)
99
111
  zf = ::Zip::File.new(file_name, create)
100
112
  return zf unless block_given?
101
113
  begin
@@ -121,16 +133,16 @@ module Zip
121
133
  unless IO_METHODS.map { |method| io.respond_to?(method) }.all? || io.is_a?(String)
122
134
  raise "Zip::File.open_buffer expects a String or IO-like argument (responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
123
135
  end
124
- if io.is_a?(::String)
125
- require 'stringio'
126
- io = ::StringIO.new(io)
127
- elsif io.respond_to?(:binmode)
128
- # https://github.com/rubyzip/rubyzip/issues/119
129
- io.binmode
130
- 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
+
131
142
  zf = ::Zip::File.new(io, true, true, options)
132
- zf.read_from_stream(io)
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|
@@ -340,7 +360,7 @@ module Zip
340
360
  @entry_set.each do |e|
341
361
  return true if e.dirty
342
362
  end
343
- @comment != @stored_comment || @entry_set != @stored_entries || @create == ::Zip::File::CREATE
363
+ @comment != @stored_comment || @entry_set != @stored_entries || @create
344
364
  end
345
365
 
346
366
  # Searches for entry with the specified name. Returns nil if
@@ -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?('/')
@@ -403,27 +423,18 @@ module Zip
403
423
  end
404
424
 
405
425
  def on_success_replace
406
- tmp_filename = create_tmpname
407
- if yield tmp_filename
408
- ::File.rename(tmp_filename, name)
409
- ::File.chmod(@file_permissions, name) if defined?(@file_permissions)
410
- end
411
- ensure
412
- ::File.unlink(tmp_filename) if ::File.exist?(tmp_filename)
413
- end
414
-
415
- def create_tmpname
416
426
  dirname, basename = ::File.split(name)
417
- ::Dir::Tmpname.create(basename, dirname) do |tmpname|
418
- opts = {perm: 0600, mode: ::File::CREAT | ::File::WRONLY | ::File::EXCL}
419
- f = File.open(tmpname, opts)
420
- f.close
427
+ ::Dir::Tmpname.create(basename, dirname) do |tmp_filename|
428
+ begin
429
+ if yield tmp_filename
430
+ ::File.rename(tmp_filename, name)
431
+ ::File.chmod(@file_permissions, name) unless @create
432
+ end
433
+ ensure
434
+ ::File.unlink(tmp_filename) if ::File.exist?(tmp_filename)
435
+ end
421
436
  end
422
437
  end
423
-
424
- def create_file_permissions
425
- ::Zip::RUNNING_ON_WINDOWS ? 0644 : 0666 - ::File.umask
426
- end
427
438
  end
428
439
  end
429
440
 
@@ -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
 
@@ -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,17 +15,17 @@ 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)
22
22
  self << case an_object
23
- when Fixnum
23
+ when Integer
24
24
  an_object.chr
25
25
  when String
26
26
  an_object
27
27
  else
28
- raise TypeError, 'putc: Only Fixnum and String supported'
28
+ raise TypeError, 'putc: Only Integer and String supported'
29
29
  end
30
30
  an_object
31
31
  end