rubyzip 1.3.0 → 3.4.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 (138) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +516 -0
  3. data/LICENSE.md +24 -0
  4. data/README.md +213 -51
  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 +120 -0
  11. data/lib/zip/crypto/decrypted_io.rb +46 -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 +65 -13
  19. data/lib/zip/entry.rb +433 -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 +18 -18
  24. data/lib/zip/extra_field/ntfs.rb +10 -4
  25. data/lib/zip/extra_field/old_unix.rb +6 -2
  26. data/lib/zip/extra_field/universal_time.rb +45 -13
  27. data/lib/zip/extra_field/unix.rb +7 -3
  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 +225 -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 +8 -8
  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 +69 -50
  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 +10 -11
  52. data/lib/zip/version.rb +4 -1
  53. data/lib/zip.rb +32 -7
  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 +85 -177
  63. data/TODO +0 -15
  64. data/lib/zip/extra_field/zip64_placeholder.rb +0 -15
  65. data/test/basic_zip_file_test.rb +0 -60
  66. data/test/case_sensitivity_test.rb +0 -69
  67. data/test/central_directory_entry_test.rb +0 -69
  68. data/test/central_directory_test.rb +0 -100
  69. data/test/crypto/null_encryption_test.rb +0 -57
  70. data/test/crypto/traditional_encryption_test.rb +0 -80
  71. data/test/data/WarnInvalidDate.zip +0 -0
  72. data/test/data/file1.txt +0 -46
  73. data/test/data/file1.txt.deflatedData +0 -0
  74. data/test/data/file2.txt +0 -1504
  75. data/test/data/globTest/foo/bar/baz/foo.txt +0 -0
  76. data/test/data/globTest/foo.txt +0 -0
  77. data/test/data/globTest/food.txt +0 -0
  78. data/test/data/globTest.zip +0 -0
  79. data/test/data/gpbit3stored.zip +0 -0
  80. data/test/data/mimetype +0 -1
  81. data/test/data/notzippedruby.rb +0 -7
  82. data/test/data/ntfs.zip +0 -0
  83. data/test/data/oddExtraField.zip +0 -0
  84. data/test/data/path_traversal/Makefile +0 -10
  85. data/test/data/path_traversal/jwilk/README.md +0 -5
  86. data/test/data/path_traversal/jwilk/absolute1.zip +0 -0
  87. data/test/data/path_traversal/jwilk/absolute2.zip +0 -0
  88. data/test/data/path_traversal/jwilk/dirsymlink.zip +0 -0
  89. data/test/data/path_traversal/jwilk/dirsymlink2a.zip +0 -0
  90. data/test/data/path_traversal/jwilk/dirsymlink2b.zip +0 -0
  91. data/test/data/path_traversal/jwilk/relative0.zip +0 -0
  92. data/test/data/path_traversal/jwilk/relative2.zip +0 -0
  93. data/test/data/path_traversal/jwilk/symlink.zip +0 -0
  94. data/test/data/path_traversal/relative1.zip +0 -0
  95. data/test/data/path_traversal/tilde.zip +0 -0
  96. data/test/data/path_traversal/tuzovakaoff/README.md +0 -3
  97. data/test/data/path_traversal/tuzovakaoff/absolutepath.zip +0 -0
  98. data/test/data/path_traversal/tuzovakaoff/symlink.zip +0 -0
  99. data/test/data/rubycode.zip +0 -0
  100. data/test/data/rubycode2.zip +0 -0
  101. data/test/data/test.xls +0 -0
  102. data/test/data/testDirectory.bin +0 -0
  103. data/test/data/zip64-sample.zip +0 -0
  104. data/test/data/zipWithDirs.zip +0 -0
  105. data/test/data/zipWithEncryption.zip +0 -0
  106. data/test/deflater_test.rb +0 -65
  107. data/test/encryption_test.rb +0 -42
  108. data/test/entry_set_test.rb +0 -163
  109. data/test/entry_test.rb +0 -154
  110. data/test/errors_test.rb +0 -35
  111. data/test/extra_field_test.rb +0 -76
  112. data/test/file_extract_directory_test.rb +0 -54
  113. data/test/file_extract_test.rb +0 -145
  114. data/test/file_permissions_test.rb +0 -65
  115. data/test/file_split_test.rb +0 -57
  116. data/test/file_test.rb +0 -666
  117. data/test/filesystem/dir_iterator_test.rb +0 -58
  118. data/test/filesystem/directory_test.rb +0 -139
  119. data/test/filesystem/file_mutating_test.rb +0 -87
  120. data/test/filesystem/file_nonmutating_test.rb +0 -508
  121. data/test/filesystem/file_stat_test.rb +0 -64
  122. data/test/gentestfiles.rb +0 -126
  123. data/test/inflater_test.rb +0 -14
  124. data/test/input_stream_test.rb +0 -182
  125. data/test/ioextras/abstract_input_stream_test.rb +0 -102
  126. data/test/ioextras/abstract_output_stream_test.rb +0 -106
  127. data/test/ioextras/fake_io_test.rb +0 -18
  128. data/test/local_entry_test.rb +0 -154
  129. data/test/output_stream_test.rb +0 -128
  130. data/test/pass_thru_compressor_test.rb +0 -30
  131. data/test/pass_thru_decompressor_test.rb +0 -14
  132. data/test/path_traversal_test.rb +0 -141
  133. data/test/samples/example_recursive_test.rb +0 -37
  134. data/test/settings_test.rb +0 -95
  135. data/test/test_helper.rb +0 -234
  136. data/test/unicode_file_names_and_comments_test.rb +0 -62
  137. data/test/zip64_full_test.rb +0 -51
  138. data/test/zip64_support_test.rb +0 -14
