rubyzip 2.4.1 → 3.0.0.alpha

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +368 -0
  3. data/README.md +112 -37
  4. data/Rakefile +11 -7
  5. data/lib/zip/central_directory.rb +164 -118
  6. data/lib/zip/compressor.rb +3 -1
  7. data/lib/zip/constants.rb +25 -21
  8. data/lib/zip/crypto/decrypted_io.rb +3 -1
  9. data/lib/zip/crypto/encryption.rb +4 -2
  10. data/lib/zip/crypto/null_encryption.rb +5 -3
  11. data/lib/zip/crypto/traditional_encryption.rb +5 -3
  12. data/lib/zip/decompressor.rb +4 -3
  13. data/lib/zip/deflater.rb +10 -8
  14. data/lib/zip/dirtyable.rb +32 -0
  15. data/lib/zip/dos_time.rb +32 -3
  16. data/lib/zip/entry.rb +262 -198
  17. data/lib/zip/entry_set.rb +9 -7
  18. data/lib/zip/errors.rb +115 -16
  19. data/lib/zip/extra_field/generic.rb +3 -10
  20. data/lib/zip/extra_field/ntfs.rb +4 -2
  21. data/lib/zip/extra_field/old_unix.rb +3 -1
  22. data/lib/zip/extra_field/universal_time.rb +3 -1
  23. data/lib/zip/extra_field/unix.rb +5 -3
  24. data/lib/zip/extra_field/unknown.rb +33 -0
  25. data/lib/zip/extra_field/zip64.rb +12 -5
  26. data/lib/zip/extra_field.rb +15 -21
  27. data/lib/zip/file.rb +144 -265
  28. data/lib/zip/file_split.rb +97 -0
  29. data/lib/zip/filesystem/dir.rb +86 -0
  30. data/lib/zip/filesystem/directory_iterator.rb +48 -0
  31. data/lib/zip/filesystem/file.rb +262 -0
  32. data/lib/zip/filesystem/file_stat.rb +110 -0
  33. data/lib/zip/filesystem/zip_file_name_mapper.rb +81 -0
  34. data/lib/zip/filesystem.rb +26 -595
  35. data/lib/zip/inflater.rb +7 -5
  36. data/lib/zip/input_stream.rb +44 -39
  37. data/lib/zip/ioextras/abstract_input_stream.rb +14 -9
  38. data/lib/zip/ioextras/abstract_output_stream.rb +5 -3
  39. data/lib/zip/ioextras.rb +6 -6
  40. data/lib/zip/null_compressor.rb +3 -1
  41. data/lib/zip/null_decompressor.rb +3 -1
  42. data/lib/zip/null_input_stream.rb +3 -1
  43. data/lib/zip/output_stream.rb +47 -48
  44. data/lib/zip/pass_thru_compressor.rb +3 -1
  45. data/lib/zip/pass_thru_decompressor.rb +4 -2
  46. data/lib/zip/streamable_directory.rb +3 -1
  47. data/lib/zip/streamable_stream.rb +3 -0
  48. data/lib/zip/version.rb +3 -1
  49. data/lib/zip.rb +15 -20
  50. data/rubyzip.gemspec +38 -0
  51. data/samples/example.rb +8 -3
  52. data/samples/example_filesystem.rb +2 -1
  53. data/samples/example_recursive.rb +3 -1
  54. data/samples/gtk_ruby_zip.rb +4 -2
  55. data/samples/qtzip.rb +6 -5
  56. data/samples/write_simple.rb +1 -0
  57. data/samples/zipfind.rb +1 -0
  58. metadata +84 -50
  59. data/TODO +0 -15
  60. data/lib/zip/extra_field/zip64_placeholder.rb +0 -15
data/lib/zip/file.rb CHANGED
@@ -1,132 +1,96 @@
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
54
+
55
+ IO_METHODS = [:tell, :seek, :read, :eof, :close].freeze
59
56
 
60
57
  attr_reader :name
61
58
 
62
59
  # default -> false.
63
60
  attr_accessor :restore_ownership
64
61
 
65
- # default -> false, but will be set to true in a future version.
62
+ # default -> true.
66
63
  attr_accessor :restore_permissions
67
64
 
68
- # default -> false, but will be set to true in a future version.
65
+ # default -> true.
69
66
  attr_accessor :restore_times
70
67
 
