rubyzip 2.1.0 → 3.6.0

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +538 -0
  3. data/LICENSE.md +24 -0
  4. data/README.md +235 -48
  5. data/Rakefile +17 -12
  6. data/lib/rubyzip.rb +3 -0
  7. data/lib/zip/central_directory.rb +196 -123
  8. data/lib/zip/compressor.rb +3 -1
  9. data/lib/zip/constants.rb +81 -21
  10. data/lib/zip/crypto/aes_encryption.rb +123 -0
  11. data/lib/zip/crypto/decrypted_io.rb +52 -0
  12. data/lib/zip/crypto/encryption.rb +4 -2
  13. data/lib/zip/crypto/null_encryption.rb +5 -13
  14. data/lib/zip/crypto/traditional_encryption.rb +22 -16
  15. data/lib/zip/decompressor.rb +21 -2
  16. data/lib/zip/deflater.rb +12 -8
  17. data/lib/zip/dirtyable.rb +32 -0
  18. data/lib/zip/dos_time.rb +79 -14
  19. data/lib/zip/entry.rb +430 -249
  20. data/lib/zip/entry_set.rb +13 -9
  21. data/lib/zip/errors.rb +152 -15
  22. data/lib/zip/extra_field/aes.rb +50 -0
  23. data/lib/zip/extra_field/generic.rb +17 -17
  24. data/lib/zip/extra_field/ntfs.rb +10 -4
  25. data/lib/zip/extra_field/old_unix.rb +8 -4
  26. data/lib/zip/extra_field/universal_time.rb +6 -1
  27. data/lib/zip/extra_field/unix.rb +9 -5
  28. data/lib/zip/extra_field/unknown.rb +35 -0
  29. data/lib/zip/extra_field/zip64.rb +23 -7
  30. data/lib/zip/extra_field.rb +31 -27
  31. data/lib/zip/file.rb +211 -241
  32. data/lib/zip/file_split.rb +91 -0
  33. data/lib/zip/filesystem/dir.rb +86 -0
  34. data/lib/zip/filesystem/directory_iterator.rb +48 -0
  35. data/lib/zip/filesystem/file.rb +263 -0
  36. data/lib/zip/filesystem/file_stat.rb +110 -0
  37. data/lib/zip/filesystem/zip_file_name_mapper.rb +81 -0
  38. data/lib/zip/filesystem.rb +32 -585
  39. data/lib/zip/inflater.rb +28 -37
  40. data/lib/zip/input_stream.rb +113 -54
  41. data/lib/zip/ioextras/abstract_input_stream.rb +154 -55
  42. data/lib/zip/ioextras/abstract_output_stream.rb +13 -3
  43. data/lib/zip/ioextras.rb +12 -9
  44. data/lib/zip/null_compressor.rb +3 -1
  45. data/lib/zip/null_decompressor.rb +7 -12
  46. data/lib/zip/null_input_stream.rb +3 -1
  47. data/lib/zip/output_stream.rb +76 -51
  48. data/lib/zip/pass_thru_compressor.rb +5 -3
  49. data/lib/zip/pass_thru_decompressor.rb +17 -23
  50. data/lib/zip/streamable_directory.rb +6 -4
  51. data/lib/zip/streamable_stream.rb +9 -5
  52. data/lib/zip/version.rb +4 -1
  53. data/lib/zip.rb +31 -5
  54. data/rubyzip.gemspec +39 -0
  55. data/samples/example.rb +9 -4
  56. data/samples/example_filesystem.rb +4 -3
  57. data/samples/example_recursive.rb +3 -1
  58. data/samples/gtk_ruby_zip.rb +23 -21
  59. data/samples/qtzip.rb +13 -12
  60. data/samples/write_simple.rb +3 -4
  61. data/samples/zipfind.rb +24 -22
  62. metadata +84 -28
  63. data/TODO +0 -15
  64. data/lib/zip/extra_field/zip64_placeholder.rb +0 -15
data/lib/zip/file.rb CHANGED
@@ -1,125 +1,116 @@
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, :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, 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)
77
81
  super()
78
- options = DEFAULT_OPTIONS.merge(options)
82
+
79
83
  @name = path_or_io.respond_to?(:path) ? path_or_io.path : path_or_io
80
- @comment = ''
81
84
  @create = create ? true : false # allow any truthy value to mean true
82
85
 
83
- if ::File.size?(@name.to_s)
84
- # There is a file, which exists, that is associated with this zip.
85
- @create = false
86
- @file_permissions = ::File.stat(@name).mode
86
+ initialize_cdir(path_or_io, buffer: buffer)
87
87
 
