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.
- checksums.yaml +4 -4
- data/Changelog.md +525 -0
- data/LICENSE.md +24 -0
- data/README.md +222 -43
- data/Rakefile +15 -13
- data/lib/rubyzip.rb +3 -0
- data/lib/zip/central_directory.rb +194 -125
- data/lib/zip/compressor.rb +3 -1
- data/lib/zip/constants.rb +29 -21
- data/lib/zip/crypto/aes_encryption.rb +120 -0
- data/lib/zip/crypto/decrypted_io.rb +27 -15
- data/lib/zip/crypto/encryption.rb +4 -2
- data/lib/zip/crypto/null_encryption.rb +5 -13
- data/lib/zip/crypto/traditional_encryption.rb +13 -7
- data/lib/zip/decompressor.rb +4 -3
- data/lib/zip/deflater.rb +12 -8
- data/lib/zip/dirtyable.rb +32 -0
- data/lib/zip/dos_time.rb +45 -5
- data/lib/zip/entry.rb +396 -269
- data/lib/zip/entry_set.rb +11 -9
- data/lib/zip/errors.rb +152 -16
- data/lib/zip/extra_field/aes.rb +50 -0
- data/lib/zip/extra_field/generic.rb +10 -11
- data/lib/zip/extra_field/ntfs.rb +6 -4
- data/lib/zip/extra_field/old_unix.rb +3 -1
- data/lib/zip/extra_field/universal_time.rb +3 -1
- data/lib/zip/extra_field/unix.rb +5 -3
- data/lib/zip/extra_field/unknown.rb +35 -0
- data/lib/zip/extra_field/zip64.rb +19 -5
- data/lib/zip/extra_field.rb +25 -23
- data/lib/zip/file.rb +174 -267
- data/lib/zip/file_split.rb +91 -0
- data/lib/zip/filesystem/dir.rb +86 -0
- data/lib/zip/filesystem/directory_iterator.rb +48 -0
- data/lib/zip/filesystem/file.rb +263 -0
- data/lib/zip/filesystem/file_stat.rb +110 -0
- data/lib/zip/filesystem/zip_file_name_mapper.rb +81 -0
- data/lib/zip/filesystem.rb +27 -596
- data/lib/zip/inflater.rb +13 -10
- data/lib/zip/input_stream.rb +99 -60
- data/lib/zip/ioextras/abstract_input_stream.rb +146 -58
- data/lib/zip/ioextras/abstract_output_stream.rb +13 -3
- data/lib/zip/ioextras.rb +7 -7
- data/lib/zip/null_compressor.rb +3 -1
- data/lib/zip/null_decompressor.rb +6 -3
- data/lib/zip/null_input_stream.rb +3 -1
- data/lib/zip/output_stream.rb +60 -57
- data/lib/zip/pass_thru_compressor.rb +3 -1
- data/lib/zip/pass_thru_decompressor.rb +12 -9
- data/lib/zip/streamable_directory.rb +3 -1
- data/lib/zip/streamable_stream.rb +4 -1
- data/lib/zip/version.rb +4 -1
- data/lib/zip.rb +28 -23
- data/rubyzip.gemspec +39 -0
- data/samples/example.rb +8 -3
- data/samples/example_filesystem.rb +3 -2
- data/samples/example_recursive.rb +3 -1
- data/samples/gtk_ruby_zip.rb +5 -3
- data/samples/qtzip.rb +7 -6
- data/samples/write_simple.rb +2 -1
- data/samples/zipfind.rb +1 -0
- metadata +87 -52
- data/TODO +0 -15
- 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
|
-
|
|
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
|
-
|
|
23
|
+
attr_accessor :comment
|
|
12
24
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 = '')
|
|
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
|
|
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
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
@
|
|
94
|
-
|
|
95
|
-
@
|
|
96
|
-
|
|
97
|
-
@
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
132
|
-
buf =
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
155
|
-
|
|
254
|
+
# Finally, unpack the Zip64 EOCD.
|
|
255
|
+
unpack_64_e_o_c_d(zip64_eocd_data)
|
|
156
256
|
end
|
|
157
257
|
|
|
158
|
-
def
|
|
258
|
+
def eocd_data(io)
|
|
159
259
|
begin
|
|
160
|
-
io.seek(-
|
|
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
|
-
|
|
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
|
|
195
|
-
|
|
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
|
-
|
|
203
|
-
|
|
271
|
+
error = EntryNumberMismatchError.new(size, size_in_bytes)
|
|
272
|
+
raise error if Zip.validate_declared_number_of_entries
|
|
204
273
|
|
|
205
|
-
|
|
274
|
+
warn error.message
|
|
206
275
|
end
|
|
207
276
|
end
|
|
208
277
|
end
|
data/lib/zip/compressor.rb
CHANGED
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'
|
|
42
|
-
FSTYPE_AMIGA => 'Amiga'
|
|
43
|
-
FSTYPE_VMS => 'VMS (Vax or Alpha AXP)'
|
|
44
|
-
FSTYPE_UNIX => 'Unix'
|
|
45
|
-
FSTYPE_VM_CMS => 'VM/CMS'
|
|
46
|
-
FSTYPE_ATARI => 'Atari ST'
|
|
47
|
-
FSTYPE_HPFS => 'OS/2 or NT HPFS'
|
|
48
|
-
FSTYPE_MAC => 'Macintosh'
|
|
49
|
-
FSTYPE_Z_SYSTEM => 'Z-System'
|
|
50
|
-
FSTYPE_CPM => 'CP/M'
|
|
51
|
-
FSTYPE_TOPS20 => 'TOPS-20'
|
|
52
|
-
FSTYPE_NTFS => 'NTFS'
|
|
53
|
-
FSTYPE_QDOS => 'SMS/QDOS'
|
|
54
|
-
FSTYPE_ACORN => 'Acorn RISC OS'
|
|
55
|
-
FSTYPE_VFAT => 'Win32 VFAT'
|
|
56
|
-
FSTYPE_MVS => 'MVS'
|
|
57
|
-
FSTYPE_BEOS => 'BeOS'
|
|
58
|
-
FSTYPE_TANDEM => 'Tandem NSK'
|
|
59
|
-
FSTYPE_THEOS => 'Theos'
|
|
60
|
-
FSTYPE_MAC_OSX => 'Mac OS/X (Darwin)'
|
|
61
|
-
FSTYPE_ATHEOS => 'AtheOS'
|
|
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
|
|
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(
|
|
11
|
-
return (
|
|
17
|
+
def read(maxlen = nil)
|
|
18
|
+
return (maxlen.nil? || maxlen.zero? ? '' : nil) if eof?
|
|
12
19
|
|
|
13
|
-
while
|
|
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
|
-
|
|
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
|
-
|
|
41
|
+
!@bytes_remaining.positive?
|
|
34
42
|
end
|
|
35
43
|
|
|
36
44
|
def produce_input
|
|
37
|
-
@
|
|
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
|