rubyzip 1.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Changelog.md +516 -0
- data/LICENSE.md +24 -0
- data/README.md +213 -51
- data/Rakefile +17 -12
- data/lib/rubyzip.rb +3 -0
- data/lib/zip/central_directory.rb +196 -123
- data/lib/zip/compressor.rb +3 -1
- data/lib/zip/constants.rb +81 -21
- data/lib/zip/crypto/aes_encryption.rb +120 -0
- data/lib/zip/crypto/decrypted_io.rb +46 -0
- data/lib/zip/crypto/encryption.rb +4 -2
- data/lib/zip/crypto/null_encryption.rb +5 -13
- data/lib/zip/crypto/traditional_encryption.rb +22 -16
- data/lib/zip/decompressor.rb +21 -2
- data/lib/zip/deflater.rb +12 -8
- data/lib/zip/dirtyable.rb +32 -0
- data/lib/zip/dos_time.rb +65 -13
- data/lib/zip/entry.rb +433 -249
- data/lib/zip/entry_set.rb +13 -9
- data/lib/zip/errors.rb +152 -15
- data/lib/zip/extra_field/aes.rb +50 -0
- data/lib/zip/extra_field/generic.rb +18 -18
- data/lib/zip/extra_field/ntfs.rb +10 -4
- data/lib/zip/extra_field/old_unix.rb +6 -2
- data/lib/zip/extra_field/universal_time.rb +45 -13
- data/lib/zip/extra_field/unix.rb +7 -3
- data/lib/zip/extra_field/unknown.rb +35 -0
- data/lib/zip/extra_field/zip64.rb +23 -7
- data/lib/zip/extra_field.rb +31 -27
- data/lib/zip/file.rb +225 -241
- 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 +32 -585
- data/lib/zip/inflater.rb +28 -37
- data/lib/zip/input_stream.rb +113 -54
- data/lib/zip/ioextras/abstract_input_stream.rb +154 -55
- data/lib/zip/ioextras/abstract_output_stream.rb +13 -3
- data/lib/zip/ioextras.rb +8 -8
- data/lib/zip/null_compressor.rb +3 -1
- data/lib/zip/null_decompressor.rb +7 -12
- data/lib/zip/null_input_stream.rb +3 -1
- data/lib/zip/output_stream.rb +69 -50
- data/lib/zip/pass_thru_compressor.rb +5 -3
- data/lib/zip/pass_thru_decompressor.rb +17 -23
- data/lib/zip/streamable_directory.rb +6 -4
- data/lib/zip/streamable_stream.rb +10 -11
- data/lib/zip/version.rb +4 -1
- data/lib/zip.rb +32 -7
- data/rubyzip.gemspec +39 -0
- data/samples/example.rb +9 -4
- data/samples/example_filesystem.rb +4 -3
- data/samples/example_recursive.rb +3 -1
- data/samples/gtk_ruby_zip.rb +23 -21
- data/samples/qtzip.rb +13 -12
- data/samples/write_simple.rb +3 -4
- data/samples/zipfind.rb +24 -22
- metadata +85 -177
- data/TODO +0 -15
- data/lib/zip/extra_field/zip64_placeholder.rb +0 -15
- data/test/basic_zip_file_test.rb +0 -60
- data/test/case_sensitivity_test.rb +0 -69
- data/test/central_directory_entry_test.rb +0 -69
- data/test/central_directory_test.rb +0 -100
- data/test/crypto/null_encryption_test.rb +0 -57
- data/test/crypto/traditional_encryption_test.rb +0 -80
- data/test/data/WarnInvalidDate.zip +0 -0
- data/test/data/file1.txt +0 -46
- data/test/data/file1.txt.deflatedData +0 -0
- data/test/data/file2.txt +0 -1504
- data/test/data/globTest/foo/bar/baz/foo.txt +0 -0
- data/test/data/globTest/foo.txt +0 -0
- data/test/data/globTest/food.txt +0 -0
- data/test/data/globTest.zip +0 -0
- data/test/data/gpbit3stored.zip +0 -0
- data/test/data/mimetype +0 -1
- data/test/data/notzippedruby.rb +0 -7
- data/test/data/ntfs.zip +0 -0
- data/test/data/oddExtraField.zip +0 -0
- data/test/data/path_traversal/Makefile +0 -10
- data/test/data/path_traversal/jwilk/README.md +0 -5
- data/test/data/path_traversal/jwilk/absolute1.zip +0 -0
- data/test/data/path_traversal/jwilk/absolute2.zip +0 -0
- data/test/data/path_traversal/jwilk/dirsymlink.zip +0 -0
- data/test/data/path_traversal/jwilk/dirsymlink2a.zip +0 -0
- data/test/data/path_traversal/jwilk/dirsymlink2b.zip +0 -0
- data/test/data/path_traversal/jwilk/relative0.zip +0 -0
- data/test/data/path_traversal/jwilk/relative2.zip +0 -0
- data/test/data/path_traversal/jwilk/symlink.zip +0 -0
- data/test/data/path_traversal/relative1.zip +0 -0
- data/test/data/path_traversal/tilde.zip +0 -0
- data/test/data/path_traversal/tuzovakaoff/README.md +0 -3
- data/test/data/path_traversal/tuzovakaoff/absolutepath.zip +0 -0
- data/test/data/path_traversal/tuzovakaoff/symlink.zip +0 -0
- data/test/data/rubycode.zip +0 -0
- data/test/data/rubycode2.zip +0 -0
- data/test/data/test.xls +0 -0
- data/test/data/testDirectory.bin +0 -0
- data/test/data/zip64-sample.zip +0 -0
- data/test/data/zipWithDirs.zip +0 -0
- data/test/data/zipWithEncryption.zip +0 -0
- data/test/deflater_test.rb +0 -65
- data/test/encryption_test.rb +0 -42
- data/test/entry_set_test.rb +0 -163
- data/test/entry_test.rb +0 -154
- data/test/errors_test.rb +0 -35
- data/test/extra_field_test.rb +0 -76
- data/test/file_extract_directory_test.rb +0 -54
- data/test/file_extract_test.rb +0 -145
- data/test/file_permissions_test.rb +0 -65
- data/test/file_split_test.rb +0 -57
- data/test/file_test.rb +0 -666
- data/test/filesystem/dir_iterator_test.rb +0 -58
- data/test/filesystem/directory_test.rb +0 -139
- data/test/filesystem/file_mutating_test.rb +0 -87
- data/test/filesystem/file_nonmutating_test.rb +0 -508
- data/test/filesystem/file_stat_test.rb +0 -64
- data/test/gentestfiles.rb +0 -126
- data/test/inflater_test.rb +0 -14
- data/test/input_stream_test.rb +0 -182
- data/test/ioextras/abstract_input_stream_test.rb +0 -102
- data/test/ioextras/abstract_output_stream_test.rb +0 -106
- data/test/ioextras/fake_io_test.rb +0 -18
- data/test/local_entry_test.rb +0 -154
- data/test/output_stream_test.rb +0 -128
- data/test/pass_thru_compressor_test.rb +0 -30
- data/test/pass_thru_decompressor_test.rb +0 -14
- data/test/path_traversal_test.rb +0 -141
- data/test/samples/example_recursive_test.rb +0 -37
- data/test/settings_test.rb +0 -95
- data/test/test_helper.rb +0 -234
- data/test/unicode_file_names_and_comments_test.rb +0 -62
- data/test/zip64_full_test.rb +0 -51
- data/test/zip64_support_test.rb +0 -14
|
@@ -1,45 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'forwardable'
|
|
4
|
+
|
|
5
|
+
require_relative 'dirtyable'
|
|
6
|
+
|
|
1
7
|
module Zip
|
|
2
|
-
class CentralDirectory
|
|
3
|
-
|
|
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,
|
|
@@ -65,16 +97,14 @@ module Zip
|
|
|
65
97
|
@entry_set ? @entry_set.size : 0, # number of entries on this disk
|
|
66
98
|
@entry_set ? @entry_set.size : 0, # number of entries total
|
|
67
99
|
cdir_size, # size of central directory
|
|
68
|
-
offset
|
|
100
|
+
offset # offset of start of central directory in its disk
|
|
69
101
|
]
|
|
70
102
|
io << tmp.pack('VQ<vvVVQ<Q<Q<Q<')
|
|
71
103
|
end
|
|
72
104
|
|
|
73
|
-
private :write_64_e_o_c_d
|
|
74
|
-
|
|
75
105
|
def write_64_eocd_locator(io, zip64_eocd_offset)
|
|
76
106
|
tmp = [
|
|
77
|
-
|
|
107
|
+
ZIP64_EOCD_LOCATOR_SIG,
|
|
78
108
|
0, # number of disk containing the start of zip64 eocd record
|
|
79
109
|
zip64_eocd_offset, # offset of the start of zip64 eocd record in its disk
|
|
80
110
|
1 # total number of disks
|
|
@@ -82,123 +112,166 @@ module Zip
|
|
|
82
112
|
io << tmp.pack('VVQ<V')
|
|
83
113
|
end
|
|
84
114
|
|
|
85
|
-
|
|
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
|
-
|
|
144
|
-
|
|
219
|
+
def read_eocds(io) # :nodoc:
|
|
220
|
+
base_location, data = eocd_data(io)
|
|
221
|
+
|
|
222
|
+
eocd_location = data.rindex([END_OF_CD_SIG].pack('V'))
|
|
223
|
+
raise Error, 'Zip end of central directory signature not found' unless eocd_location
|
|
224
|
+
|
|
225
|
+
# Parse the EOCD and return if it is complete without Zip64 extensions.
|
|
226
|
+
return if unpack_e_o_c_d(data.slice(eocd_location..-1))
|
|
145
227
|
|
|
146
|
-
|
|
147
|
-
|
|
228
|
+
# Need to read in the Zip64 EOCD locator and then the Zip64 EOCD.
|
|
229
|
+
zip64_eocd_locator = data.rindex([ZIP64_EOCD_LOCATOR_SIG].pack('V'), eocd_location)
|
|
230
|
+
unless zip64_eocd_locator
|
|
231
|
+
raise Error, 'Zip64 end of central directory locator signature expected but not found'
|
|
148
232
|
end
|
|
149
233
|
|
|
150
|
-
|
|
151
|
-
|
|
234
|
+
# Do we already have the Zip64 EOCD in the data we've read?
|
|
235
|
+
zip64_eocd_location = data.rindex([ZIP64_END_OF_CD_SIG].pack('V'), zip64_eocd_locator)
|
|
236
|
+
|
|
237
|
+
zip64_eocd_data =
|
|
238
|
+
if zip64_eocd_location
|
|
239
|
+
# Yes.
|
|
240
|
+
data.slice(zip64_eocd_location..zip64_eocd_locator)
|
|
241
|
+
else
|
|
242
|
+
# No. Read its location from the locator and then read it in.
|
|
243
|
+
zip64_eocd_location = unpack_64_eocd_locator(
|
|
244
|
+
data.slice(zip64_eocd_locator..eocd_location)
|
|
245
|
+
)
|
|
246
|
+
unless zip64_eocd_location
|
|
247
|
+
raise Error, 'Zip64 end of central directory signature not found'
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
io.seek(zip64_eocd_location, IO::SEEK_SET)
|
|
251
|
+
io.read(base_location + zip64_eocd_locator - zip64_eocd_location)
|
|
252
|
+
end
|
|
152
253
|
|
|
153
|
-
|
|
154
|
-
|
|
254
|
+
# Finally, unpack the Zip64 EOCD.
|
|
255
|
+
unpack_64_e_o_c_d(zip64_eocd_data)
|
|
155
256
|
end
|
|
156
257
|
|
|
157
|
-
def
|
|
258
|
+
def eocd_data(io)
|
|
158
259
|
begin
|
|
159
|
-
io.seek(-
|
|
260
|
+
io.seek(-MAX_END_OF_CD_SIZE, IO::SEEK_END)
|
|
160
261
|
rescue Errno::EINVAL
|
|
161
262
|
io.seek(0, IO::SEEK_SET)
|
|
162
263
|
end
|
|
163
|
-
io.read
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def get_64_e_o_c_d(buf) #:nodoc:
|
|
167
|
-
zip_64_start = buf.rindex([ZIP64_END_OF_CDS].pack('V'))
|
|
168
|
-
raise Error, 'Zip64 end of central directory signature not found' unless zip_64_start
|
|
169
|
-
zip_64_locator = buf.rindex([ZIP64_EOCD_LOCATOR].pack('V'))
|
|
170
|
-
raise Error, 'Zip64 end of central directory signature locator not found' unless zip_64_locator
|
|
171
|
-
buf = buf.slice!((zip_64_start + 4)..zip_64_locator)
|
|
172
|
-
|
|
173
|
-
def buf.read(count)
|
|
174
|
-
slice!(0, count)
|
|
175
|
-
end
|
|
176
264
|
|
|
177
|
-
|
|
265
|
+
[io.tell, io.read]
|
|
178
266
|
end
|
|
179
267
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
@entry_set.each(&proc)
|
|
183
|
-
end
|
|
268
|
+
def validate_size!(size, size_in_bytes)
|
|
269
|
+
return if size * CDIR_ENTRY_STATIC_HEADER_LENGTH <= size_in_bytes
|
|
184
270
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def size
|
|
188
|
-
@entry_set.size
|
|
189
|
-
end
|
|
271
|
+
error = EntryNumberMismatchError.new(size, size_in_bytes)
|
|
272
|
+
raise error if Zip.validate_declared_number_of_entries
|
|
190
273
|
|
|
191
|
-
|
|
192
|
-
cdir = new
|
|
193
|
-
cdir.read_from_stream(io)
|
|
194
|
-
return cdir
|
|
195
|
-
rescue Error
|
|
196
|
-
return nil
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def ==(other) #:nodoc:
|
|
200
|
-
return false unless other.kind_of?(CentralDirectory)
|
|
201
|
-
@entry_set.entries.sort == other.entries.sort && comment == other.comment
|
|
274
|
+
warn error.message
|
|
202
275
|
end
|
|
203
276
|
end
|
|
204
277
|
end
|
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,26 +44,80 @@ 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
|
|
69
|
+
|
|
70
|
+
COMPRESSION_METHOD_STORE = 0
|
|
71
|
+
COMPRESSION_METHOD_SHRINK = 1
|
|
72
|
+
COMPRESSION_METHOD_REDUCE_1 = 2
|
|
73
|
+
COMPRESSION_METHOD_REDUCE_2 = 3
|
|
74
|
+
COMPRESSION_METHOD_REDUCE_3 = 4
|
|
75
|
+
COMPRESSION_METHOD_REDUCE_4 = 5
|
|
76
|
+
COMPRESSION_METHOD_IMPLODE = 6
|
|
77
|
+
# RESERVED = 7
|
|
78
|
+
COMPRESSION_METHOD_DEFLATE = 8
|
|
79
|
+
COMPRESSION_METHOD_DEFLATE_64 = 9
|
|
80
|
+
COMPRESSION_METHOD_PKWARE_DCLI = 10
|
|
81
|
+
# RESERVED = 11
|
|
82
|
+
COMPRESSION_METHOD_BZIP2 = 12
|
|
83
|
+
# RESERVED = 13
|
|
84
|
+
COMPRESSION_METHOD_LZMA = 14
|
|
85
|
+
# RESERVED = 15
|
|
86
|
+
COMPRESSION_METHOD_IBM_CMPSC = 16
|
|
87
|
+
# RESERVED = 17
|
|
88
|
+
COMPRESSION_METHOD_IBM_TERSE = 18
|
|
89
|
+
COMPRESSION_METHOD_IBM_LZ77 = 19
|
|
90
|
+
COMPRESSION_METHOD_JPEG = 96
|
|
91
|
+
COMPRESSION_METHOD_WAVPACK = 97
|
|
92
|
+
COMPRESSION_METHOD_PPMD = 98
|
|
93
|
+
COMPRESSION_METHOD_AES = 99
|
|
94
|
+
|
|
95
|
+
COMPRESSION_METHODS = {
|
|
96
|
+
COMPRESSION_METHOD_STORE => 'Store (no compression)',
|
|
97
|
+
COMPRESSION_METHOD_SHRINK => 'Shrink',
|
|
98
|
+
COMPRESSION_METHOD_REDUCE_1 => 'Reduce with compression factor 1',
|
|
99
|
+
COMPRESSION_METHOD_REDUCE_2 => 'Reduce with compression factor 2',
|
|
100
|
+
COMPRESSION_METHOD_REDUCE_3 => 'Reduce with compression factor 3',
|
|
101
|
+
COMPRESSION_METHOD_REDUCE_4 => 'Reduce with compression factor 4',
|
|
102
|
+
COMPRESSION_METHOD_IMPLODE => 'Implode',
|
|
103
|
+
# RESERVED = 7
|
|
104
|
+
COMPRESSION_METHOD_DEFLATE => 'Deflate',
|
|
105
|
+
COMPRESSION_METHOD_DEFLATE_64 => 'Deflate64(tm)',
|
|
106
|
+
COMPRESSION_METHOD_PKWARE_DCLI => 'PKWARE Data Compression Library Imploding (old IBM TERSE)',
|
|
107
|
+
# RESERVED = 11
|
|
108
|
+
COMPRESSION_METHOD_BZIP2 => 'BZIP2',
|
|
109
|
+
# RESERVED = 13
|
|
110
|
+
COMPRESSION_METHOD_LZMA => 'LZMA',
|
|
111
|
+
# RESERVED = 15
|
|
112
|
+
COMPRESSION_METHOD_IBM_CMPSC => 'IBM z/OS CMPSC Compression',
|
|
113
|
+
# RESERVED = 17
|
|
114
|
+
COMPRESSION_METHOD_IBM_TERSE => 'IBM TERSE (new)',
|
|
115
|
+
COMPRESSION_METHOD_IBM_LZ77 => 'IBM LZ77 z Architecture (PFS)',
|
|
116
|
+
COMPRESSION_METHOD_JPEG => 'JPEG variant',
|
|
117
|
+
COMPRESSION_METHOD_WAVPACK => 'WavPack compressed data',
|
|
118
|
+
COMPRESSION_METHOD_PPMD => 'PPMd version I, Rev 1',
|
|
119
|
+
COMPRESSION_METHOD_AES => 'AES encryption'
|
|
120
|
+
}.freeze
|
|
121
|
+
|
|
122
|
+
# :startdoc:
|
|
63
123
|
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
|
|
5
|
+
module Zip
|
|
6
|
+
module AESEncryption # :nodoc:
|
|
7
|
+
VERIFIER_LENGTH = 2
|
|
8
|
+
BLOCK_SIZE = 16
|
|
9
|
+
AUTHENTICATION_CODE_LENGTH = 10
|
|
10
|
+
|
|
11
|
+
VERSION_AE_1 = 0x01
|
|
12
|
+
VERSION_AE_2 = 0x02
|
|
13
|
+
|
|
14
|
+
VERSIONS = [
|
|
15
|
+
VERSION_AE_1,
|
|
16
|
+
VERSION_AE_2
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
STRENGTH_128_BIT = 0x01
|
|
20
|
+
STRENGTH_192_BIT = 0x02
|
|
21
|
+
STRENGTH_256_BIT = 0x03
|
|
22
|
+
|
|
23
|
+
STRENGTHS = [
|
|
24
|
+
STRENGTH_128_BIT,
|
|
25
|
+
STRENGTH_192_BIT,
|
|
26
|
+
STRENGTH_256_BIT
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
BITS = {
|
|
30
|
+
STRENGTH_128_BIT => 128,
|
|
31
|
+
STRENGTH_192_BIT => 192,
|
|
32
|
+
STRENGTH_256_BIT => 256
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
KEY_LENGTHS = {
|
|
36
|
+
STRENGTH_128_BIT => 16,
|
|
37
|
+
STRENGTH_192_BIT => 24,
|
|
38
|
+
STRENGTH_256_BIT => 32
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
SALT_LENGTHS = {
|
|
42
|
+
STRENGTH_128_BIT => 8,
|
|
43
|
+
STRENGTH_192_BIT => 12,
|
|
44
|
+
STRENGTH_256_BIT => 16
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
def initialize(password, strength)
|
|
48
|
+
@password = password
|
|
49
|
+
@strength = strength
|
|
50
|
+
@bits = BITS[@strength]
|
|
51
|
+
@key_length = KEY_LENGTHS[@strength]
|
|
52
|
+
@salt_length = SALT_LENGTHS[@strength]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def header_bytesize
|
|
56
|
+
@salt_length + VERIFIER_LENGTH
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def gp_flags
|
|
60
|
+
0x0001
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class AESDecrypter < Decrypter # :nodoc:
|
|
65
|
+
include AESEncryption
|
|
66
|
+
|
|
67
|
+
def decrypt(encrypted_data)
|
|
68
|
+
@hmac.update(encrypted_data)
|
|
69
|
+
|
|
70
|
+
idx = 0
|
|
71
|
+
decrypted_data = +''
|
|
72
|
+
amount_to_read = encrypted_data.size
|
|
73
|
+
|
|
74
|
+
while amount_to_read.positive?
|
|
75
|
+
@cipher.iv = [@counter + 1].pack('Vx12')
|
|
76
|
+
begin_index = BLOCK_SIZE * idx
|
|
77
|
+
end_index = begin_index + [BLOCK_SIZE, amount_to_read].min
|
|
78
|
+
decrypted_data << @cipher.update(encrypted_data[begin_index...end_index])
|
|
79
|
+
amount_to_read -= BLOCK_SIZE
|
|
80
|
+
@counter += 1
|
|
81
|
+
idx += 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# JRuby requires finalization of the cipher. This is a bug, as noted in
|
|
85
|
+
# jruby/jruby-openssl#182 and jruby/jruby-openssl#183.
|
|
86
|
+
decrypted_data << @cipher.final if defined?(JRUBY_VERSION)
|
|
87
|
+
decrypted_data
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def reset!(header)
|
|
91
|
+
raise Error, "Unsupported encryption AES-#{@bits}" unless STRENGTHS.include? @strength
|
|
92
|
+
|
|
93
|
+
salt = header[0...@salt_length]
|
|
94
|
+
pwd_verify = header[-VERIFIER_LENGTH..]
|
|
95
|
+
key_material = OpenSSL::KDF.pbkdf2_hmac(
|
|
96
|
+
@password,
|
|
97
|
+
salt: salt,
|
|
98
|
+
iterations: 1000,
|
|
99
|
+
length: (2 * @key_length) + VERIFIER_LENGTH,
|
|
100
|
+
hash: 'sha1'
|
|
101
|
+
)
|
|
102
|
+
enc_key = key_material[0...@key_length]
|
|
103
|
+
enc_hmac_key = key_material[@key_length...(2 * @key_length)]
|
|
104
|
+
enc_pwd_verify = key_material[-VERIFIER_LENGTH..]
|
|
105
|
+
|
|
106
|
+
raise Error, 'Bad password' if enc_pwd_verify != pwd_verify
|
|
107
|
+
|
|
108
|
+
@counter = 0
|
|
109
|
+
@cipher = OpenSSL::Cipher::AES.new(@bits, :CTR)
|
|
110
|
+
@cipher.decrypt
|
|
111
|
+
@cipher.key = enc_key
|
|
112
|
+
@hmac = OpenSSL::HMAC.new(enc_hmac_key, OpenSSL::Digest.new('SHA1'))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def check_integrity!(io)
|
|
116
|
+
auth_code = io.read(AUTHENTICATION_CODE_LENGTH)
|
|
117
|
+
raise Error, 'Integrity fault' if @hmac.digest[0...AUTHENTICATION_CODE_LENGTH] != auth_code
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|