ziperu 0.1.1-aarch64-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.
Files changed (105) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +24 -0
  3. data/LICENSE-rubyzip.txt +24 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +276 -0
  6. data/lib/rubyzip.rb +4 -0
  7. data/lib/zip/central_directory.rb +5 -0
  8. data/lib/zip/compressor.rb +5 -0
  9. data/lib/zip/constants.rb +5 -0
  10. data/lib/zip/crypto/aes_encryption.rb +5 -0
  11. data/lib/zip/crypto/decrypted_io.rb +5 -0
  12. data/lib/zip/crypto/encryption.rb +5 -0
  13. data/lib/zip/crypto/null_encryption.rb +5 -0
  14. data/lib/zip/crypto/traditional_encryption.rb +5 -0
  15. data/lib/zip/crypto.rb +5 -0
  16. data/lib/zip/decompressor.rb +5 -0
  17. data/lib/zip/deflater.rb +5 -0
  18. data/lib/zip/dirtyable.rb +5 -0
  19. data/lib/zip/dos_time.rb +5 -0
  20. data/lib/zip/entry.rb +5 -0
  21. data/lib/zip/entry_set.rb +5 -0
  22. data/lib/zip/errors.rb +5 -0
  23. data/lib/zip/extra_field/aes.rb +5 -0
  24. data/lib/zip/extra_field/generic.rb +5 -0
  25. data/lib/zip/extra_field/ntfs.rb +5 -0
  26. data/lib/zip/extra_field/old_unix.rb +5 -0
  27. data/lib/zip/extra_field/universal_time.rb +5 -0
  28. data/lib/zip/extra_field/unix.rb +5 -0
  29. data/lib/zip/extra_field/unknown.rb +5 -0
  30. data/lib/zip/extra_field/zip64.rb +5 -0
  31. data/lib/zip/extra_field.rb +5 -0
  32. data/lib/zip/file.rb +5 -0
  33. data/lib/zip/file_split.rb +5 -0
  34. data/lib/zip/filesystem/dir.rb +5 -0
  35. data/lib/zip/filesystem/directory_iterator.rb +5 -0
  36. data/lib/zip/filesystem/file.rb +5 -0
  37. data/lib/zip/filesystem/file_stat.rb +5 -0
  38. data/lib/zip/filesystem/zip_file_name_mapper.rb +5 -0
  39. data/lib/zip/filesystem.rb +5 -0
  40. data/lib/zip/inflater.rb +5 -0
  41. data/lib/zip/input_stream.rb +5 -0
  42. data/lib/zip/ioextras/abstract_input_stream.rb +5 -0
  43. data/lib/zip/ioextras/abstract_output_stream.rb +5 -0
  44. data/lib/zip/ioextras/fake_io.rb +5 -0
  45. data/lib/zip/ioextras.rb +5 -0
  46. data/lib/zip/null_compressor.rb +5 -0
  47. data/lib/zip/null_decompressor.rb +5 -0
  48. data/lib/zip/null_input_stream.rb +5 -0
  49. data/lib/zip/output_stream.rb +5 -0
  50. data/lib/zip/pass_thru_compressor.rb +5 -0
  51. data/lib/zip/pass_thru_decompressor.rb +5 -0
  52. data/lib/zip/streamable_directory.rb +5 -0
  53. data/lib/zip/streamable_stream.rb +5 -0
  54. data/lib/zip/version.rb +5 -0
  55. data/lib/zip.rb +19 -0
  56. data/lib/ziperu/3.1/ziperu.so +0 -0
  57. data/lib/ziperu/3.2/ziperu.so +0 -0
  58. data/lib/ziperu/3.3/ziperu.so +0 -0
  59. data/lib/ziperu/3.4/ziperu.so +0 -0
  60. data/lib/ziperu/4.0/ziperu.so +0 -0
  61. data/lib/ziperu/central_directory.rb +136 -0
  62. data/lib/ziperu/compressor.rb +14 -0
  63. data/lib/ziperu/constants.rb +148 -0
  64. data/lib/ziperu/crypto.rb +72 -0
  65. data/lib/ziperu/decompressor.rb +35 -0
  66. data/lib/ziperu/deflater.rb +41 -0
  67. data/lib/ziperu/dirtyable.rb +48 -0
  68. data/lib/ziperu/dos_time.rb +87 -0
  69. data/lib/ziperu/entry.rb +760 -0
  70. data/lib/ziperu/entry_set.rb +147 -0
  71. data/lib/ziperu/errors.rb +154 -0
  72. data/lib/ziperu/extra_field/aes.rb +52 -0
  73. data/lib/ziperu/extra_field/generic.rb +45 -0
  74. data/lib/ziperu/extra_field/ntfs.rb +98 -0
  75. data/lib/ziperu/extra_field/old_unix.rb +52 -0
  76. data/lib/ziperu/extra_field/universal_time.rb +81 -0
  77. data/lib/ziperu/extra_field/unix.rb +45 -0
  78. data/lib/ziperu/extra_field/unknown.rb +37 -0
  79. data/lib/ziperu/extra_field/zip64.rb +86 -0
  80. data/lib/ziperu/extra_field.rb +109 -0
  81. data/lib/ziperu/file.rb +614 -0
  82. data/lib/ziperu/file_split.rb +93 -0
  83. data/lib/ziperu/filesystem/dir.rb +88 -0
  84. data/lib/ziperu/filesystem/directory_iterator.rb +50 -0
  85. data/lib/ziperu/filesystem/file.rb +265 -0
  86. data/lib/ziperu/filesystem/file_stat.rb +112 -0
  87. data/lib/ziperu/filesystem/zip_file_name_mapper.rb +83 -0
  88. data/lib/ziperu/filesystem.rb +76 -0
  89. data/lib/ziperu/inflater.rb +60 -0
  90. data/lib/ziperu/input_stream.rb +296 -0
  91. data/lib/ziperu/ioextras/abstract_input_stream.rb +137 -0
  92. data/lib/ziperu/ioextras/abstract_output_stream.rb +57 -0
  93. data/lib/ziperu/ioextras/fake_io.rb +41 -0
  94. data/lib/ziperu/ioextras.rb +42 -0
  95. data/lib/ziperu/null_compressor.rb +20 -0
  96. data/lib/ziperu/null_decompressor.rb +25 -0
  97. data/lib/ziperu/null_input_stream.rb +55 -0
  98. data/lib/ziperu/output_stream.rb +223 -0
  99. data/lib/ziperu/pass_thru_compressor.rb +29 -0
  100. data/lib/ziperu/pass_thru_decompressor.rb +37 -0
  101. data/lib/ziperu/streamable_directory.rb +15 -0
  102. data/lib/ziperu/streamable_stream.rb +59 -0
  103. data/lib/ziperu/version.rb +7 -0
  104. data/lib/ziperu.rb +116 -0
  105. metadata +156 -0
@@ -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