rubyzip 2.4.1 → 3.0.0.alpha

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +368 -0
  3. data/README.md +112 -37
  4. data/Rakefile +11 -7
  5. data/lib/zip/central_directory.rb +164 -118
  6. data/lib/zip/compressor.rb +3 -1
  7. data/lib/zip/constants.rb +25 -21
  8. data/lib/zip/crypto/decrypted_io.rb +3 -1
  9. data/lib/zip/crypto/encryption.rb +4 -2
  10. data/lib/zip/crypto/null_encryption.rb +5 -3
  11. data/lib/zip/crypto/traditional_encryption.rb +5 -3
  12. data/lib/zip/decompressor.rb +4 -3
  13. data/lib/zip/deflater.rb +10 -8
  14. data/lib/zip/dirtyable.rb +32 -0
  15. data/lib/zip/dos_time.rb +32 -3
  16. data/lib/zip/entry.rb +262 -198
  17. data/lib/zip/entry_set.rb +9 -7
  18. data/lib/zip/errors.rb +115 -16
  19. data/lib/zip/extra_field/generic.rb +3 -10
  20. data/lib/zip/extra_field/ntfs.rb +4 -2
  21. data/lib/zip/extra_field/old_unix.rb +3 -1
  22. data/lib/zip/extra_field/universal_time.rb +3 -1
  23. data/lib/zip/extra_field/unix.rb +5 -3
  24. data/lib/zip/extra_field/unknown.rb +33 -0
  25. data/lib/zip/extra_field/zip64.rb +12 -5
  26. data/lib/zip/extra_field.rb +15 -21
  27. data/lib/zip/file.rb +144 -265
  28. data/lib/zip/file_split.rb +97 -0
  29. data/lib/zip/filesystem/dir.rb +86 -0
  30. data/lib/zip/filesystem/directory_iterator.rb +48 -0
  31. data/lib/zip/filesystem/file.rb +262 -0
  32. data/lib/zip/filesystem/file_stat.rb +110 -0
  33. data/lib/zip/filesystem/zip_file_name_mapper.rb +81 -0
  34. data/lib/zip/filesystem.rb +26 -595
  35. data/lib/zip/inflater.rb +7 -5
  36. data/lib/zip/input_stream.rb +44 -39
  37. data/lib/zip/ioextras/abstract_input_stream.rb +14 -9
  38. data/lib/zip/ioextras/abstract_output_stream.rb +5 -3
  39. data/lib/zip/ioextras.rb +6 -6
  40. data/lib/zip/null_compressor.rb +3 -1
  41. data/lib/zip/null_decompressor.rb +3 -1
  42. data/lib/zip/null_input_stream.rb +3 -1
  43. data/lib/zip/output_stream.rb +47 -48
  44. data/lib/zip/pass_thru_compressor.rb +3 -1
  45. data/lib/zip/pass_thru_decompressor.rb +4 -2
  46. data/lib/zip/streamable_directory.rb +3 -1
  47. data/lib/zip/streamable_stream.rb +3 -0
  48. data/lib/zip/version.rb +3 -1
  49. data/lib/zip.rb +15 -20
  50. data/rubyzip.gemspec +38 -0
  51. data/samples/example.rb +8 -3
  52. data/samples/example_filesystem.rb +2 -1
  53. data/samples/example_recursive.rb +3 -1
  54. data/samples/gtk_ruby_zip.rb +4 -2
  55. data/samples/qtzip.rb +6 -5
  56. data/samples/write_simple.rb +1 -0
  57. data/samples/zipfind.rb +1 -0
  58. metadata +84 -50
  59. data/TODO +0 -15
  60. data/lib/zip/extra_field/zip64_placeholder.rb +0 -15
data/lib/zip/entry_set.rb CHANGED
@@ -1,7 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
2
- class EntrySet #:nodoc:all
4
+ class EntrySet # :nodoc:all
3
5
  include Enumerable
4
- attr_accessor :entry_set, :entry_order
6
+
7
+ attr_reader :entry_set
8
+ protected :entry_set
5
9
 
6
10
  def initialize(an_enumerable = [])
7
11
  super()
@@ -33,10 +37,8 @@ module Zip
33
37
  entry if @entry_set.delete(to_key(entry))
34
38
  end
35
39
 
36
- def each
37
- @entry_set = sorted_entries.dup.each do |_, value|
38
- yield(value)
39
- end
40
+ def each(&block)
41
+ entries.each(&block)
40
42
  end
41
43
 
42
44
  def entries
@@ -70,7 +72,7 @@ module Zip
70
72
  protected
