rubyzip 2.4.1 → 3.2.2

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +485 -0
  3. data/LICENSE.md +24 -0
  4. data/README.md +180 -40
  5. data/Rakefile +15 -13
  6. data/lib/zip/central_directory.rb +179 -125
  7. data/lib/zip/compressor.rb +3 -1
  8. data/lib/zip/constants.rb +29 -21
  9. data/lib/zip/crypto/aes_encryption.rb +120 -0
  10. data/lib/zip/crypto/decrypted_io.rb +20 -14
  11. data/lib/zip/crypto/encryption.rb +4 -2
  12. data/lib/zip/crypto/null_encryption.rb +5 -13
  13. data/lib/zip/crypto/traditional_encryption.rb +10 -6
  14. data/lib/zip/decompressor.rb +4 -3
  15. data/lib/zip/deflater.rb +12 -8
  16. data/lib/zip/dirtyable.rb +32 -0
  17. data/lib/zip/dos_time.rb +45 -5
  18. data/lib/zip/entry.rb +391 -264
  19. data/lib/zip/entry_set.rb +11 -9
  20. data/lib/zip/errors.rb +136 -16
  21. data/lib/zip/extra_field/aes.rb +50 -0
  22. data/lib/zip/extra_field/generic.rb +10 -11
  23. data/lib/zip/extra_field/ntfs.rb +6 -4
  24. data/lib/zip/extra_field/old_unix.rb +3 -1
  25. data/lib/zip/extra_field/universal_time.rb +3 -1
  26. data/lib/zip/extra_field/unix.rb +5 -3
  27. data/lib/zip/extra_field/unknown.rb +35 -0
  28. data/lib/zip/extra_field/zip64.rb +19 -5
  29. data/lib/zip/extra_field.rb +25 -23
  30. data/lib/zip/file.rb +174 -267
  31. data/lib/zip/file_split.rb +91 -0
  32. data/lib/zip/filesystem/dir.rb +86 -0
  33. data/lib/zip/filesystem/directory_iterator.rb +48 -0
  34. data/lib/zip/filesystem/file.rb +263 -0
  35. data/lib/zip/filesystem/file_stat.rb +110 -0
  36. data/lib/zip/filesystem/zip_file_name_mapper.rb +81 -0
  37. data/lib/zip/filesystem.rb +27 -596
  38. data/lib/zip/inflater.rb +11 -8
  39. data/lib/zip/input_stream.rb +76 -57
  40. data/lib/zip/ioextras/abstract_input_stream.rb +19 -13
  41. data/lib/zip/ioextras/abstract_output_stream.rb +13 -3
  42. data/lib/zip/ioextras.rb +7 -7
  43. data/lib/zip/null_compressor.rb +3 -1
  44. data/lib/zip/null_decompressor.rb +6 -3
  45. data/lib/zip/null_input_stream.rb +3 -1
  46. data/lib/zip/output_stream.rb +60 -57
  47. data/lib/zip/pass_thru_compressor.rb +3 -1
  48. data/lib/zip/pass_thru_decompressor.rb +8 -5
  49. data/lib/zip/streamable_directory.rb +3 -1
  50. data/lib/zip/streamable_stream.rb +4 -1
  51. data/lib/zip/version.rb +4 -1
  52. data/lib/zip.rb +25 -22
  53. data/rubyzip.gemspec +39 -0
  54. data/samples/example.rb +8 -3
  55. data/samples/example_filesystem.rb +3 -2
  56. data/samples/example_recursive.rb +3 -1
  57. data/samples/gtk_ruby_zip.rb +5 -3
  58. data/samples/qtzip.rb +7 -6
  59. data/samples/write_simple.rb +2 -1
  60. data/samples/zipfind.rb +1 -0
  61. metadata +86 -52
  62. data/TODO +0 -15
  63. 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,151 @@ 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?
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
+ zip64_extensible_data_size =
128
+ @size_of_zip64_e_o_c_d - ZIP64_STATIC_EOCD_SIZE + 12
129
+ @zip64_extensible_data = if zip64_extensible_data_size.zero?
130
+ ''
131
+ else
132
+ buffer.slice(
133
+ ZIP64_STATIC_EOCD_SIZE,
134
+ zip64_extensible_data_size
135
+ )
136
+ end
137
+ end
138
+
139
+ def unpack_64_eocd_locator(buffer) # :nodoc:
140
+ _, # ZIP64_EOCD_LOCATOR_SIG. We know we have this at this point.
141
+ _, zip64_eocd_offset, = buffer.unpack('VVQ<V')
142
+
143
+ zip64_eocd_offset
100
144
  end