data/lib/zip/file.rb CHANGED
@@ -1,115 +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]
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
53
56
 
57
+ IO_METHODS = [:tell, :seek, :read, :eof, :close].freeze # :nodoc:
58
+
59
+ # The name of this zip archive.
54
60
  attr_reader :name
55
61
 
56
- # default -> false
62
+ # default -> false.
57
63
  attr_accessor :restore_ownership
58
- # default -> false
64
+
65
+ # default -> true.
59
66
  attr_accessor :restore_permissions
60
- # default -> true
67
+
68
+ # default -> true.
61
69
  attr_accessor :restore_times
62
- # Returns the zip files comment, if it has one
63
- attr_accessor :comment
64
70
 
65
- # Opens a zip archive. Pass true as the second parameter to create
71
+ def_delegators :@cdir, :comment, :comment=, :each, :entries, :glob, :size
72
+
73
+ # Opens a zip archive. Pass create: true to create
66
74
  # a new archive if it doesn't exist already.
67
- 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)
68
81
  super()
82
+
69
83
  @name = path_or_io.respond_to?(:path) ? path_or_io.path : path_or_io
70
- @comment = ''
71
84
  @create = create ? true : false # allow any truthy value to mean true
72
85
 
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
86
+ initialize_cdir(path_or_io, buffer: buffer)
98
87
 
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
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
104
93
  end
105
94
 
106
95
  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)
96
+ # Similar to ::new. If a block is passed the Zip::File object is passed
97
+ # to the block and is automatically closed afterwards, just as with
98
+ # ruby's builtin File::open method.
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
+
112
112
  return zf unless block_given?
113
+
113
114
  begin
114
115
  yield zf
115
116
  ensure
@@ -117,30 +118,32 @@ module Zip
117
118
  end
118
119
  end
119
120
 
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
121
  # Like #open, but reads zip archive contents from a String or open IO
129
122
  # stream, and outputs data to a buffer.
130
123
  # (This can be used to extract data from a
131
124
  # 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}"
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}"
135
134
  end
136
135
 
137
- io = ::StringIO.new(io) if io.is_a?(::String)
136
+ io = ::StringIO.new(io) if io.kind_of?(::String)
138
137
 
139
- # https://github.com/rubyzip/rubyzip/issues/119
140
- 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)
141
144
 
142
- zf = ::Zip::File.new(io, true, true, options)
143
145
  return zf unless block_given?
146
+
144
147
  yield zf
145
148
 
146
149
  begin
@@ -156,93 +159,32 @@ module Zip
156
159
  # whereas ZipInputStream jumps through the entire archive accessing the
157
160
  # local entry headers (which contain the same information as the
158
161
  # central directory).
