rubyzip 1.3.0 → 3.4.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 (138) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +516 -0
  3. data/LICENSE.md +24 -0
  4. data/README.md +213 -51
  5. data/Rakefile +17 -12
  6. data/lib/rubyzip.rb +3 -0
  7. data/lib/zip/central_directory.rb +196 -123
  8. data/lib/zip/compressor.rb +3 -1
  9. data/lib/zip/constants.rb +81 -21
  10. data/lib/zip/crypto/aes_encryption.rb +120 -0
  11. data/lib/zip/crypto/decrypted_io.rb +46 -0
  12. data/lib/zip/crypto/encryption.rb +4 -2
  13. data/lib/zip/crypto/null_encryption.rb +5 -13
  14. data/lib/zip/crypto/traditional_encryption.rb +22 -16
  15. data/lib/zip/decompressor.rb +21 -2
  16. data/lib/zip/deflater.rb +12 -8
  17. data/lib/zip/dirtyable.rb +32 -0
  18. data/lib/zip/dos_time.rb +65 -13
  19. data/lib/zip/entry.rb +433 -249
  20. data/lib/zip/entry_set.rb +13 -9
  21. data/lib/zip/errors.rb +152 -15
  22. data/lib/zip/extra_field/aes.rb +50 -0
  23. data/lib/zip/extra_field/generic.rb +18 -18
  24. data/lib/zip/extra_field/ntfs.rb +10 -4
  25. data/lib/zip/extra_field/old_unix.rb +6 -2
  26. data/lib/zip/extra_field/universal_time.rb +45 -13
  27. data/lib/zip/extra_field/unix.rb +7 -3
  28. data/lib/zip/extra_field/unknown.rb +35 -0
  29. data/lib/zip/extra_field/zip64.rb +23 -7
  30. data/lib/zip/extra_field.rb +31 -27
  31. data/lib/zip/file.rb +225 -241
  32. data/lib/zip/file_split.rb +91 -0
  33. data/lib/zip/filesystem/dir.rb +86 -0
  34. data/lib/zip/filesystem/directory_iterator.rb +48 -0
  35. data/lib/zip/filesystem/file.rb +263 -0
  36. data/lib/zip/filesystem/file_stat.rb +110 -0
  37. data/lib/zip/filesystem/zip_file_name_mapper.rb +81 -0
  38. data/lib/zip/filesystem.rb +32 -585
  39. data/lib/zip/inflater.rb +28 -37
  40. data/lib/zip/input_stream.rb +113 -54
  41. data/lib/zip/ioextras/abstract_input_stream.rb +154 -55
  42. data/lib/zip/ioextras/abstract_output_stream.rb +13 -3
  43. data/lib/zip/ioextras.rb +8 -8
  44. data/lib/zip/null_compressor.rb +3 -1
  45. data/lib/zip/null_decompressor.rb +7 -12
  46. data/lib/zip/null_input_stream.rb +3 -1
  47. data/lib/zip/output_stream.rb +69 -50
  48. data/lib/zip/pass_thru_compressor.rb +5 -3
  49. data/lib/zip/pass_thru_decompressor.rb +17 -23
  50. data/lib/zip/streamable_directory.rb +6 -4
  51. data/lib/zip/streamable_stream.rb +10 -11
  52. data/lib/zip/version.rb +4 -1
  53. data/lib/zip.rb +32 -7
  54. data/rubyzip.gemspec +39 -0
  55. data/samples/example.rb +9 -4
  56. data/samples/example_filesystem.rb +4 -3
  57. data/samples/example_recursive.rb +3 -1
  58. data/samples/gtk_ruby_zip.rb +23 -21
  59. data/samples/qtzip.rb +13 -12
  60. data/samples/write_simple.rb +3 -4
  61. data/samples/zipfind.rb +24 -22
  62. metadata +85 -177
  63. data/TODO +0 -15
  64. data/lib/zip/extra_field/zip64_placeholder.rb +0 -15
  65. data/test/basic_zip_file_test.rb +0 -60
  66. data/test/case_sensitivity_test.rb +0 -69
  67. data/test/central_directory_entry_test.rb +0 -69
  68. data/test/central_directory_test.rb +0 -100
  69. data/test/crypto/null_encryption_test.rb +0 -57
  70. data/test/crypto/traditional_encryption_test.rb +0 -80
  71. data/test/data/WarnInvalidDate.zip +0 -0
  72. data/test/data/file1.txt +0 -46
  73. data/test/data/file1.txt.deflatedData +0 -0
  74. data/test/data/file2.txt +0 -1504
  75. data/test/data/globTest/foo/bar/baz/foo.txt +0 -0
  76. data/test/data/globTest/foo.txt +0 -0
  77. data/test/data/globTest/food.txt +0 -0
  78. data/test/data/globTest.zip +0 -0
  79. data/test/data/gpbit3stored.zip +0 -0
  80. data/test/data/mimetype +0 -1
  81. data/test/data/notzippedruby.rb +0 -7
  82. data/test/data/ntfs.zip +0 -0
  83. data/test/data/oddExtraField.zip +0 -0
  84. data/test/data/path_traversal/Makefile +0 -10
  85. data/test/data/path_traversal/jwilk/README.md +0 -5
  86. data/test/data/path_traversal/jwilk/absolute1.zip +0 -0
  87. data/test/data/path_traversal/jwilk/absolute2.zip +0 -0
  88. data/test/data/path_traversal/jwilk/dirsymlink.zip +0 -0
  89. data/test/data/path_traversal/jwilk/dirsymlink2a.zip +0 -0
  90. data/test/data/path_traversal/jwilk/dirsymlink2b.zip +0 -0
  91. data/test/data/path_traversal/jwilk/relative0.zip +0 -0
  92. data/test/data/path_traversal/jwilk/relative2.zip +0 -0
  93. data/test/data/path_traversal/jwilk/symlink.zip +0 -0
  94. data/test/data/path_traversal/relative1.zip +0 -0
  95. data/test/data/path_traversal/tilde.zip +0 -0
  96. data/test/data/path_traversal/tuzovakaoff/README.md +0 -3
  97. data/test/data/path_traversal/tuzovakaoff/absolutepath.zip +0 -0
  98. data/test/data/path_traversal/tuzovakaoff/symlink.zip +0 -0
  99. data/test/data/rubycode.zip +0 -0
  100. data/test/data/rubycode2.zip +0 -0
  101. data/test/data/test.xls +0 -0
  102. data/test/data/testDirectory.bin +0 -0
  103. data/test/data/zip64-sample.zip +0 -0
  104. data/test/data/zipWithDirs.zip +0 -0
  105. data/test/data/zipWithEncryption.zip +0 -0
  106. data/test/deflater_test.rb +0 -65
  107. data/test/encryption_test.rb +0 -42
  108. data/test/entry_set_test.rb +0 -163
  109. data/test/entry_test.rb +0 -154
  110. data/test/errors_test.rb +0 -35
  111. data/test/extra_field_test.rb +0 -76
  112. data/test/file_extract_directory_test.rb +0 -54
  113. data/test/file_extract_test.rb +0 -145
  114. data/test/file_permissions_test.rb +0 -65
  115. data/test/file_split_test.rb +0 -57
  116. data/test/file_test.rb +0 -666
  117. data/test/filesystem/dir_iterator_test.rb +0 -58
  118. data/test/filesystem/directory_test.rb +0 -139
  119. data/test/filesystem/file_mutating_test.rb +0 -87
  120. data/test/filesystem/file_nonmutating_test.rb +0 -508
  121. data/test/filesystem/file_stat_test.rb +0 -64
  122. data/test/gentestfiles.rb +0 -126
  123. data/test/inflater_test.rb +0 -14
  124. data/test/input_stream_test.rb +0 -182
  125. data/test/ioextras/abstract_input_stream_test.rb +0 -102
  126. data/test/ioextras/abstract_output_stream_test.rb +0 -106
  127. data/test/ioextras/fake_io_test.rb +0 -18
  128. data/test/local_entry_test.rb +0 -154
  129. data/test/output_stream_test.rb +0 -128
  130. data/test/pass_thru_compressor_test.rb +0 -30
  131. data/test/pass_thru_decompressor_test.rb +0 -14
  132. data/test/path_traversal_test.rb +0 -141
  133. data/test/samples/example_recursive_test.rb +0 -37
  134. data/test/settings_test.rb +0 -95
  135. data/test/test_helper.rb +0 -234
  136. data/test/unicode_file_names_and_comments_test.rb +0 -62
  137. data/test/zip64_full_test.rb +0 -51
  138. data/test/zip64_support_test.rb +0 -14
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
@@ -50,6 +52,7 @@ module Zip
50
52
 