88
- if buffer
89
- read_from_stream(path_or_io)
90
- else
91
- ::File.open(@name, 'rb') do |f|
92
- read_from_stream(f)
93
- end
94
- end
95
- elsif buffer && path_or_io.size > 0
96
- # This zip is probably a non-empty StringIO.
97
- read_from_stream(path_or_io)
98
- elsif @create
99
- # This zip is completely new/empty and is to be created.
100
- @entry_set = EntrySet.new
101
- elsif ::File.zero?(@name)
102
- # A file exists, but it is empty.
103
- raise Error, "File #{@name} has zero size. Did you mean to pass the create flag?"
104
- else
105
- # Everything is wrong.
106
- raise Error, "File #{@name} not found"
107
- end
108
-
109
- @stored_entries = @entry_set.dup
110
- @stored_comment = @comment
111
- @restore_ownership = options[:restore_ownership]
112
- @restore_permissions = options[:restore_permissions]
113
- @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
114
93
  end
115
94
 
116
95
  class << self
117
96
  # Similar to ::new. If a block is passed the Zip::File object is passed
118
97
  # to the block and is automatically closed afterwards, just as with
119
98
  # ruby's builtin File::open method.
120
- def open(file_name, create = false, options = {})
121
- zf = ::Zip::File.new(file_name, create, false, options)
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)
111
+
122
112
  return zf unless block_given?
113
+
123
114
  begin
124
115
  yield zf
125
116
  ensure
@@ -127,30 +118,32 @@ module Zip
127
118
  end
128
119
  end
129
120
 
130
- # Same as #open. But outputs data to a buffer instead of a file
131
- def add_buffer
132
- io = ::StringIO.new('')
133
- zf = ::Zip::File.new(io, true, true)
134
- yield zf
135
- zf.write_buffer(io)
136
- end
137
-
138
121
  # Like #open, but reads zip archive contents from a String or open IO
139
122
  # stream, and outputs data to a buffer.
140
123
  # (This can be used to extract data from a
141
124
  # downloaded zip archive without first saving it to disk.)
142
- def open_buffer(io, options = {})
143
- unless IO_METHODS.map { |method| io.respond_to?(method) }.all? || io.is_a?(String)
144
- raise "Zip::File.open_buffer expects a String or IO-like argument (responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
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)
131
+ unless IO_METHODS.map { |method| io.respond_to?(method) }.all? || io.kind_of?(String)
132
+ raise 'Zip::File.open_buffer expects a String or IO-like argument' \
133
+ "(responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
145
134
  end
146
135
 
147
- io = ::StringIO.new(io) if io.is_a?(::String)
136
+ io = ::StringIO.new(io) if io.kind_of?(::String)
148
137
 
149
- # https://github.com/rubyzip/rubyzip/issues/119
150
- 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)
151
144
 
152
- zf = ::Zip::File.new(io, true, true, options)
153
145
  return zf unless block_given?
146
+
154
147
  yield zf
155
148
 
156
149
  begin
@@ -166,93 +159,32 @@ module Zip
166
159
  # whereas ZipInputStream jumps through the entire archive accessing the
167
160
  # local entry headers (which contain the same information as the
168
161
  # central directory).
169
- def foreach(aZipFileName, &block)
170
- open(aZipFileName) do |zipFile|
171
- zipFile.each(&block)
172
- end
173
- end
174
-
175
- def get_segment_size_for_split(segment_size)
176
- if MIN_SEGMENT_SIZE > segment_size
177
- MIN_SEGMENT_SIZE
178
- elsif MAX_SEGMENT_SIZE < segment_size
179
- MAX_SEGMENT_SIZE
180
- else
181
- segment_size
162
+ def foreach(zip_file_name, &block)
163
+ ::Zip::File.open(zip_file_name) do |zip_file|
164
+ zip_file.each(&block)
182
165
  end
183
166
  end
184
167
 
185
- def get_partial_zip_file_name(zip_file_name, partial_zip_file_name)
186
- unless partial_zip_file_name.nil?
187
- partial_zip_file_name = zip_file_name.sub(/#{::File.basename(zip_file_name)}\z/,
188
- partial_zip_file_name + ::File.extname(zip_file_name))
189
- end
190
- partial_zip_file_name ||= zip_file_name
191
- partial_zip_file_name
192
- end
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
193
172
 