71
73
 
72
74
  def sorted_entries
73
- ::Zip.sort_entries ? Hash[@entry_set.sort] : @entry_set
75
+ ::Zip.sort_entries ? @entry_set.sort.to_h : @entry_set
74
76
  end
75
77
 
76
78
  private
data/lib/zip/errors.rb CHANGED
@@ -1,19 +1,118 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
4
+ # The superclass for all rubyzip error types. Simply rescue this one if
5
+ # you don't need to know what sort of error has been raised.
2
6
  class Error < StandardError; end
3
- class EntryExistsError < Error; end
4
- class DestinationFileExistsError < Error; end
5
- class CompressionMethodError < Error; end
6
- class EntryNameError < Error; end
7
- class EntrySizeError < Error; end
8
- class InternalError < Error; end
9
- class GPFBit3Error < Error; end
10
- class DecompressionError < Error; end
11
-
12
- # Backwards compatibility with v1 (delete in v2)
13
- ZipError = Error
14
- ZipEntryExistsError = EntryExistsError
15
- ZipDestinationFileExistsError = DestinationFileExistsError
16
- ZipCompressionMethodError = CompressionMethodError
17
- ZipEntryNameError = EntryNameError
18
- ZipInternalError = InternalError
7
+
8
+ # Error raised if an unsupported compression method is used.
9
+ class CompressionMethodError < Error
10
+ attr_reader :compression_method
11
+
12
+ def initialize(method)
13
+ super()
14
+ @compression_method = method
15
+ end
16
+
17
+ def message
18
+ "Unsupported compression method: #{COMPRESSION_METHODS[@compression_method]}."
19
+ end
20
+ end
21
+
22
+ # Error raised if there is a problem while decompressing an archive entry.
23
+ class DecompressionError < Error
24
+ attr_reader :zlib_error
25
+
26
+ def initialize(zlib_error)
27
+ super()
28
+ @zlib_error = zlib_error
29
+ end
30
+
31
+ def message
32
+ "Zlib error ('#{@zlib_error.message}') while inflating."
33
+ end
34
+ end
35
+
36
+ # Error raised when trying to extract an archive entry over an
37
+ # existing file.
38
+ class DestinationExistsError < Error
39
+ def initialize(destination)
40
+ super()
41
+ @destination = destination
42
+ end
43
+
44
+ def message
45
+ "Cannot create file or directory '#{@destination}'. " \
46
+ 'A file already exists with that name.'
47
+ end
48
+ end
49
+
50
+ # Error raised when trying to add an entry to an archive where the
51
+ # entry name already exists.
52
+ class EntryExistsError < Error
53
+ def initialize(source, name)
54
+ super()
55
+ @source = source
56
+ @name = name
57
+ end
58
+
59
+ def message
60
+ "'#{@source}' failed. Entry #{@name} already exists."
61
+ end
62
+ end
63
+
64
+ # Error raised when an entry name is invalid.
65
+ class EntryNameError < Error
66
+ def initialize(name = nil)
67
+ super()
68
+ @name = name
69
+ end
70
+
71
+ def message
72
+ if @name.nil?
73
+ 'Illegal entry name. Names must have fewer than 65,536 characters.'
74
+ else
75
+ "Illegal entry name '#{@name}'. Names must not start with '/'."
76
+ end
77
+ end
78
+ end
79
+
80
+ # Error raised if an entry is larger on extraction than it is advertised
81
+ # to be.
82
+ class EntrySizeError < Error
83
+ attr_reader :entry
84
+
85
+ def initialize(entry)
86
+ super()
87
+ @entry = entry
88
+ end
89
+
90
+ def message
91
+ "Entry '#{@entry.name}' should be #{@entry.size}B, but is larger when inflated."
92
+ end
93
+ end
94
+
95
+ # Error raised if a split archive is read. Rubyzip does not support reading
96
+ # split archives.
97
+ class SplitArchiveError < Error
98
+ def message
99
+ 'Rubyzip cannot extract from split archives at this time.'
100
+ end
101
+ end
102
+
103
+ # Error raised if there is not enough metadata for the entry to be streamed.
104
+ class StreamingError < Error
105
+ attr_reader :entry
106
+
107
+ def initialize(entry)
108
+ super()
109
+ @entry = entry
110
+ end
111
+
112
+ def message
113
+ "The local header of this entry ('#{@entry.name}') does not contain " \
114
+ 'the correct metadata for `Zip::InputStream` to be able to ' \
115
+ 'uncompress it. Please use `Zip::File` instead of `Zip::InputStream`.'
116
+ end
117
+ end
19
118
  end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
