rubyzip 2.4.1 → 3.0.0.rc1

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 +419 -0
  3. data/LICENSE.md +24 -0
  4. data/README.md +137 -37
  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 +43 -4
  17. data/lib/zip/entry.rb +333 -242
  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 +166 -264
  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 +7 -5
  37. data/lib/zip/input_stream.rb +50 -50
  38. data/lib/zip/ioextras/abstract_input_stream.rb +16 -11
  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 +55 -56
  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 +18 -22
  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 +87 -51
  60. data/TODO +0 -15
  61. data/lib/zip/extra_field/zip64_placeholder.rb +0 -15
data/lib/zip/file.rb CHANGED
@@ -1,132 +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, dep_create = false, dep_buffer = false,
77
- 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)
78
78
  super()
79
79
 
80
- Zip.warn_about_v3_api('File#new') if dep_create || dep_buffer
81
-
82
- options = DEFAULT_OPTIONS.merge(options)
83
80
  @name = path_or_io.respond_to?(:path) ? path_or_io.path : path_or_io
84
- @comment = ''
85
- @create = create || dep_create ? true : false # allow any truthy value to mean true
86
- buffer ||= dep_buffer
81
+ @create = create ? true : false # allow any truthy value to mean true
87
82
 
88
- if ::File.size?(@name.to_s)
89
- # There is a file, which exists, that is associated with this zip.
90
- @create = false
91
- @file_permissions = ::File.stat(@name).mode
83
+ initialize_cdir(path_or_io, buffer: buffer)
92
84
 
93
- if buffer
94
- read_from_stream(path_or_io)
95
- else
96
- ::File.open(@name, 'rb') do |f|
97
- read_from_stream(f)
98
- end
99
- end
100
- elsif buffer && path_or_io.size > 0
101
- # This zip is probably a non-empty StringIO.
102
- @create = false
103
- read_from_stream(path_or_io)
104
- elsif @create
105
- # This zip is completely new/empty and is to be created.
106
- @entry_set = EntrySet.new
107
- elsif ::File.zero?(@name)
108
- # A file exists, but it is empty.
109
- raise Error, "File #{@name} has zero size. Did you mean to pass the create flag?"
110
- else
111
- # Everything is wrong.
112
- raise Error, "File #{@name} not found"
113
- end
114
-
115
- @stored_entries = @entry_set.dup
116
- @stored_comment = @comment
117
- @restore_ownership = options[:restore_ownership]
118
- @restore_permissions = options[:restore_permissions]
119
- @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
120
89
  end
121
90
 
122
91
  class << self
123
92
  # Similar to ::new. If a block is passed the Zip::File object is passed
124
93
  # to the block and is automatically closed afterwards, just as with
125
94
  # ruby's builtin File::open method.
126
- def open(file_name, dep_create = false, create: false, **options)
127
- Zip.warn_about_v3_api('Zip::File.open') if dep_create
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)
128
106
 
129
- zf = ::Zip::File.new(file_name, create: (dep_create || create), buffer: false, **options)
130
107
  return zf unless block_given?
131
108
 
132
109
  begin
@@ -136,31 +113,29 @@ module Zip
136
113
  end
137
114
  end
138
115
 
139
- # Same as #open. But outputs data to a buffer instead of a file
140
- def add_buffer
141
- Zip.warn_about_v3_api('Zip::File.add_buffer')
142
-
143
- io = ::StringIO.new
144
- zf = ::Zip::File.new(io, true, true)
145
- yield zf
146
- zf.write_buffer(io)
147
- end
148
-
149
116
  # Like #open, but reads zip archive contents from a String or open IO
150
117
  # stream, and outputs data to a buffer.
151
118
  # (This can be used to extract data from a
152
119
  # downloaded zip archive without first saving it to disk.)
153
- 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
+
154
126
  unless IO_METHODS.map { |method| io.respond_to?(method) }.all? || io.kind_of?(String)
155
- 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}"
156
129
  end
157
130
 
158
131
  io = ::StringIO.new(io) if io.kind_of?(::String)
159
132
 
160
- # https://github.com/rubyzip/rubyzip/issues/119
161
- 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)
162
138
 
163
- zf = ::Zip::File.new(io, create: true, buffer: true, **options)
164
139
  return zf unless block_given?
165
140
 
166
141
  yield zf
@@ -184,88 +159,18 @@ module Zip
184
159
  end
185
160
  end
186
161
 
