rubyzip 2.4.1 → 3.5.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +525 -0
  3. data/LICENSE.md +24 -0
  4. data/README.md +222 -43
  5. data/Rakefile +15 -13
  6. data/lib/rubyzip.rb +3 -0
  7. data/lib/zip/central_directory.rb +194 -125
  8. data/lib/zip/compressor.rb +3 -1
  9. data/lib/zip/constants.rb +29 -21
  10. data/lib/zip/crypto/aes_encryption.rb +120 -0
  11. data/lib/zip/crypto/decrypted_io.rb +27 -15
  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 +13 -7
  15. data/lib/zip/decompressor.rb +4 -3
  16. data/lib/zip/deflater.rb +12 -8
  17. data/lib/zip/dirtyable.rb +32 -0
  18. data/lib/zip/dos_time.rb +45 -5
  19. data/lib/zip/entry.rb +396 -269
  20. data/lib/zip/entry_set.rb +11 -9
  21. data/lib/zip/errors.rb +152 -16
  22. data/lib/zip/extra_field/aes.rb +50 -0
  23. data/lib/zip/extra_field/generic.rb +10 -11
  24. data/lib/zip/extra_field/ntfs.rb +6 -4
  25. data/lib/zip/extra_field/old_unix.rb +3 -1
  26. data/lib/zip/extra_field/universal_time.rb +3 -1
  27. data/lib/zip/extra_field/unix.rb +5 -3
  28. data/lib/zip/extra_field/unknown.rb +35 -0
  29. data/lib/zip/extra_field/zip64.rb +19 -5
  30. data/lib/zip/extra_field.rb +25 -23
  31. data/lib/zip/file.rb +174 -267
  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 +27 -596
  39. data/lib/zip/inflater.rb +13 -10
  40. data/lib/zip/input_stream.rb +99 -60
  41. data/lib/zip/ioextras/abstract_input_stream.rb +146 -58
  42. data/lib/zip/ioextras/abstract_output_stream.rb +13 -3
  43. data/lib/zip/ioextras.rb +7 -7
  44. data/lib/zip/null_compressor.rb +3 -1
  45. data/lib/zip/null_decompressor.rb +6 -3
  46. data/lib/zip/null_input_stream.rb +3 -1
  47. data/lib/zip/output_stream.rb +60 -57
  48. data/lib/zip/pass_thru_compressor.rb +3 -1
  49. data/lib/zip/pass_thru_decompressor.rb +12 -9
  50. data/lib/zip/streamable_directory.rb +3 -1
  51. data/lib/zip/streamable_stream.rb +4 -1
  52. data/lib/zip/version.rb +4 -1
  53. data/lib/zip.rb +28 -23
  54. data/rubyzip.gemspec +39 -0
  55. data/samples/example.rb +8 -3
  56. data/samples/example_filesystem.rb +3 -2
  57. data/samples/example_recursive.rb +3 -1
  58. data/samples/gtk_ruby_zip.rb +5 -3
  59. data/samples/qtzip.rb +7 -6
  60. data/samples/write_simple.rb +2 -1
  61. data/samples/zipfind.rb +1 -0
  62. metadata +87 -52
  63. data/TODO +0 -15
  64. data/lib/zip/extra_field/zip64_placeholder.rb +0 -15
@@ -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,
@@ -70,11 +102,9 @@ module Zip
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,127 +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
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
144
224
 
145
- buf = buf.slice!((sig_index + 4)..(buf.bytesize))
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))
146
227
 
147
- def buf.read(count)
148
- 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'
149
232
  end
150
233
 
151
- buf
152
- 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
153
253
 
154
- def zip64_file?(buf)
155
- 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)
156
256
  end
157
257
 
158
- def start_buf(io)
258
+ def eocd_data(io)
159
259
  begin
160
- io.seek(-MAX_END_OF_CDS_SIZE, IO::SEEK_END)
260
+ io.seek(-MAX_END_OF_CD_SIZE, IO::SEEK_END)
161
261
  rescue Errno::EINVAL
162
262
  io.seek(0, IO::SEEK_SET)
163
263
  end
164
- io.read
165
- end
166
264
 