2
- class ExtraField::Generic
4
+ class ExtraField::Generic # :nodoc:
3
5
  def self.register_map
4
6
  return unless const_defined?(:HEADER_ID)
5
7
 
@@ -22,15 +24,6 @@ module Zip
22
24
  [binstr[2, 2].unpack1('v'), binstr[4..-1]]
23
25
  end
24
26
 
25
- def ==(other)
26
- return false if self.class != other.class
27
-
28
- each do |k, v|
29
- return false if v != other[k]
30
- end
31
- true
32
- end
33
-
34
27
  def to_local_bin
35
28
  s = pack_for_local
36
29
  self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v') << s
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
2
4
  # PKWARE NTFS Extra Field (0x000a)
3
5
  # Only Tag 0x0001 is supported
4
- class ExtraField::NTFS < ExtraField::Generic
6
+ class ExtraField::NTFS < ExtraField::Generic # :nodoc:
5
7
  HEADER_ID = [0x000A].pack('v')
6
8
  register_map
7
9
 
@@ -51,7 +53,7 @@ module Zip
51
53
  # reserved 0 and tag 1
52
54
  s = [0, 1].pack('Vv')
53
55
 
54
- tag1 = ''.b
56
+ tag1 = (+'').force_encoding(Encoding::BINARY)
55
57
  if @mtime
56
58
  tag1 << [to_ntfs_time(@mtime)].pack('Q<')
57
59
  if @atime
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
2
4
  # Olf Info-ZIP Extra for UNIX uid/gid and file timestampes
3
- class ExtraField::OldUnix < ExtraField::Generic
5
+ class ExtraField::OldUnix < ExtraField::Generic # :nodoc:
4
6
  HEADER_ID = 'UX'
5
7
  register_map
6
8
 
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
2
4
  # Info-ZIP Additional timestamp field
3
- class ExtraField::UniversalTime < ExtraField::Generic
5
+ class ExtraField::UniversalTime < ExtraField::Generic # :nodoc:
4
6
  HEADER_ID = 'UT'
5
7
  register_map
6
8
 
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
2
4
  # Info-ZIP Extra for UNIX uid/gid
3
- class ExtraField::IUnix < ExtraField::Generic
5
+ class ExtraField::IUnix < ExtraField::Generic # :nodoc:
4
6
  HEADER_ID = 'Ux'
5
7
  register_map
6
8
 
@@ -20,8 +22,8 @@ module Zip
20
22
  return if !size || size == 0
21
23
 
22
24
  uid, gid = content.unpack('vv')
23
- @uid ||= uid
24
- @gid ||= gid # rubocop:disable Naming/MemoizedInstanceVariableName
25
+ @uid = uid
26
+ @gid = gid
25
27
  end
26
28
 
27
29
  def ==(other)
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zip
4
+ # A class to hold unknown extra fields so that they are preserved.
5
+ class ExtraField::Unknown # :nodoc:
6
+ def initialize
7
+ @local_bin = +''
8
+ @cdir_bin = +''
9
+ end
10
+
11
+ def merge(binstr, local: false)
12
+ return if binstr.empty?
13
+
14
+ if local
15
+ @local_bin << binstr
16
+ else
17
+ @cdir_bin << binstr
18
+ end
19
+ end
20
+
21
+ def to_local_bin
22
+ @local_bin
23
+ end
24
+
25
+ def to_c_dir_bin
26
+ @cdir_bin
27
+ end
28
+
29
+ def ==(other)
30
+ @local_bin == other.to_local_bin && @cdir_bin == other.to_c_dir_bin
31
+ end
32
+ end
33
+ end
@@ -1,7 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
2
4
  # Info-ZIP Extra for Zip64 size
3
- class ExtraField::Zip64 < ExtraField::Generic
4
- attr_accessor :original_size, :compressed_size, :relative_header_offset, :disk_start_number
5
+ class ExtraField::Zip64 < ExtraField::Generic # :nodoc:
6
+ attr_accessor :compressed_size, :disk_start_number,
7
+ :original_size, :relative_header_offset
8
+
5
9
  HEADER_ID = ['0100'].pack('H*')
6
10
  register_map
7
11
 
@@ -36,7 +40,9 @@ module Zip
36
40
  def parse(original_size, compressed_size, relative_header_offset = nil, disk_start_number = nil)
37
41
  @original_size = extract(8, 'Q<') if original_size == 0xFFFFFFFF
38
42
  @compressed_size = extract(8, 'Q<') if compressed_size == 0xFFFFFFFF