159
- def foreach(aZipFileName, &block)
160
- open(aZipFileName) do |zipFile|
161
- zipFile.each(&block)
162
+ def foreach(zip_file_name, &block)
163
+ ::Zip::File.open(zip_file_name) do |zip_file|
164
+ zip_file.each(&block)
162
165
  end
163
166
  end
164
167
 
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
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
174
172
 
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)
173
+ if path_or_io.kind_of?(String)
174
+ ::File.open(path_or_io, 'rb') do |f|
175
+ cdir.count_entries(f)
234
176
  end
177
+ else
178
+ cdir.count_entries(path_or_io)
235
179
  end
236
- ::File.delete(zip_file_name) if delete_zip_file
237
- szip_file_index
238
180
  end
239
181
  end
240
182
 
241
183
  # Returns an input stream to the specified entry. If a block is passed
242
184
  # the stream object is passed to the block and the stream is automatically
243
185
  # 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)
186
+ def get_input_stream(entry, &a_proc)
187
+ get_entry(entry).get_input_stream(&a_proc)
246
188
  end
247
189
 
248
190
  # Returns an output stream to the specified entry. If entry is not an instance
@@ -250,21 +192,29 @@ module Zip
250
192
  # specified. If a block is passed the stream object is passed to the block and
251
193
  # the stream is automatically closed afterwards just as with ruby's builtin
252
194
  # 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)
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)
254
199
  new_entry =
255
200
  if entry.kind_of?(Entry)
256
201
  entry
257
202
  else
258
- 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
+ )
259
209
  end
260
210
  if new_entry.directory?
261
211
  raise ArgumentError,
262
212
  "cannot open stream to directory entry - '#{new_entry}'"
263
213
  end
264
- new_entry.unix_perms = permission_int
214
+ new_entry.unix_perms = permissions
265
215
  zip_streamable_entry = StreamableStream.new(new_entry)
266
- @entry_set << zip_streamable_entry
267
- zip_streamable_entry.get_output_stream(&aProc)
216
+ @cdir << zip_streamable_entry
217
+ zip_streamable_entry.get_output_stream(&a_proc)
268
218
  end
269
219
 
270
220
  # Returns the name of the zip archive
@@ -274,77 +224,92 @@ module Zip
274
224
 
275
225
  # Returns a string containing the contents of the specified entry
276
226
  def read(entry)
277
- get_input_stream(entry) { |is| is.read }
227
+ get_input_stream(entry, &:read)
278
228
  end
279
229
 
280
230
  # Convenience method for adding the contents of a file to the archive
281
231
  def add(entry, src_path, &continue_on_exists_proc)
282
232
  continue_on_exists_proc ||= proc { ::Zip.continue_on_exists_proc }
283
233
  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)
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
285
242
  new_entry.gather_fileinfo_from_srcpath(src_path)
286
- new_entry.dirty = true
287
- @entry_set << new_entry
243
+ @cdir << new_entry
288
244
  end
289
245
 
290
246
  # Convenience method for adding the contents of a file to the archive
291
247
  # in Stored format (uncompressed)
292
248
  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)
249
+ entry = ::Zip::Entry.new(
250
+ @name, entry.to_s, compression_method: ::Zip::Entry::STORED
251
+ )
294
252
  add(entry, src_path, &continue_on_exists_proc)
295
253
  end
296
254
 
297
255
  # Removes the specified entry.
298
256
  def remove(entry)
299
- @entry_set.delete(get_entry(entry))
257
+ @cdir.delete(get_entry(entry))
300
258
  end
301
259
 
302
260
  # Renames the specified entry.
303
261
  def rename(entry, new_name, &continue_on_exists_proc)
304
- foundEntry = get_entry(entry)
262
+ found_entry = get_entry(entry)
305
263
  check_entry_exists(new_name, continue_on_exists_proc, 'rename')
306
- @entry_set.delete(foundEntry)
307
- foundEntry.name = new_name
308
- @entry_set << foundEntry
264
+ @cdir.delete(found_entry)
265
+ found_entry.name = new_name
266
+ @cdir << found_entry
309
267
  end
310
268
 
311
- # Replaces the specified entry with the contents of srcPath (from
269
+ # Replaces the specified entry with the contents of src_path (from
312
270
  # the file system).
