rubyzip 2.4.1 → 3.1.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +457 -0
  3. data/LICENSE.md +24 -0
  4. data/README.md +138 -40
  5. data/Rakefile +15 -13
  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/aes_encryption.rb +120 -0
  10. data/lib/zip/crypto/decrypted_io.rb +20 -14
  11. data/lib/zip/crypto/encryption.rb +4 -2
  12. data/lib/zip/crypto/null_encryption.rb +5 -13
  13. data/lib/zip/crypto/traditional_encryption.rb +10 -6
  14. data/lib/zip/decompressor.rb +4 -3
  15. data/lib/zip/deflater.rb +12 -8
  16. data/lib/zip/dirtyable.rb +32 -0
  17. data/lib/zip/dos_time.rb +45 -5
  18. data/lib/zip/entry.rb +373 -249
  19. data/lib/zip/entry_set.rb +11 -9
  20. data/lib/zip/errors.rb +136 -16
  21. data/lib/zip/extra_field/aes.rb +45 -0
  22. data/lib/zip/extra_field/generic.rb +6 -13
  23. data/lib/zip/extra_field/ntfs.rb +6 -4
  24. data/lib/zip/extra_field/old_unix.rb +3 -1
  25. data/lib/zip/extra_field/universal_time.rb +3 -1
  26. data/lib/zip/extra_field/unix.rb +5 -3
  27. data/lib/zip/extra_field/unknown.rb +33 -0
  28. data/lib/zip/extra_field/zip64.rb +12 -5
  29. data/lib/zip/extra_field.rb +17 -22
  30. data/lib/zip/file.rb +166 -265
  31. data/lib/zip/file_split.rb +91 -0
  32. data/lib/zip/filesystem/dir.rb +86 -0
  33. data/lib/zip/filesystem/directory_iterator.rb +48 -0
  34. data/lib/zip/filesystem/file.rb +263 -0
  35. data/lib/zip/filesystem/file_stat.rb +110 -0
  36. data/lib/zip/filesystem/zip_file_name_mapper.rb +81 -0
  37. data/lib/zip/filesystem.rb +27 -596
  38. data/lib/zip/inflater.rb +11 -8
  39. data/lib/zip/input_stream.rb +76 -57
  40. data/lib/zip/ioextras/abstract_input_stream.rb +19 -13
  41. data/lib/zip/ioextras/abstract_output_stream.rb +13 -3
  42. data/lib/zip/ioextras.rb +7 -7
  43. data/lib/zip/null_compressor.rb +3 -1
  44. data/lib/zip/null_decompressor.rb +6 -3
  45. data/lib/zip/null_input_stream.rb +3 -1
  46. data/lib/zip/output_stream.rb +55 -56
  47. data/lib/zip/pass_thru_compressor.rb +3 -1
  48. data/lib/zip/pass_thru_decompressor.rb +8 -5
  49. data/lib/zip/streamable_directory.rb +3 -1
  50. data/lib/zip/streamable_stream.rb +4 -1
  51. data/lib/zip/version.rb +4 -1
  52. data/lib/zip.rb +25 -22
  53. data/rubyzip.gemspec +39 -0
  54. data/samples/example.rb +8 -3
  55. data/samples/example_filesystem.rb +3 -2
  56. data/samples/example_recursive.rb +3 -1
  57. data/samples/gtk_ruby_zip.rb +5 -3
  58. data/samples/qtzip.rb +7 -6
  59. data/samples/write_simple.rb +2 -1
  60. data/samples/zipfind.rb +1 -0
  61. metadata +87 -49
  62. data/TODO +0 -15
  63. 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 'fileutils'
4
+ require 'forwardable'
5
+
6
+ require_relative 'file_split'
7
+
1
8
  module Zip