101
145
 
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?
146
+ # Unpack the EOCD and return a boolean indicating whether this header is
147
+ # complete without needing Zip64 extensions.
148
+ def unpack_e_o_c_d(buffer) # :nodoc: # rubocop:disable Naming/PredicateMethod
149
+ _, # END_OF_CD_SIG. We know we have this at this point.
150
+ @number_of_this_disk,
151
+ @number_of_disk_with_start_of_cdir,
152
+ @total_number_of_entries_in_cdir_on_this_disk,
153
+ @size,
154
+ @size_in_bytes,
155
+ @cdir_offset,
156
+ comment_length = buffer.unpack('VvvvvVVv')
157
+
158
+ @comment = if comment_length.positive?
159
+ buffer.slice(STATIC_EOCD_SIZE, comment_length)
160
+ else
161
+ ''
162
+ end
163
+
164
+ !([@number_of_this_disk, @number_of_disk_with_start_of_cdir,
165
+ @total_number_of_entries_in_cdir_on_this_disk, @size].any?(0xFFFF) ||
166
+ @size_in_bytes == 0xFFFFFFFF || @cdir_offset == 0xFFFFFFFF)
117
167
  end
118
168
 
119
- def read_central_directory_entries(io) #:nodoc:
169
+ def read_central_directory_entries(io) # :nodoc:
170
+ # `StringIO` doesn't raise `EINVAL` if you seek beyond the current end,
171
+ # so we need to catch that *and* query `io#eof?` here.
172
+ eof = false
120
173
  begin
121
174
  io.seek(@cdir_offset, IO::SEEK_SET)
122
175
  rescue Errno::EINVAL
123
- raise Error, 'Zip consistency problem while reading central directory entry'
176
+ eof = true
124
177
  end
178
+ raise Error, 'Zip consistency problem while reading central directory entry' if eof || io.eof?
179
+
125
180
  @entry_set = EntrySet.new
126
181
  @size.times do
127
- @entry_set << Entry.read_c_dir_entry(io)
128
- end
129
- end
182
+ entry = Entry.read_c_dir_entry(io)
183
+ next unless entry
184
+
185
+ offset = if entry.zip64?
186
+ entry.extra[:zip64].relative_header_offset
187
+ else
188
+ entry.local_header_offset
189
+ end
190
+
191
+ unless offset.nil?
192
+ io_save = io.tell
193
+ io.seek(offset, IO::SEEK_SET)
194
+ entry.read_extra_field(read_local_extra_field(io), local: true)
195
+ io.seek(io_save, IO::SEEK_SET)
196
+ end
130
197
 
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)
198
+ @entry_set << entry
137
199
  end
138
- read_central_directory_entries(io)
139
200
  end
140
201
 
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
-
145
- buf = buf.slice!((sig_index + 4)..(buf.bytesize))
202
+ def read_local_extra_field(io)
203
+ buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
204
+ return '' unless buf.bytesize == ::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH
146
205
 
147
- def buf.read(count)
148
- slice!(0, count)
149
- end
150
-
151
- buf
152
- end
206
+ head, _, _, _, _, _, _, _, _, _, n_len, e_len = buf.unpack('VCCvvvvVVVvv')
207
+ return '' unless head == ::Zip::LOCAL_ENTRY_SIGNATURE
153
208
 
154
- def zip64_file?(buf)
155
- buf.rindex([ZIP64_END_OF_CDS].pack('V')) && buf.rindex([ZIP64_EOCD_LOCATOR].pack('V'))
156
- end
157
-
158
- def start_buf(io)
159
- begin
160
- io.seek(-MAX_END_OF_CDS_SIZE, IO::SEEK_END)
161
- rescue Errno::EINVAL
162
- io.seek(0, IO::SEEK_SET)
163
- end
164
- io.read
209
+ io.seek(n_len, IO::SEEK_CUR) # Skip over the entry name.
210
+ io.read(e_len)
165
211
  end
166
212
 
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
213
+ def read_eocds(io) # :nodoc:
214
+ base_location, data = eocd_data(io)
170
215
 
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
216
+ eocd_location = data.rindex([END_OF_CD_SIG].pack('V'))
217
+ raise Error, 'Zip end of central directory signature not found' unless eocd_location
173
218
 
