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
@@ -1,45 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ require_relative 'dirtyable'
6
+
1
7
  module Zip
2
- class CentralDirectory
3
- include Enumerable
8
+ class CentralDirectory # :nodoc:
9
+ extend Forwardable
10
+ include Dirtyable
11
+
12
+ END_OF_CD_SIG = 0x06054b50
13
+ ZIP64_END_OF_CD_SIG = 0x06064b50
14
+ ZIP64_EOCD_LOCATOR_SIG = 0x07064b50
4
15
 
5
- END_OF_CDS = 0x06054b50
6
- ZIP64_END_OF_CDS = 0x06064b50
7
- ZIP64_EOCD_LOCATOR = 0x07064b50
8
- MAX_END_OF_CDS_SIZE = 65_536 + 18
9
16
  STATIC_EOCD_SIZE = 22
17
+ ZIP64_STATIC_EOCD_SIZE = 56
18
+ ZIP64_EOCD_LOC_SIZE = 20
19
+ MAX_FILE_COMMENT_SIZE = (1 << 16) - 1
20
+ MAX_END_OF_CD_SIZE =
21
+ MAX_FILE_COMMENT_SIZE + STATIC_EOCD_SIZE + ZIP64_EOCD_LOC_SIZE
10
22
 
11
- attr_reader :comment
23
+ attr_accessor :comment
12
24
 
13
- # Returns an Enumerable containing the entries.
14
- def entries
15
- @entry_set.entries
16
- end
25
+ def_delegators :@entry_set,
26
+ :<<, :delete, :each, :entries, :find_entry, :glob,
27
+ :include?, :size
28
+
29
+ mark_dirty :<<, :comment=, :delete
17
30
 
18
- def initialize(entries = EntrySet.new, comment = '') #:nodoc:
19
- super()
31
+ def initialize(entries = EntrySet.new, comment = '') # :nodoc:
32
+ super(dirty_on_create: false)
20
33
  @entry_set = entries.kind_of?(EntrySet) ? entries : EntrySet.new(entries)
21
34
  @comment = comment
22
35
  end
23
36
 
24
- def write_to_stream(io) #:nodoc:
37
+ def read_from_stream(io)
38
+ read_eocds(io)
39
+ read_central_directory_entries(io)
40
+ end
41
+
42
+ def write_to_stream(io, suppress_extra_fields: false) # :nodoc:
25
43
  cdir_offset = io.tell
26
- @entry_set.each { |entry| entry.write_c_dir_entry(io) }
44
+ @entry_set.each do |entry|
45
+ entry.write_c_dir_entry(io, suppress_extra_fields: suppress_extra_fields)
46
+ end
27
47
  eocd_offset = io.tell
28
48
  cdir_size = eocd_offset - cdir_offset
29
- if ::Zip.write_zip64_support
30
- need_zip64_eocd = cdir_offset > 0xFFFFFFFF || cdir_size > 0xFFFFFFFF || @entry_set.size > 0xFFFF
31
- need_zip64_eocd ||= @entry_set.any? { |entry| entry.extra['Zip64'] }
32
- if need_zip64_eocd
33
- write_64_e_o_c_d(io, cdir_offset, cdir_size)
34
- write_64_eocd_locator(io, eocd_offset)
35
- end
49
+ if Zip.write_zip64_support &&
50
+ (cdir_offset > 0xFFFFFFFF || cdir_size > 0xFFFFFFFF || @entry_set.size > 0xFFFF)
51
+ write_64_e_o_c_d(io, cdir_offset, cdir_size)
52
+ write_64_eocd_locator(io, eocd_offset)
36
53
  end
37
54
  write_e_o_c_d(io, cdir_offset, cdir_size)
38
55
  end
39
56
 
