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.
- checksums.yaml +4 -4
- data/Changelog.md +485 -0
- data/LICENSE.md +24 -0
- data/README.md +180 -40
- data/Rakefile +15 -13
- data/lib/zip/central_directory.rb +179 -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 +20 -14
- data/lib/zip/crypto/encryption.rb +4 -2
- data/lib/zip/crypto/null_encryption.rb +5 -13
- data/lib/zip/crypto/traditional_encryption.rb +10 -6
- 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 +391 -264
- data/lib/zip/entry_set.rb +11 -9
- data/lib/zip/errors.rb +136 -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 +11 -8
- data/lib/zip/input_stream.rb +76 -57
- data/lib/zip/ioextras/abstract_input_stream.rb +19 -13
- 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 +8 -5
- 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 +25 -22
- 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 +86 -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,151 @@ 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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
@
|
|
107
|
-
@
|
|
108
|
-
@
|
|
109
|
-
@
|
|
110
|
-
|
|
111
|
-
@
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
|
168
|
-
|
|
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
|
-
|
|
172
|
-
raise Error, '
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
|
203
|
-
|
|
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
|
-
|
|
259
|
+
[io.tell, io.read]
|
|
206
260
|
end
|
|
207
261
|
end
|
|
208
262
|
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,46 @@
|
|
|
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 = +''
|
|
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
|
-
|
|
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
|
-
|
|
35
|
+
!@bytes_remaining.positive?
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
def produce_input
|
|
37
|
-
@
|
|
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
|