rubyzip 2.4.1 → 3.0.2
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 +436 -0
- data/LICENSE.md +24 -0
- data/README.md +126 -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 +12 -8
- data/lib/zip/dirtyable.rb +32 -0
- data/lib/zip/dos_time.rb +43 -4
- data/lib/zip/entry.rb +352 -249
- 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 +167 -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 +51 -51
- data/lib/zip/ioextras/abstract_input_stream.rb +16 -11
- data/lib/zip/ioextras/abstract_output_stream.rb +13 -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 +23 -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 +85 -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,175 +58,222 @@ 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
|
-
def initialize(
|
57
|
-
name
|
58
|
-
|
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)
|
59
86
|
|
60
87
|
set_default_vars_values
|
61
88
|
@fstype = ::Zip::RUNNING_ON_WINDOWS ? ::Zip::FSTYPE_FAT : ::Zip::FSTYPE_UNIX
|
62
89
|
|
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
|
-
|
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?
|
91
112
|
def encrypted?
|
92
113
|
gp_flags & 1 == 1
|
93
114
|
end
|
94
115
|
|
95
|
-
def incomplete?
|
96
|
-
gp_flags & 8 == 8
|
116
|
+
def incomplete? # :nodoc:
|
117
|
+
(gp_flags & 8 == 8) && (crc == 0 || size == 0 || compressed_size == 0)
|
97
118
|
end
|
98
119
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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)
|
109
139
|
end
|
110
140
|
|
111
141
|
alias mtime time
|
112
142
|
|
113
|
-
|
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
|
114
158
|
unless @extra.member?('UniversalTime') || @extra.member?('NTFS')
|
115
159
|
@extra.create('UniversalTime')
|
116
160
|
end
|
117
|
-
|
118
|
-
|
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
|
119
166
|
end
|
120
167
|
|
121
|
-
|
122
|
-
raise InternalError, "current filetype is unknown: #{inspect}" unless @ftype
|
168
|
+
alias mtime= time=
|
123
169
|
|
124
|
-
|
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
|
125
212
|
end
|
126
213
|
|
127
214
|
# Dynamic checkers
|
128
215
|
%w[directory file symlink].each do |k|
|
129
|
-
define_method "#{k}?" do
|
216
|
+
define_method :"#{k}?" do
|
130
217
|
file_type_is?(k.to_sym)
|
131
218
|
end
|
132
219
|
end
|
133
220
|
|
134
|
-
def name_is_directory?
|
221
|
+
def name_is_directory? # :nodoc:
|
135
222
|
@name.end_with?('/')
|
136
223
|
end
|
137
224
|
|
138
225
|
# Is the name a relative path, free of `..` patterns that could lead to
|
139
226
|
# path traversal attacks? This does NOT handle symlinks; if the path
|
140
227
|
# contains symlinks, this check is NOT enough to guarantee safety.
|
141
|
-
def name_safe?
|
228
|
+
def name_safe? # :nodoc:
|
142
229
|
cleanpath = Pathname.new(@name).cleanpath
|
143
230
|
return false unless cleanpath.relative?
|
144
231
|
|
145
232
|
root = ::File::SEPARATOR
|
146
|
-
|
147
|
-
|
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)
|
148
236
|
end
|
149
237
|
|
150
|
-
def local_entry_offset
|
238
|
+
def local_entry_offset # :nodoc:
|
151
239
|
local_header_offset + @local_header_size
|
152
240
|
end
|
153
241
|
|
154
|
-
def name_size
|
242
|
+
def name_size # :nodoc:
|
155
243
|
@name ? @name.bytesize : 0
|
156
244
|
end
|
157
245
|
|
158
|
-
def extra_size
|
246
|
+
def extra_size # :nodoc:
|
159
247
|
@extra ? @extra.local_size : 0
|
160
248
|
end
|
161
249
|
|
162
|
-
def comment_size
|
250
|
+
def comment_size # :nodoc:
|
163
251
|
@comment ? @comment.bytesize : 0
|
164
252
|
end
|
165
253
|
|
166
|
-
def calculate_local_header_size
|
254
|
+
def calculate_local_header_size # :nodoc:
|
167
255
|
LOCAL_ENTRY_STATIC_HEADER_LENGTH + name_size + extra_size
|
168
256
|
end
|
169
257
|
|
170
258
|
# check before rewriting an entry (after file sizes are known)
|
171
259
|
# that we didn't change the header size (and thus clobber file data or something)
|
172
|
-
def verify_local_header_size!
|
260
|
+
def verify_local_header_size! # :nodoc:
|
173
261
|
return if @local_header_size.nil?
|
174
262
|
|
175
263
|
new_size = calculate_local_header_size
|
176
|
-
|
264
|
+
return unless @local_header_size != new_size
|
265
|
+
|
266
|
+
raise Error,
|
267
|
+
"Local header size changed (#{@local_header_size} -> #{new_size})"
|
177
268
|
end
|
178
269
|
|
179
|
-
def cdir_header_size
|
270
|
+
def cdir_header_size # :nodoc:
|
180
271
|
CDIR_ENTRY_STATIC_HEADER_LENGTH + name_size +
|
181
272
|
(@extra ? @extra.c_dir_size : 0) + comment_size
|
182
273
|
end
|
183
274
|
|
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
|
275
|
+
def next_header_offset # :nodoc:
|
276
|
+
local_entry_offset + compressed_size
|
206
277
|
end
|
207
278
|
|
208
279
|
# Extracts this entry to a file at `entry_path`, with
|
@@ -210,7 +281,7 @@ module Zip
|
|
210
281
|
#
|
211
282
|
# NB: The caller is responsible for making sure `destination_directory` is
|
212
283
|
# safe, if it is passed.
|
213
|
-
def
|
284
|
+
def extract(entry_path = @name, destination_directory: '.', &block)
|
214
285
|
dest_dir = ::File.absolute_path(destination_directory || '.')
|
215
286
|
extract_path = ::File.absolute_path(::File.join(dest_dir, entry_path))
|
216
287
|
|
@@ -227,24 +298,12 @@ module Zip
|
|
227
298
|
self
|
228
299
|
end
|
229
300
|
|
230
|
-
def to_s
|
301
|
+
def to_s # :nodoc:
|
231
302
|
@name
|
232
303
|
end
|
233
304
|
|
234
305
|
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
|
306
|
+
def read_c_dir_entry(io) # :nodoc:
|
248
307
|
path = if io.respond_to?(:path)
|
249
308
|
io.path
|
250
309
|
else
|
@@ -257,16 +316,18 @@ module Zip
|
|
257
316
|
nil
|
258
317
|
end
|
259
318
|
|
260
|
-
def read_local_entry(io)
|
319
|
+
def read_local_entry(io) # :nodoc:
|
261
320
|
entry = new(io)
|
262
321
|
entry.read_local_entry(io)
|
263
322
|
entry
|
323
|
+
rescue SplitArchiveError
|
324
|
+
raise
|
264
325
|
rescue Error
|
265
326
|
nil
|
266
327
|
end
|
267
328
|
end
|
268
329
|
|
269
|
-
def unpack_local_entry(buf)
|
330
|
+
def unpack_local_entry(buf) # :nodoc:
|
270
331
|
@header_signature,
|
271
332
|
@version,
|
272
333
|
@fstype,
|
@@ -281,62 +342,66 @@ module Zip
|
|
281
342
|
@extra_length = buf.unpack('VCCvvvvVVVvv')
|
282
343
|
end
|
283
344
|
|
284
|
-
def read_local_entry(io)
|
285
|
-
@
|
345
|
+
def read_local_entry(io) # :nodoc:
|
346
|
+
@dirty = false # No changes at this point.
|
347
|
+
current_offset = io.tell
|
286
348
|
|
287
|
-
|
349
|
+
read_local_header_fields(io)
|
288
350
|
|
289
|
-
|
290
|
-
raise
|
291
|
-
end
|
351
|
+
if @header_signature == SPLIT_FILE_SIGNATURE
|
352
|
+
raise SplitArchiveError if current_offset.zero?
|
292
353
|
|
293
|
-
|
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
|
294
359
|
|
295
|
-
unless @header_signature ==
|
296
|
-
raise
|
360
|
+
unless @header_signature == LOCAL_ENTRY_SIGNATURE
|
361
|
+
raise Error, "Zip local header magic not found at location '#{current_offset}'"
|
297
362
|
end
|
298
363
|
|
364
|
+
@local_header_offset = current_offset
|
365
|
+
|
299
366
|
set_time(@last_mod_date, @last_mod_time)
|
300
367
|
|
301
368
|
@name = io.read(@name_length)
|
302
|
-
extra = io.read(@extra_length)
|
303
|
-
|
304
|
-
@name.tr!('\\', '/')
|
305
369
|
if ::Zip.force_entry_names_encoding
|
306
370
|
@name.force_encoding(::Zip.force_entry_names_encoding)
|
307
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
|
308
377
|
|
378
|
+
extra = io.read(@extra_length)
|
309
379
|
if extra && extra.bytesize != @extra_length
|
310
380
|
raise ::Zip::Error, 'Truncated local zip entry header'
|
311
381
|
end
|
312
382
|
|
313
|
-
|
314
|
-
@extra.merge(extra) if extra
|
315
|
-
else
|
316
|
-
@extra = ::Zip::ExtraField.new(extra)
|
317
|
-
end
|
318
|
-
|
383
|
+
read_extra_field(extra, local: true)
|
319
384
|
parse_zip64_extra(true)
|
320
385
|
@local_header_size = calculate_local_header_size
|
321
386
|
end
|
322
387
|
|
323
|
-
def pack_local_entry
|
388
|
+
def pack_local_entry # :nodoc:
|
324
389
|
zip64 = @extra['Zip64']
|
325
390
|
[::Zip::LOCAL_ENTRY_SIGNATURE,
|
326
391
|
@version_needed_to_extract, # version needed to extract
|
327
392
|
@gp_flags, # @gp_flags
|
328
|
-
|
393
|
+
compression_method,
|
329
394
|
@time.to_binary_dos_time, # @last_mod_time
|
330
395
|
@time.to_binary_dos_date, # @last_mod_date
|
331
396
|
@crc,
|
332
397
|
zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
|
333
|
-
zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
|
398
|
+
zip64 && zip64.original_size ? 0xFFFFFFFF : (@size || 0),
|
334
399
|
name_size,
|
335
400
|
@extra ? @extra.local_size : 0].pack('VvvvvvVVVvv')
|
336
401
|
end
|
337
402
|
|
338
|
-
def write_local_entry(io, rewrite
|
339
|
-
|
403
|
+
def write_local_entry(io, rewrite: false) # :nodoc:
|
404
|
+
prep_local_zip64_extra
|
340
405
|
verify_local_header_size! if rewrite
|
341
406
|
@local_header_offset = io.tell
|
342
407
|
|
@@ -347,7 +412,7 @@ module Zip
|
|
347
412
|
@local_header_size = io.tell - @local_header_offset
|
348
413
|
end
|
349
414
|
|
350
|
-
def unpack_c_dir_entry(buf)
|
415
|
+
def unpack_c_dir_entry(buf) # :nodoc:
|
351
416
|
@header_signature,
|
352
417
|
@version, # version of encoding software
|
353
418
|
@fstype, # filesystem type
|
@@ -371,7 +436,7 @@ module Zip
|
|
371
436
|
@comment = buf.unpack('VCCvvvvvVVVvvvvvVV')
|
372
437
|
end
|
373
438
|
|
374
|
-
def set_ftype_from_c_dir_entry
|
439
|
+
def set_ftype_from_c_dir_entry # :nodoc:
|
375
440
|
@ftype = case @fstype
|
376
441
|
when ::Zip::FSTYPE_UNIX
|
377
442
|
@unix_perms = (@external_file_attributes >> 16) & 0o7777
|
@@ -383,8 +448,9 @@ module Zip
|
|
383
448
|
when ::Zip::FILE_TYPE_SYMLINK
|
384
449
|
:symlink
|
385
450
|
else
|
386
|
-
#
|
387
|
-
# 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.
|
388
454
|
if name_is_directory?
|
389
455
|
:directory
|
390
456
|
else
|
@@ -400,43 +466,47 @@ module Zip
|
|
400
466
|
end
|
401
467
|
end
|
402
468
|
|
403
|
-
def check_c_dir_entry_static_header_length(buf)
|
404
|
-
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
|
405
471
|
|
406
472
|
raise Error, 'Premature end of file. Not enough data for zip cdir entry header'
|
407
473
|
end
|
408
474
|
|
409
|
-
def check_c_dir_entry_signature
|
410
|
-
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
|
411
477
|
|
412
478
|
raise Error, "Zip local header magic not found at location '#{local_header_offset}'"
|
413
479
|
end
|
414
480
|
|
415
|
-
def check_c_dir_entry_comment_size
|
481
|
+
def check_c_dir_entry_comment_size # :nodoc:
|
416
482
|
return if @comment && @comment.bytesize == @comment_length
|
417
483
|
|
418
484
|
raise ::Zip::Error, 'Truncated cdir zip entry header'
|
419
485
|
end
|
420
486
|
|
421
|
-
def
|
487
|
+
def read_extra_field(buf, local: false) # :nodoc:
|
422
488
|
if @extra.kind_of?(::Zip::ExtraField)
|
423
|
-
@extra.merge(
|
489
|
+
@extra.merge(buf, local: local) if buf
|
424
490
|
else
|
425
|
-
@extra = ::Zip::ExtraField.new(
|
491
|
+
@extra = ::Zip::ExtraField.new(buf, local: local)
|
426
492
|
end
|
427
493
|
end
|
428
494
|
|
429
|
-
def read_c_dir_entry(io)
|
495
|
+
def read_c_dir_entry(io) # :nodoc:
|
496
|
+
@dirty = false # No changes at this point.
|
430
497
|
static_sized_fields_buf = io.read(::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH)
|
431
498
|
check_c_dir_entry_static_header_length(static_sized_fields_buf)
|
432
499
|
unpack_c_dir_entry(static_sized_fields_buf)
|
433
500
|
check_c_dir_entry_signature
|
434
501
|
set_time(@last_mod_date, @last_mod_time)
|
502
|
+
|
435
503
|
@name = io.read(@name_length)
|
436
504
|
if ::Zip.force_entry_names_encoding
|
437
505
|
@name.force_encoding(::Zip.force_entry_names_encoding)
|
438
506
|
end
|
439
|
-
|
507
|
+
@name.tr!('\\', '/') # Normalise filepath separators after encoding set.
|
508
|
+
|
509
|
+
read_extra_field(io.read(@extra_length))
|
440
510
|
@comment = io.read(@comment_length)
|
441
511
|
check_c_dir_entry_comment_size
|
442
512
|
set_ftype_from_c_dir_entry
|
@@ -452,27 +522,27 @@ module Zip
|
|
452
522
|
end
|
453
523
|
|
454
524
|
def get_extra_attributes_from_path(path) # :nodoc:
|
455
|
-
|
525
|
+
stat = file_stat(path)
|
526
|
+
@time = DOSTime.from_time(stat.mtime)
|
527
|
+
return if ::Zip::RUNNING_ON_WINDOWS
|
456
528
|
|
457
|
-
stat = file_stat(path)
|
458
529
|
@unix_uid = stat.uid
|
459
530
|
@unix_gid = stat.gid
|
460
531
|
@unix_perms = stat.mode & 0o7777
|
461
|
-
@time = ::Zip::DOSTime.from_time(stat.mtime)
|
462
532
|
end
|
463
533
|
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
unix_perms_mask = 0o7777
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
::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
|
475
544
|
end
|
545
|
+
# rubocop:enable Style/GuardClause
|
476
546
|
|
477
547
|
def set_extra_attributes_on_path(dest_path) # :nodoc:
|
478
548
|
return unless file? || directory?
|
@@ -481,9 +551,14 @@ module Zip
|
|
481
551
|
when ::Zip::FSTYPE_UNIX
|
482
552
|
set_unix_attributes_on_path(dest_path)
|
483
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
|
484
559
|
end
|
485
560
|
|
486
|
-
def pack_c_dir_entry
|
561
|
+
def pack_c_dir_entry # :nodoc:
|
487
562
|
zip64 = @extra['Zip64']
|
488
563
|
[
|
489
564
|
@header_signature,
|
@@ -491,12 +566,12 @@ module Zip
|
|
491
566
|
@fstype, # filesystem type
|
492
567
|
@version_needed_to_extract, # @versionNeededToExtract
|
493
568
|
@gp_flags, # @gp_flags
|
494
|
-
|
569
|
+
compression_method,
|
495
570
|
@time.to_binary_dos_time, # @last_mod_time
|
496
571
|
@time.to_binary_dos_date, # @last_mod_date
|
497
572
|
@crc,
|
498
573
|
zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
|
499
|
-
zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
|
574
|
+
zip64 && zip64.original_size ? 0xFFFFFFFF : (@size || 0),
|
500
575
|
name_size,
|
501
576
|
@extra ? @extra.c_dir_size : 0,
|
502
577
|
comment_size,
|
@@ -510,11 +585,12 @@ module Zip
|
|
510
585
|
].pack('VCCvvvvvVVVvvvvvVV')
|
511
586
|
end
|
512
587
|
|
513
|
-
def write_c_dir_entry(io)
|
514
|
-
|
588
|
+
def write_c_dir_entry(io) # :nodoc:
|
589
|
+
prep_cdir_zip64_extra
|
590
|
+
|
515
591
|
case @fstype
|
516
592
|
when ::Zip::FSTYPE_UNIX
|
517
|
-
ft = case
|
593
|
+
ft = case ftype
|
518
594
|
when :file
|
519
595
|
@unix_perms ||= 0o644
|
520
596
|
::Zip::FILE_TYPE_FILE
|
@@ -527,7 +603,7 @@ module Zip
|
|
527
603
|
end
|
528
604
|
|
529
605
|
unless ft.nil?
|
530
|
-
@external_file_attributes = (ft << 12 | (@unix_perms & 0o7777)) << 16
|
606
|
+
@external_file_attributes = ((ft << 12) | (@unix_perms & 0o7777)) << 16
|
531
607
|
end
|
532
608
|
end
|
533
609
|
|
@@ -538,43 +614,42 @@ module Zip
|
|
538
614
|
io << @comment
|
539
615
|
end
|
540
616
|
|
541
|
-
def ==(other)
|
617
|
+
def ==(other) # :nodoc:
|
542
618
|
return false unless other.class == self.class
|
543
619
|
|
544
620
|
# Compares contents of local entry and exposed fields
|
545
|
-
|
621
|
+
%w[compression_method crc compressed_size size name extra filepath time].all? do |k|
|
546
622
|
other.__send__(k.to_sym) == __send__(k.to_sym)
|
547
623
|
end
|
548
|
-
keys_equal && time == other.time
|
549
624
|
end
|
550
625
|
|
551
|
-
def <=>(other)
|
626
|
+
def <=>(other) # :nodoc:
|
552
627
|
to_s <=> other.to_s
|
553
628
|
end
|
554
629
|
|
555
630
|
# Returns an IO like object for the given ZipEntry.
|
556
631
|
# Warning: may behave weird with symlinks.
|
557
632
|
def get_input_stream(&block)
|
558
|
-
if
|
559
|
-
yield ::Zip::NullInputStream if
|
633
|
+
if ftype == :directory
|
634
|
+
yield ::Zip::NullInputStream if block
|
560
635
|
::Zip::NullInputStream
|
561
636
|
elsif @filepath
|
562
|
-
case
|
637
|
+
case ftype
|
563
638
|
when :file
|
564
639
|
::File.open(@filepath, 'rb', &block)
|
565
640
|
when :symlink
|
566
641
|
linkpath = ::File.readlink(@filepath)
|
567
642
|
stringio = ::StringIO.new(linkpath)
|
568
|
-
yield(stringio) if
|
643
|
+
yield(stringio) if block
|
569
644
|
stringio
|
570
645
|
else
|
571
|
-
raise "unknown @file_type #{
|
646
|
+
raise "unknown @file_type #{ftype}"
|
572
647
|
end
|
573
648
|
else
|
574
649
|
zis = ::Zip::InputStream.new(@zipfile, offset: local_header_offset)
|
575
650
|
zis.instance_variable_set(:@complete_entry, self)
|
576
651
|
zis.get_next_entry
|
577
|
-
if
|
652
|
+
if block
|
578
653
|
begin
|
579
654
|
yield(zis)
|
580
655
|
ensure
|
@@ -593,7 +668,7 @@ module Zip
|
|
593
668
|
if name_is_directory?
|
594
669
|
raise ArgumentError,
|
595
670
|
"entry name '#{newEntry}' indicates directory entry, but " \
|
596
|
-
|
671
|
+
"'#{src_path}' is not a directory"
|
597
672
|
end
|
598
673
|
:file
|
599
674
|
when 'directory'
|
@@ -603,7 +678,7 @@ module Zip
|
|
603
678
|
if name_is_directory?
|
604
679
|
raise ArgumentError,
|
605
680
|
"entry name '#{newEntry}' indicates directory entry, but " \
|
606
|
-
|
681
|
+
"'#{src_path}' is not a directory"
|
607
682
|
end
|
608
683
|
:symlink
|
609
684
|
else
|
@@ -611,27 +686,30 @@ module Zip
|
|
611
686
|
end
|
612
687
|
|
613
688
|
@filepath = src_path
|
689
|
+
@size = stat.size
|
614
690
|
get_extra_attributes_from_path(@filepath)
|
615
691
|
end
|
616
692
|
|
617
|
-
def write_to_zip_output_stream(zip_output_stream)
|
618
|
-
if
|
619
|
-
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)
|
620
696
|
elsif @filepath
|
621
|
-
zip_output_stream.put_next_entry(self
|
622
|
-
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
|
623
701
|
else
|
624
702
|
zip_output_stream.copy_raw_entry(self)
|
625
703
|
end
|
626
704
|
end
|
627
705
|
|
628
|
-
def parent_as_string
|
706
|
+
def parent_as_string # :nodoc:
|
629
707
|
entry_name = name.chomp('/')
|
630
708
|
slash_index = entry_name.rindex('/')
|
631
709
|
slash_index ? entry_name.slice(0, slash_index + 1) : nil
|
632
710
|
end
|
633
711
|
|
634
|
-
def get_raw_input_stream(&block)
|
712
|
+
def get_raw_input_stream(&block) # :nodoc:
|
635
713
|
if @zipfile.respond_to?(:seek) && @zipfile.respond_to?(:read)
|
636
714
|
yield @zipfile
|
637
715
|
else
|
@@ -639,12 +717,22 @@ module Zip
|
|
639
717
|
end
|
640
718
|
end
|
641
719
|
|
642
|
-
def clean_up
|
643
|
-
#
|
720
|
+
def clean_up # :nodoc:
|
721
|
+
@dirty = false # Any changes are written at this point.
|
644
722
|
end
|
645
723
|
|
646
724
|
private
|
647
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
|
+
|
648
736
|
def set_time(binary_dos_date, binary_dos_time)
|
649
737
|
@time = ::Zip::DOSTime.parse_binary_dos_format(binary_dos_date, binary_dos_time)
|
650
738
|
rescue ArgumentError
|
@@ -653,9 +741,9 @@ module Zip
|
|
653
741
|
|
654
742
|
def create_file(dest_path, _continue_on_exists_proc = proc { Zip.continue_on_exists_proc })
|
655
743
|
if ::File.exist?(dest_path) && !yield(self, dest_path)
|
656
|
-
raise ::Zip::
|
657
|
-
"Destination '#{dest_path}' already exists"
|
744
|
+
raise ::Zip::DestinationExistsError, dest_path
|
658
745
|
end
|
746
|
+
|
659
747
|
::File.open(dest_path, 'wb') do |os|
|
660
748
|
get_input_stream do |is|
|
661
749
|
bytes_written = 0
|
@@ -666,10 +754,10 @@ module Zip
|
|
666
754
|
bytes_written += buf.bytesize
|
667
755
|
next unless bytes_written > size && !warned
|
668
756
|
|
669
|
-
|
670
|
-
raise
|
757
|
+
error = ::Zip::EntrySizeError.new(self)
|
758
|
+
raise error if ::Zip.validate_entry_sizes
|
671
759
|
|
672
|
-
warn "WARNING: #{message}"
|
760
|
+
warn "WARNING: #{error.message}"
|
673
761
|
warned = true
|
674
762
|
end
|
675
763
|
end
|
@@ -682,14 +770,11 @@ module Zip
|
|
682
770
|
return if ::File.directory?(dest_path)
|
683
771
|
|
684
772
|
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
|
773
|
+
raise ::Zip::DestinationExistsError, dest_path unless block_given? && yield(self, dest_path)
|
774
|
+
|
775
|
+
::FileUtils.rm_f dest_path
|
692
776
|
end
|
777
|
+
|
693
778
|
::FileUtils.mkdir_p(dest_path)
|
694
779
|
set_extra_attributes_on_path(dest_path)
|
695
780
|
end
|
@@ -703,53 +788,71 @@ module Zip
|
|
703
788
|
|
704
789
|
# apply missing data from the zip64 extra information field, if present
|
705
790
|
# (required when file sizes exceed 2**32, but can be used for all files)
|
706
|
-
def parse_zip64_extra(for_local_header)
|
707
|
-
return
|
791
|
+
def parse_zip64_extra(for_local_header) # :nodoc:
|
792
|
+
return unless zip64?
|
708
793
|
|
709
794
|
if for_local_header
|
710
795
|
@size, @compressed_size = @extra['Zip64'].parse(@size, @compressed_size)
|
711
796
|
else
|
712
|
-
@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
|
+
)
|
713
800
|
end
|
714
801
|
end
|
715
802
|
|
716
|
-
|
717
|
-
|
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
|
718
822
|
end
|
719
823
|
|
720
|
-
#
|
721
|
-
def
|
824
|
+
# rubocop:disable Style/GuardClause
|
825
|
+
def prep_local_zip64_extra
|
722
826
|
return unless ::Zip.write_zip64_support
|
827
|
+
return if (!zip64? && @size && @size < 0xFFFFFFFF) || !file?
|
723
828
|
|
724
|
-
|
725
|
-
|
726
|
-
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
|
727
832
|
@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')
|
833
|
+
zip64 = @extra['Zip64'] || @extra.create('Zip64')
|
742
834
|
|
743
|
-
#
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
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
|
751
853
|
end
|
752
854
|
end
|
855
|
+
# rubocop:enable Style/GuardClause
|
753
856
|
end
|
754
857
|
end
|
755
858
|
|