rubyzip 3.0.2 → 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: c4afc6447f838d0d2edc82bb44ab842a5c45f50bd47d453aae1a7b487c9dcbee
4
- data.tar.gz: 776833d664351c8e0323826263d16b2b11486fee80a7ceda1a8385d05dd2a38e
3
+ metadata.gz: 169fa7d5832e775e15c2f8a409a35e9da32c0177117039f089f9fa1646aa281e
4
+ data.tar.gz: 9b4fb8929262cb097e3631ad8d8a9efa98d691488ce79e37092df4de27204aea
5
5
  SHA512:
6
- metadata.gz: b52059d2cfba5c05959bb347383d4bfe79c8f3c0f945cba64362462267c750f0e16e13f63fbf272b2cf0935173678b876164c3993ddae85fb89dec726afea940
7
- data.tar.gz: 83a932fbadf6fbafa0a1e64434f0b2a2b7f58fb56d9015350eae217430f9afaf5492254a56de9be124bdf9676a8a5aa373515c996ce2f74966aceb87b72472d1
6
+ metadata.gz: 4bc8630670298d013486cf4632084554a10ed2ba327c2b54f65c105602f31ca84b87a42976fa1a606226d7cc5b88ecbc356f8a0932a9a53880e0b68c4b7f6275
7
+ data.tar.gz: 4cc717180899d18767c73913fa16abaafeb3624167fc2c4e14a43dde406eef815d9791e84a7bb9f018a255e32d08109dc01efdd8844cc1503a85ac8d931d4b22
data/Changelog.md CHANGED
@@ -1,3 +1,35 @@
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
+
25
+ # 3.1.0 (2025-09-06)
26
+
27
+ - Support AES decryption. [#579](https://github.com/rubyzip/rubyzip/pull/579) and [#645](https://github.com/rubyzip/rubyzip/pull/645)
28
+
29
+ Tooling/internal:
30
+
31
+ - 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.
32
+
1
33
  # 3.0.2 (2025-08-21)
2
34
 
3
35
  - 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.
@@ -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,23 +232,23 @@ 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`.
203
246
 
204
- ### Password Protection (Experimental)
247
+ ### Password Protection (experimental)
205
248
 
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.:
249
+ 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
250
 
208
- #### Version 2.x
251
+ #### Version 2.x (ZipCrypto only)
209
252
 
210
253
  ```ruby
211
254
  # Writing.
@@ -224,9 +267,17 @@ Zip::InputStream.open(buffer, 0, dec) do |input|
224
267
  end
225
268
  ```
226
269
 
227
- #### Version 3.x
270
+ #### Version 3.x (AES reading and ZipCrypto read/write)
228
271
 
229
272
  ```ruby
273
+ # Reading AES, version 3.1 and later.
274
+ dec = Zip::AESDecrypter.new('password', Zip::AESEncryption::STRENGTH_256_BIT)
275
+ Zip::InputStream.open('aes-encrypted-file.zip', decrypter: dec) do |input|
276
+ entry = input.get_next_entry
277
+ puts "Contents of '#{entry.name}':"
278
+ puts input.read
279
+ end
280
+
230
281
  # Writing.
231
282
  enc = Zip::TraditionalEncrypter.new('password')
232
283
  buffer = Zip::OutputStream.write_buffer(encrypter: enc) do |output|
@@ -243,7 +294,7 @@ Zip::InputStream.open(buffer, decrypter: dec) do |input|
243
294
  end
244
295
  ```
245
296
 
246
- _This is an experimental feature and the interface for encryption may change in future versions._
297
+ _This is an evolving feature and the interface for encryption may change in future versions._
247
298
 
248
299
  ## Known issues
249
300
 
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
@@ -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
@@ -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,7 +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
+ end
205
+
206
+ # Is this entry encrypted with AES encryption?
207
+ def aes?
208
+ !@extra[:aes].nil?
204
209
  end
205
210
 
206
211
  def file_type_is?(type) # :nodoc:
@@ -382,11 +387,12 @@ 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
 
388
394
  def pack_local_entry # :nodoc:
389
- zip64 = @extra['Zip64']
395
+ zip64 = @extra[:zip64]
390
396
  [::Zip::LOCAL_ENTRY_SIGNATURE,
391
397
  @version_needed_to_extract, # version needed to extract
392
398
  @gp_flags, # @gp_flags
@@ -400,9 +406,17 @@ module Zip
400
406
  @extra ? @extra.local_size : 0].pack('VvvvvvVVVvv')
401
407
  end
402
408
 
403
- def write_local_entry(io, rewrite: false) # :nodoc:
409
+ def write_local_entry(io, suppress_extra_fields: false, rewrite: false) # :nodoc:
404
410
  prep_local_zip64_extra
405
- 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
+
406
420
  @local_header_offset = io.tell
407
421
 
408
422
  io << pack_local_entry
@@ -430,10 +444,7 @@ module Zip
430
444
  _, # diskNumberStart
431
445
  @internal_file_attributes,
432
446
  @external_file_attributes,
433
- @local_header_offset,
434
- @name,
435
- @extra,
436
- @comment = buf.unpack('VCCvvvvvVVVvvvvvVV')
447
+ @local_header_offset = buf.unpack('VCCvvvvvVVVvvvvvVV')
437
448
  end
438
449
 
439
450
  def set_ftype_from_c_dir_entry # :nodoc:
@@ -511,6 +522,7 @@ module Zip
511
522
  check_c_dir_entry_comment_size
512
523
  set_ftype_from_c_dir_entry
513
524
  parse_zip64_extra(false)
525
+ parse_aes_extra
514
526
  end
515
527
 
516
528
  def file_stat(path) # :nodoc:
@@ -559,7 +571,7 @@ module Zip
559
571
  end
560
572
 
561
573
  def pack_c_dir_entry # :nodoc:
562
- zip64 = @extra['Zip64']
574
+ zip64 = @extra[:zip64]
563
575
  [
564
576
  @header_signature,
565
577
  @version, # version of encoding software
@@ -578,14 +590,11 @@ module Zip
578
590
  zip64 && zip64.disk_start_number ? 0xFFFF : 0, # disk number start
579
591
  @internal_file_attributes, # file type (binary=0, text=1)
580
592
  @external_file_attributes, # native filesystem attributes
581
- zip64 && zip64.relative_header_offset ? 0xFFFFFFFF : @local_header_offset,
582
- @name,
583
- @extra,
584
- @comment
593
+ zip64 && zip64.relative_header_offset ? 0xFFFFFFFF : @local_header_offset
585
594
  ].pack('VCCvvvvvVVVvvvvvVV')
586
595
  end
587
596
 
588
- def write_c_dir_entry(io) # :nodoc:
597
+ def write_c_dir_entry(io, suppress_extra_fields: false) # :nodoc:
589
598
  prep_cdir_zip64_extra
590
599
 
591
600
  case @fstype
@@ -607,6 +616,7 @@ module Zip
607
616
  end
608
617
  end
609
618
 
619
+ @extra.suppress_fields!(suppress_extra_fields) if suppress_extra_fields
610
620
  io << pack_c_dir_entry
611
621
 
612
622
  io << @name
@@ -792,14 +802,28 @@ module Zip
792
802
  return unless zip64?
793
803
 
794
804
  if for_local_header
795
- @size, @compressed_size = @extra['Zip64'].parse(@size, @compressed_size)
805
+ @size, @compressed_size = @extra[:zip64].parse(@size, @compressed_size)
796
806
  else
797
- @size, @compressed_size, @local_header_offset = @extra['Zip64'].parse(
807
+ @size, @compressed_size, @local_header_offset = @extra[:zip64].parse(
798
808
  @size, @compressed_size, @local_header_offset
799
809
  )
800
810
  end
801
811
  end
802
812
 
813
+ def parse_aes_extra # :nodoc:
814
+ return unless aes?
815
+
816
+ if @extra[:aes].vendor_id != 'AE'
817
+ raise Error, "Unsupported encryption method #{@extra[:aes].vendor_id}"
818
+ end
819
+
820
+ unless ::Zip::AESEncryption::VERSIONS.include? @extra[:aes].vendor_version
821
+ raise Error, "Unsupported encryption style #{@extra[:aes].vendor_version}"
822
+ end
823
+
824
+ @compression_method = @extra[:aes].compression_method if ftype != :directory
825
+ end
826
+
803
827
  # For DEFLATED compression *only*: set the general purpose flags 1 and 2 to
804
828
  # indicate compression level. This seems to be mainly cosmetic but they are
805
829
  # generally set by other tools - including in docx files. It is these flags
@@ -830,7 +854,7 @@ module Zip
830
854
  # If we already have a ZIP64 extra (placeholder) then we must fill it in.
831
855
  if zip64? || @size.nil? || @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
832
856
  @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
833
- zip64 = @extra['Zip64'] || @extra.create('Zip64')
857
+ zip64 = @extra[:zip64] || @extra.create(:zip64)
834
858
 
835
859
  # Local header always includes size and compressed size.
836
860
  zip64.original_size = @size || 0
@@ -844,7 +868,7 @@ module Zip
844
868
  if (@size && @size >= 0xFFFFFFFF) || @compressed_size >= 0xFFFFFFFF ||
845
869
  @local_header_offset >= 0xFFFFFFFF
846
870
  @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
847
- zip64 = @extra['Zip64'] || @extra.create('Zip64')
871
+ zip64 = @extra[:zip64] || @extra.create(:zip64)
848
872
 
849
873
  # Central directory entry entries include whichever fields are necessary.
850
874
  zip64.original_size = @size if @size && @size >= 0xFFFFFFFF