174
- buf = buf.slice!((zip_64_start + 4)..zip_64_locator)
219
+ # Parse the EOCD and return if it is complete without Zip64 extensions.
220
+ return if unpack_e_o_c_d(data.slice(eocd_location..-1))
175
221
 
176
- def buf.read(count)
177
- slice!(0, count)
222
+ # Need to read in the Zip64 EOCD locator and then the Zip64 EOCD.
223
+ zip64_eocd_locator = data.rindex([ZIP64_EOCD_LOCATOR_SIG].pack('V'), eocd_location)
224
+ unless zip64_eocd_locator
225
+ raise Error, 'Zip64 end of central directory locator signature expected but not found'
178
226
  end
179
227
 
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
192
- end
228
+ # Do we already have the Zip64 EOCD in the data we've read?
229
+ zip64_eocd_location = data.rindex([ZIP64_END_OF_CD_SIG].pack('V'), zip64_eocd_locator)
230
+
231
+ zip64_eocd_data =
232
+ if zip64_eocd_location
233
+ # Yes.
234
+ data.slice(zip64_eocd_location..zip64_eocd_locator)
235
+ else
236
+ # No. Read its location from the locator and then read it in.
237
+ zip64_eocd_location = unpack_64_eocd_locator(
238
+ data.slice(zip64_eocd_locator..eocd_location)
239
+ )
240
+ unless zip64_eocd_location
241
+ raise Error, 'Zip64 end of central directory signature not found'
242
+ end
243
+
244
+ io.seek(zip64_eocd_location, IO::SEEK_SET)
245
+ io.read(base_location + zip64_eocd_locator - zip64_eocd_location)
246
+ end
193
247
 
194
- def self.read_from_stream(io) #:nodoc:
195
- cdir = new
196
- cdir.read_from_stream(io)
197
- cdir
198
- rescue Error
199
- nil
248
+ # Finally, unpack the Zip64 EOCD.
249
+ unpack_64_e_o_c_d(zip64_eocd_data)
200
250
  end
201
251
 
202
- def ==(other) #:nodoc:
203
- return false unless other.kind_of?(CentralDirectory)
252
+ def eocd_data(io)
253
+ begin
254
+ io.seek(-MAX_END_OF_CD_SIZE, IO::SEEK_END)
255
+ rescue Errno::EINVAL
256
+ io.seek(0, IO::SEEK_SET)
257
+ end
204
258
 
205
- @entry_set.entries.sort == other.entries.sort && comment == other.comment
259
+ [io.tell, io.read]
206
260
  end
207
261
  end
208
262
  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,46 @@
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 = +''
8
12
  end
9
13
 
10
14
  def read(length = nil, outbuf = +'')
11
- return (length.nil? || length.zero? ? '' : nil) if eof
15
+ return (length.nil? || length.zero? ? '' : nil) if eof?
12
16
 
13
- while length.nil? || (buffer.bytesize < length)
17
+ while length.nil? || (@buffer.bytesize < length)
14
18
  break if input_finished?
15
19
 
16
- buffer << produce_input
20
+ @buffer << produce_input
17
21
  end
18
22
 
19
- outbuf.replace(buffer.slice!(0...(length || output_buffer.bytesize)))
23
+ @decrypter.check_integrity!(@io) if input_finished?
24
+
25
+ outbuf.replace(@buffer.slice!(0...(length || @buffer.bytesize)))
20
26
  end
21
27
 
22
28
  private
23
29
 
24
- def eof
25
- buffer.empty? && input_finished?
26
- end
27
-
28
- def buffer
29
- @buffer ||= +''
30
+ def eof?
31
+ @buffer.empty? && input_finished?
30
32
  end
31
33
 
32
34
  def input_finished?
33
- @io.eof
35
+ !@bytes_remaining.positive?
34
36
  end
35
37
 
36
38
  def produce_input
37
- @decrypter.decrypt(@io.read(CHUNK_SIZE))
39
+ chunk_size = [@bytes_remaining, CHUNK_SIZE].min
40
+ return '' unless chunk_size.positive?
41
+
42
+ @bytes_remaining -= chunk_size
43
+ @decrypter.decrypt(@io.read(chunk_size))
38
44
  end
39
45
  end
40
46
  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