rubyzip 2.4.1 → 3.2.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 +476 -0
  3. data/LICENSE.md +24 -0
  4. data/README.md +180 -40
  5. data/Rakefile +15 -13
  6. data/lib/zip/central_directory.rb +172 -124
  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 +391 -264
  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 +50 -0
  22. data/lib/zip/extra_field/generic.rb +10 -11
  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 +35 -0
  28. data/lib/zip/extra_field/zip64.rb +19 -5
  29. data/lib/zip/extra_field.rb +25 -23
  30. data/lib/zip/file.rb +174 -267
  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 +60 -57
  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 +86 -52
  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,114 @@
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,
80
+ suppress_extra_fields: false)
78
81
  super()
79
82
 
80
- Zip.warn_about_v3_api('File#new') if dep_create || dep_buffer
81
-
82
- options = DEFAULT_OPTIONS.merge(options)
83
83
  @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
84
+ @create = create ? true : false # allow any truthy value to mean true
87
85
 
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
86
+ initialize_cdir(path_or_io, buffer: buffer)
92
87
 
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]
88
+ @restore_ownership = restore_ownership
89
+ @restore_permissions = restore_permissions
90
+ @restore_times = restore_times
91
+ @compression_level = compression_level
92
+ @suppress_extra_fields = suppress_extra_fields
120
93
  end
121
94
 
122
95
  class << self
123
96
  # Similar to ::new. If a block is passed the Zip::File object is passed
124
97
  # to the block and is automatically closed afterwards, just as with
125
98
  # 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
99
+ def open(file_name, create: false,
100
+ restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
101
+ restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
102
+ restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
103
+ compression_level: ::Zip.default_compression,
104
+ suppress_extra_fields: false)
105
+ zf = ::Zip::File.new(file_name, create: create,
106
+ restore_ownership: restore_ownership,
107
+ restore_permissions: restore_permissions,
108
+ restore_times: restore_times,
109
+ compression_level: compression_level,
110
+ suppress_extra_fields: suppress_extra_fields)
128
111
 
129
- zf = ::Zip::File.new(file_name, create: (dep_create || create), buffer: false, **options)
130
112
  return zf unless block_given?
131
113
 
132
114
  begin
@@ -136,31 +118,30 @@ module Zip
136
118
  end
137
119
  end
138
120
 
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
121
  # Like #open, but reads zip archive contents from a String or open IO
150
122
  # stream, and outputs data to a buffer.
151
123
  # (This can be used to extract data from a
152
124
  # downloaded zip archive without first saving it to disk.)
153
- def open_buffer(io, **options)
125
+ def open_buffer(io = ::StringIO.new, create: false,
126
+ restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
127
+ restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
128
+ restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
129
+ compression_level: ::Zip.default_compression,
130
+ suppress_extra_fields: false)
154
131
  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}"
132
+ raise 'Zip::File.open_buffer expects a String or IO-like argument' \
133
+ "(responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
156
134
  end
157
135
 
158
136
  io = ::StringIO.new(io) if io.kind_of?(::String)
159
137
 
160
- # https://github.com/rubyzip/rubyzip/issues/119
161
- io.binmode if io.respond_to?(:binmode)
138
+ zf = ::Zip::File.new(io, create: create, buffer: true,
139
+ restore_ownership: restore_ownership,
140
+ restore_permissions: restore_permissions,
141
+ restore_times: restore_times,
142
+ compression_level: compression_level,
143
+ suppress_extra_fields: suppress_extra_fields)
162
144
 
163
- zf = ::Zip::File.new(io, create: true, buffer: true, **options)
164
145
  return zf unless block_given?
165
146
 
166
147
  yield zf
@@ -184,88 +165,18 @@ module Zip
184
165
  end
185
166
  end
186
167
 
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)
168
+ # Count the entries in a zip archive without reading the whole set of
169
+ # entry data into memory.
170
+ def count_entries(path_or_io)
171
+ cdir = ::Zip::CentralDirectory.new
246
172
 
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)
173
+ if path_or_io.kind_of?(String)
174
+ ::File.open(path_or_io, 'rb') do |f|
175
+ cdir.count_entries(f)
264
176
  end
177
+ else
178
+ cdir.count_entries(path_or_io)
265
179
  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
180
  end
270
181
  end
271
182
 
@@ -281,45 +192,30 @@ module Zip
281
192
  # specified. If a block is passed the stream object is passed to the block and
282
193
  # the stream is automatically closed afterwards just as with ruby's builtin
283
194
  # 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,
195
+ def get_output_stream(entry, permissions: nil, comment: nil,
290
196
  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
-
197
+ compression_method: nil, compression_level: nil,
198
+ size: nil, time: nil, &a_proc)
300
199
  new_entry =