194
- def get_segment_count_for_split(zip_file_size, segment_size)
195
- (zip_file_size / segment_size).to_i + (zip_file_size % segment_size == 0 ? 0 : 1)
196
- end
197
-
198
- def put_split_signature(szip_file, segment_size)
199
- signature_packed = [SPLIT_SIGNATURE].pack('V')
200
- szip_file << signature_packed
201
- segment_size - signature_packed.size
202
- end
203
-
204
- #
205
- # TODO: Make the code more understandable
206
- #
207
- def save_splited_part(zip_file, partial_zip_file_name, zip_file_size, szip_file_index, segment_size, segment_count)
208
- ssegment_size = zip_file_size - zip_file.pos
209
- ssegment_size = segment_size if ssegment_size > segment_size
210
- szip_file_name = "#{partial_zip_file_name}.#{format('%03d', szip_file_index)}"
211
- ::File.open(szip_file_name, 'wb') do |szip_file|
212
- if szip_file_index == 1
213
- ssegment_size = put_split_signature(szip_file, segment_size)
214
- end
215
- chunk_bytes = 0
216
- until ssegment_size == chunk_bytes || zip_file.eof?
217
- segment_bytes_left = ssegment_size - chunk_bytes
218
- buffer_size = segment_bytes_left < DATA_BUFFER_SIZE ? segment_bytes_left : DATA_BUFFER_SIZE
219
- chunk = zip_file.read(buffer_size)
220
- chunk_bytes += buffer_size
221
- szip_file << chunk
222
- # Info for track splitting
223
- yield segment_count, szip_file_index, chunk_bytes, ssegment_size if block_given?
224
- end
225
- end
226
- end
227
-
228
- # Splits an archive into parts with segment size
229
- def split(zip_file_name, segment_size = MAX_SEGMENT_SIZE, delete_zip_file = true, partial_zip_file_name = nil)
230
- raise Error, "File #{zip_file_name} not found" unless ::File.exist?(zip_file_name)
231
- raise Errno::ENOENT, zip_file_name unless ::File.readable?(zip_file_name)
232
- zip_file_size = ::File.size(zip_file_name)
233
- segment_size = get_segment_size_for_split(segment_size)
234
- return if zip_file_size <= segment_size
235
- segment_count = get_segment_count_for_split(zip_file_size, segment_size)
236
- # Checking for correct zip structure
237
- open(zip_file_name) {}
238
- partial_zip_file_name = get_partial_zip_file_name(zip_file_name, partial_zip_file_name)
239
- szip_file_index = 0
240
- ::File.open(zip_file_name, 'rb') do |zip_file|
241
- until zip_file.eof?
242
- szip_file_index += 1
243
- 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)
244
176
  end
177
+ else
178
+ cdir.count_entries(path_or_io)
245
179
  end
246
- ::File.delete(zip_file_name) if delete_zip_file
247
- szip_file_index
248
180
  end
249
181
  end
250
182
 
251
183
  # Returns an input stream to the specified entry. If a block is passed
252
184
  # the stream object is passed to the block and the stream is automatically
253
185
  # closed afterwards just as with ruby's builtin File.open method.
254
- def get_input_stream(entry, &aProc)
255
- get_entry(entry).get_input_stream(&aProc)
186
+ def get_input_stream(entry, &a_proc)
187
+ get_entry(entry).get_input_stream(&a_proc)
256
188
  end
257
189
 
258
190
  # Returns an output stream to the specified entry. If entry is not an instance
@@ -260,21 +192,29 @@ module Zip
260
192
  # specified. If a block is passed the stream object is passed to the block and
261
193
  # the stream is automatically closed afterwards just as with ruby's builtin
262
194
  # File.open method.
263
- def get_output_stream(entry, permission_int = nil, comment = nil, extra = nil, compressed_size = nil, crc = nil, compression_method = nil, size = nil, time = nil, &aProc)
195
+ def get_output_stream(entry, permissions: nil, comment: nil,
196
+ extra: nil, compressed_size: nil, crc: nil,
197
+ compression_method: nil, compression_level: nil,
198
+ size: nil, time: nil, &a_proc)
264
199
  new_entry =
265
200
  if entry.kind_of?(Entry)
266
201
  entry
267
202
  else
268
- Entry.new(@name, entry.to_s, comment, extra, compressed_size, crc, compression_method, size, 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
+ )
269
209
  end
270
210
  if new_entry.directory?
271
211
  raise ArgumentError,
272
212
  "cannot open stream to directory entry - '#{new_entry}'"
273
213
  end
274
- new_entry.unix_perms = permission_int
214
+ new_entry.unix_perms = permissions
275
215
  zip_streamable_entry = StreamableStream.new(new_entry)