71
- # Returns the zip files comment, if it has one
72
- attr_accessor :comment
68
+ def_delegators :@cdir, :comment, :comment=, :each, :entries, :glob, :size
73
69
 
74
- # Opens a zip archive. Pass true as the second parameter to create
70
+ # Opens a zip archive. Pass create: true to create
75
71
  # 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)
72
+ def initialize(path_or_io, create: false, buffer: false, **options)
78
73
  super()
79
-
80
- Zip.warn_about_v3_api('File#new') if dep_create || dep_buffer
81
-
82
- options = DEFAULT_OPTIONS.merge(options)
74
+ options = DEFAULT_RESTORE_OPTIONS
75
+ .merge(compression_level: ::Zip.default_compression)
76
+ .merge(options)
83
77
  @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
87
-
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
78
+ @create = create ? true : false # allow any truthy value to mean true
92
79
 
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
80
+ initialize_cdir(path_or_io, buffer: buffer)
114
81
 
115
- @stored_entries = @entry_set.dup
116
- @stored_comment = @comment
117
82
  @restore_ownership = options[:restore_ownership]
118
83
  @restore_permissions = options[:restore_permissions]
119
84
  @restore_times = options[:restore_times]
85
+ @compression_level = options[:compression_level]
120
86
  end
121
87
 
122
88
  class << self
123
89
  # Similar to ::new. If a block is passed the Zip::File object is passed
124
90
  # to the block and is automatically closed afterwards, just as with
125
91
  # 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
128
-
129
- zf = ::Zip::File.new(file_name, create: (dep_create || create), buffer: false, **options)
92
+ def open(file_name, create: false, **options)
93
+ zf = ::Zip::File.new(file_name, create: create, **options)
130
94
  return zf unless block_given?
131
95
 
132
96
  begin
@@ -136,31 +100,19 @@ module Zip
136
100
  end
137
101
  end
138
102
 
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
103
  # Like #open, but reads zip archive contents from a String or open IO
150
104
  # stream, and outputs data to a buffer.
151
105
  # (This can be used to extract data from a
152
106
  # downloaded zip archive without first saving it to disk.)
153
- def open_buffer(io, **options)
107
+ def open_buffer(io = ::StringIO.new, create: false, **options)
154
108
  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}"
109
+ raise 'Zip::File.open_buffer expects a String or IO-like argument' \
110
+ "(responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
156
111
  end
157
112
 
158
113
  io = ::StringIO.new(io) if io.kind_of?(::String)
159
114
 
160
- # https://github.com/rubyzip/rubyzip/issues/119
161
- io.binmode if io.respond_to?(:binmode)
162
-
163
- zf = ::Zip::File.new(io, create: true, buffer: true, **options)
115
+ zf = ::Zip::File.new(io, create: create, buffer: true, **options)
164
116
  return zf unless block_given?
165
117
 
166
118
  yield zf
@@ -184,88 +136,18 @@ module Zip
184
136
  end
185
137
  end
186
138
 
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
139
+ # Count the entries in a zip archive without reading the whole set of
140
+ # entry data into memory.
141
+ def count_entries(path_or_io)
142
+ cdir = ::Zip::CentralDirectory.new
250
143
 
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)
144
+ if path_or_io.kind_of?(String)
145
+ ::File.open(path_or_io, 'rb') do |f|
146
+ cdir.count_entries(f)
264
147
  end
148
+ else
149
+ cdir.count_entries(path_or_io)
265
150
  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
151
  end
270
152
  end
271
153
 
@@ -281,45 +163,31 @@ module Zip
281
163
  # specified. If a block is passed the stream object is passed to the block and
282
164
  # the stream is automatically closed afterwards just as with ruby's builtin
283
165
  # 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,
166
+ def get_output_stream(entry, permissions: nil, comment: nil,
290
167
  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
168
+ compression_method: nil, compression_level: nil,
169
+ size: nil, time: nil, &a_proc)
299
170
 
300
171
  new_entry =
301
172
  if entry.kind_of?(Entry)
302
173
  entry
303
174
  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))
175
+ Entry.new(
176
+ @name, entry.to_s, comment: comment, extra: extra,
177
+ compressed_size: compressed_size, crc: crc, size: size,
178
+ compression_method: compression_method,
179
+ compression_level: compression_level, time: time
180
+ )
312
181
  end
313
182
  if new_entry.directory?