187
- def get_segment_size_for_split(segment_size)
188
- if MIN_SEGMENT_SIZE > segment_size
189
- MIN_SEGMENT_SIZE
190
- elsif MAX_SEGMENT_SIZE < segment_size
191
- MAX_SEGMENT_SIZE
192
- else
193
- segment_size
194
- end
195
- end
196
-
197
- def get_partial_zip_file_name(zip_file_name, partial_zip_file_name)
198
- unless partial_zip_file_name.nil?
199
- partial_zip_file_name = zip_file_name.sub(/#{::File.basename(zip_file_name)}\z/,
200
- partial_zip_file_name + ::File.extname(zip_file_name))
201
- end
202
- partial_zip_file_name ||= zip_file_name
203
- partial_zip_file_name
204
- end
205
-
206
- def get_segment_count_for_split(zip_file_size, segment_size)
207
- (zip_file_size / segment_size).to_i + (zip_file_size % segment_size == 0 ? 0 : 1)
208
- end
209
-
210
- def put_split_signature(szip_file, segment_size)
211
- signature_packed = [SPLIT_SIGNATURE].pack('V')
212
- szip_file << signature_packed
213
- segment_size - signature_packed.size
214
- end
215
-
216
- #
217
- # TODO: Make the code more understandable
218
- #
219
- def save_splited_part(zip_file, partial_zip_file_name, zip_file_size, szip_file_index, segment_size, segment_count)
220
- ssegment_size = zip_file_size - zip_file.pos
221
- ssegment_size = segment_size if ssegment_size > segment_size
222
- szip_file_name = "#{partial_zip_file_name}.#{format('%03d', szip_file_index)}"
223
- ::File.open(szip_file_name, 'wb') do |szip_file|
224
- if szip_file_index == 1
225
- ssegment_size = put_split_signature(szip_file, segment_size)
226
- end
227
- chunk_bytes = 0
228
- until ssegment_size == chunk_bytes || zip_file.eof?
229
- segment_bytes_left = ssegment_size - chunk_bytes
230
- buffer_size = segment_bytes_left < DATA_BUFFER_SIZE ? segment_bytes_left : DATA_BUFFER_SIZE
231
- chunk = zip_file.read(buffer_size)
232
- chunk_bytes += buffer_size
233
- szip_file << chunk
234
- # Info for track splitting
235
- yield segment_count, szip_file_index, chunk_bytes, ssegment_size if block_given?
236
- end
237
- end
238
- end
239
-
240
- # Splits an archive into parts with segment size
241
- def split(zip_file_name,
242
- dep_segment_size = MAX_SEGMENT_SIZE, dep_delete_zip_file = true, dep_partial_zip_file_name = nil,
243
- segment_size: MAX_SEGMENT_SIZE, delete_zip_file: nil, partial_zip_file_name: nil)
244
- raise Error, "File #{zip_file_name} not found" unless ::File.exist?(zip_file_name)
245
- raise Errno::ENOENT, zip_file_name unless ::File.readable?(zip_file_name)
246
-
247
- if dep_segment_size != MAX_SEGMENT_SIZE || !dep_delete_zip_file || dep_partial_zip_file_name
248
- Zip.warn_about_v3_api('Zip::File.split')
249
- 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
250
166
 
251
- zip_file_size = ::File.size(zip_file_name)
252
- segment_size = get_segment_size_for_split(segment_size || dep_segment_size)
253
- return if zip_file_size <= segment_size
254
-
255
- segment_count = get_segment_count_for_split(zip_file_size, segment_size)
256
- # Checking for correct zip structure
257
- ::Zip::File.open(zip_file_name) {}
258
- partial_zip_file_name = get_partial_zip_file_name(zip_file_name, (partial_zip_file_name || dep_partial_zip_file_name))
259
- szip_file_index = 0
260
- ::File.open(zip_file_name, 'rb') do |zip_file|
261
- until zip_file.eof?
262
- szip_file_index += 1
263
- 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)
264
170
  end
171
+ else
172
+ cdir.count_entries(path_or_io)
265
173
  end
266
- delete_zip_file = delete_zip_file.nil? ? dep_delete_zip_file : delete_zip_file
267
- ::File.delete(zip_file_name) if delete_zip_file
268
- szip_file_index
269
174
  end
270
175
  end
271
176
 
@@ -281,45 +186,31 @@ module Zip
281
186
  # specified. If a block is passed the stream object is passed to the block and
282
187
  # the stream is automatically closed afterwards just as with ruby's builtin
283
188
  # File.open method.
