rubyzip 2.4.1 → 3.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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
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
@@ -59,18 +61,18 @@ module Zip
59
61
  end
60
62
 
61
63
  def glob(pattern, flags = ::File::FNM_PATHNAME | ::File::FNM_DOTMATCH | ::File::FNM_EXTGLOB)
62
- entries.map do |entry|
64
+ entries.filter_map do |entry|
63
65
  next nil unless ::File.fnmatch(pattern, entry.name.chomp('/'), flags)
64
66
 
65
67
  yield(entry) if block_given?
66
68
  entry
67
- end.compact
69
+ end
68
70
  end
69
71
 
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,139 @@
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
+ # The compression method that has caused this error.
11
+ attr_reader :compression_method
12
+
13
+ # Create a new CompressionMethodError with the specified incorrect
14
+ # compression method.
15
+ def initialize(method)
16
+ super()
17
+ @compression_method = method
18
+ end
19
+
20
+ # The message returned by this error.
21
+ def message
22
+ "Unsupported compression method: #{COMPRESSION_METHODS[@compression_method]}."
23
+ end
24
+ end
25
+
26
+ # Error raised if there is a problem while decompressing an archive entry.
27
+ class DecompressionError < Error
28
+ # The error from the underlying Zlib library that caused this error.
29
+ attr_reader :zlib_error
30
+
31
+ # Create a new DecompressionError with the specified underlying Zlib
32
+ # error.
33
+ def initialize(zlib_error)
34
+ super()
35
+ @zlib_error = zlib_error
36
+ end
37
+
38
+ # The message returned by this error.
39
+ def message
40
+ "Zlib error ('#{@zlib_error.message}') while inflating."
41
+ end
42
+ end
43
+
44
+ # Error raised when trying to extract an archive entry over an
45
+ # existing file.
46
+ class DestinationExistsError < Error
47
+ # Create a new DestinationExistsError with the clashing destination.
48
+ def initialize(destination)
49
+ super()
50
+ @destination = destination
51
+ end
52
+
53
+ # The message returned by this error.
54
+ def message
55
+ "Cannot create file or directory '#{@destination}'. " \
56
+ 'A file already exists with that name.'
57
+ end
58
+ end
59
+
60
+ # Error raised when trying to add an entry to an archive where the
61
+ # entry name already exists.
62
+ class EntryExistsError < Error
63
+ # Create a new EntryExistsError with the specified source and name.
64
+ def initialize(source, name)
65
+ super()
66
+ @source = source
67
+ @name = name
68
+ end
69
+
70
+ # The message returned by this error.
71
+ def message
72
+ "'#{@source}' failed. Entry #{@name} already exists."
73
+ end
74
+ end
75
+
76
+ # Error raised when an entry name is invalid.
77
+ class EntryNameError < Error
78
+ # Create a new EntryNameError with the specified name.
79
+ def initialize(name = nil)
80
+ super()
81
+ @name = name
82
+ end
83
+
84
+ # The message returned by this error.
85
+ def message
86
+ if @name.nil?
87
+ 'Illegal entry name. Names must have fewer than 65,536 characters.'
88
+ else
89
+ "Illegal entry name '#{@name}'. Names must not start with '/'."
90
+ end
91
+ end
92
+ end
93
+
94
+ # Error raised if an entry is larger on extraction than it is advertised
95
+ # to be.
96
+ class EntrySizeError < Error
97
+ # The entry that has caused this error.
98
+ attr_reader :entry
99
+
100
+ # Create a new EntrySizeError with the specified entry.
101
+ def initialize(entry)
102
+ super()
103
+ @entry = entry
104
+ end
105
+
106
+ # The message returned by this error.
107
+ def message
108
+ "Entry '#{@entry.name}' should be #{@entry.size}B, but is larger when inflated."
109
+ end
110
+ end
111
+
112
+ # Error raised if a split archive is read. Rubyzip does not support reading
113
+ # split archives.
114
+ class SplitArchiveError < Error
115
+ # The message returned by this error.
116
+ def message
117
+ 'Rubyzip cannot extract from split archives at this time.'
118
+ end
119
+ end
120
+
121
+ # Error raised if there is not enough metadata for the entry to be streamed.
122
+ class StreamingError < Error
123
+ # The entry that has caused this error.
124
+ attr_reader :entry
125
+
126
+ # Create a new StreamingError with the specified entry.
127
+ def initialize(entry)
128
+ super()
129
+ @entry = entry
130
+ end
131
+
132
+ # The message returned by this error.
133
+ def message
134
+ "The local header of this entry ('#{@entry.name}') does not contain " \
135
+ 'the correct metadata for `Zip::InputStream` to be able to ' \
136
+ 'uncompress it. Please use `Zip::File` instead of `Zip::InputStream`.'
137
+ end
138
+ end
19
139
  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
 
@@ -19,26 +21,17 @@ module Zip
19
21
  return false
20
22
  end
21
23
 
22
- [binstr[2, 2].unpack1('v'), binstr[4..-1]]
23
- end
24
-
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
24
+ [binstr[2, 2].unpack1('v'), binstr[4..]]
32
25
  end
33
26
 
34
27
  def to_local_bin
35
28
  s = pack_for_local
36
- self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v') << s
29
+ (self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v')) << s
37
30
  end
38
31
 
39
32
  def to_c_dir_bin
40
33
  s = pack_for_c_dir
41
- self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v') << s
34
+ (self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v')) << s
42
35
  end
43
36
  end
44
37
  end
@@ -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
 
@@ -23,7 +25,7 @@ module Zip
23
25
  size, content = initial_parse(binstr)
24
26
  (size && content) || return
25
27
 
26
- content = content[4..-1]
28
+ content = content[4..]
27
29
  tags = parse_tags(content)
28
30
 
29
31
  tag1 = tags[1]
@@ -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
@@ -84,7 +86,7 @@ module Zip
84
86
  end
85
87
 
86
88
  def from_ntfs_time(ntfs_time)
87
- ::Zip::DOSTime.at(ntfs_time / WINDOWS_TICK - SEC_TO_UNIX_EPOCH)
89
+ ::Zip::DOSTime.at((ntfs_time / WINDOWS_TICK) - SEC_TO_UNIX_EPOCH)
88
90
  end
89
91
 
90
92
  def to_ntfs_time(time)
@@ -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
- if !len || len + 4 > binstr[index..-1].bytesize
22
- self['Unknown'] << binstr[index..-1]
21
+ def extra_field_type_unknown(binstr, len, index, local)
22
+ self['Unknown'] ||= Unknown.new
23
+
24
+ if !len || len + 4 > binstr[index..].bytesize
25
+ self['Unknown'].merge(binstr[index..], 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