rubyzip 3.1.0 → 3.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f572a7ce284e7e1dcdd10b0ba8e278c862957e9ac47d1b00dad5ea26db6fcba
4
- data.tar.gz: 8afd167d9dcd0b902d79d1dc4111964c3edb75739419cd30cff9ebc36c38bdc7
3
+ metadata.gz: 169fa7d5832e775e15c2f8a409a35e9da32c0177117039f089f9fa1646aa281e
4
+ data.tar.gz: 9b4fb8929262cb097e3631ad8d8a9efa98d691488ce79e37092df4de27204aea
5
5
  SHA512:
6
- metadata.gz: e09c8b007382e0243cdb57d63560d5b6c8ef9a153c8dafc35a1c284ad41dc49c5afc284cf6175ffc311691e98dcdd81221cee8f2d1b060a665ade5de11d00c58
7
- data.tar.gz: 306dfe485b10b421d4232c44314bd609c14004b3f0b1b8d76d7ef78234f6a34dc76eae38126a53ac34553f7538a87a807991be1d5b06b8788a81bd8eba723fdc
6
+ metadata.gz: 4bc8630670298d013486cf4632084554a10ed2ba327c2b54f65c105602f31ca84b87a42976fa1a606226d7cc5b88ecbc356f8a0932a9a53880e0b68c4b7f6275
7
+ data.tar.gz: 4cc717180899d18767c73913fa16abaafeb3624167fc2c4e14a43dde406eef815d9791e84a7bb9f018a255e32d08109dc01efdd8844cc1503a85ac8d931d4b22
data/Changelog.md CHANGED
@@ -1,3 +1,27 @@
1
+ # 3.2.0 (2025-10-14)
2
+
3
+ - Add option to suppress extra fields. [#653](https://github.com/rubyzip/rubyzip/pull/653) (fixes [#34](https://github.com/rubyzip/rubyzip/issues/34), [#398](https://github.com/rubyzip/rubyzip/issues/398) and [#648](https://github.com/rubyzip/rubyzip/issues/648))
4
+
5
+ Tooling/internal:
6
+
7
+ - Entry: clean up reading and writing the Central Directory headers.
8
+ - Improve Zip64 tests for `OutputStream`.
9
+ - Extra fields: use symbols as indices as opposed to strings.
10
+ - Ensure that `Unknown` extra field has a superclass.
11
+
12
+ # 3.1.1 (2025-09-26)
13
+
14
+ - Improve the IO pipeline when decompressing. [#649](https://github.com/rubyzip/rubyzip/pull/649) (which also fixes [#647](https://github.com/rubyzip/rubyzip/issues/647))
15
+
16
+ Tooling/internal:
17
+
18
+ - Improve the `DecryptedIo` class with various updates and optimizations.
19
+ - Remove the `NullDecrypter` class.
20
+ - Properly convert the test suite to use minitest.
21
+ - Move all test helper code into separate files.
22
+ - Updates to the Actions CI, including new OS versions.
23
+ - Update rubocop versions and fix resultant cop failures. [#646](https://github.com/rubyzip/rubyzip/pull/646)
24
+
1
25
  # 3.1.0 (2025-09-06)
2
26
 
3
27
  - Support AES decryption. [#579](https://github.com/rubyzip/rubyzip/pull/579) and [#645](https://github.com/rubyzip/rubyzip/pull/645)
data/README.md CHANGED
@@ -3,7 +3,8 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/rubyzip.svg)](http://badge.fury.io/rb/rubyzip)
4
4
  [![Tests](https://github.com/rubyzip/rubyzip/actions/workflows/tests.yml/badge.svg)](https://github.com/rubyzip/rubyzip/actions/workflows/tests.yml)
5
5
  [![Linter](https://github.com/rubyzip/rubyzip/actions/workflows/lint.yml/badge.svg)](https://github.com/rubyzip/rubyzip/actions/workflows/lint.yml)
6
- [![Code Climate](https://codeclimate.com/github/rubyzip/rubyzip.svg)](https://codeclimate.com/github/rubyzip/rubyzip)
6
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop/rubocop)
7
+ [![Maintainability](https://qlty.sh/gh/rubyzip/projects/rubyzip/maintainability.svg)](https://qlty.sh/gh/rubyzip/projects/rubyzip)
7
8
  [![Coverage Status](https://img.shields.io/coveralls/rubyzip/rubyzip.svg)](https://coveralls.io/r/rubyzip/rubyzip?branch=master)
8
9
 
9
10
  Rubyzip is a ruby library for reading and writing zip files.
@@ -66,6 +67,48 @@ Zip::File.open(zipfile_name, create: true) do |zipfile|
66
67
  end
67
68
  ```
68
69
 
70
+ ### Creating a Zip file with `Zip::OutputStream`
71
+
72
+ ```ruby
73
+ require 'rubygems'
74
+ require 'zip'
75
+
76
+ Zip::OutputStream.open('archive.zip') do |zos|
77
+ # Quick.
78
+ zos.put_next_entry('greeting.txt')
79
+ zos << 'Hello, World!'
80
+
81
+ # More control.
82
+ # You MUST NOT make any calls on your `Entry` after calling `put_next_entry`.
83
+ entry = Zip::Entry.new(nil, 'parting.txt')
84
+ entry.atime = Time.now
85
+ zos.put_next_entry(entry)
86
+ zos.write('TTFN')
87
+ end
88
+ ```
89
+
90
+ You can generate a Zip archive in memory using `Zip::OutputStream.write_buffer`.
91
+
92
+ ### Suppressing extra fields
93
+
94
+ If you wish to suppress extra fields from being added to your entries, you can do so by passing the `suppress_extra_fields` parameter to any of the archive opening calls within `Zip::File` or `Zip::OutputStream`, e.g.:
95
+
96
+ ```ruby
97
+ # Suppress all extra fields.
98
+ Zip::File.open('archive.zip', create: true, suppress_extra_fields: true)
99
+ Zip::OutputStream.open('archive.zip', suppress_extra_fields: true)
100
+
101
+ # Suppress an individual extra field.
102
+ Zip::File.open('archive.zip', create: true, suppress_extra_fields: :zip64)
103
+ Zip::OutputStream.open('archive.zip', suppress_extra_fields: :zip64)
104
+
105
+ # Suppress multiple extra fields.
106
+ Zip::File.open('archive.zip', create: true, suppress_extra_fields: [:ntfs, :zip64])
107
+ Zip::OutputStream.open('archive.zip', suppress_extra_fields: [:ntfs, :zip64])
108
+ ```
109
+
110
+ Note that there are some extra fields that cannot be suppressed at all (e.g. `:aes`), and some which will only be suppressed if it is safe to do so (e.g. `:zip64`).
111
+
69
112
  ### Zipping a directory recursively
70
113
 
71
114
  Copy from [here](https://github.com/rubyzip/rubyzip/blob/9d891f7353e66052283562d3e252fe380bb4b199/samples/example_recursive.rb)
@@ -179,7 +222,7 @@ Zip::File.open('foo.zip') do |zip_file|
179
222
  end
180
223
  ```
181
224
 
182
- ### Notes on `Zip::InputStream`
225
+ ### Reading a Zip file with `Zip::InputStream`
183
226
 
184
227
  `Zip::InputStream` can be used for faster reading of zip file content because it does not read the Central directory up front.
185
228
 
@@ -189,14 +232,14 @@ There is one exception where it can not work however, and this is if the file do
189
232
 
190
233
  If `Zip::InputStream` finds such an entry in the zip archive it will raise an exception (`Zip::StreamingError`).
191
234
 
192
- `Zip::InputStream` is not designed to be used for random access in a zip file. When performing any operations on an entry that you are accessing via `Zip::InputStream.get_next_entry` then you should complete any such operations before the next call to `get_next_entry`.
235
+ `Zip::InputStream` is not designed to be used for random access in a zip file. When performing any operations on an entry that you are accessing via `Zip::InputStream#get_next_entry` then you should complete any such operations before the next call to `get_next_entry`.
193
236
 
194
237
  ```ruby
195
- zip_stream = Zip::InputStream.new(File.open('file.zip'))
196
-
197
- while entry = zip_stream.get_next_entry
198
- # All required operations on `entry` go here.
199
- end
238
+ Zip::InputStream.open('file.zip') do |zip_stream|
239
+ while entry = zip_stream.get_next_entry
240
+ # All required operations on `entry` go here.
241
+ end
242
+ end # The `InputStream` is closed at the end of the block.
200
243
  ```
201
244
 
202
245
  Any attempt to move about in a zip file opened with `Zip::InputStream` could result in the incorrect entry being accessed and/or Zlib buffer errors. If you need random access in a zip file, use `Zip::File`.
data/Rakefile CHANGED
@@ -1,17 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bundler/gem_tasks'
4
- require 'rake/testtask'
4
+ require 'minitest/test_task'
5
5
  require 'rdoc/task'
6
6
  require 'rubocop/rake_task'
7
7
 
8
8
  task default: :test
9
9
 
10
- Rake::TestTask.new(:test) do |test|
11
- test.libs << 'lib'
12
- test.libs << 'test'
13
- test.pattern = 'test/**/*_test.rb'
14
- test.verbose = true
10
+ Minitest::TestTask.create do |test|
11
+ test.framework = 'require "simplecov"'
12
+ test.test_globs = 'test/**/*_test.rb'
15
13
  end
16
14
 
17
15
  RDoc::Task.new do |rdoc|
@@ -39,9 +39,11 @@ module Zip
39
39
  read_central_directory_entries(io)
40
40
  end
41
41
 
42
- def write_to_stream(io) # :nodoc:
42
+ def write_to_stream(io, suppress_extra_fields: false) # :nodoc:
43
43
  cdir_offset = io.tell
44
- @entry_set.each { |entry| entry.write_c_dir_entry(io) }
44
+ @entry_set.each do |entry|
45
+ entry.write_c_dir_entry(io, suppress_extra_fields: suppress_extra_fields)
46
+ end
45
47
  eocd_offset = io.tell
46
48
  cdir_size = eocd_offset - cdir_offset
47
49
  if Zip.write_zip64_support &&
@@ -182,7 +184,7 @@ module Zip
182
184
  next unless entry
183
185
 
184
186
  offset = if entry.zip64?
185
- entry.extra['Zip64'].relative_header_offset
187
+ entry.extra[:zip64].relative_header_offset
186
188
  else
187
189
  entry.local_header_offset
188
190
  end
@@ -112,7 +112,8 @@ module Zip
112
112
  @hmac = OpenSSL::HMAC.new(enc_hmac_key, OpenSSL::Digest.new('SHA1'))
113
113
  end
114
114
 
115
- def check_integrity(auth_code)
115
+ def check_integrity!(io)
116
+ auth_code = io.read(AUTHENTICATION_CODE_LENGTH)
116
117
  raise Error, 'Integrity fault' if @hmac.digest[0...AUTHENTICATION_CODE_LENGTH] != auth_code
117
118
  end
118
119
  end
@@ -7,48 +7,39 @@ module Zip
7
7
  def initialize(io, decrypter, compressed_size)
8
8
  @io = io
9
9
  @decrypter = decrypter
10
- @offset = io.tell
11
- @compressed_size = compressed_size
10
+ @bytes_remaining = compressed_size
11
+ @buffer = +''
12
12
  end
13
13
 
14
14
  def read(length = nil, outbuf = +'')
15
- return (length.nil? || length.zero? ? '' : nil) if eof
15
+ return (length.nil? || length.zero? ? '' : nil) if eof?
16
16
 
17
- while length.nil? || (buffer.bytesize < length)
17
+ while length.nil? || (@buffer.bytesize < length)
18
18
  break if input_finished?
19
19
 
20
- buffer << produce_input
20
+ @buffer << produce_input
21
21
  end
22
22
 
23
- if @decrypter.kind_of?(::Zip::AESDecrypter) && input_finished?
24
- @decrypter.check_integrity(@io.read(::Zip::AESEncryption::AUTHENTICATION_CODE_LENGTH))
25
- end
23
+ @decrypter.check_integrity!(@io) if input_finished?
26
24
 
27
- outbuf.replace(buffer.slice!(0...(length || buffer.bytesize)))
25
+ outbuf.replace(@buffer.slice!(0...(length || @buffer.bytesize)))
28
26
  end
29
27
 
30
28
  private
31
29
 
32
- def eof
33
- buffer.empty? && input_finished?
34
- end
35
-
36
- def buffer
37
- @buffer ||= +''
38
- end
39
-
40
- def pos
41
- @io.tell - @offset
30
+ def eof?
31
+ @buffer.empty? && input_finished?
42
32
  end
43
33
 
44
34
  def input_finished?
45
- @io.eof || pos >= @compressed_size
35
+ !@bytes_remaining.positive?
46
36
  end
47
37
 
48
38
  def produce_input
49
- chunk_size = [CHUNK_SIZE, @compressed_size - pos].min
39
+ chunk_size = [@bytes_remaining, CHUNK_SIZE].min
50
40
  return '' unless chunk_size.positive?
51
41
 
42
+ @bytes_remaining -= chunk_size
52
43
  @decrypter.decrypt(@io.read(chunk_size))
53
44
  end
54
45
  end
@@ -28,16 +28,6 @@ module Zip
28
28
 
29
29
  def reset!; end
30
30
  end
31
-
32
- class NullDecrypter < Decrypter # :nodoc:
33
- include NullEncryption
34
-
35
- def decrypt(data)
36
- data
37
- end
38
-
39
- def reset!(_header); end
40
- end
41
31
  end
42
32
 
43
33
  # Copyright (C) 2002, 2003 Thomas Sondergaard
@@ -86,6 +86,8 @@ module Zip
86
86
  end
87
87
  end
88
88
 
89
+ def check_integrity!(_io); end
90
+
89
91
  private
90
92
 
91
93
  def decode(num)
data/lib/zip/dos_time.rb CHANGED
@@ -21,7 +21,7 @@ module Zip
21
21
  def absolute_time?
22
22
  # If absolute time is not set, we can assume it is an absolute time
23
23
  # because times do have timezone information by default.
24
- @absolute_time.nil? ? true : @absolute_time
24
+ @absolute_time.nil? || @absolute_time
25
25
  end
26
26
 
27
27
  def to_binary_dos_time
@@ -36,7 +36,8 @@ module Zip
36
36
  ((year - 1980) << 9)
37
37
  end
38
38
 
39
- def dos_equals(other)
39
+ # Deprecated. Remove for version 4.
40
+ def dos_equals(other) # rubocop:disable Naming/PredicateMethod
40
41
  warn 'Zip::DOSTime#dos_equals is deprecated. Use `==` instead.'
41
42
  self == other
42
43
  end
data/lib/zip/entry.rb CHANGED
@@ -127,10 +127,10 @@ module Zip
127
127
  # Returns modification time by default.
128
128
  def time(component: :mtime)
129
129
  time =
130
- if @extra['UniversalTime']
131
- @extra['UniversalTime'].send(component)
132
- elsif @extra['NTFS']
133
- @extra['NTFS'].send(component)
130
+ if @extra[:universaltime]
131
+ @extra[:universaltime].send(component)
132
+ elsif @extra[:ntfs]
133
+ @extra[:ntfs].send(component)
134
134
  end
135
135
 
136
136
  # Standard time field in central directory has local time
@@ -155,13 +155,13 @@ module Zip
155
155
  # Sets modification time by default.
156
156
  def time=(value, component: :mtime)
157
157
  @dirty = true
158
- unless @extra.member?('UniversalTime') || @extra.member?('NTFS')
159
- @extra.create('UniversalTime')
158
+ unless @extra.member?(:universaltime) || @extra.member?(:ntfs)
159
+ @extra.create(:universaltime)
160
160
  end
161
161
 
162
162
  value = DOSTime.from_time(value)
163
163
  comp = "#{component}=" unless component.to_s.end_with?('=')
164
- (@extra['UniversalTime'] || @extra['NTFS']).send(comp, value)
164
+ (@extra[:universaltime] || @extra[:ntfs]).send(comp, value)
165
165
  @time = value if component == :mtime
166
166
  end
167
167
 
@@ -179,7 +179,7 @@ module Zip
179
179
 
180
180
  # Does this entry return time fields with accurate timezone information?
181
181
  def absolute_time?
182
- @extra.member?('UniversalTime') || @extra.member?('NTFS')
182
+ @extra.member?(:universaltime) || @extra.member?(:ntfs)
183
183
  end
184
184
 
185
185
  # Return the compression method for this entry.
@@ -200,12 +200,12 @@ module Zip
200
200
 
201
201
  # Does this entry use the ZIP64 extensions?
202
202
  def zip64?
203
- !@extra['Zip64'].nil?
203
+ !@extra[:zip64].nil?
204
204
  end
205
205
 
206
206
  # Is this entry encrypted with AES encryption?
207
207
  def aes?
208
- !@extra['AES'].nil?
208
+ !@extra[:aes].nil?
209
209
  end
210
210
 
211
211
  def file_type_is?(type) # :nodoc:
@@ -392,7 +392,7 @@ module Zip
392
392
  end
393
393
 
394
394
  def pack_local_entry # :nodoc:
395
- zip64 = @extra['Zip64']
395
+ zip64 = @extra[:zip64]
396
396
  [::Zip::LOCAL_ENTRY_SIGNATURE,
397
397
  @version_needed_to_extract, # version needed to extract
398
398
  @gp_flags, # @gp_flags
@@ -406,9 +406,17 @@ module Zip
406
406
  @extra ? @extra.local_size : 0].pack('VvvvvvVVVvv')
407
407
  end
408
408
 
409
- def write_local_entry(io, rewrite: false) # :nodoc:
409
+ def write_local_entry(io, suppress_extra_fields: false, rewrite: false) # :nodoc:
410
410
  prep_local_zip64_extra
411
- verify_local_header_size! if rewrite
411
+
412
+ # If we are rewriting the local header, then we verify that we haven't changed
413
+ # its size. At this point we have to keep extra fields if they are present.
414
+ if rewrite
415
+ verify_local_header_size!
416
+ elsif suppress_extra_fields
417
+ @extra.suppress_fields!(suppress_extra_fields)
418
+ end
419
+
412
420
  @local_header_offset = io.tell
413
421
 
414
422
  io << pack_local_entry
@@ -436,10 +444,7 @@ module Zip
436
444
  _, # diskNumberStart
437
445
  @internal_file_attributes,
438
446
  @external_file_attributes,
439
- @local_header_offset,
440
- @name,
441
- @extra,
442
- @comment = buf.unpack('VCCvvvvvVVVvvvvvVV')
447
+ @local_header_offset = buf.unpack('VCCvvvvvVVVvvvvvVV')
443
448
  end
444
449
 
445
450
  def set_ftype_from_c_dir_entry # :nodoc:
@@ -566,7 +571,7 @@ module Zip
566
571
  end
567
572
 
568
573
  def pack_c_dir_entry # :nodoc:
569
- zip64 = @extra['Zip64']
574
+ zip64 = @extra[:zip64]
570
575
  [
571
576
  @header_signature,
572
577
  @version, # version of encoding software
@@ -585,14 +590,11 @@ module Zip
585
590
  zip64 && zip64.disk_start_number ? 0xFFFF : 0, # disk number start
586
591
  @internal_file_attributes, # file type (binary=0, text=1)
587
592
  @external_file_attributes, # native filesystem attributes
588
- zip64 && zip64.relative_header_offset ? 0xFFFFFFFF : @local_header_offset,
589
- @name,
590
- @extra,
591
- @comment
593
+ zip64 && zip64.relative_header_offset ? 0xFFFFFFFF : @local_header_offset
592
594
  ].pack('VCCvvvvvVVVvvvvvVV')
593
595
  end
594
596
 
595
- def write_c_dir_entry(io) # :nodoc:
597
+ def write_c_dir_entry(io, suppress_extra_fields: false) # :nodoc:
596
598
  prep_cdir_zip64_extra
597
599
 
598
600
  case @fstype
@@ -614,6 +616,7 @@ module Zip
614
616
  end
615
617
  end
616
618
 
619
+ @extra.suppress_fields!(suppress_extra_fields) if suppress_extra_fields
617
620
  io << pack_c_dir_entry
618
621
 
619
622
  io << @name
@@ -799,9 +802,9 @@ module Zip
799
802
  return unless zip64?
800
803
 
801
804
  if for_local_header
802
- @size, @compressed_size = @extra['Zip64'].parse(@size, @compressed_size)
805
+ @size, @compressed_size = @extra[:zip64].parse(@size, @compressed_size)
803
806
  else
804
- @size, @compressed_size, @local_header_offset = @extra['Zip64'].parse(
807
+ @size, @compressed_size, @local_header_offset = @extra[:zip64].parse(
805
808
  @size, @compressed_size, @local_header_offset
806
809
  )
807
810
  end
@@ -810,15 +813,15 @@ module Zip
810
813
  def parse_aes_extra # :nodoc:
811
814
  return unless aes?
812
815
 
813
- if @extra['AES'].vendor_id != 'AE'
814
- raise Error, "Unsupported encryption method #{@extra['AES'].vendor_id}"
816
+ if @extra[:aes].vendor_id != 'AE'
817
+ raise Error, "Unsupported encryption method #{@extra[:aes].vendor_id}"
815
818
  end
816
819
 
817
- unless ::Zip::AESEncryption::VERSIONS.include? @extra['AES'].vendor_version
818
- raise Error, "Unsupported encryption style #{@extra['AES'].vendor_version}"
820
+ unless ::Zip::AESEncryption::VERSIONS.include? @extra[:aes].vendor_version
821
+ raise Error, "Unsupported encryption style #{@extra[:aes].vendor_version}"
819
822
  end
820
823
 
821
- @compression_method = @extra['AES'].compression_method if ftype != :directory
824
+ @compression_method = @extra[:aes].compression_method if ftype != :directory
822
825
  end
823
826
 
824
827
  # For DEFLATED compression *only*: set the general purpose flags 1 and 2 to
@@ -851,7 +854,7 @@ module Zip
851
854
  # If we already have a ZIP64 extra (placeholder) then we must fill it in.
852
855
  if zip64? || @size.nil? || @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
853
856
  @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
854
- zip64 = @extra['Zip64'] || @extra.create('Zip64')
857
+ zip64 = @extra[:zip64] || @extra.create(:zip64)
855
858
 
856
859
  # Local header always includes size and compressed size.
857
860
  zip64.original_size = @size || 0
@@ -865,7 +868,7 @@ module Zip
865
868
  if (@size && @size >= 0xFFFFFFFF) || @compressed_size >= 0xFFFFFFFF ||
866
869
  @local_header_offset >= 0xFFFFFFFF
867
870
  @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
868
- zip64 = @extra['Zip64'] || @extra.create('Zip64')
871
+ zip64 = @extra[:zip64] || @extra.create(:zip64)
869
872
 
870
873
  # Central directory entry entries include whichever fields are necessary.
871
874
  zip64.original_size = @size if @size && @size >= 0xFFFFFFFF
@@ -33,6 +33,11 @@ module Zip
33
33
  @encryption_strength, @compression_method = content.unpack('va2Cv')
34
34
  end
35
35
 
36
+ # We can never suppress the AES extra field as it is needed to read the file.
37
+ def suppress?
38
+ false
39
+ end
40
+
36
41
  def pack_for_local
37
42
  [@vendor_version, @vendor_id,
38
43
  @encryption_strength, @compression_method].pack('va2Cv')
@@ -9,7 +9,7 @@ module Zip
9
9
  end
10
10
 
11
11
  def self.name
12
- @name ||= to_s.split('::')[-1]
12
+ @name ||= to_s.split('::').last.downcase.to_sym
13
13
  end
14
14
 
15
15
  # return field [size, content] or false
@@ -24,6 +24,12 @@ module Zip
24
24
  [binstr[2, 2].unpack1('v'), binstr[4..]]
25
25
  end
26
26
 
27
+ # Default strategy is to suppress all extra fields if we're asked to.
28
+ # Specific extra field types can override this if they need to be kept.
29
+ def suppress?
30
+ true
31
+ end
32
+
27
33
  def to_local_bin
28
34
  s = pack_for_local
29
35
  (self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v')) << s
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'generic'
4
+
3
5
  module Zip
4
6
  # A class to hold unknown extra fields so that they are preserved.
5
- class ExtraField::Unknown # :nodoc:
7
+ class ExtraField::Unknown < ExtraField::Generic # :nodoc:
6
8
  def initialize
7
9
  @local_bin = +''
8
10
  @cdir_bin = +''
@@ -56,6 +56,13 @@ module Zip
56
56
  end
57
57
  private :extract
58
58
 
59
+ # We can suppress the zip64 extra field unless we know the size is large or
60
+ # the relative header offset is large (for central directory entries).
61
+ def suppress?
62
+ !(@original_size && @original_size >= 0xFFFFFFFF) ||
63
+ (@relative_header_offset && @relative_header_offset >= 0xFFFFFFFF)
64
+ end
65
+
59
66
  def pack_for_local
60
67
  # Local header entries must contain original size and compressed size;
61
68
  # other fields do not apply.
@@ -19,14 +19,14 @@ module Zip
19
19
  end
20
20
 
21
21
  def extra_field_type_unknown(binstr, len, index, local)
22
- self['Unknown'] ||= Unknown.new
22
+ self[:unknown] ||= Unknown.new
23
23
 
24
24
  if !len || len + 4 > binstr[index..].bytesize
25
- self['Unknown'].merge(binstr[index..], local: local)
25
+ self[:unknown].merge(binstr[index..], local: local)
26
26
  return
27
27
  end
28
28
 
29
- self['Unknown'].merge(binstr[index, len + 4], local: local)
29
+ self[:unknown].merge(binstr[index, len + 4], local: local)
30
30
  end
31
31
 
32
32
  def merge(binstr, local: false)
@@ -57,10 +57,17 @@ module Zip
57
57
  # signature/size does not prevent known fields from being read back in.
58
58
  def ordered_values
59
59
  result = []
60
- each { |k, v| k == 'Unknown' ? result.push(v) : result.unshift(v) }
60
+ each { |k, v| k == :unknown ? result.push(v) : result.unshift(v) }
61
61
  result
62
62
  end
63
63
 
64
+ # Remove any extra fields that indicate they can be safely suppressed.
65
+ def suppress_fields!(fields)
66
+ reject! do |k, v|
67
+ v.suppress? if fields == true || [*fields].include?(k)
68
+ end
69
+ end
70
+
64
71
  def to_local_bin
65
72
  ordered_values.map! { |v| v.to_local_bin.force_encoding('BINARY') }.join
66
73
  end
data/lib/zip/file.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
3
4
  require 'forwardable'
4
5
 
5
6
  require_relative 'file_split'
@@ -75,7 +76,8 @@ module Zip
75
76
  restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
76
77
  restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
77
78
  restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
78
- compression_level: ::Zip.default_compression)
79
+ compression_level: ::Zip.default_compression,
80
+ suppress_extra_fields: false)
79
81
  super()
80
82
 
81
83
  @name = path_or_io.respond_to?(:path) ? path_or_io.path : path_or_io
@@ -83,10 +85,11 @@ module Zip
83
85
 
84
86
  initialize_cdir(path_or_io, buffer: buffer)
85
87
 
86
- @restore_ownership = restore_ownership
87
- @restore_permissions = restore_permissions
88
- @restore_times = restore_times
89
- @compression_level = compression_level
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
90
93
  end
91
94
 
92
95
  class << self
@@ -97,13 +100,14 @@ module Zip
97
100
  restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
98
101
  restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
99
102
  restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
100
- compression_level: ::Zip.default_compression)
101
-
102
- zf = ::Zip::File.new(file_name, create: create,
103
- restore_ownership: restore_ownership,
104
- restore_permissions: restore_permissions,
105
- restore_times: restore_times,
106
- compression_level: compression_level)
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)
107
111
 
108
112
  return zf unless block_given?
109
113
 
@@ -122,8 +126,8 @@ module Zip
122
126
  restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
123
127
  restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
124
128
  restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
125
- compression_level: ::Zip.default_compression)
126
-
129
+ compression_level: ::Zip.default_compression,
130
+ suppress_extra_fields: false)
127
131
  unless IO_METHODS.map { |method| io.respond_to?(method) }.all? || io.kind_of?(String)
128
132
  raise 'Zip::File.open_buffer expects a String or IO-like argument' \
129
133
  "(responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
@@ -132,10 +136,11 @@ module Zip
132
136
  io = ::StringIO.new(io) if io.kind_of?(::String)
133
137
 
134
138
  zf = ::Zip::File.new(io, create: create, buffer: true,
135
- restore_ownership: restore_ownership,
136
- restore_permissions: restore_permissions,
137
- restore_times: restore_times,
138
- compression_level: compression_level)
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)
139
144
 
140
145
  return zf unless block_given?
141
146
 
@@ -191,7 +196,6 @@ module Zip
191
196
  extra: nil, compressed_size: nil, crc: nil,
192
197
  compression_method: nil, compression_level: nil,
193
198
  size: nil, time: nil, &a_proc)
194
-
195
199
  new_entry =
196
200
  if entry.kind_of?(Entry)
197
201
  entry
@@ -288,7 +292,7 @@ module Zip
288
292
  return if name.kind_of?(StringIO) || !commit_required?
289
293
 
290
294
  on_success_replace do |tmp_file|
291
- ::Zip::OutputStream.open(tmp_file) do |zos|
295
+ ::Zip::OutputStream.open(tmp_file, suppress_extra_fields: @suppress_extra_fields) do |zos|
292
296
  @cdir.each do |e|
293
297
  e.write_to_zip_output_stream(zos)
294
298
  e.clean_up
@@ -304,7 +308,7 @@ module Zip
304
308
  def write_buffer(io = ::StringIO.new)
305
309
  return io unless commit_required?
306
310
 
307
- ::Zip::OutputStream.write_buffer(io) do |zos|
311
+ ::Zip::OutputStream.write_buffer(io, suppress_extra_fields: @suppress_extra_fields) do |zos|
308
312
  @cdir.each { |e| e.write_to_zip_output_stream(zos) }
309
313
  zos.comment = comment
310
314
  end
@@ -412,7 +416,7 @@ module Zip
412
416
  ::File.chmod(@file_permissions, name) unless @create
413
417
  end
414
418
  ensure
415
- ::File.unlink(tmp_filename) if ::File.exist?(tmp_filename)
419
+ FileUtils.rm_f(tmp_filename)
416
420
  end
417
421
  end
418
422
  end
@@ -27,7 +27,7 @@ module Zip
27
27
 
28
28
  def unix_mode_cmp(filename, mode)
29
29
  e = find_entry(filename)
30
- e.fstype == FSTYPE_UNIX && ((e.external_file_attributes >> 16) & mode) != 0
30
+ e.fstype == FSTYPE_UNIX && (e.external_file_attributes >> 16).anybits?(mode)
31
31
  rescue Errno::ENOENT
32
32
  false
33
33
  end
@@ -102,18 +102,19 @@ module Zip
102
102
  @mapped_zip.get_entry(filename).size
103
103
  end
104
104
 
105
- # Returns nil for not found and nil for directories
105
+ # Returns nil for not found and nil for directories.
106
+ # We disable the cop here for compatibility with `::File.size?`.
106
107
  def size?(filename)
107
108
  entry = @mapped_zip.find_entry(filename)
108
- entry.nil? || entry.directory? ? nil : entry.size
109
+ entry.nil? || entry.directory? ? nil : entry.size # rubocop:disable Style/ReturnNilInPredicateMethodDefinition
109
110
  end
110
111
 
111
112
  def chown(owner, group, *filenames)
112
113
  filenames.each do |filename|
113
114
  e = find_entry(filename)
114
- e.extra.create('IUnix') unless e.extra.member?('IUnix')
115
- e.extra['IUnix'].uid = owner
116
- e.extra['IUnix'].gid = group
115
+ e.extra.create(:iunix) unless e.extra.member?(:iunix)
116
+ e.extra[:iunix].uid = owner
117
+ e.extra[:iunix].gid = group
117
118
  end
118
119
  filenames.size
119
120
  end
@@ -36,8 +36,8 @@ module Zip
36
36
 
37
37
  def gid
38
38
  e = find_entry
39
- if e.extra.member? 'IUnix'
40
- e.extra['IUnix'].gid || 0
39
+ if e.extra.member? :iunix
40
+ e.extra[:iunix].gid || 0
41
41
  else
42
42
  0
43
43
  end
@@ -45,8 +45,8 @@ module Zip
45
45
 
46
46
  def uid
47
47
  e = find_entry
48
- if e.extra.member? 'IUnix'
49
- e.extra['IUnix'].uid || 0
48
+ if e.extra.member? :iunix
49
+ e.extra[:iunix].uid || 0
50
50
  else
51
51
  0
52
52
  end
data/lib/zip/inflater.rb CHANGED
@@ -10,7 +10,7 @@ module Zip
10
10
  end
11
11
 
12
12
  def read(length = nil, outbuf = +'')
13
- return (length.nil? || length.zero? ? '' : nil) if eof
13
+ return (length.nil? || length.zero? ? '' : nil) if eof?
14
14
 
15
15
  while length.nil? || (@buffer.bytesize < length)
16
16
  break if input_finished?
@@ -21,11 +21,12 @@ module Zip
21
21
  outbuf.replace(@buffer.slice!(0...(length || @buffer.bytesize)))
22
22
  end
23
23
 
24
- def eof
24
+ def eof?
25
25
  @buffer.empty? && input_finished?
26
26
  end
27
27
 
28
- alias eof? eof
28
+ # Alias for compatibility. Remove for version 4.
29
+ alias eof eof?
29
30
 
30
31
  private
31
32
 
@@ -55,7 +55,7 @@ module Zip
55
55
  super()
56
56
  @archive_io = get_io(context, offset)
57
57
  @decompressor = ::Zip::NullDecompressor
58
- @decrypter = decrypter || ::Zip::NullDecrypter.new
58
+ @decrypter = decrypter
59
59
  @current_entry = nil
60
60
  @complete_entry = nil
61
61
  end
@@ -135,21 +135,27 @@ module Zip
135
135
  @current_entry = ::Zip::Entry.read_local_entry(@archive_io)
136
136
  return if @current_entry.nil?
137
137
 
138
- if @current_entry.encrypted? && @decrypter.kind_of?(NullDecrypter)
139
- raise Error,
140
- 'A password is required to decode this zip file'
141
- end
142
-
143
138
  if @current_entry.incomplete? && @current_entry.compressed_size == 0 && !@complete_entry
144
139
  raise StreamingError, @current_entry
145
140
  end
146
141
 
147
- @decrypted_io = get_decrypted_io
148
- @decompressor = get_decompressor
142
+ @decompressor = assemble_io
149
143
  flush
150
144
  @current_entry
151
145
  end
152
146
 
147
+ def assemble_io # :nodoc:
148
+ io = if @current_entry.encrypted?
149
+ raise Error, 'A password is required to decode this zip file.' if @decrypter.nil?
150
+
151
+ get_decrypted_io
152
+ else
153
+ @archive_io
154
+ end
155
+
156
+ get_decompressor(io)
157
+ end
158
+
153
159
  def get_decrypted_io # :nodoc:
154
160
  header = @archive_io.read(@decrypter.header_bytesize)
155
161
  @decrypter.reset!(header)
@@ -170,7 +176,7 @@ module Zip
170
176
  ::Zip::DecryptedIo.new(@archive_io, @decrypter, compressed_size)
171
177
  end
172
178
 
173
- def get_decompressor # :nodoc:
179
+ def get_decompressor(io) # :nodoc:
174
180
  return ::Zip::NullDecompressor if @current_entry.nil?
175
181
 
176
182
  decompressed_size =
@@ -188,7 +194,7 @@ module Zip
188
194
  raise ::Zip::CompressionMethodError, @current_entry.compression_method
189
195
  end
190
196
 
191
- decompressor_class.new(@decrypted_io, decompressed_size)
197
+ decompressor_class.new(io, decompressed_size)
192
198
  end
193
199
 
194
200
  def produce_input # :nodoc:
@@ -196,7 +202,7 @@ module Zip
196
202
  end
197
203
 
198
204
  def input_finished? # :nodoc:
199
- @decompressor.eof
205
+ @decompressor.eof?
200
206
  end
201
207
  end
202
208
  end
@@ -117,11 +117,12 @@ module Zip
117
117
 
118
118
  alias each each_line
119
119
 
120
- def eof
120
+ def eof?
121
121
  @output_buffer.empty? && input_finished?
122
122
  end
123
123
 
124
- alias eof? eof
124
+ # Alias for compatibility. Remove for version 4.
125
+ alias eof eof?
125
126
  end
126
127
  end
127
128
  end
@@ -8,11 +8,12 @@ module Zip
8
8
  nil
9
9
  end
10
10
 
11
- def eof
11
+ def eof?
12
12
  true
13
13
  end
14
14
 
15
- alias eof? eof
15
+ # Alias for compatibility. Remove for version 4.
16
+ alias eof eof?
16
17
  end
17
18
  end
18
19
 
@@ -29,7 +29,7 @@ module Zip
29
29
 
30
30
  # Opens the indicated zip file. If a file with that name already
31
31
  # exists it will be overwritten.
32
- def initialize(file_name, stream: false, encrypter: nil)
32
+ def initialize(file_name, stream: false, encrypter: nil, suppress_extra_fields: false)
33
33
  super()
34
34
  @file_name = file_name
35
35
  @output_stream = if stream
@@ -43,6 +43,7 @@ module Zip
43
43
  @cdir = ::Zip::CentralDirectory.new
44
44
  @compressor = ::Zip::NullCompressor.instance
45
45
  @encrypter = encrypter || ::Zip::NullEncrypter.new
46
+ @suppress_extra_fields = suppress_extra_fields
46
47
  @closed = false
47
48
  @current_entry = nil
48
49
  end
@@ -51,19 +52,21 @@ module Zip
51
52
  # Same as #initialize but if a block is passed the opened
52
53
  # stream is passed to the block and closed when the block
53
54
  # returns.
54
- def open(file_name, encrypter: nil)
55
+ def open(file_name, encrypter: nil, suppress_extra_fields: false)
55
56
  return new(file_name) unless block_given?
56
57
 
57
- zos = new(file_name, stream: false, encrypter: encrypter)
58
+ zos = new(file_name, stream: false, encrypter: encrypter,
59
+ suppress_extra_fields: suppress_extra_fields)
58
60
  yield zos
59
61
  ensure
60
62
  zos.close if zos
61
63
  end
62
64
 
63
65
  # Same as #open but writes to a filestream instead
64
- def write_buffer(io = ::StringIO.new, encrypter: nil)
66
+ def write_buffer(io = ::StringIO.new, encrypter: nil, suppress_extra_fields: false)
65
67
  io.binmode if io.respond_to?(:binmode)
66
- zos = new(io, stream: true, encrypter: encrypter)
68
+ zos = new(io, stream: true, encrypter: encrypter,
69
+ suppress_extra_fields: suppress_extra_fields)
67
70
  yield zos
68
71
  zos.close_buffer
69
72
  end
@@ -75,7 +78,7 @@ module Zip
75
78
 
76
79
  finalize_current_entry
77
80
  update_local_headers
78
- @cdir.write_to_stream(@output_stream)
81
+ @cdir.write_to_stream(@output_stream, suppress_extra_fields: @suppress_extra_fields)
79
82
  @output_stream.close
80
83
  @closed = true
81
84
  end
@@ -86,7 +89,7 @@ module Zip
86
89
 
87
90
  finalize_current_entry
88
91
  update_local_headers
89
- @cdir.write_to_stream(@output_stream)
92
+ @cdir.write_to_stream(@output_stream, suppress_extra_fields: @suppress_extra_fields)
90
93
  @closed = true
91
94
  @output_stream.flush
92
95
  @output_stream
@@ -157,7 +160,7 @@ module Zip
157
160
  def init_next_entry(entry)
158
161
  finalize_current_entry
159
162
  @cdir << entry
160
- entry.write_local_entry(@output_stream)
163
+ entry.write_local_entry(@output_stream, suppress_extra_fields: @suppress_extra_fields)
161
164
  @encrypter.reset!
162
165
  @output_stream << @encrypter.header(entry.mtime)
163
166
  @compressor = get_compressor(entry)
@@ -178,7 +181,8 @@ module Zip
178
181
  pos = @output_stream.pos
179
182
  @cdir.each do |entry|
180
183
  @output_stream.pos = entry.local_header_offset
181
- entry.write_local_entry(@output_stream, rewrite: true)
184
+ entry.write_local_entry(@output_stream, suppress_extra_fields: @suppress_extra_fields,
185
+ rewrite: true)
182
186
  end
183
187
  @output_stream.pos = pos
184
188
  end
@@ -8,7 +8,7 @@ module Zip
8
8
  end
9
9
 
10
10
  def read(length = nil, outbuf = +'')
11
- return (length.nil? || length.zero? ? '' : nil) if eof
11
+ return (length.nil? || length.zero? ? '' : nil) if eof?
12
12
 
13
13
  if length.nil? || (@read_so_far + length) > decompressed_size
14
14
  length = decompressed_size - @read_so_far
@@ -18,11 +18,12 @@ module Zip
18
18
  input_stream.read(length, outbuf)
19
19
  end
20
20
 
21
- def eof
21
+ def eof?
22
22
  @read_so_far >= decompressed_size
23
23
  end
24
24
 
25
- alias eof? eof
25
+ # Alias for compatibility. Remove for version 4.
26
+ alias eof eof?
26
27
  end
27
28
 
28
29
  ::Zip::Decompressor.register(::Zip::COMPRESSION_METHOD_STORE, ::Zip::PassThruDecompressor)
@@ -3,7 +3,7 @@
3
3
  module Zip
4
4
  class StreamableStream < DelegateClass(Entry) # :nodoc:all
5
5
  def initialize(entry)
6
- super(entry)
6
+ super
7
7
  @temp_file = Tempfile.new(::File.basename(name))
8
8
  @temp_file.binmode
9
9
  end
data/lib/zip/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Zip
4
4
  # The version of the Rubyzip library.
5
- VERSION = '3.1.0'
5
+ VERSION = '3.2.0'
6
6
  end
data/lib/zip.rb CHANGED
@@ -44,6 +44,7 @@ require 'zip/errors'
44
44
  # ::Dir APIs then `require 'zip/filesystem'` and see FileSystem.
45
45
  module Zip
46
46
  extend self
47
+
47
48
  attr_accessor :unicode_names,
48
49
  :on_exists_proc,
49
50
  :continue_on_exists_proc,
data/rubyzip.gemspec CHANGED
@@ -31,9 +31,9 @@ Gem::Specification.new do |s|
31
31
  s.add_development_dependency 'minitest', '~> 5.25'
32
32
  s.add_development_dependency 'rake', '~> 13.2'
33
33
  s.add_development_dependency 'rdoc', '~> 6.11'
34
- s.add_development_dependency 'rubocop', '~> 1.61.0'
35
- s.add_development_dependency 'rubocop-performance', '~> 1.20.0'
36
- s.add_development_dependency 'rubocop-rake', '~> 0.6.0'
34
+ s.add_development_dependency 'rubocop', '~> 1.80.2'
35
+ s.add_development_dependency 'rubocop-performance', '~> 1.26.0'
36
+ s.add_development_dependency 'rubocop-rake', '~> 0.7.1'
37
37
  s.add_development_dependency 'simplecov', '~> 0.22.0'
38
38
  s.add_development_dependency 'simplecov-lcov', '~> 0.8'
39
39
  end
@@ -10,7 +10,7 @@ require 'zip'
10
10
 
11
11
  class MainApp < Gtk::Window
12
12
  def initialize
13
- super()
13
+ super
14
14
  set_usize(400, 256)
15
15
  set_title('rubyzip')
16
16
  signal_connect(Gtk::Window::SIGNAL_DESTROY) { Gtk.main_quit }
data/samples/qtzip.rb CHANGED
@@ -14,7 +14,7 @@ a = Qt::Application.new(ARGV)
14
14
 
15
15
  class ZipDialog < ZipDialogUI
16
16
  def initialize
17
- super()
17
+ super
18
18
  connect(child('add_button'), SIGNAL('clicked()'),
19
19
  self, SLOT('add_files()'))
20
20
  connect(child('extract_button'), SIGNAL('clicked()'),
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubyzip
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Haines
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2025-09-06 00:00:00.000000000 Z
13
+ date: 2025-10-14 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: minitest
@@ -60,42 +60,42 @@ dependencies:
60
60
  requirements:
61
61
  - - "~>"
62
62
  - !ruby/object:Gem::Version
63
- version: 1.61.0
63
+ version: 1.80.2
64
64
  type: :development
65
65
  prerelease: false
66
66
  version_requirements: !ruby/object:Gem::Requirement
67
67
  requirements:
68
68
  - - "~>"
69
69
  - !ruby/object:Gem::Version
70
- version: 1.61.0
70
+ version: 1.80.2
71
71
  - !ruby/object:Gem::Dependency
72
72
  name: rubocop-performance
73
73
  requirement: !ruby/object:Gem::Requirement
74
74
  requirements:
75
75
  - - "~>"
76
76
  - !ruby/object:Gem::Version
77
- version: 1.20.0
77
+ version: 1.26.0
78
78
  type: :development
79
79
  prerelease: false
80
80
  version_requirements: !ruby/object:Gem::Requirement
81
81
  requirements:
82
82
  - - "~>"
83
83
  - !ruby/object:Gem::Version
84
- version: 1.20.0
84
+ version: 1.26.0
85
85
  - !ruby/object:Gem::Dependency
86
86
  name: rubocop-rake
87
87
  requirement: !ruby/object:Gem::Requirement
88
88
  requirements:
89
89
  - - "~>"
90
90
  - !ruby/object:Gem::Version
91
- version: 0.6.0
91
+ version: 0.7.1
92
92
  type: :development
93
93
  prerelease: false
94
94
  version_requirements: !ruby/object:Gem::Requirement
95
95
  requirements:
96
96
  - - "~>"
97
97
  - !ruby/object:Gem::Version
98
- version: 0.6.0
98
+ version: 0.7.1
99
99
  - !ruby/object:Gem::Dependency
100
100
  name: simplecov
101
101
  requirement: !ruby/object:Gem::Requirement
@@ -197,9 +197,9 @@ licenses:
197
197
  - BSD-2-Clause
198
198
  metadata:
199
199
  bug_tracker_uri: https://github.com/rubyzip/rubyzip/issues
200
- changelog_uri: https://github.com/rubyzip/rubyzip/blob/v3.1.0/Changelog.md
201
- documentation_uri: https://www.rubydoc.info/gems/rubyzip/3.1.0
202
- source_code_uri: https://github.com/rubyzip/rubyzip/tree/v3.1.0
200
+ changelog_uri: https://github.com/rubyzip/rubyzip/blob/v3.2.0/Changelog.md
201
+ documentation_uri: https://www.rubydoc.info/gems/rubyzip/3.2.0
202
+ source_code_uri: https://github.com/rubyzip/rubyzip/tree/v3.2.0
203
203
  wiki_uri: https://github.com/rubyzip/rubyzip/wiki
204
204
  rubygems_mfa_required: 'true'
205
205
  post_install_message: