rubyzip 2.3.2 → 3.0.1

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 +429 -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 +364 -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 +178 -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 +19 -3
  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 -49
  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,110 @@
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
+ include Enumerable
53
+ extend Forwardable
54
+ extend FileSplit
59
55
 
56
+ IO_METHODS = [:tell, :seek, :read, :eof, :close].freeze # :nodoc:
57
+
58
+ # The name of this zip archive.
60
59
  attr_reader :name
61
60
 
62
61
  # default -> false.
63
62
  attr_accessor :restore_ownership
64
63
 
65
- # default -> false, but will be set to true in a future version.
64
+ # default -> true.
66
65
  attr_accessor :restore_permissions
67
66
 
68
- # default -> false, but will be set to true in a future version.
67
+ # default -> true.
69
68
  attr_accessor :restore_times
70
69
 
71
- # Returns the zip files comment, if it has one
72
- attr_accessor :comment
70
+ def_delegators :@cdir, :comment, :comment=, :each, :entries, :glob, :size
73
71
 
74
- # Opens a zip archive. Pass true as the second parameter to create
72
+ # Opens a zip archive. Pass create: true to create
75
73
  # a new archive if it doesn't exist already.
76
- def initialize(path_or_io, create = false, buffer = false, options = {})
74
+ def initialize(path_or_io, create: false, buffer: false,
75
+ restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
76
+ restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
77
+ restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
78
+ compression_level: ::Zip.default_compression)
77
79
  super()
78
- options = DEFAULT_OPTIONS.merge(options)
80
+
79
81
  @name = path_or_io.respond_to?(:path) ? path_or_io.path : path_or_io
80
- @comment = ''
81
82
  @create = create ? true : false # allow any truthy value to mean true
82
83
 
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
84
+ initialize_cdir(path_or_io, buffer: buffer)
108
85
 
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]
86
+ @restore_ownership = restore_ownership
87
+ @restore_permissions = restore_permissions
88
+ @restore_times = restore_times
89
+ @compression_level = compression_level
114
90
  end
115
91
 
116
92
  class << self
117
93
  # Similar to ::new. If a block is passed the Zip::File object is passed
118
94
  # to the block and is automatically closed afterwards, just as with
119
95
  # ruby's builtin File::open method.
120
- def open(file_name, create = false, options = {})
121
- zf = ::Zip::File.new(file_name, create, false, options)
96
+ def open(file_name, create: false,
97
+ restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
98
+ restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
99
+ restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
100
+ compression_level: ::Zip.default_compression)
101
+
102
+ zf = ::Zip::File.new(file_name, create: create,
103
+ restore_ownership: restore_ownership,
104
+ restore_permissions: restore_permissions,
105
+ restore_times: restore_times,
106
+ compression_level: compression_level)
107
+
122
108
  return zf unless block_given?
123
109
 
124
110
  begin
@@ -128,29 +114,29 @@ module Zip
128
114
  end
129
115
  end
130
116
 
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
117
  # Like #open, but reads zip archive contents from a String or open IO
140
118
  # stream, and outputs data to a buffer.
141
119
  # (This can be used to extract data from a
142
120
  # downloaded zip archive without first saving it to disk.)
143
- def open_buffer(io, options = {})
121
+ def open_buffer(io = ::StringIO.new, create: false,
122
+ restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
123
+ restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
124
+ restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
125
+ compression_level: ::Zip.default_compression)
126
+
144
127
  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}"
128
+ raise 'Zip::File.open_buffer expects a String or IO-like argument' \
129
+ "(responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
146
130
  end
147
131
 
148
132
  io = ::StringIO.new(io) if io.kind_of?(::String)
149
133
 
150
- # https://github.com/rubyzip/rubyzip/issues/119
151
- io.binmode if io.respond_to?(:binmode)
134
+ zf = ::Zip::File.new(io, create: create, buffer: true,
135
+ restore_ownership: restore_ownership,
136
+ restore_permissions: restore_permissions,
137
+ restore_times: restore_times,
138
+ compression_level: compression_level)
152
139
 
153
- zf = ::Zip::File.new(io, true, true, options)
154
140
  return zf unless block_given?
155
141
 
156
142
  yield zf
@@ -174,81 +160,18 @@ module Zip
174
160
  end
175
161
  end
176
162
 
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
163
+ # Count the entries in a zip archive without reading the whole set of
164
+ # entry data into memory.
165
+ def count_entries(path_or_io)
166
+ cdir = ::Zip::CentralDirectory.new
205
167
 
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)
168
+ if path_or_io.kind_of?(String)
169
+ ::File.open(path_or_io, 'rb') do |f|
170
+ cdir.count_entries(f)
248
171
  end
172
+ else
173
+ cdir.count_entries(path_or_io)
249
174
  end