314
183
  raise ArgumentError,
315
184
  "cannot open stream to directory entry - '#{new_entry}'"
316
185
  end
317
- new_entry.unix_perms = (permission_int || dep_permission_int)
186
+ new_entry.unix_perms = permissions
318
187
  zip_streamable_entry = StreamableStream.new(new_entry)
319
- @entry_set << zip_streamable_entry
188
+ @cdir << zip_streamable_entry
320
189
  zip_streamable_entry.get_output_stream(&a_proc)
321
190
  end
322
- # rubocop:enable Metrics/ParameterLists, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
323
191
 
324
192
  # Returns the name of the zip archive
325
193
  def to_s
@@ -335,31 +203,39 @@ module Zip
335
203
  def add(entry, src_path, &continue_on_exists_proc)
336
204
  continue_on_exists_proc ||= proc { ::Zip.continue_on_exists_proc }
337
205
  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)
206
+ new_entry = if entry.kind_of?(::Zip::Entry)
207
+ entry
208
+ else
209
+ ::Zip::Entry.new(
210
+ @name, entry.to_s,
211
+ compression_level: @compression_level
212
+ )
213
+ end
339
214
  new_entry.gather_fileinfo_from_srcpath(src_path)
340
- new_entry.dirty = true
341
- @entry_set << new_entry
215
+ @cdir << new_entry
342
216
  end
343
217
 
344
218
  # Convenience method for adding the contents of a file to the archive
345
219
  # in Stored format (uncompressed)
346
220
  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)
221
+ entry = ::Zip::Entry.new(
222
+ @name, entry.to_s, compression_method: ::Zip::Entry::STORED
223
+ )
348
224
  add(entry, src_path, &continue_on_exists_proc)
349
225
  end
350
226
 
351
227
  # Removes the specified entry.
352
228
  def remove(entry)
353
- @entry_set.delete(get_entry(entry))
229
+ @cdir.delete(get_entry(entry))
354
230
  end
355
231
 
356
232
  # Renames the specified entry.
357
233
  def rename(entry, new_name, &continue_on_exists_proc)
358
234
  found_entry = get_entry(entry)
359
235
  check_entry_exists(new_name, continue_on_exists_proc, 'rename')
360
- @entry_set.delete(found_entry)
236
+ @cdir.delete(found_entry)
361
237
  found_entry.name = new_name
362
- @entry_set << found_entry
238
+ @cdir << found_entry
363
239
  end
364
240
 
365
241
  # Replaces the specified entry with the contents of src_path (from
@@ -370,25 +246,16 @@ module Zip
370
246
  add(entry, src_path)
371
247
  end
372
248
 
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
249
  # Extracts `entry` to a file at `entry_path`, with `destination_directory`
383
250
  # as the base location in the filesystem.
384
251
  #
385
252
  # NB: The caller is responsible for making sure `destination_directory` is
386
253
  # safe, if it is passed.
387
- def extract_v3(entry, entry_path = nil, destination_directory: '.', &block)
254
+ def extract(entry, entry_path = nil, destination_directory: '.', &block)
388
255
  block ||= proc { ::Zip.on_exists_proc }
389
256
  found_entry = get_entry(entry)
390
257
  entry_path ||= found_entry.name
391
- found_entry.extract_v3(entry_path, destination_directory: destination_directory, &block)
258
+ found_entry.extract(entry_path, destination_directory: destination_directory, &block)
392
259
  end
393
260
 
394
261
  # Commits changes that has been made since the previous commit to
@@ -398,24 +265,23 @@ module Zip
398
265
 
399
266
  on_success_replace do |tmp_file|
400
267
  ::Zip::OutputStream.open(tmp_file) do |zos|
401
- @entry_set.each do |e|
268
+ @cdir.each do |e|
402
269
  e.write_to_zip_output_stream(zos)
403
- e.dirty = false
404
270
  e.clean_up
405
271
  end
406
272
  zos.comment = comment
407
273
  end
408
274
  true
409
275
  end
410
- initialize(name)
276
+ initialize_cdir(@name)
411
277
  end
412
278
 
413
279
  # Write buffer write changes to buffer and return
414
280
  def write_buffer(io = ::StringIO.new)
415
- return io unless commit_required?
281
+ return unless commit_required?
416
282
 
417
283
  ::Zip::OutputStream.write_buffer(io) do |zos|
