rubyzip 2.3.1 → 3.0.0

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