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