301
200
  if entry.kind_of?(Entry)
302
201
  entry
303
202
  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))
203
+ Entry.new(
204
+ @name, entry.to_s, comment: comment, extra: extra,
205
+ compressed_size: compressed_size, crc: crc, size: size,
206
+ compression_method: compression_method,
207
+ compression_level: compression_level, time: time
208
+ )
312
209
  end
313
210
  if new_entry.directory?
314
211
  raise ArgumentError,
315
212
  "cannot open stream to directory entry - '#{new_entry}'"
316
213
  end
317
- new_entry.unix_perms = (permission_int || dep_permission_int)
214
+ new_entry.unix_perms = permissions
318
215
  zip_streamable_entry = StreamableStream.new(new_entry)
319
- @entry_set << zip_streamable_entry
216
+ @cdir << zip_streamable_entry
320
217
  zip_streamable_entry.get_output_stream(&a_proc)
321
218
  end
322
- # rubocop:enable Metrics/ParameterLists, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
323
219
 
324
220
  # Returns the name of the zip archive
325
221
  def to_s
@@ -335,31 +231,39 @@ module Zip
335
231
  def add(entry, src_path, &continue_on_exists_proc)
336
232
  continue_on_exists_proc ||= proc { ::Zip.continue_on_exists_proc }
337
233
  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)
234
+ new_entry = if entry.kind_of?(::Zip::Entry)
235
+ entry
236
+ else
237
+ ::Zip::Entry.new(
238
+ @name, entry.to_s,
239
+ compression_level: @compression_level
240
+ )
241
+ end
339
242
  new_entry.gather_fileinfo_from_srcpath(src_path)
340
- new_entry.dirty = true
341
- @entry_set << new_entry
243
+ @cdir << new_entry
342
244
  end
343
245
 
344
246
  # Convenience method for adding the contents of a file to the archive
345
247
  # in Stored format (uncompressed)
346
248
  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)
249
+ entry = ::Zip::Entry.new(
250
+ @name, entry.to_s, compression_method: ::Zip::Entry::STORED
251
+ )
348
252
  add(entry, src_path, &continue_on_exists_proc)
349
253
  end
350
254
 
351
255
  # Removes the specified entry.
352
256
  def remove(entry)
353
- @entry_set.delete(get_entry(entry))
257
+ @cdir.delete(get_entry(entry))
354
258
  end
355
259
 
356
260
  # Renames the specified entry.
357
261
  def rename(entry, new_name, &continue_on_exists_proc)
358
262
  found_entry = get_entry(entry)
359
263
  check_entry_exists(new_name, continue_on_exists_proc, 'rename')
360
- @entry_set.delete(found_entry)
264
+ @cdir.delete(found_entry)
361
265
  found_entry.name = new_name
362
- @entry_set << found_entry
266
+ @cdir << found_entry
363
267
  end
364
268
 
365
269
  # Replaces the specified entry with the contents of src_path (from
@@ -370,25 +274,16 @@ module Zip
370
274
  add(entry, src_path)
371
275
  end
372
276
 
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
277
  # Extracts `entry` to a file at `entry_path`, with `destination_directory`
383
278
  # as the base location in the filesystem.
384
279
  #
385
280
  # NB: The caller is responsible for making sure `destination_directory` is
386
281
  # safe, if it is passed.
387
- def extract_v3(entry, entry_path = nil, destination_directory: '.', &block)
282
+ def extract(entry, entry_path = nil, destination_directory: '.', &block)
388
283
  block ||= proc { ::Zip.on_exists_proc }
389
284
  found_entry = get_entry(entry)
390
285
  entry_path ||= found_entry.name
391
- found_entry.extract_v3(entry_path, destination_directory: destination_directory, &block)
286
+ found_entry.extract(entry_path, destination_directory: destination_directory, &block)
392
287
  end
393
288
 
394
289
  # Commits changes that has been made since the previous commit to
@@ -397,25 +292,24 @@ module Zip
397
292
  return if name.kind_of?(StringIO) || !commit_required?
398
293
 
399
294
  on_success_replace do |tmp_file|
400
- ::Zip::OutputStream.open(tmp_file) do |zos|
401
- @entry_set.each do |e|
295
+ ::Zip::OutputStream.open(tmp_file, suppress_extra_fields: @suppress_extra_fields) do |zos|
296
+ @cdir.each do |e|
402
297
  e.write_to_zip_output_stream(zos)
403
- e.dirty = false
404
298
  e.clean_up
405
299
  end
406
300
  zos.comment = comment
407
301
  end
408
302
  true
409
303
  end
410
- initialize(name)
304
+ initialize_cdir(@name)
411
305
  end
412
306
 