51
53
  def ==(other)
52
54
  return false unless other.kind_of?(EntrySet)
55
+
53
56
  @entry_set.values == other.entry_set.values
54
57
  end
55
58
 
@@ -58,17 +61,18 @@ module Zip
58
61
  end
59
62
 
60
63
  def glob(pattern, flags = ::File::FNM_PATHNAME | ::File::FNM_DOTMATCH | ::File::FNM_EXTGLOB)
61
- entries.map do |entry|
64
+ entries.filter_map do |entry|
62
65
  next nil unless ::File.fnmatch(pattern, entry.name.chomp('/'), flags)
66
+
63
67
  yield(entry) if block_given?
64
68
  entry
65
- end.compact
69
+ end
66
70
  end
67
71
 
68
72
  protected
69
73
 
70
74
  def sorted_entries
71
- ::Zip.sort_entries ? Hash[@entry_set.sort] : @entry_set
75
+ ::Zip.sort_entries ? @entry_set.sort.to_h : @entry_set
72
76
  end
73
77
 
74
78
  private
data/lib/zip/errors.rb CHANGED
@@ -1,18 +1,155 @@
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
-
11
- # Backwards compatibility with v1 (delete in v2)
12
- ZipError = Error
13
- ZipEntryExistsError = EntryExistsError
14
- ZipDestinationFileExistsError = DestinationFileExistsError
15
- ZipCompressionMethodError = CompressionMethodError
16
- ZipEntryNameError = EntryNameError
17
- 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
+ class EntryNumberMismatchError < Error
113
+ # Create a new EntryNumberMismatchError with the specified size and size in bytes.
114
+ def initialize(size, size_in_bytes)
115
+ super()
116
+ @size = size
117
+ @size_in_bytes = size_in_bytes
118
+ end
119
+
120
+ # The message returned by this error.
121
+ def message
122
+ "Zip consistency problem: an impossibly high number of entries (#{@size}) " \
123
+ 'is declared in this file, compared to the size of the central directory ' \
124
+ "(#{@size_in_bytes} bytes)."
125
+ end
126
+ end
127
+
128
+ # Error raised if a split archive is read. Rubyzip does not support reading
129
+ # split archives.
130
+ class SplitArchiveError < Error
131
+ # The message returned by this error.
132
+ def message
133
+ 'Rubyzip cannot extract from split archives at this time.'
134
+ end
135
+ end
136
+
137
+ # Error raised if there is not enough metadata for the entry to be streamed.
138
+ class StreamingError < Error
139
+ # The entry that has caused this error.
140
+ attr_reader :entry
141
+
142
+ # Create a new StreamingError with the specified entry.
143
+ def initialize(entry)
144
+ super()
145
+ @entry = entry
146
+ end
147
+
148
+ # The message returned by this error.
149
+ def message
150
+ "The local header of this entry ('#{@entry.name}') does not contain " \
151
+ 'the correct metadata for `Zip::InputStream` to be able to ' \
152
+ 'uncompress it. Please use `Zip::File` instead of `Zip::InputStream`.'
153
+ end
154
+ end
18
155
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zip
4
+ # Info-ZIP Extra for AES encryption
5
+ class ExtraField::AES < ExtraField::Generic # :nodoc:
6
+ attr_reader :vendor_version, :vendor_id, :encryption_strength, :compression_method
7
+
8
+ HEADER_ID = [0x9901].pack('v')
9
+ register_map
10
+
11
+ def initialize(binstr = nil)
12
+ @vendor_version = nil
13
+ @vendor_id = nil
14
+ @encryption_strength = nil
15
+ @compression_method = nil
16
+ binstr && merge(binstr)
17
+ end
18
+
19
+ def ==(other)
20
+ @vendor_version == other.vendor_version &&
21
+ @vendor_id == other.vendor_id &&
22
+ @encryption_strength == other.encryption_strength &&
23
+ @compression_method == other.compression_method
24
+ end
25
+
26
+ def merge(binstr)
27
+ return if binstr.empty?
28
+
29
+ size, content = initial_parse(binstr)
30
+ (size && content) || return
31
+
32
+ @vendor_version, @vendor_id,
33
+ @encryption_strength, @compression_method = content.unpack('va2Cv')
34
+ end
35
+
36
+ # We can never suppress the AES extra field as it is needed to read the file.
37
+ def suppress?
38
+ false
39
+ end
40
+
41
+ def pack_for_local
42
+ [@vendor_version, @vendor_id,
43
+ @encryption_strength, @compression_method].pack('va2Cv')
44
+ end
45
+
46
+ def pack_for_c_dir
47
+ pack_for_local
48
+ end
49
+ end
50
+ end
@@ -1,43 +1,43 @@
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
- if const_defined?(:HEADER_ID)
5
- ::Zip::ExtraField::ID_MAP[const_get(:HEADER_ID)] = self
6
- end
6
+ return unless const_defined?(:HEADER_ID)
7
+
8
+ ::Zip::ExtraField::ID_MAP[const_get(:HEADER_ID)] = self
7
9
  end
