rubyzip 2.4.1 → 3.0.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +436 -0
  3. data/LICENSE.md +24 -0
  4. data/README.md +126 -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 +12 -8
  15. data/lib/zip/dirtyable.rb +32 -0
  16. data/lib/zip/dos_time.rb +43 -4
  17. data/lib/zip/entry.rb +352 -249
  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 +167 -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 +51 -51
  38. data/lib/zip/ioextras/abstract_input_stream.rb +16 -11
  39. data/lib/zip/ioextras/abstract_output_stream.rb +13 -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 +23 -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 +85 -49
  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,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, dep_create = false, dep_buffer = false,
77
- 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)
78
79
  super()
79
80
 
80
- Zip.warn_about_v3_api('File#new') if dep_create || dep_buffer
81
-
82
- options = DEFAULT_OPTIONS.merge(options)
83
81
  @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
82
+ @create = create ? true : false # allow any truthy value to mean true
87
83
 
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
84
+ initialize_cdir(path_or_io, buffer: buffer)
92
85
 
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]
86
+ @restore_ownership = restore_ownership
87
+ @restore_permissions = restore_permissions
88
+ @restore_times = restore_times
89
+ @compression_level = compression_level
120
90
  end
121
91
 
122
92
  class << self
123
93
  # Similar to ::new. If a block is passed the Zip::File object is passed
124
94
  # to the block and is automatically closed afterwards, just as with
125
95
  # 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
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)
128
107
 
129
- zf = ::Zip::File.new(file_name, create: (dep_create || create), buffer: false, **options)
130
108
  return zf unless block_given?
131
109
 
132
110
  begin
@@ -136,31 +114,29 @@ module Zip
136
114
  end
137
115
  end
138
116
 
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
117
  # Like #open, but reads zip archive contents from a String or open IO
150
118
  # stream, and outputs data to a buffer.
151
119
  # (This can be used to extract data from a
152
120
  # downloaded zip archive without first saving it to disk.)
153
- 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
+
154
127
  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}"
128
+ raise 'Zip::File.open_buffer expects a String or IO-like argument' \
129
+ "(responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
156
130
  end
157
131
 
158
132
  io = ::StringIO.new(io) if io.kind_of?(::String)
159
133
 
160
- # https://github.com/rubyzip/rubyzip/issues/119
161
- 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)
162
139
 
163
- zf = ::Zip::File.new(io, create: true, buffer: true, **options)
164
140
  return zf unless block_given?
165
141
 
166
142
  yield zf
@@ -184,88 +160,18 @@ module Zip
184
160
  end
185
161
  end
186
162
 
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
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
250
167
 
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)
168
+ if path_or_io.kind_of?(String)
169
+ ::File.open(path_or_io, 'rb') do |f|
170
+ cdir.count_entries(f)
264
171
  end
172
+ else
173
+ cdir.count_entries(path_or_io)
265
174
  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
175
  end
270
176
  end
271
177
 
@@ -281,45 +187,31 @@ module Zip
281
187
  # specified. If a block is passed the stream object is passed to the block and
282
188
  # the stream is automatically closed afterwards just as with ruby's builtin
283
189
  # 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,
190
+ def get_output_stream(entry, permissions: nil, comment: nil,
290
191
  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
192
+ compression_method: nil, compression_level: nil,
193
+ size: nil, time: nil, &a_proc)
299
194
 
300
195
  new_entry =
301
196
  if entry.kind_of?(Entry)
302
197
  entry
303
198
  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))
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
+ )
312
205
  end
313
206
  if new_entry.directory?
314
207
  raise ArgumentError,
315
208
  "cannot open stream to directory entry - '#{new_entry}'"
316
209
  end
317
- new_entry.unix_perms = (permission_int || dep_permission_int)
210
+ new_entry.unix_perms = permissions
318
211
  zip_streamable_entry = StreamableStream.new(new_entry)
319
- @entry_set << zip_streamable_entry
212
+ @cdir << zip_streamable_entry
320
213
  zip_streamable_entry.get_output_stream(&a_proc)
321
214
  end
322
- # rubocop:enable Metrics/ParameterLists, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
323
215
 
324
216
  # Returns the name of the zip archive
325
217
  def to_s
@@ -335,31 +227,39 @@ module Zip
335
227
  def add(entry, src_path, &continue_on_exists_proc)
336
228
  continue_on_exists_proc ||= proc { ::Zip.continue_on_exists_proc }
337
229
  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)
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
339
238
  new_entry.gather_fileinfo_from_srcpath(src_path)
340
- new_entry.dirty = true
341
- @entry_set << new_entry
239
+ @cdir << new_entry
342
240
  end
343
241
 
344
242
  # Convenience method for adding the contents of a file to the archive
345
243
  # in Stored format (uncompressed)
346
244
  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)
245
+ entry = ::Zip::Entry.new(
246
+ @name, entry.to_s, compression_method: ::Zip::Entry::STORED
247
+ )
348
248
  add(entry, src_path, &continue_on_exists_proc)
349
249
  end
350
250
 
351
251
  # Removes the specified entry.
352
252
  def remove(entry)
353
- @entry_set.delete(get_entry(entry))
253
+ @cdir.delete(get_entry(entry))
354
254
  end
355
255
 
356
256
  # Renames the specified entry.