284
- # rubocop:disable Metrics/ParameterLists, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
285
- def get_output_stream(entry,
286
- dep_permission_int = nil, dep_comment = nil,
287
- dep_extra = nil, dep_compressed_size = nil, dep_crc = nil,
288
- dep_compression_method = nil, dep_size = nil, dep_time = nil,
289
- permission_int: nil, comment: nil,
189
+ def get_output_stream(entry, permissions: nil, comment: nil,
290
190
  extra: nil, compressed_size: nil, crc: nil,
291
- compression_method: nil, size: nil, time: nil,
292
- &a_proc)
293
-
294
- unless dep_permission_int.nil? && dep_comment.nil? && dep_extra.nil? &&
295
- dep_compressed_size.nil? && dep_crc.nil? && dep_compression_method.nil? &&
296
- dep_size.nil? && dep_time.nil?
297
- Zip.warn_about_v3_api('Zip::File#get_output_stream')
298
- end
191
+ compression_method: nil, compression_level: nil,
192
+ size: nil, time: nil, &a_proc)
299
193
 
300
194
  new_entry =
301
195
  if entry.kind_of?(Entry)
302
196
  entry
303
197
  else
304
- Entry.new(@name, entry.to_s,
305
- comment: (comment || dep_comment),
306
- extra: (extra || dep_extra),
307
- compressed_size: (compressed_size || dep_compressed_size),
308
- crc: (crc || dep_crc),
309
- compression_method: (compression_method || dep_compression_method),
310
- size: (size || dep_size),
311
- time: (time || dep_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
+ )
312
204
  end
313
205
  if new_entry.directory?
314
206
  raise ArgumentError,
315
207
  "cannot open stream to directory entry - '#{new_entry}'"
316
208
  end
317
- new_entry.unix_perms = (permission_int || dep_permission_int)
209
+ new_entry.unix_perms = permissions
318
210
  zip_streamable_entry = StreamableStream.new(new_entry)
319
- @entry_set << zip_streamable_entry
211
+ @cdir << zip_streamable_entry
320
212
  zip_streamable_entry.get_output_stream(&a_proc)
321
213
  end
322
- # rubocop:enable Metrics/ParameterLists, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
323
214
 
324
215
  # Returns the name of the zip archive
325
216
  def to_s
@@ -335,31 +226,39 @@ module Zip
335
226
  def add(entry, src_path, &continue_on_exists_proc)
336
227
  continue_on_exists_proc ||= proc { ::Zip.continue_on_exists_proc }
337
228
  check_entry_exists(entry, continue_on_exists_proc, 'add')
338
- 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
339
237
  new_entry.gather_fileinfo_from_srcpath(src_path)
340
- new_entry.dirty = true
341
- @entry_set << new_entry
238
+ @cdir << new_entry
342
239
  end
343
240
 
344
241
  # Convenience method for adding the contents of a file to the archive
345
242
  # in Stored format (uncompressed)
346
243
  def add_stored(entry, src_path, &continue_on_exists_proc)
347
- 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
+ )
348
247
  add(entry, src_path, &continue_on_exists_proc)
349
248
  end
350
249
 
351
250
  # Removes the specified entry.
352
251
  def remove(entry)
353
- @entry_set.delete(get_entry(entry))
252
+ @cdir.delete(get_entry(entry))
354
253
  end
355
254
 
356
255
  # Renames the specified entry.
357
256
  def rename(entry, new_name, &continue_on_exists_proc)
358
257
  found_entry = get_entry(entry)
359
258
  check_entry_exists(new_name, continue_on_exists_proc, 'rename')
360
- @entry_set.delete(found_entry)
259
+ @cdir.delete(found_entry)
361
260
  found_entry.name = new_name
362
- @entry_set << found_entry
261
+ @cdir << found_entry
363
262
  end
364
263
 
365
264
  # Replaces the specified entry with the contents of src_path (from
@@ -370,25 +269,16 @@ module Zip
370
269
  add(entry, src_path)
371
270
  end
372
271
 
373
- # Extracts entry to file dest_path.
374
- def extract(entry, dest_path, &block)
375
- Zip.warn_about_v3_api('Zip::File#extract')
376
-
377
- block ||= proc { ::Zip.on_exists_proc }
378
- found_entry = get_entry(entry)
379
- found_entry.extract(dest_path, &block)
380
- end
381
-
382
272
  # Extracts `entry` to a file at `entry_path`, with `destination_directory`
383
273
  # as the base location in the filesystem.
384
274
  #
385
275
  # NB: The caller is responsible for making sure `destination_directory` is
386
276
  # safe, if it is passed.
387
- def extract_v3(entry, entry_path = nil, destination_directory: '.', &block)
277
+ def extract(entry, entry_path = nil, destination_directory: '.', &block)
388
278
  block ||= proc { ::Zip.on_exists_proc }
389
279
  found_entry = get_entry(entry)
390
280
  entry_path ||= found_entry.name
391
- found_entry.extract_v3(entry_path, destination_directory: destination_directory, &block)
281
+ found_entry.extract(entry_path, destination_directory: destination_directory, &block)
392
282
  end