413
307
  # Write buffer write changes to buffer and return
414
308
  def write_buffer(io = ::StringIO.new)
415
309
  return io unless commit_required?
416
310
 
417
- ::Zip::OutputStream.write_buffer(io) do |zos|
418
- @entry_set.each { |e| e.write_to_zip_output_stream(zos) }
311
+ ::Zip::OutputStream.write_buffer(io, suppress_extra_fields: @suppress_extra_fields) do |zos|
312
+ @cdir.each { |e| e.write_to_zip_output_stream(zos) }
419
313
  zos.comment = comment
420
314
  end
421
315
  end
@@ -428,16 +322,19 @@ module Zip
428
322
  # Returns true if any changes has been made to this archive since
429
323
  # the previous commit
430
324
  def commit_required?
431
- @entry_set.each do |e|
432
- return true if e.dirty
325
+ return true if @create || @cdir.dirty?
326
+
327
+ @cdir.each do |e|
328
+ return true if e.dirty?
433
329
  end
434
- @comment != @stored_comment || @entry_set != @stored_entries || @create
330
+
331
+ false
435
332
  end
436
333
 
437
334
  # Searches for entry with the specified name. Returns nil if
438
335
  # no entry is found. See also get_entry
439
336
  def find_entry(entry_name)
440
- selected_entry = @entry_set.find_entry(entry_name)
337
+ selected_entry = @cdir.find_entry(entry_name)
441
338
  return if selected_entry.nil?
442
339
 
443
340
  selected_entry.restore_ownership = @restore_ownership
@@ -446,11 +343,6 @@ module Zip
446
343
  selected_entry
447
344
  end
448
345
 
449
- # Searches for entries given a glob
450
- def glob(*args, &block)
451
- @entry_set.glob(*args, &block)
452
- end
453
-
454
346
  # Searches for an entry just as find_entry, but throws Errno::ENOENT
455
347
  # if no entry is found.
456
348
  def get_entry(entry)
@@ -466,33 +358,50 @@ module Zip
466
358
 
467
359
  entry_name = entry_name.dup.to_s
468
360
  entry_name << '/' unless entry_name.end_with?('/')
469
- @entry_set << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
361
+ @cdir << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
470
362
  end
471
363
 
472
364
  private
473
365
 
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 += '/'
366
+ def initialize_cdir(path_or_io, buffer: false)
367
+ @cdir = ::Zip::CentralDirectory.new
368
+
369
+ if ::File.size?(@name.to_s)
370
+ # There is a file, which exists, that is associated with this zip.
371
+ @create = false
372
+ @file_permissions = ::File.stat(@name).mode
373
+
374
+ if buffer
375
+ # https://github.com/rubyzip/rubyzip/issues/119
376
+ path_or_io.binmode if path_or_io.respond_to?(:binmode)
377
+ @cdir.read_from_stream(path_or_io)
378
+ else
379
+ ::File.open(@name, 'rb') do |f|
380
+ @cdir.read_from_stream(f)
381
+ end
382
+ end
383
+ elsif buffer && path_or_io.size > 0
384
+ # This zip is probably a non-empty StringIO.
385
+ @create = false
386
+ @cdir.read_from_stream(path_or_io)
387
+ elsif !@create && ::File.empty?(@name)
388
+ # A file exists, but it is empty, and we've said we're
389
+ # NOT creating a new zip.
390
+ raise Error, "File #{@name} has zero size. Did you mean to pass the create flag?"
391
+ elsif !@create
392
+ # If we get here, and we're not creating a new zip, then
393
+ # everything is wrong.
394
+ raise Error, "File #{@name} not found"
482
395
  end
483
- new_entry.directory? && path_is_directory
484
396
  end
485
397
 
486
398
  def check_entry_exists(entry_name, continue_on_exists_proc, proc_name)
399
+ return unless @cdir.include?(entry_name)
400
+
487
401
  continue_on_exists_proc ||= proc { Zip.continue_on_exists_proc }
488
- return unless @entry_set.include?(entry_name)
402
+ raise ::Zip::EntryExistsError.new proc_name, entry_name unless continue_on_exists_proc.call
489
403
 
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
404
+ remove get_entry(entry_name)
496
405
  end
497
406
 
498
407
  def check_file(path)
@@ -502,14 +411,12 @@ module Zip
502
411
  def on_success_replace
503
412
  dirname, basename = ::File.split(name)
504
413
  ::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)
414
+ if yield tmp_filename
415
+ ::File.rename(tmp_filename, name)
416
+ ::File.chmod(@file_permissions, name) unless @create
512
417
  end
418
+ ensure
419
+ FileUtils.rm_f(tmp_filename)
513
420
  end
514
421
  end
515
422
  end