Skip to content

Updating to version 3.x

Robert Haines edited this page Apr 16, 2023 · 30 revisions

General points to note

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

  • Version 3.x requires at least ruby 2.5.
  • 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.

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, **options)

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, **options)

::open_buffer

Don't assume 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, **options)

::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)

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)

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

#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)