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 +4 -4
- data/Changelog.md +32 -0
- data/README.md +64 -13
- data/Rakefile +4 -6
- data/lib/zip/central_directory.rb +5 -3
- data/lib/zip/crypto/aes_encryption.rb +120 -0
- data/lib/zip/crypto/decrypted_io.rb +17 -13
- data/lib/zip/crypto/null_encryption.rb +0 -10
- data/lib/zip/crypto/traditional_encryption.rb +2 -0
- data/lib/zip/dos_time.rb +3 -2
- data/lib/zip/entry.rb +50 -26
- data/lib/zip/extra_field/aes.rb +50 -0
- data/lib/zip/extra_field/generic.rb +7 -1
- data/lib/zip/extra_field/unknown.rb +3 -1
- data/lib/zip/extra_field/zip64.rb +7 -0
- data/lib/zip/extra_field.rb +12 -4
- data/lib/zip/file.rb +26 -22
- data/lib/zip/filesystem/file.rb +7 -6
- data/lib/zip/filesystem/file_stat.rb +4 -4
- data/lib/zip/inflater.rb +4 -3
- data/lib/zip/input_stream.rb +31 -12
- data/lib/zip/ioextras/abstract_input_stream.rb +3 -2
- data/lib/zip/null_decompressor.rb +3 -2
- data/lib/zip/output_stream.rb +13 -9
- data/lib/zip/pass_thru_decompressor.rb +4 -3
- data/lib/zip/streamable_stream.rb +1 -1
- data/lib/zip/version.rb +2 -1
- data/lib/zip.rb +2 -0
- data/rubyzip.gemspec +3 -3
- data/samples/gtk_ruby_zip.rb +1 -1
- data/samples/qtzip.rb +1 -1
- metadata +14 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 169fa7d5832e775e15c2f8a409a35e9da32c0177117039f089f9fa1646aa281e
|
|
4
|
+
data.tar.gz: 9b4fb8929262cb097e3631ad8d8a9efa98d691488ce79e37092df4de27204aea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
[](http://badge.fury.io/rb/rubyzip)
|
|
4
4
|
[](https://github.com/rubyzip/rubyzip/actions/workflows/tests.yml)
|
|
5
5
|
[](https://github.com/rubyzip/rubyzip/actions/workflows/lint.yml)
|
|
6
|
-
[](https://github.com/rubocop/rubocop)
|
|
7
|
+
[](https://qlty.sh/gh/rubyzip/projects/rubyzip)
|
|
7
8
|
[](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
|
-
###
|
|
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
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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 (
|
|
247
|
+
### Password Protection (experimental)
|
|
205
248
|
|
|
206
|
-
Rubyzip supports reading
|
|
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
|
|
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 '
|
|
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
|
-
|
|
11
|
-
test.
|
|
12
|
-
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
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
35
|
+
!@bytes_remaining.positive?
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def produce_input
|
|
39
|
-
@
|
|
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
|
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?
|
|
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
|
-
|
|
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[
|
|
131
|
-
@extra[
|
|
132
|
-
elsif @extra[
|
|
133
|
-
@extra[
|
|
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?(
|
|
159
|
-
@extra.create(
|
|
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[
|
|
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?(
|
|
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[
|
|
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[
|
|
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
|
-
|
|
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[
|
|
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[
|
|
805
|
+
@size, @compressed_size = @extra[:zip64].parse(@size, @compressed_size)
|
|
796
806
|
else
|
|
797
|
-
@size, @compressed_size, @local_header_offset = @extra[
|
|
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[
|
|
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[
|
|
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
|