250
- ::File.delete(zip_file_name) if delete_zip_file
251
- szip_file_index
252
175
  end
253
176
  end
254
177
 
@@ -264,24 +187,29 @@ module Zip
264
187
  # specified. If a block is passed the stream object is passed to the block and
265
188
  # the stream is automatically closed afterwards just as with ruby's builtin
266
189
  # 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)
190
+ def get_output_stream(entry, permissions: nil, comment: nil,
191
+ extra: nil, compressed_size: nil, crc: nil,
192
+ compression_method: nil, compression_level: nil,
193
+ size: nil, time: nil, &a_proc)
271
194
 
272
195
  new_entry =
273
196
  if entry.kind_of?(Entry)
274
197
  entry
275
198
  else
276
- Entry.new(@name, entry.to_s, comment, extra, compressed_size, crc, compression_method, size, time)
199
+ Entry.new(
200
+ @name, entry.to_s, comment: comment, extra: extra,
201
+ compressed_size: compressed_size, crc: crc, size: size,
202
+ compression_method: compression_method,
203
+ compression_level: compression_level, time: time
204
+ )
277
205
  end
278
206
  if new_entry.directory?
279
207
  raise ArgumentError,
280
208
  "cannot open stream to directory entry - '#{new_entry}'"
281
209
  end
282
- new_entry.unix_perms = permission_int
210
+ new_entry.unix_perms = permissions
283
211
  zip_streamable_entry = StreamableStream.new(new_entry)
284
- @entry_set << zip_streamable_entry
212
+ @cdir << zip_streamable_entry
285
213
  zip_streamable_entry.get_output_stream(&a_proc)
286
214
  end
287
215
 
@@ -299,31 +227,39 @@ module Zip
299
227
  def add(entry, src_path, &continue_on_exists_proc)
300
228
  continue_on_exists_proc ||= proc { ::Zip.continue_on_exists_proc }
301
229
  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)
230
+ new_entry = if entry.kind_of?(::Zip::Entry)
231
+ entry
232
+ else
233
+ ::Zip::Entry.new(
234
+ @name, entry.to_s,
235
+ compression_level: @compression_level
236
+ )
237
+ end
303
238
  new_entry.gather_fileinfo_from_srcpath(src_path)
304
- new_entry.dirty = true
305
- @entry_set << new_entry
239
+ @cdir << new_entry
306
240
  end
307
241
 
308
242
  # Convenience method for adding the contents of a file to the archive
309
243
  # in Stored format (uncompressed)
310
244
  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)
245
+ entry = ::Zip::Entry.new(
246
+ @name, entry.to_s, compression_method: ::Zip::Entry::STORED
247
+ )
312
248
  add(entry, src_path, &continue_on_exists_proc)
313
249
  end
314
250
 
315
251
  # Removes the specified entry.
316
252
  def remove(entry)
317
- @entry_set.delete(get_entry(entry))
253
+ @cdir.delete(get_entry(entry))
318
254
  end
319
255
 
320
256
  # Renames the specified entry.
321
257
  def rename(entry, new_name, &continue_on_exists_proc)
322
258
  found_entry = get_entry(entry)
323
259
  check_entry_exists(new_name, continue_on_exists_proc, 'rename')
324
- @entry_set.delete(found_entry)
260
+ @cdir.delete(found_entry)
325
261
  found_entry.name = new_name
326
- @entry_set << found_entry
262
+ @cdir << found_entry
327
263
  end
328
264
 
329
265
  # Replaces the specified entry with the contents of src_path (from
@@ -334,11 +270,16 @@ module Zip
334
270
  add(entry, src_path)
335
271
  end
336
272
 
337
- # Extracts entry to file dest_path.
338
- def extract(entry, dest_path, &block)
273
+ # Extracts `entry` to a file at `entry_path`, with `destination_directory`
274
+ # as the base location in the filesystem.
275
+ #
276
+ # NB: The caller is responsible for making sure `destination_directory` is
277
+ # safe, if it is passed.
278
+ def extract(entry, entry_path = nil, destination_directory: '.', &block)
339
279
  block ||= proc { ::Zip.on_exists_proc }
340
280
  found_entry = get_entry(entry)
341
- found_entry.extract(dest_path, &block)
281
+ entry_path ||= found_entry.name
282
+ found_entry.extract(entry_path, destination_directory: destination_directory, &block)
342
283
  end
343
284
 
344
285
  # Commits changes that has been made since the previous commit to
@@ -348,22 +289,23 @@ module Zip
348
289
 
349
290
  on_success_replace do |tmp_file|
350
291
  ::Zip::OutputStream.open(tmp_file) do |zos|
351
- @entry_set.each do |e|
292
+ @cdir.each do |e|
352
293
  e.write_to_zip_output_stream(zos)
353
- e.dirty = false
354
294
  e.clean_up
355
295
  end
356
296
  zos.comment = comment