2
- # ZipFile is modeled after java.util.zip.ZipFile from the Java SDK.
3
- # The most important methods are those inherited from
4
- # ZipCentralDirectory for accessing information about the entries in
5
- # the archive and methods such as get_input_stream and
6
- # get_output_stream for reading from and writing entries to the
9
+ # Zip::File is modeled after java.util.zip.ZipFile from the Java SDK.
10
+ # The most important methods are those for accessing information about
11
+ # the entries in
12
+ # the archive and methods such as `get_input_stream` and
13
+ # `get_output_stream` for reading from and writing entries to the
7
14
  # archive. The class includes a few convenience methods such as
8
- # #extract for extracting entries to the filesystem, and #remove,
9
- # #replace, #rename and #mkdir for making simple modifications to
15
+ # `extract` for extracting entries to the filesystem, and `remove`,
16
+ # `replace`, `rename` and `mkdir` for making simple modifications to
10
17
  # the archive.
11
18
  #
12
- # Modifications to a zip archive are not committed until #commit or
13
- # #close is called. The method #open accepts a block following
14
- # the pattern from File.open offering a simple way to
19
+ # Modifications to a zip archive are not committed until `commit` or
20
+ # `close` is called. The method `open` accepts a block following
21
+ # the pattern from ::File.open offering a simple way to
15
22
  # automatically close the archive when the block returns.
16
23
  #
17
- # The following example opens zip archive <code>my.zip</code>
24
+ # The following example opens zip archive `my.zip`
18
25
  # (creating it if it doesn't exist) and adds an entry
19
- # <code>first.txt</code> and a directory entry <code>a_dir</code>
26
+ # `first.txt` and a directory entry `a_dir`
20
27
  # to it.
21
28
  #
22
- # require 'zip'
29
+ # ```
30
+ # require 'zip'
23
31
  #
24
- # Zip::File.open("my.zip", Zip::File::CREATE) {
25
- # |zipfile|
26
- # zipfile.get_output_stream("first.txt") { |f| f.puts "Hello from ZipFile" }
27
- # zipfile.mkdir("a_dir")
28
- # }
32
+ # Zip::File.open('my.zip', create: true) do |zipfile|
33
+ # zipfile.get_output_stream('first.txt') { |f| f.puts 'Hello from Zip::File' }
34
+ # zipfile.mkdir('a_dir')
35
+ # end
36
+ # ```
29
37
  #
30
- # The next example reopens <code>my.zip</code> writes the contents of
31
- # <code>first.txt</code> to standard out and deletes the entry from
38
+ # The next example reopens `my.zip`, writes the contents of
39
+ # `first.txt` to standard out and deletes the entry from
32
40
  # the archive.
33
41
  #
34
- # require 'zip'
42
+ # ```
43
+ # require 'zip'
35
44
  #
36
- # Zip::File.open("my.zip", Zip::File::CREATE) {
37
- # |zipfile|
38
- # puts zipfile.read("first.txt")
39
- # zipfile.remove("first.txt")
40
- # }
45
+ # Zip::File.open('my.zip', create: true) do |zipfile|
46
+ # puts zipfile.read('first.txt')
47
+ # zipfile.remove('first.txt')
48
+ # end
41
49
  #
42
- # ZipFileSystem offers an alternative API that emulates ruby's
43
- # interface for accessing the filesystem, ie. the File and Dir classes.
44
-
45
- class File < CentralDirectory
46
- CREATE = true
47
- SPLIT_SIGNATURE = 0x08074b50
48
- ZIP64_EOCD_SIGNATURE = 0x06064b50
49
- MAX_SEGMENT_SIZE = 3_221_225_472
50
- MIN_SEGMENT_SIZE = 65_536
51
- DATA_BUFFER_SIZE = 8192
52
- IO_METHODS = [:tell, :seek, :read, :eof, :close]
53
-
54
- DEFAULT_OPTIONS = {
55
- restore_ownership: false,
56
- restore_permissions: false,
57
- restore_times: false
58
- }.freeze
50
+ # Zip::FileSystem offers an alternative API that emulates ruby's
51
+ # interface for accessing the filesystem, ie. the ::File and ::Dir classes.
52
+ class File
53
+ include Enumerable
54
+ extend Forwardable
55
+ extend FileSplit
59
56
 