357
257
  def rename(entry, new_name, &continue_on_exists_proc)
358
258
  found_entry = get_entry(entry)
359
259
  check_entry_exists(new_name, continue_on_exists_proc, 'rename')
360
- @entry_set.delete(found_entry)
260
+ @cdir.delete(found_entry)
361
261
  found_entry.name = new_name
362
- @entry_set << found_entry
262
+ @cdir << found_entry
363
263
  end
364
264
 
365
265
  # Replaces the specified entry with the contents of src_path (from
@@ -370,25 +270,16 @@ module Zip
370
270
  add(entry, src_path)
371
271
  end
372
272
 
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
273
  # Extracts `entry` to a file at `entry_path`, with `destination_directory`
383
274
  # as the base location in the filesystem.
384
275
  #
385
276
  # NB: The caller is responsible for making sure `destination_directory` is
386
277
  # safe, if it is passed.
387
- def extract_v3(entry, entry_path = nil, destination_directory: '.', &block)
278
+ def extract(entry, entry_path = nil, destination_directory: '.', &block)
388
279
  block ||= proc { ::Zip.on_exists_proc }
389
280
  found_entry = get_entry(entry)
390
281
  entry_path ||= found_entry.name
391
- found_entry.extract_v3(entry_path, destination_directory: destination_directory, &block)
282
+ found_entry.extract(entry_path, destination_directory: destination_directory, &block)
392
283
  end
393
284
 
394
285
  # Commits changes that has been made since the previous commit to
@@ -398,16 +289,15 @@ module Zip
398
289
 
399
290
  on_success_replace do |tmp_file|
400
291
  ::Zip::OutputStream.open(tmp_file) do |zos|
401
- @entry_set.each do |e|
292
+ @cdir.each do |e|
402
293
  e.write_to_zip_output_stream(zos)
403
- e.dirty = false
404
294
  e.clean_up
405
295
  end
406
296
  zos.comment = comment
407
297
  end
408
298
  true
409
299
  end
410
- initialize(name)
300
+ initialize_cdir(@name)
411
301
  end
412
302
 
413
303
  # Write buffer write changes to buffer and return
@@ -415,7 +305,7 @@ module Zip
415
305
  return io unless commit_required?
416
306
 
417
307
  ::Zip::OutputStream.write_buffer(io) do |zos|
418
- @entry_set.each { |e| e.write_to_zip_output_stream(zos) }
308
+ @cdir.each { |e| e.write_to_zip_output_stream(zos) }
419
309
  zos.comment = comment
420
310
  end
421
311
  end
@@ -428,16 +318,19 @@ module Zip
428
318
  # Returns true if any changes has been made to this archive since
429
319
  # the previous commit
430
320
  def commit_required?
431
- @entry_set.each do |e|
432
- return true if e.dirty
321
+ return true if @create || @cdir.dirty?
322
+
323
+ @cdir.each do |e|
324
+ return true if e.dirty?
433
325
  end
434
- @comment != @stored_comment || @entry_set != @stored_entries || @create
326
+
327
+ false
435
328
  end
436
329
 
437
330
  # Searches for entry with the specified name. Returns nil if
438
331
  # no entry is found. See also get_entry
439
332
  def find_entry(entry_name)
440
- selected_entry = @entry_set.find_entry(entry_name)
333
+ selected_entry = @cdir.find_entry(entry_name)
441
334
  return if selected_entry.nil?
442
335
 
443
336
  selected_entry.restore_ownership = @restore_ownership
@@ -446,11 +339,6 @@ module Zip
446
339
  selected_entry
447
340
  end
448
341
 
449
- # Searches for entries given a glob
450
- def glob(*args, &block)
451
- @entry_set.glob(*args, &block)
452
- end
453
-
454
342
  # Searches for an entry just as find_entry, but throws Errno::ENOENT
455
343
  # if no entry is found.
456
344
  def get_entry(entry)
@@ -466,33 +354,50 @@ module Zip
466
354
 
467
355
  entry_name = entry_name.dup.to_s
468
356
  entry_name << '/' unless entry_name.end_with?('/')
469
- @entry_set << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
357
+ @cdir << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
470
358
  end
471
359
 
472
360
  private
473
361
 
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 += '/'
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"
482
391
  end
483
- new_entry.directory? && path_is_directory
484
392
  end
485
393
 
486
394
  def check_entry_exists(entry_name, continue_on_exists_proc, proc_name)
395
+ return unless @cdir.include?(entry_name)
396
+
487
397
  continue_on_exists_proc ||= proc { Zip.continue_on_exists_proc }
488
- return unless @entry_set.include?(entry_name)
398
+ raise ::Zip::EntryExistsError.new proc_name, entry_name unless continue_on_exists_proc.call
489
399
 
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
400
+ remove get_entry(entry_name)
496
401
  end
497
402
 
498
403
  def check_file(path)
@@ -502,14 +407,12 @@ module Zip
502
407
  def on_success_replace
503
408
  dirname, basename = ::File.split(name)
504
409
  ::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)
410
+ if yield tmp_filename
411
+ ::File.rename(tmp_filename, name)
412
+ ::File.chmod(@file_permissions, name) unless @create
512
413
  end
414
+ ensure
415
+ ::File.unlink(tmp_filename) if ::File.exist?(tmp_filename)
513
416
  end
514
417
  end
515
418
  end