393
283
 
394
284
  # Commits changes that has been made since the previous commit to
@@ -398,16 +288,15 @@ module Zip
398
288
 
399
289
  on_success_replace do |tmp_file|
400
290
  ::Zip::OutputStream.open(tmp_file) do |zos|
401
- @entry_set.each do |e|
291
+ @cdir.each do |e|
402
292
  e.write_to_zip_output_stream(zos)
403
- e.dirty = false
404
293
  e.clean_up
405
294
  end
406
295
  zos.comment = comment
407
296
  end
408
297
  true
409
298
  end
410
- initialize(name)
299
+ initialize_cdir(@name)
411
300
  end
412
301
 
413
302
  # Write buffer write changes to buffer and return
@@ -415,7 +304,7 @@ module Zip
415
304
  return io unless commit_required?
416
305
 
417
306
  ::Zip::OutputStream.write_buffer(io) do |zos|
418
- @entry_set.each { |e| e.write_to_zip_output_stream(zos) }
307
+ @cdir.each { |e| e.write_to_zip_output_stream(zos) }
419
308
  zos.comment = comment
420
309
  end
421
310
  end
@@ -428,16 +317,19 @@ module Zip
428
317
  # Returns true if any changes has been made to this archive since
429
318
  # the previous commit
430
319
  def commit_required?
431
- @entry_set.each do |e|
432
- return true if e.dirty
320
+ return true if @create || @cdir.dirty?
321
+
322
+ @cdir.each do |e|
323
+ return true if e.dirty?
433
324
  end
434
- @comment != @stored_comment || @entry_set != @stored_entries || @create
325
+
326
+ false
435
327
  end
436
328
 
437
329
  # Searches for entry with the specified name. Returns nil if
438
330
  # no entry is found. See also get_entry
439
331
  def find_entry(entry_name)
440
- selected_entry = @entry_set.find_entry(entry_name)
332
+ selected_entry = @cdir.find_entry(entry_name)
441
333
  return if selected_entry.nil?
442
334
 
443
335
  selected_entry.restore_ownership = @restore_ownership
@@ -446,11 +338,6 @@ module Zip
446
338
  selected_entry
447
339
  end
448
340
 
449
- # Searches for entries given a glob
450
- def glob(*args, &block)
451
- @entry_set.glob(*args, &block)
452
- end
453
-
454
341
  # Searches for an entry just as find_entry, but throws Errno::ENOENT
455
342
  # if no entry is found.
456
343
  def get_entry(entry)
@@ -466,33 +353,50 @@ module Zip
466
353
 
467
354
  entry_name = entry_name.dup.to_s
468
355
  entry_name << '/' unless entry_name.end_with?('/')
469
- @entry_set << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
356
+ @cdir << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
470
357
  end
471
358
 
472
359
  private
473
360
 
474
- def directory?(new_entry, src_path)
475
- path_is_directory = ::File.directory?(src_path)
476
- if new_entry.directory? && !path_is_directory
477
- raise ArgumentError,
478
- "entry name '#{new_entry}' indicates directory entry, but " \
479
- "'#{src_path}' is not a directory"
480
- elsif !new_entry.directory? && path_is_directory
481
- 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"
482
390
  end
483
- new_entry.directory? && path_is_directory
484
391
  end
485
392
 
486
393
  def check_entry_exists(entry_name, continue_on_exists_proc, proc_name)
394
+ return unless @cdir.include?(entry_name)
395
+
487
396
  continue_on_exists_proc ||= proc { Zip.continue_on_exists_proc }
488
- return unless @entry_set.include?(entry_name)
397
+ raise ::Zip::EntryExistsError.new proc_name, entry_name unless continue_on_exists_proc.call
489
398
 
490
- if continue_on_exists_proc.call
491
- remove get_entry(entry_name)
492
- else
493
- raise ::Zip::EntryExistsError,
494
- proc_name + " failed. Entry #{entry_name} already exists"
495
- end
399
+ remove get_entry(entry_name)
496
400
  end
497
401
 
498
402
  def check_file(path)
@@ -502,14 +406,12 @@ module Zip
502
406
  def on_success_replace
503
407
  dirname, basename = ::File.split(name)
504
408
  ::Dir::Tmpname.create(basename, dirname) do |tmp_filename|
505
- begin
506
- if yield tmp_filename
507
- ::File.rename(tmp_filename, name)
508
- ::File.chmod(@file_permissions, name) unless @create
509
- end
510
- ensure
511
- ::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
512
412
  end
413
+ ensure
414
+ ::File.unlink(tmp_filename) if ::File.exist?(tmp_filename)
513
415
  end
514
416
  end
515
417
  end