8
10
 
9
11
  def self.name
10
- @name ||= to_s.split('::')[-1]
12
+ @name ||= to_s.split('::').last.downcase.to_sym
11
13
  end
12
14
 
13
15
  # return field [size, content] or false
14
16
  def initial_parse(binstr)
15
- if !binstr
16
- # If nil, start with empty.
17
- return false
18
- elsif binstr[0, 2] != self.class.const_get(:HEADER_ID)
19
- $stderr.puts 'Warning: weired extra feild header ID. skip parsing'
17
+ return false unless binstr
18
+
19
+ if binstr[0, 2] != self.class.const_get(:HEADER_ID)
20
+ warn 'WARNING: weird extra field header ID. Skip parsing it.'
20
21
  return false
21
22
  end
22
- [binstr[2, 2].unpack('v')[0], binstr[4..-1]]
23
+
24
+ [binstr[2, 2].unpack1('v'), binstr[4..]]
23
25
  end
24
26
 
25
- def ==(other)
26
- return false if self.class != other.class
27
- each do |k, v|
28
- return false if v != other[k]
29
- end
27
+ # Default strategy is to suppress all extra fields if we're asked to.
28
+ # Specific extra field types can override this if they need to be kept.
29
+ def suppress?
30
30
  true
31
31
  end
