rubyzip 2.4.1 → 3.0.0.rc1

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +419 -0
  3. data/LICENSE.md +24 -0
  4. data/README.md +137 -37
  5. data/Rakefile +11 -7
  6. data/lib/zip/central_directory.rb +169 -123
  7. data/lib/zip/compressor.rb +3 -1
  8. data/lib/zip/constants.rb +29 -21
  9. data/lib/zip/crypto/decrypted_io.rb +4 -2
  10. data/lib/zip/crypto/encryption.rb +4 -2
  11. data/lib/zip/crypto/null_encryption.rb +6 -4
  12. data/lib/zip/crypto/traditional_encryption.rb +8 -6
  13. data/lib/zip/decompressor.rb +4 -3
  14. data/lib/zip/deflater.rb +10 -8
  15. data/lib/zip/dirtyable.rb +32 -0
  16. data/lib/zip/dos_time.rb +43 -4
  17. data/lib/zip/entry.rb +333 -242
  18. data/lib/zip/entry_set.rb +11 -9
  19. data/lib/zip/errors.rb +136 -16
  20. data/lib/zip/extra_field/generic.rb +6 -13
  21. data/lib/zip/extra_field/ntfs.rb +6 -4
  22. data/lib/zip/extra_field/old_unix.rb +3 -1
  23. data/lib/zip/extra_field/universal_time.rb +3 -1
  24. data/lib/zip/extra_field/unix.rb +5 -3
  25. data/lib/zip/extra_field/unknown.rb +33 -0
  26. data/lib/zip/extra_field/zip64.rb +12 -5
  27. data/lib/zip/extra_field.rb +16 -22
  28. data/lib/zip/file.rb +166 -264
  29. data/lib/zip/file_split.rb +91 -0
  30. data/lib/zip/filesystem/dir.rb +86 -0
  31. data/lib/zip/filesystem/directory_iterator.rb +48 -0
  32. data/lib/zip/filesystem/file.rb +262 -0
  33. data/lib/zip/filesystem/file_stat.rb +110 -0
  34. data/lib/zip/filesystem/zip_file_name_mapper.rb +81 -0
  35. data/lib/zip/filesystem.rb +27 -596
  36. data/lib/zip/inflater.rb +7 -5
  37. data/lib/zip/input_stream.rb +50 -50
  38. data/lib/zip/ioextras/abstract_input_stream.rb +16 -11
  39. data/lib/zip/ioextras/abstract_output_stream.rb +5 -3
  40. data/lib/zip/ioextras.rb +7 -7
  41. data/lib/zip/null_compressor.rb +3 -1
  42. data/lib/zip/null_decompressor.rb +3 -1
  43. data/lib/zip/null_input_stream.rb +3 -1
  44. data/lib/zip/output_stream.rb +55 -56
  45. data/lib/zip/pass_thru_compressor.rb +3 -1
  46. data/lib/zip/pass_thru_decompressor.rb +4 -2
  47. data/lib/zip/streamable_directory.rb +3 -1
  48. data/lib/zip/streamable_stream.rb +3 -0
  49. data/lib/zip/version.rb +3 -1
  50. data/lib/zip.rb +18 -22
  51. data/rubyzip.gemspec +39 -0
  52. data/samples/example.rb +8 -3
  53. data/samples/example_filesystem.rb +3 -2
  54. data/samples/example_recursive.rb +3 -1
  55. data/samples/gtk_ruby_zip.rb +4 -2
  56. data/samples/qtzip.rb +6 -5
  57. data/samples/write_simple.rb +2 -1
  58. data/samples/zipfind.rb +1 -0
  59. metadata +87 -51
  60. data/TODO +0 -15
  61. data/lib/zip/extra_field/zip64_placeholder.rb +0 -15
@@ -1,4 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'zip'
4
+ require_relative 'filesystem/zip_file_name_mapper'
5
+ require_relative 'filesystem/directory_iterator'
6
+ require_relative 'filesystem/dir'
7
+ require_relative 'filesystem/file'
2
8
 
3
9
  module Zip
4
10
  # The ZipFileSystem API provides an API for accessing entries in
@@ -13,627 +19,52 @@ module Zip
13
19
  # <code>first.txt</code>, a directory entry named <code>mydir</code>