276
- @entry_set << zip_streamable_entry
277
- zip_streamable_entry.get_output_stream(&aProc)
216
+ @cdir << zip_streamable_entry
217
+ zip_streamable_entry.get_output_stream(&a_proc)
278
218
  end
279
219
 
280
220
  # Returns the name of the zip archive
@@ -284,77 +224,92 @@ module Zip
284
224
 
285
225
  # Returns a string containing the contents of the specified entry
286
226
  def read(entry)
287
- get_input_stream(entry) { |is| is.read }
227
+ get_input_stream(entry, &:read)
288
228
  end
289
229
 
290
230
  # Convenience method for adding the contents of a file to the archive
291
231
  def add(entry, src_path, &continue_on_exists_proc)
292
232
  continue_on_exists_proc ||= proc { ::Zip.continue_on_exists_proc }
293
233
  check_entry_exists(entry, continue_on_exists_proc, 'add')
294
- 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
295
242
  new_entry.gather_fileinfo_from_srcpath(src_path)
296
- new_entry.dirty = true
297
- @entry_set << new_entry
243
+ @cdir << new_entry
298
244
  end
299
245
 
300
246
  # Convenience method for adding the contents of a file to the archive
301
247
  # in Stored format (uncompressed)
302
248
  def add_stored(entry, src_path, &continue_on_exists_proc)
303
- 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
+ )
304
252
  add(entry, src_path, &continue_on_exists_proc)
305
253
  end
306
254
 
307
255
  # Removes the specified entry.
308
256
  def remove(entry)
309
- @entry_set.delete(get_entry(entry))
257
+ @cdir.delete(get_entry(entry))
310
258
  end
311
259
 
312
260
  # Renames the specified entry.
313
261
  def rename(entry, new_name, &continue_on_exists_proc)
314
- foundEntry = get_entry(entry)
262
+ found_entry = get_entry(entry)
315
263
  check_entry_exists(new_name, continue_on_exists_proc, 'rename')
316
- @entry_set.delete(foundEntry)
317
- foundEntry.name = new_name
318
- @entry_set << foundEntry
264
+ @cdir.delete(found_entry)
265
+ found_entry.name = new_name
266
+ @cdir << found_entry
319
267
  end
320
268
 
321
- # Replaces the specified entry with the contents of srcPath (from
269
+ # Replaces the specified entry with the contents of src_path (from
322
270
  # the file system).
323
- def replace(entry, srcPath)
324
- check_file(srcPath)
271
+ def replace(entry, src_path)
272
+ check_file(src_path)
325
273
  remove(entry)
326
- add(entry, srcPath)
274
+ add(entry, src_path)
327
275
  end
328
276
 
329
- # Extracts entry to file dest_path.
330
- def extract(entry, dest_path, &block)
277
+ # Extracts `entry` to a file at `entry_path`, with `destination_directory`
278
+ # as the base location in the filesystem.
279
+ #
280
+ # NB: The caller is responsible for making sure `destination_directory` is
281
+ # safe, if it is passed.
282
+ def extract(entry, entry_path = nil, destination_directory: '.', &block)
331
283
  block ||= proc { ::Zip.on_exists_proc }
332
284
  found_entry = get_entry(entry)
333
- found_entry.extract(dest_path, &block)
285
+ entry_path ||= found_entry.name
286
+ found_entry.extract(entry_path, destination_directory: destination_directory, &block)
334
287
  end
335
288
 
336
289
  # Commits changes that has been made since the previous commit to
337
290
  # the zip archive.
338
291
  def commit
339
- return if name.is_a?(StringIO) || !commit_required?
292
+ return if name.kind_of?(StringIO) || !commit_required?
293
+
340
294
  on_success_replace do |tmp_file|
341
- ::Zip::OutputStream.open(tmp_file) do |zos|
342
- @entry_set.each do |e|
295
+ ::Zip::OutputStream.open(tmp_file, suppress_extra_fields: @suppress_extra_fields) do |zos|
296
+ @cdir.each do |e|
343
297
  e.write_to_zip_output_stream(zos)
344
- e.dirty = false
345
298
  e.clean_up
346
299
  end
347
300
  zos.comment = comment
348
301
  end
349
302
  true
350
303
  end
351
- initialize(name)
304
+ initialize_cdir(@name)
352
305
  end
353
306
 
354
307
  # Write buffer write changes to buffer and return