418
- @entry_set.each { |e| e.write_to_zip_output_stream(zos) }
284
+ @cdir.each { |e| e.write_to_zip_output_stream(zos) }
419
285
  zos.comment = comment
420
286
  end
421
287
  end
@@ -428,16 +294,19 @@ module Zip
428
294
  # Returns true if any changes has been made to this archive since
429
295
  # the previous commit
430
296
  def commit_required?
431
- @entry_set.each do |e|
432
- return true if e.dirty
297
+ return true if @create || @cdir.dirty?
298
+
299
+ @cdir.each do |e|
300
+ return true if e.dirty?
433
301
  end
434
- @comment != @stored_comment || @entry_set != @stored_entries || @create
302
+
303
+ false
435
304
  end
436
305
 
437
306
  # Searches for entry with the specified name. Returns nil if
438
307
  # no entry is found. See also get_entry
439
308
  def find_entry(entry_name)
440
- selected_entry = @entry_set.find_entry(entry_name)
309
+ selected_entry = @cdir.find_entry(entry_name)
441
310
  return if selected_entry.nil?
442
311
 
443
312
  selected_entry.restore_ownership = @restore_ownership
@@ -446,11 +315,6 @@ module Zip
446
315
  selected_entry
447
316
  end
448
317
 
449
- # Searches for entries given a glob
450
- def glob(*args, &block)
451
- @entry_set.glob(*args, &block)
452
- end
453
-
454
318
  # Searches for an entry just as find_entry, but throws Errno::ENOENT
455
319
  # if no entry is found.
456
320
  def get_entry(entry)
@@ -466,33 +330,50 @@ module Zip
466
330
 
467
331
  entry_name = entry_name.dup.to_s
468
332
  entry_name << '/' unless entry_name.end_with?('/')
469
- @entry_set << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
333
+ @cdir << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
470
334
  end
471
335
 
472
336
  private
473
337
 
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 += '/'
338
+ def initialize_cdir(path_or_io, buffer: false)
339
+ @cdir = ::Zip::CentralDirectory.new
340
+
341
+ if ::File.size?(@name.to_s)
342
+ # There is a file, which exists, that is associated with this zip.
343
+ @create = false
344
+ @file_permissions = ::File.stat(@name).mode
345
+
346
+ if buffer
347
+ # https://github.com/rubyzip/rubyzip/issues/119
348
+ path_or_io.binmode if path_or_io.respond_to?(:binmode)
349
+ @cdir.read_from_stream(path_or_io)
350
+ else
351
+ ::File.open(@name, 'rb') do |f|
352
+ @cdir.read_from_stream(f)
353
+ end
354
+ end
355
+ elsif buffer && path_or_io.size > 0
356
+ # This zip is probably a non-empty StringIO.
357
+ @create = false
358
+ @cdir.read_from_stream(path_or_io)
359
+ elsif !@create && ::File.zero?(@name)
360
+ # A file exists, but it is empty, and we've said we're
361
+ # NOT creating a new zip.
362
+ raise Error, "File #{@name} has zero size. Did you mean to pass the create flag?"
363
+ elsif !@create
364
+ # If we get here, and we're not creating a new zip, then
365
+ # everything is wrong.
366
+ raise Error, "File #{@name} not found"
482
367
  end
483
- new_entry.directory? && path_is_directory
484
368
  end
485
369
 
486
370
  def check_entry_exists(entry_name, continue_on_exists_proc, proc_name)
371
+ return unless @cdir.include?(entry_name)
372
+
487
373
  continue_on_exists_proc ||= proc { Zip.continue_on_exists_proc }
488
- return unless @entry_set.include?(entry_name)
374
+ raise ::Zip::EntryExistsError.new proc_name, entry_name unless continue_on_exists_proc.call
489
375
 
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
376
+ remove get_entry(entry_name)
496
377
  end
497
378
 
498
379
  def check_file(path)
@@ -502,14 +383,12 @@ module Zip
502
383
  def on_success_replace
503
384
  dirname, basename = ::File.split(name)
504
385
  ::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)
386
+ if yield tmp_filename
387
+ ::File.rename(tmp_filename, name)
388
+ ::File.chmod(@file_permissions, name) unless @create
512
389
  end
390
+ ensure
391
+ ::File.unlink(tmp_filename) if ::File.exist?(tmp_filename)
513
392
  end
514
393
  end
515
394
  end