357
297
  end
358
298
  true
359
299
  end
360
- initialize(name)
300
+ initialize_cdir(@name)
361
301
  end
362
302
 
363
303
  # Write buffer write changes to buffer and return
364
- def write_buffer(io = ::StringIO.new(''))
304
+ def write_buffer(io = ::StringIO.new)
305
+ return io unless commit_required?
306
+
365
307
  ::Zip::OutputStream.write_buffer(io) do |zos|
366
- @entry_set.each { |e| e.write_to_zip_output_stream(zos) }
308
+ @cdir.each { |e| e.write_to_zip_output_stream(zos) }
367
309
  zos.comment = comment
368
310
  end
369
311
  end
@@ -376,16 +318,19 @@ module Zip
376
318
  # Returns true if any changes has been made to this archive since
377
319
  # the previous commit
378
320
  def commit_required?
379
- @entry_set.each do |e|
380
- return true if e.dirty
321
+ return true if @create || @cdir.dirty?
322
+
323
+ @cdir.each do |e|
324
+ return true if e.dirty?
381
325
  end
382
- @comment != @stored_comment || @entry_set != @stored_entries || @create
326
+
327
+ false
383
328
  end
384
329
 
385
330
  # Searches for entry with the specified name. Returns nil if
386
331
  # no entry is found. See also get_entry
387
332
  def find_entry(entry_name)
388
- selected_entry = @entry_set.find_entry(entry_name)
333
+ selected_entry = @cdir.find_entry(entry_name)
389
334
  return if selected_entry.nil?
390
335
 
391
336
  selected_entry.restore_ownership = @restore_ownership
@@ -394,11 +339,6 @@ module Zip
394
339
  selected_entry
395
340
  end
396
341
 
397
- # Searches for entries given a glob
398
- def glob(*args, &block)
399
- @entry_set.glob(*args, &block)
400
- end
401
-
402
342
  # Searches for an entry just as find_entry, but throws Errno::ENOENT
403
343
  # if no entry is found.
404
344
  def get_entry(entry)
@@ -414,33 +354,50 @@ module Zip
414
354
 
415
355
  entry_name = entry_name.dup.to_s
416
356
  entry_name << '/' unless entry_name.end_with?('/')
417
- @entry_set << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
357
+ @cdir << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
418
358
  end
419
359
 
420
360
  private
421
361
 
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 += '/'
362
+ def initialize_cdir(path_or_io, buffer: false)
363
+ @cdir = ::Zip::CentralDirectory.new
364
+
365
+ if ::File.size?(@name.to_s)
366
+ # There is a file, which exists, that is associated with this zip.
367
+ @create = false
368
+ @file_permissions = ::File.stat(@name).mode
369
+
370
+ if buffer
371
+ # https://github.com/rubyzip/rubyzip/issues/119
372
+ path_or_io.binmode if path_or_io.respond_to?(:binmode)
373
+ @cdir.read_from_stream(path_or_io)
374
+ else
375
+ ::File.open(@name, 'rb') do |f|
376
+ @cdir.read_from_stream(f)
377
+ end
378
+ end
379
+ elsif buffer && path_or_io.size > 0
380
+ # This zip is probably a non-empty StringIO.
381
+ @create = false
382
+ @cdir.read_from_stream(path_or_io)
383
+ elsif !@create && ::File.empty?(@name)
384
+ # A file exists, but it is empty, and we've said we're
385
+ # NOT creating a new zip.
386
+ raise Error, "File #{@name} has zero size. Did you mean to pass the create flag?"
387
+ elsif !@create
388
+ # If we get here, and we're not creating a new zip, then
389
+ # everything is wrong.
390
+ raise Error, "File #{@name} not found"
430
391
  end
431
- new_entry.directory? && path_is_directory
432
392
  end
433
393
 
434
394
  def check_entry_exists(entry_name, continue_on_exists_proc, proc_name)
395
+ return unless @cdir.include?(entry_name)
396
+
435
397
  continue_on_exists_proc ||= proc { Zip.continue_on_exists_proc }
436
- return unless @entry_set.include?(entry_name)
398
+ raise ::Zip::EntryExistsError.new proc_name, entry_name unless continue_on_exists_proc.call
437
399
 
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
400
+ remove get_entry(entry_name)
444
401
  end
445
402
 
446
403
  def check_file(path)
@@ -450,14 +407,12 @@ module Zip
450
407
  def on_success_replace
451
408
  dirname, basename = ::File.split(name)
452
409
  ::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)
410
+ if yield tmp_filename
411
+ ::File.rename(tmp_filename, name)
412
+ ::File.chmod(@file_permissions, name) unless @create
460
413
  end
414
+ ensure
415
+ ::File.unlink(tmp_filename) if ::File.exist?(tmp_filename)
461
416
  end
462
417
  end
463
418
  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