ziperu 0.1.1-x86_64-linux
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 +7 -0
- data/CHANGELOG.md +24 -0
- data/LICENSE-rubyzip.txt +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +276 -0
- data/lib/rubyzip.rb +4 -0
- data/lib/zip/central_directory.rb +5 -0
- data/lib/zip/compressor.rb +5 -0
- data/lib/zip/constants.rb +5 -0
- data/lib/zip/crypto/aes_encryption.rb +5 -0
- data/lib/zip/crypto/decrypted_io.rb +5 -0
- data/lib/zip/crypto/encryption.rb +5 -0
- data/lib/zip/crypto/null_encryption.rb +5 -0
- data/lib/zip/crypto/traditional_encryption.rb +5 -0
- data/lib/zip/crypto.rb +5 -0
- data/lib/zip/decompressor.rb +5 -0
- data/lib/zip/deflater.rb +5 -0
- data/lib/zip/dirtyable.rb +5 -0
- data/lib/zip/dos_time.rb +5 -0
- data/lib/zip/entry.rb +5 -0
- data/lib/zip/entry_set.rb +5 -0
- data/lib/zip/errors.rb +5 -0
- data/lib/zip/extra_field/aes.rb +5 -0
- data/lib/zip/extra_field/generic.rb +5 -0
- data/lib/zip/extra_field/ntfs.rb +5 -0
- data/lib/zip/extra_field/old_unix.rb +5 -0
- data/lib/zip/extra_field/universal_time.rb +5 -0
- data/lib/zip/extra_field/unix.rb +5 -0
- data/lib/zip/extra_field/unknown.rb +5 -0
- data/lib/zip/extra_field/zip64.rb +5 -0
- data/lib/zip/extra_field.rb +5 -0
- data/lib/zip/file.rb +5 -0
- data/lib/zip/file_split.rb +5 -0
- data/lib/zip/filesystem/dir.rb +5 -0
- data/lib/zip/filesystem/directory_iterator.rb +5 -0
- data/lib/zip/filesystem/file.rb +5 -0
- data/lib/zip/filesystem/file_stat.rb +5 -0
- data/lib/zip/filesystem/zip_file_name_mapper.rb +5 -0
- data/lib/zip/filesystem.rb +5 -0
- data/lib/zip/inflater.rb +5 -0
- data/lib/zip/input_stream.rb +5 -0
- data/lib/zip/ioextras/abstract_input_stream.rb +5 -0
- data/lib/zip/ioextras/abstract_output_stream.rb +5 -0
- data/lib/zip/ioextras/fake_io.rb +5 -0
- data/lib/zip/ioextras.rb +5 -0
- data/lib/zip/null_compressor.rb +5 -0
- data/lib/zip/null_decompressor.rb +5 -0
- data/lib/zip/null_input_stream.rb +5 -0
- data/lib/zip/output_stream.rb +5 -0
- data/lib/zip/pass_thru_compressor.rb +5 -0
- data/lib/zip/pass_thru_decompressor.rb +5 -0
- data/lib/zip/streamable_directory.rb +5 -0
- data/lib/zip/streamable_stream.rb +5 -0
- data/lib/zip/version.rb +5 -0
- data/lib/zip.rb +19 -0
- data/lib/ziperu/3.1/ziperu.so +0 -0
- data/lib/ziperu/3.2/ziperu.so +0 -0
- data/lib/ziperu/3.3/ziperu.so +0 -0
- data/lib/ziperu/3.4/ziperu.so +0 -0
- data/lib/ziperu/4.0/ziperu.so +0 -0
- data/lib/ziperu/central_directory.rb +136 -0
- data/lib/ziperu/compressor.rb +14 -0
- data/lib/ziperu/constants.rb +148 -0
- data/lib/ziperu/crypto.rb +72 -0
- data/lib/ziperu/decompressor.rb +35 -0
- data/lib/ziperu/deflater.rb +41 -0
- data/lib/ziperu/dirtyable.rb +48 -0
- data/lib/ziperu/dos_time.rb +87 -0
- data/lib/ziperu/entry.rb +760 -0
- data/lib/ziperu/entry_set.rb +147 -0
- data/lib/ziperu/errors.rb +154 -0
- data/lib/ziperu/extra_field/aes.rb +52 -0
- data/lib/ziperu/extra_field/generic.rb +45 -0
- data/lib/ziperu/extra_field/ntfs.rb +98 -0
- data/lib/ziperu/extra_field/old_unix.rb +52 -0
- data/lib/ziperu/extra_field/universal_time.rb +81 -0
- data/lib/ziperu/extra_field/unix.rb +45 -0
- data/lib/ziperu/extra_field/unknown.rb +37 -0
- data/lib/ziperu/extra_field/zip64.rb +86 -0
- data/lib/ziperu/extra_field.rb +109 -0
- data/lib/ziperu/file.rb +614 -0
- data/lib/ziperu/file_split.rb +93 -0
- data/lib/ziperu/filesystem/dir.rb +88 -0
- data/lib/ziperu/filesystem/directory_iterator.rb +50 -0
- data/lib/ziperu/filesystem/file.rb +265 -0
- data/lib/ziperu/filesystem/file_stat.rb +112 -0
- data/lib/ziperu/filesystem/zip_file_name_mapper.rb +83 -0
- data/lib/ziperu/filesystem.rb +76 -0
- data/lib/ziperu/inflater.rb +60 -0
- data/lib/ziperu/input_stream.rb +296 -0
- data/lib/ziperu/ioextras/abstract_input_stream.rb +137 -0
- data/lib/ziperu/ioextras/abstract_output_stream.rb +57 -0
- data/lib/ziperu/ioextras/fake_io.rb +41 -0
- data/lib/ziperu/ioextras.rb +42 -0
- data/lib/ziperu/null_compressor.rb +20 -0
- data/lib/ziperu/null_decompressor.rb +25 -0
- data/lib/ziperu/null_input_stream.rb +55 -0
- data/lib/ziperu/output_stream.rb +223 -0
- data/lib/ziperu/pass_thru_compressor.rb +29 -0
- data/lib/ziperu/pass_thru_decompressor.rb +37 -0
- data/lib/ziperu/streamable_directory.rb +15 -0
- data/lib/ziperu/streamable_stream.rb +59 -0
- data/lib/ziperu/version.rb +7 -0
- data/lib/ziperu.rb +116 -0
- metadata +156 -0
data/lib/ziperu/file.rb
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ziperu
|
|
4
|
+
# Ziperu::File is modeled after java.util.zip.ZipFile from the Java SDK, with
|
|
5
|
+
# rubyzip's API. The most important methods are those for accessing
|
|
6
|
+
# information about the entries in the archive and `get_input_stream` /
|
|
7
|
+
# `get_output_stream` for reading from and writing entries to it, plus the
|
|
8
|
+
# conveniences `extract`, `extract_all`, `add_recursive`, `remove`,
|
|
9
|
+
# `replace`, `rename` and `mkdir`.
|
|
10
|
+
#
|
|
11
|
+
# Modifications are not committed until `commit` or `close` is called.
|
|
12
|
+
# `open` accepts a block following the pattern of ::File.open, closing the
|
|
13
|
+
# archive when the block returns:
|
|
14
|
+
#
|
|
15
|
+
# Ziperu::File.open('my.zip', create: true) do |zipfile|
|
|
16
|
+
# zipfile.get_output_stream('first.txt') { |f| f.puts 'Hello from Ziperu::File' }
|
|
17
|
+
# zipfile.mkdir('a_dir')
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# On commit, entries read from the archive are copied byte for byte
|
|
21
|
+
# without recompression; only added or replaced entries are compressed.
|
|
22
|
+
class File
|
|
23
|
+
include Enumerable
|
|
24
|
+
extend Forwardable
|
|
25
|
+
|
|
26
|
+
IO_METHODS = [:tell, :seek, :read, :eof, :close].freeze # :nodoc:
|
|
27
|
+
|
|
28
|
+
# The name of this zip archive.
|
|
29
|
+
attr_reader :name
|
|
30
|
+
|
|
31
|
+
attr_accessor :restore_ownership, :restore_permissions, :restore_times
|
|
32
|
+
|
|
33
|
+
# The archive comment.
|
|
34
|
+
attr_reader :comment
|
|
35
|
+
|
|
36
|
+
def_delegators :@cdir, :each, :entries, :glob, :size
|
|
37
|
+
|
|
38
|
+
# Opens a zip archive. Pass create: true to create a new archive if it
|
|
39
|
+
# doesn't exist already.
|
|
40
|
+
def initialize(path_or_io, create: false, buffer: false,
|
|
41
|
+
restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
|
|
42
|
+
restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
|
|
43
|
+
restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
|
|
44
|
+
compression_level: ::Ziperu.default_compression,
|
|
45
|
+
suppress_extra_fields: false)
|
|
46
|
+
super()
|
|
47
|
+
|
|
48
|
+
@name = path_or_io.respond_to?(:path) ? path_or_io.path : path_or_io
|
|
49
|
+
@create = create ? true : false # allow any truthy value to mean true
|
|
50
|
+
|
|
51
|
+
initialize_cdir(path_or_io, buffer: buffer)
|
|
52
|
+
|
|
53
|
+
@restore_ownership = restore_ownership
|
|
54
|
+
@restore_permissions = restore_permissions
|
|
55
|
+
@restore_times = restore_times
|
|
56
|
+
@compression_level = compression_level
|
|
57
|
+
@suppress_extra_fields = suppress_extra_fields
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class << self
|
|
61
|
+
include FileSplit
|
|
62
|
+
|
|
63
|
+
# Similar to ::new. If a block is passed the Ziperu::File object is passed
|
|
64
|
+
# to the block and is automatically closed afterwards.
|
|
65
|
+
def open(file_name, create: false,
|
|
66
|
+
restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
|
|
67
|
+
restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
|
|
68
|
+
restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
|
|
69
|
+
compression_level: ::Ziperu.default_compression,
|
|
70
|
+
suppress_extra_fields: false)
|
|
71
|
+
zf = ::Ziperu::File.new(file_name, create: create,
|
|
72
|
+
restore_ownership: restore_ownership,
|
|
73
|
+
restore_permissions: restore_permissions,
|
|
74
|
+
restore_times: restore_times,
|
|
75
|
+
compression_level: compression_level,
|
|
76
|
+
suppress_extra_fields: suppress_extra_fields)
|
|
77
|
+
|
|
78
|
+
return zf unless block_given?
|
|
79
|
+
|
|
80
|
+
begin
|
|
81
|
+
yield zf
|
|
82
|
+
ensure
|
|
83
|
+
zf.close
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Like #open, but reads zip archive contents from a String or open IO
|
|
88
|
+
# stream, and outputs data to a buffer.
|
|
89
|
+
def open_buffer(io = ::StringIO.new, create: false,
|
|
90
|
+
restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
|
|
91
|
+
restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
|
|
92
|
+
restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
|
|
93
|
+
compression_level: ::Ziperu.default_compression,
|
|
94
|
+
suppress_extra_fields: false)
|
|
95
|
+
unless IO_METHODS.map { |method| io.respond_to?(method) }.all? || io.kind_of?(String)
|
|
96
|
+
raise 'Ziperu::File.open_buffer expects a String or IO-like argument' \
|
|
97
|
+
"(responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
io = ::StringIO.new(io) if io.kind_of?(::String)
|
|
101
|
+
|
|
102
|
+
zf = ::Ziperu::File.new(io, create: create, buffer: true,
|
|
103
|
+
restore_ownership: restore_ownership,
|
|
104
|
+
restore_permissions: restore_permissions,
|
|
105
|
+
restore_times: restore_times,
|
|
106
|
+
compression_level: compression_level,
|
|
107
|
+
suppress_extra_fields: suppress_extra_fields)
|
|
108
|
+
|
|
109
|
+
return zf unless block_given?
|
|
110
|
+
|
|
111
|
+
yield zf
|
|
112
|
+
|
|
113
|
+
begin
|
|
114
|
+
zf.write_buffer(io)
|
|
115
|
+
rescue IOError => e
|
|
116
|
+
raise unless e.message == 'not opened for writing'
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Iterates over the contents of the ZipFile.
|
|
121
|
+
def foreach(zip_file_name, &block)
|
|
122
|
+
::Ziperu::File.open(zip_file_name) do |zip_file|
|
|
123
|
+
zip_file.each(&block)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Count the entries in a zip archive without building entry objects.
|
|
128
|
+
def count_entries(path_or_io)
|
|
129
|
+
CentralDirectory.count_entries(path_or_io)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Recursively adds the contents of `src_dir` to the new archive
|
|
133
|
+
# `zip_file_name`, nested under `prefix` if given.
|
|
134
|
+
def add_recursive(zip_file_name, src_dir, prefix: '', max_depth: 16, &continue_on_exists_proc)
|
|
135
|
+
Ziperu::File.open(zip_file_name, create: true) do |zf|
|
|
136
|
+
zf.add_recursive(src_dir, prefix: prefix, max_depth: max_depth, &continue_on_exists_proc)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Extracts every entry of the archive at `zip_file_name` into
|
|
141
|
+
# `destination_directory`.
|
|
142
|
+
def extract_all(zip_file_name, destination_directory = '.', &block)
|
|
143
|
+
Ziperu::File.open(zip_file_name) do |zf|
|
|
144
|
+
zf.extract_all(destination_directory, &block)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def comment=(comment)
|
|
150
|
+
@dirty = true
|
|
151
|
+
@comment = comment
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Returns an input stream to the specified entry. If a block is passed
|
|
155
|
+
# the stream object is passed to the block and closed afterwards.
|
|
156
|
+
def get_input_stream(entry, &a_proc)
|
|
157
|
+
get_entry(entry).get_input_stream(&a_proc)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Returns an output stream to the specified entry. If entry is not an
|
|
161
|
+
# instance of Ziperu::Entry, a new Ziperu::Entry will be initialized using the
|
|
162
|
+
# arguments specified. If a block is passed the stream object is passed
|
|
163
|
+
# to the block and closed afterwards.
|
|
164
|
+
def get_output_stream(entry, permissions: nil, comment: nil,
|
|
165
|
+
extra: nil, compressed_size: nil, crc: nil,
|
|
166
|
+
compression_method: nil, compression_level: nil,
|
|
167
|
+
size: nil, time: nil, &a_proc)
|
|
168
|
+
new_entry =
|
|
169
|
+
if entry.kind_of?(Entry)
|
|
170
|
+
entry
|
|
171
|
+
else
|
|
172
|
+
Entry.new(
|
|
173
|
+
@name, entry.to_s, comment: comment, extra: extra,
|
|
174
|
+
compressed_size: compressed_size, crc: crc, size: size,
|
|
175
|
+
compression_method: compression_method,
|
|
176
|
+
compression_level: compression_level, time: time
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
if new_entry.directory?
|
|
180
|
+
raise ArgumentError,
|
|
181
|
+
"cannot open stream to directory entry - '#{new_entry}'"
|
|
182
|
+
end
|
|
183
|
+
new_entry.unix_perms = permissions
|
|
184
|
+
zip_streamable_entry = StreamableStream.new(new_entry)
|
|
185
|
+
@cdir << zip_streamable_entry
|
|
186
|
+
@dirty = true
|
|
187
|
+
zip_streamable_entry.get_output_stream(&a_proc)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Returns the name of the zip archive
|
|
191
|
+
def to_s
|
|
192
|
+
@name
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Returns a string containing the contents of the specified entry
|
|
196
|
+
def read(entry)
|
|
197
|
+
get_entry(entry).read_all
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Convenience method for adding the contents of a file to the archive
|
|
201
|
+
def add(entry, src_path, &continue_on_exists_proc)
|
|
202
|
+
continue_on_exists_proc ||= proc { ::Ziperu.continue_on_exists_proc }
|
|
203
|
+
check_entry_exists(entry, continue_on_exists_proc, 'add')
|
|
204
|
+
new_entry = if entry.kind_of?(::Ziperu::Entry)
|
|
205
|
+
entry
|
|
206
|
+
else
|
|
207
|
+
::Ziperu::Entry.new(
|
|
208
|
+
@name, entry.to_s,
|
|
209
|
+
compression_level: @compression_level
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
new_entry.gather_fileinfo_from_srcpath(src_path)
|
|
213
|
+
@cdir << new_entry
|
|
214
|
+
@dirty = true
|
|
215
|
+
new_entry
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Convenience method for adding the contents of a file to the archive
|
|
219
|
+
# in Stored format (uncompressed)
|
|
220
|
+
def add_stored(entry, src_path, &continue_on_exists_proc)
|
|
221
|
+
entry = ::Ziperu::Entry.new(
|
|
222
|
+
@name, entry.to_s, compression_method: ::Ziperu::Entry::STORED
|
|
223
|
+
)
|
|
224
|
+
add(entry, src_path, &continue_on_exists_proc)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Recursively adds the contents of `src_dir` to the archive, nested
|
|
228
|
+
# under `prefix` if given. Symlinks are skipped with a warning;
|
|
229
|
+
# `max_depth` limits how many directory levels are walked.
|
|
230
|
+
#
|
|
231
|
+
# `progress:` is called with `(entry, done, nil)` after each file or
|
|
232
|
+
# directory is added; the total is not known up front.
|
|
233
|
+
def add_recursive(src_dir, prefix: '', max_depth: 16, progress: nil, &continue_on_exists_proc)
|
|
234
|
+
raise Errno::ENOENT, src_dir unless ::File.directory?(src_dir)
|
|
235
|
+
raise ArgumentError, 'max_depth must be at least 1' if max_depth < 1
|
|
236
|
+
|
|
237
|
+
prefix = prefix.to_s
|
|
238
|
+
prefix += '/' unless prefix.empty? || prefix.end_with?('/')
|
|
239
|
+
|
|
240
|
+
done = 0
|
|
241
|
+
report = ->(entry) { done += 1; progress&.call(entry, done, nil) }
|
|
242
|
+
add_recursive_dir(prefix, src_dir, 1, max_depth, continue_on_exists_proc, report)
|
|
243
|
+
self
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Removes the specified entry.
|
|
247
|
+
def remove(entry)
|
|
248
|
+
found_entry = get_entry(entry)
|
|
249
|
+
@dirty = true
|
|
250
|
+
@cdir.delete(found_entry)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Renames the specified entry.
|
|
254
|
+
def rename(entry, new_name, &continue_on_exists_proc)
|
|
255
|
+
found_entry = get_entry(entry)
|
|
256
|
+
check_entry_exists(new_name, continue_on_exists_proc, 'rename')
|
|
257
|
+
@cdir.delete(found_entry)
|
|
258
|
+
found_entry.name = new_name
|
|
259
|
+
@cdir << found_entry
|
|
260
|
+
@dirty = true
|
|
261
|
+
found_entry
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Replaces the specified entry with the contents of src_path (from
|
|
265
|
+
# the file system).
|
|
266
|
+
def replace(entry, src_path)
|
|
267
|
+
check_file(src_path)
|
|
268
|
+
remove(entry)
|
|
269
|
+
add(entry, src_path)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Extracts `entry` to a file at `entry_path`, with
|
|
273
|
+
# `destination_directory` as the base. The block (or Ziperu.on_exists_proc)
|
|
274
|
+
# decides whether an existing file is overwritten.
|
|
275
|
+
def extract(entry, entry_path = nil, destination_directory: '.', symlinks: :skip, &block)
|
|
276
|
+
block ||= proc { ::Ziperu.on_exists_proc }
|
|
277
|
+
found_entry = get_entry(entry)
|
|
278
|
+
entry_path ||= found_entry.name
|
|
279
|
+
found_entry.extract(entry_path, destination_directory: destination_directory,
|
|
280
|
+
symlinks: symlinks, &block)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Extracts every entry in the archive into `destination_directory`,
|
|
284
|
+
# preserving the archive's directory structure. Symlink entries are
|
|
285
|
+
# skipped with a warning; see `Entry#extract` for the path-safety checks
|
|
286
|
+
# applied to every other entry.
|
|
287
|
+
#
|
|
288
|
+
# `symlinks:` is `:skip` (the default, with a warning per link, as in
|
|
289
|
+
# rubyzip 3) or `:create`, which creates links whose target stays inside
|
|
290
|
+
# `destination_directory`. `progress:` is called with
|
|
291
|
+
# `(entry, done, total)` after each entry.
|
|
292
|
+
def extract_all(destination_directory = '.', symlinks: :skip, progress: nil, &block)
|
|
293
|
+
# Directories first, so files have somewhere to go; links last, so
|
|
294
|
+
# nothing is written through one. Of several records under one name
|
|
295
|
+
# only the last is extracted, which is the one rubyzip would know.
|
|
296
|
+
dirs, rest = entries.reject { |e| @cdir.shadowed?(e) }.partition(&:directory?)
|
|
297
|
+
links, others = rest.partition(&:symlink?)
|
|
298
|
+
ordered = dirs + others + links
|
|
299
|
+
total = ordered.size
|
|
300
|
+
done = 0
|
|
301
|
+
report = ->(entry) { done += 1; progress&.call(entry, done, total) }
|
|
302
|
+
|
|
303
|
+
# With no overwrite decision to make (no block, and Ziperu.on_exists_proc
|
|
304
|
+
# left at its default of raising) the native core extracts every
|
|
305
|
+
# qualifying entry in one call; the rest take the per-entry path.
|
|
306
|
+
slow = if block.nil? && !::Ziperu.on_exists_proc && !@restore_ownership
|
|
307
|
+
extract_all_native(ordered, destination_directory, report)
|
|
308
|
+
else
|
|
309
|
+
ordered
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
slow.each do |entry|
|
|
313
|
+
if entry.symlink? && symlinks != :create
|
|
314
|
+
warn "WARNING: skipped symlink '#{entry.name}' during extract_all."
|
|
315
|
+
else
|
|
316
|
+
entry.extract(entry.name, destination_directory: destination_directory,
|
|
317
|
+
create_parent_directories: true,
|
|
318
|
+
symlinks: symlinks, &block)
|
|
319
|
+
end
|
|
320
|
+
report.call(entry)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
self
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Inflates every entry (without writing anything) so that the CRC of
|
|
327
|
+
# each is checked. Returns a Hash of the entries that failed to the
|
|
328
|
+
# exception raised for them; empty when the archive is sound. Encrypted
|
|
329
|
+
# entries are checked when a `decrypter` is given and skipped otherwise.
|
|
330
|
+
def verify(decrypter: nil)
|
|
331
|
+
native = native_archive
|
|
332
|
+
password = decrypter&.password&.b
|
|
333
|
+
failures = {}
|
|
334
|
+
entries.each do |entry|
|
|
335
|
+
next if entry.directory? || !entry.archive.equal?(native)
|
|
336
|
+
next if entry.encrypted? && password.nil?
|
|
337
|
+
|
|
338
|
+
begin
|
|
339
|
+
if entry.index
|
|
340
|
+
native.verify_entry(entry.index, password)
|
|
341
|
+
else
|
|
342
|
+
native.verify_entry_at(entry.local_header_offset, entry.size, entry.compressed_size,
|
|
343
|
+
entry.crc, password)
|
|
344
|
+
end
|
|
345
|
+
rescue Error, IOError => e
|
|
346
|
+
failures[entry] = e
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
failures
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Commits changes made since the previous commit to the zip archive: a
|
|
353
|
+
# new archive is written beside the old one and renamed over it.
|
|
354
|
+
def commit
|
|
355
|
+
return if name.kind_of?(StringIO) || !commit_required?
|
|
356
|
+
|
|
357
|
+
on_success_replace do |tmp_file|
|
|
358
|
+
::Ziperu::OutputStream.open(tmp_file, suppress_extra_fields: @suppress_extra_fields) do |zos|
|
|
359
|
+
@cdir.each do |e|
|
|
360
|
+
e.write_to_zip_output_stream(zos) unless @cdir.shadowed?(e)
|
|
361
|
+
e.clean_up
|
|
362
|
+
end
|
|
363
|
+
zos.comment = comment
|
|
364
|
+
end
|
|
365
|
+
true
|
|
366
|
+
end
|
|
367
|
+
initialize_cdir(@name)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Writes the archive, with any changes, to `io` and returns it.
|
|
371
|
+
def write_buffer(io = ::StringIO.new)
|
|
372
|
+
return io unless commit_required?
|
|
373
|
+
|
|
374
|
+
::Ziperu::OutputStream.write_buffer(io, suppress_extra_fields: @suppress_extra_fields) do |zos|
|
|
375
|
+
@cdir.each { |e| e.write_to_zip_output_stream(zos) unless @cdir.shadowed?(e) }
|
|
376
|
+
zos.comment = comment
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Closes the zip file committing any changes that has been made.
|
|
381
|
+
def close
|
|
382
|
+
commit
|
|
383
|
+
# A buffer stays readable after close (it is the caller's IO); a file
|
|
384
|
+
# handle is released and reopened on demand by `native_archive`.
|
|
385
|
+
release_native unless @buffer
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# The open native archive behind this file, reopening it after `close`
|
|
389
|
+
# so entries can still be read, as rubyzip allows.
|
|
390
|
+
#
|
|
391
|
+
# @api private
|
|
392
|
+
def native_archive
|
|
393
|
+
@native = InputStream.open_archive(@name) if @native.nil? || @native.closed?
|
|
394
|
+
@native
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Returns true if any changes has been made to this archive since the
|
|
398
|
+
# previous commit
|
|
399
|
+
def commit_required?
|
|
400
|
+
return true if @create || @dirty
|
|
401
|
+
|
|
402
|
+
@cdir.each do |e|
|
|
403
|
+
return true if e.dirty?
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
false
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Searches for entry with the specified name. Returns nil if no entry is
|
|
410
|
+
# found. See also get_entry
|
|
411
|
+
def find_entry(entry_name)
|
|
412
|
+
selected_entry = @cdir.find_entry(entry_name)
|
|
413
|
+
return if selected_entry.nil?
|
|
414
|
+
|
|
415
|
+
selected_entry.restore_ownership = @restore_ownership
|
|
416
|
+
selected_entry.restore_permissions = @restore_permissions
|
|
417
|
+
selected_entry.restore_times = @restore_times
|
|
418
|
+
selected_entry
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Searches for an entry just as find_entry, but throws Errno::ENOENT
|
|
422
|
+
# if no entry is found.
|
|
423
|
+
def get_entry(entry)
|
|
424
|
+
selected_entry = find_entry(entry)
|
|
425
|
+
raise Errno::ENOENT, entry if selected_entry.nil?
|
|
426
|
+
|
|
427
|
+
selected_entry
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# Creates a directory
|
|
431
|
+
def mkdir(entry_name, permission = 0o755)
|
|
432
|
+
raise Errno::EEXIST, "File exists - #{entry_name}" if find_entry(entry_name)
|
|
433
|
+
|
|
434
|
+
entry_name = entry_name.dup.to_s
|
|
435
|
+
entry_name << '/' unless entry_name.end_with?('/')
|
|
436
|
+
new_entry = ::Ziperu::StreamableDirectory.new(@name, entry_name, nil, permission)
|
|
437
|
+
@cdir << new_entry
|
|
438
|
+
@dirty = true
|
|
439
|
+
new_entry
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
private
|
|
443
|
+
|
|
444
|
+
# Entries the native core cannot take are returned for the Ruby loop:
|
|
445
|
+
# symlinks (skipped there with a warning), encrypted entries, entries
|
|
446
|
+
# not read from this archive, and names it cannot represent.
|
|
447
|
+
def extract_all_native(ordered, destination_directory, report)
|
|
448
|
+
native = native_archive
|
|
449
|
+
dest = ::File.absolute_path(destination_directory || '.')
|
|
450
|
+
slow = []
|
|
451
|
+
specs = []
|
|
452
|
+
by_index = {}
|
|
453
|
+
ordered.each do |entry|
|
|
454
|
+
if entry.symlink? || entry.encrypted? || !native_extractable?(entry, native)
|
|
455
|
+
slow << entry
|
|
456
|
+
else
|
|
457
|
+
specs << native_extract_spec(entry)
|
|
458
|
+
by_index[entry.index] = entry
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# A few hundred entries per call, so an interrupt is noticed between
|
|
463
|
+
# calls rather than after the whole archive.
|
|
464
|
+
specs.each_slice(NATIVE_EXTRACT_BATCH) do |batch|
|
|
465
|
+
native.extract_entries(dest, batch, ::Ziperu.validate_entry_sizes).each do |index, kind, path|
|
|
466
|
+
entry = by_index[index]
|
|
467
|
+
case kind
|
|
468
|
+
when :unsafe
|
|
469
|
+
warn "WARNING: skipped extracting '#{entry.name}' to '#{path}' as unsafe."
|
|
470
|
+
when :oversize
|
|
471
|
+
warn "WARNING: #{EntrySizeError.new(entry).message}"
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
batch.each { |spec| report.call(spec[0]) }
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
slow
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
NATIVE_EXTRACT_BATCH = 256 # :nodoc:
|
|
481
|
+
|
|
482
|
+
def native_extractable?(entry, native)
|
|
483
|
+
return false unless entry.instance_of?(::Ziperu::Entry) && entry.index && entry.archive.equal?(native)
|
|
484
|
+
return false unless entry.file? || entry.directory?
|
|
485
|
+
|
|
486
|
+
name = entry.name
|
|
487
|
+
name.ascii_only? || (name.encoding == ::Encoding::UTF_8 && name.valid_encoding?)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# What `Entry#extract` would do for the entry, as plain values:
|
|
491
|
+
# [entry, index, name, mtime seconds, mtime nanoseconds, unix mode, directory?].
|
|
492
|
+
def native_extract_spec(entry)
|
|
493
|
+
mtime = entry.time if @restore_times
|
|
494
|
+
mode = entry.unix_perms & 0o1777 if @restore_permissions && entry.fstype == FSTYPE_UNIX && entry.unix_perms
|
|
495
|
+
[entry, entry.index, entry.name, mtime&.to_i, mtime&.nsec, mode, entry.directory?]
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def add_recursive_dir(entry_prefix, src_dir, depth, max_depth, continue_on_exists_proc, report)
|
|
499
|
+
::Dir.children(src_dir).sort.each do |child|
|
|
500
|
+
src_path = ::File.join(src_dir, child)
|
|
501
|
+
|
|
502
|
+
if ::File.symlink?(src_path)
|
|
503
|
+
warn "WARNING: skipped symlink '#{src_path}' while adding directory contents."
|
|
504
|
+
elsif ::File.directory?(src_path)
|
|
505
|
+
entry_name = "#{entry_prefix}#{child}/"
|
|
506
|
+
report.call(add(entry_name, src_path, &continue_on_exists_proc))
|
|
507
|
+
|
|
508
|
+
if depth < max_depth
|
|
509
|
+
add_recursive_dir(entry_name, src_path, depth + 1, max_depth, continue_on_exists_proc, report)
|
|
510
|
+
else
|
|
511
|
+
warn "WARNING: max_depth (#{max_depth}) reached, not descending into '#{src_path}'."
|
|
512
|
+
end
|
|
513
|
+
elsif ::File.file?(src_path)
|
|
514
|
+
report.call(add("#{entry_prefix}#{child}", src_path, &continue_on_exists_proc))
|
|
515
|
+
else
|
|
516
|
+
warn "WARNING: skipped '#{src_path}' - not a regular file or directory."
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
# (Re)reads the central directory, or starts an empty one for a new
|
|
522
|
+
# archive.
|
|
523
|
+
def initialize_cdir(path_or_io, buffer: false)
|
|
524
|
+
release_native
|
|
525
|
+
@buffer = buffer
|
|
526
|
+
@cdir = ::Ziperu::EntrySet.new
|
|
527
|
+
@comment = ''
|
|
528
|
+
@dirty = false
|
|
529
|
+
|
|
530
|
+
if ::File.size?(@name.to_s)
|
|
531
|
+
# There is a file, which exists, that is associated with this zip.
|
|
532
|
+
@create = false
|
|
533
|
+
@file_permissions = ::File.stat(@name).mode
|
|
534
|
+
read_archive(buffer ? path_or_io : @name)
|
|
535
|
+
elsif buffer && path_or_io.size > 0
|
|
536
|
+
# This zip is probably a non-empty StringIO.
|
|
537
|
+
@create = false
|
|
538
|
+
read_archive(path_or_io)
|
|
539
|
+
elsif buffer && !@create
|
|
540
|
+
raise Error, 'The buffer is empty. Did you mean to pass the create flag?'
|
|
541
|
+
elsif !@create && ::File.empty?(@name)
|
|
542
|
+
raise Error, "File #{@name} has zero size. Did you mean to pass the create flag?"
|
|
543
|
+
elsif !@create
|
|
544
|
+
raise Error, "File #{@name} not found"
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def read_archive(path_or_io)
|
|
549
|
+
path_or_io.binmode if path_or_io.respond_to?(:binmode)
|
|
550
|
+
@native = InputStream.open_archive(path_or_io)
|
|
551
|
+
@comment = @native.comment # the stored bytes, as rubyzip reads them
|
|
552
|
+
@native.entries.each { |meta| @cdir.push_record(Entry.from_meta(@native, meta, self)) }
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def release_native
|
|
556
|
+
@native&.close
|
|
557
|
+
@native = nil
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
# Windows refuses to replace a file that is still open. A stream the
|
|
561
|
+
# caller forgot to close holds its own handle until the GC frees it, so
|
|
562
|
+
# on failure collect garbage once and try again.
|
|
563
|
+
def rename_over_original(tmp_filename)
|
|
564
|
+
::File.rename(tmp_filename, name)
|
|
565
|
+
rescue Errno::EACCES
|
|
566
|
+
raise unless ::Ziperu::RUNNING_ON_WINDOWS
|
|
567
|
+
|
|
568
|
+
GC.start
|
|
569
|
+
begin
|
|
570
|
+
::File.rename(tmp_filename, name)
|
|
571
|
+
rescue Errno::EACCES
|
|
572
|
+
# Something else (another Ziperu::File, say) still has the archive
|
|
573
|
+
# open. Renaming an open file is allowed where replacing one is
|
|
574
|
+
# not, so move the old archive aside first; that handle keeps
|
|
575
|
+
# reading the old bytes, as it would on Unix.
|
|
576
|
+
aside = "#{name}.#{Process.pid}.#{rand(1 << 32).to_s(36)}.old"
|
|
577
|
+
::File.rename(name, aside)
|
|
578
|
+
::File.rename(tmp_filename, name)
|
|
579
|
+
begin
|
|
580
|
+
::File.delete(aside)
|
|
581
|
+
rescue SystemCallError
|
|
582
|
+
warn "WARNING: could not remove '#{aside}' (still open elsewhere); delete it once it is closed."
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def check_entry_exists(entry_name, continue_on_exists_proc, proc_name)
|
|
588
|
+
return unless @cdir.include?(entry_name)
|
|
589
|
+
|
|
590
|
+
continue_on_exists_proc ||= proc { Ziperu.continue_on_exists_proc }
|
|
591
|
+
raise ::Ziperu::EntryExistsError.new proc_name, entry_name unless continue_on_exists_proc.call
|
|
592
|
+
|
|
593
|
+
remove get_entry(entry_name)
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def check_file(path)
|
|
597
|
+
raise Errno::ENOENT, path unless ::File.readable?(path)
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def on_success_replace
|
|
601
|
+
dirname, basename = ::File.split(name)
|
|
602
|
+
::Dir::Tmpname.create(basename, dirname) do |tmp_filename|
|
|
603
|
+
if yield tmp_filename
|
|
604
|
+
# The native handle must be gone before the rename on Windows.
|
|
605
|
+
release_native
|
|
606
|
+
rename_over_original(tmp_filename)
|
|
607
|
+
::File.chmod(@file_permissions, name) unless @create
|
|
608
|
+
end
|
|
609
|
+
ensure
|
|
610
|
+
FileUtils.rm_f(tmp_filename)
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ported verbatim from rubyzip (Copyright (C) 2002-2026 the rubyzip authors).
|
|
4
|
+
|
|
5
|
+
module Ziperu
|
|
6
|
+
module FileSplit # :nodoc:
|
|
7
|
+
MAX_SEGMENT_SIZE = 3_221_225_472
|
|
8
|
+
MIN_SEGMENT_SIZE = 65_536
|
|
9
|
+
DATA_BUFFER_SIZE = 8192
|
|
10
|
+
|
|
11
|
+
def get_segment_size_for_split(segment_size)
|
|
12
|
+
segment_size.clamp(MIN_SEGMENT_SIZE, MAX_SEGMENT_SIZE)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def get_partial_zip_file_name(zip_file_name, partial_zip_file_name)
|
|
16
|
+
unless partial_zip_file_name.nil?
|
|
17
|
+
partial_zip_file_name = zip_file_name.sub(
|
|
18
|
+
/#{::File.basename(zip_file_name)}\z/,
|
|
19
|
+
partial_zip_file_name + ::File.extname(zip_file_name)
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
partial_zip_file_name ||= zip_file_name
|
|
23
|
+
partial_zip_file_name
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def get_segment_count_for_split(zip_file_size, segment_size)
|
|
27
|
+
(zip_file_size / segment_size).to_i +
|
|
28
|
+
((zip_file_size % segment_size).zero? ? 0 : 1)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def put_split_signature(szip_file, segment_size)
|
|
32
|
+
signature_packed = [SPLIT_FILE_SIGNATURE].pack('V')
|
|
33
|
+
szip_file << signature_packed
|
|
34
|
+
segment_size - signature_packed.size
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
#
|
|
38
|
+
# TODO: Make the code more understandable
|
|
39
|
+
#
|
|
40
|
+
def save_splited_part(
|
|
41
|
+
zip_file, partial_zip_file_name, zip_file_size,
|
|
42
|
+
szip_file_index, segment_size, segment_count
|
|
43
|
+
)
|
|
44
|
+
ssegment_size = zip_file_size - zip_file.pos
|
|
45
|
+
ssegment_size = segment_size if ssegment_size > segment_size
|
|
46
|
+
szip_file_name = "#{partial_zip_file_name}.#{format('%03d', szip_file_index)}"
|
|
47
|
+
::File.open(szip_file_name, 'wb') do |szip_file|
|
|
48
|
+
if szip_file_index == 1
|
|
49
|
+
ssegment_size = put_split_signature(szip_file, segment_size)
|
|
50
|
+
end
|
|
51
|
+
chunk_bytes = 0
|
|
52
|
+
until ssegment_size == chunk_bytes || zip_file.eof?
|
|
53
|
+
segment_bytes_left = ssegment_size - chunk_bytes
|
|
54
|
+
buffer_size = [segment_bytes_left, DATA_BUFFER_SIZE].min
|
|
55
|
+
chunk = zip_file.read(buffer_size)
|
|
56
|
+
chunk_bytes += buffer_size
|
|
57
|
+
szip_file << chunk
|
|
58
|
+
# Info for track splitting
|
|
59
|
+
yield segment_count, szip_file_index, chunk_bytes, ssegment_size if block_given?
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Splits an archive into parts with segment size
|
|
65
|
+
def split(
|
|
66
|
+
zip_file_name, segment_size: MAX_SEGMENT_SIZE,
|
|
67
|
+
delete_original: true, partial_zip_file_name: nil
|
|
68
|
+
)
|
|
69
|
+
raise Error, "File #{zip_file_name} not found" unless ::File.exist?(zip_file_name)
|
|
70
|
+
raise Errno::ENOENT, zip_file_name unless ::File.readable?(zip_file_name)
|
|
71
|
+
|
|
72
|
+
zip_file_size = ::File.size(zip_file_name)
|
|
73
|
+
segment_size = get_segment_size_for_split(segment_size)
|
|
74
|
+
return if zip_file_size <= segment_size
|
|
75
|
+
|
|
76
|
+
segment_count = get_segment_count_for_split(zip_file_size, segment_size)
|
|
77
|
+
::Ziperu::File.open(zip_file_name) {} # Check for correct zip structure.
|
|
78
|
+
partial_zip_file_name = get_partial_zip_file_name(zip_file_name, partial_zip_file_name)
|
|
79
|
+
szip_file_index = 0
|
|
80
|
+
::File.open(zip_file_name, 'rb') do |zip_file|
|
|
81
|
+
until zip_file.eof?
|
|
82
|
+
szip_file_index += 1
|
|
83
|
+
save_splited_part(
|
|
84
|
+
zip_file, partial_zip_file_name, zip_file_size,
|
|
85
|
+
szip_file_index, segment_size, segment_count
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
::File.delete(zip_file_name) if delete_original
|
|
90
|
+
szip_file_index
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|