14
20
  # and finally another normal entry named <code>second.txt</code>
15
21
  #
16
- # require 'zip/filesystem'
22
+ # ```
23
+ # require 'zip/filesystem'
17
24
  #
18
- # Zip::File.open("my.zip", Zip::File::CREATE) {
19
- # |zipfile|
20
- # zipfile.file.open("first.txt", "w") { |f| f.puts "Hello world" }
21
- # zipfile.dir.mkdir("mydir")
22
- # zipfile.file.open("mydir/second.txt", "w") { |f| f.puts "Hello again" }
23
- # }
25
+ # Zip::File.open('my.zip', create: true) do |zipfile|
26
+ # zipfile.file.open('first.txt', 'w') { |f| f.puts 'Hello world' }
27
+ # zipfile.dir.mkdir('mydir')
28
+ # zipfile.file.open('mydir/second.txt', 'w') { |f| f.puts 'Hello again' }
29
+ # end
30
+ # ```
24
31
  #
25
32
  # Reading is as easy as writing, as the following example shows. The
26
33
  # example writes the contents of <code>first.txt</code> from zip archive
27
34
  # <code>my.zip</code> to standard out.
28
35
  #
29
- # require 'zip/filesystem'
36
+ # ```
37
+ # require 'zip/filesystem'
30
38
  #
31
- # Zip::File.open("my.zip") {
32
- # |zipfile|
33
- # puts zipfile.file.read("first.txt")
34
- # }
35
-
39
+ # Zip::File.open('my.zip') do |zipfile|
40
+ # puts zipfile.file.read('first.txt')
41
+ # end
42
+ # ```
36
43
  module FileSystem
37
44
  def initialize # :nodoc:
38
45
  mapped_zip = ZipFileNameMapper.new(self)
39
- @zip_fs_dir = ZipFsDir.new(mapped_zip)
40
- @zip_fs_file = ZipFsFile.new(mapped_zip)
46
+ @zip_fs_dir = Dir.new(mapped_zip)
47
+ @zip_fs_file = File.new(mapped_zip)
41
48
  @zip_fs_dir.file = @zip_fs_file
42
49
  @zip_fs_file.dir = @zip_fs_dir
43
50
  end
44
51
 
45
- # Returns a ZipFsDir which is much like ruby's builtin Dir (class)
46
- # object, except it works on the Zip::File on which this method is
52
+ # Returns a Zip::FileSystem::Dir which is much like ruby's builtin Dir
53
+ # (class) object, except it works on the Zip::File on which this method is
47
54
  # invoked
48
55
  def dir
49
56
  @zip_fs_dir
50
57
  end
51
58
 
52
- # Returns a ZipFsFile which is much like ruby's builtin File (class)
53
- # object, except it works on the Zip::File on which this method is
59
+ # Returns a Zip::FileSystem::File which is much like ruby's builtin File
60
+ # (class) object, except it works on the Zip::File on which this method is
54
61
  # invoked
55
62
  def file
56
63
  @zip_fs_file
57
64
  end
