rubyzip 1.0.0 → 2.4.1
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 +6 -14
- data/README.md +231 -46
- data/Rakefile +13 -5
- data/TODO +0 -1
- data/lib/zip/central_directory.rb +64 -29
- data/lib/zip/compressor.rb +1 -2
- data/lib/zip/constants.rb +59 -5
- data/lib/zip/crypto/decrypted_io.rb +40 -0
- data/lib/zip/crypto/encryption.rb +11 -0
- data/lib/zip/crypto/null_encryption.rb +43 -0
- data/lib/zip/crypto/traditional_encryption.rb +99 -0
- data/lib/zip/decompressor.rb +22 -4
- data/lib/zip/deflater.rb +11 -6
- data/lib/zip/dos_time.rb +27 -16
- data/lib/zip/entry.rb +299 -163
- data/lib/zip/entry_set.rb +22 -20
- data/lib/zip/errors.rb +17 -6
- data/lib/zip/extra_field/generic.rb +15 -14
- data/lib/zip/extra_field/ntfs.rb +94 -0
- data/lib/zip/extra_field/old_unix.rb +46 -0
- data/lib/zip/extra_field/universal_time.rb +46 -16
- data/lib/zip/extra_field/unix.rb +10 -9
- data/lib/zip/extra_field/zip64.rb +46 -6
- data/lib/zip/extra_field/zip64_placeholder.rb +15 -0
- data/lib/zip/extra_field.rb +32 -18
- data/lib/zip/file.rb +260 -160
- data/lib/zip/filesystem.rb +297 -276
- data/lib/zip/inflater.rb +23 -34
- data/lib/zip/input_stream.rb +130 -82
- data/lib/zip/ioextras/abstract_input_stream.rb +36 -22
- data/lib/zip/ioextras/abstract_output_stream.rb +4 -6
- data/lib/zip/ioextras.rb +4 -6
- data/lib/zip/null_compressor.rb +2 -2
- data/lib/zip/null_decompressor.rb +4 -12
- data/lib/zip/null_input_stream.rb +2 -1
- data/lib/zip/output_stream.rb +75 -45
- data/lib/zip/pass_thru_compressor.rb +6 -6
- data/lib/zip/pass_thru_decompressor.rb +14 -24
- data/lib/zip/streamable_directory.rb +3 -3
- data/lib/zip/streamable_stream.rb +21 -16
- data/lib/zip/version.rb +1 -1
- data/lib/zip.rb +42 -3
- data/samples/example.rb +30 -40
- data/samples/example_filesystem.rb +16 -18
- data/samples/example_recursive.rb +33 -28
- data/samples/gtk_ruby_zip.rb +84 -0
- data/samples/qtzip.rb +25 -34
- data/samples/write_simple.rb +10 -13
- data/samples/zipfind.rb +38 -45
- metadata +129 -28
- data/NEWS +0 -182
- data/samples/gtkRubyzip.rb +0 -86
data/lib/zip/entry.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require 'pathname'
|
|
1
2
|
module Zip
|
|
2
3
|
class Entry
|
|
3
4
|
STORED = 0
|
|
@@ -7,6 +8,7 @@ module Zip
|
|
|
7
8
|
|
|
8
9
|
attr_accessor :comment, :compressed_size, :crc, :extra, :compression_method,
|
|
9
10
|
:name, :size, :local_header_offset, :zipfile, :fstype, :external_file_attributes,
|
|
11
|
+
:internal_file_attributes,
|
|
10
12
|
:gp_flags, :header_signature, :follow_symlinks,
|
|
11
13
|
:restore_times, :restore_permissions, :restore_ownership,
|
|
12
14
|
:unix_uid, :unix_gid, :unix_perms,
|
|
@@ -15,13 +17,13 @@ module Zip
|
|
|
15
17
|
|
|
16
18
|
def set_default_vars_values
|
|
17
19
|
@local_header_offset = 0
|
|
18
|
-
@local_header_size =
|
|
20
|
+
@local_header_size = nil # not known until local entry is created or read
|
|
19
21
|
@internal_file_attributes = 1
|
|
20
22
|
@external_file_attributes = 0
|
|
21
23
|
@header_signature = ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
|
|
22
24
|
|
|
23
25
|
@version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT
|
|
24
|
-
@version =
|
|
26
|
+
@version = VERSION_MADE_BY
|
|
25
27
|
|
|
26
28
|
@ftype = nil # unspecified or unknown
|
|
27
29
|
@filepath = nil
|
|
@@ -32,48 +34,73 @@ module Zip
|
|
|
32
34
|
end
|
|
33
35
|
@follow_symlinks = false
|
|
34
36
|
|
|
35
|
-
@restore_times =
|
|
37
|
+
@restore_times = false
|
|
36
38
|
@restore_permissions = false
|
|
37
39
|
@restore_ownership = false
|
|
38
40
|
# BUG: need an extra field to support uid/gid's
|
|
39
41
|
@unix_uid = nil
|
|
40
42
|
@unix_gid = nil
|
|
41
43
|
@unix_perms = nil
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
# @posix_acl = nil
|
|
45
|
+
# @ntfs_acl = nil
|
|
44
46
|
@dirty = false
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
def check_name(name)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
return unless name.start_with?('/')
|
|
51
|
+
|
|
52
|
+
raise ::Zip::EntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
|
|
51
53
|
end
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
56
|
+
def initialize(zipfile = nil, name = nil, *args)
|
|
57
|
+
name ||= ''
|
|
55
58
|
check_name(name)
|
|
56
59
|
|
|
57
60
|
set_default_vars_values
|
|
58
61
|
@fstype = ::Zip::RUNNING_ON_WINDOWS ? ::Zip::FSTYPE_FAT : ::Zip::FSTYPE_UNIX
|
|
59
62
|
|
|
60
|
-
@zipfile =
|
|
63
|
+
@zipfile = zipfile || ''
|
|
61
64
|
@name = name
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
|
|
66
|
+
if (args_hash = args.first).kind_of?(::Hash)
|
|
67
|
+
@comment = args_hash[:comment] || ''
|
|
68
|
+
@extra = args_hash[:extra] || ''
|
|
69
|
+
@compressed_size = args_hash[:compressed_size] || 0
|
|
70
|
+
@crc = args_hash[:crc] || 0
|
|
71
|
+
@compression_method = args_hash[:compression_method] || ::Zip::Entry::DEFLATED
|
|
72
|
+
@size = args_hash[:size] || 0
|
|
73
|
+
@time = args_hash[:time] || ::Zip::DOSTime.now
|
|
74
|
+
else
|
|
75
|
+
Zip.warn_about_v3_api('Zip::Entry.new') unless args.empty?
|
|
76
|
+
|
|
77
|
+
@comment = args[0] || ''
|
|
78
|
+
@extra = args[1] || ''
|
|
79
|
+
@compressed_size = args[2] || 0
|
|
80
|
+
@crc = args[3] || 0
|
|
81
|
+
@compression_method = args[4] || ::Zip::Entry::DEFLATED
|
|
82
|
+
@size = args[5] || 0
|
|
83
|
+
@time = args[6] || ::Zip::DOSTime.now
|
|
84
|
+
end
|
|
69
85
|
|
|
70
86
|
@ftype = name_is_directory? ? :directory : :file
|
|
71
|
-
@extra = ::Zip::ExtraField.new(@extra.to_s) unless ::Zip::ExtraField
|
|
87
|
+
@extra = ::Zip::ExtraField.new(@extra.to_s) unless @extra.kind_of?(::Zip::ExtraField)
|
|
88
|
+
end
|
|
89
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
90
|
+
|
|
91
|
+
def encrypted?
|
|
92
|
+
gp_flags & 1 == 1
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def incomplete?
|
|
96
|
+
gp_flags & 8 == 8
|
|
72
97
|
end
|
|
73
98
|
|
|
74
99
|
def time
|
|
75
100
|
if @extra['UniversalTime']
|
|
76
101
|
@extra['UniversalTime'].mtime
|
|
102
|
+
elsif @extra['NTFS']
|
|
103
|
+
@extra['NTFS'].mtime
|
|
77
104
|
else
|
|
78
105
|
# Standard time field in central directory has local time
|
|
79
106
|
# under archive creator. Then, we can't get timezone.
|
|
@@ -81,23 +108,24 @@ module Zip
|
|
|
81
108
|
end
|
|
82
109
|
end
|
|
83
110
|
|
|
84
|
-
alias
|
|
111
|
+
alias mtime time
|
|
85
112
|
|
|
86
113
|
def time=(value)
|
|
87
|
-
unless @extra.member?('UniversalTime')
|
|
114
|
+
unless @extra.member?('UniversalTime') || @extra.member?('NTFS')
|
|
88
115
|
@extra.create('UniversalTime')
|
|
89
116
|
end
|
|
90
|
-
@extra['UniversalTime'].mtime = value
|
|
91
|
-
@time
|
|
117
|
+
(@extra['UniversalTime'] || @extra['NTFS']).mtime = value
|
|
118
|
+
@time = value
|
|
92
119
|
end
|
|
93
120
|
|
|
94
121
|
def file_type_is?(type)
|
|
95
|
-
raise
|
|
122
|
+
raise InternalError, "current filetype is unknown: #{inspect}" unless @ftype
|
|
123
|
+
|
|
96
124
|
@ftype == type
|
|
97
125
|
end
|
|
98
126
|
|
|
99
127
|
# Dynamic checkers
|
|
100
|
-
%w
|
|
128
|
+
%w[directory file symlink].each do |k|
|
|
101
129
|
define_method "#{k}?" do
|
|
102
130
|
file_type_is?(k.to_sym)
|
|
103
131
|
end
|
|
@@ -107,6 +135,18 @@ module Zip
|
|
|
107
135
|
@name.end_with?('/')
|
|
108
136
|
end
|
|
109
137
|
|
|
138
|
+
# Is the name a relative path, free of `..` patterns that could lead to
|
|
139
|
+
# path traversal attacks? This does NOT handle symlinks; if the path
|
|
140
|
+
# contains symlinks, this check is NOT enough to guarantee safety.
|
|
141
|
+
def name_safe?
|
|
142
|
+
cleanpath = Pathname.new(@name).cleanpath
|
|
143
|
+
return false unless cleanpath.relative?
|
|
144
|
+
|
|
145
|
+
root = ::File::SEPARATOR
|
|
146
|
+
naive_expanded_path = ::File.join(root, cleanpath.to_s)
|
|
147
|
+
::File.absolute_path(cleanpath.to_s, root) == naive_expanded_path
|
|
148
|
+
end
|
|
149
|
+
|
|
110
150
|
def local_entry_offset #:nodoc:all
|
|
111
151
|
local_header_offset + @local_header_size
|
|
112
152
|
end
|
|
@@ -124,29 +164,66 @@ module Zip
|
|
|
124
164
|
end
|
|
125
165
|
|
|
126
166
|
def calculate_local_header_size #:nodoc:all
|
|
127
|
-
fix_zip64_sizes!
|
|
128
167
|
LOCAL_ENTRY_STATIC_HEADER_LENGTH + name_size + extra_size
|
|
129
168
|
end
|
|
130
169
|
|
|
170
|
+
# check before rewriting an entry (after file sizes are known)
|
|
171
|
+
# that we didn't change the header size (and thus clobber file data or something)
|
|
172
|
+
def verify_local_header_size!
|
|
173
|
+
return if @local_header_size.nil?
|
|
174
|
+
|
|
175
|
+
new_size = calculate_local_header_size
|
|
176
|
+
raise Error, "local header size changed (#{@local_header_size} -> #{new_size})" if @local_header_size != new_size
|
|
177
|
+
end
|
|
178
|
+
|
|
131
179
|
def cdir_header_size #:nodoc:all
|
|
132
180
|
CDIR_ENTRY_STATIC_HEADER_LENGTH + name_size +
|
|
133
181
|
(@extra ? @extra.c_dir_size : 0) + comment_size
|
|
134
182
|
end
|
|
135
183
|
|
|
136
184
|
def next_header_offset #:nodoc:all
|
|
137
|
-
local_entry_offset +
|
|
185
|
+
local_entry_offset + compressed_size + data_descriptor_size
|
|
138
186
|
end
|
|
139
187
|
|
|
140
188
|
# Extracts entry to file dest_path (defaults to @name).
|
|
141
|
-
|
|
189
|
+
# NB: The caller is responsible for making sure dest_path is safe, if it
|
|
190
|
+
# is passed.
|
|
191
|
+
def extract(dest_path = nil, &block)
|
|
192
|
+
Zip.warn_about_v3_api('Zip::Entry#extract')
|
|
193
|
+
|
|
194
|
+
if dest_path.nil? && !name_safe?
|
|
195
|
+
warn "WARNING: skipped '#{@name}' as unsafe."
|
|
196
|
+
return self
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
dest_path ||= @name
|
|
142
200
|
block ||= proc { ::Zip.on_exists_proc }
|
|
143
201
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
202
|
+
raise "unknown file type #{inspect}" unless directory? || file? || symlink?
|
|
203
|
+
|
|
204
|
+
__send__("create_#{@ftype}", dest_path, &block)
|
|
205
|
+
self
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Extracts this entry to a file at `entry_path`, with
|
|
209
|
+
# `destination_directory` as the base location in the filesystem.
|
|
210
|
+
#
|
|
211
|
+
# NB: The caller is responsible for making sure `destination_directory` is
|
|
212
|
+
# safe, if it is passed.
|
|
213
|
+
def extract_v3(entry_path = @name, destination_directory: '.', &block)
|
|
214
|
+
dest_dir = ::File.absolute_path(destination_directory || '.')
|
|
215
|
+
extract_path = ::File.absolute_path(::File.join(dest_dir, entry_path))
|
|
216
|
+
|
|
217
|
+
unless extract_path.start_with?(dest_dir)
|
|
218
|
+
warn "WARNING: skipped extracting '#{@name}' to '#{extract_path}' as unsafe."
|
|
219
|
+
return self
|
|
148
220
|
end
|
|
149
221
|
|
|
222
|
+
block ||= proc { ::Zip.on_exists_proc }
|
|
223
|
+
|
|
224
|
+
raise "unknown file type #{inspect}" unless directory? || file? || symlink?
|
|
225
|
+
|
|
226
|
+
__send__(:"create_#{ftype}", extract_path, &block)
|
|
150
227
|
self
|
|
151
228
|
end
|
|
152
229
|
|
|
@@ -154,41 +231,41 @@ module Zip
|
|
|
154
231
|
@name
|
|
155
232
|
end
|
|
156
233
|
|
|
157
|
-
protected
|
|
158
|
-
|
|
159
234
|
class << self
|
|
160
235
|
def read_zip_short(io) # :nodoc:
|
|
161
|
-
io.read(2).
|
|
236
|
+
io.read(2).unpack1('v')
|
|
162
237
|
end
|
|
163
238
|
|
|
164
239
|
def read_zip_long(io) # :nodoc:
|
|
165
|
-
io.read(4).
|
|
240
|
+
io.read(4).unpack1('V')
|
|
166
241
|
end
|
|
167
242
|
|
|
168
243
|
def read_zip_64_long(io) # :nodoc:
|
|
169
|
-
io.read(8).
|
|
244
|
+
io.read(8).unpack1('Q<')
|
|
170
245
|
end
|
|
171
246
|
|
|
172
247
|
def read_c_dir_entry(io) #:nodoc:all
|
|
173
|
-
|
|
248
|
+
path = if io.respond_to?(:path)
|
|
249
|
+
io.path
|
|
250
|
+
else
|
|
251
|
+
io
|
|
252
|
+
end
|
|
253
|
+
entry = new(path)
|
|
174
254
|
entry.read_c_dir_entry(io)
|
|
175
255
|
entry
|
|
176
|
-
rescue
|
|
256
|
+
rescue Error
|
|
177
257
|
nil
|
|
178
258
|
end
|
|
179
259
|
|
|
180
260
|
def read_local_entry(io)
|
|
181
|
-
entry = new(io
|
|
261
|
+
entry = new(io)
|
|
182
262
|
entry.read_local_entry(io)
|
|
183
263
|
entry
|
|
184
|
-
rescue
|
|
264
|
+
rescue Error
|
|
185
265
|
nil
|
|
186
266
|
end
|
|
187
|
-
|
|
188
267
|
end
|
|
189
268
|
|
|
190
|
-
public
|
|
191
|
-
|
|
192
269
|
def unpack_local_entry(buf)
|
|
193
270
|
@header_signature,
|
|
194
271
|
@version,
|
|
@@ -207,57 +284,67 @@ module Zip
|
|
|
207
284
|
def read_local_entry(io) #:nodoc:all
|
|
208
285
|
@local_header_offset = io.tell
|
|
209
286
|
|
|
210
|
-
static_sized_fields_buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH)
|
|
287
|
+
static_sized_fields_buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
|
|
211
288
|
|
|
212
289
|
unless static_sized_fields_buf.bytesize == ::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH
|
|
213
|
-
raise
|
|
290
|
+
raise Error, 'Premature end of file. Not enough data for zip entry local header'
|
|
214
291
|
end
|
|
215
292
|
|
|
216
293
|
unpack_local_entry(static_sized_fields_buf)
|
|
217
294
|
|
|
218
295
|
unless @header_signature == ::Zip::LOCAL_ENTRY_SIGNATURE
|
|
219
|
-
raise ::Zip::
|
|
296
|
+
raise ::Zip::Error, "Zip local header magic not found at location '#{local_header_offset}'"
|
|
220
297
|
end
|
|
298
|
+
|
|
221
299
|
set_time(@last_mod_date, @last_mod_time)
|
|
222
300
|
|
|
223
301
|
@name = io.read(@name_length)
|
|
224
302
|
extra = io.read(@extra_length)
|
|
225
303
|
|
|
226
|
-
@name.
|
|
304
|
+
@name.tr!('\\', '/')
|
|
305
|
+
if ::Zip.force_entry_names_encoding
|
|
306
|
+
@name.force_encoding(::Zip.force_entry_names_encoding)
|
|
307
|
+
end
|
|
227
308
|
|
|
228
309
|
if extra && extra.bytesize != @extra_length
|
|
229
|
-
raise ::Zip::
|
|
310
|
+
raise ::Zip::Error, 'Truncated local zip entry header'
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
if @extra.kind_of?(::Zip::ExtraField)
|
|
314
|
+
@extra.merge(extra) if extra
|
|
230
315
|
else
|
|
231
|
-
|
|
232
|
-
@extra.merge(extra)
|
|
233
|
-
else
|
|
234
|
-
@extra = ::Zip::ExtraField.new(extra)
|
|
235
|
-
end
|
|
316
|
+
@extra = ::Zip::ExtraField.new(extra)
|
|
236
317
|
end
|
|
318
|
+
|
|
319
|
+
parse_zip64_extra(true)
|
|
237
320
|
@local_header_size = calculate_local_header_size
|
|
238
321
|
end
|
|
239
322
|
|
|
240
323
|
def pack_local_entry
|
|
324
|
+
zip64 = @extra['Zip64']
|
|
241
325
|
[::Zip::LOCAL_ENTRY_SIGNATURE,
|
|
242
326
|
@version_needed_to_extract, # version needed to extract
|
|
243
|
-
@gp_flags, # @gp_flags
|
|
327
|
+
@gp_flags, # @gp_flags
|
|
244
328
|
@compression_method,
|
|
245
|
-
@time.to_binary_dos_time, # @last_mod_time
|
|
246
|
-
@time.to_binary_dos_date, # @last_mod_date
|
|
329
|
+
@time.to_binary_dos_time, # @last_mod_time
|
|
330
|
+
@time.to_binary_dos_date, # @last_mod_date
|
|
247
331
|
@crc,
|
|
248
|
-
@compressed_size,
|
|
249
|
-
@size,
|
|
332
|
+
zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
|
|
333
|
+
zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
|
|
250
334
|
name_size,
|
|
251
335
|
@extra ? @extra.local_size : 0].pack('VvvvvvVVVvv')
|
|
252
336
|
end
|
|
253
337
|
|
|
254
|
-
def write_local_entry(io) #:nodoc:all
|
|
338
|
+
def write_local_entry(io, rewrite = false) #:nodoc:all
|
|
339
|
+
prep_zip64_extra(true)
|
|
340
|
+
verify_local_header_size! if rewrite
|
|
255
341
|
@local_header_offset = io.tell
|
|
256
342
|
|
|
257
343
|
io << pack_local_entry
|
|
258
344
|
|
|
259
345
|
io << @name
|
|
260
|
-
io <<
|
|
346
|
+
io << @extra.to_local_bin if @extra
|
|
347
|
+
@local_header_size = io.tell - @local_header_offset
|
|
261
348
|
end
|
|
262
349
|
|
|
263
350
|
def unpack_c_dir_entry(buf)
|
|
@@ -287,7 +374,7 @@ module Zip
|
|
|
287
374
|
def set_ftype_from_c_dir_entry
|
|
288
375
|
@ftype = case @fstype
|
|
289
376
|
when ::Zip::FSTYPE_UNIX
|
|
290
|
-
@unix_perms = (@external_file_attributes >> 16) &
|
|
377
|
+
@unix_perms = (@external_file_attributes >> 16) & 0o7777
|
|
291
378
|
case (@external_file_attributes >> 28)
|
|
292
379
|
when ::Zip::FILE_TYPE_DIR
|
|
293
380
|
:directory
|
|
@@ -296,8 +383,8 @@ module Zip
|
|
|
296
383
|
when ::Zip::FILE_TYPE_SYMLINK
|
|
297
384
|
:symlink
|
|
298
385
|
else
|
|
299
|
-
#best case guess for whether it is a file or not
|
|
300
|
-
#Otherwise this would be set to unknown and that entry would never be able to extracted
|
|
386
|
+
# best case guess for whether it is a file or not
|
|
387
|
+
# Otherwise this would be set to unknown and that entry would never be able to extracted
|
|
301
388
|
if name_is_directory?
|
|
302
389
|
:directory
|
|
303
390
|
else
|
|
@@ -314,25 +401,25 @@ module Zip
|
|
|
314
401
|
end
|
|
315
402
|
|
|
316
403
|
def check_c_dir_entry_static_header_length(buf)
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
end
|
|
404
|
+
return if buf.bytesize == ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
|
|
405
|
+
|
|
406
|
+
raise Error, 'Premature end of file. Not enough data for zip cdir entry header'
|
|
320
407
|
end
|
|
321
408
|
|
|
322
409
|
def check_c_dir_entry_signature
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
410
|
+
return if header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
|
|
411
|
+
|
|
412
|
+
raise Error, "Zip local header magic not found at location '#{local_header_offset}'"
|
|
326
413
|
end
|
|
327
414
|
|
|
328
415
|
def check_c_dir_entry_comment_size
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
416
|
+
return if @comment && @comment.bytesize == @comment_length
|
|
417
|
+
|
|
418
|
+
raise ::Zip::Error, 'Truncated cdir zip entry header'
|
|
332
419
|
end
|
|
333
420
|
|
|
334
421
|
def read_c_dir_extra_field(io)
|
|
335
|
-
if @extra.
|
|
422
|
+
if @extra.kind_of?(::Zip::ExtraField)
|
|
336
423
|
@extra.merge(io.read(@extra_length))
|
|
337
424
|
else
|
|
338
425
|
@extra = ::Zip::ExtraField.new(io.read(@extra_length))
|
|
@@ -345,70 +432,78 @@ module Zip
|
|
|
345
432
|
unpack_c_dir_entry(static_sized_fields_buf)
|
|
346
433
|
check_c_dir_entry_signature
|
|
347
434
|
set_time(@last_mod_date, @last_mod_time)
|
|
348
|
-
@name = io.read(@name_length)
|
|
435
|
+
@name = io.read(@name_length)
|
|
436
|
+
if ::Zip.force_entry_names_encoding
|
|
437
|
+
@name.force_encoding(::Zip.force_entry_names_encoding)
|
|
438
|
+
end
|
|
349
439
|
read_c_dir_extra_field(io)
|
|
350
440
|
@comment = io.read(@comment_length)
|
|
351
441
|
check_c_dir_entry_comment_size
|
|
352
442
|
set_ftype_from_c_dir_entry
|
|
353
|
-
|
|
443
|
+
parse_zip64_extra(false)
|
|
354
444
|
end
|
|
355
445
|
|
|
356
446
|
def file_stat(path) # :nodoc:
|
|
357
447
|
if @follow_symlinks
|
|
358
|
-
::File
|
|
448
|
+
::File.stat(path)
|
|
359
449
|
else
|
|
360
|
-
::File
|
|
450
|
+
::File.lstat(path)
|
|
361
451
|
end
|
|
362
452
|
end
|
|
363
453
|
|
|
364
454
|
def get_extra_attributes_from_path(path) # :nodoc:
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
455
|
+
return if Zip::RUNNING_ON_WINDOWS
|
|
456
|
+
|
|
457
|
+
stat = file_stat(path)
|
|
458
|
+
@unix_uid = stat.uid
|
|
459
|
+
@unix_gid = stat.gid
|
|
460
|
+
@unix_perms = stat.mode & 0o7777
|
|
461
|
+
@time = ::Zip::DOSTime.from_time(stat.mtime)
|
|
371
462
|
end
|
|
372
463
|
|
|
373
|
-
def
|
|
374
|
-
# BUG: does not update timestamps into account
|
|
464
|
+
def set_unix_attributes_on_path(dest_path)
|
|
375
465
|
# ignore setuid/setgid bits by default. honor if @restore_ownership
|
|
376
|
-
unix_perms_mask =
|
|
377
|
-
unix_perms_mask =
|
|
466
|
+
unix_perms_mask = 0o1777
|
|
467
|
+
unix_perms_mask = 0o7777 if @restore_ownership
|
|
378
468
|
::FileUtils.chmod(@unix_perms & unix_perms_mask, dest_path) if @restore_permissions && @unix_perms
|
|
379
469
|
::FileUtils.chown(@unix_uid, @unix_gid, dest_path) if @restore_ownership && @unix_uid && @unix_gid && ::Process.egid == 0
|
|
380
|
-
|
|
470
|
+
|
|
471
|
+
# Restore the timestamp on a file. This will either have come from the
|
|
472
|
+
# original source file that was copied into the archive, or from the
|
|
473
|
+
# creation date of the archive if there was no original source file.
|
|
474
|
+
::FileUtils.touch(dest_path, mtime: time) if @restore_times
|
|
381
475
|
end
|
|
382
476
|
|
|
383
477
|
def set_extra_attributes_on_path(dest_path) # :nodoc:
|
|
384
|
-
return unless
|
|
478
|
+
return unless file? || directory?
|
|
385
479
|
|
|
386
480
|
case @fstype
|
|
387
481
|
when ::Zip::FSTYPE_UNIX
|
|
388
|
-
|
|
482
|
+
set_unix_attributes_on_path(dest_path)
|
|
389
483
|
end
|
|
390
484
|
end
|
|
391
485
|
|
|
392
486
|
def pack_c_dir_entry
|
|
487
|
+
zip64 = @extra['Zip64']
|
|
393
488
|
[
|
|
394
489
|
@header_signature,
|
|
395
490
|
@version, # version of encoding software
|
|
396
491
|
@fstype, # filesystem type
|
|
397
|
-
@version_needed_to_extract, # @versionNeededToExtract
|
|
398
|
-
@gp_flags, # @gp_flags
|
|
492
|
+
@version_needed_to_extract, # @versionNeededToExtract
|
|
493
|
+
@gp_flags, # @gp_flags
|
|
399
494
|
@compression_method,
|
|
400
|
-
@time.to_binary_dos_time, # @last_mod_time
|
|
401
|
-
@time.to_binary_dos_date, # @last_mod_date
|
|
495
|
+
@time.to_binary_dos_time, # @last_mod_time
|
|
496
|
+
@time.to_binary_dos_date, # @last_mod_date
|
|
402
497
|
@crc,
|
|
403
|
-
@compressed_size,
|
|
404
|
-
@size,
|
|
498
|
+
zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
|
|
499
|
+
zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
|
|
405
500
|
name_size,
|
|
406
501
|
@extra ? @extra.c_dir_size : 0,
|
|
407
502
|
comment_size,
|
|
408
|
-
0, # disk number start
|
|
503
|
+
zip64 && zip64.disk_start_number ? 0xFFFF : 0, # disk number start
|
|
409
504
|
@internal_file_attributes, # file type (binary=0, text=1)
|
|
410
505
|
@external_file_attributes, # native filesystem attributes
|
|
411
|
-
@local_header_offset,
|
|
506
|
+
zip64 && zip64.relative_header_offset ? 0xFFFFFFFF : @local_header_offset,
|
|
412
507
|
@name,
|
|
413
508
|
@extra,
|
|
414
509
|
@comment
|
|
@@ -416,22 +511,23 @@ module Zip
|
|
|
416
511
|
end
|
|
417
512
|
|
|
418
513
|
def write_c_dir_entry(io) #:nodoc:all
|
|
514
|
+
prep_zip64_extra(false)
|
|
419
515
|
case @fstype
|
|
420
516
|
when ::Zip::FSTYPE_UNIX
|
|
421
517
|
ft = case @ftype
|
|
422
518
|
when :file
|
|
423
|
-
@unix_perms ||=
|
|
519
|
+
@unix_perms ||= 0o644
|
|
424
520
|
::Zip::FILE_TYPE_FILE
|
|
425
521
|
when :directory
|
|
426
|
-
@unix_perms ||=
|
|
522
|
+
@unix_perms ||= 0o755
|
|
427
523
|
::Zip::FILE_TYPE_DIR
|
|
428
524
|
when :symlink
|
|
429
|
-
@unix_perms ||=
|
|
525
|
+
@unix_perms ||= 0o755
|
|
430
526
|
::Zip::FILE_TYPE_SYMLINK
|
|
431
527
|
end
|
|
432
528
|
|
|
433
529
|
unless ft.nil?
|
|
434
|
-
@external_file_attributes = (ft << 12 | (@unix_perms &
|
|
530
|
+
@external_file_attributes = (ft << 12 | (@unix_perms & 0o7777)) << 16
|
|
435
531
|
end
|
|
436
532
|
end
|
|
437
533
|
|
|
@@ -444,23 +540,24 @@ module Zip
|
|
|
444
540
|
|
|
445
541
|
def ==(other)
|
|
446
542
|
return false unless other.class == self.class
|
|
543
|
+
|
|
447
544
|
# Compares contents of local entry and exposed fields
|
|
448
|
-
keys_equal = %w
|
|
449
|
-
other.__send__(k.to_sym) ==
|
|
545
|
+
keys_equal = %w[compression_method crc compressed_size size name extra filepath].all? do |k|
|
|
546
|
+
other.__send__(k.to_sym) == __send__(k.to_sym)
|
|
450
547
|
end
|
|
451
|
-
keys_equal &&
|
|
548
|
+
keys_equal && time == other.time
|
|
452
549
|
end
|
|
453
550
|
|
|
454
|
-
def <=>
|
|
455
|
-
|
|
551
|
+
def <=>(other)
|
|
552
|
+
to_s <=> other.to_s
|
|
456
553
|
end
|
|
457
554
|
|
|
458
555
|
# Returns an IO like object for the given ZipEntry.
|
|
459
556
|
# Warning: may behave weird with symlinks.
|
|
460
557
|
def get_input_stream(&block)
|
|
461
558
|
if @ftype == :directory
|
|
462
|
-
yield
|
|
463
|
-
::Zip::NullInputStream
|
|
559
|
+
yield ::Zip::NullInputStream if block_given?
|
|
560
|
+
::Zip::NullInputStream
|
|
464
561
|
elsif @filepath
|
|
465
562
|
case @ftype
|
|
466
563
|
when :file
|
|
@@ -474,7 +571,8 @@ module Zip
|
|
|
474
571
|
raise "unknown @file_type #{@ftype}"
|
|
475
572
|
end
|
|
476
573
|
else
|
|
477
|
-
zis = ::Zip::InputStream.new(@zipfile, local_header_offset)
|
|
574
|
+
zis = ::Zip::InputStream.new(@zipfile, offset: local_header_offset)
|
|
575
|
+
zis.instance_variable_set(:@complete_entry, self)
|
|
478
576
|
zis.get_next_entry
|
|
479
577
|
if block_given?
|
|
480
578
|
begin
|
|
@@ -494,22 +592,22 @@ module Zip
|
|
|
494
592
|
when 'file'
|
|
495
593
|
if name_is_directory?
|
|
496
594
|
raise ArgumentError,
|
|
497
|
-
"entry name '#{newEntry}' indicates directory entry, but "
|
|
498
|
-
|
|
595
|
+
"entry name '#{newEntry}' indicates directory entry, but " \
|
|
596
|
+
"'#{src_path}' is not a directory"
|
|
499
597
|
end
|
|
500
598
|
:file
|
|
501
599
|
when 'directory'
|
|
502
|
-
@name +=
|
|
600
|
+
@name += '/' unless name_is_directory?
|
|
503
601
|
:directory
|
|
504
602
|
when 'link'
|
|
505
603
|
if name_is_directory?
|
|
506
604
|
raise ArgumentError,
|
|
507
|
-
"entry name '#{newEntry}' indicates directory entry, but "
|
|
508
|
-
|
|
605
|
+
"entry name '#{newEntry}' indicates directory entry, but " \
|
|
606
|
+
"'#{src_path}' is not a directory"
|
|
509
607
|
end
|
|
510
608
|
:symlink
|
|
511
609
|
else
|
|
512
|
-
raise
|
|
610
|
+
raise "unknown file type: #{src_path.inspect} #{stat.inspect}"
|
|
513
611
|
end
|
|
514
612
|
|
|
515
613
|
@filepath = src_path
|
|
@@ -518,9 +616,9 @@ module Zip
|
|
|
518
616
|
|
|
519
617
|
def write_to_zip_output_stream(zip_output_stream) #:nodoc:all
|
|
520
618
|
if @ftype == :directory
|
|
521
|
-
zip_output_stream.put_next_entry(self)
|
|
619
|
+
zip_output_stream.put_next_entry(self, nil, nil, ::Zip::Entry::STORED)
|
|
522
620
|
elsif @filepath
|
|
523
|
-
zip_output_stream.put_next_entry(self, nil, nil,
|
|
621
|
+
zip_output_stream.put_next_entry(self, nil, nil, compression_method || ::Zip::Entry::DEFLATED)
|
|
524
622
|
get_input_stream { |is| ::Zip::IOExtras.copy_stream(zip_output_stream, is) }
|
|
525
623
|
else
|
|
526
624
|
zip_output_stream.copy_raw_entry(self)
|
|
@@ -530,11 +628,19 @@ module Zip
|
|
|
530
628
|
def parent_as_string
|
|
531
629
|
entry_name = name.chomp('/')
|
|
532
630
|
slash_index = entry_name.rindex('/')
|
|
533
|
-
slash_index ? entry_name.slice(0, slash_index+1) : nil
|
|
631
|
+
slash_index ? entry_name.slice(0, slash_index + 1) : nil
|
|
534
632
|
end
|
|
535
633
|
|
|
536
634
|
def get_raw_input_stream(&block)
|
|
537
|
-
|
|
635
|
+
if @zipfile.respond_to?(:seek) && @zipfile.respond_to?(:read)
|
|
636
|
+
yield @zipfile
|
|
637
|
+
else
|
|
638
|
+
::File.open(@zipfile, 'rb', &block)
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def clean_up
|
|
643
|
+
# By default, do nothing
|
|
538
644
|
end
|
|
539
645
|
|
|
540
646
|
private
|
|
@@ -542,35 +648,46 @@ module Zip
|
|
|
542
648
|
def set_time(binary_dos_date, binary_dos_time)
|
|
543
649
|
@time = ::Zip::DOSTime.parse_binary_dos_format(binary_dos_date, binary_dos_time)
|
|
544
650
|
rescue ArgumentError
|
|
545
|
-
|
|
651
|
+
warn 'WARNING: invalid date/time in zip entry.' if ::Zip.warn_invalid_date
|
|
546
652
|
end
|
|
547
653
|
|
|
548
|
-
def create_file(dest_path,
|
|
549
|
-
if ::File.
|
|
550
|
-
raise ::Zip::
|
|
654
|
+
def create_file(dest_path, _continue_on_exists_proc = proc { Zip.continue_on_exists_proc })
|
|
655
|
+
if ::File.exist?(dest_path) && !yield(self, dest_path)
|
|
656
|
+
raise ::Zip::DestinationFileExistsError,
|
|
551
657
|
"Destination '#{dest_path}' already exists"
|
|
552
658
|
end
|
|
553
|
-
::File.open(dest_path,
|
|
659
|
+
::File.open(dest_path, 'wb') do |os|
|
|
554
660
|
get_input_stream do |is|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
buf = ''
|
|
558
|
-
while buf = is.sysread(::Zip::Decompressor::CHUNK_SIZE, buf)
|
|
661
|
+
bytes_written = 0
|
|
662
|
+
warned = false
|
|
663
|
+
buf = +''
|
|
664
|
+
while (buf = is.sysread(::Zip::Decompressor::CHUNK_SIZE, buf))
|
|
559
665
|
os << buf
|
|
666
|
+
bytes_written += buf.bytesize
|
|
667
|
+
next unless bytes_written > size && !warned
|
|
668
|
+
|
|
669
|
+
message = "entry '#{name}' should be #{size}B, but is larger when inflated."
|
|
670
|
+
raise ::Zip::EntrySizeError, message if ::Zip.validate_entry_sizes
|
|
671
|
+
|
|
672
|
+
warn "WARNING: #{message}"
|
|
673
|
+
warned = true
|
|
560
674
|
end
|
|
561
675
|
end
|
|
562
676
|
end
|
|
677
|
+
|
|
678
|
+
set_extra_attributes_on_path(dest_path)
|
|
563
679
|
end
|
|
564
680
|
|
|
565
681
|
def create_directory(dest_path)
|
|
566
682
|
return if ::File.directory?(dest_path)
|
|
567
|
-
|
|
683
|
+
|
|
684
|
+
if ::File.exist?(dest_path)
|
|
568
685
|
if block_given? && yield(self, dest_path)
|
|
569
|
-
::FileUtils
|
|
686
|
+
::FileUtils.rm_f dest_path
|
|
570
687
|
else
|
|
571
|
-
raise ::Zip::
|
|
572
|
-
"Cannot create directory '#{dest_path}'. "
|
|
573
|
-
|
|
688
|
+
raise ::Zip::DestinationFileExistsError,
|
|
689
|
+
"Cannot create directory '#{dest_path}'. " \
|
|
690
|
+
'A file already exists with that name'
|
|
574
691
|
end
|
|
575
692
|
end
|
|
576
693
|
::FileUtils.mkdir_p(dest_path)
|
|
@@ -579,41 +696,60 @@ module Zip
|
|
|
579
696
|
|
|
580
697
|
# BUG: create_symlink() does not use &block
|
|
581
698
|
def create_symlink(dest_path)
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
end
|
|
699
|
+
# TODO: Symlinks pose security challenges. Symlink support temporarily
|
|
700
|
+
# removed in view of https://github.com/rubyzip/rubyzip/issues/369 .
|
|
701
|
+
warn "WARNING: skipped symlink '#{dest_path}'."
|
|
702
|
+
end
|
|
587
703
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
"Cannot create symlink '#{dest_path}'. "+
|
|
598
|
-
"A symlink already exists with that name"
|
|
599
|
-
end
|
|
600
|
-
else
|
|
601
|
-
raise ZipDestinationFileExistsError,
|
|
602
|
-
"Cannot create symlink '#{dest_path}'. "+
|
|
603
|
-
"A file already exists with that name"
|
|
604
|
-
end
|
|
704
|
+
# apply missing data from the zip64 extra information field, if present
|
|
705
|
+
# (required when file sizes exceed 2**32, but can be used for all files)
|
|
706
|
+
def parse_zip64_extra(for_local_header) #:nodoc:all
|
|
707
|
+
return if @extra['Zip64'].nil?
|
|
708
|
+
|
|
709
|
+
if for_local_header
|
|
710
|
+
@size, @compressed_size = @extra['Zip64'].parse(@size, @compressed_size)
|
|
711
|
+
else
|
|
712
|
+
@size, @compressed_size, @local_header_offset = @extra['Zip64'].parse(@size, @compressed_size, @local_header_offset)
|
|
605
713
|
end
|
|
714
|
+
end
|
|
606
715
|
|
|
607
|
-
|
|
716
|
+
def data_descriptor_size
|
|
717
|
+
(@gp_flags & 0x0008) > 0 ? 16 : 0
|
|
608
718
|
end
|
|
609
719
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
720
|
+
# create a zip64 extra information field if we need one
|
|
721
|
+
def prep_zip64_extra(for_local_header) #:nodoc:all
|
|
722
|
+
return unless ::Zip.write_zip64_support
|
|
723
|
+
|
|
724
|
+
need_zip64 = @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
|
|
725
|
+
need_zip64 ||= @local_header_offset >= 0xFFFFFFFF unless for_local_header
|
|
726
|
+
if need_zip64
|
|
727
|
+
@version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
|
|
728
|
+
@extra.delete('Zip64Placeholder')
|
|
729
|
+
zip64 = @extra.create('Zip64')
|
|
730
|
+
if for_local_header
|
|
731
|
+
# local header always includes size and compressed size
|
|
732
|
+
zip64.original_size = @size
|
|
733
|
+
zip64.compressed_size = @compressed_size
|
|
734
|
+
else
|
|
735
|
+
# central directory entry entries include whichever fields are necessary
|
|
736
|
+
zip64.original_size = @size if @size >= 0xFFFFFFFF
|
|
737
|
+
zip64.compressed_size = @compressed_size if @compressed_size >= 0xFFFFFFFF
|
|
738
|
+
zip64.relative_header_offset = @local_header_offset if @local_header_offset >= 0xFFFFFFFF
|
|
739
|
+
end
|
|
740
|
+
else
|
|
741
|
+
@extra.delete('Zip64')
|
|
742
|
+
|
|
743
|
+
# if this is a local header entry, create a placeholder
|
|
744
|
+
# so we have room to write a zip64 extra field afterward
|
|
745
|
+
# (we won't know if it's needed until the file data is written)
|
|
746
|
+
if for_local_header
|
|
747
|
+
@extra.create('Zip64Placeholder')
|
|
748
|
+
else
|
|
749
|
+
@extra.delete('Zip64Placeholder')
|
|
750
|
+
end
|
|
614
751
|
end
|
|
615
752
|
end
|
|
616
|
-
|
|
617
753
|
end
|
|
618
754
|
end
|
|
619
755
|
|