313
- def replace(entry, srcPath)
314
- check_file(srcPath)
271
+ def replace(entry, src_path)
272
+ check_file(src_path)
315
273
  remove(entry)
316
- add(entry, srcPath)
274
+ add(entry, src_path)
317
275
  end
318
276
 
319
- # Extracts entry to file dest_path.
320
- 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)
321
283
  block ||= proc { ::Zip.on_exists_proc }
322
284
  found_entry = get_entry(entry)
323
- found_entry.extract(dest_path, &block)
285
+ entry_path ||= found_entry.name
286
+ found_entry.extract(entry_path, destination_directory: destination_directory, &block)
324
287
  end
325
288
 
326
289
  # Commits changes that has been made since the previous commit to
327
290
  # the zip archive.
328
291
  def commit
329
- return if name.is_a?(StringIO) || !commit_required?
292
+ return if name.kind_of?(StringIO) || !commit_required?
293
+
330
294
  on_success_replace do |tmp_file|
331
- ::Zip::OutputStream.open(tmp_file) do |zos|
332
- @entry_set.each do |e|
295
+ ::Zip::OutputStream.open(tmp_file, suppress_extra_fields: @suppress_extra_fields) do |zos|
296
+ @cdir.each do |e|
333
297
  e.write_to_zip_output_stream(zos)
334
- e.dirty = false
335
298
  e.clean_up
336
299
  end
337
300
  zos.comment = comment
338
301
  end
339
302
  true
340
303
  end
341
- initialize(name)
304
+ initialize_cdir(@name)
342
305
  end
343
306
 
344
307
  # Write buffer write changes to buffer and return
345
- def write_buffer(io = ::StringIO.new(''))
346
- ::Zip::OutputStream.write_buffer(io) do |zos|
347
- @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) }
348
313
  zos.comment = comment
349
314
  end
350
315
  end
@@ -357,65 +322,86 @@ module Zip
357
322
  # Returns true if any changes has been made to this archive since
358
323
  # the previous commit
359
324
  def commit_required?
360
- @entry_set.each do |e|
361
- return true if e.dirty
325
+ return true if @create || @cdir.dirty?
326
+
327
+ @cdir.each do |e|
328
+ return true if e.dirty?
362
329
  end
363
- @comment != @stored_comment || @entry_set != @stored_entries || @create
330
+
331
+ false
364
332
  end
365
333
 
366
334
  # Searches for entry with the specified name. Returns nil if
367
335
  # no entry is found. See also get_entry
368
336
  def find_entry(entry_name)
369
- @entry_set.find_entry(entry_name)
370
- end
337
+ selected_entry = @cdir.find_entry(entry_name)
338
+ return if selected_entry.nil?
371
339
 
372
- # Searches for entries given a glob
373
- def glob(*args, &block)
374
- @entry_set.glob(*args, &block)
340
+ selected_entry.restore_ownership = @restore_ownership
341
+ selected_entry.restore_permissions = @restore_permissions
342
+ selected_entry.restore_times = @restore_times
343
+ selected_entry
375
344
  end
376
345
 
377
346
  # Searches for an entry just as find_entry, but throws Errno::ENOENT
378
347
  # if no entry is found.
379
348
  def get_entry(entry)
380
349
  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
350
+ raise Errno::ENOENT, entry if selected_entry.nil?
351
+
385
352
  selected_entry
386
353
  end
387
354
 
388
355
  # 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)
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)
394
362
  end
395
363
 
396
364
  private
397
365
 
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 += '/'
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"
406
395
  end
407
- newEntry.directory? && srcPathIsDirectory
408
396
  end
409
397
 
410
- 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
+
411
401
  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
402
+ raise ::Zip::EntryExistsError.new proc_name, entry_name unless continue_on_exists_proc.call
403
+
404
+ remove get_entry(entry_name)
419
405
  end
420
406
 
421
407
  def check_file(path)
@@ -425,14 +411,12 @@ module Zip
425
411
  def on_success_replace
426
412
  dirname, basename = ::File.split(name)
427
413
  ::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)
414
+ if yield tmp_filename
415
+ ::File.rename(tmp_filename, name)
416
+ ::File.chmod(@file_permissions, name) unless @create
435
417
  end
418
+ ensure
419
+ FileUtils.rm_f(tmp_filename)
436
420
  end
437
421
  end
438
422
  end