39
- @relative_header_offset = extract(8, 'Q<') if relative_header_offset && relative_header_offset == 0xFFFFFFFF
43
+ if relative_header_offset && relative_header_offset == 0xFFFFFFFF
44
+ @relative_header_offset = extract(8, 'Q<')
45
+ end
40
46
  @disk_start_number = extract(4, 'V') if disk_start_number && disk_start_number == 0xFFFF
41
47
  @content = nil
42
48
  [@original_size || original_size,
@@ -51,7 +57,8 @@ module Zip
51
57
  private :extract
52
58
 
53
59
  def pack_for_local
54
- # local header entries must contain original size and compressed size; other fields do not apply
60
+ # Local header entries must contain original size and compressed size;
61
+ # other fields do not apply.
55
62
  return '' unless @original_size && @compressed_size
56
63
 
57
64
  [@original_size, @compressed_size].pack('Q<Q<')
@@ -59,7 +66,7 @@ module Zip
59
66
 
60
67
  def pack_for_c_dir
61
68
  # central directory entries contain only fields that didn't fit in the main entry part
62
- packed = ''.b
69
+ packed = (+'').force_encoding('BINARY')
63
70
  packed << [@original_size].pack('Q<') if @original_size
64
71
  packed << [@compressed_size].pack('Q<') if @compressed_size
65
72
  packed << [@relative_header_offset].pack('Q<') if @relative_header_offset
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
2
- class ExtraField < Hash
4
+ class ExtraField < Hash # :nodoc:all
3
5
  ID_MAP = {}
4
6
 
5
- def initialize(binstr = nil)
6
- merge(binstr) if binstr
7
+ def initialize(binstr = nil, local: false)
8
+ merge(binstr, local: local) if binstr
7
9
  end
8
10
 
9
11
  def extra_field_type_exist(binstr, id, len, index)
@@ -16,25 +18,18 @@ module Zip
16
18
  end
17
19
  end
18
20
 
19
- def extra_field_type_unknown(binstr, len, index)
20
- create_unknown_item unless self['Unknown']
21
+ def extra_field_type_unknown(binstr, len, index, local)
22
+ self['Unknown'] ||= Unknown.new
23
+
21
24
  if !len || len + 4 > binstr[index..-1].bytesize
22
- self['Unknown'] << binstr[index..-1]
25
+ self['Unknown'].merge(binstr[index..-1], local: local)
23
26
  return
24
27
  end
25
- self['Unknown'] << binstr[index, len + 4]
26
- end
27
28
 
28
- def create_unknown_item
29
- s = +''
30
- class << s
31
- alias_method :to_c_dir_bin, :to_s
32
- alias_method :to_local_bin, :to_s
33
- end
34
- self['Unknown'] = s
29
+ self['Unknown'].merge(binstr[index, len + 4], local: local)
35
30
  end
36
31
 
37
- def merge(binstr)
32
+ def merge(binstr, local: false)
38
33
  return if binstr.empty?
39
34
 
40
35
  i = 0
@@ -44,8 +39,7 @@ module Zip
44
39
  if id && ID_MAP.member?(id)
45
40
  extra_field_type_exist(binstr, id, len, i)
46
41
  elsif id
47
- create_unknown_item unless self['Unknown']
48
- break unless extra_field_type_unknown(binstr, len, i)
42
+ break unless extra_field_type_unknown(binstr, len, i, local)
49
43
  end
50
44
  i += len + 4
51
45
  end
@@ -59,8 +53,8 @@ module Zip
59
53
  self[name] = field_class.new
60
54
  end
61
55
 
62
- # place Unknown last, so "extra" data that is missing the proper signature/size
63
- # does not prevent known fields from being read back in
56
+ # Place Unknown last, so "extra" data that is missing the proper
57
+ # signature/size does not prevent known fields from being read back in.
64
58
  def ordered_values
65
59
  result = []
66
60
  each { |k, v| k == 'Unknown' ? result.push(v) : result.unshift(v) }
@@ -90,12 +84,12 @@ module Zip
90
84
  end
91
85
  end
92
86
 
87
+ require 'zip/extra_field/unknown'
93
88
  require 'zip/extra_field/generic'
94
89
  require 'zip/extra_field/universal_time'
95
90
  require 'zip/extra_field/old_unix'
96
91
  require 'zip/extra_field/unix'
97
92
  require 'zip/extra_field/zip64'
98
- require 'zip/extra_field/zip64_placeholder'
99
93
  require 'zip/extra_field/ntfs'
100
94
 
101
95
  # Copyright (C) 2002, 2003 Thomas Sondergaard