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