167
- def get_64_e_o_c_d(buf) #:nodoc:
168
- zip_64_start = buf.rindex([ZIP64_END_OF_CDS].pack('V'))
169
- raise Error, 'Zip64 end of central directory signature not found' unless zip_64_start
170
-
171
- zip_64_locator = buf.rindex([ZIP64_EOCD_LOCATOR].pack('V'))
172
- raise Error, 'Zip64 end of central directory signature locator not found' unless zip_64_locator
173
-
174
- buf = buf.slice!((zip_64_start + 4)..zip_64_locator)
175
-
176
- def buf.read(count)
177
- slice!(0, count)
178
- end
179
-
180
- buf
181
- end
182
-
183
- # For iterating over the entries.
184
- def each(&a_proc)
185
- @entry_set.each(&a_proc)
186
- end
187
-
188
- # Returns the number of entries in the central directory (and
189
- # consequently in the zip archive).
190
- def size
191
- @entry_set.size
265
+ [io.tell, io.read]
192
266
  end
193
267
 
194
- def self.read_from_stream(io) #:nodoc:
195
- cdir = new
196
- cdir.read_from_stream(io)
197
- cdir
198
- rescue Error
199
- nil
200
- end
268
+ def validate_size!(size, size_in_bytes)
269
+ return if size * CDIR_ENTRY_STATIC_HEADER_LENGTH <= size_in_bytes
201
270
 
202
- def ==(other) #:nodoc:
203
- return false unless other.kind_of?(CentralDirectory)
271
+ error = EntryNumberMismatchError.new(size, size_in_bytes)
272
+ raise error if Zip.validate_declared_number_of_entries
204
273
 
205
- @entry_set.entries.sort == other.entries.sort && comment == other.comment
274
+ warn error.message
206
275
  end
207
276
  end
208
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,27 +44,27 @@ 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
63
69
 
64
70
  COMPRESSION_METHOD_STORE = 0
@@ -112,4 +118,6 @@ module Zip
112
118
  COMPRESSION_METHOD_PPMD => 'PPMd version I, Rev 1',
113
119
  COMPRESSION_METHOD_AES => 'AES encryption'
114
120
  }.freeze
121
+
122
+ # :startdoc:
115
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
@@ -1,40 +1,52 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
2
- class DecryptedIo #:nodoc:all
4
+ class DecryptedIo # :nodoc:all
3
5
  CHUNK_SIZE = 32_768
4
6
 
5
- def initialize(io, decrypter)
7
+ def initialize(io, decrypter, compressed_size)
6
8
  @io = io
7
9
  @decrypter = decrypter
10
+ @bytes_remaining = compressed_size
11
+ @buffer = +''.b
12
+ # Ensures that integrity is only checked once,
13
+ # even if read is called multiple times after the input has finished.
14
+ @integrity_verified = false
8
15
  end
9
16
 
10
- def read(length = nil, outbuf = +'')
11
- return (length.nil? || length.zero? ? '' : nil) if eof
17
+ def read(maxlen = nil)
18
+ return (maxlen.nil? || maxlen.zero? ? '' : nil) if eof?
12
19
 
13
- while length.nil? || (buffer.bytesize < length)
20
+ while maxlen.nil? || (@buffer.bytesize < maxlen)
14
21
  break if input_finished?
15
22
 
16
- buffer << produce_input
23
+ @buffer << produce_input
24
+ end
25
+
26
+ if input_finished? && !@integrity_verified
27
+ @decrypter.check_integrity!(@io)
28
+ @integrity_verified = true
17
29
  end
18
30
 
19
- outbuf.replace(buffer.slice!(0...(length || output_buffer.bytesize)))
31
+ @buffer.slice!(0...(maxlen || @buffer.bytesize))
20
32
  end
21
33
 
22
34
  private
23
35
 
24
- def eof
25
- buffer.empty? && input_finished?
26
- end
27
-
28
- def buffer
29
- @buffer ||= +''
36
+ def eof?
37
+ @buffer.empty? && input_finished?
30
38
  end
31
39
 
32
40
  def input_finished?
33
- @io.eof
41
+ !@bytes_remaining.positive?
34
42
  end
35
43
 
36
44
  def produce_input
37
- @decrypter.decrypt(@io.read(CHUNK_SIZE))
45
+ chunk_size = [@bytes_remaining, CHUNK_SIZE].min
46
+ return '' unless chunk_size.positive?
47
+
48
+ @bytes_remaining -= chunk_size
49
+ @decrypter.decrypt(@io.read(chunk_size))
38
50
  end
39
51
  end
40
52
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
2
- class Encrypter #:nodoc:all
4
+ class Encrypter # :nodoc:all
3
5
  end
4
6
 
5
- class Decrypter
7
+ class Decrypter # :nodoc:all
6
8
  end
7
9
  end
8
10