rubyzip 3.0.2 → 3.1.1

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: c4afc6447f838d0d2edc82bb44ab842a5c45f50bd47d453aae1a7b487c9dcbee
4
- data.tar.gz: 776833d664351c8e0323826263d16b2b11486fee80a7ceda1a8385d05dd2a38e
3
+ metadata.gz: 869a32fef1374d44c2d01fcf90d92d10760ceb619cbc2ccb545f7753503efee4
4
+ data.tar.gz: 9eb0b0601572717187029c44fbcfdd7fa3a47005d869d8aea2b36ab465b18de5
5
5
  SHA512:
6
- metadata.gz: b52059d2cfba5c05959bb347383d4bfe79c8f3c0f945cba64362462267c750f0e16e13f63fbf272b2cf0935173678b876164c3993ddae85fb89dec726afea940
7
- data.tar.gz: 83a932fbadf6fbafa0a1e64434f0b2a2b7f58fb56d9015350eae217430f9afaf5492254a56de9be124bdf9676a8a5aa373515c996ce2f74966aceb87b72472d1
6
+ metadata.gz: 8cc3d0b1cb58eabdafb91db1cc8e7a2ef3932bcaf993df9a8c21ae261d480ffa43d431111faa11f5e19e52aa59a70e14a4724bbd174a8628a7ffacb48119ccdb
7
+ data.tar.gz: 1171566d3c91b07b01e3f5fe77e9d098f6c5fa6f94891cf289c1edbaa35bbe2a3a96165bebfcb7b35f2d41eedb268b13e90aab27683cd05175ea0ffbc9a485c6
data/Changelog.md CHANGED
@@ -1,3 +1,24 @@
1
+ # 3.1.1 (2025-09-26)
2
+
3
+ - 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))
4
+
5
+ Tooling/internal:
6
+
7
+ - Improve the `DecryptedIo` class with various updates and optimizations.
8
+ - Remove the `NullDecrypter` class.
9
+ - Properly convert the test suite to use minitest.
10
+ - Move all test helper code into separate files.
11
+ - Updates to the Actions CI, including new OS versions.
12
+ - Update rubocop versions and fix resultant cop failures. [#646](https://github.com/rubyzip/rubyzip/pull/646)
13
+
14
+ # 3.1.0 (2025-09-06)
15
+
16
+ - Support AES decryption. [#579](https://github.com/rubyzip/rubyzip/pull/579) and [#645](https://github.com/rubyzip/rubyzip/pull/645)
17
+
18
+ Tooling/internal:
19
+
20
+ - Add various useful zip specification documents to the repo for ease of finding them in the future. These are not included in the gem release.
21
+
1
22
  # 3.0.2 (2025-08-21)
2
23
 
3
24
  - Fix `InputStream#sysread` to handle frozen string literals. [#643](https://github.com/rubyzip/rubyzip/pull/643)
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.
@@ -201,11 +202,11 @@ end
201
202
 
202
203
  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`.
203
204
 
204
- ### Password Protection (Experimental)
205
+ ### Password Protection (experimental)
205
206
 
206
- Rubyzip supports reading/writing zip files with traditional zip encryption (a.k.a. "ZipCrypto"). AES encryption is not yet supported. It can be used with buffer streams, e.g.:
207
+ Rubyzip supports reading zip files with AES encryption (version 3.1 and later), and reading and writing zip files with traditional zip encryption (a.k.a. "ZipCrypto"). Encryption is currently only available with the stream API, with either files or buffers, e.g.:
207
208
 
208
- #### Version 2.x
209
+ #### Version 2.x (ZipCrypto only)
209
210
 
210
211
  ```ruby
211
212
  # Writing.
@@ -224,9 +225,17 @@ Zip::InputStream.open(buffer, 0, dec) do |input|
224
225
  end
225
226
  ```
226
227
 
227
- #### Version 3.x
228
+ #### Version 3.x (AES reading and ZipCrypto read/write)
228
229
 
229
230
  ```ruby
231
+ # Reading AES, version 3.1 and later.
232
+ dec = Zip::AESDecrypter.new('password', Zip::AESEncryption::STRENGTH_256_BIT)
233
+ Zip::InputStream.open('aes-encrypted-file.zip', decrypter: dec) do |input|
234
+ entry = input.get_next_entry
235
+ puts "Contents of '#{entry.name}':"
236
+ puts input.read
237
+ end
238
+
230
239
  # Writing.
231
240
  enc = Zip::TraditionalEncrypter.new('password')
232
241
  buffer = Zip::OutputStream.write_buffer(encrypter: enc) do |output|
@@ -243,7 +252,7 @@ Zip::InputStream.open(buffer, decrypter: dec) do |input|
243
252
  end
244
253
  ```
245
254
 
246
- _This is an experimental feature and the interface for encryption may change in future versions._
255
+ _This is an evolving feature and the interface for encryption may change in future versions._
247
256
 
248
257
  ## Known issues
249
258
 
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|
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module Zip
6
+ module AESEncryption # :nodoc:
7
+ VERIFIER_LENGTH = 2
8
+ BLOCK_SIZE = 16
9
+ AUTHENTICATION_CODE_LENGTH = 10
10
+
11
+ VERSION_AE_1 = 0x01
12
+ VERSION_AE_2 = 0x02
13
+
14
+ VERSIONS = [
15
+ VERSION_AE_1,
16
+ VERSION_AE_2
17
+ ].freeze
18
+
19
+ STRENGTH_128_BIT = 0x01
20
+ STRENGTH_192_BIT = 0x02
21
+ STRENGTH_256_BIT = 0x03
22
+
23
+ STRENGTHS = [
24
+ STRENGTH_128_BIT,
25
+ STRENGTH_192_BIT,
26
+ STRENGTH_256_BIT
27
+ ].freeze
28
+
29
+ BITS = {
30
+ STRENGTH_128_BIT => 128,
31
+ STRENGTH_192_BIT => 192,
32
+ STRENGTH_256_BIT => 256
33
+ }.freeze
34
+
35
+ KEY_LENGTHS = {
36
+ STRENGTH_128_BIT => 16,
37
+ STRENGTH_192_BIT => 24,
38
+ STRENGTH_256_BIT => 32
39
+ }.freeze
40
+
41
+ SALT_LENGTHS = {
42
+ STRENGTH_128_BIT => 8,
43
+ STRENGTH_192_BIT => 12,
44
+ STRENGTH_256_BIT => 16
45
+ }.freeze
46
+
47
+ def initialize(password, strength)
48
+ @password = password
49
+ @strength = strength
50
+ @bits = BITS[@strength]
51
+ @key_length = KEY_LENGTHS[@strength]
52
+ @salt_length = SALT_LENGTHS[@strength]
53
+ end
54
+
55
+ def header_bytesize
56
+ @salt_length + VERIFIER_LENGTH
57
+ end
58
+
59
+ def gp_flags
60
+ 0x0001
61
+ end
62
+ end
63
+
64
+ class AESDecrypter < Decrypter # :nodoc:
65
+ include AESEncryption
66
+
67
+ def decrypt(encrypted_data)
68
+ @hmac.update(encrypted_data)
69
+
70
+ idx = 0
71
+ decrypted_data = +''
72
+ amount_to_read = encrypted_data.size
73
+
74
+ while amount_to_read.positive?
75
+ @cipher.iv = [@counter + 1].pack('Vx12')
76
+ begin_index = BLOCK_SIZE * idx
77
+ end_index = begin_index + [BLOCK_SIZE, amount_to_read].min
78
+ decrypted_data << @cipher.update(encrypted_data[begin_index...end_index])
79
+ amount_to_read -= BLOCK_SIZE
80
+ @counter += 1
81
+ idx += 1
82
+ end
83
+
84
+ # JRuby requires finalization of the cipher. This is a bug, as noted in
85
+ # jruby/jruby-openssl#182 and jruby/jruby-openssl#183.
86
+ decrypted_data << @cipher.final if defined?(JRUBY_VERSION)
87
+ decrypted_data
88
+ end
89
+
90
+ def reset!(header)
91
+ raise Error, "Unsupported encryption AES-#{@bits}" unless STRENGTHS.include? @strength
92
+
93
+ salt = header[0...@salt_length]
94
+ pwd_verify = header[-VERIFIER_LENGTH..]
95
+ key_material = OpenSSL::KDF.pbkdf2_hmac(
96
+ @password,
97
+ salt: salt,
98
+ iterations: 1000,
99
+ length: (2 * @key_length) + VERIFIER_LENGTH,
100
+ hash: 'sha1'
101
+ )
102
+ enc_key = key_material[0...@key_length]
103
+ enc_hmac_key = key_material[@key_length...(2 * @key_length)]
104
+ enc_pwd_verify = key_material[-VERIFIER_LENGTH..]
105
+
106
+ raise Error, 'Bad password' if enc_pwd_verify != pwd_verify
107
+
108
+ @counter = 0
109
+ @cipher = OpenSSL::Cipher::AES.new(@bits, :CTR)
110
+ @cipher.decrypt
111
+ @cipher.key = enc_key
112
+ @hmac = OpenSSL::HMAC.new(enc_hmac_key, OpenSSL::Digest.new('SHA1'))
113
+ end
114
+
115
+ def check_integrity!(io)
116
+ auth_code = io.read(AUTHENTICATION_CODE_LENGTH)
117
+ raise Error, 'Integrity fault' if @hmac.digest[0...AUTHENTICATION_CODE_LENGTH] != auth_code
118
+ end
119
+ end
120
+ end
@@ -4,39 +4,43 @@ module Zip
4
4
  class DecryptedIo # :nodoc:all
5
5
  CHUNK_SIZE = 32_768
6
6
 
7
- def initialize(io, decrypter)
7
+ def initialize(io, decrypter, compressed_size)
8
8
  @io = io
9
9
  @decrypter = decrypter
10
+ @bytes_remaining = compressed_size
11
+ @buffer = +''
10
12
  end
11
13
 
12
14
  def read(length = nil, outbuf = +'')
13
- return (length.nil? || length.zero? ? '' : nil) if eof
15
+ return (length.nil? || length.zero? ? '' : nil) if eof?
14
16
 
15
- while length.nil? || (buffer.bytesize < length)
17
+ while length.nil? || (@buffer.bytesize < length)
16
18
  break if input_finished?
17
19
 
18
- buffer << produce_input
20
+ @buffer << produce_input
19
21
  end
20
22
 
21
- outbuf.replace(buffer.slice!(0...(length || buffer.bytesize)))
23
+ @decrypter.check_integrity!(@io) if input_finished?
24
+
25
+ outbuf.replace(@buffer.slice!(0...(length || @buffer.bytesize)))
22
26
  end
23
27
 
24
28
  private
25
29
 
26
- def eof
27
- buffer.empty? && input_finished?
28
- end
29
-
30
- def buffer
31
- @buffer ||= +''
30
+ def eof?
31
+ @buffer.empty? && input_finished?
32
32
  end
33
33
 
34
34
  def input_finished?
35
- @io.eof
35
+ !@bytes_remaining.positive?
36
36
  end
37
37
 
38
38
  def produce_input
39
- @decrypter.decrypt(@io.read(CHUNK_SIZE))
39
+ chunk_size = [@bytes_remaining, CHUNK_SIZE].min
40
+ return '' unless chunk_size.positive?
41
+
42
+ @bytes_remaining -= chunk_size
43
+ @decrypter.decrypt(@io.read(chunk_size))
40
44
  end
41
45
  end
42
46
  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
@@ -203,6 +203,11 @@ module Zip
203
203
  !@extra['Zip64'].nil?
204
204
  end
205
205
 
206
+ # Is this entry encrypted with AES encryption?
207
+ def aes?
208
+ !@extra['AES'].nil?
209
+ end
210
+
206
211
  def file_type_is?(type) # :nodoc:
207
212
  ftype == type
208
213
  end
@@ -382,6 +387,7 @@ module Zip
382
387
 
383
388
  read_extra_field(extra, local: true)
384
389
  parse_zip64_extra(true)
390
+ parse_aes_extra
385
391
  @local_header_size = calculate_local_header_size
386
392
  end
387
393
 
@@ -511,6 +517,7 @@ module Zip
511
517
  check_c_dir_entry_comment_size
512
518
  set_ftype_from_c_dir_entry
513
519
  parse_zip64_extra(false)
520
+ parse_aes_extra
514
521
  end
515
522
 
516
523
  def file_stat(path) # :nodoc:
@@ -800,6 +807,20 @@ module Zip
800
807
  end
801
808
  end
802
809
 
810
+ def parse_aes_extra # :nodoc:
811
+ return unless aes?
812
+
813
+ if @extra['AES'].vendor_id != 'AE'
814
+ raise Error, "Unsupported encryption method #{@extra['AES'].vendor_id}"
815
+ end
816
+
817
+ unless ::Zip::AESEncryption::VERSIONS.include? @extra['AES'].vendor_version
818
+ raise Error, "Unsupported encryption style #{@extra['AES'].vendor_version}"
819
+ end
820
+
821
+ @compression_method = @extra['AES'].compression_method if ftype != :directory
822
+ end
823
+
803
824
  # For DEFLATED compression *only*: set the general purpose flags 1 and 2 to
804
825
  # indicate compression level. This seems to be mainly cosmetic but they are
805
826
  # generally set by other tools - including in docx files. It is these flags
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zip
4
+ # Info-ZIP Extra for AES encryption
5
+ class ExtraField::AES < ExtraField::Generic # :nodoc:
6
+ attr_reader :vendor_version, :vendor_id, :encryption_strength, :compression_method
7
+
8
+ HEADER_ID = [0x9901].pack('v')
9
+ register_map
10
+
11
+ def initialize(binstr = nil)
12
+ @vendor_version = nil
13
+ @vendor_id = nil
14
+ @encryption_strength = nil
15
+ @compression_method = nil
16
+ binstr && merge(binstr)
17
+ end
18
+
19
+ def ==(other)
20
+ @vendor_version == other.vendor_version &&
21
+ @vendor_id == other.vendor_id &&
22
+ @encryption_strength == other.encryption_strength &&
23
+ @compression_method == other.compression_method
24
+ end
25
+
26
+ def merge(binstr)
27
+ return if binstr.empty?
28
+
29
+ size, content = initial_parse(binstr)
30
+ (size && content) || return
31
+
32
+ @vendor_version, @vendor_id,
33
+ @encryption_strength, @compression_method = content.unpack('va2Cv')
34
+ end
35
+
36
+ def pack_for_local
37
+ [@vendor_version, @vendor_id,
38
+ @encryption_strength, @compression_method].pack('va2Cv')
39
+ end
40
+
41
+ def pack_for_c_dir
42
+ pack_for_local
43
+ end
44
+ end
45
+ end
@@ -91,6 +91,7 @@ require 'zip/extra_field/old_unix'
91
91
  require 'zip/extra_field/unix'
92
92
  require 'zip/extra_field/zip64'
93
93
  require 'zip/extra_field/ntfs'
94
+ require 'zip/extra_field/aes'
94
95
 
95
96
  # Copyright (C) 2002, 2003 Thomas Sondergaard
96
97
  # rubyzip is free software; you can redistribute it and/or
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'
@@ -98,7 +99,6 @@ module Zip
98
99
  restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
99
100
  restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
100
101
  compression_level: ::Zip.default_compression)
101
-
102
102
  zf = ::Zip::File.new(file_name, create: create,
103
103
  restore_ownership: restore_ownership,
104
104
  restore_permissions: restore_permissions,
@@ -123,7 +123,6 @@ module Zip
123
123
  restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
124
124
  restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
125
125
  compression_level: ::Zip.default_compression)
126
-
127
126
  unless IO_METHODS.map { |method| io.respond_to?(method) }.all? || io.kind_of?(String)
128
127
  raise 'Zip::File.open_buffer expects a String or IO-like argument' \
129
128
  "(responds to #{IO_METHODS.join(', ')}). Found: #{io.class}"
@@ -191,7 +190,6 @@ module Zip
191
190
  extra: nil, compressed_size: nil, crc: nil,
192
191
  compression_method: nil, compression_level: nil,
193
192
  size: nil, time: nil, &a_proc)
194
-
195
193
  new_entry =
196
194
  if entry.kind_of?(Entry)
197
195
  entry
@@ -412,7 +410,7 @@ module Zip
412
410
  ::File.chmod(@file_permissions, name) unless @create
413
411
  end
414
412
  ensure
415
- ::File.unlink(tmp_filename) if ::File.exist?(tmp_filename)
413
+ FileUtils.rm_f(tmp_filename)
416
414
  end
417
415
  end
418
416
  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,10 +102,11 @@ 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)
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,29 +135,48 @@ 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)
156
162
 
157
- ::Zip::DecryptedIo.new(@archive_io, @decrypter)
163
+ compressed_size =
164
+ if @current_entry.incomplete? && @current_entry.crc == 0 &&
165
+ @current_entry.compressed_size == 0 && @complete_entry
166
+ @complete_entry.compressed_size
167
+ else
168
+ @current_entry.compressed_size
169
+ end
170
+
171
+ if @decrypter.kind_of?(::Zip::AESDecrypter)
172
+ compressed_size -= @decrypter.header_bytesize
173
+ compressed_size -= ::Zip::AESEncryption::AUTHENTICATION_CODE_LENGTH
174
+ end
175
+
176
+ ::Zip::DecryptedIo.new(@archive_io, @decrypter, compressed_size)
158
177
  end
159
178
 
160
- def get_decompressor # :nodoc:
179
+ def get_decompressor(io) # :nodoc:
161
180
  return ::Zip::NullDecompressor if @current_entry.nil?
162
181
 
163
182
  decompressed_size =
@@ -175,7 +194,7 @@ module Zip
175
194
  raise ::Zip::CompressionMethodError, @current_entry.compression_method
176
195
  end
177
196
 
178
- decompressor_class.new(@decrypted_io, decompressed_size)
197
+ decompressor_class.new(io, decompressed_size)
179
198
  end
180
199
 
181
200
  def produce_input # :nodoc:
@@ -183,7 +202,7 @@ module Zip
183
202
  end
184
203
 
185
204
  def input_finished? # :nodoc:
186
- @decompressor.eof
205
+ @decompressor.eof?
187
206
  end
188
207
  end
189
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
 
@@ -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
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zip
4
- VERSION = '3.0.2'
4
+ # The version of the Rubyzip library.
5
+ VERSION = '3.1.1'
5
6
  end
data/lib/zip.rb CHANGED
@@ -30,6 +30,7 @@ require 'zip/crypto/decrypted_io'
30
30
  require 'zip/crypto/encryption'
31
31
  require 'zip/crypto/null_encryption'
32
32
  require 'zip/crypto/traditional_encryption'
33
+ require 'zip/crypto/aes_encryption'
33
34
  require 'zip/inflater'
34
35
  require 'zip/deflater'
35
36
  require 'zip/streamable_stream'
@@ -43,6 +44,7 @@ require 'zip/errors'
43
44
  # ::Dir APIs then `require 'zip/filesystem'` and see FileSystem.
44
45
  module Zip
45
46
  extend self
47
+
46
48
  attr_accessor :unicode_names,
47
49
  :on_exists_proc,
48
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.0.2
4
+ version: 3.1.1
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-08-21 00:00:00.000000000 Z
13
+ date: 2025-09-26 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
@@ -141,6 +141,7 @@ files:
141
141
  - lib/zip/central_directory.rb
142
142
  - lib/zip/compressor.rb
143
143
  - lib/zip/constants.rb
144
+ - lib/zip/crypto/aes_encryption.rb
144
145
  - lib/zip/crypto/decrypted_io.rb
145
146
  - lib/zip/crypto/encryption.rb
146
147
  - lib/zip/crypto/null_encryption.rb
@@ -153,6 +154,7 @@ files:
153
154
  - lib/zip/entry_set.rb
154
155
  - lib/zip/errors.rb
155
156
  - lib/zip/extra_field.rb
157
+ - lib/zip/extra_field/aes.rb
156
158
  - lib/zip/extra_field/generic.rb
157
159
  - lib/zip/extra_field/ntfs.rb
158
160
  - lib/zip/extra_field/old_unix.rb
@@ -195,9 +197,9 @@ licenses:
195
197
  - BSD-2-Clause
196
198
  metadata:
197
199
  bug_tracker_uri: https://github.com/rubyzip/rubyzip/issues
198
- changelog_uri: https://github.com/rubyzip/rubyzip/blob/v3.0.2/Changelog.md
199
- documentation_uri: https://www.rubydoc.info/gems/rubyzip/3.0.2
200
- source_code_uri: https://github.com/rubyzip/rubyzip/tree/v3.0.2
200
+ changelog_uri: https://github.com/rubyzip/rubyzip/blob/v3.1.1/Changelog.md
201
+ documentation_uri: https://www.rubydoc.info/gems/rubyzip/3.1.1
202
+ source_code_uri: https://github.com/rubyzip/rubyzip/tree/v3.1.1
201
203
  wiki_uri: https://github.com/rubyzip/rubyzip/wiki
202
204
  rubygems_mfa_required: 'true'
203
205
  post_install_message:
@@ -215,7 +217,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
215
217
  - !ruby/object:Gem::Version
216
218
  version: '0'
217
219
  requirements: []
218
- rubygems_version: 3.4.1
220
+ rubygems_version: 3.4.19
219
221
  signing_key:
220
222
  specification_version: 4
221
223
  summary: rubyzip is a ruby module for reading and writing zip files