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