57
+ IO_METHODS = [:tell, :seek, :read, :eof, :close].freeze # :nodoc:
58
+
59
+ # The name of this zip archive.
60
60
  attr_reader :name
61
61
 
62
62
  # default -> false.
63
63
  attr_accessor :restore_ownership
64
64
 
65
- # default -> false, but will be set to true in a future version.
65
+ # default -> true.
66
66
  attr_accessor :restore_permissions
67
67
 
68
- # default -> false, but will be set to true in a future version.
68
+ # default -> true.
69
69
  attr_accessor :restore_times
70
70
 
71
- # Returns the zip files comment, if it has one
72
- attr_accessor :comment
71
+ def_delegators :@cdir, :comment, :comment=, :each, :entries, :glob, :size
73
72
 
74
- # Opens a zip archive. Pass true as the second parameter to create
73
+ # Opens a zip archive. Pass create: true to create
75
74
  # a new archive if it doesn't exist already.
76
- def initialize(path_or_io, dep_create = false, dep_buffer = false,
77
- create: false, buffer: false, **options)
75
+ def initialize(path_or_io, create: false, buffer: false,
76
+ restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
77
+ restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
78
+ restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
79
+ compression_level: ::Zip.default_compression)
78
80
  super()
79
81
 
80
- Zip.warn_about_v3_api('File#new') if dep_create || dep_buffer
81
-
82
- options = DEFAULT_OPTIONS.merge(options)
83
82
  @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
83
+ @create = create ? true : false # allow any truthy value to mean true
87
84
 
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
85
+ initialize_cdir(path_or_io, buffer: buffer)
92
86
 
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]
87
+ @restore_ownership = restore_ownership
88
+ @restore_permissions = restore_permissions
89
+ @restore_times = restore_times
90
+ @compression_level = compression_level
120
91
  end
121
92
 
122
93
  class << self
123
94
  # Similar to ::new. If a block is passed the Zip::File object is passed
124
95
  # to the block and is automatically closed afterwards, just as with
125
96
  # 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
97
+ def open(file_name, create: false,
98
+ restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
99
+ restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
100
+ restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
101
+ compression_level: ::Zip.default_compression)
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,28 @@ 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)
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)
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
246
166
 
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
250
-
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,30 @@ 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
299
-
191
+ compression_method: nil, compression_level: nil,
192
+ size: nil, time: nil, &a_proc)
300
193
  new_entry =
301
194
  if entry.kind_of?(Entry)
302
195
  entry
303
196
  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))
197
+ Entry.new(
198
+ @name, entry.to_s, comment: comment, extra: extra,
199
+ compressed_size: compressed_size, crc: crc, size: size,
200
+ compression_method: compression_method,
201
+ compression_level: compression_level, time: time
202
+ )
312
203
  end
313
204
  if new_entry.directory?
314
205
  raise ArgumentError,
315
206
  "cannot open stream to directory entry - '#{new_entry}'"
316
207
  end
317
- new_entry.unix_perms = (permission_int || dep_permission_int)
208
+ new_entry.unix_perms = permissions
318
209
  zip_streamable_entry = StreamableStream.new(new_entry)
319
- @entry_set << zip_streamable_entry
210
+ @cdir << zip_streamable_entry
320
211
  zip_streamable_entry.get_output_stream(&a_proc)
321
212
  end
322
- # rubocop:enable Metrics/ParameterLists, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
323
213
 
324
214
  # Returns the name of the zip archive
325
215
  def to_s
@@ -335,31 +225,39 @@ module Zip
335
225
  def add(entry, src_path, &continue_on_exists_proc)
336
226
  continue_on_exists_proc ||= proc { ::Zip.continue_on_exists_proc }