355
- def write_buffer(io = ::StringIO.new(''))
356
- ::Zip::OutputStream.write_buffer(io) do |zos|
357
- @entry_set.each { |e| e.write_to_zip_output_stream(zos) }
308
+ def write_buffer(io = ::StringIO.new)
309
+ return io unless commit_required?
310
+
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) }
358
313
  zos.comment = comment
359
314
  end
360
315
  end
@@ -367,16 +322,19 @@ module Zip
367
322
  # Returns true if any changes has been made to this archive since
368
323
  # the previous commit
369
324
  def commit_required?
370
- @entry_set.each do |e|
371
- return true if e.dirty
325
+ return true if @create || @cdir.dirty?
326
+
327
+ @cdir.each do |e|
328
+ return true if e.dirty?
372
329
  end
373
- @comment != @stored_comment || @entry_set != @stored_entries || @create
330
+
331
+ false
374
332
  end
375
333
 
376
334
  # Searches for entry with the specified name. Returns nil if
377
335
  # no entry is found. See also get_entry
378
336
  def find_entry(entry_name)
379
- selected_entry = @entry_set.find_entry(entry_name)
337
+ selected_entry = @cdir.find_entry(entry_name)
380
338
  return if selected_entry.nil?
381
339
 
382
340
  selected_entry.restore_ownership = @restore_ownership
@@ -385,11 +343,6 @@ module Zip
385
343
  selected_entry
386
344
  end
387
345
 
388
- # Searches for entries given a glob
389
- def glob(*args, &block)
390
- @entry_set.glob(*args, &block)
391
- end
392
-
393
346
  # Searches for an entry just as find_entry, but throws Errno::ENOENT
394
347
  # if no entry is found.
395
348
  def get_entry(entry)
@@ -400,36 +353,55 @@ module Zip
400
353
  end
401
354
 
402
355
  # Creates a directory
403
- def mkdir(entryName, permissionInt = 0o755)
404
- raise Errno::EEXIST, "File exists - #{entryName}" if find_entry(entryName)
405
- entryName = entryName.dup.to_s
406
- entryName << '/' unless entryName.end_with?('/')
407
- @entry_set << ::Zip::StreamableDirectory.new(@name, entryName, nil, permissionInt)
356
+ def mkdir(entry_name, permission = 0o755)
357
+ raise Errno::EEXIST, "File exists - #{entry_name}" if find_entry(entry_name)
358
+
359
+ entry_name = entry_name.dup.to_s
360
+ entry_name << '/' unless entry_name.end_with?('/')
361
+ @cdir << ::Zip::StreamableDirectory.new(@name, entry_name, nil, permission)
408
362
  end
409
363
 
410
364
  private
411
365
 
412
- def directory?(newEntry, srcPath)
413
- srcPathIsDirectory = ::File.directory?(srcPath)
414
- if newEntry.directory? && !srcPathIsDirectory
415
- raise ArgumentError,
416
- "entry name '#{newEntry}' indicates directory entry, but " \
417
- "'#{srcPath}' is not a directory"
418
- elsif !newEntry.directory? && srcPathIsDirectory
419
- newEntry.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"
420
395
  end
421
- newEntry.directory? && srcPathIsDirectory
422
396
  end
423
397
 
424
- def check_entry_exists(entryName, continue_on_exists_proc, procedureName)
398
+ def check_entry_exists(entry_name, continue_on_exists_proc, proc_name)
399
+ return unless @cdir.include?(entry_name)
400
+
425
401
  continue_on_exists_proc ||= proc { Zip.continue_on_exists_proc }
426
- return unless @entry_set.include?(entryName)
427
- if continue_on_exists_proc.call
428
- remove get_entry(entryName)
429
- else
430
- raise ::Zip::EntryExistsError,
431
- procedureName + " failed. Entry #{entryName} already exists"
432
- end
402
+ raise ::Zip::EntryExistsError.new proc_name, entry_name unless continue_on_exists_proc.call
403
+
404
+ remove get_entry(entry_name)
433
405
  end
434
406
 
435
407
  def check_file(path)
@@ -439,14 +411,12 @@ module Zip
439
411
  def on_success_replace
440
412
  dirname, basename = ::File.split(name)
441
413
  ::Dir::Tmpname.create(basename, dirname) do |tmp_filename|
442
- begin
443
- if yield tmp_filename
444
- ::File.rename(tmp_filename, name)
445
- ::File.chmod(@file_permissions, name) unless @create
446
- end
447
- ensure
448
- ::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
449
417
  end
418
+ ensure
419
+ FileUtils.rm_f(tmp_filename)
450
420
  end
451
421
  end
452
422
  end