rubyzip 2.3.2 → 3.2.2

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