rubyzip 2.4.1 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Changelog.md +549 -0
- data/LICENSE.md +24 -0
- data/README.md +250 -44
- data/Rakefile +15 -13
- data/lib/rubyzip.rb +3 -0
- data/lib/zip/central_directory.rb +194 -125
- data/lib/zip/compressor.rb +3 -1
- data/lib/zip/constants.rb +30 -21
- data/lib/zip/crypto/aes_encryption.rb +213 -0
- data/lib/zip/crypto/decrypted_io.rb +27 -15
- data/lib/zip/crypto/encryption.rb +13 -2
- data/lib/zip/crypto/null_encryption.rb +5 -13
- data/lib/zip/crypto/traditional_encryption.rb +13 -7
- 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 +64 -6
- data/lib/zip/entry.rb +432 -270
- data/lib/zip/entry_set.rb +11 -9
- data/lib/zip/errors.rb +152 -16
- data/lib/zip/extra_field/aes.rb +50 -0
- data/lib/zip/extra_field/generic.rb +10 -11
- data/lib/zip/extra_field/ntfs.rb +6 -4
- data/lib/zip/extra_field/old_unix.rb +5 -3
- data/lib/zip/extra_field/universal_time.rb +3 -1
- data/lib/zip/extra_field/unix.rb +7 -5
- data/lib/zip/extra_field/unknown.rb +35 -0
- data/lib/zip/extra_field/zip64.rb +19 -5
- data/lib/zip/extra_field.rb +25 -23
- data/lib/zip/file.rb +261 -262
- 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 +263 -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 +13 -10
- data/lib/zip/input_stream.rb +102 -61
- data/lib/zip/ioextras/abstract_input_stream.rb +156 -58
- data/lib/zip/ioextras/abstract_output_stream.rb +15 -3
- data/lib/zip/ioextras/fake_io.rb +39 -0
- data/lib/zip/ioextras.rb +12 -16
- data/lib/zip/null_compressor.rb +3 -1
- data/lib/zip/null_decompressor.rb +6 -3
- data/lib/zip/null_input_stream.rb +3 -1
- data/lib/zip/output_stream.rb +76 -64
- data/lib/zip/pass_thru_compressor.rb +6 -3
- data/lib/zip/pass_thru_decompressor.rb +12 -9
- data/lib/zip/streamable_directory.rb +3 -1
- data/lib/zip/streamable_stream.rb +4 -1
- data/lib/zip/version.rb +4 -1
- data/lib/zip.rb +28 -23
- data/rubyzip.gemspec +39 -0
- data/samples/example.rb +8 -3
- data/samples/example_filesystem.rb +3 -2
- data/samples/example_recursive.rb +3 -1
- data/samples/gtk_ruby_zip.rb +5 -3
- data/samples/qtzip.rb +7 -6
- data/samples/write_simple.rb +2 -1
- data/samples/zipfind.rb +1 -0
- metadata +88 -52
- data/TODO +0 -15
- data/lib/zip/extra_field/zip64_placeholder.rb +0 -15
data/lib/zip/file.rb
CHANGED
|
@@ -1,132 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'forwardable'
|
|
5
|
+
|
|
6
|
+
require_relative 'file_split'
|
|
7
|
+
|
|
1
8
|
module Zip
|
|
2
|
-
#
|
|
3
|
-
# The most important methods are those
|
|
4
|
-
#
|
|
5
|
-
# the archive and methods such as get_input_stream and
|
|
6
|
-
# get_output_stream for reading from and writing entries to the
|
|
9
|
+
# Zip::File is modeled after java.util.zip.ZipFile from the Java SDK.
|
|
10
|
+
# The most important methods are those for accessing information about
|
|
11
|
+
# the entries in
|
|
12
|
+
# the archive and methods such as `get_input_stream` and
|
|
13
|
+
# `get_output_stream` for reading from and writing entries to the
|
|
7
14
|
# archive. The class includes a few convenience methods such as
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
15
|
+
# `extract` for extracting entries to the filesystem, `extract_all` for
|
|
16
|
+
# extracting every entry, `add_recursive` for adding the contents of a
|
|
17
|
+
# directory tree, and `remove`, `replace`, `rename` and `mkdir` for
|
|
18
|
+
# making simple modifications to the archive.
|
|
11
19
|
#
|
|
12
|
-
# Modifications to a zip archive are not committed until
|
|
13
|
-
#
|
|
14
|
-
# the pattern from File.open offering a simple way to
|
|
20
|
+
# Modifications to a zip archive are not committed until `commit` or
|
|
21
|
+
# `close` is called. The method `open` accepts a block following
|
|
22
|
+
# the pattern from ::File.open offering a simple way to
|
|
15
23
|
# automatically close the archive when the block returns.
|
|
16
24
|
#
|
|
17
|
-
# The following example opens zip archive
|
|
25
|
+
# The following example opens zip archive `my.zip`
|
|
18
26
|
# (creating it if it doesn't exist) and adds an entry
|
|
19
|
-
#
|
|
27
|
+
# `first.txt` and a directory entry `a_dir`
|
|
20
28
|
# to it.
|
|
21
29
|
#
|
|
22
|
-
#
|
|
30
|
+
# ```
|
|
31
|
+
# require 'zip'
|
|
23
32
|
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
33
|
+
# Zip::File.open('my.zip', create: true) do |zipfile|
|
|
34
|
+
# zipfile.get_output_stream('first.txt') { |f| f.puts 'Hello from Zip::File' }
|
|
35
|
+
# zipfile.mkdir('a_dir')
|
|
36
|
+
# end
|
|
37
|
+
# ```
|
|
29
38
|
#
|
|
30
|
-
# The next example reopens
|
|
31
|
-
#
|
|
39
|
+
# The next example reopens `my.zip`, writes the contents of
|
|
40
|
+
# `first.txt` to standard out and deletes the entry from
|
|
32
41
|
# the archive.
|
|
33
42
|
#
|
|
34
|
-
#
|
|
43
|
+
# ```
|
|
44
|
+
# require 'zip'
|
|
35
45
|
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
# }
|
|
46
|
+
# Zip::File.open('my.zip', create: true) do |zipfile|
|
|
47
|
+
# puts zipfile.read('first.txt')
|
|
48
|
+
# zipfile.remove('first.txt')
|
|
49
|
+
# end
|
|
41
50
|
#
|
|
42
|
-
#
|
|
43
|
-
# interface for accessing the filesystem, ie. the File and Dir classes.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
ZIP64_EOCD_SIGNATURE = 0x06064b50
|
|
49
|
-
MAX_SEGMENT_SIZE = 3_221_225_472
|
|
50
|
-
MIN_SEGMENT_SIZE = 65_536
|
|
51
|
-
DATA_BUFFER_SIZE = 8192
|
|
52
|
-
IO_METHODS = [:tell, :seek, :read, :eof, :close]
|
|
53
|
-
|
|
54
|
-
DEFAULT_OPTIONS = {
|
|
55
|
-
restore_ownership: false,
|
|
56
|
-
restore_permissions: false,
|
|
57
|
-
restore_times: false
|
|
58
|
-
}.freeze
|
|
51
|
+
# Zip::FileSystem offers an alternative API that emulates ruby's
|
|
52
|
+
# interface for accessing the filesystem, ie. the ::File and ::Dir classes.
|
|
53
|
+
class File
|
|
54
|
+
include Enumerable
|
|
55
|
+
extend Forwardable
|
|
56
|
+
extend FileSplit
|
|
59
57
|
|
|
58
|
+
IO_METHODS = [:tell, :seek, :read, :eof, :close].freeze # :nodoc:
|
|
59
|
+
|
|
60
|
+
# The name of this zip archive.
|
|
60
61
|
attr_reader :name
|
|
61
62
|
|
|
62
63
|
# default -> false.
|
|
63
64
|
attr_accessor :restore_ownership
|
|
64
65
|
|
|
65
|
-
# default ->
|
|
66
|
+
# default -> true.
|
|
66
67
|
attr_accessor :restore_permissions
|
|
67
68
|
|
|
68
|
-
# default ->
|
|
69
|
+
# default -> true.
|
|
69
70
|
attr_accessor :restore_times
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
attr_accessor :comment
|
|
72
|
+
def_delegators :@cdir, :comment, :comment=, :each, :entries, :glob, :size
|
|
73
73
|
|
|
74
|
-
# Opens a zip archive. Pass true
|
|
74
|
+
# Opens a zip archive. Pass create: true to create
|
|
75
75
|
# a new archive if it doesn't exist already.
|
|
76
|
-
def initialize(path_or_io,
|
|
77
|
-
|
|
76
|
+
def initialize(path_or_io, create: false, buffer: false,
|
|
77
|
+
restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
|
|
78
|
+
restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
|
|
79
|
+
restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
|
|
80
|
+
compression_level: ::Zip.default_compression,
|
|
81
|
+
suppress_extra_fields: false)
|
|
78
82
|
super()
|
|
79
83
|
|
|
80
|
-
Zip.warn_about_v3_api('File#new') if dep_create || dep_buffer
|
|
81
|
-
|
|
82
|
-
options = DEFAULT_OPTIONS.merge(options)
|
|
83
84
|
@name = path_or_io.respond_to?(:path) ? path_or_io.path : path_or_io
|
|
84
|
-
@
|
|
85
|
-
@create = create || dep_create ? true : false # allow any truthy value to mean true
|
|
86
|
-
buffer ||= dep_buffer
|
|
85
|
+
@create = create ? true : false # allow any truthy value to mean true
|
|
87
86
|
|
|
88
|
-
|
|
89
|
-
# There is a file, which exists, that is associated with this zip.
|
|
90
|
-
@create = false
|
|
91
|
-
@file_permissions = ::File.stat(@name).mode
|
|
87
|
+
initialize_cdir(path_or_io, buffer: buffer)
|
|
92
88
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
elsif buffer && path_or_io.size > 0
|
|
101
|
-
# This zip is probably a non-empty StringIO.
|
|
102
|
-
@create = false
|
|
103
|
-
read_from_stream(path_or_io)
|
|
104
|
-
elsif @create
|
|
105
|
-
# This zip is completely new/empty and is to be created.
|
|
106
|
-
@entry_set = EntrySet.new
|
|
107
|
-
elsif ::File.zero?(@name)
|
|
108
|
-
# A file exists, but it is empty.
|
|
109
|
-
raise Error, "File #{@name} has zero size. Did you mean to pass the create flag?"
|
|
110
|
-
else
|
|
111
|
-
# Everything is wrong.
|
|
112
|
-
raise Error, "File #{@name} not found"
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
@stored_entries = @entry_set.dup
|
|
116
|
-
@stored_comment = @comment
|
|
117
|
-
@restore_ownership = options[:restore_ownership]
|
|
118
|
-
@restore_permissions = options[:restore_permissions]
|
|
119
|
-
@restore_times = options[:restore_times]
|
|
89
|
+
@restore_ownership = restore_ownership
|
|
90
|
+
@restore_permissions = restore_permissions
|
|
91
|
+
@restore_times = restore_times
|
|
92
|
+
@compression_level = compression_level
|
|
93
|
+
@suppress_extra_fields = suppress_extra_fields
|
|
120
94
|
end
|
|
121
95
|
|
|
122
96
|
class << self
|
|
123
97
|
# Similar to ::new. If a block is passed the Zip::File object is passed
|
|
124
98
|
# to the block and is automatically closed afterwards, just as with
|
|
125
99
|
# ruby's builtin File::open method.
|
|
126
|
-
def open(file_name,
|
|
127
|
-
|
|
100
|
+
def open(file_name, create: false,
|
|
101
|
+
restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
|
|
102
|
+
restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
|
|
103
|
+
restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
|
|
104
|
+
compression_level: ::Zip.default_compression,
|
|
105
|
+
suppress_extra_fields: false)
|
|
106
|
+
zf = ::Zip::File.new(file_name, create: create,
|
|
107
|
+
restore_ownership: restore_ownership,
|
|
108
|
+
restore_permissions: restore_permissions,
|
|
109
|
+
restore_times: restore_times,
|
|
110
|
+
compression_level: compression_level,
|
|
111
|
+
suppress_extra_fields: suppress_extra_fields)
|
|
128
112
|
|
|
129
|
-
zf = ::Zip::File.new(file_name, create: (dep_create || create), buffer: false, **options)
|
|
130
113
|
return zf unless block_given?
|
|
131
114
|
|
|
132
115
|
begin
|
|
@@ -136,31 +119,30 @@ module Zip
|
|
|
136
119
|
end
|
|
137
120
|
end
|
|
138
121
|
|
|
139
|
-
# Same as #open. But outputs data to a buffer instead of a file
|
|
140
|
-
def add_buffer
|
|
141
|
-
Zip.warn_about_v3_api('Zip::File.add_buffer')
|
|
142
|
-
|
|
143
|
-
io = ::StringIO.new
|
|
144
|
-
zf = ::Zip::File.new(io, true, true)
|
|
145
|
-
yield zf
|
|
146
|
-
zf.write_buffer(io)
|
|
147
|
-
end
|
|
148
|
-
|
|
149
122
|
# Like #open, but reads zip archive contents from a String or open IO
|
|
150
123
|
# stream, and outputs data to a buffer.
|
|
151
124
|
# (This can be used to extract data from a
|
|
152
125
|
# downloaded zip archive without first saving it to disk.)
|
|
153
|
-
def open_buffer(io,
|
|
126
|
+
def open_buffer(io = ::StringIO.new, create: false,
|
|
127
|
+
restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
|
|
128
|
+
restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
|
|
129
|
+
restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
|
|
130
|
+
compression_level: ::Zip.default_compression,
|
|
131
|
+
suppress_extra_fields: false)
|
|
154
132
|
unless IO_METHODS.map { |method| io.respond_to?(method) }.all? || io.kind_of?(String)
|
|
155
|
-
raise
|
|
133
|
+
raise 'Zip::File.open_buffer expects a String or IO-like argument' \
|
|
134
|
+
"(responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
|
|
156
135
|
end
|
|
157
136
|
|
|
158
137
|
io = ::StringIO.new(io) if io.kind_of?(::String)
|
|
159
138
|
|
|
160
|
-
|
|
161
|
-
|
|
139
|
+
zf = ::Zip::File.new(io, create: create, buffer: true,
|
|
140
|
+
restore_ownership: restore_ownership,
|
|
141
|
+
restore_permissions: restore_permissions,
|
|
142
|
+
restore_times: restore_times,
|
|
143
|
+
compression_level: compression_level,
|
|
144
|
+
suppress_extra_fields: suppress_extra_fields)
|
|
162
145
|
|
|
163
|
-
zf = ::Zip::File.new(io, create: true, buffer: true, **options)
|
|
164
146
|
return zf unless block_given?
|
|
165
147
|
|
|
166
148
|
yield zf
|
|
@@ -184,88 +166,40 @@ module Zip
|
|
|
184
166
|
end
|
|
185
167
|
end
|
|
186
168
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
MAX_SEGMENT_SIZE
|
|
192
|
-
else
|
|
193
|
-
segment_size
|
|
194
|
-
end
|
|
195
|
-
end
|
|
169
|
+
# Count the entries in a zip archive without reading the whole set of
|
|
170
|
+
# entry data into memory.
|
|
171
|
+
def count_entries(path_or_io)
|
|
172
|
+
cdir = ::Zip::CentralDirectory.new
|
|
196
173
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
174
|
+
if path_or_io.kind_of?(String)
|
|
175
|
+
::File.open(path_or_io, 'rb') do |f|
|
|
176
|
+
cdir.count_entries(f)
|
|
177
|
+
end
|
|
178
|
+
else
|
|
179
|
+
cdir.count_entries(path_or_io)
|
|
201
180
|
end
|
|
202
|
-
partial_zip_file_name ||= zip_file_name
|
|
203
|
-
partial_zip_file_name
|
|
204
181
|
end
|
|
205
182
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
def put_split_signature(szip_file, segment_size)
|
|
211
|
-
signature_packed = [SPLIT_SIGNATURE].pack('V')
|
|
212
|
-
szip_file << signature_packed
|
|
213
|
-
segment_size - signature_packed.size
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
#
|
|
217
|
-
# TODO: Make the code more understandable
|
|
183
|
+
# Recursively adds the contents of `src_dir` to the new archive
|
|
184
|
+
# `zip_file_name`, nested under `prefix` if given (the archive root
|
|
185
|
+
# otherwise). `src_dir` itself is not added, only its contents.
|
|
218
186
|
#
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
end
|
|
227
|
-
chunk_bytes = 0
|
|
228
|
-
until ssegment_size == chunk_bytes || zip_file.eof?
|
|
229
|
-
segment_bytes_left = ssegment_size - chunk_bytes
|
|
230
|
-
buffer_size = segment_bytes_left < DATA_BUFFER_SIZE ? segment_bytes_left : DATA_BUFFER_SIZE
|
|
231
|
-
chunk = zip_file.read(buffer_size)
|
|
232
|
-
chunk_bytes += buffer_size
|
|
233
|
-
szip_file << chunk
|
|
234
|
-
# Info for track splitting
|
|
235
|
-
yield segment_count, szip_file_index, chunk_bytes, ssegment_size if block_given?
|
|
236
|
-
end
|
|
187
|
+
# Symlinks are ignored (skipped, with a warning) rather than followed
|
|
188
|
+
# or added, to avoid the security issues they can pose. `max_depth`
|
|
189
|
+
# limits how many directory levels below `src_dir` are walked; anything
|
|
190
|
+
# deeper is skipped, also with a warning (default: 16).
|
|
191
|
+
def add_recursive(zip_file_name, src_dir, prefix: '', max_depth: 16, &continue_on_exists_proc)
|
|
192
|
+
Zip::File.open(zip_file_name, create: true) do |zf|
|
|
193
|
+
zf.add_recursive(src_dir, prefix: prefix, max_depth: max_depth, &continue_on_exists_proc)
|
|
237
194
|
end
|
|
238
195
|
end
|
|
239
196
|
|
|
240
|
-
#
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
raise Errno::ENOENT, zip_file_name unless ::File.readable?(zip_file_name)
|
|
246
|
-
|
|
247
|
-
if dep_segment_size != MAX_SEGMENT_SIZE || !dep_delete_zip_file || dep_partial_zip_file_name
|
|
248
|
-
Zip.warn_about_v3_api('Zip::File.split')
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
zip_file_size = ::File.size(zip_file_name)
|
|
252
|
-
segment_size = get_segment_size_for_split(segment_size || dep_segment_size)
|
|
253
|
-
return if zip_file_size <= segment_size
|
|
254
|
-
|
|
255
|
-
segment_count = get_segment_count_for_split(zip_file_size, segment_size)
|
|
256
|
-
# Checking for correct zip structure
|
|
257
|
-
::Zip::File.open(zip_file_name) {}
|
|
258
|
-
partial_zip_file_name = get_partial_zip_file_name(zip_file_name, (partial_zip_file_name || dep_partial_zip_file_name))
|
|
259
|
-
szip_file_index = 0
|
|
260
|
-
::File.open(zip_file_name, 'rb') do |zip_file|
|
|
261
|
-
until zip_file.eof?
|
|
262
|
-
szip_file_index += 1
|
|
263
|
-
save_splited_part(zip_file, partial_zip_file_name, zip_file_size, szip_file_index, segment_size, segment_count)
|
|
264
|
-
end
|
|
197
|
+
# Extracts every entry in the zip archive `zip_file_name` into
|
|
198
|
+
# `destination_directory`, preserving the archive's directory structure.
|
|
199
|
+
def extract_all(zip_file_name, destination_directory = '.', &block)
|
|
200
|
+
Zip::File.open(zip_file_name) do |zf|
|
|
201
|
+
zf.extract_all(destination_directory, &block)
|
|
265
202
|
end
|
|
266
|
-
delete_zip_file = delete_zip_file.nil? ? dep_delete_zip_file : delete_zip_file
|
|
267
|
-
::File.delete(zip_file_name) if delete_zip_file
|
|
268
|
-
szip_file_index
|
|
269
203
|
end
|
|
270
204
|
end
|
|
271
205
|
|
|
@@ -281,45 +215,30 @@ module Zip
|
|
|
281
215
|
# specified. If a block is passed the stream object is passed to the block and
|
|
282
216
|
# the stream is automatically closed afterwards just as with ruby's builtin
|
|
283
217
|
# File.open method.
|
|
284
|
-
|
|
285
|
-
def get_output_stream(entry,
|
|
286
|
-
dep_permission_int = nil, dep_comment = nil,
|
|
287
|
-
dep_extra = nil, dep_compressed_size = nil, dep_crc = nil,
|
|
288
|
-
dep_compression_method = nil, dep_size = nil, dep_time = nil,
|
|
289
|
-
permission_int: nil, comment: nil,
|
|
218
|
+
def get_output_stream(entry, permissions: nil, comment: nil,
|
|
290
219
|
extra: nil, compressed_size: nil, crc: nil,
|
|
291
|
-
compression_method: nil,
|
|
292
|
-
&a_proc)
|
|
293
|
-
|
|
294
|
-
unless dep_permission_int.nil? && dep_comment.nil? && dep_extra.nil? &&
|
|
295
|
-
dep_compressed_size.nil? && dep_crc.nil? && dep_compression_method.nil? &&
|
|
296
|
-
dep_size.nil? && dep_time.nil?
|
|
297
|
-
Zip.warn_about_v3_api('Zip::File#get_output_stream')
|
|
298
|
-
end
|
|
299
|
-
|
|
220
|
+
compression_method: nil, compression_level: nil,
|
|
221
|
+
size: nil, time: nil, &a_proc)
|
|
300
222
|
new_entry =
|
|
301
223
|
if entry.kind_of?(Entry)
|
|
302
224
|
entry
|
|
303
225
|
else
|
|
304
|
-
Entry.new(
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
size: (size || dep_size),
|
|
311
|
-
time: (time || dep_time))
|
|
226
|
+
Entry.new(
|
|
227
|
+
@name, entry.to_s, comment: comment, extra: extra,
|
|
228
|
+
compressed_size: compressed_size, crc: crc, size: size,
|
|
229
|
+
compression_method: compression_method,
|
|
230
|
+
compression_level: compression_level, time: time
|
|
231
|
+
)
|
|
312
232
|
end
|
|
313
233
|
if new_entry.directory?
|
|
314
234
|
raise ArgumentError,
|
|
315
235
|
"cannot open stream to directory entry - '#{new_entry}'"
|
|
316
236
|
end
|
|
317
|
-
new_entry.unix_perms =
|
|
237
|
+
new_entry.unix_perms = permissions
|
|
318
238
|
zip_streamable_entry = StreamableStream.new(new_entry)
|
|
319
|
-
@
|
|
239
|
+
@cdir << zip_streamable_entry
|
|
320
240
|
zip_streamable_entry.get_output_stream(&a_proc)
|
|
321
241
|
end
|
|
322
|
-
# rubocop:enable Metrics/ParameterLists, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
|
323
242
|
|
|
324
243
|
# Returns the name of the zip archive
|
|
325
244
|
def to_s
|
|
@@ -335,31 +254,58 @@ module Zip
|
|
|
335
254
|
def add(entry, src_path, &continue_on_exists_proc)
|
|
336
255
|
continue_on_exists_proc ||= proc { ::Zip.continue_on_exists_proc }
|
|
337
256
|
check_entry_exists(entry, continue_on_exists_proc, 'add')
|
|
338
|
-
new_entry = entry.kind_of?(::Zip::Entry)
|
|
257
|
+
new_entry = if entry.kind_of?(::Zip::Entry)
|
|
258
|
+
entry
|
|
259
|
+
else
|
|
260
|
+
::Zip::Entry.new(
|
|
261
|
+
@name, entry.to_s,
|
|
262
|
+
compression_level: @compression_level
|
|
263
|
+
)
|
|
264
|
+
end
|
|
339
265
|
new_entry.gather_fileinfo_from_srcpath(src_path)
|
|
340
|
-
|
|
341
|
-
@entry_set << new_entry
|
|
266
|
+
@cdir << new_entry
|
|
342
267
|
end
|
|
343
268
|
|
|
344
269
|
# Convenience method for adding the contents of a file to the archive
|
|
345
270
|
# in Stored format (uncompressed)
|
|
346
271
|
def add_stored(entry, src_path, &continue_on_exists_proc)
|
|
347
|
-
entry = ::Zip::Entry.new(
|
|
272
|
+
entry = ::Zip::Entry.new(
|
|
273
|
+
@name, entry.to_s, compression_method: ::Zip::Entry::STORED
|
|
274
|
+
)
|
|
348
275
|
add(entry, src_path, &continue_on_exists_proc)
|
|
349
276
|
end
|
|
350
277
|
|
|
278
|
+
# Recursively adds the contents of `src_dir` to the archive, nested
|
|
279
|
+
# under `prefix` if given (the archive root otherwise). `src_dir`
|
|
280
|
+
# itself is not added, only its contents.
|
|
281
|
+
#
|
|
282
|
+
# Symlinks are ignored (skipped, with a warning) rather than followed
|
|
283
|
+
# or added, to avoid the security issues they can pose. `max_depth`
|
|
284
|
+
# limits how many directory levels below `src_dir` are walked; anything
|
|
285
|
+
# deeper is skipped, also with a warning (default: 16).
|
|
286
|
+
def add_recursive(src_dir, prefix: '', max_depth: 16, &continue_on_exists_proc)
|
|
287
|
+
raise Errno::ENOENT, src_dir unless ::File.directory?(src_dir)
|
|
288
|
+
raise ArgumentError, 'max_depth must be at least 1' if max_depth < 1
|
|
289
|
+
|
|
290
|
+
prefix = prefix.to_s
|
|
291
|
+
prefix += '/' unless prefix.empty? || prefix.end_with?('/')
|
|
292
|
+
|
|
293
|
+
add_recursive_dir(prefix, src_dir, 1, max_depth, continue_on_exists_proc)
|
|
294
|
+
self
|
|
295
|
+
end
|
|
296
|
+
|
|
351
297
|
# Removes the specified entry.
|
|
352
298
|
def remove(entry)
|
|
353
|
-
@
|
|
299
|
+
@cdir.delete(get_entry(entry))
|
|
354
300
|
end
|
|
355
301
|
|
|
356
302
|
# Renames the specified entry.
|
|
357
303
|
def rename(entry, new_name, &continue_on_exists_proc)
|
|
358
304
|
found_entry = get_entry(entry)
|
|
359
305
|
check_entry_exists(new_name, continue_on_exists_proc, 'rename')
|
|
360
|
-
@
|
|
306
|
+
@cdir.delete(found_entry)
|
|
361
307
|
found_entry.name = new_name
|
|
362
|
-
@
|
|
308
|
+
@cdir << found_entry
|
|
363
309
|
end
|
|
364
310
|
|
|
365
311
|
# Replaces the specified entry with the contents of src_path (from
|
|
@@ -370,25 +316,43 @@ module Zip
|
|
|
370
316
|
add(entry, src_path)
|
|
371
317
|
end
|
|
372
318
|
|
|
373
|
-
# Extracts entry to file dest_path.
|
|
374
|
-
def extract(entry, dest_path, &block)
|
|
375
|
-
Zip.warn_about_v3_api('Zip::File#extract')
|
|
376
|
-
|
|
377
|
-
block ||= proc { ::Zip.on_exists_proc }
|
|
378
|
-
found_entry = get_entry(entry)
|
|
379
|
-
found_entry.extract(dest_path, &block)
|
|
380
|
-
end
|
|
381
|
-
|
|
382
319
|
# Extracts `entry` to a file at `entry_path`, with `destination_directory`
|
|
383
320
|
# as the base location in the filesystem.
|
|
384
321
|
#
|
|
385
322
|
# NB: The caller is responsible for making sure `destination_directory` is
|
|
386
323
|
# safe, if it is passed.
|
|
387
|
-
def
|
|
324
|
+
def extract(entry, entry_path = nil, destination_directory: '.', &block)
|
|
388
325
|
block ||= proc { ::Zip.on_exists_proc }
|
|
389
326
|
found_entry = get_entry(entry)
|
|
390
327
|
entry_path ||= found_entry.name
|
|
391
|
-
found_entry.
|
|
328
|
+
found_entry.extract(entry_path, destination_directory: destination_directory, &block)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Extracts every entry in the archive into `destination_directory`,
|
|
332
|
+
# preserving the archive's directory structure.
|
|
333
|
+
#
|
|
334
|
+
# Symlink entries are ignored (skipped, with a warning) rather than
|
|
335
|
+
# extracted. See `Entry#extract` for the path-safety checks applied to
|
|
336
|
+
# every other entry.
|
|
337
|
+
#
|
|
338
|
+
# NB: The caller is responsible for making sure `destination_directory` is
|
|
339
|
+
# safe, if it is passed.
|
|
340
|
+
def extract_all(destination_directory = '.', &block)
|
|
341
|
+
# Partition the entries into directories and non-directories, so that
|
|
342
|
+
# directories are created first.
|
|
343
|
+
dirs, others = entries.partition(&:directory?)
|
|
344
|
+
|
|
345
|
+
(dirs + others).each do |entry|
|
|
346
|
+
if entry.symlink?
|
|
347
|
+
warn "WARNING: skipped symlink '#{entry.name}' during extract_all."
|
|
348
|
+
next
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
entry.extract(entry.name, destination_directory: destination_directory,
|
|
352
|
+
create_parent_directories: true, &block)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
self
|
|
392
356
|
end
|
|
393
357
|
|
|
394
358
|
# Commits changes that has been made since the previous commit to
|
|
@@ -397,25 +361,24 @@ module Zip
|
|
|
397
361
|
return if name.kind_of?(StringIO) || !commit_required?
|
|
398
362
|
|
|
399
363
|
on_success_replace do |tmp_file|
|
|
400
|
-
::Zip::OutputStream.open(tmp_file) do |zos|
|
|
401
|
-
@
|
|
364
|
+
::Zip::OutputStream.open(tmp_file, suppress_extra_fields: @suppress_extra_fields) do |zos|
|
|
365
|
+
@cdir.each do |e|
|
|
402
366
|
e.write_to_zip_output_stream(zos)
|
|
403
|
-
e.dirty = false
|
|
404
367
|
e.clean_up
|
|
405
368
|
end
|
|
406
369
|
zos.comment = comment
|
|
407
370
|
end
|
|
408
371
|
true
|
|
409
372
|
end
|
|
410
|
-
|
|
373
|
+
initialize_cdir(@name)
|
|
411
374
|
end
|
|
412
375
|
|
|
413
376
|
# Write buffer write changes to buffer and return
|
|
414
377
|
def write_buffer(io = ::StringIO.new)
|
|
415
378
|
return io unless commit_required?
|
|
416
379
|
|
|
417
|
-
::Zip::OutputStream.write_buffer(io) do |zos|
|
|
418
|
-
@
|
|
380
|
+
::Zip::OutputStream.write_buffer(io, suppress_extra_fields: @suppress_extra_fields) do |zos|
|
|
381
|
+
@cdir.each { |e| e.write_to_zip_output_stream(zos) }
|
|
419
382
|
zos.comment = comment
|
|
420
383
|
end
|
|
421
384
|
end
|
|
@@ -428,16 +391,19 @@ module Zip
|
|
|
428
391
|
# Returns true if any changes has been made to this archive since
|
|
429
392
|
# the previous commit
|
|
430
393
|
def commit_required?
|
|
431
|
-
@
|
|
432
|
-
|
|
394
|
+
return true if @create || @cdir.dirty?
|
|
395
|
+
|
|
396
|
+
@cdir.each do |e|
|
|
397
|
+
return true if e.dirty?
|
|
433
398
|
end
|
|
434
|
-
|
|
399
|
+
|
|
400
|
+
false
|
|
435
401
|
end
|
|
436
402
|
|
|
437
403
|
# Searches for entry with the specified name. Returns nil if
|
|
438
404
|
# no entry is found. See also get_entry
|
|
439
405
|
def find_entry(entry_name)
|
|
440
|
-
selected_entry = @
|
|
406
|
+
selected_entry = @cdir.find_entry(entry_name)
|
|
441
407
|
return if selected_entry.nil?
|
|
442
408
|
|
|
443
409
|
selected_entry.restore_ownership = @restore_ownership
|
|
@@ -446,11 +412,6 @@ module Zip
|
|
|
446
412
|
selected_entry
|
|
447
413
|
end
|
|
448
414
|
|
|
449
|
-
# Searches for entries given a glob
|
|
450
|
-
def glob(*args, &block)
|
|
451
|
-
@entry_set.glob(*args, &block)
|
|
452
|
-
end
|
|
453
|
-
|
|
454
415
|
# Searches for an entry just as find_entry, but throws Errno::ENOENT
|
|
455
416
|
# if no entry is found.
|
|
456
417
|
def get_entry(entry)
|
|
@@ -466,33 +427,73 @@ module Zip
|
|
|
466
427
|
|
|
467
428
|
entry_name = entry_name.dup.to_s
|
|
468
429
|
entry_name << '/' unless entry_name.end_with?('/')
|
|
469
|
-
@
|
|
430
|
+
@cdir << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
|
|
470
431
|
end
|
|
471
432
|
|
|
472
433
|
private
|
|
473
434
|
|
|
474
|
-
def
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
435
|
+
def add_recursive_dir(entry_prefix, src_dir, depth, max_depth, continue_on_exists_proc)
|
|
436
|
+
::Dir.children(src_dir).sort.each do |child|
|
|
437
|
+
src_path = ::File.join(src_dir, child)
|
|
438
|
+
|
|
439
|
+
if ::File.symlink?(src_path)
|
|
440
|
+
warn "WARNING: skipped symlink '#{src_path}' while adding directory contents."
|
|
441
|
+
elsif ::File.directory?(src_path)
|
|
442
|
+
entry_name = "#{entry_prefix}#{child}/"
|
|
443
|
+
add(entry_name, src_path, &continue_on_exists_proc)
|
|
444
|
+
|
|
445
|
+
if depth < max_depth
|
|
446
|
+
add_recursive_dir(entry_name, src_path, depth + 1, max_depth, continue_on_exists_proc)
|
|
447
|
+
else
|
|
448
|
+
warn "WARNING: max_depth (#{max_depth}) reached, not descending into '#{src_path}'."
|
|
449
|
+
end
|
|
450
|
+
elsif ::File.file?(src_path)
|
|
451
|
+
add("#{entry_prefix}#{child}", src_path, &continue_on_exists_proc)
|
|
452
|
+
else
|
|
453
|
+
warn "WARNING: skipped '#{src_path}' - not a regular file or directory."
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def initialize_cdir(path_or_io, buffer: false)
|
|
459
|
+
@cdir = ::Zip::CentralDirectory.new
|
|
460
|
+
|
|
461
|
+
if ::File.size?(@name.to_s)
|
|
462
|
+
# There is a file, which exists, that is associated with this zip.
|
|
463
|
+
@create = false
|
|
464
|
+
@file_permissions = ::File.stat(@name).mode
|
|
465
|
+
|
|
466
|
+
if buffer
|
|
467
|
+
# https://github.com/rubyzip/rubyzip/issues/119
|
|
468
|
+
path_or_io.binmode if path_or_io.respond_to?(:binmode)
|
|
469
|
+
@cdir.read_from_stream(path_or_io)
|
|
470
|
+
else
|
|
471
|
+
::File.open(@name, 'rb') do |f|
|
|
472
|
+
@cdir.read_from_stream(f)
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
elsif buffer && path_or_io.size > 0
|
|
476
|
+
# This zip is probably a non-empty StringIO.
|
|
477
|
+
@create = false
|
|
478
|
+
@cdir.read_from_stream(path_or_io)
|
|
479
|
+
elsif !@create && ::File.empty?(@name)
|
|
480
|
+
# A file exists, but it is empty, and we've said we're
|
|
481
|
+
# NOT creating a new zip.
|
|
482
|
+
raise Error, "File #{@name} has zero size. Did you mean to pass the create flag?"
|
|
483
|
+
elsif !@create
|
|
484
|
+
# If we get here, and we're not creating a new zip, then
|
|
485
|
+
# everything is wrong.
|
|
486
|
+
raise Error, "File #{@name} not found"
|
|
482
487
|
end
|
|
483
|
-
new_entry.directory? && path_is_directory
|
|
484
488
|
end
|
|
485
489
|
|
|
486
490
|
def check_entry_exists(entry_name, continue_on_exists_proc, proc_name)
|
|
491
|
+
return unless @cdir.include?(entry_name)
|
|
492
|
+
|
|
487
493
|
continue_on_exists_proc ||= proc { Zip.continue_on_exists_proc }
|
|
488
|
-
|
|
494
|
+
raise ::Zip::EntryExistsError.new proc_name, entry_name unless continue_on_exists_proc.call
|
|
489
495
|
|
|
490
|
-
|
|
491
|
-
remove get_entry(entry_name)
|
|
492
|
-
else
|
|
493
|
-
raise ::Zip::EntryExistsError,
|
|
494
|
-
proc_name + " failed. Entry #{entry_name} already exists"
|
|
495
|
-
end
|
|
496
|
+
remove get_entry(entry_name)
|
|
496
497
|
end
|
|
497
498
|
|
|
498
499
|
def check_file(path)
|
|
@@ -502,14 +503,12 @@ module Zip
|
|
|
502
503
|
def on_success_replace
|
|
503
504
|
dirname, basename = ::File.split(name)
|
|
504
505
|
::Dir::Tmpname.create(basename, dirname) do |tmp_filename|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
::File.chmod(@file_permissions, name) unless @create
|
|
509
|
-
end
|
|
510
|
-
ensure
|
|
511
|
-
::File.unlink(tmp_filename) if ::File.exist?(tmp_filename)
|
|
506
|
+
if yield tmp_filename
|
|
507
|
+
::File.rename(tmp_filename, name)
|
|
508
|
+
::File.chmod(@file_permissions, name) unless @create
|
|
512
509
|
end
|
|
510
|
+
ensure
|
|
511
|
+
FileUtils.rm_f(tmp_filename)
|
|
513
512
|
end
|
|
514
513
|
end
|
|
515
514
|
end
|