40
- def write_e_o_c_d(io, offset, cdir_size) #:nodoc:
57
+ # Reads the End of Central Directory Record (and the Zip64 equivalent if
58
+ # needs be) and returns the number of entries in the archive. This is a
59
+ # convenience method that avoids reading in all of the entry data to get a
60
+ # very quick entry count.
61
+ def count_entries(io)
62
+ read_eocds(io)
63
+ @size
64
+ end
65
+
66
+ def ==(other) # :nodoc:
67
+ return false unless other.kind_of?(CentralDirectory)
68
+
69
+ @entry_set.entries.sort == other.entries.sort && comment == other.comment
70
+ end
71
+
72
+ private
73
+
74
+ def write_e_o_c_d(io, offset, cdir_size) # :nodoc:
41
75
  tmp = [
42
- END_OF_CDS,
76
+ END_OF_CD_SIG,
43
77
  0, # @numberOfThisDisk
44
78
  0, # @numberOfDiskWithStartOfCDir
45
79
  @entry_set ? [@entry_set.size, 0xFFFF].min : 0,
@@ -52,11 +86,9 @@ module Zip
52
86
  io << @comment
53
87
  end
54
88
 
55
- private :write_e_o_c_d
56
-
57
- def write_64_e_o_c_d(io, offset, cdir_size) #:nodoc:
89
+ def write_64_e_o_c_d(io, offset, cdir_size) # :nodoc:
58
90
  tmp = [
59
- ZIP64_END_OF_CDS,
91
+ ZIP64_END_OF_CD_SIG,
60
92
  44, # size of zip64 end of central directory record (excludes signature and field itself)
61
93
  VERSION_MADE_BY,
62
94
  VERSION_NEEDED_TO_EXTRACT_ZIP64,
@@ -65,16 +97,14 @@ module Zip
65
97
  @entry_set ? @entry_set.size : 0, # number of entries on this disk
66
98
  @entry_set ? @entry_set.size : 0, # number of entries total
67
99
  cdir_size, # size of central directory
68
- offset, # offset of start of central directory in its disk
100
+ offset # offset of start of central directory in its disk
69
101
  ]
70
102
  io << tmp.pack('VQ<vvVVQ<Q<Q<Q<')
71
103
  end
72
104
 
73
- private :write_64_e_o_c_d
74
-
75
105
  def write_64_eocd_locator(io, zip64_eocd_offset)
76
106
  tmp = [
77
- ZIP64_EOCD_LOCATOR,
107
+ ZIP64_EOCD_LOCATOR_SIG,
78
108
  0, # number of disk containing the start of zip64 eocd record
79
109
  zip64_eocd_offset, # offset of the start of zip64 eocd record in its disk
80
110
  1 # total number of disks
@@ -82,123 +112,166 @@ module Zip
82
112
  io << tmp.pack('VVQ<V')
83
113
  end
84
114
 
85
- private :write_64_eocd_locator
86
-
87
- def read_64_e_o_c_d(buf) #:nodoc:
88
- buf = get_64_e_o_c_d(buf)
89
- @size_of_zip64_e_o_c_d = Entry.read_zip_64_long(buf)
90
- @version_made_by = Entry.read_zip_short(buf)
91
- @version_needed_for_extract = Entry.read_zip_short(buf)
92
- @number_of_this_disk = Entry.read_zip_long(buf)
93
- @number_of_disk_with_start_of_cdir = Entry.read_zip_long(buf)
94
- @total_number_of_entries_in_cdir_on_this_disk = Entry.read_zip_64_long(buf)
95
- @size = Entry.read_zip_64_long(buf)
96
- @size_in_bytes = Entry.read_zip_64_long(buf)
97
- @cdir_offset = Entry.read_zip_64_long(buf)
98
- @zip_64_extensible = buf.slice!(0, buf.bytesize)
99
- raise Error, 'Zip consistency problem while reading eocd structure' unless buf.empty?
100
- end
101
-
102
- def read_e_o_c_d(buf) #:nodoc:
103
- buf = get_e_o_c_d(buf)
104
- @number_of_this_disk = Entry.read_zip_short(buf)
105
- @number_of_disk_with_start_of_cdir = Entry.read_zip_short(buf)
106
- @total_number_of_entries_in_cdir_on_this_disk = Entry.read_zip_short(buf)
107
- @size = Entry.read_zip_short(buf)
108
- @size_in_bytes = Entry.read_zip_long(buf)
109
- @cdir_offset = Entry.read_zip_long(buf)
110
- comment_length = Entry.read_zip_short(buf)
111
- @comment = if comment_length.to_i <= 0
112
- buf.slice!(0, buf.size)
113
- else
114
- buf.read(comment_length)
115
- end
116
- raise Error, 'Zip consistency problem while reading eocd structure' unless buf.empty?
117
- end
118
-
119
- def read_central_directory_entries(io) #:nodoc:
115
+ def unpack_64_e_o_c_d(buffer) # :nodoc:
116
+ _, # ZIP64_END_OF_CD_SIG. We know we have this at this point.
117
+ size_of_zip64_e_o_c_d,
118
+ _version_made_by,
119
+ _version_needed_for_extract,
120
+ _number_of_this_disk,
121
+ _number_of_disk_with_start_of_cdir,
122
+ _total_number_of_entries_in_cdir_on_this_disk,
123
+ @size,
124
+ size_in_bytes,
125
+ @cdir_offset = buffer.unpack('VQ<vvVVQ<Q<Q<Q<')
126
+
127
+ validate_size!(@size, size_in_bytes)
128
+
129
+ zip64_extensible_data_size =
130
+ size_of_zip64_e_o_c_d - ZIP64_STATIC_EOCD_SIZE + 12
131
+ @zip64_extensible_data = if zip64_extensible_data_size.zero?
132
+ ''
133
+ else
134
+ buffer.slice(
135
+ ZIP64_STATIC_EOCD_SIZE,
136
+ zip64_extensible_data_size
137
+ )
138
+ end
139
+ end
140
+
141
+ def unpack_64_eocd_locator(buffer) # :nodoc:
142
+ _, # ZIP64_EOCD_LOCATOR_SIG. We know we have this at this point.
143
+ _, zip64_eocd_offset, = buffer.unpack('VVQ<V')
144
+
145
+ zip64_eocd_offset
146
+ end
147
+
148
+ # Unpack the EOCD and return a boolean indicating whether this header is
149
+ # complete without needing Zip64 extensions.
150
+ def unpack_e_o_c_d(buffer) # :nodoc:
151
+ _, # END_OF_CD_SIG. We know we have this at this point.
152
+ number_of_this_disk,
153
+ number_of_disk_with_start_of_cdir,
154
+ total_number_of_entries_in_cdir_on_this_disk,
155
+ @size,
156
+ size_in_bytes,
157
+ @cdir_offset,
158
+ comment_length = buffer.unpack('VvvvvVVv')
159
+
160
+ complete = !([number_of_this_disk, number_of_disk_with_start_of_cdir,
161
+ total_number_of_entries_in_cdir_on_this_disk, @size].any?(0xFFFF) ||
162
+ size_in_bytes == 0xFFFFFFFF || @cdir_offset == 0xFFFFFFFF)
163
+
164
+ validate_size!(@size, size_in_bytes) if complete
165
+
166
+ @comment = if comment_length.positive?
167
+ buffer.slice(STATIC_EOCD_SIZE, comment_length)
168
+ else
169
+ ''
170
+ end
171
+
172
+ complete
173
+ end
174
+
175
+ def read_central_directory_entries(io) # :nodoc:
176
+ # `StringIO` doesn't raise `EINVAL` if you seek beyond the current end,
177
+ # so we need to catch that *and* query `io#eof?` here.
178
+ eof = false
120
179
  begin
121
180
  io.seek(@cdir_offset, IO::SEEK_SET)
122
181
  rescue Errno::EINVAL
123
- raise Error, 'Zip consistency problem while reading central directory entry'
182
+ eof = true
124
183
  end
184
+ raise Error, 'Zip consistency problem while reading central directory entry' if eof || io.eof?
185
+
125
186
  @entry_set = EntrySet.new
126
187
  @size.times do
127
- @entry_set << Entry.read_c_dir_entry(io)
188
+ entry = Entry.read_c_dir_entry(io)
189
+ break unless entry
190
+
191
+ offset = if entry.zip64?
192
+ entry.extra[:zip64].relative_header_offset
193
+ else
194
+ entry.local_header_offset
195
+ end
196
+
197
+ unless offset.nil?
198
+ io_save = io.tell
199
+ io.seek(offset, IO::SEEK_SET)
200
+ entry.read_extra_field(read_local_extra_field(io), local: true)
201
+ io.seek(io_save, IO::SEEK_SET)
202
+ end
203
+
204
+ @entry_set << entry
128
205
  end
129
206
  end
130
207
 
131
- def read_from_stream(io) #:nodoc:
132
- buf = start_buf(io)
133
- if zip64_file?(buf)
134
- read_64_e_o_c_d(buf)
135
- else
136
- read_e_o_c_d(buf)
137
- end
138
- read_central_directory_entries(io)
208
+ def read_local_extra_field(io)
209
+ buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
210
+ return '' unless buf.bytesize == ::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH
211
+
212
+ head, _, _, _, _, _, _, _, _, _, n_len, e_len = buf.unpack('VCCvvvvVVVvv')
213
+ return '' unless head == ::Zip::LOCAL_ENTRY_SIGNATURE
214
+
215
+ io.seek(n_len, IO::SEEK_CUR) # Skip over the entry name.
216
+ io.read(e_len)
139
217
  end
140
218
 
141
- def get_e_o_c_d(buf) #:nodoc:
142
- sig_index = buf.rindex([END_OF_CDS].pack('V'))
143
- raise Error, 'Zip end of central directory signature not found' unless sig_index
144
- buf = buf.slice!((sig_index + 4)..(buf.bytesize))
219
+ def read_eocds(io) # :nodoc:
220
+ base_location, data = eocd_data(io)
221
+
222
+ eocd_location = data.rindex([END_OF_CD_SIG].pack('V'))
223
+ raise Error, 'Zip end of central directory signature not found' unless eocd_location
224
+
225
+ # Parse the EOCD and return if it is complete without Zip64 extensions.
226
+ return if unpack_e_o_c_d(data.slice(eocd_location..-1))
145
227
 
146
- def buf.read(count)
147
- slice!(0, count)
228
+ # Need to read in the Zip64 EOCD locator and then the Zip64 EOCD.
229
+ zip64_eocd_locator = data.rindex([ZIP64_EOCD_LOCATOR_SIG].pack('V'), eocd_location)
230
+ unless zip64_eocd_locator
231
+ raise Error, 'Zip64 end of central directory locator signature expected but not found'
148
232
  end
149
233
 
150
- buf
151
- end
234
+ # Do we already have the Zip64 EOCD in the data we've read?
235
+ zip64_eocd_location = data.rindex([ZIP64_END_OF_CD_SIG].pack('V'), zip64_eocd_locator)
236
+
237
+ zip64_eocd_data =
238
+ if zip64_eocd_location
239
+ # Yes.
240
+ data.slice(zip64_eocd_location..zip64_eocd_locator)
241
+ else
242
+ # No. Read its location from the locator and then read it in.
243
+ zip64_eocd_location = unpack_64_eocd_locator(
244
+ data.slice(zip64_eocd_locator..eocd_location)
245
+ )
246
+ unless zip64_eocd_location
247
+ raise Error, 'Zip64 end of central directory signature not found'
248
+ end
249
+
250
+ io.seek(zip64_eocd_location, IO::SEEK_SET)
251
+ io.read(base_location + zip64_eocd_locator - zip64_eocd_location)
252
+ end
152
253
 
153
- def zip64_file?(buf)
154
- buf.rindex([ZIP64_END_OF_CDS].pack('V')) && buf.rindex([ZIP64_EOCD_LOCATOR].pack('V'))
254
+ # Finally, unpack the Zip64 EOCD.
255
+ unpack_64_e_o_c_d(zip64_eocd_data)
155
256
  end
156
257
 
157
- def start_buf(io)
258
+ def eocd_data(io)
158
259
  begin
159
- io.seek(-MAX_END_OF_CDS_SIZE, IO::SEEK_END)
260
+ io.seek(-MAX_END_OF_CD_SIZE, IO::SEEK_END)
160
261
  rescue Errno::EINVAL
161
262
  io.seek(0, IO::SEEK_SET)
162
263
  end
163
- io.read
164
- end
165
-
166
- def get_64_e_o_c_d(buf) #:nodoc:
167
- zip_64_start = buf.rindex([ZIP64_END_OF_CDS].pack('V'))
168
- raise Error, 'Zip64 end of central directory signature not found' unless zip_64_start
169
- zip_64_locator = buf.rindex([ZIP64_EOCD_LOCATOR].pack('V'))
170
- raise Error, 'Zip64 end of central directory signature locator not found' unless zip_64_locator
171
- buf = buf.slice!((zip_64_start + 4)..zip_64_locator)
172
-
173
- def buf.read(count)
174
- slice!(0, count)
175
- end
176
264
 
177
- buf
265
+ [io.tell, io.read]
178
266
  end
179
267
 
180
- # For iterating over the entries.
181
- def each(&proc)
182
- @entry_set.each(&proc)
183
- end
268
+ def validate_size!(size, size_in_bytes)
269
+ return if size * CDIR_ENTRY_STATIC_HEADER_LENGTH <= size_in_bytes
184
270
 
185
- # Returns the number of entries in the central directory (and
186
- # consequently in the zip archive).
187
- def size
188
- @entry_set.size
189
- end
271
+ error = EntryNumberMismatchError.new(size, size_in_bytes)
272
+ raise error if Zip.validate_declared_number_of_entries
190
273
 
191
- def self.read_from_stream(io) #:nodoc:
192
- cdir = new
193
- cdir.read_from_stream(io)
194
- return cdir
195
- rescue Error
196
- return nil
197
- end
198
-
199
- def ==(other) #:nodoc:
200
- return false unless other.kind_of?(CentralDirectory)
201
- @entry_set.entries.sort == other.entries.sort && comment == other.comment
274
+ warn error.message
202
275
  end
203
276
  end
204
277
  end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
2
- class Compressor #:nodoc:all
4
+ class Compressor # :nodoc:all
3
5
  def finish; end
4
6
  end
5
7
  end
data/lib/zip/constants.rb CHANGED
@@ -1,4 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
4
+ # :stopdoc:
5
+
2
6
  RUNNING_ON_WINDOWS = RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/i
3
7
 
4
8
  CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50
@@ -11,6 +15,8 @@ module Zip
11
15
  VERSION_NEEDED_TO_EXTRACT = 20
12
16
  VERSION_NEEDED_TO_EXTRACT_ZIP64 = 45
13
17
 
18
+ SPLIT_FILE_SIGNATURE = 0x08074b50
19
+
14
20
  FILE_TYPE_FILE = 0o10
15
21
  FILE_TYPE_DIR = 0o04
16
22
  FILE_TYPE_SYMLINK = 0o12
@@ -38,26 +44,80 @@ module Zip
38
44
  FSTYPE_ATHEOS = 30
39
45
 
40
46
  FSTYPES = {
41
- FSTYPE_FAT => 'FAT'.freeze,
42
- FSTYPE_AMIGA => 'Amiga'.freeze,
43
- FSTYPE_VMS => 'VMS (Vax or Alpha AXP)'.freeze,
44
- FSTYPE_UNIX => 'Unix'.freeze,
45
- FSTYPE_VM_CMS => 'VM/CMS'.freeze,
46
- FSTYPE_ATARI => 'Atari ST'.freeze,
47
- FSTYPE_HPFS => 'OS/2 or NT HPFS'.freeze,
48
- FSTYPE_MAC => 'Macintosh'.freeze,
49
- FSTYPE_Z_SYSTEM => 'Z-System'.freeze,
50
- FSTYPE_CPM => 'CP/M'.freeze,
51
- FSTYPE_TOPS20 => 'TOPS-20'.freeze,
52
- FSTYPE_NTFS => 'NTFS'.freeze,
53
- FSTYPE_QDOS => 'SMS/QDOS'.freeze,
54
- FSTYPE_ACORN => 'Acorn RISC OS'.freeze,
55
- FSTYPE_VFAT => 'Win32 VFAT'.freeze,
56
- FSTYPE_MVS => 'MVS'.freeze,
57
- FSTYPE_BEOS => 'BeOS'.freeze,
58
- FSTYPE_TANDEM => 'Tandem NSK'.freeze,
59
- FSTYPE_THEOS => 'Theos'.freeze,
60
- FSTYPE_MAC_OSX => 'Mac OS/X (Darwin)'.freeze,
61
- FSTYPE_ATHEOS => 'AtheOS'.freeze
47
+ FSTYPE_FAT => 'FAT',
48
+ FSTYPE_AMIGA => 'Amiga',
49
+ FSTYPE_VMS => 'VMS (Vax or Alpha AXP)',
50
+ FSTYPE_UNIX => 'Unix',
51
+ FSTYPE_VM_CMS => 'VM/CMS',
52
+ FSTYPE_ATARI => 'Atari ST',
53
+ FSTYPE_HPFS => 'OS/2 or NT HPFS',
54
+ FSTYPE_MAC => 'Macintosh',
55
+ FSTYPE_Z_SYSTEM => 'Z-System',
56
+ FSTYPE_CPM => 'CP/M',
57
+ FSTYPE_TOPS20 => 'TOPS-20',
58
+ FSTYPE_NTFS => 'NTFS',
59
+ FSTYPE_QDOS => 'SMS/QDOS',
60
+ FSTYPE_ACORN => 'Acorn RISC OS',
61
+ FSTYPE_VFAT => 'Win32 VFAT',
62
+ FSTYPE_MVS => 'MVS',
63
+ FSTYPE_BEOS => 'BeOS',
64
+ FSTYPE_TANDEM => 'Tandem NSK',
65
+ FSTYPE_THEOS => 'Theos',
66
+ FSTYPE_MAC_OSX => 'Mac OS/X (Darwin)',
67
+ FSTYPE_ATHEOS => 'AtheOS'
62
68
  }.freeze
69
+
70
+ COMPRESSION_METHOD_STORE = 0
71
+ COMPRESSION_METHOD_SHRINK = 1
72
+ COMPRESSION_METHOD_REDUCE_1 = 2
73
+ COMPRESSION_METHOD_REDUCE_2 = 3
74
+ COMPRESSION_METHOD_REDUCE_3 = 4
75
+ COMPRESSION_METHOD_REDUCE_4 = 5
76
+ COMPRESSION_METHOD_IMPLODE = 6
77
+ # RESERVED = 7
78
+ COMPRESSION_METHOD_DEFLATE = 8
79
+ COMPRESSION_METHOD_DEFLATE_64 = 9
80
+ COMPRESSION_METHOD_PKWARE_DCLI = 10
81
+ # RESERVED = 11
82
+ COMPRESSION_METHOD_BZIP2 = 12
83
+ # RESERVED = 13
84
+ COMPRESSION_METHOD_LZMA = 14
85
+ # RESERVED = 15
86
+ COMPRESSION_METHOD_IBM_CMPSC = 16
87
+ # RESERVED = 17
88
+ COMPRESSION_METHOD_IBM_TERSE = 18
89
+ COMPRESSION_METHOD_IBM_LZ77 = 19
90
+ COMPRESSION_METHOD_JPEG = 96
91
+ COMPRESSION_METHOD_WAVPACK = 97
92
+ COMPRESSION_METHOD_PPMD = 98
93
+ COMPRESSION_METHOD_AES = 99
94
+
95
+ COMPRESSION_METHODS = {
96
+ COMPRESSION_METHOD_STORE => 'Store (no compression)',
97
+ COMPRESSION_METHOD_SHRINK => 'Shrink',
98
+ COMPRESSION_METHOD_REDUCE_1 => 'Reduce with compression factor 1',
99
+ COMPRESSION_METHOD_REDUCE_2 => 'Reduce with compression factor 2',
100
+ COMPRESSION_METHOD_REDUCE_3 => 'Reduce with compression factor 3',
101
+ COMPRESSION_METHOD_REDUCE_4 => 'Reduce with compression factor 4',
102
+ COMPRESSION_METHOD_IMPLODE => 'Implode',
103
+ # RESERVED = 7
104
+ COMPRESSION_METHOD_DEFLATE => 'Deflate',
105
+ COMPRESSION_METHOD_DEFLATE_64 => 'Deflate64(tm)',
106
+ COMPRESSION_METHOD_PKWARE_DCLI => 'PKWARE Data Compression Library Imploding (old IBM TERSE)',
107
+ # RESERVED = 11
108
+ COMPRESSION_METHOD_BZIP2 => 'BZIP2',
109
+ # RESERVED = 13
110
+ COMPRESSION_METHOD_LZMA => 'LZMA',
111
+ # RESERVED = 15
112
+ COMPRESSION_METHOD_IBM_CMPSC => 'IBM z/OS CMPSC Compression',
113
+ # RESERVED = 17
114
+ COMPRESSION_METHOD_IBM_TERSE => 'IBM TERSE (new)',
115
+ COMPRESSION_METHOD_IBM_LZ77 => 'IBM LZ77 z Architecture (PFS)',
116
+ COMPRESSION_METHOD_JPEG => 'JPEG variant',
117
+ COMPRESSION_METHOD_WAVPACK => 'WavPack compressed data',
118
+ COMPRESSION_METHOD_PPMD => 'PPMd version I, Rev 1',
119
+ COMPRESSION_METHOD_AES => 'AES encryption'
120
+ }.freeze
121
+
122
+ # :startdoc:
63
123
  end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module Zip
6
+ module AESEncryption # :nodoc:
7
+ VERIFIER_LENGTH = 2
8
+ BLOCK_SIZE = 16
9
+ AUTHENTICATION_CODE_LENGTH = 10
10
+
11
+ VERSION_AE_1 = 0x01
12
+ VERSION_AE_2 = 0x02
13
+
14
+ VERSIONS = [
15
+ VERSION_AE_1,
16
+ VERSION_AE_2
17
+ ].freeze
18
+
19
+ STRENGTH_128_BIT = 0x01
20
+ STRENGTH_192_BIT = 0x02
21
+ STRENGTH_256_BIT = 0x03
22
+
23
+ STRENGTHS = [
24
+ STRENGTH_128_BIT,
25
+ STRENGTH_192_BIT,
26
+ STRENGTH_256_BIT
27
+ ].freeze
28
+
29
+ BITS = {
30
+ STRENGTH_128_BIT => 128,
31
+ STRENGTH_192_BIT => 192,
32
+ STRENGTH_256_BIT => 256
33
+ }.freeze
34
+
35
+ KEY_LENGTHS = {
36
+ STRENGTH_128_BIT => 16,
37
+ STRENGTH_192_BIT => 24,
38
+ STRENGTH_256_BIT => 32
39
+ }.freeze
40
+
41
+ SALT_LENGTHS = {
42
+ STRENGTH_128_BIT => 8,
43
+ STRENGTH_192_BIT => 12,
44
+ STRENGTH_256_BIT => 16
45
+ }.freeze
46
+
47
+ def initialize(password, strength)
48
+ @password = password
49
+ @strength = strength
50
+ @bits = BITS[@strength]
51
+ @key_length = KEY_LENGTHS[@strength]
52
+ @salt_length = SALT_LENGTHS[@strength]
53
+ end
54
+
55
+ def header_bytesize
56
+ @salt_length + VERIFIER_LENGTH
57
+ end
58
+
59
+ def gp_flags
60
+ 0x0001
61
+ end
62
+ end
63
+
64
+ class AESDecrypter < Decrypter # :nodoc:
65
+ include AESEncryption
66
+
67
+ def decrypt(encrypted_data)
68
+ @hmac.update(encrypted_data)
69
+
70
+ idx = 0
71
+ decrypted_data = +''
72
+ amount_to_read = encrypted_data.size
73
+
74
+ while amount_to_read.positive?
75
+ @cipher.iv = [@counter + 1].pack('Vx12')
76
+ begin_index = BLOCK_SIZE * idx
77
+ end_index = begin_index + [BLOCK_SIZE, amount_to_read].min
78
+ decrypted_data << @cipher.update(encrypted_data[begin_index...end_index])
79
+ amount_to_read -= BLOCK_SIZE
80
+ @counter += 1
81
+ idx += 1
82
+ end
83
+
84
+ # JRuby requires finalization of the cipher. This is a bug, as noted in
85
+ # jruby/jruby-openssl#182 and jruby/jruby-openssl#183.
86
+ decrypted_data << @cipher.final if defined?(JRUBY_VERSION)
87
+ decrypted_data
88
+ end
89
+
90
+ def reset!(header)
91
+ raise Error, "Unsupported encryption AES-#{@bits}" unless STRENGTHS.include? @strength
92
+
93
+ salt = header[0...@salt_length]
94
+ pwd_verify = header[-VERIFIER_LENGTH..]
95
+ key_material = OpenSSL::KDF.pbkdf2_hmac(
96
+ @password,
97
+ salt: salt,
98
+ iterations: 1000,
99
+ length: (2 * @key_length) + VERIFIER_LENGTH,
100
+ hash: 'sha1'
101
+ )
102
+ enc_key = key_material[0...@key_length]
103
+ enc_hmac_key = key_material[@key_length...(2 * @key_length)]
104
+ enc_pwd_verify = key_material[-VERIFIER_LENGTH..]
105
+
106
+ raise Error, 'Bad password' if enc_pwd_verify != pwd_verify
107
+
108
+ @counter = 0
109
+ @cipher = OpenSSL::Cipher::AES.new(@bits, :CTR)
110
+ @cipher.decrypt
111
+ @cipher.key = enc_key
112
+ @hmac = OpenSSL::HMAC.new(enc_hmac_key, OpenSSL::Digest.new('SHA1'))
113
+ end
114
+
115
+ def check_integrity!(io)
116
+ auth_code = io.read(AUTHENTICATION_CODE_LENGTH)
117
+ raise Error, 'Integrity fault' if @hmac.digest[0...AUTHENTICATION_CODE_LENGTH] != auth_code
118
+ end
119
+ end
120
+ end