rubyzip 3.2.2 → 3.6.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: 67852d915e2ec168efb617d7577ac60196bf7433b3c2800981a14c5a43b939c5
4
- data.tar.gz: '039b39e7e9e7f46e056da4d0070554eba081e26a4dd99e3dd47d76428628d29f'
3
+ metadata.gz: 8ad461af594aaafc83f2a9af47a7e72681431835864534c174970e2245748764
4
+ data.tar.gz: 6c5bd535a0b049a606bd64ca90106e633c385926d91c32ca3db72314d2b031f0
5
5
  SHA512:
6
- metadata.gz: ba24363c26265acbd295289685d5baa4d351a913d98de67064171c35f0055f04ac34b2e6db384af248e124b12abc505bac558205eadb2975d3d4da59178b9a54
7
- data.tar.gz: b654167d21076c70ea58b6a185f4be83a6ef172d208bf4f8a8928ac82aa909773b20df462870dc833b1182f669334e18fd7283e23cf4040b770faea1a49d1e6a
6
+ metadata.gz: ecd014dee5b905fe5a85af4273e564a0abbdb9c75409a489363352999b2298bfa6baf311c4722c022ddd87807e402d2b9b03f54ec2c110a4cf8c882ed916d301
7
+ data.tar.gz: 7711a4b6bd467e46d020b9d36b58ad726c0ff312d2247e49c9030cbdfb53d206dd5e16ddbb265f34f3f9802c728de06517926de352db7fac8f43ceafd3a3f496
data/Changelog.md CHANGED
@@ -1,3 +1,56 @@
1
+ # 3.6.0 (2026-09-01)
2
+
3
+ - Forward options from OutputStream.open without a block. [#674](https://github.com/rubyzip/rubyzip/pull/674)
4
+ - Clamp DOS date and time to the range the fields can hold. [#671](https://github.com/rubyzip/rubyzip/pull/671)
5
+
6
+ Tooling/internal:
7
+
8
+ - Unix owner ids no longer default to 0 (root).
9
+ - Legacy Unix owner ids no longer default to 0 (root).
10
+ - Handle short reads in `copy_stream_n`. [#675](https://github.com/rubyzip/rubyzip/pull/675)
11
+ - Retain required Zip64 offset fields. [#676](https://github.com/rubyzip/rubyzip/pull/676)
12
+ - Load openssl only when AES encryption is used. [#672](https://github.com/rubyzip/rubyzip/pull/672)
13
+
14
+ # 3.5.0 (2026-08-18)
15
+
16
+ - Fix the link to Ruby doc in README to the latest version. [#670](https://github.com/rubyzip/rubyzip/pull/670)
17
+ - Support passing decrypter to encrypted entry via get_input_stream. [#667](https://github.com/rubyzip/rubyzip/pull/667)
18
+
19
+ # 3.4.1 (2026-06-27)
20
+
21
+ - Fixed `DecryptedIo` to only perform integrity once. [#665](https://github.com/rubyzip/rubyzip/pull/665)
22
+
23
+ # 3.4.0 (2026-06-14)
24
+
25
+ - Prevent entries from being extracted outside specified directory. [#664](https://github.com/rubyzip/rubyzip/issues/664). Thanks to @connorshea for additional reporting on this.
26
+ - Use `SecureRandom` in place of insecure `Random`.
27
+ - Stop reading the central directory on first error.
28
+ - Add a check on number of declared entries in a zip file. Thanks to @connorshea for reporting this.
29
+ - Add note to README re reporting security issues privately.
30
+ - Add lib/rubyzip.rb for Bundler auto-require. [#660](https://github.com/rubyzip/rubyzip/pull/660)
31
+
32
+ Tooling/internal:
33
+
34
+ - Replace the test Excel spreadsheet fixture.
35
+ - Use `assert_silent` shorthand when expecting no output from a test.
36
+ - Clean up CentralDirectory instance variables.
37
+ - Add Ruby 4.0 to the Windows CI and update CI matrix in the README.
38
+ - List ZIP docs that we store here and link to online versions. [#657](https://github.com/rubyzip/rubyzip/issues/657)
39
+
40
+ # 3.3.1 (2026-05-30)
41
+
42
+ - Reinstate default param for `InputStream#sysread`. [#663](https://github.com/rubyzip/rubyzip/issues/663)
43
+
44
+ # 3.3.0 (2026-05-02)
45
+
46
+ - Refactor `InputStream` and `AbstractInputStream`.
47
+ [#661](https://github.com/rubyzip/rubyzip/pull/661)
48
+
49
+ Tooling/internal:
50
+
51
+ - Update Actions to use checkout@v5.
52
+ - Add Ruby4.0 to the CI matrix. [#659](https://github.com/rubyzip/rubyzip/pull/659)
53
+
1
54
  # 3.2.2 (2025-11-02)
2
55
 
3
56
  - Fix reading EOCDs when header signatures are in an Entry payload. [#656](https://github.com/rubyzip/rubyzip/issues/656)
data/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  BSD 2-Clause License
2
2
 
3
- Copyright (c) 2002-2025, The Rubyzip Developers
3
+ Copyright (c) 2002-2026, The Rubyzip Developers
4
4
 
5
5
  Redistribution and use in source and binary forms, with or without
6
6
  modification, are permitted provided that the following conditions are met:
data/README.md CHANGED
@@ -11,6 +11,10 @@ Rubyzip is a ruby library for reading and writing zip files.
11
11
 
12
12
  ## Important notes
13
13
 
14
+ ### Reporting security issues with this library
15
+
16
+ If you think you have found a security issue with this library, please don't submit a public issue or PR. Email me directly at hainesr@gmail.com with as much information as you can provide - steps for replication are particularly helpful if you can - and we'll get it sorted ASAP. Thank you.
17
+
14
18
  ### Updating to version 3.0
15
19
 
16
20
  The public API of some classes has been modernized to use named parameters for optional arguments. Please check your usage of the following Rubyzip classes:
@@ -40,9 +44,12 @@ gem install rubyzip
40
44
  Or in your Gemfile:
41
45
 
42
46
  ```ruby
43
- gem 'rubyzip'
47
+ gem 'rubyzip', require: 'zip' # For versions before 3.4.
48
+ gem 'rubyzip' # For version 3.4 and after.
44
49
  ```
45
50
 
51
+ From version 3.4 onwards, you can `require` either 'zip' or 'rubyzip' to use this library. Before version 3.4 you need to `require 'zip'` explicitly.
52
+
46
53
  ## Usage
47
54
 
48
55
  ### Basic zip archive creation
@@ -194,7 +201,7 @@ On Posix file systems the default file permissions applied to a new archive
194
201
  are (0666 - umask), which mimics the behavior of standard tools such as `touch`.
195
202
 
196
203
  On Windows the default file permissions are set to 0644 as suggested by the
197
- [Ruby File documentation](http://ruby-doc.org/core-2.2.2/File.html).
204
+ [Ruby File documentation](https://docs.ruby-lang.org/en/master/File.html#class-file-file-permissions).
198
205
 
199
206
  When modifying a zip archive the file permissions of the archive are preserved.
200
207
 
@@ -294,6 +301,25 @@ Zip::InputStream.open(buffer, decrypter: dec) do |input|
294
301
  end
295
302
  ```
296
303
 
304
+ #### Passing a decrypter to an individual `Entry`
305
+
306
+ You can also decrypt an `Entry` by passing a decrypter into `Zip::Entry#get_input_stream`. This allows you to decrypt files from a Zip file with different passwords.
307
+
308
+ ```ruby
309
+ Zip::File.open('aes-encrypted-file.zip') do |zip|
310
+ entries = [['file1.txt', 'password1'], ['file2.txt', 'password2']]
311
+
312
+ entries.each do |entry_name, password|
313
+ entry = zip.find_entry(entry_name)
314
+ dec = Zip::AESDecrypter.new(password, Zip::AESEncryption::STRENGTH_256_BIT)
315
+ zis = entry.get_input_stream(decrypter: dec)
316
+
317
+ puts "Contents of '#{entry.name}':"
318
+ puts zis.read
319
+ end
320
+ end
321
+ ```
322
+
297
323
  _This is an evolving feature and the interface for encryption may change in future versions._
298
324
 
299
325
  ## Known issues
@@ -341,7 +367,7 @@ Zip.continue_on_exists_proc = true
341
367
 
342
368
  ### Non-ASCII Names
343
369
 
344
- If you want to store non-english names and want to open them on Windows(pre 7) you need to set this option:
370
+ To store non-English filenames as UTF-8 (decoded correctly by Windows 7 or later and other modern tools), set this option:
345
371
 
346
372
  ```ruby
347
373
  Zip.unicode_names = true
@@ -363,7 +389,19 @@ Some zip files might have an invalid date format, which will raise a warning. Yo
363
389
  Zip.warn_invalid_date = false
364
390
  ```
365
391
 
366
- ### Size Validation
392
+ ### Validating Declared Number of Entries
393
+
394
+ When reading a zip file it is potentially dangerous to trust what it tells you about how many entries it contains. A malformed zip file could claim a high number of entries in an attempt to waste internal resources, or crash the processing application.
395
+
396
+ By default rubyzip will warn if the number of declared entries is impossible based on the actual size of the Central Directory headers. You can set this check to raise an error:
397
+
398
+ ```ruby
399
+ Zip.validate_declared_number_of_entries = true
400
+ ```
401
+
402
+ It is likely that the default behaviour for this check will be changed to raise an error in version 4.
403
+
404
+ ### Entry Size Validation
367
405
 
368
406
  By default (in rubyzip >= 2.0), rubyzip's `extract` method checks that an entry's reported uncompressed size is not (significantly) smaller than its actual size. This is to help you protect your application against [zip bombs](https://en.wikipedia.org/wiki/Zip_bomb). Before `extract`ing an entry, you should check that its size is in the range you expect. For example, if your application supports processing up to 100 files at once, each up to 10MiB, your zip extraction code might look like:
369
407
 
@@ -445,11 +483,12 @@ Rubyzip 2.4 is known to work on MRI 2.4 to 3.4 on Linux and Mac, and JRuby and T
445
483
 
446
484
  Please see the table below for what we think the current situation is. Note: an empty cell means "unknown", not "does not work".
447
485
 
448
- | OS/Ruby | 3.0 | 3.1 | 3.2 | 3.3 | 3.4 | Head | JRuby 10.0.1.0 | JRuby Head | Truffleruby 24.2.1 | Truffleruby Head |
449
- |---------|-----|-----|-----|-----|-----|------|---------------|------------|--------------------|------------------|
450
- |Ubuntu 24.04| CI | CI | CI | CI | CI | ci | CI | ci | CI | ci |
451
- |Mac OS 14.7.6| CI | CI | CI | CI | CI | ci | x | | x | |
452
- |Windows Server 2022| CI | | | | CI&nbsp;mswin</br>CI&nbsp;ucrt | | | | | |
486
+ | OS/Ruby | 3.0 | 3.1 | 3.2 | 3.3 | 3.4 | 4.0 | Head | JRuby 10.0.1.0 | JRuby Head | Truffleruby 34.0.1 | Truffleruby Head |
487
+ |---------|-----|-----|-----|-----|-----|-----|------|----------------|------------|-------------------|------------------|
488
+ |Ubuntu 24.04| CI | CI | CI | CI | CI | CI | ci | CI | ci | CI | ci |
489
+ |Mac OS 15.7.7| CI | x | x | CI | CI | CI | ci | x | | x | |
490
+ |Windows Server 2022| CI | | | | | | | | | | |
491
+ |Windows Server 2025| | | | CI | | CI | CI&nbsp;mswin</br>CI&nbsp;ucrt | | | | |
453
492
 
454
493
  Key: `CI` - tested in CI, should work; `ci` - tested in CI, might fail; `x` - known working; `o` - known failing.
455
494
 
data/lib/rubyzip.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'zip'
@@ -114,18 +114,20 @@ module Zip
114
114
 
115
115
  def unpack_64_e_o_c_d(buffer) # :nodoc:
116
116
  _, # ZIP64_END_OF_CD_SIG. We know we have this at this point.
117
- @size_of_zip64_e_o_c_d,
118
- @version_made_by,
119
- @version_needed_for_extract,
120
- @number_of_this_disk,
121
- @number_of_disk_with_start_of_cdir,
122
- @total_number_of_entries_in_cdir_on_this_disk,
117
+ size_of_zip64_e_o_c_d,
118
+ _version_made_by,
119
+ _version_needed_for_extract,
120
+ _number_of_this_disk,
121
+ _number_of_disk_with_start_of_cdir,
122
+ _total_number_of_entries_in_cdir_on_this_disk,
123
123
  @size,
124
- @size_in_bytes,
124
+ size_in_bytes,
125
125
  @cdir_offset = buffer.unpack('VQ<vvVVQ<Q<Q<Q<')
126
126
 
127
+ validate_size!(@size, size_in_bytes)
128
+
127
129
  zip64_extensible_data_size =
128
- @size_of_zip64_e_o_c_d - ZIP64_STATIC_EOCD_SIZE + 12
130
+ size_of_zip64_e_o_c_d - ZIP64_STATIC_EOCD_SIZE + 12
129
131
  @zip64_extensible_data = if zip64_extensible_data_size.zero?
130
132
  ''
131
133
  else
@@ -145,25 +147,29 @@ module Zip
145
147
 
146
148
  # Unpack the EOCD and return a boolean indicating whether this header is
147
149
  # complete without needing Zip64 extensions.
148
- def unpack_e_o_c_d(buffer) # :nodoc: # rubocop:disable Naming/PredicateMethod
150
+ def unpack_e_o_c_d(buffer) # :nodoc:
149
151
  _, # END_OF_CD_SIG. We know we have this at this point.
150
- @number_of_this_disk,
151
- @number_of_disk_with_start_of_cdir,
152
- @total_number_of_entries_in_cdir_on_this_disk,
152
+ number_of_this_disk,
153
+ number_of_disk_with_start_of_cdir,
154
+ total_number_of_entries_in_cdir_on_this_disk,
153
155
  @size,
154
- @size_in_bytes,
156
+ size_in_bytes,
155
157
  @cdir_offset,
156
158
  comment_length = buffer.unpack('VvvvvVVv')
157
159
 
160
+ complete = !([number_of_this_disk, number_of_disk_with_start_of_cdir,
161
+ total_number_of_entries_in_cdir_on_this_disk, @size].any?(0xFFFF) ||
162
+ size_in_bytes == 0xFFFFFFFF || @cdir_offset == 0xFFFFFFFF)
163
+
164
+ validate_size!(@size, size_in_bytes) if complete
165
+
158
166
  @comment = if comment_length.positive?
159
167
  buffer.slice(STATIC_EOCD_SIZE, comment_length)
160
168
  else
161
169
  ''
162
170
  end
163
171
 
164
- !([@number_of_this_disk, @number_of_disk_with_start_of_cdir,
165
- @total_number_of_entries_in_cdir_on_this_disk, @size].any?(0xFFFF) ||
166
- @size_in_bytes == 0xFFFFFFFF || @cdir_offset == 0xFFFFFFFF)
172
+ complete
167
173
  end
168
174
 
169
175
  def read_central_directory_entries(io) # :nodoc:
@@ -180,7 +186,7 @@ module Zip
180
186
  @entry_set = EntrySet.new
181
187
  @size.times do
182
188
  entry = Entry.read_c_dir_entry(io)
183
- next unless entry
189
+ break unless entry
184
190
 
185
191
  offset = if entry.zip64?
186
192
  entry.extra[:zip64].relative_header_offset
@@ -258,6 +264,15 @@ module Zip
258
264
 
259
265
  [io.tell, io.read]
260
266
  end
267
+
268
+ def validate_size!(size, size_in_bytes)
269
+ return if size * CDIR_ENTRY_STATIC_HEADER_LENGTH <= size_in_bytes
270
+
271
+ error = EntryNumberMismatchError.new(size, size_in_bytes)
272
+ raise error if Zip.validate_declared_number_of_entries
273
+
274
+ warn error.message
275
+ end
261
276
  end
262
277
  end
263
278
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'openssl'
4
-
5
3
  module Zip
6
4
  module AESEncryption # :nodoc:
7
5
  VERIFIER_LENGTH = 2
@@ -45,6 +43,11 @@ module Zip
45
43
  }.freeze
46
44
 
47
45
  def initialize(password, strength)
46
+ # Loaded here rather than at the top of the file so that `require 'zip'`
47
+ # does not pull in openssl. Only AES-encrypted archives need it, and it
48
+ # is the single largest cost of loading this library.
49
+ require 'openssl'
50
+
48
51
  @password = password
49
52
  @strength = strength
50
53
  @bits = BITS[@strength]
@@ -8,21 +8,27 @@ module Zip
8
8
  @io = io
9
9
  @decrypter = decrypter
10
10
  @bytes_remaining = compressed_size
11
- @buffer = +''
11
+ @buffer = +''.b
12
+ # Ensures that integrity is only checked once,
13
+ # even if read is called multiple times after the input has finished.
14
+ @integrity_verified = false
12
15
  end
13
16
 
14
- def read(length = nil, outbuf = +'')
15
- return (length.nil? || length.zero? ? '' : nil) if eof?
17
+ def read(maxlen = nil)
18
+ return (maxlen.nil? || maxlen.zero? ? '' : nil) if eof?
16
19
 
17
- while length.nil? || (@buffer.bytesize < length)
20
+ while maxlen.nil? || (@buffer.bytesize < maxlen)
18
21
  break if input_finished?
19
22
 
20
23
  @buffer << produce_input
21
24
  end
22
25
 
23
- @decrypter.check_integrity!(@io) if input_finished?
26
+ if input_finished? && !@integrity_verified
27
+ @decrypter.check_integrity!(@io)
28
+ @integrity_verified = true
29
+ end
24
30
 
25
- outbuf.replace(@buffer.slice!(0...(length || @buffer.bytesize)))
31
+ @buffer.slice!(0...(maxlen || @buffer.bytesize))
26
32
  end
27
33
 
28
34
  private
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'securerandom'
4
+
3
5
  module Zip
4
6
  module TraditionalEncryption # :nodoc:
5
7
  def initialize(password)
@@ -44,7 +46,7 @@ module Zip
44
46
  def header(mtime)
45
47
  [].tap do |header|
46
48
  (header_bytesize - 2).times do
47
- header << Random.rand(0..255)
49
+ header << SecureRandom.rand(0..255)
48
50
  end
49
51
  header << (mtime.to_binary_dos_time & 0xff)
50
52
  header << (mtime.to_binary_dos_time >> 8)
data/lib/zip/dos_time.rb CHANGED
@@ -16,6 +16,18 @@ module Zip
16
16
  # bits 5-8 month (1-12)
17
17
  # bits 9-15 year (four digit year minus 1980)
18
18
 
19
+ # The MS-DOS date field is seven bits of "year minus 1980", so only
20
+ # 1980-01-01 00:00:00 through 2107-12-31 23:59:58 can be represented.
21
+ # Times outside that are clamped rather than allowed to wrap: the real
22
+ # time is still carried in the universal time (UT) extra field, but the
23
+ # DOS fields have to stay in range for readers that only look at those.
24
+ DOS_EPOCH_YEAR = 1980
25
+ DOS_MAX_YEAR = 2107
26
+ DOS_MIN_DATE = 0x0021 # 1980-01-01
27
+ DOS_MAX_DATE = 0xff9f # 2107-12-31
28
+ DOS_MIN_TIME = 0x0000 # 00:00:00
29
+ DOS_MAX_TIME = 0xbf7d # 23:59:58
30
+
19
31
  attr_writer :absolute_time # :nodoc:
20
32
 
21
33
  def absolute_time?
@@ -25,15 +37,21 @@ module Zip
25
37
  end
26
38
 
27
39
  def to_binary_dos_time
40
+ return DOS_MIN_TIME if year < DOS_EPOCH_YEAR
41
+ return DOS_MAX_TIME if year > DOS_MAX_YEAR
42
+
28
43
  (sec / 2) +
29
44
  (min << 5) +
30
45
  (hour << 11)
31
46
  end
32
47
 
33
48
  def to_binary_dos_date
49
+ return DOS_MIN_DATE if year < DOS_EPOCH_YEAR
50
+ return DOS_MAX_DATE if year > DOS_MAX_YEAR
51
+
34
52
  day +
35
53
  (month << 5) +
36
- ((year - 1980) << 9)
54
+ ((year - DOS_EPOCH_YEAR) << 9)
37
55
  end
38
56
 
39
57
  # Deprecated. Remove for version 4.
data/lib/zip/entry.rb CHANGED
@@ -290,7 +290,7 @@ module Zip
290
290
  dest_dir = ::File.absolute_path(destination_directory || '.')
291
291
  extract_path = ::File.absolute_path(::File.join(dest_dir, entry_path))
292
292
 
293
- unless extract_path.start_with?(dest_dir)
293
+ unless extract_path.start_with?(dest_dir + ::File::SEPARATOR) || dest_dir == ::File::SEPARATOR
294
294
  warn "WARNING: skipped extracting '#{@name}' to '#{extract_path}' as unsafe."
295
295
  return self
296
296
  end
@@ -638,8 +638,9 @@ module Zip
638
638
  end
639
639
 
640
640
  # Returns an IO like object for the given ZipEntry.
641
+ # Optional decrypter can be provided for an encrypted entry
641
642
  # Warning: may behave weird with symlinks.
642
- def get_input_stream(&block)
643
+ def get_input_stream(decrypter: nil, &block)
643
644
  if ftype == :directory
644
645
  yield ::Zip::NullInputStream if block
645
646
  ::Zip::NullInputStream
@@ -656,7 +657,7 @@ module Zip
656
657
  raise "unknown @file_type #{ftype}"
657
658
  end
658
659
  else
659
- zis = ::Zip::InputStream.new(@zipfile, offset: local_header_offset)
660
+ zis = ::Zip::InputStream.new(@zipfile, offset: local_header_offset, decrypter: decrypter)
660
661
  zis.instance_variable_set(:@complete_entry, self)
661
662
  zis.get_next_entry
662
663
  if block
@@ -751,21 +752,20 @@ module Zip
751
752
 
752
753
  def create_file(dest_path, _continue_on_exists_proc = proc { Zip.continue_on_exists_proc })
753
754
  if ::File.exist?(dest_path) && !yield(self, dest_path)
754
- raise ::Zip::DestinationExistsError, dest_path
755
+ raise DestinationExistsError, dest_path
755
756
  end
756
757
 
757
758
  ::File.open(dest_path, 'wb') do |os|
758
759
  get_input_stream do |is|
759
760
  bytes_written = 0
760
761
  warned = false
761
- buf = +''
762
- while (buf = is.sysread(::Zip::Decompressor::CHUNK_SIZE, buf))
762
+ while (buf = is.sysread(Decompressor::CHUNK_SIZE))
763
763
  os << buf
764
764
  bytes_written += buf.bytesize
765
765
  next unless bytes_written > size && !warned
766
766
 
767
- error = ::Zip::EntrySizeError.new(self)
768
- raise error if ::Zip.validate_entry_sizes
767
+ error = EntrySizeError.new(self)
768
+ raise error if Zip.validate_entry_sizes
769
769
 
770
770
  warn "WARNING: #{error.message}"
771
771
  warned = true
data/lib/zip/errors.rb CHANGED
@@ -109,6 +109,22 @@ module Zip
109
109
  end
110
110
  end
111
111
 
112
+ class EntryNumberMismatchError < Error
113
+ # Create a new EntryNumberMismatchError with the specified size and size in bytes.
114
+ def initialize(size, size_in_bytes)
115
+ super()
116
+ @size = size
117
+ @size_in_bytes = size_in_bytes
118
+ end
119
+
120
+ # The message returned by this error.
121
+ def message
122
+ "Zip consistency problem: an impossibly high number of entries (#{@size}) " \
123
+ 'is declared in this file, compared to the size of the central directory ' \
124
+ "(#{@size_in_bytes} bytes)."
125
+ end
126
+ end
127
+
112
128
  # Error raised if a split archive is read. Rubyzip does not support reading
113
129
  # split archives.
114
130
  class SplitArchiveError < Error
@@ -7,8 +7,8 @@ module Zip
7
7
  register_map
8
8
 
9
9
  def initialize(binstr = nil)
10
- @uid = 0
11
- @gid = 0
10
+ @uid = nil
11
+ @gid = nil
12
12
  @atime = nil
13
13
  @mtime = nil
14
14
  binstr && merge(binstr)
@@ -7,8 +7,8 @@ module Zip
7
7
  register_map
8
8
 
9
9
  def initialize(binstr = nil)
10
- @uid = 0
11
- @gid = 0
10
+ @uid = nil
11
+ @gid = nil
12
12
  binstr && merge(binstr)
13
13
  end
14
14
 
@@ -59,8 +59,8 @@ module Zip
59
59
  # We can suppress the zip64 extra field unless we know the size is large or
60
60
  # the relative header offset is large (for central directory entries).
61
61
  def suppress?
62
- !(@original_size && @original_size >= 0xFFFFFFFF) ||
63
- (@relative_header_offset && @relative_header_offset >= 0xFFFFFFFF)
62
+ !((@original_size && @original_size >= 0xFFFFFFFF) ||
63
+ (@relative_header_offset && @relative_header_offset >= 0xFFFFFFFF))
64
64
  end
65
65
 
66
66
  def pack_for_local
data/lib/zip/inflater.rb CHANGED
@@ -5,20 +5,20 @@ module Zip
5
5
  def initialize(*args)
6
6
  super
7
7
 
8
- @buffer = +''
8
+ @buffer = +''.b
9
9
  @zlib_inflater = ::Zlib::Inflate.new(-Zlib::MAX_WBITS)
10
10
  end
11
11
 
12
- def read(length = nil, outbuf = +'')
13
- return (length.nil? || length.zero? ? '' : nil) if eof?
12
+ def read(maxlen = nil)
13
+ return (maxlen.nil? || maxlen.zero? ? '' : nil) if eof?
14
14
 
15
- while length.nil? || (@buffer.bytesize < length)
15
+ while maxlen.nil? || (@buffer.bytesize < maxlen)
16
16
  break if input_finished?
17
17
 
18
18
  @buffer << produce_input
19
19
  end
20
20
 
21
- outbuf.replace(@buffer.slice!(0...(length || @buffer.bytesize)))
21
+ @buffer.slice!(0...(maxlen || @buffer.bytesize))
22
22
  end
23
23
 
24
24
  def eof?
@@ -89,9 +89,29 @@ module Zip
89
89
  open_entry
90
90
  end
91
91
 
92
- # Modeled after IO.sysread
93
- def sysread(length = nil, outbuf = +'')
94
- @decompressor.read(length, outbuf)
92
+ # Modelled after IO#sysread.
93
+ #
94
+ # Reads up to maxlen bytes from the stream; returns a string
95
+ # (either a new string or the given out_string).
96
+ # Its encoding is the unchanged encoding of out_string, if out_string is
97
+ # given; ASCII-8BIT, otherwise. Output contains maxlen bytes from the
98
+ # stream, if available; otherwise contains all available bytes, if any
99
+ # available; otherwise is an empty string.
100
+ #
101
+ # This method should not be used with buffered input stream-reader methods,
102
+ # such as #read, #readline, #gets.
103
+ def sysread(maxlen = nil, out_string = nil)
104
+ # Remove the default value for maxlen for version 4.
105
+ return (maxlen.nil? || maxlen.zero? ? '' : nil) if eof?
106
+
107
+ output = produce_input(maxlen)
108
+
109
+ if out_string.nil?
110
+ output.force_encoding(Encoding::ASCII_8BIT)
111
+ else
112
+ encoding = out_string.encoding
113
+ out_string.replace(output).force_encoding(encoding)
114
+ end
95
115
  end
96
116
 
97
117
  # Returns the size of the current entry, or `nil` if there isn't one.
@@ -197,8 +217,8 @@ module Zip
197
217
  decompressor_class.new(io, decompressed_size)
198
218
  end
199
219
 
200
- def produce_input # :nodoc:
201
- @decompressor.read(CHUNK_SIZE)
220
+ def produce_input(maxlen = CHUNK_SIZE) # :nodoc:
221
+ @decompressor.read(maxlen)
202
222
  end
203
223
 
204
224
  def input_finished? # :nodoc:
@@ -3,126 +3,208 @@
3
3
  module Zip
4
4
  module IOExtras # :nodoc:
5
5
  # Implements many of the convenience methods of IO
6
- # such as gets, getc, readline and readlines
6
+ # such as gets, getc, read, readline and readlines
7
7
  # depends on: input_finished?, produce_input and read
8
- module AbstractInputStream # :nodoc:
8
+ module AbstractInputStream
9
9
  include Enumerable
10
10
  include FakeIO
11
11
 
12
- def initialize
12
+ def initialize # :nodoc:
13
13
  super
14
14
  @lineno = 0
15
15
  @pos = 0
16
- @output_buffer = +''
16
+ @output_buffer = +''.b
17
17
  end
18
18
 
19
+ # Returns (or sets) the current line number in the decompressed
20
+ # (possibly decrypted) data stream. See the Line Number documentation
21
+ # for the IO class for more information.
19
22
  attr_accessor :lineno
23
+
24
+ # Returns the current position (in bytes) in the decompressed (possibly
25
+ # decrypted) data stream.
20
26
  attr_reader :pos
21
27
 
22
- def read(number_of_bytes = nil, buf = +'')
28
+ # Reads bytes from the stream decompressed (possibly decrypted) data
29
+ # stream. If `maxlen` is `nil`, reads all bytes; otherwise, reads up to
30
+ # `maxlen` bytes. If `maxlen` is zero, returns an empty string.
31
+ #
32
+ # Returns a string (either a new string or the given `out_string`)
33
+ # containing the bytes read. The string's encoding is the unchanged
34
+ # encoding of `out_string`, if `out_string` is given; `ASCII-8BIT`,
35
+ # otherwise.
36
+ def read(maxlen = nil, out_string = nil) # rubocop:disable Metrics/PerceivedComplexity
37
+ return (maxlen.nil? || maxlen.zero? ? '' : nil) if eof?
38
+
23
39
  tbuf = if @output_buffer.bytesize > 0
24
- if number_of_bytes && number_of_bytes <= @output_buffer.bytesize
25
- @output_buffer.slice!(0, number_of_bytes)
40
+ if maxlen && maxlen <= @output_buffer.bytesize
41
+ @output_buffer.slice!(0, maxlen)
26
42
  else
27
- number_of_bytes -= @output_buffer.bytesize if number_of_bytes
28
- rbuf = sysread(number_of_bytes, buf)
43
+ maxlen -= @output_buffer.bytesize if maxlen
44
+ rbuf = produce_input(maxlen)
29
45
  out = @output_buffer
30
46
  out << rbuf if rbuf
31
- @output_buffer = ''
47
+ @output_buffer = +''.b
32
48
  out
33
49
  end
34
50
  else
35
- sysread(number_of_bytes, buf)
51
+ produce_input(maxlen)
36
52
  end
37
53
 
38
54
  if tbuf.nil? || tbuf.empty?
39
- return nil if number_of_bytes&.positive?
55
+ return nil if maxlen&.positive?
40
56
 
41
57
  return ''
42
58
  end
43
59
 
44
60
  @pos += tbuf.length
45
61
 
46
- if buf
47
- buf.replace(tbuf)
62
+ if out_string.nil?
63
+ tbuf.force_encoding(Encoding::ASCII_8BIT)
48
64
  else
49
- buf = tbuf
65
+ encoding = out_string.encoding
66
+ out_string.replace(tbuf).force_encoding(encoding)
50
67
  end
51
- buf
52
68
  end
53
69
 
54
- def readlines(a_sep_string = $INPUT_RECORD_SEPARATOR)
55
- ret_val = []
56
- each_line(a_sep_string) { |line| ret_val << line }
57
- ret_val
70
+ # Reads and returns all remaining lines from the stream. See the Line IO
71
+ # documentation in the IO class for more information.
72
+ #
73
+ # With no arguments given, returns lines as determined by line
74
+ # separator `$/`, or `nil` if none.
75
+ #
76
+ # With only string argument `sep` given, returns lines as
77
+ # determined by line separator `sep`, or `nil` if none. See the
78
+ # Line Separator documentation in the IO class for more information.
79
+ # The two special values for `sep` (`nil` and `""`) are honoured.
80
+ #
81
+ # With only integer argument `limit` given, limits the number of bytes
82
+ # in each line; see the Line Limit documentation in the IO class for more
83
+ # information.
84
+ #
85
+ # With arguments `sep` and `limit` given, combines the two behaviors.
86
+ #
87
+ # Optional keyword argument `chomp` specifies whether line separators
88
+ # are to be omitted.
89
+ def readlines(sep = $INPUT_RECORD_SEPARATOR, limit = nil, chomp: false)
90
+ each(sep, limit, chomp: chomp).to_a
58
91
  end
59
92
 
60
- def gets(a_sep_string = $INPUT_RECORD_SEPARATOR, number_of_bytes = nil)
61
- @lineno = @lineno.next
93
+ # Reads and returns a line from the stream. See the Line IO
94
+ # documentation in the IO class for more information.
95
+ #
96
+ # With no arguments given, returns the next line as determined by line
97
+ # separator `$/`, or `nil` if none.
98
+ #
99
+ # With only string argument `sep` given, returns the next line as
100
+ # determined by line separator `sep`, or `nil` if none. See the
101
+ # Line Separator documentation in the IO class for more information.
102
+ # The two special values for `sep` (`nil` and `""`) are honoured.
103
+ #
104
+ # With only integer argument `limit` given, limits the number of bytes
105
+ # in the line; see the Line Limit documentation in the IO class for more
106
+ # information.
107
+ #
108
+ # With arguments `sep` and `limit` given, combines the two behaviors.
109
+ #
110
+ # Optional keyword argument `chomp` specifies whether line separators
111
+ # are to be omitted.
112
+ def gets(sep = $INPUT_RECORD_SEPARATOR, limit = nil, chomp: false)
113
+ if sep.nil?
114
+ return nil if eof?
115
+
116
+ @lineno = @lineno.next
117
+ return read(limit)
118
+ end
62
119
 
63
- if number_of_bytes.respond_to?(:to_int)
64
- number_of_bytes = number_of_bytes.to_int
65
- a_sep_string = a_sep_string.to_str if a_sep_string
66
- elsif a_sep_string.respond_to?(:to_int)
67
- number_of_bytes = a_sep_string.to_int
68
- a_sep_string = $INPUT_RECORD_SEPARATOR
69
- else
70
- number_of_bytes = nil
71
- a_sep_string = a_sep_string.to_str if a_sep_string
120
+ if sep.respond_to?(:to_int)
121
+ limit = sep.to_int
122
+ sep = $INPUT_RECORD_SEPARATOR
123
+ elsif sep&.empty?
124
+ sep = "#{$INPUT_RECORD_SEPARATOR}#{$INPUT_RECORD_SEPARATOR}"
72
125
  end
73
126
 
74
- return read(number_of_bytes) if a_sep_string.nil?
127
+ buffer_index = 0
128
+ while (sep_index = @output_buffer.index(sep, buffer_index)).nil?
129
+ break if limit && @output_buffer.bytesize >= limit
75
130
 
76
- a_sep_string = "#{$INPUT_RECORD_SEPARATOR}#{$INPUT_RECORD_SEPARATOR}" if a_sep_string.empty?
131
+ if input_finished?
132
+ return nil if @output_buffer.empty?
77
133
 
78
- buffer_index = 0
79
- over_limit = number_of_bytes && @output_buffer.bytesize >= number_of_bytes
80
- while (match_index = @output_buffer.index(a_sep_string, buffer_index)).nil? && !over_limit
81
- buffer_index = [buffer_index, @output_buffer.bytesize - a_sep_string.bytesize].max
82
- return @output_buffer.empty? ? nil : flush if input_finished?
134
+ @lineno = @lineno.next
135
+ @pos += @output_buffer.bytesize
136
+ return @output_buffer.slice!(0..)
137
+ end
83
138
 
139
+ buffer_index = [buffer_index, @output_buffer.bytesize - sep.bytesize].max
84
140
  @output_buffer << produce_input
85
- over_limit = number_of_bytes && @output_buffer.bytesize >= number_of_bytes
86
141
  end
87
- sep_index = [
88
- match_index + a_sep_string.bytesize,
89
- number_of_bytes || @output_buffer.bytesize
90
- ].min
91
- @pos += sep_index
92
- @output_buffer.slice!(0...sep_index)
142
+
143
+ limit ||= @output_buffer.bytesize
144
+ cut_index = sep_index ? [sep_index + sep.bytesize, limit].min : limit
145
+ @lineno = @lineno.next
146
+ @pos += cut_index
147
+ chomp ? @output_buffer.slice!(0, cut_index).chomp(sep) : @output_buffer.slice!(0, cut_index)
93
148
  end
94
149
 
95
- def ungetc(byte)
150
+ def ungetc(byte) # :nodoc:
96
151
  @output_buffer = byte.chr + @output_buffer
97
152
  end
98
153
 
99
- def flush
100
- ret_val = @output_buffer
101
- @output_buffer = +''
102
- ret_val
154
+ def flush # :nodoc:
155
+ @output_buffer.slice!(0..)
103
156
  end
104
157
 
105
- def readline(a_sep_string = $INPUT_RECORD_SEPARATOR)
106
- ret_val = gets(a_sep_string)
107
- raise EOFError unless ret_val
158
+ # Reads a line as with #gets, but raises `EOFError` if already at
159
+ # end-of-stream.
160
+ #
161
+ # Optional keyword argument `chomp` specifies whether line separators
162
+ # are to be omitted.
163
+ def readline(sep = $INPUT_RECORD_SEPARATOR, limit = nil, chomp: false)
164
+ raise EOFError if eof?
108
165
 
109
- ret_val
166
+ gets(sep, limit, chomp: chomp)
110
167
  end
111
168
 
112
- def each_line(a_sep_string = $INPUT_RECORD_SEPARATOR)
113
- loop { yield readline(a_sep_string) }
114
- rescue EOFError
115
- # We just need to catch this; we don't need to handle it.
169
+ # Calls the block with each remaining line read from the stream.
170
+ # Does nothing if already at end-of-stream. See the Line IO
171
+ # documentation in the IO class for more information.
172
+ #
173
+ # With no arguments given, reads lines as determined by line separator
174
+ # `$/`. With only string argument `sep` given, reads lines as determined
175
+ # by line separator `sep`. See the Line Separator documentation in the
176
+ # IO class for more information. The two special values for `sep`
177
+ # (`nil` and `""`) are honoured.
178
+ #
179
+ # With only integer argument `limit` given, limits the number of bytes
180
+ # in each line; see the Line Limit documentation in the IO class for
181
+ # more information.
182
+ #
183
+ # With arguments `sep` and `limit` given, combines the two behaviors.
184
+ #
185
+ # Optional keyword argument `chomp` specifies whether line separators
186
+ # are to be omitted.
187
+ #
188
+ # Returns an `Enumerator` if no block is given.
189
+ def each(sep = $INPUT_RECORD_SEPARATOR, limit = nil, chomp: false)
190
+ return to_enum(:each, sep, limit, chomp: chomp) unless block_given?
191
+
192
+ while (line = gets(sep, limit, chomp: chomp))
193
+ yield line
194
+ end
116
195
  end
117
196
 
118
- alias each each_line
197
+ alias each_line each
119
198
 
199
+ # Returns `true` if the stream is positioned at its end, `false`
200
+ # otherwise. See Position documentation in the IO class for more
201
+ # information.
120
202
  def eof?
121
203
  @output_buffer.empty? && input_finished?
122
204
  end
123
205
 
124
206
  # Alias for compatibility. Remove for version 4.
125
- alias eof eof?
207
+ alias eof eof? # :nodoc:
126
208
  end
127
209
  end
128
210
  end
data/lib/zip/ioextras.rb CHANGED
@@ -13,8 +13,11 @@ module Zip
13
13
  toread = nbytes
14
14
  while toread > 0 && !istream.eof?
15
15
  tr = [toread, CHUNK_SIZE].min
16
- ostream.write(istream.read(tr, +''))
17
- toread -= tr
16
+ chunk = istream.read(tr, +'')
17
+ break if !chunk || chunk.empty?
18
+
19
+ ostream.write(chunk)
20
+ toread -= chunk.bytesize
18
21
  end
19
22
  end
20
23
  end
@@ -53,7 +53,13 @@ module Zip
53
53
  # stream is passed to the block and closed when the block
54
54
  # returns.
55
55
  def open(file_name, encrypter: nil, suppress_extra_fields: false)
56
- return new(file_name) unless block_given?
56
+ unless block_given?
57
+ return new(
58
+ file_name,
59
+ encrypter: encrypter,
60
+ suppress_extra_fields: suppress_extra_fields
61
+ )
62
+ end
57
63
 
58
64
  zos = new(file_name, stream: false, encrypter: encrypter,
59
65
  suppress_extra_fields: suppress_extra_fields)
@@ -7,15 +7,15 @@ module Zip
7
7
  @read_so_far = 0
8
8
  end
9
9
 
10
- def read(length = nil, outbuf = +'')
11
- return (length.nil? || length.zero? ? '' : nil) if eof?
10
+ def read(maxlen = nil)
11
+ return (maxlen.nil? || maxlen.zero? ? '' : nil) if eof?
12
12
 
13
- if length.nil? || (@read_so_far + length) > decompressed_size
14
- length = decompressed_size - @read_so_far
13
+ if maxlen.nil? || (@read_so_far + maxlen) > decompressed_size
14
+ maxlen = decompressed_size - @read_so_far
15
15
  end
16
16
 
17
- @read_so_far += length
18
- input_stream.read(length, outbuf)
17
+ @read_so_far += maxlen
18
+ input_stream.read(maxlen)
19
19
  end
20
20
 
21
21
  def eof?
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.2.2'
5
+ VERSION = '3.6.0'
6
6
  end
data/lib/zip.rb CHANGED
@@ -54,7 +54,8 @@ module Zip
54
54
  :warn_invalid_date,
55
55
  :case_insensitive_match,
56
56
  :force_entry_names_encoding,
57
- :validate_entry_sizes
57
+ :validate_entry_sizes,
58
+ :validate_declared_number_of_entries
58
59
 
59
60
  DEFAULT_RESTORE_OPTIONS = {
60
61
  restore_ownership: false,
@@ -78,6 +79,7 @@ module Zip
78
79
  @case_insensitive_match = false
79
80
  @force_entry_names_encoding = nil
80
81
  @validate_entry_sizes = true
82
+ @validate_declared_number_of_entries = false # Set this to `true` in v4.0.0?
81
83
  end
82
84
 
83
85
  # Set options for RubyZip in one block.
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.2.2
4
+ version: 3.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Haines
@@ -135,6 +135,7 @@ files:
135
135
  - LICENSE.md
136
136
  - README.md
137
137
  - Rakefile
138
+ - lib/rubyzip.rb
138
139
  - lib/zip.rb
139
140
  - lib/zip/central_directory.rb
140
141
  - lib/zip/compressor.rb
@@ -195,9 +196,9 @@ licenses:
195
196
  - BSD-2-Clause
196
197
  metadata:
197
198
  bug_tracker_uri: https://github.com/rubyzip/rubyzip/issues
198
- changelog_uri: https://github.com/rubyzip/rubyzip/blob/v3.2.2/Changelog.md
199
- documentation_uri: https://www.rubydoc.info/gems/rubyzip/3.2.2
200
- source_code_uri: https://github.com/rubyzip/rubyzip/tree/v3.2.2
199
+ changelog_uri: https://github.com/rubyzip/rubyzip/blob/v3.6.0/Changelog.md
200
+ documentation_uri: https://www.rubydoc.info/gems/rubyzip/3.6.0
201
+ source_code_uri: https://github.com/rubyzip/rubyzip/tree/v3.6.0
201
202
  wiki_uri: https://github.com/rubyzip/rubyzip/wiki
202
203
  rubygems_mfa_required: 'true'
203
204
  rdoc_options: []
@@ -214,7 +215,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
214
215
  - !ruby/object:Gem::Version
215
216
  version: '0'
216
217
  requirements: []
217
- rubygems_version: 3.7.2
218
+ rubygems_version: 4.0.17
218
219
  specification_version: 4
219
220
  summary: rubyzip is a ruby module for reading and writing zip files
220
221
  test_files: []