Skip to content

Updating to version 3.x

Robert Haines edited this page Apr 8, 2024 · 30 revisions

General points to note

⚠️ Version 3.x requires at least Ruby 3.0. ⚠️

⚠️ From version 3.x onwards, all RubyZip source files declare frozen_string_literal: true. ⚠️

RubyZip options changes

⚠️ Please read this list in full as all of these points affects the operation, or behaviour, of the whole library in some way. ⚠️

There are a few options whose defaults have changed:

  • Zip64 extensions support is turned on by default. This should not affect reading files at all, but if you really need to ensure that Zip64 extensions are not used (say, because you need to support a really old version of ZIP) then you can turn them off. See the Configuration section of the README for details of this.

  • Zip::File#restore_times is now set to true by default. This means that when files are extracted from a ZIP archive, RubyZip will try to restore the timestamps to those stored within the archive.

  • Zip::File#restore_permissions is now set to true by default. This means that when files are extracted from a ZIP archive, RubyZip will try to restore the read, write and execute permissions to those stored within the archive.

Help with migrating to Rubyzip 3.x

In the next section we list the major API changes in version 3. To help you migrate we have done two things:

  • Wherever possible we have made the methods listed below compatible with both the 2.x API and 3.x API.
    • File#extract and Entry#extract are notable exceptions. See their descriptions below for details.
  • We have added configurable warnings to tell you where you are using version 2 API calls. Please note that this is as comprehensive as possible, but may not catch everything.

Suggested migration path

  1. Run your tests and satisfy yourself that everything is working as expected before going any further. (I know you know to do this, but this is a reminder for everyone else.)
  2. Update your dependencies to use version 2.4 of the Rubyzip gem (coming soon!).
  3. Run your tests - everything should still work as expected at this stage.
  4. Run your tests with --enable-frozen-string-literal set - everything should still work as expected at this stage.
  5. Run your tests with the environment variable RUBYZIP_V3_API_WARN set. E.g.:
    $ RUBYZIP_V3_API_WARN=1 rake test
    You should see warnings where you need to make changes, and if you are using an old version of Ruby.
  6. Make the required changes so that the messages disappear, or are at least minimised. See the next section for the details of the changes.
  7. Update your dependencies to use version 3.0 of the Rubyzip gem (coming soon!).
  8. Run your tests and check everything still works.

API changes

There are a number of places where the API has changed between version 2.x and 3.x

Zip::File

Most changes are due to methods now using named parameters.

::new

No changes in functionality, but now uses named parameters for readability:

-    def initialize(path_or_io, create = false, buffer = false, options = {})
+    def initialize(path_or_io, create: false, buffer: false,
+                   restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
+                   restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
+                   restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
+                   compression_level: ::Zip.default_compression)

In general, use of File::new is discouraged; favour ::open and ::open_buffer if possible.

::add_buffer

This method has been removed. Please use ::open_buffer instead.

::count_entries

This is a new method. Use this to count the number of entries in an archive without reading in the whole central directory, or stepping through all the entries with InputStream.

::open

No changes in functionality, but now uses named parameters for readability:

-      def open(file_name, create = false, options = {})
+      def open(file_name, create: false,
+               restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
+               restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
+               restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
+               compression_level: ::Zip.default_compression)

::open_buffer

No longer assumes that opening a buffer is to create a new archive in it. Also now uses named parameters for readability:

-      def open_buffer(io, **options)
+      def open_buffer(io = ::StringIO.new, create: false,
+                      restore_ownership: DEFAULT_RESTORE_OPTIONS[:restore_ownership],
+                      restore_permissions: DEFAULT_RESTORE_OPTIONS[:restore_permissions],
+                      restore_times: DEFAULT_RESTORE_OPTIONS[:restore_times],
+                      compression_level: ::Zip.default_compression)

::split

No changes in functionality, but now uses named parameters for readability:

-    def split(zip_file_name, segment_size = MAX_SEGMENT_SIZE, delete_zip_file = true, partial_zip_file_name = nil)
+    def split(zip_file_name, segment_size: MAX_SEGMENT_SIZE, delete_original: true, partial_zip_file_name: nil)

#extract

Major Update!

This method now forces extraction into the current working directory unless this is overridden by supplying a different directory via destination_directory.

-    def extract(entry, dest_path, &block)
+    def extract(entry, entry_path = nil, destination_directory: '.', &block)

⚠️ In Rubyzip 2.4, use File#extract_v3 for the version 3.x API.