58
-
59
- # Instances of this class are normally accessed via the accessor
60
- # Zip::File::file. An instance of ZipFsFile behaves like ruby's
61
- # builtin File (class) object, except it works on Zip::File entries.
62
- #
63
- # The individual methods are not documented due to their
64
- # similarity with the methods in File
65
- class ZipFsFile
66
- attr_writer :dir
67
- # protected :dir
68
-
69
- class ZipFsStat
70
- class << self
71
- def delegate_to_fs_file(*methods)
72
- methods.each do |method|
73
- class_eval <<-END_EVAL, __FILE__, __LINE__ + 1
74
- def #{method} # def file?
75
- @zip_fs_file.#{method}(@entry_name) # @zip_fs_file.file?(@entry_name)
76
- end # end
77
- END_EVAL
78
- end
79
- end
80
- end
81
-
82
- def initialize(zip_fs_file, entry_name)
83
- @zip_fs_file = zip_fs_file
84
- @entry_name = entry_name
85
- end
86
-
87
- def kind_of?(type)
88
- super || type == ::File::Stat
89
- end
90
-
91
- delegate_to_fs_file :file?, :directory?, :pipe?, :chardev?, :symlink?,
92
- :socket?, :blockdev?, :readable?, :readable_real?, :writable?, :ctime,
93
- :writable_real?, :executable?, :executable_real?, :sticky?, :owned?,
94
- :grpowned?, :setuid?, :setgid?, :zero?, :size, :size?, :mtime, :atime
95
-
96
- def blocks
97
- nil
98
- end
99
-
100
- def get_entry
101
- @zip_fs_file.__send__(:get_entry, @entry_name)
102
- end
103
- private :get_entry
104
-
105
- def gid
106
- e = get_entry
107
- if e.extra.member? 'IUnix'
108
- e.extra['IUnix'].gid || 0
109
- else
110
- 0
111
- end
112
- end
113
-
114
- def uid
115
- e = get_entry
116
- if e.extra.member? 'IUnix'
117
- e.extra['IUnix'].uid || 0
118
- else
119
- 0
120
- end
121
- end
122
-
123
- def ino
124
- 0
125
- end
126
-
127
- def dev
128
- 0
129
- end
130
-
131
- def rdev
132
- 0
133
- end
134
-
135
- def rdev_major
136
- 0
137
- end
138
-
139
- def rdev_minor
140
- 0
141
- end
142
-
143
- def ftype
144
- if file?
145
- 'file'
146
- elsif directory?
147
- 'directory'
148
- else
149
- raise StandardError, 'Unknown file type'
150
- end
151
- end
152
-
153
- def nlink
154
- 1
155
- end
156
-
157
- def blksize
158
- nil
159
- end
160
-
161
- def mode
162
- e = get_entry
163
- if e.fstype == 3
164
- e.external_file_attributes >> 16
165
- else
166
- 33_206 # 33206 is equivalent to -rw-rw-rw-
167
- end
168
- end
169
- end
170
-
171
- def initialize(mapped_zip)
172
- @mapped_zip = mapped_zip
173
- end
174
-
175
- def get_entry(filename)
176
- unless exists?(filename)
177
- raise Errno::ENOENT, "No such file or directory - #{filename}"
178
- end
179
-
180
- @mapped_zip.find_entry(filename)
181
- end
182
- private :get_entry
183
-
184
- def unix_mode_cmp(filename, mode)
185
- e = get_entry(filename)
186
- e.fstype == 3 && ((e.external_file_attributes >> 16) & mode) != 0
187
- rescue Errno::ENOENT
188
- false
189
- end
190
- private :unix_mode_cmp
191
-
192
- def exists?(filename)
193
- expand_path(filename) == '/' || !@mapped_zip.find_entry(filename).nil?
194
- end
195
- alias exist? exists?
196
-
197
- # Permissions not implemented, so if the file exists it is accessible
198
- alias owned? exists?
199
- alias grpowned? exists?
200
-
201
- def readable?(filename)
202
- unix_mode_cmp(filename, 0o444)
203
- end
204
- alias readable_real? readable?
205
-
206
- def writable?(filename)
207
- unix_mode_cmp(filename, 0o222)
208
- end
209
- alias writable_real? writable?
210
-
211
- def executable?(filename)
212
- unix_mode_cmp(filename, 0o111)
213
- end
214
- alias executable_real? executable?
215
-
216
- def setuid?(filename)
217
- unix_mode_cmp(filename, 0o4000)
218
- end
219
-
220
- def setgid?(filename)
221
- unix_mode_cmp(filename, 0o2000)
222
- end
223
-
224
- def sticky?(filename)
225
- unix_mode_cmp(filename, 0o1000)
226
- end
227
-
228
- def umask(*args)
229
- ::File.umask(*args)
230
- end
231
-
232
- def truncate(_filename, _len)
233
- raise StandardError, 'truncate not supported'
234
- end
235
-
236
- def directory?(filename)
237
- entry = @mapped_zip.find_entry(filename)
238
- expand_path(filename) == '/' || (!entry.nil? && entry.directory?)
239
- end
240
-
241
- def open(filename, mode = 'r', permissions = 0o644, &block)
242
- mode = mode.delete('b') # ignore b option
243
- case mode
244
- when 'r'
245
- @mapped_zip.get_input_stream(filename, &block)
246
- when 'w'
247
- @mapped_zip.get_output_stream(filename, permissions, &block)
248
- else
249
- raise StandardError, "openmode '#{mode} not supported" unless mode == 'r'
250
- end
251
- end
252
-
253
- def new(filename, mode = 'r')
254
- self.open(filename, mode)
255
- end
256
-
257
- def size(filename)
258
- @mapped_zip.get_entry(filename).size
259
- end
260
-
261
- # Returns nil for not found and nil for directories
262
- def size?(filename)
263
- entry = @mapped_zip.find_entry(filename)
264
- entry.nil? || entry.directory? ? nil : entry.size
265
- end
266
-
267
- def chown(owner, group, *filenames)
268
- filenames.each do |filename|
269
- e = get_entry(filename)
270
- e.extra.create('IUnix') unless e.extra.member?('IUnix')
271
- e.extra['IUnix'].uid = owner
272
- e.extra['IUnix'].gid = group
273
- end
274
- filenames.size
275
- end
276
-
277
- def chmod(mode, *filenames)
278
- filenames.each do |filename|
279
- e = get_entry(filename)
280
- e.fstype = 3 # force convertion filesystem type to unix
281
- e.unix_perms = mode
282
- e.external_file_attributes = mode << 16
283
- e.dirty = true
284
- end
285
- filenames.size
286
- end
287
-
288
- def zero?(filename)
289
- sz = size(filename)
290
- sz.nil? || sz == 0
291
- rescue Errno::ENOENT
292
- false
293
- end
294
-
295
- def file?(filename)
296
- entry = @mapped_zip.find_entry(filename)
297
- !entry.nil? && entry.file?
298
- end
299
-
300
- def dirname(filename)
301
- ::File.dirname(filename)
302
- end
303
-
304
- def basename(filename)
305
- ::File.basename(filename)
306
- end
307
-
308
- def split(filename)
309
- ::File.split(filename)
310
- end
311
-
312
- def join(*fragments)
313
- ::File.join(*fragments)
314
- end
315
-
316
- def utime(modified_time, *filenames)
317
- filenames.each do |filename|
318
- get_entry(filename).time = modified_time
319
- end
320
- end
321
-
322
- def mtime(filename)
323
- @mapped_zip.get_entry(filename).mtime
324
- end
325
-
326
- def atime(filename)
327
- e = get_entry(filename)
328
- if e.extra.member? 'UniversalTime'
329
- e.extra['UniversalTime'].atime
330
- elsif e.extra.member? 'NTFS'
331
- e.extra['NTFS'].atime
332
- end
333
- end
334
-
335
- def ctime(filename)
336
- e = get_entry(filename)
337
- if e.extra.member? 'UniversalTime'
338
- e.extra['UniversalTime'].ctime
339
- elsif e.extra.member? 'NTFS'
340
- e.extra['NTFS'].ctime
341
- end
342
- end
343
-
344
- def pipe?(_filename)
345
- false
346
- end
347
-
348
- def blockdev?(_filename)
349
- false
350
- end
351
-
352
- def chardev?(_filename)
353
- false
354
- end
355
-
356
- def symlink?(_filename)
357
- false
358
- end
359
-
360
- def socket?(_filename)
361
- false
362
- end
363
-
364
- def ftype(filename)
365
- @mapped_zip.get_entry(filename).directory? ? 'directory' : 'file'
366
- end
367
-
368
- def readlink(_filename)
369
- raise NotImplementedError, 'The readlink() function is not implemented'
370
- end
371
-
372
- def symlink(_filename, _symlink_name)
373
- raise NotImplementedError, 'The symlink() function is not implemented'
374
- end
375
-
376
- def link(_filename, _symlink_name)
377
- raise NotImplementedError, 'The link() function is not implemented'
378
- end
379
-
380
- def pipe
381
- raise NotImplementedError, 'The pipe() function is not implemented'
382
- end
383
-
384
- def stat(filename)
385
- raise Errno::ENOENT, filename unless exists?(filename)
386
-
387
- ZipFsStat.new(self, filename)
388
- end
389
-
390
- alias lstat stat
391
-
392
- def readlines(filename)
393
- self.open(filename, &:readlines)
394
- end
395
-
396
- def read(filename)
397
- @mapped_zip.read(filename)
398
- end
399
-
400
- def popen(*args, &a_proc)
401
- ::File.popen(*args, &a_proc)
402
- end
403
-
404
- def foreach(filename, sep = $INPUT_RECORD_SEPARATOR, &a_proc)
405
- self.open(filename) { |is| is.each_line(sep, &a_proc) }
406
- end
407
-
408
- def delete(*args)
409
- args.each do |filename|
410
- if directory?(filename)
411
- raise Errno::EISDIR, "Is a directory - \"#{filename}\""
412
- end
413
-
414
- @mapped_zip.remove(filename)
415
- end
416
- end
417
-
418
- def rename(file_to_rename, new_name)
419
- @mapped_zip.rename(file_to_rename, new_name) { true }
420
- end
421
-
422
- alias unlink delete
423
-
424
- def expand_path(path)
425
- @mapped_zip.expand_path(path)
426
- end
427
- end
428
-
429
- # Instances of this class are normally accessed via the accessor
430
- # ZipFile::dir. An instance of ZipFsDir behaves like ruby's
431
- # builtin Dir (class) object, except it works on ZipFile entries.
432
- #
433
- # The individual methods are not documented due to their
434
- # similarity with the methods in Dir
435
- class ZipFsDir
436
- def initialize(mapped_zip)
437
- @mapped_zip = mapped_zip
438
- end
439
-
440
- attr_writer :file
441
-
442
- def new(directory_name)
443
- ZipFsDirIterator.new(entries(directory_name))
444
- end
445
-
446
- def open(directory_name)
447
- dir_iter = new(directory_name)
448
- if block_given?
449
- begin
450
- yield(dir_iter)
451
- return nil
452
- ensure
453
- dir_iter.close
454
- end
455
- end
456
- dir_iter
457
- end
458
-
459
- def pwd
460
- @mapped_zip.pwd
461
- end
462
- alias getwd pwd
463
-
464
- def chdir(directory_name)
465
- unless @file.stat(directory_name).directory?
466
- raise Errno::EINVAL, "Invalid argument - #{directory_name}"
467
- end
468
-
469
- @mapped_zip.pwd = @file.expand_path(directory_name)
470
- end
471
-
472
- def entries(directory_name)
473
- entries = []
474
- foreach(directory_name) { |e| entries << e }
475
- entries
476
- end
477
-
478
- def glob(*args, &block)
479
- @mapped_zip.glob(*args, &block)
480
- end
481
-
482
- def foreach(directory_name)
483
- unless @file.stat(directory_name).directory?
484
- raise Errno::ENOTDIR, directory_name
485
- end
486
-
487
- path = @file.expand_path(directory_name)
488
- path << '/' unless path.end_with?('/')
489
- path = Regexp.escape(path)
490
- subdir_entry_regex = Regexp.new("^#{path}([^/]+)$")
491
- @mapped_zip.each do |filename|
492
- match = subdir_entry_regex.match(filename)
493
- yield(match[1]) unless match.nil?
494
- end
495
- end
496
-
497
- def delete(entry_name)
498
- unless @file.stat(entry_name).directory?
499
- raise Errno::EINVAL, "Invalid argument - #{entry_name}"
500
- end
501
-
502
- @mapped_zip.remove(entry_name)
503
- end
504
- alias rmdir delete
505
- alias unlink delete
506
-
507
- def mkdir(entry_name, permissions = 0o755)
508
- @mapped_zip.mkdir(entry_name, permissions)
509
- end
510
-
511
- def chroot(*_args)
512
- raise NotImplementedError, 'The chroot() function is not implemented'
513
- end
514
- end
515
-
516
- class ZipFsDirIterator # :nodoc:all
517
- include Enumerable
518
-
519
- def initialize(filenames)
520
- @filenames = filenames
521
- @index = 0
522
- end
523
-
524
- def close
525
- @filenames = nil
526
- end
527
-
528
- def each(&a_proc)
529
- raise IOError, 'closed directory' if @filenames.nil?
530
-
531
- @filenames.each(&a_proc)
532
- end
533
-
534
- def read
535
- raise IOError, 'closed directory' if @filenames.nil?
536
-
537
- @filenames[(@index += 1) - 1]
538
- end
539
-
540
- def rewind
541
- raise IOError, 'closed directory' if @filenames.nil?
542
-
543
- @index = 0
544
- end
545
-
546
- def seek(position)
547
- raise IOError, 'closed directory' if @filenames.nil?
548
-
549
- @index = position
550
- end
551
-
552
- def tell
553
- raise IOError, 'closed directory' if @filenames.nil?
554
-
555
- @index
556
- end
557
- end
558
-
559
- # All access to Zip::File from ZipFsFile and ZipFsDir goes through a
560
- # ZipFileNameMapper, which has one responsibility: ensure
561
- class ZipFileNameMapper # :nodoc:all
562
- include Enumerable
563
-
564
- def initialize(zip_file)
565
- @zip_file = zip_file
566
- @pwd = '/'
567
- end
568
-
569
- attr_accessor :pwd
570
-
571
- def find_entry(filename)
572
- @zip_file.find_entry(expand_to_entry(filename))
573
- end
574
-
575
- def get_entry(filename)
576
- @zip_file.get_entry(expand_to_entry(filename))
577
- end
578
-
579
- def get_input_stream(filename, &a_proc)
580
- @zip_file.get_input_stream(expand_to_entry(filename), &a_proc)
581
- end
582
-
583
- def get_output_stream(filename, permissions = nil, &a_proc)
584
- @zip_file.get_output_stream(
585
- expand_to_entry(filename), permissions, &a_proc
586
- )
587
- end
588
-
589
- def glob(pattern, *flags, &block)
590
- @zip_file.glob(expand_to_entry(pattern), *flags, &block)
591
- end
592
-
593
- def read(filename)
594
- @zip_file.read(expand_to_entry(filename))
595
- end
596
-
597
- def remove(filename)
598
- @zip_file.remove(expand_to_entry(filename))
599
- end
600
-
601
- def rename(filename, new_name, &continue_on_exists_proc)
602
- @zip_file.rename(
603
- expand_to_entry(filename),
604
- expand_to_entry(new_name),
605
- &continue_on_exists_proc
606
- )
607
- end
608
-
609
- def mkdir(filename, permissions = 0o755)
610
- @zip_file.mkdir(expand_to_entry(filename), permissions)
611
- end
612
-
613
- # Turns entries into strings and adds leading /
614
- # and removes trailing slash on directories
615
- def each
616
- @zip_file.each do |e|
617
- yield('/' + e.to_s.chomp('/'))
618
- end
619
- end
620
-
621
- def expand_path(path)
622
- expanded = path.start_with?('/') ? path.dup : ::File.join(@pwd, path)
623
- expanded.gsub!(/\/\.(\/|$)/, '')
624
- expanded.gsub!(/[^\/]+\/\.\.(\/|$)/, '')
625
- expanded.empty? ? '/' : expanded
626
- end
627
-
628
- private
629
-
630
- def expand_to_entry(path)
631
- expand_path(path)[1..-1]
632
- end
633
- end
634
65
  end
635
66
 
636
- class File
67
+ class File # :nodoc:
637
68
  include FileSystem
638
69
  end
639
70
  end
data/lib/zip/inflater.rb CHANGED
@@ -1,13 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
2
- class Inflater < Decompressor #:nodoc:all
4
+ class Inflater < Decompressor # :nodoc:all
3
5
  def initialize(*args)
4
6
  super
5
7
 
6
- @buffer = ''.b
8
+ @buffer = +''
7
9
  @zlib_inflater = ::Zlib::Inflate.new(-Zlib::MAX_WBITS)
8
10
  end
9
11
 
10
- def read(length = nil, outbuf = ''.b)
12
+ def read(length = nil, outbuf = +'')
11
13
  return (length.nil? || length.zero? ? '' : nil) if eof
12
14
 
13
15
  while length.nil? || (@buffer.bytesize < length)
@@ -37,8 +39,8 @@ module Zip
37
39
  retried += 1
38
40
  retry
39
41
  end
40
- rescue Zlib::Error
41
- raise(::Zip::DecompressionError, 'zlib error while inflating')
42
+ rescue Zlib::Error => e
43
+ raise ::Zip::DecompressionError, e
42
44
  end
43
45
 
44
46
  def input_finished?