rubyzip 2.3.1 → 3.0.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 +422 -0
- data/LICENSE.md +24 -0
- data/README.md +138 -41
- data/Rakefile +11 -7
- data/lib/zip/central_directory.rb +169 -123
- data/lib/zip/compressor.rb +3 -1
- data/lib/zip/constants.rb +29 -21
- data/lib/zip/crypto/decrypted_io.rb +4 -2
- data/lib/zip/crypto/encryption.rb +4 -2
- data/lib/zip/crypto/null_encryption.rb +6 -4
- data/lib/zip/crypto/traditional_encryption.rb +8 -6
- data/lib/zip/decompressor.rb +4 -3
- data/lib/zip/deflater.rb +10 -8
- data/lib/zip/dirtyable.rb +32 -0
- data/lib/zip/dos_time.rb +51 -5
- data/lib/zip/entry.rb +363 -222
- data/lib/zip/entry_set.rb +11 -9
- data/lib/zip/errors.rb +136 -16
- data/lib/zip/extra_field/generic.rb +6 -13
- 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 +33 -0
- data/lib/zip/extra_field/zip64.rb +12 -5
- data/lib/zip/extra_field.rb +16 -22
- data/lib/zip/file.rb +177 -223
- 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 +262 -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 +6 -4
- data/lib/zip/input_stream.rb +50 -37
- data/lib/zip/ioextras/abstract_input_stream.rb +15 -10
- data/lib/zip/ioextras/abstract_output_stream.rb +5 -3
- data/lib/zip/ioextras.rb +7 -7
- data/lib/zip/null_compressor.rb +3 -1
- data/lib/zip/null_decompressor.rb +3 -1
- data/lib/zip/null_input_stream.rb +3 -1
- data/lib/zip/output_stream.rb +53 -47
- data/lib/zip/pass_thru_compressor.rb +3 -1
- data/lib/zip/pass_thru_decompressor.rb +4 -2
- data/lib/zip/streamable_directory.rb +3 -1
- data/lib/zip/streamable_stream.rb +3 -0
- data/lib/zip/version.rb +3 -1
- data/lib/zip.rb +17 -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 +4 -2
- data/samples/qtzip.rb +6 -5
- data/samples/write_simple.rb +2 -1
- data/samples/zipfind.rb +1 -0
- metadata +83 -33
- data/TODO +0 -15
- data/lib/zip/extra_field/zip64_placeholder.rb +0 -15
data/lib/zip/entry.rb
CHANGED
@@ -1,21 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'pathname'
|
4
|
+
|
5
|
+
require_relative 'dirtyable'
|
6
|
+
|
2
7
|
module Zip
|
8
|
+
# Zip::Entry represents an entry in a Zip archive.
|
3
9
|
class Entry
|
4
|
-
|
5
|
-
|
10
|
+
include Dirtyable
|
11
|
+
|
12
|
+
# Constant used to specify that the entry is stored (i.e., not compressed).
|
13
|
+
STORED = ::Zip::COMPRESSION_METHOD_STORE
|
14
|
+
|
15
|
+
# Constant used to specify that the entry is deflated (i.e., compressed).
|
16
|
+
DEFLATED = ::Zip::COMPRESSION_METHOD_DEFLATE
|
17
|
+
|
6
18
|
# Language encoding flag (EFS) bit
|
7
|
-
EFS = 0b100000000000
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
:
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
+
EFS = 0b100000000000 # :nodoc:
|
20
|
+
|
21
|
+
# Compression level flags (used as part of the gp flags).
|
22
|
+
COMPRESSION_LEVEL_SUPERFAST_GPFLAG = 0b110 # :nodoc:
|
23
|
+
COMPRESSION_LEVEL_FAST_GPFLAG = 0b100 # :nodoc:
|
24
|
+
COMPRESSION_LEVEL_MAX_GPFLAG = 0b010 # :nodoc:
|
25
|
+
|
26
|
+
attr_accessor :comment, :compressed_size, :follow_symlinks, :name,
|
27
|
+
:restore_ownership, :restore_permissions, :restore_times,
|
28
|
+
:unix_gid, :unix_perms, :unix_uid
|
29
|
+
|
30
|
+
attr_accessor :crc, :external_file_attributes, :fstype, :gp_flags,
|
31
|
+
:internal_file_attributes, :local_header_offset # :nodoc:
|
32
|
+
|
33
|
+
attr_reader :extra, :compression_level, :filepath # :nodoc:
|
34
|
+
|
35
|
+
attr_writer :size # :nodoc:
|
36
|
+
|
37
|
+
mark_dirty :comment=, :compressed_size=, :external_file_attributes=,
|
38
|
+
:fstype=, :gp_flags=, :name=, :size=,
|
39
|
+
:unix_gid=, :unix_perms=, :unix_uid=
|
40
|
+
|
41
|
+
def set_default_vars_values # :nodoc:
|
19
42
|
@local_header_offset = 0
|
20
43
|
@local_header_size = nil # not known until local entry is created or read
|
21
44
|
@internal_file_attributes = 1
|
@@ -34,178 +57,252 @@ module Zip
|
|
34
57
|
end
|
35
58
|
@follow_symlinks = false
|
36
59
|
|
37
|
-
@restore_times =
|
38
|
-
@restore_permissions =
|
39
|
-
@restore_ownership =
|
60
|
+
@restore_times = DEFAULT_RESTORE_OPTIONS[:restore_times]
|
61
|
+
@restore_permissions = DEFAULT_RESTORE_OPTIONS[:restore_permissions]
|
62
|
+
@restore_ownership = DEFAULT_RESTORE_OPTIONS[:restore_ownership]
|
40
63
|
# BUG: need an extra field to support uid/gid's
|
41
64
|
@unix_uid = nil
|
42
65
|
@unix_gid = nil
|
43
66
|
@unix_perms = nil
|
44
|
-
# @posix_acl = nil
|
45
|
-
# @ntfs_acl = nil
|
46
|
-
@dirty = false
|
47
67
|
end
|
48
68
|
|
49
|
-
def check_name(name)
|
50
|
-
|
51
|
-
|
52
|
-
raise ::Zip::EntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
|
69
|
+
def check_name(name) # :nodoc:
|
70
|
+
raise EntryNameError, name if name.start_with?('/')
|
71
|
+
raise EntryNameError if name.length > 65_535
|
53
72
|
end
|
54
73
|
|
55
|
-
|
56
|
-
|
57
|
-
|
74
|
+
# Create a new Zip::Entry.
|
75
|
+
def initialize(
|
76
|
+
zipfile = '', name = '',
|
77
|
+
comment: '', size: nil, compressed_size: 0, crc: 0,
|
78
|
+
compression_method: DEFLATED,
|
79
|
+
compression_level: ::Zip.default_compression,
|
80
|
+
time: ::Zip::DOSTime.now, extra: ::Zip::ExtraField.new
|
81
|
+
)
|
82
|
+
super()
|
83
|
+
@name = name
|
84
|
+
check_name(@name)
|
58
85
|
|
59
86
|
set_default_vars_values
|
60
87
|
@fstype = ::Zip::RUNNING_ON_WINDOWS ? ::Zip::FSTYPE_FAT : ::Zip::FSTYPE_UNIX
|
61
88
|
|
62
|
-
@zipfile =
|
63
|
-
@
|
64
|
-
@
|
65
|
-
@
|
66
|
-
@compressed_size =
|
67
|
-
@crc =
|
68
|
-
@
|
69
|
-
@
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
89
|
+
@zipfile = zipfile
|
90
|
+
@comment = comment || ''
|
91
|
+
@compression_method = compression_method || DEFLATED
|
92
|
+
@compression_level = compression_level || ::Zip.default_compression
|
93
|
+
@compressed_size = compressed_size || 0
|
94
|
+
@crc = crc || 0
|
95
|
+
@size = size
|
96
|
+
@time = case time
|
97
|
+
when ::Zip::DOSTime
|
98
|
+
time
|
99
|
+
when Time
|
100
|
+
::Zip::DOSTime.from_time(time)
|
101
|
+
else
|
102
|
+
::Zip::DOSTime.now
|
103
|
+
end
|
104
|
+
@extra =
|
105
|
+
extra.kind_of?(ExtraField) ? extra : ExtraField.new(extra.to_s)
|
106
|
+
|
107
|
+
set_compression_level_flags
|
108
|
+
end
|
109
|
+
|
110
|
+
# Is this entry encrypted?
|
76
111
|
def encrypted?
|
77
112
|
gp_flags & 1 == 1
|
78
113
|
end
|
79
114
|
|
80
|
-
def incomplete?
|
81
|
-
gp_flags & 8 == 8
|
115
|
+
def incomplete? # :nodoc:
|
116
|
+
(gp_flags & 8 == 8) && (crc == 0 || size == 0 || compressed_size == 0)
|
82
117
|
end
|
83
118
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
119
|
+
# The uncompressed size of the entry.
|
120
|
+
def size
|
121
|
+
@size || 0
|
122
|
+
end
|
123
|
+
|
124
|
+
# Get a timestamp component of this entry.
|
125
|
+
#
|
126
|
+
# Returns modification time by default.
|
127
|
+
def time(component: :mtime)
|
128
|
+
time =
|
129
|
+
if @extra['UniversalTime']
|
130
|
+
@extra['UniversalTime'].send(component)
|
131
|
+
elsif @extra['NTFS']
|
132
|
+
@extra['NTFS'].send(component)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Standard time field in central directory has local time
|
136
|
+
# under archive creator. Then, we can't get timezone.
|
137
|
+
time || (@time if component == :mtime)
|
94
138
|
end
|
95
139
|
|
96
140
|
alias mtime time
|
97
141
|
|
98
|
-
|
142
|
+
# Get the last access time of this entry, if available.
|
143
|
+
def atime
|
144
|
+
time(component: :atime)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Get the creation time of this entry, if available.
|
148
|
+
def ctime
|
149
|
+
time(component: :ctime)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Set a timestamp component of this entry.
|
153
|
+
#
|
154
|
+
# Sets modification time by default.
|
155
|
+
def time=(value, component: :mtime)
|
156
|
+
@dirty = true
|
99
157
|
unless @extra.member?('UniversalTime') || @extra.member?('NTFS')
|
100
158
|
@extra.create('UniversalTime')
|
101
159
|
end
|
102
|
-
|
103
|
-
|
160
|
+
|
161
|
+
value = DOSTime.from_time(value)
|
162
|
+
comp = "#{component}=" unless component.to_s.end_with?('=')
|
163
|
+
(@extra['UniversalTime'] || @extra['NTFS']).send(comp, value)
|
164
|
+
@time = value if component == :mtime
|
104
165
|
end
|
105
166
|
|
106
|
-
|
107
|
-
raise InternalError, "current filetype is unknown: #{inspect}" unless @ftype
|
167
|
+
alias mtime= time=
|
108
168
|
|
109
|
-
|
169
|
+
# Set the last access time of this entry.
|
170
|
+
def atime=(value)
|
171
|
+
send(:time=, value, component: :atime)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Set the creation time of this entry.
|
175
|
+
def ctime=(value)
|
176
|
+
send(:time=, value, component: :ctime)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Does this entry return time fields with accurate timezone information?
|
180
|
+
def absolute_time?
|
181
|
+
@extra.member?('UniversalTime') || @extra.member?('NTFS')
|
182
|
+
end
|
183
|
+
|
184
|
+
# Return the compression method for this entry.
|
185
|
+
#
|
186
|
+
# Returns STORED if the entry is a directory or if the compression
|
187
|
+
# level is 0.
|
188
|
+
def compression_method
|
189
|
+
return STORED if ftype == :directory || @compression_level == 0
|
190
|
+
|
191
|
+
@compression_method
|
192
|
+
end
|
193
|
+
|
194
|
+
# Set the compression method for this entry.
|
195
|
+
def compression_method=(method)
|
196
|
+
@dirty = true
|
197
|
+
@compression_method = (ftype == :directory ? STORED : method)
|
198
|
+
end
|
199
|
+
|
200
|
+
# Does this entry use the ZIP64 extensions?
|
201
|
+
def zip64?
|
202
|
+
!@extra['Zip64'].nil?
|
203
|
+
end
|
204
|
+
|
205
|
+
def file_type_is?(type) # :nodoc:
|
206
|
+
ftype == type
|
207
|
+
end
|
208
|
+
|
209
|
+
def ftype # :nodoc:
|
210
|
+
@ftype ||= name_is_directory? ? :directory : :file
|
110
211
|
end
|
111
212
|
|
112
213
|
# Dynamic checkers
|
113
214
|
%w[directory file symlink].each do |k|
|
114
|
-
define_method "#{k}?" do
|
215
|
+
define_method :"#{k}?" do
|
115
216
|
file_type_is?(k.to_sym)
|
116
217
|
end
|
117
218
|
end
|
118
219
|
|
119
|
-
def name_is_directory?
|
220
|
+
def name_is_directory? # :nodoc:
|
120
221
|
@name.end_with?('/')
|
121
222
|
end
|
122
223
|
|
123
224
|
# Is the name a relative path, free of `..` patterns that could lead to
|
124
225
|
# path traversal attacks? This does NOT handle symlinks; if the path
|
125
226
|
# contains symlinks, this check is NOT enough to guarantee safety.
|
126
|
-
def name_safe?
|
227
|
+
def name_safe? # :nodoc:
|
127
228
|
cleanpath = Pathname.new(@name).cleanpath
|
128
229
|
return false unless cleanpath.relative?
|
129
230
|
|
130
231
|
root = ::File::SEPARATOR
|
131
|
-
|
132
|
-
|
232
|
+
naive = ::File.join(root, cleanpath.to_s)
|
233
|
+
# Allow for Windows drive mappings at the root.
|
234
|
+
::File.absolute_path(cleanpath.to_s, root).match?(/([A-Z]:)?#{naive}/i)
|
133
235
|
end
|
134
236
|
|
135
|
-
def local_entry_offset
|
237
|
+
def local_entry_offset # :nodoc:
|
136
238
|
local_header_offset + @local_header_size
|
137
239
|
end
|
138
240
|
|
139
|
-
def name_size
|
241
|
+
def name_size # :nodoc:
|
140
242
|
@name ? @name.bytesize : 0
|
141
243
|
end
|
142
244
|
|
143
|
-
def extra_size
|
245
|
+
def extra_size # :nodoc:
|
144
246
|
@extra ? @extra.local_size : 0
|
145
247
|
end
|
146
248
|
|
147
|
-
def comment_size
|
249
|
+
def comment_size # :nodoc:
|
148
250
|
@comment ? @comment.bytesize : 0
|
149
251
|
end
|
150
252
|
|
151
|
-
def calculate_local_header_size
|
253
|
+
def calculate_local_header_size # :nodoc:
|
152
254
|
LOCAL_ENTRY_STATIC_HEADER_LENGTH + name_size + extra_size
|
153
255
|
end
|
154
256
|
|
155
257
|
# check before rewriting an entry (after file sizes are known)
|
156
258
|
# that we didn't change the header size (and thus clobber file data or something)
|
157
|
-
def verify_local_header_size!
|
259
|
+
def verify_local_header_size! # :nodoc:
|
158
260
|
return if @local_header_size.nil?
|
159
261
|
|
160
262
|
new_size = calculate_local_header_size
|
161
|
-
|
263
|
+
return unless @local_header_size != new_size
|
264
|
+
|
265
|
+
raise Error,
|
266
|
+
"Local header size changed (#{@local_header_size} -> #{new_size})"
|
162
267
|
end
|
163
268
|
|
164
|
-
def cdir_header_size
|
269
|
+
def cdir_header_size # :nodoc:
|
165
270
|
CDIR_ENTRY_STATIC_HEADER_LENGTH + name_size +
|
166
271
|
(@extra ? @extra.c_dir_size : 0) + comment_size
|
167
272
|
end
|
168
273
|
|
169
|
-
def next_header_offset
|
170
|
-
local_entry_offset + compressed_size
|
274
|
+
def next_header_offset # :nodoc:
|
275
|
+
local_entry_offset + compressed_size
|
171
276
|
end
|
172
277
|
|
173
|
-
# Extracts entry to file
|
174
|
-
#
|
175
|
-
#
|
176
|
-
|
177
|
-
|
178
|
-
|
278
|
+
# Extracts this entry to a file at `entry_path`, with
|
279
|
+
# `destination_directory` as the base location in the filesystem.
|
280
|
+
#
|
281
|
+
# NB: The caller is responsible for making sure `destination_directory` is
|
282
|
+
# safe, if it is passed.
|
283
|
+
def extract(entry_path = @name, destination_directory: '.', &block)
|
284
|
+
dest_dir = ::File.absolute_path(destination_directory || '.')
|
285
|
+
extract_path = ::File.absolute_path(::File.join(dest_dir, entry_path))
|
286
|
+
|
287
|
+
unless extract_path.start_with?(dest_dir)
|
288
|
+
warn "WARNING: skipped extracting '#{@name}' to '#{extract_path}' as unsafe."
|
179
289
|
return self
|
180
290
|
end
|
181
291
|
|
182
|
-
dest_path ||= @name
|
183
292
|
block ||= proc { ::Zip.on_exists_proc }
|
184
293
|
|
185
294
|
raise "unknown file type #{inspect}" unless directory? || file? || symlink?
|
186
295
|
|
187
|
-
__send__("create_#{
|
296
|
+
__send__(:"create_#{ftype}", extract_path, &block)
|
188
297
|
self
|
189
298
|
end
|
190
299
|
|
191
|
-
def to_s
|
300
|
+
def to_s # :nodoc:
|
192
301
|
@name
|
193
302
|
end
|
194
303
|
|
195
304
|
class << self
|
196
|
-
def
|
197
|
-
io.read(2).unpack1('v')
|
198
|
-
end
|
199
|
-
|
200
|
-
def read_zip_long(io) # :nodoc:
|
201
|
-
io.read(4).unpack1('V')
|
202
|
-
end
|
203
|
-
|
204
|
-
def read_zip_64_long(io) # :nodoc:
|
205
|
-
io.read(8).unpack1('Q<')
|
206
|
-
end
|
207
|
-
|
208
|
-
def read_c_dir_entry(io) #:nodoc:all
|
305
|
+
def read_c_dir_entry(io) # :nodoc:
|
209
306
|
path = if io.respond_to?(:path)
|
210
307
|
io.path
|
211
308
|
else
|
@@ -218,16 +315,18 @@ module Zip
|
|
218
315
|
nil
|
219
316
|
end
|
220
317
|
|
221
|
-
def read_local_entry(io)
|
318
|
+
def read_local_entry(io) # :nodoc:
|
222
319
|
entry = new(io)
|
223
320
|
entry.read_local_entry(io)
|
224
321
|
entry
|
322
|
+
rescue SplitArchiveError
|
323
|
+
raise
|
225
324
|
rescue Error
|
226
325
|
nil
|
227
326
|
end
|
228
327
|
end
|
229
328
|
|
230
|
-
def unpack_local_entry(buf)
|
329
|
+
def unpack_local_entry(buf) # :nodoc:
|
231
330
|
@header_signature,
|
232
331
|
@version,
|
233
332
|
@fstype,
|
@@ -242,62 +341,66 @@ module Zip
|
|
242
341
|
@extra_length = buf.unpack('VCCvvvvVVVvv')
|
243
342
|
end
|
244
343
|
|
245
|
-
def read_local_entry(io)
|
246
|
-
@
|
344
|
+
def read_local_entry(io) # :nodoc:
|
345
|
+
@dirty = false # No changes at this point.
|
346
|
+
current_offset = io.tell
|
247
347
|
|
248
|
-
|
348
|
+
read_local_header_fields(io)
|
249
349
|
|
250
|
-
|
251
|
-
raise
|
252
|
-
end
|
350
|
+
if @header_signature == SPLIT_FILE_SIGNATURE
|
351
|
+
raise SplitArchiveError if current_offset.zero?
|
253
352
|
|
254
|
-
|
353
|
+
# Rewind, skipping the data descriptor, then try to read the local header again.
|
354
|
+
current_offset += 16
|
355
|
+
io.seek(current_offset)
|
356
|
+
read_local_header_fields(io)
|
357
|
+
end
|
255
358
|
|
256
|
-
unless @header_signature ==
|
257
|
-
raise
|
359
|
+
unless @header_signature == LOCAL_ENTRY_SIGNATURE
|
360
|
+
raise Error, "Zip local header magic not found at location '#{current_offset}'"
|
258
361
|
end
|
259
362
|
|
363
|
+
@local_header_offset = current_offset
|
364
|
+
|
260
365
|
set_time(@last_mod_date, @last_mod_time)
|
261
366
|
|
262
367
|
@name = io.read(@name_length)
|
263
|
-
extra = io.read(@extra_length)
|
264
|
-
|
265
|
-
@name.tr!('\\', '/')
|
266
368
|
if ::Zip.force_entry_names_encoding
|
267
369
|
@name.force_encoding(::Zip.force_entry_names_encoding)
|
268
370
|
end
|
371
|
+
@name.tr!('\\', '/') # Normalise filepath separators after encoding set.
|
372
|
+
|
373
|
+
# We need to do this here because `initialize` has so many side-effects.
|
374
|
+
# :-(
|
375
|
+
@ftype = name_is_directory? ? :directory : :file
|
269
376
|
|
377
|
+
extra = io.read(@extra_length)
|
270
378
|
if extra && extra.bytesize != @extra_length
|
271
379
|
raise ::Zip::Error, 'Truncated local zip entry header'
|
272
380
|
end
|
273
381
|
|
274
|
-
|
275
|
-
@extra.merge(extra) if extra
|
276
|
-
else
|
277
|
-
@extra = ::Zip::ExtraField.new(extra)
|
278
|
-
end
|
279
|
-
|
382
|
+
read_extra_field(extra, local: true)
|
280
383
|
parse_zip64_extra(true)
|
281
384
|
@local_header_size = calculate_local_header_size
|
282
385
|
end
|
283
386
|
|
284
|
-
def pack_local_entry
|
387
|
+
def pack_local_entry # :nodoc:
|
285
388
|
zip64 = @extra['Zip64']
|
286
389
|
[::Zip::LOCAL_ENTRY_SIGNATURE,
|
287
390
|
@version_needed_to_extract, # version needed to extract
|
288
391
|
@gp_flags, # @gp_flags
|
289
|
-
|
392
|
+
compression_method,
|
290
393
|
@time.to_binary_dos_time, # @last_mod_time
|
291
394
|
@time.to_binary_dos_date, # @last_mod_date
|
292
395
|
@crc,
|
293
396
|
zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
|
294
|
-
zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
|
397
|
+
zip64 && zip64.original_size ? 0xFFFFFFFF : (@size || 0),
|
295
398
|
name_size,
|
296
399
|
@extra ? @extra.local_size : 0].pack('VvvvvvVVVvv')
|
297
400
|
end
|
298
401
|
|
299
|
-
def write_local_entry(io, rewrite
|
300
|
-
|
402
|
+
def write_local_entry(io, rewrite: false) # :nodoc:
|
403
|
+
prep_local_zip64_extra
|
301
404
|
verify_local_header_size! if rewrite
|
302
405
|
@local_header_offset = io.tell
|
303
406
|
|
@@ -308,7 +411,7 @@ module Zip
|
|
308
411
|
@local_header_size = io.tell - @local_header_offset
|
309
412
|
end
|
310
413
|
|
311
|
-
def unpack_c_dir_entry(buf)
|
414
|
+
def unpack_c_dir_entry(buf) # :nodoc:
|
312
415
|
@header_signature,
|
313
416
|
@version, # version of encoding software
|
314
417
|
@fstype, # filesystem type
|
@@ -332,7 +435,7 @@ module Zip
|
|
332
435
|
@comment = buf.unpack('VCCvvvvvVVVvvvvvVV')
|
333
436
|
end
|
334
437
|
|
335
|
-
def set_ftype_from_c_dir_entry
|
438
|
+
def set_ftype_from_c_dir_entry # :nodoc:
|
336
439
|
@ftype = case @fstype
|
337
440
|
when ::Zip::FSTYPE_UNIX
|
338
441
|
@unix_perms = (@external_file_attributes >> 16) & 0o7777
|
@@ -344,8 +447,9 @@ module Zip
|
|
344
447
|
when ::Zip::FILE_TYPE_SYMLINK
|
345
448
|
:symlink
|
346
449
|
else
|
347
|
-
#
|
348
|
-
# Otherwise this would be set to unknown and that
|
450
|
+
# Best case guess for whether it is a file or not.
|
451
|
+
# Otherwise this would be set to unknown and that
|
452
|
+
# entry would never be able to be extracted.
|
349
453
|
if name_is_directory?
|
350
454
|
:directory
|
351
455
|
else
|
@@ -361,43 +465,47 @@ module Zip
|
|
361
465
|
end
|
362
466
|
end
|
363
467
|
|
364
|
-
def check_c_dir_entry_static_header_length(buf)
|
365
|
-
return
|
468
|
+
def check_c_dir_entry_static_header_length(buf) # :nodoc:
|
469
|
+
return unless buf.nil? || buf.bytesize != ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
|
366
470
|
|
367
471
|
raise Error, 'Premature end of file. Not enough data for zip cdir entry header'
|
368
472
|
end
|
369
473
|
|
370
|
-
def check_c_dir_entry_signature
|
371
|
-
return if header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
|
474
|
+
def check_c_dir_entry_signature # :nodoc:
|
475
|
+
return if @header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
|
372
476
|
|
373
477
|
raise Error, "Zip local header magic not found at location '#{local_header_offset}'"
|
374
478
|
end
|
375
479
|
|
376
|
-
def check_c_dir_entry_comment_size
|
480
|
+
def check_c_dir_entry_comment_size # :nodoc:
|
377
481
|
return if @comment && @comment.bytesize == @comment_length
|
378
482
|
|
379
483
|
raise ::Zip::Error, 'Truncated cdir zip entry header'
|
380
484
|
end
|
381
485
|
|
382
|
-
def
|
486
|
+
def read_extra_field(buf, local: false) # :nodoc:
|
383
487
|
if @extra.kind_of?(::Zip::ExtraField)
|
384
|
-
@extra.merge(
|
488
|
+
@extra.merge(buf, local: local) if buf
|
385
489
|
else
|
386
|
-
@extra = ::Zip::ExtraField.new(
|
490
|
+
@extra = ::Zip::ExtraField.new(buf, local: local)
|
387
491
|
end
|
388
492
|
end
|
389
493
|
|
390
|
-
def read_c_dir_entry(io)
|
494
|
+
def read_c_dir_entry(io) # :nodoc:
|
495
|
+
@dirty = false # No changes at this point.
|
391
496
|
static_sized_fields_buf = io.read(::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH)
|
392
497
|
check_c_dir_entry_static_header_length(static_sized_fields_buf)
|
393
498
|
unpack_c_dir_entry(static_sized_fields_buf)
|
394
499
|
check_c_dir_entry_signature
|
395
500
|
set_time(@last_mod_date, @last_mod_time)
|
501
|
+
|
396
502
|
@name = io.read(@name_length)
|
397
503
|
if ::Zip.force_entry_names_encoding
|
398
504
|
@name.force_encoding(::Zip.force_entry_names_encoding)
|
399
505
|
end
|
400
|
-
|
506
|
+
@name.tr!('\\', '/') # Normalise filepath separators after encoding set.
|
507
|
+
|
508
|
+
read_extra_field(io.read(@extra_length))
|
401
509
|
@comment = io.read(@comment_length)
|
402
510
|
check_c_dir_entry_comment_size
|
403
511
|
set_ftype_from_c_dir_entry
|
@@ -413,27 +521,27 @@ module Zip
|
|
413
521
|
end
|
414
522
|
|
415
523
|
def get_extra_attributes_from_path(path) # :nodoc:
|
416
|
-
|
524
|
+
stat = file_stat(path)
|
525
|
+
@time = DOSTime.from_time(stat.mtime)
|
526
|
+
return if ::Zip::RUNNING_ON_WINDOWS
|
417
527
|
|
418
|
-
stat = file_stat(path)
|
419
528
|
@unix_uid = stat.uid
|
420
529
|
@unix_gid = stat.gid
|
421
530
|
@unix_perms = stat.mode & 0o7777
|
422
|
-
@time = ::Zip::DOSTime.from_time(stat.mtime)
|
423
531
|
end
|
424
532
|
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
unix_perms_mask = 0o7777
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
::FileUtils.touch(dest_path, mtime: time) if @restore_times
|
533
|
+
# rubocop:disable Style/GuardClause
|
534
|
+
def set_unix_attributes_on_path(dest_path) # :nodoc:
|
535
|
+
# Ignore setuid/setgid bits by default. Honour if @restore_ownership.
|
536
|
+
unix_perms_mask = (@restore_ownership ? 0o7777 : 0o1777)
|
537
|
+
if @restore_permissions && @unix_perms
|
538
|
+
::FileUtils.chmod(@unix_perms & unix_perms_mask, dest_path)
|
539
|
+
end
|
540
|
+
if @restore_ownership && @unix_uid && @unix_gid && ::Process.egid == 0
|
541
|
+
::FileUtils.chown(@unix_uid, @unix_gid, dest_path)
|
542
|
+
end
|
436
543
|
end
|
544
|
+
# rubocop:enable Style/GuardClause
|
437
545
|
|
438
546
|
def set_extra_attributes_on_path(dest_path) # :nodoc:
|
439
547
|
return unless file? || directory?
|
@@ -442,9 +550,14 @@ module Zip
|
|
442
550
|
when ::Zip::FSTYPE_UNIX
|
443
551
|
set_unix_attributes_on_path(dest_path)
|
444
552
|
end
|
553
|
+
|
554
|
+
# Restore the timestamp on a file. This will either have come from the
|
555
|
+
# original source file that was copied into the archive, or from the
|
556
|
+
# creation date of the archive if there was no original source file.
|
557
|
+
::FileUtils.touch(dest_path, mtime: time) if @restore_times
|
445
558
|
end
|
446
559
|
|
447
|
-
def pack_c_dir_entry
|
560
|
+
def pack_c_dir_entry # :nodoc:
|
448
561
|
zip64 = @extra['Zip64']
|
449
562
|
[
|
450
563
|
@header_signature,
|
@@ -452,12 +565,12 @@ module Zip
|
|
452
565
|
@fstype, # filesystem type
|
453
566
|
@version_needed_to_extract, # @versionNeededToExtract
|
454
567
|
@gp_flags, # @gp_flags
|
455
|
-
|
568
|
+
compression_method,
|
456
569
|
@time.to_binary_dos_time, # @last_mod_time
|
457
570
|
@time.to_binary_dos_date, # @last_mod_date
|
458
571
|
@crc,
|
459
572
|
zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
|
460
|
-
zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
|
573
|
+
zip64 && zip64.original_size ? 0xFFFFFFFF : (@size || 0),
|
461
574
|
name_size,
|
462
575
|
@extra ? @extra.c_dir_size : 0,
|
463
576
|
comment_size,
|
@@ -471,11 +584,12 @@ module Zip
|
|
471
584
|
].pack('VCCvvvvvVVVvvvvvVV')
|
472
585
|
end
|
473
586
|
|
474
|
-
def write_c_dir_entry(io)
|
475
|
-
|
587
|
+
def write_c_dir_entry(io) # :nodoc:
|
588
|
+
prep_cdir_zip64_extra
|
589
|
+
|
476
590
|
case @fstype
|
477
591
|
when ::Zip::FSTYPE_UNIX
|
478
|
-
ft = case
|
592
|
+
ft = case ftype
|
479
593
|
when :file
|
480
594
|
@unix_perms ||= 0o644
|
481
595
|
::Zip::FILE_TYPE_FILE
|
@@ -488,7 +602,7 @@ module Zip
|
|
488
602
|
end
|
489
603
|
|
490
604
|
unless ft.nil?
|
491
|
-
@external_file_attributes = (ft << 12 | (@unix_perms & 0o7777)) << 16
|
605
|
+
@external_file_attributes = ((ft << 12) | (@unix_perms & 0o7777)) << 16
|
492
606
|
end
|
493
607
|
end
|
494
608
|
|
@@ -499,43 +613,42 @@ module Zip
|
|
499
613
|
io << @comment
|
500
614
|
end
|
501
615
|
|
502
|
-
def ==(other)
|
616
|
+
def ==(other) # :nodoc:
|
503
617
|
return false unless other.class == self.class
|
504
618
|
|
505
619
|
# Compares contents of local entry and exposed fields
|
506
|
-
|
620
|
+
%w[compression_method crc compressed_size size name extra filepath time].all? do |k|
|
507
621
|
other.__send__(k.to_sym) == __send__(k.to_sym)
|
508
622
|
end
|
509
|
-
keys_equal && time.dos_equals(other.time)
|
510
623
|
end
|
511
624
|
|
512
|
-
def <=>(other)
|
625
|
+
def <=>(other) # :nodoc:
|
513
626
|
to_s <=> other.to_s
|
514
627
|
end
|
515
628
|
|
516
629
|
# Returns an IO like object for the given ZipEntry.
|
517
630
|
# Warning: may behave weird with symlinks.
|
518
631
|
def get_input_stream(&block)
|
519
|
-
if
|
520
|
-
yield ::Zip::NullInputStream if
|
632
|
+
if ftype == :directory
|
633
|
+
yield ::Zip::NullInputStream if block
|
521
634
|
::Zip::NullInputStream
|
522
635
|
elsif @filepath
|
523
|
-
case
|
636
|
+
case ftype
|
524
637
|
when :file
|
525
638
|
::File.open(@filepath, 'rb', &block)
|
526
639
|
when :symlink
|
527
640
|
linkpath = ::File.readlink(@filepath)
|
528
641
|
stringio = ::StringIO.new(linkpath)
|
529
|
-
yield(stringio) if
|
642
|
+
yield(stringio) if block
|
530
643
|
stringio
|
531
644
|
else
|
532
|
-
raise "unknown @file_type #{
|
645
|
+
raise "unknown @file_type #{ftype}"
|
533
646
|
end
|
534
647
|
else
|
535
|
-
zis = ::Zip::InputStream.new(@zipfile, local_header_offset)
|
648
|
+
zis = ::Zip::InputStream.new(@zipfile, offset: local_header_offset)
|
536
649
|
zis.instance_variable_set(:@complete_entry, self)
|
537
650
|
zis.get_next_entry
|
538
|
-
if
|
651
|
+
if block
|
539
652
|
begin
|
540
653
|
yield(zis)
|
541
654
|
ensure
|
@@ -554,7 +667,7 @@ module Zip
|
|
554
667
|
if name_is_directory?
|
555
668
|
raise ArgumentError,
|
556
669
|
"entry name '#{newEntry}' indicates directory entry, but " \
|
557
|
-
|
670
|
+
"'#{src_path}' is not a directory"
|
558
671
|
end
|
559
672
|
:file
|
560
673
|
when 'directory'
|
@@ -564,7 +677,7 @@ module Zip
|
|
564
677
|
if name_is_directory?
|
565
678
|
raise ArgumentError,
|
566
679
|
"entry name '#{newEntry}' indicates directory entry, but " \
|
567
|
-
|
680
|
+
"'#{src_path}' is not a directory"
|
568
681
|
end
|
569
682
|
:symlink
|
570
683
|
else
|
@@ -572,27 +685,30 @@ module Zip
|
|
572
685
|
end
|
573
686
|
|
574
687
|
@filepath = src_path
|
688
|
+
@size = stat.size
|
575
689
|
get_extra_attributes_from_path(@filepath)
|
576
690
|
end
|
577
691
|
|
578
|
-
def write_to_zip_output_stream(zip_output_stream)
|
579
|
-
if
|
580
|
-
zip_output_stream.put_next_entry(self
|
692
|
+
def write_to_zip_output_stream(zip_output_stream) # :nodoc:
|
693
|
+
if ftype == :directory
|
694
|
+
zip_output_stream.put_next_entry(self)
|
581
695
|
elsif @filepath
|
582
|
-
zip_output_stream.put_next_entry(self
|
583
|
-
get_input_stream
|
696
|
+
zip_output_stream.put_next_entry(self)
|
697
|
+
get_input_stream do |is|
|
698
|
+
::Zip::IOExtras.copy_stream(zip_output_stream, is)
|
699
|
+
end
|
584
700
|
else
|
585
701
|
zip_output_stream.copy_raw_entry(self)
|
586
702
|
end
|
587
703
|
end
|
588
704
|
|
589
|
-
def parent_as_string
|
705
|
+
def parent_as_string # :nodoc:
|
590
706
|
entry_name = name.chomp('/')
|
591
707
|
slash_index = entry_name.rindex('/')
|
592
708
|
slash_index ? entry_name.slice(0, slash_index + 1) : nil
|
593
709
|
end
|
594
710
|
|
595
|
-
def get_raw_input_stream(&block)
|
711
|
+
def get_raw_input_stream(&block) # :nodoc:
|
596
712
|
if @zipfile.respond_to?(:seek) && @zipfile.respond_to?(:read)
|
597
713
|
yield @zipfile
|
598
714
|
else
|
@@ -600,12 +716,22 @@ module Zip
|
|
600
716
|
end
|
601
717
|
end
|
602
718
|
|
603
|
-
def clean_up
|
604
|
-
#
|
719
|
+
def clean_up # :nodoc:
|
720
|
+
@dirty = false # Any changes are written at this point.
|
605
721
|
end
|
606
722
|
|
607
723
|
private
|
608
724
|
|
725
|
+
def read_local_header_fields(io) # :nodoc:
|
726
|
+
static_sized_fields_buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
|
727
|
+
|
728
|
+
unless static_sized_fields_buf.bytesize == ::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH
|
729
|
+
raise Error, 'Premature end of file. Not enough data for zip entry local header'
|
730
|
+
end
|
731
|
+
|
732
|
+
unpack_local_entry(static_sized_fields_buf)
|
733
|
+
end
|
734
|
+
|
609
735
|
def set_time(binary_dos_date, binary_dos_time)
|
610
736
|
@time = ::Zip::DOSTime.parse_binary_dos_format(binary_dos_date, binary_dos_time)
|
611
737
|
rescue ArgumentError
|
@@ -614,9 +740,9 @@ module Zip
|
|
614
740
|
|
615
741
|
def create_file(dest_path, _continue_on_exists_proc = proc { Zip.continue_on_exists_proc })
|
616
742
|
if ::File.exist?(dest_path) && !yield(self, dest_path)
|
617
|
-
raise ::Zip::
|
618
|
-
"Destination '#{dest_path}' already exists"
|
743
|
+
raise ::Zip::DestinationExistsError, dest_path
|
619
744
|
end
|
745
|
+
|
620
746
|
::File.open(dest_path, 'wb') do |os|
|
621
747
|
get_input_stream do |is|
|
622
748
|
bytes_written = 0
|
@@ -627,10 +753,10 @@ module Zip
|
|
627
753
|
bytes_written += buf.bytesize
|
628
754
|
next unless bytes_written > size && !warned
|
629
755
|
|
630
|
-
|
631
|
-
raise
|
756
|
+
error = ::Zip::EntrySizeError.new(self)
|
757
|
+
raise error if ::Zip.validate_entry_sizes
|
632
758
|
|
633
|
-
warn "WARNING: #{message}"
|
759
|
+
warn "WARNING: #{error.message}"
|
634
760
|
warned = true
|
635
761
|
end
|
636
762
|
end
|
@@ -643,14 +769,11 @@ module Zip
|
|
643
769
|
return if ::File.directory?(dest_path)
|
644
770
|
|
645
771
|
if ::File.exist?(dest_path)
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
raise ::Zip::DestinationFileExistsError,
|
650
|
-
"Cannot create directory '#{dest_path}'. " \
|
651
|
-
'A file already exists with that name'
|
652
|
-
end
|
772
|
+
raise ::Zip::DestinationExistsError, dest_path unless block_given? && yield(self, dest_path)
|
773
|
+
|
774
|
+
::FileUtils.rm_f dest_path
|
653
775
|
end
|
776
|
+
|
654
777
|
::FileUtils.mkdir_p(dest_path)
|
655
778
|
set_extra_attributes_on_path(dest_path)
|
656
779
|
end
|
@@ -664,53 +787,71 @@ module Zip
|
|
664
787
|
|
665
788
|
# apply missing data from the zip64 extra information field, if present
|
666
789
|
# (required when file sizes exceed 2**32, but can be used for all files)
|
667
|
-
def parse_zip64_extra(for_local_header)
|
668
|
-
return
|
790
|
+
def parse_zip64_extra(for_local_header) # :nodoc:
|
791
|
+
return unless zip64?
|
669
792
|
|
670
793
|
if for_local_header
|
671
794
|
@size, @compressed_size = @extra['Zip64'].parse(@size, @compressed_size)
|
672
795
|
else
|
673
|
-
@size, @compressed_size, @local_header_offset = @extra['Zip64'].parse(
|
796
|
+
@size, @compressed_size, @local_header_offset = @extra['Zip64'].parse(
|
797
|
+
@size, @compressed_size, @local_header_offset
|
798
|
+
)
|
674
799
|
end
|
675
800
|
end
|
676
801
|
|
677
|
-
|
678
|
-
|
802
|
+
# For DEFLATED compression *only*: set the general purpose flags 1 and 2 to
|
803
|
+
# indicate compression level. This seems to be mainly cosmetic but they are
|
804
|
+
# generally set by other tools - including in docx files. It is these flags
|
805
|
+
# that are used by commandline tools (and elsewhere) to give an indication
|
806
|
+
# of how compressed a file is. See the PKWARE APPNOTE for more information:
|
807
|
+
# https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
|
808
|
+
#
|
809
|
+
# It's safe to simply OR these flags here as compression_level is read only.
|
810
|
+
def set_compression_level_flags
|
811
|
+
return unless compression_method == DEFLATED
|
812
|
+
|
813
|
+
case @compression_level
|
814
|
+
when 1
|
815
|
+
@gp_flags |= COMPRESSION_LEVEL_SUPERFAST_GPFLAG
|
816
|
+
when 2
|
817
|
+
@gp_flags |= COMPRESSION_LEVEL_FAST_GPFLAG
|
818
|
+
when 8, 9
|
819
|
+
@gp_flags |= COMPRESSION_LEVEL_MAX_GPFLAG
|
820
|
+
end
|
679
821
|
end
|
680
822
|
|
681
|
-
#
|
682
|
-
def
|
823
|
+
# rubocop:disable Style/GuardClause
|
824
|
+
def prep_local_zip64_extra
|
683
825
|
return unless ::Zip.write_zip64_support
|
826
|
+
return if (!zip64? && @size && @size < 0xFFFFFFFF) || !file?
|
684
827
|
|
685
|
-
|
686
|
-
|
687
|
-
if
|
828
|
+
# Might not know size here, so need ZIP64 just in case.
|
829
|
+
# If we already have a ZIP64 extra (placeholder) then we must fill it in.
|
830
|
+
if zip64? || @size.nil? || @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
|
688
831
|
@version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
|
689
|
-
@extra.
|
690
|
-
zip64 = @extra.create('Zip64')
|
691
|
-
if for_local_header
|
692
|
-
# local header always includes size and compressed size
|
693
|
-
zip64.original_size = @size
|
694
|
-
zip64.compressed_size = @compressed_size
|
695
|
-
else
|
696
|
-
# central directory entry entries include whichever fields are necessary
|
697
|
-
zip64.original_size = @size if @size >= 0xFFFFFFFF
|
698
|
-
zip64.compressed_size = @compressed_size if @compressed_size >= 0xFFFFFFFF
|
699
|
-
zip64.relative_header_offset = @local_header_offset if @local_header_offset >= 0xFFFFFFFF
|
700
|
-
end
|
701
|
-
else
|
702
|
-
@extra.delete('Zip64')
|
832
|
+
zip64 = @extra['Zip64'] || @extra.create('Zip64')
|
703
833
|
|
704
|
-
#
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
834
|
+
# Local header always includes size and compressed size.
|
835
|
+
zip64.original_size = @size || 0
|
836
|
+
zip64.compressed_size = @compressed_size
|
837
|
+
end
|
838
|
+
end
|
839
|
+
|
840
|
+
def prep_cdir_zip64_extra
|
841
|
+
return unless ::Zip.write_zip64_support
|
842
|
+
|
843
|
+
if (@size && @size >= 0xFFFFFFFF) || @compressed_size >= 0xFFFFFFFF ||
|
844
|
+
@local_header_offset >= 0xFFFFFFFF
|
845
|
+
@version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
|
846
|
+
zip64 = @extra['Zip64'] || @extra.create('Zip64')
|
847
|
+
|
848
|
+
# Central directory entry entries include whichever fields are necessary.
|
849
|
+
zip64.original_size = @size if @size && @size >= 0xFFFFFFFF
|
850
|
+
zip64.compressed_size = @compressed_size if @compressed_size >= 0xFFFFFFFF
|
851
|
+
zip64.relative_header_offset = @local_header_offset if @local_header_offset >= 0xFFFFFFFF
|
712
852
|
end
|
713
853
|
end
|
854
|
+
# rubocop:enable Style/GuardClause
|
714
855
|
end
|
715
856
|
end
|
716
857
|
|