337
227
  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)
228
+ new_entry = if entry.kind_of?(::Zip::Entry)
229
+ entry
230
+ else
231
+ ::Zip::Entry.new(
232
+ @name, entry.to_s,
233
+ compression_level: @compression_level
234
+ )
235
+ end
339
236
  new_entry.gather_fileinfo_from_srcpath(src_path)
340
- new_entry.dirty = true
341
- @entry_set << new_entry
237
+ @cdir << new_entry
342
238
  end
343
239
 
344
240
  # Convenience method for adding the contents of a file to the archive
345
241
  # in Stored format (uncompressed)
346
242
  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)
243
+ entry = ::Zip::Entry.new(
244
+ @name, entry.to_s, compression_method: ::Zip::Entry::STORED
245
+ )
348
246
  add(entry, src_path, &continue_on_exists_proc)
349
247
  end
350
248
 
351
249
  # Removes the specified entry.
352
250
  def remove(entry)
353
- @entry_set.delete(get_entry(entry))
251
+ @cdir.delete(get_entry(entry))
354
252
  end
355
253
 
356
254
  # Renames the specified entry.
357
255
  def rename(entry, new_name, &continue_on_exists_proc)
358
256
  found_entry = get_entry(entry)
359
257
  check_entry_exists(new_name, continue_on_exists_proc, 'rename')
360
- @entry_set.delete(found_entry)
258
+ @cdir.delete(found_entry)
361
259
  found_entry.name = new_name
362
- @entry_set << found_entry
260
+ @cdir << found_entry
363
261
  end
364
262
 
365
263
  # Replaces the specified entry with the contents of src_path (from
@@ -370,25 +268,16 @@ module Zip
370
268
  add(entry, src_path)
371
269
  end
372
270
 
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
271
  # Extracts `entry` to a file at `entry_path`, with `destination_directory`
383
272
  # as the base location in the filesystem.
384
273
  #
385
274
  # NB: The caller is responsible for making sure `destination_directory` is
386
275
  # safe, if it is passed.
387
- def extract_v3(entry, entry_path = nil, destination_directory: '.', &block)
276
+ def extract(entry, entry_path = nil, destination_directory: '.', &block)
388
277
  block ||= proc { ::Zip.on_exists_proc }
389
278
  found_entry = get_entry(entry)
390
279
  entry_path ||= found_entry.name
391
- found_entry.extract_v3(entry_path, destination_directory: destination_directory, &block)
280
+ found_entry.extract(entry_path, destination_directory: destination_directory, &block)
392
281
  end
393
282
 
394
283
  # Commits changes that has been made since the previous commit to
@@ -398,16 +287,15 @@ module Zip
398
287
 
399
288
  on_success_replace do |tmp_file|
400
289
  ::Zip::OutputStream.open(tmp_file) do |zos|
401
- @entry_set.each do |e|
290
+ @cdir.each do |e|
402
291
  e.write_to_zip_output_stream(zos)
403
- e.dirty = false
404
292
  e.clean_up
405
293
  end
406
294
  zos.comment = comment
407
295
  end
408
296
  true
409
297
  end
410
- initialize(name)
298
+ initialize_cdir(@name)
411
299
  end
412
300
 
413
301
  # Write buffer write changes to buffer and return
@@ -415,7 +303,7 @@ module Zip
415
303
  return io unless commit_required?
416
304
 
417
305
  ::Zip::OutputStream.write_buffer(io) do |zos|
418
- @entry_set.each { |e| e.write_to_zip_output_stream(zos) }
306
+ @cdir.each { |e| e.write_to_zip_output_stream(zos) }
419
307
  zos.comment = comment
420
308
  end
421
309
  end
@@ -428,16 +316,19 @@ module Zip
428
316
  # Returns true if any changes has been made to this archive since
429
317
  # the previous commit
430
318
  def commit_required?