32
32
 
33
33
  def to_local_bin
34
34
  s = pack_for_local
35
- self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v') << s
35
+ (self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v')) << s
36
36
  end
37
37
 
38
38
  def to_c_dir_bin
39
39
  s = pack_for_c_dir
40
- self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v') << s
40
+ (self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v')) << s
41
41
  end
42
42
  end
43
43
  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
 
@@ -19,14 +21,16 @@ module Zip
19
21
 
20
22
  def merge(binstr)
21
23
  return if binstr.empty?
24
+
22
25
  size, content = initial_parse(binstr)
23
26
  (size && content) || return
24
27
 
25
- content = content[4..-1]
28
+ content = content[4..]
26
29
  tags = parse_tags(content)
27
30
 
28
31
  tag1 = tags[1]
29
32
  return unless tag1
33
+
30
34
  ntfs_mtime, ntfs_atime, ntfs_ctime = tag1.unpack('Q<Q<Q<')
31
35
  ntfs_mtime && @mtime ||= from_ntfs_time(ntfs_mtime)
32
36
  ntfs_atime && @atime ||= from_ntfs_time(ntfs_atime)
@@ -49,7 +53,7 @@ module Zip
49
53
  # reserved 0 and tag 1
50
54
  s = [0, 1].pack('Vv')
51
55
 
52
- tag1 = ''.force_encoding(Encoding::BINARY)
56
+ tag1 = (+'').force_encoding(Encoding::BINARY)
53
57
  if @mtime
54
58
  tag1 << [to_ntfs_time(@mtime)].pack('Q<')
55
59
  if @atime
@@ -65,12 +69,14 @@ module Zip
65
69
 
66
70
  def parse_tags(content)
67
71
  return {} if content.nil?
72
+
68
73
  tags = {}
69
74
  i = 0
70
75
  while i < content.bytesize
71
76
  tag, size = content[i, 4].unpack('vv')
72
77
  i += 4
73
78
  break unless tag && size
79
+
74
80
  value = content[i, size]
75
81
  i += size
76
82
  tags[tag] = value
@@ -80,7 +86,7 @@ module Zip
80
86
  end
81
87
 
82
88
  def from_ntfs_time(ntfs_time)
83
- ::Zip::DOSTime.at(ntfs_time / WINDOWS_TICK - SEC_TO_UNIX_EPOCH)
89
+ ::Zip::DOSTime.at((ntfs_time / WINDOWS_TICK) - SEC_TO_UNIX_EPOCH)
84
90
  end
85
91
 
86
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
 
@@ -16,14 +18,16 @@ module Zip
16
18
 
17
19
  def merge(binstr)
18
20
  return if binstr.empty?
21
+
19
22
  size, content = initial_parse(binstr)
20
23
  # size: 0 for central directory. 4 for local header
21
24
  return if !size || size == 0
25
+
22
26
  atime, mtime, uid, gid = content.unpack('VVvv')
23
27
  @uid ||= uid
24
28
  @gid ||= gid
25
29
  @atime ||= atime
26
- @mtime ||= mtime
30
+ @mtime ||= mtime # rubocop:disable Naming/MemoizedInstanceVariableName
27
31
  end
28
32
 
29
33
  def ==(other)
@@ -1,27 +1,59 @@
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
 
9
+ ATIME_MASK = 0b010
10
+ CTIME_MASK = 0b100
11
+ MTIME_MASK = 0b001
12
+
7
13
  def initialize(binstr = nil)
8
14
  @ctime = nil
9
15
  @mtime = nil
10
16
  @atime = nil
11
- @flag = nil
12
- binstr && merge(binstr)
17
+ @flag = 0
18
+
19
+ merge(binstr) unless binstr.nil?
20
+ end
21
+
22
+ attr_reader :atime, :ctime, :mtime, :flag
23
+
24
+ def atime=(time)
25
+ @flag = time.nil? ? @flag & ~ATIME_MASK : @flag | ATIME_MASK
26
+ @atime = time
13
27
  end
14
28
 
15
- attr_accessor :atime, :ctime, :mtime, :flag
29
+ def ctime=(time)
30
+ @flag = time.nil? ? @flag & ~CTIME_MASK : @flag | CTIME_MASK
31
+ @ctime = time
32
+ end
33
+
34
+ def mtime=(time)
35
+ @flag = time.nil? ? @flag & ~MTIME_MASK : @flag | MTIME_MASK
36
+ @mtime = time
37
+ end
16
38
 
17
39
  def merge(binstr)
18
40
  return if binstr.empty?
41
+
19
42
  size, content = initial_parse(binstr)
20
- size || return
21
- @flag, mtime, atime, ctime = content.unpack('CVVV')
22
- mtime && @mtime ||= ::Zip::DOSTime.at(mtime)
23
- atime && @atime ||= ::Zip::DOSTime.at(atime)
24
- ctime && @ctime ||= ::Zip::DOSTime.at(ctime)
43
+ return if !size || size <= 0
44
+
45
+ @flag, *times = content.unpack('Cl<l<l<')
46
+
47
+ # Parse the timestamps, in order, based on which flags are set.
48
+ return if times[0].nil?
49
+
50
+ @mtime ||= ::Zip::DOSTime.at(times.shift) unless @flag & MTIME_MASK == 0
51
+ return if times[0].nil?
52
+
53
+ @atime ||= ::Zip::DOSTime.at(times.shift) unless @flag & ATIME_MASK == 0
54
+ return if times[0].nil?
55
+
56
+ @ctime ||= ::Zip::DOSTime.at(times.shift) unless @flag & CTIME_MASK == 0
25
57
  end
26
58
 
27
59
  def ==(other)
@@ -32,15 +64,15 @@ module Zip
32
64
 
33
65
  def pack_for_local
34
66
  s = [@flag].pack('C')
35
- @flag & 1 != 0 && s << [@mtime.to_i].pack('V')
36
- @flag & 2 != 0 && s << [@atime.to_i].pack('V')
37
- @flag & 4 != 0 && s << [@ctime.to_i].pack('V')
67
+ s << [@mtime.to_i].pack('l<') unless @flag & MTIME_MASK == 0
68
+ s << [@atime.to_i].pack('l<') unless @flag & ATIME_MASK == 0
69
+ s << [@ctime.to_i].pack('l<') unless @flag & CTIME_MASK == 0
38
70
  s
39
71
  end
40
72
 
41
73
  def pack_for_c_dir
42
74
  s = [@flag].pack('C')
43
- @flag & 1 == 1 && s << [@mtime.to_i].pack('V')
75
+ s << [@mtime.to_i].pack('l<') unless @flag & MTIME_MASK == 0
44
76
  s
45
77
  end
46
78
  end
@@ -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
 
@@ -14,12 +16,14 @@ module Zip
14
16
 
15
17
  def merge(binstr)
16
18
  return if binstr.empty?
19
+
17
20
  size, content = initial_parse(binstr)
18
21
  # size: 0 for central directory. 4 for local header
19
22
  return if !size || size == 0
23
+
20
24
  uid, gid = content.unpack('vv')
21
- @uid ||= uid
22
- @gid ||= gid
25
+ @uid = uid
26
+ @gid = gid
23
27
  end
24
28
 
25
29
  def ==(other)
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'generic'
4
+
5
+ module Zip
6
+ # A class to hold unknown extra fields so that they are preserved.
7
+ class ExtraField::Unknown < ExtraField::Generic # :nodoc:
8
+ def initialize
9
+ @local_bin = +''
10
+ @cdir_bin = +''
11
+ end
12
+
13
+ def merge(binstr, local: false)
14
+ return if binstr.empty?
15
+
16
+ if local
17
+ @local_bin << binstr
18
+ else
19
+ @cdir_bin << binstr
20
+ end
21
+ end
22
+
23
+ def to_local_bin
24
+ @local_bin
25
+ end
26
+
27
+ def to_c_dir_bin
28
+ @cdir_bin
29
+ end
30
+
31
+ def ==(other)
32
+ @local_bin == other.to_local_bin && @cdir_bin == other.to_c_dir_bin
33
+ end
34
+ end
35
+ 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
 
@@ -9,7 +13,7 @@ module Zip
9
13
  # unparsed binary; we don't actually know what this contains
10
14
  # without looking for FFs in the associated file header
11
15
  # call parse after initializing with a binary string
12
- @content = nil
16
+ @content = nil
13
17
  @original_size = nil
14
18
  @compressed_size = nil
15
19
  @relative_header_offset = nil
@@ -26,6 +30,7 @@ module Zip
26
30
 
27
31
  def merge(binstr)
28
32
  return if binstr.empty?
33
+
29
34
  _, @content = initial_parse(binstr)
30
35
  end
31
36
 
@@ -35,7 +40,9 @@ module Zip
35
40
  def parse(original_size, compressed_size, relative_header_offset = nil, disk_start_number = nil)
36
41
  @original_size = extract(8, 'Q<') if original_size == 0xFFFFFFFF
37
42
  @compressed_size = extract(8, 'Q<') if compressed_size == 0xFFFFFFFF
38
- @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
39
46
  @disk_start_number = extract(4, 'V') if disk_start_number && disk_start_number == 0xFFFF
40
47
  @content = nil
41
48
  [@original_size || original_size,
@@ -45,19 +52,28 @@ module Zip
45
52
  end
46
53
 
47
54
  def extract(size, format)
48
- @content.slice!(0, size).unpack(format)[0]
55
+ @content.slice!(0, size).unpack1(format)
49
56
  end
50
57
  private :extract
51
58
 
59
+ # We can suppress the zip64 extra field unless we know the size is large or
60
+ # the relative header offset is large (for central directory entries).
61
+ def suppress?
62
+ !(@original_size && @original_size >= 0xFFFFFFFF) ||
63
+ (@relative_header_offset && @relative_header_offset >= 0xFFFFFFFF)
64
+ end
65
+
52
66
  def pack_for_local
53
- # local header entries must contain original size and compressed size; other fields do not apply
67
+ # Local header entries must contain original size and compressed size;
68
+ # other fields do not apply.
54
69
  return '' unless @original_size && @compressed_size
70
+
55
71
  [@original_size, @compressed_size].pack('Q<Q<')
56
72
  end
57
73
 
58
74
  def pack_for_c_dir
59
75
  # central directory entries contain only fields that didn't fit in the main entry part
60
- packed = ''.force_encoding('BINARY')
76
+ packed = (+'').force_encoding('BINARY')
61
77
  packed << [@original_size].pack('Q<') if @original_size
62
78
  packed << [@compressed_size].pack('Q<') if @compressed_size
63
79
  packed << [@relative_header_offset].pack('Q<') if @relative_header_offset