This effectively splits the final location of an extracted entry into two parts:

  • the base directory (controlled by destination_directory and . by default); and
  • the entry path (controlled by entry_path and the entry's name by default).

So, if the current working directory is /tmp, then the following holds for various combinations of entry name and the parameters supplied to #extract:

entry name entry_path destination_directory resulting path
foo/bar.txt <not set> <not set> /tmp/foo/bar.txt
foo/bar.txt <not set> /home/me/work /home/me/work/foo/bar.txt
foo/bar.txt <not set> files /tmp/files/foo/bar.txt
foo/bar.txt baz.txt <not set> /tmp/baz.txt
foo/bar.txt baz.txt /home/me/work /home/me/work/baz.txt
foo/bar.txt baz.txt files /tmp/files/baz.txt
../bar.txt <not set> <not set> Extraction is skipped and a warning printed to stderr
../bar.txt <not set> /home/me/work Extraction is skipped and a warning printed to stderr
../bar.txt <not set> files Extraction is skipped and a warning printed to stderr
../bar.txt baz.txt <not set> /tmp/baz.txt
../bar.txt baz.txt /home/me/work /home/me/work/baz.txt
../bar.txt baz.txt files /tmp/files/baz.txt

The rationale for this change is to mitigate against so called 'path traversal' hacks. For example, a Zip file may contain an entry called ../etc/passwd in the hope that someone would unpack it in /tmp with raised privileges - without path traversal protection this would overwrite /etc/passwd.

The combination of destination_directory and the entry name (possibly overridden/replaced by entry_path) is checked for safety before extraction.

If previously you were doing something like this when using Zip::File#extract

dest_dir = '/tmp/my_app'

::Zip::File.open('zipfile.zip') do |zip|
  zip.extract('known_entry.txt', "#{dest_dir}/known_entry.txt")
end

then this will still work! But it can also now be written (and looks more readable) as

dest_dir = '/tmp/my_app'

::Zip::File.open('zipfile.zip') do |zip|
  zip.extract('known_entry.txt', destination_directory: dest_dir)
end

#get_output_stream

No changes in functionality, but now uses named parameters for readability:

-    def get_output_stream(entry, permission_int = nil, comment = nil,
-                          extra = nil, compressed_size = nil, crc = nil,
-                          compression_method = nil, compression_level = nil,
-                          size = nil, time = nil, &a_proc)
+    def get_output_stream(entry, permissions: nil, comment: nil,
+                          extra: nil, compressed_size: nil, crc: nil,
+                          compression_method: nil, compression_level: nil,
+                          size: nil, time: nil, &a_proc)

Zip::Entry

::new

No changes in functionality, but now uses named parameters for readability:

-    def initialize(*args)
+    def initialize(
+      zipfile = '', name = '',
+      comment: '', size: 0, compressed_size: 0, crc: 0,
+      compression_method: DEFLATED,
+      compression_level: ::Zip.default_compression,
+      time: ::Zip::DOSTime.now, extra: ::Zip::ExtraField.new
+    )

#extract

Major Update!

This method now forces extraction into the current working directory unless this is overridden by supplying a different directory via destination_directory.

-    def extract(dest_path = nil, &block)
+    def extract(entry_path = @name, destination_directory: '.', &block)

⚠️ In Rubyzip 2.4, use Entry#extract_v3 for the version 3.x API.

This effectively splits the final location of an extracted entry into two parts:

  • the base directory (controlled by destination_directory and . by default); and
  • the entry path (controlled by entry_path and the entry's name by default).

See File#extract, above, for the details and rationale for this change.

If previously you were doing something like this when using Zip::Entry#extract

dest_dir = '/tmp/my_app'

::Zip::File.open('zipfile.zip') do |zip|
  zip.entries do |entry|
    entry.extract(::File.join(dest_dir, entry.name))
  end
end

then this will still work! But it can also now be written (and looks more readable) as

dest_dir = '/tmp/my_app'

::Zip::File.open('zipfile.zip') do |zip|
  zip.entries do |entry|
    entry.extract(destination_directory: dest_dir)
  end
end

#mtime=

New method. A new alias of #time=

#zip64?

New method. A cleaner way to detect an Entry that has ZIP64 extensions present.

Zip::InputStream

::new

No changes in functionality, but now uses named parameters for readability:

-    def initialize(context, offset = 0, decrypter = nil)
+    def initialize(context, offset: 0, decrypter: nil)

::open

No changes in functionality, but now uses named parameters for readability:

-    def open(filename_or_io, offset = 0, decrypter = nil)
+    def open(filename_or_io, offset: 0, decrypter: nil)

::open_buffer

This method was deprecated in RubyZip version 2.3 and has been removed in version 3.0.

#read

This method now returns the empty string '' if it is passed zero for number_of_bytes.

#size

New method. This returns the uncompressed size of the current entry in bytes, or nil if there is no current entry.

Zip::OutputStream

::new

No changes in functionality, but now uses named parameters for readability:

-    def initialize(file_name, stream = false, encrypter = nil)
+    def initialize(file_name, stream: false, encrypter: nil)

::open

No changes in functionality, but now uses named parameters for readability:

-      def open(file_name, encrypter = nil)
+      def open(file_name, encrypter: nil)

::write_buffer

No changes in functionality, but now uses named parameters for readability:

-      def write_buffer(io = ::StringIO.new(''), encrypter = nil)
+      def write_buffer(io = ::StringIO.new(''), encrypter: nil)

Zip::DOSTime

#dos_equals

This method has been removed. Please use #== instead.