431
- @entry_set.each do |e|
432
- return true if e.dirty
319
+ return true if @create || @cdir.dirty?
320
+
321
+ @cdir.each do |e|
322
+ return true if e.dirty?
433
323
  end
434
- @comment != @stored_comment || @entry_set != @stored_entries || @create
324
+
325
+ false
435
326
  end
436
327
 
437
328
  # Searches for entry with the specified name. Returns nil if
438
329
  # no entry is found. See also get_entry
439
330
  def find_entry(entry_name)
440
- selected_entry = @entry_set.find_entry(entry_name)
331
+ selected_entry = @cdir.find_entry(entry_name)
441
332
  return if selected_entry.nil?
442
333
 
443
334
  selected_entry.restore_ownership = @restore_ownership
@@ -446,11 +337,6 @@ module Zip
446
337
  selected_entry
447
338
  end
448
339
 
449
- # Searches for entries given a glob
450
- def glob(*args, &block)
451
- @entry_set.glob(*args, &block)
452
- end
453
-
454
340
  # Searches for an entry just as find_entry, but throws Errno::ENOENT
455
341
  # if no entry is found.
456
342
  def get_entry(entry)
@@ -466,33 +352,50 @@ module Zip
466
352
 
467
353
  entry_name = entry_name.dup.to_s
468
354
  entry_name << '/' unless entry_name.end_with?('/')
469
- @entry_set << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
355
+ @cdir << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
470
356
  end
471
357
 
472
358
  private
473
359
 
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 += '/'
360
+ def initialize_cdir(path_or_io, buffer: false)
361
+ @cdir = ::Zip::CentralDirectory.new
362
+
363
+ if ::File.size?(@name.to_s)
364
+ # There is a file, which exists, that is associated with this zip.
365
+ @create = false
366
+ @file_permissions = ::File.stat(@name).mode
367
+
368
+ if buffer
369
+ # https://github.com/rubyzip/rubyzip/issues/119
370
+ path_or_io.binmode if path_or_io.respond_to?(:binmode)
371
+ @cdir.read_from_stream(path_or_io)
372
+ else
373
+ ::File.open(@name, 'rb') do |f|
374
+ @cdir.read_from_stream(f)
375
+ end
376
+ end
377
+ elsif buffer && path_or_io.size > 0
378
+ # This zip is probably a non-empty StringIO.
379
+ @create = false
380
+ @cdir.read_from_stream(path_or_io)
381
+ elsif !@create && ::File.empty?(@name)
382
+ # A file exists, but it is empty, and we've said we're
383
+ # NOT creating a new zip.
384
+ raise Error, "File #{@name} has zero size. Did you mean to pass the create flag?"
385
+ elsif !@create
386
+ # If we get here, and we're not creating a new zip, then
387
+ # everything is wrong.
388
+ raise Error, "File #{@name} not found"
482
389
  end
483
- new_entry.directory? && path_is_directory
484
390
  end
485
391
 
486
392
  def check_entry_exists(entry_name, continue_on_exists_proc, proc_name)
393
+ return unless @cdir.include?(entry_name)
394
+
487
395
  continue_on_exists_proc ||= proc { Zip.continue_on_exists_proc }
488
- return unless @entry_set.include?(entry_name)
396
+ raise ::Zip::EntryExistsError.new proc_name, entry_name unless continue_on_exists_proc.call
489
397
 
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
398
+ remove get_entry(entry_name)
496
399
  end
497
400
 
498
401
  def check_file(path)
@@ -502,14 +405,12 @@ module Zip
502
405
  def on_success_replace
503
406
  dirname, basename = ::File.split(name)
504
407
  ::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)
408
+ if yield tmp_filename
409
+ ::File.rename(tmp_filename, name)
410
+ ::File.chmod(@file_permissions, name) unless @create
512
411
  end
412
+ ensure
413
+ FileUtils.rm_f(tmp_filename)
513
414
  end
514
415
  end
515
416
  end