Repository: rubyzip/rubyzip (ruby)

Issue:
Set compression on a per-operation basis
There may already be an API for this that I'm not seeing, but it would be nice to be able to set the compression on a per-operation basis.  Something along the lines of:

```  def write(compress: false)
    entries = Dir.entries(@inputDir); entries.delete("."); entries.delete("..")
    io = Zip::File.open(
      @outputFile, Zip::File::CREATE,
      { compression: Zlib::BEST_COMPRESSION } # New Part
    );

    writeEntries(entries, "", io)
    io.close();
  end```

Right at the moment, it only appears possible (based on the documentation) to set this setting globally, which is rather annoying.


Discussion:
Yes. Looking at this, it seems that there is an API a bit deeper in to be able to do this per entry even, but it's not exposed higher up the stack.

Before I go any further looking at this, what is the behaviour you are looking for here? Above you are suggesting that it would be good to be able to control this per zip file (I agree), but would it be useful to be able to control the compression per entry in a zip file too?
I honestly can't see any utility in doing it per file.  Setting it per operation is enough.

Patch:
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -9,21 +9,21 @@
 # Offense count: 5
 # Configuration parameters: CountComments.
 Metrics/ClassLength:
-  Max: 570
+  Max: 610
 
 # Offense count: 26
 Metrics/CyclomaticComplexity:
-  Max: 14
+  Max: 15
 
 # Offense count: 44
 # Configuration parameters: CountComments, ExcludedMethods.
 Metrics/MethodLength:
-  Max: 29
+  Max: 32
 
 # Offense count: 2
 # Configuration parameters: CountKeywordArgs.
 Metrics/ParameterLists:
-  Max: 10
+  Max: 12
 
 # Offense count: 20
 Metrics/PerceivedComplexity:
diff --git a/Changelog.md b/Changelog.md
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,5 +1,6 @@
 # X.X.X (Next)
 
+- Set compression level on a per-zipfile basis. [#448](https://github.com/rubyzip/rubyzip/pull/448)
 - Fix input stream partial read error. [#462](https://github.com/rubyzip/rubyzip/pull/462)
 
 Tooling:
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -288,15 +288,25 @@ Zip.validate_entry_sizes = false
 
 Note that if you use the lower level `Zip::InputStream` interface, `rubyzip` does *not* check the entry `size`s. In this case, the caller is responsible for making sure it does not read more data than expected from the input stream.
 
-### Default Compression
+### Compression level
 
-You can set the default compression level like so:
+When adding entries to a zip archive you can set the compression level to trade-off compressed size against compression speed. By default this is set to the same as the underlying Zlib library's default (`Zlib::DEFAULT_COMPRESSION`), which is somewhere in the middle.
+
+You can configure the default compression level with:
 
 ```ruby
-Zip.default_compression = Zlib::DEFAULT_COMPRESSION
+Zip.default_compression = X
 ```
 
-It defaults to `Zlib::DEFAULT_COMPRESSION`. Possible values are `Zlib::BEST_COMPRESSION`, `Zlib::DEFAULT_COMPRESSION` and `Zlib::NO_COMPRESSION`
+Where X is an integer between 0 and 9, inclusive. If this option is set to 0 (`Zlib::NO_COMPRESSION`) then entries will be stored in the zip archive uncompressed. A value of 1 (`Zlib::BEST_SPEED`) gives the fastest compression and 9 (`Zlib::BEST_COMPRESSION`) gives the smallest compressed file size.
+
+This can also be set for each archive as an option to `Zip::File`:
+
+```ruby
+Zip::File.open('foo.zip', Zip::File::CREATE, {compression_level: 9}) do |zip|
+  zip.add ...
+end
+```
 
 ### Zip64 Support
 
@@ -323,13 +333,22 @@ You can set multiple settings at the same time by using a block:
 
 ## Developing
 
-To run the test you need to do this:
+Install the dependencies:
 
-```
+```shell
 bundle install
+```
+
+Run the tests with `rake`:
+
+```shell
 rake
 ```
 
+Please also run `rubocop` over your changes.
+
+Our CI is here: https://travis-ci.org/github/rubyzip/rubyzip. Please note that `rubocop` is run as part of the CI configuration and will fail a build if errors are found.
+
 ## Website and Project Home
 
 http://github.com/rubyzip/rubyzip
@@ -338,15 +357,22 @@ http://rdoc.info/github/rubyzip/rubyzip/master/frames
 
 ## Authors
 
-Alexander Simonov ( alex at simonov.me)
+See https://github.com/rubyzip/rubyzip/graphs/contributors for a comprehensive list.
+
+### Current contributors
+
+* Robert Haines (@hainesr)
+* John Lees-Miller (@jdleesmiller)
 
-Alan Harper ( alan at aussiegeek.net)
+### Past contributors
 
-Thomas Sondergaard (thomas at sondergaard.cc)
+* Pavel Lobashov (@ShockwaveNN)
+* Oleksandr Simonov (@simonoff)
+* Alan Harper (@aussiegeek)
 
-Technorama Ltd. (oss-ruby-zip at technorama.net)
+### Original author
 
-extra-field support contributed by Tatsuki Sugiura (sugi at nemui.org)
+* Thomas Sondergaard
 
 ## License
 
diff --git a/lib/zip/entry.rb b/lib/zip/entry.rb
--- a/lib/zip/entry.rb
+++ b/lib/zip/entry.rb
@@ -1,19 +1,25 @@
 require 'pathname'
 module Zip
   class Entry
-    STORED   = 0
-    DEFLATED = 8
+    STORED   = ::Zip::COMPRESSION_METHOD_STORE
+    DEFLATED = ::Zip::COMPRESSION_METHOD_DEFLATE
+
     # Language encoding flag (EFS) bit
     EFS = 0b100000000000
 
-    attr_accessor :comment, :compressed_size, :crc, :extra, :compression_method,
-                  :name, :size, :local_header_offset, :zipfile, :fstype, :external_file_attributes,
-                  :internal_file_attributes,
-                  :gp_flags, :header_signature, :follow_symlinks,
-                  :restore_times, :restore_permissions, :restore_ownership,
-                  :unix_uid, :unix_gid, :unix_perms,
-                  :dirty
-    attr_reader :ftype, :filepath # :nodoc:
+    # Compression level flags (used as part of the gp flags).
+    COMPRESSION_LEVEL_SUPERFAST_GPFLAG = 0b110
+    COMPRESSION_LEVEL_FAST_GPFLAG = 0b100
+    COMPRESSION_LEVEL_MAX_GPFLAG = 0b010
+
+    attr_accessor :comment, :compressed_size, :follow_symlinks, :name,
+                  :restore_ownership, :restore_permissions, :restore_times,
+                  :size, :unix_gid, :unix_perms, :unix_uid, :zipfile
+
+    attr_accessor :crc, :dirty, :external_file_attributes, :fstype, :gp_flags,
+                  :internal_file_attributes, :local_header_offset # :nodoc:
+
+    attr_reader :extra, :compression_level, :ftype, :filepath # :nodoc:
 
     def set_default_vars_values
       @local_header_offset      = 0
@@ -52,25 +58,33 @@ def check_name(name)
       raise ::Zip::EntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
     end
 
-    def initialize(*args)
-      name = args[1] || ''
-      check_name(name)
+    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
+    )
+      @name = name
+      check_name(@name)
 
       set_default_vars_values
       @fstype = ::Zip::RUNNING_ON_WINDOWS ? ::Zip::FSTYPE_FAT : ::Zip::FSTYPE_UNIX
+      @ftype = name_is_directory? ? :directory : :file
 
-      @zipfile            = args[0] || ''
-      @name               = name
-      @comment            = args[2] || ''
-      @extra              = args[3] || ''
-      @compressed_size    = args[4] || 0
-      @crc                = args[5] || 0
-      @compression_method = args[6] || ::Zip::Entry::DEFLATED
-      @size               = args[7] || 0
-      @time               = args[8] || ::Zip::DOSTime.now
+      @zipfile            = zipfile
+      @comment            = comment
+      @compression_method = compression_method
+      @compression_level  = compression_level
 
-      @ftype = name_is_directory? ? :directory : :file
-      @extra = ::Zip::ExtraField.new(@extra.to_s) unless @extra.kind_of?(::Zip::ExtraField)
+      @compressed_size    = compressed_size
+      @crc                = crc
+      @size               = size
+      @time               = time
+      @extra              =
+        extra.kind_of?(ExtraField) ? extra : ExtraField.new(extra.to_s)
+
+      set_compression_level_flags
     end
 
     def encrypted?
@@ -81,6 +95,14 @@ def incomplete?
       gp_flags & 8 == 8
     end
 
+    def extra=(field)
+      @extra = if field.nil?
+                 ExtraField.new
+               else
+                 field.kind_of?(ExtraField) ? field : ExtraField.new(field.to_s)
+               end
+    end
+
     def time
       if @extra['UniversalTime']
         @extra['UniversalTime'].mtime
@@ -103,6 +125,16 @@ def time=(value)
       @time = value
     end
 
+    def compression_method
+      return STORED if @ftype == :directory || @compression_level == 0
+
+      @compression_method
+    end
+
+    def compression_method=(method)
+      @compression_method = (@ftype == :directory ? STORED : method)
+    end
+
     def file_type_is?(type)
       raise InternalError, "current filetype is unknown: #{inspect}" unless @ftype
 
@@ -281,7 +313,7 @@ def pack_local_entry
       [::Zip::LOCAL_ENTRY_SIGNATURE,
        @version_needed_to_extract, # version needed to extract
        @gp_flags, # @gp_flags
-       @compression_method,
+       compression_method,
        @time.to_binary_dos_time, # @last_mod_time
        @time.to_binary_dos_date, # @last_mod_date
        @crc,
@@ -363,7 +395,7 @@ def check_c_dir_entry_static_header_length(buf)
     end
 
     def check_c_dir_entry_signature
-      return if header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
+      return if @header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
 
       raise Error, "Zip local header magic not found at location '#{local_header_offset}'"
     end
@@ -447,7 +479,7 @@ def pack_c_dir_entry
         @fstype, # filesystem type
         @version_needed_to_extract, # @versionNeededToExtract
         @gp_flags, # @gp_flags
-        @compression_method,
+        compression_method,
         @time.to_binary_dos_time, # @last_mod_time
         @time.to_binary_dos_date, # @last_mod_date
         @crc,
@@ -572,10 +604,12 @@ def gather_fileinfo_from_srcpath(src_path) # :nodoc:
 
     def write_to_zip_output_stream(zip_output_stream) #:nodoc:all
       if @ftype == :directory
-        zip_output_stream.put_next_entry(self, nil, nil, ::Zip::Entry::STORED)
+        zip_output_stream.put_next_entry(self)
       elsif @filepath
-        zip_output_stream.put_next_entry(self, nil, nil, compression_method || ::Zip::Entry::DEFLATED)
-        get_input_stream { |is| ::Zip::IOExtras.copy_stream(zip_output_stream, is) }
+        zip_output_stream.put_next_entry(self)
+        get_input_stream do |is|
+          ::Zip::IOExtras.copy_stream(zip_output_stream, is)
+        end
       else
         zip_output_stream.copy_raw_entry(self)
       end
@@ -673,6 +707,27 @@ def data_descriptor_size
       (@gp_flags & 0x0008) > 0 ? 16 : 0
     end
 
+    # For DEFLATED compression *only*: set the general purpose flags 1 and 2 to
+    # indicate compression level. This seems to be mainly cosmetic but they are
+    # generally set by other tools - including in docx files. It is these flags
+    # that are used by commandline tools (and elsewhere) to give an indication
+    # of how compressed a file is. See the PKWARE APPNOTE for more information:
+    # https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
+    #
+    # It's safe to simply OR these flags here as compression_level is read only.
+    def set_compression_level_flags
+      return unless compression_method == DEFLATED
+
+      case @compression_level
+      when 1
+        @gp_flags |= COMPRESSION_LEVEL_SUPERFAST_GPFLAG
+      when 2
+        @gp_flags |= COMPRESSION_LEVEL_FAST_GPFLAG
+      when 8, 9
+        @gp_flags |= COMPRESSION_LEVEL_MAX_GPFLAG
+      end
+    end
+
     # create a zip64 extra information field if we need one
     def prep_zip64_extra(for_local_header) #:nodoc:all
       return unless ::Zip.write_zip64_support
diff --git a/lib/zip/file.rb b/lib/zip/file.rb
--- a/lib/zip/file.rb
+++ b/lib/zip/file.rb
@@ -75,7 +75,9 @@ class File < CentralDirectory
     # a new archive if it doesn't exist already.
     def initialize(path_or_io, create = false, buffer = false, options = {})
       super()
-      options  = DEFAULT_OPTIONS.merge(options)
+      options  = DEFAULT_OPTIONS
+                 .merge(compression_level: ::Zip.default_compression)
+                 .merge(options)
       @name    = path_or_io.respond_to?(:path) ? path_or_io.path : path_or_io
       @comment = ''
       @create  = create ? true : false # allow any truthy value to mean true
@@ -111,6 +113,7 @@ def initialize(path_or_io, create = false, buffer = false, options = {})
       @restore_ownership   = options[:restore_ownership]
       @restore_permissions = options[:restore_permissions]
       @restore_times       = options[:restore_times]
+      @compression_level   = options[:compression_level]
     end
 
     class << self
@@ -266,14 +269,19 @@ def get_input_stream(entry, &a_proc)
     # File.open method.
     def get_output_stream(entry, permission_int = nil, comment = nil,
                           extra = nil, compressed_size = nil, crc = nil,
-                          compression_method = nil, size = nil, time = nil,
-                          &a_proc)
+                          compression_method = nil, compression_level = nil,
+                          size = nil, time = nil, &a_proc)
 
       new_entry =
         if entry.kind_of?(Entry)
           entry
         else
-          Entry.new(@name, entry.to_s, comment, extra, compressed_size, crc, compression_method, size, time)
+          Entry.new(
+            @name, entry.to_s, comment: comment, extra: extra,
+            compressed_size: compressed_size, crc: crc, size: size,
+            compression_method: compression_method,
+            compression_level: compression_level, time: time
+          )
         end
       if new_entry.directory?
         raise ArgumentError,
@@ -299,7 +307,14 @@ def read(entry)
     def add(entry, src_path, &continue_on_exists_proc)
       continue_on_exists_proc ||= proc { ::Zip.continue_on_exists_proc }
       check_entry_exists(entry, continue_on_exists_proc, 'add')
-      new_entry = entry.kind_of?(::Zip::Entry) ? entry : ::Zip::Entry.new(@name, entry.to_s)
+      new_entry = if entry.kind_of?(::Zip::Entry)
+                    entry
+                  else
+                    ::Zip::Entry.new(
+                      @name, entry.to_s,
+                      compression_level: @compression_level
+                    )
+                  end
       new_entry.gather_fileinfo_from_srcpath(src_path)
       new_entry.dirty = true
       @entry_set << new_entry
@@ -308,7 +323,9 @@ def add(entry, src_path, &continue_on_exists_proc)
     # Convenience method for adding the contents of a file to the archive
     # in Stored format (uncompressed)
     def add_stored(entry, src_path, &continue_on_exists_proc)
-      entry = ::Zip::Entry.new(@name, entry.to_s, nil, nil, nil, nil, ::Zip::Entry::STORED)
+      entry = ::Zip::Entry.new(
+        @name, entry.to_s, compression_method: ::Zip::Entry::STORED
+      )
       add(entry, src_path, &continue_on_exists_proc)
     end
 
diff --git a/lib/zip/output_stream.rb b/lib/zip/output_stream.rb
--- a/lib/zip/output_stream.rb
+++ b/lib/zip/output_stream.rb
@@ -89,20 +89,23 @@ def close_buffer
 
     # Closes the current entry and opens a new for writing.
     # +entry+ can be a ZipEntry object or a string.
-    def put_next_entry(entry_name, comment = nil, extra = nil, compression_method = Entry::DEFLATED, level = Zip.default_compression)
+    def put_next_entry(
+      entry_name, comment = '', extra = ExtraField.new,
+      compression_method = Entry::DEFLATED, level = Zip.default_compression
+    )
       raise Error, 'zip stream is closed' if @closed
 
       new_entry = if entry_name.kind_of?(Entry)
                     entry_name
                   else
-                    Entry.new(@file_name, entry_name.to_s)
+                    Entry.new(
+                      @file_name, entry_name.to_s, comment: comment,
+                      extra: extra, compression_method: compression_method,
+                      compression_level: level
+                    )
                   end
-      new_entry.comment = comment unless comment.nil?
-      unless extra.nil?
-        new_entry.extra = extra.kind_of?(ExtraField) ? extra : ExtraField.new(extra.to_s)
-      end
-      new_entry.compression_method = compression_method unless compression_method.nil?
-      init_next_entry(new_entry, level)
+
+      init_next_entry(new_entry)
       @current_entry = new_entry
     end
 
@@ -142,19 +145,19 @@ def finalize_current_entry
       @compressor = ::Zip::NullCompressor.instance
     end
 
-    def init_next_entry(entry, level = Zip.default_compression)
+    def init_next_entry(entry)
       finalize_current_entry
       @entry_set << entry
       entry.write_local_entry(@output_stream)
       @encrypter.reset!
       @output_stream << @encrypter.header(entry.mtime)
-      @compressor = get_compressor(entry, level)
+      @compressor = get_compressor(entry)
     end
 
-    def get_compressor(entry, level)
+    def get_compressor(entry)
       case entry.compression_method
       when Entry::DEFLATED
-        ::Zip::Deflater.new(@output_stream, level, @encrypter)
+        ::Zip::Deflater.new(@output_stream, entry.compression_level, @encrypter)
       when Entry::STORED
         ::Zip::PassThruCompressor.new(@output_stream)
       else


Test patch:
diff --git a/test/case_sensitivity_test.rb b/test/case_sensitivity_test.rb
--- a/test/case_sensitivity_test.rb
+++ b/test/case_sensitivity_test.rb
@@ -7,7 +7,7 @@ class ZipCaseSensitivityTest < MiniTest::Test
                ['test/data/file2.txt', 'testFILE.rb']]
 
   def teardown
-    ::Zip.case_insensitive_match = false
+    ::Zip.reset!
   end
 
   # Ensure that everything functions normally when +case_insensitive_match = false+
diff --git a/test/central_directory_test.rb b/test/central_directory_test.rb
--- a/test/central_directory_test.rb
+++ b/test/central_directory_test.rb
@@ -38,9 +38,14 @@ def test_read_from_truncated_zip_file
   end
 
   def test_write_to_stream
-    entries = [::Zip::Entry.new('file.zip', 'flimse', 'myComment', 'somethingExtra'),
-               ::Zip::Entry.new('file.zip', 'secondEntryName'),
-               ::Zip::Entry.new('file.zip', 'lastEntry.txt', 'Has a comment too')]
+    entries = [
+      ::Zip::Entry.new(
+        'file.zip', 'flimse',
+        comment: 'myComment', extra: 'somethingExtra'
+      ),
+      ::Zip::Entry.new('file.zip', 'secondEntryName'),
+      ::Zip::Entry.new('file.zip', 'lastEntry.txt', comment: 'Has a comment')
+    ]
 
     cdir = ::Zip::CentralDirectory.new(entries, 'my zip comment')
     File.open('test/data/generated/cdirtest.bin', 'wb') do |f|
@@ -57,10 +62,26 @@ def test_write_to_stream
 
   def test_write64_to_stream
     ::Zip.write_zip64_support = true
-    entries = [::Zip::Entry.new('file.zip', 'file1-little', 'comment1', '', 200, 101, ::Zip::Entry::STORED, 200),
-               ::Zip::Entry.new('file.zip', 'file2-big', 'comment2', '', 18_000_000_000, 102, ::Zip::Entry::DEFLATED, 20_000_000_000),
-               ::Zip::Entry.new('file.zip', 'file3-alsobig', 'comment3', '', 15_000_000_000, 103, ::Zip::Entry::DEFLATED, 21_000_000_000),
-               ::Zip::Entry.new('file.zip', 'file4-little', 'comment4', '', 100, 104, ::Zip::Entry::DEFLATED, 121)]
+    entries = [
+      ::Zip::Entry.new(
+        'file.zip', 'file1-little', comment: 'comment1', size: 200,
+        compressed_size: 200, crc: 101,
+        compression_method: ::Zip::Entry::STORED
+      ),
+      ::Zip::Entry.new(
+        'file.zip', 'file2-big', comment: 'comment2',
+        size: 20_000_000_000, compressed_size: 18_000_000_000, crc: 102
+      ),
+      ::Zip::Entry.new(
+        'file.zip', 'file3-alsobig', comment: 'comment3',
+        size: 21_000_000_000, compressed_size: 15_000_000_000, crc: 103
+      ),
+      ::Zip::Entry.new(
+        'file.zip', 'file4-little', comment: 'comment4',
+        size: 121, compressed_size: 100, crc: 104
+      )
+    ]
+
     [0, 250, 18_000_000_300, 33_000_000_350].each_with_index do |offset, index|
       entries[index].local_header_offset = offset
     end
@@ -80,25 +101,37 @@ def test_write64_to_stream
   end
 
   def test_equality
-    cdir1 = ::Zip::CentralDirectory.new([::Zip::Entry.new('file.zip', 'flimse', nil,
-                                                          'somethingExtra'),
-                                         ::Zip::Entry.new('file.zip', 'secondEntryName'),
-                                         ::Zip::Entry.new('file.zip', 'lastEntry.txt')],
-                                        'my zip comment')
-    cdir2 = ::Zip::CentralDirectory.new([::Zip::Entry.new('file.zip', 'flimse', nil,
-                                                          'somethingExtra'),
-                                         ::Zip::Entry.new('file.zip', 'secondEntryName'),
-                                         ::Zip::Entry.new('file.zip', 'lastEntry.txt')],
-                                        'my zip comment')
-    cdir3 = ::Zip::CentralDirectory.new([::Zip::Entry.new('file.zip', 'flimse', nil,
-                                                          'somethingExtra'),
-                                         ::Zip::Entry.new('file.zip', 'secondEntryName'),
-                                         ::Zip::Entry.new('file.zip', 'lastEntry.txt')],
-                                        'comment?')
-    cdir4 = ::Zip::CentralDirectory.new([::Zip::Entry.new('file.zip', 'flimse', nil,
-                                                          'somethingExtra'),
-                                         ::Zip::Entry.new('file.zip', 'lastEntry.txt')],
-                                        'comment?')
+    cdir1 = ::Zip::CentralDirectory.new(
+      [
+        ::Zip::Entry.new('file.zip', 'flimse', extra: 'somethingExtra'),
+        ::Zip::Entry.new('file.zip', 'secondEntryName'),
+        ::Zip::Entry.new('file.zip', 'lastEntry.txt')
+      ],
+      'my zip comment'
+    )
+    cdir2 = ::Zip::CentralDirectory.new(
+      [
+        ::Zip::Entry.new('file.zip', 'flimse', extra: 'somethingExtra'),
+        ::Zip::Entry.new('file.zip', 'secondEntryName'),
+        ::Zip::Entry.new('file.zip', 'lastEntry.txt')
+      ],
+      'my zip comment'
+    )
+    cdir3 = ::Zip::CentralDirectory.new(
+      [
+        ::Zip::Entry.new('file.zip', 'flimse', extra: 'somethingExtra'),
+        ::Zip::Entry.new('file.zip', 'secondEntryName'),
+        ::Zip::Entry.new('file.zip', 'lastEntry.txt')
+      ],
+      'comment?'
+    )
+    cdir4 = ::Zip::CentralDirectory.new(
+      [
+        ::Zip::Entry.new('file.zip', 'flimse', extra: 'somethingExtra'),
+        ::Zip::Entry.new('file.zip', 'lastEntry.txt')
+      ],
+      'comment?'
+    )
     assert_equal(cdir1, cdir1)
     assert_equal(cdir1, cdir2)
 
diff --git a/test/deflater_test.rb b/test/deflater_test.rb
--- a/test/deflater_test.rb
+++ b/test/deflater_test.rb
@@ -8,6 +8,10 @@ class DeflaterTest < MiniTest::Test
   DEFAULT_COMP_FILE = 'test/data/generated/compressiontest_default_compression.bin'
   NO_COMP_FILE = 'test/data/generated/compressiontest_no_compression.bin'
 
+  def teardown
+    Zip.reset!
+  end
+
   def test_output_operator
     txt = load_file('test/data/file2.txt')
     deflate(txt, DEFLATER_TEST_FILE)
diff --git a/test/encryption_test.rb b/test/encryption_test.rb
--- a/test/encryption_test.rb
+++ b/test/encryption_test.rb
@@ -5,12 +5,11 @@ class EncryptionTest < MiniTest::Test
   INPUT_FILE1 = 'test/data/file1.txt'
 
   def setup
-    @default_compression = Zip.default_compression
     Zip.default_compression = ::Zlib::DEFAULT_COMPRESSION
   end
 
   def teardown
-    Zip.default_compression = @default_compression
+    Zip.reset!
   end
 
   def test_encrypt
diff --git a/test/entry_set_test.rb b/test/entry_set_test.rb
--- a/test/entry_set_test.rb
+++ b/test/entry_set_test.rb
@@ -2,12 +2,12 @@
 
 class ZipEntrySetTest < MiniTest::Test
   ZIP_ENTRIES = [
-    ::Zip::Entry.new('zipfile.zip', 'name1', 'comment1'),
-    ::Zip::Entry.new('zipfile.zip', 'name3', 'comment1'),
-    ::Zip::Entry.new('zipfile.zip', 'name2', 'comment1'),
-    ::Zip::Entry.new('zipfile.zip', 'name4', 'comment1'),
-    ::Zip::Entry.new('zipfile.zip', 'name5', 'comment1'),
-    ::Zip::Entry.new('zipfile.zip', 'name6', 'comment1')
+    ::Zip::Entry.new('zipfile.zip', 'name1', comment: 'comment1'),
+    ::Zip::Entry.new('zipfile.zip', 'name3', comment: 'comment1'),
+    ::Zip::Entry.new('zipfile.zip', 'name2', comment: 'comment1'),
+    ::Zip::Entry.new('zipfile.zip', 'name4', comment: 'comment1'),
+    ::Zip::Entry.new('zipfile.zip', 'name5', comment: 'comment1'),
+    ::Zip::Entry.new('zipfile.zip', 'name6', comment: 'comment1')
   ]
 
   def setup
@@ -20,13 +20,17 @@ def teardown
 
   def test_include
     assert(@zip_entry_set.include?(ZIP_ENTRIES.first))
-    assert(!@zip_entry_set.include?(::Zip::Entry.new('different.zip', 'different', 'aComment')))
+    assert(
+      !@zip_entry_set.include?(
+        ::Zip::Entry.new('different.zip', 'different', comment: 'aComment')
+      )
+    )
   end
 
   def test_size
     assert_equal(ZIP_ENTRIES.size, @zip_entry_set.size)
     assert_equal(ZIP_ENTRIES.size, @zip_entry_set.length)
-    @zip_entry_set << ::Zip::Entry.new('a', 'b', 'c')
+    @zip_entry_set << ::Zip::Entry.new('a', 'b', comment: 'c')
     assert_equal(ZIP_ENTRIES.size + 1, @zip_entry_set.length)
   end
 
@@ -66,7 +70,9 @@ def test_entries
   end
 
   def test_find_entry
-    entries = [::Zip::Entry.new('zipfile.zip', 'MiXeDcAsEnAmE', 'comment1')]
+    entries = [
+      ::Zip::Entry.new('zipfile.zip', 'MiXeDcAsEnAmE', comment: 'comment1')
+    ]
 
     ::Zip.case_insensitive_match = true
     zip_entry_set = ::Zip::EntrySet.new(entries)
@@ -96,7 +102,9 @@ def test_entries_sorted_in_each
   end
 
   def test_compound
-    new_entry = ::Zip::Entry.new('zf.zip', 'new entry', "new entry's comment")
+    new_entry = ::Zip::Entry.new(
+      'zf.zip', 'new entry', comment: "new entry's comment"
+    )
     assert_equal(ZIP_ENTRIES.size, @zip_entry_set.size)
     @zip_entry_set << new_entry
     assert_equal(ZIP_ENTRIES.size + 1, @zip_entry_set.size)
diff --git a/test/entry_test.rb b/test/entry_test.rb
--- a/test/entry_test.rb
+++ b/test/entry_test.rb
@@ -3,16 +3,19 @@
 class ZipEntryTest < MiniTest::Test
   include ZipEntryData
 
+  def teardown
+    ::Zip.reset!
+  end
+
   def test_constructor_and_getters
-    entry = ::Zip::Entry.new(TEST_ZIPFILE,
-                             TEST_NAME,
-                             TEST_COMMENT,
-                             TEST_EXTRA,
-                             TEST_COMPRESSED_SIZE,
-                             TEST_CRC,
-                             TEST_COMPRESSIONMETHOD,
-                             TEST_SIZE,
-                             TEST_TIME)
+    entry = ::Zip::Entry.new(
+      TEST_ZIPFILE, TEST_NAME,
+      comment: TEST_COMMENT, extra: TEST_EXTRA,
+      compressed_size: TEST_COMPRESSED_SIZE,
+      crc: TEST_CRC, size: TEST_SIZE, time: TEST_TIME,
+      compression_method: TEST_COMPRESSIONMETHOD,
+      compression_level: TEST_COMPRESSIONLEVEL
+    )
 
     assert_equal(TEST_COMMENT, entry.comment)
     assert_equal(TEST_COMPRESSED_SIZE, entry.compressed_size)
@@ -39,30 +42,54 @@ def test_is_directory_and_is_file
   end
 
   def test_equality
-    entry1 = ::Zip::Entry.new('file.zip', 'name', 'isNotCompared',
-                              'something extra', 123, 1234,
-                              ::Zip::Entry::DEFLATED, 10_000)
-    entry2 = ::Zip::Entry.new('file.zip', 'name', 'isNotComparedXXX',
-                              'something extra', 123, 1234,
-                              ::Zip::Entry::DEFLATED, 10_000)
-    entry3 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
-                              'something extra', 123, 1234,
-                              ::Zip::Entry::DEFLATED, 10_000)
-    entry4 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
-                              'something extraXX', 123, 1234,
-                              ::Zip::Entry::DEFLATED, 10_000)
-    entry5 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
-                              'something extraXX', 12, 1234,
-                              ::Zip::Entry::DEFLATED, 10_000)
-    entry6 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
-                              'something extraXX', 12, 123,
-                              ::Zip::Entry::DEFLATED, 10_000)
-    entry7 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
-                              'something extraXX', 12, 123,
-                              ::Zip::Entry::STORED, 10_000)
-    entry8 = ::Zip::Entry.new('file.zip', 'name2', 'isNotComparedXXX',
-                              'something extraXX', 12, 123,
-                              ::Zip::Entry::STORED, 100_000)
+    entry1 = ::Zip::Entry.new(
+      'file.zip', 'name',
+      comment: 'isNotCompared', extra: 'something extra',
+      compressed_size: 123, crc: 1234, size: 10_000
+    )
+
+    entry2 = ::Zip::Entry.new(
+      'file.zip', 'name',
+      comment: 'isNotComparedXXX', extra: 'something extra',
+      compressed_size: 123, crc: 1234, size: 10_000
+    )
+
+    entry3 = ::Zip::Entry.new(
+      'file.zip', 'name2',
+      comment: 'isNotComparedXXX', extra: 'something extra',
+      compressed_size: 123, crc: 1234, size: 10_000
+    )
+
+    entry4 = ::Zip::Entry.new(
+      'file.zip', 'name2',
+      comment: 'isNotComparedXXX', extra: 'something extraXX',
+      compressed_size: 123, crc: 1234, size: 10_000
+    )
+
+    entry5 = ::Zip::Entry.new(
+      'file.zip', 'name2',
+      comment: 'isNotComparedXXX', extra: 'something extraXX',
+      compressed_size: 12, crc: 1234, size: 10_000
+    )
+
+    entry6 = ::Zip::Entry.new(
+      'file.zip', 'name2',
+      comment: 'isNotComparedXXX', extra: 'something extraXX',
+      compressed_size: 12, crc: 123, size: 10_000
+    )
+
+    entry7 = ::Zip::Entry.new(
+      'file.zip', 'name2', comment: 'isNotComparedXXX',
+      extra: 'something extraXX', compressed_size: 12, crc: 123, size: 10_000,
+      compression_method: ::Zip::Entry::STORED
+    )
+
+    entry8 = ::Zip::Entry.new(
+      'file.zip', 'name2',
+      comment: 'isNotComparedXXX', extra: 'something extraXX',
+      compressed_size: 12, crc: 123, size: 100_000,
+      compression_method: ::Zip::Entry::STORED
+    )
 
     assert_equal(entry1, entry1)
     assert_equal(entry1, entry2)
@@ -130,13 +157,11 @@ def test_store_file_without_compression
     end
 
     zipfile = Zip::File.open('/tmp/no_compress.zip', Zip::File::CREATE)
-    mimetype_entry = Zip::Entry.new(zipfile,                # @zipfile
-                                    'mimetype',             # @name
-                                    '',                     # @comment
-                                    '',                     # @extra
-                                    0,                      # @compressed_size
-                                    0,                      # @crc
-                                    Zip::Entry::STORED)     # @comppressed_method
+    mimetype_entry = Zip::Entry.new(
+      zipfile,                # @zipfile
+      'mimetype',             # @name
+      compression_method: Zip::Entry::STORED
+    )
 
     zipfile.add(mimetype_entry, 'test/data/mimetype')
 
@@ -169,4 +194,64 @@ def test_incomplete?
     entry.gp_flags = 0
     assert_equal(false, entry.incomplete?)
   end
+
+  def test_compression_level_flags
+    [
+      [Zip.default_compression, 0],
+      [0, 0],
+      [1, 6],
+      [2, 4],
+      [3, 0],
+      [7, 0],
+      [8, 2],
+      [9, 2]
+    ].each do |level, flags|
+      # Check flags are set correctly when DEFLATED is (implicitly) specified.
+      e_def = Zip::Entry.new(
+        '', '',
+        compression_level: level
+      )
+      assert_equal(flags, e_def.gp_flags & 0b110)
+
+      # Check that flags are not set when STORED is specified.
+      e_sto = Zip::Entry.new(
+        '', '',
+        compression_method: Zip::Entry::STORED,
+        compression_level:  level
+      )
+      assert_equal(0, e_sto.gp_flags & 0b110)
+    end
+
+    # Check that a directory entry's flags are not set, even if DEFLATED
+    # is specified.
+    e_dir = Zip::Entry.new(
+      '', 'd/', compression_method: Zip::Entry::DEFLATED, compression_level: 1
+    )
+    assert_equal(0, e_dir.gp_flags & 0b110)
+  end
+
+  def test_compression_method_reader
+    [
+      [Zip.default_compression, Zip::Entry::DEFLATED],
+      [0, Zip::Entry::STORED],
+      [1, Zip::Entry::DEFLATED],
+      [9, Zip::Entry::DEFLATED]
+    ].each do |level, method|
+      # Check that the correct method is returned when DEFLATED is specified.
+      entry = Zip::Entry.new(compression_level: level)
+      assert_equal(method, entry.compression_method)
+    end
+
+    # Check that the correct method is returned when STORED is specified.
+    entry = Zip::Entry.new(
+      compression_method: Zip::Entry::STORED, compression_level: 1
+    )
+    assert_equal(Zip::Entry::STORED, entry.compression_method)
+
+    # Check that directories are always STORED, whatever level is specified.
+    entry = Zip::Entry.new(
+      '', 'd/', compression_method: Zip::Entry::DEFLATED, compression_level: 1
+    )
+    assert_equal(Zip::Entry::STORED, entry.compression_method)
+  end
 end
diff --git a/test/file_test.rb b/test/file_test.rb
--- a/test/file_test.rb
+++ b/test/file_test.rb
@@ -8,7 +8,7 @@ class ZipFileTest < MiniTest::Test
   OK_DELETE_MOVED_FILE = 'test/data/generated/okToDeleteMoved.txt'
 
   def teardown
-    ::Zip.write_zip64_support = false
+    ::Zip.reset!
   end
 
   def test_create_from_scratch_to_buffer
@@ -77,7 +77,10 @@ def test_get_output_stream
       assert_equal(count + 1, zf.size)
       assert_equal('Putting stuff in data/generated/empty.txt', zf.read('test/data/generated/empty.txt'))
 
-      custom_entry_args = [TEST_COMMENT, TEST_EXTRA, TEST_COMPRESSED_SIZE, TEST_CRC, ::Zip::Entry::STORED, TEST_SIZE, TEST_TIME]
+      custom_entry_args = [
+        TEST_COMMENT, TEST_EXTRA, TEST_COMPRESSED_SIZE, TEST_CRC,
+        ::Zip::Entry::STORED, ::Zlib::BEST_SPEED, TEST_SIZE, TEST_TIME
+      ]
       zf.get_output_stream('entry_with_custom_args.txt', nil, *custom_entry_args) do |os|
         os.write 'Some data'
       end
@@ -87,8 +90,9 @@ def test_get_output_stream
       assert_equal(custom_entry_args[2], entry.compressed_size)
       assert_equal(custom_entry_args[3], entry.crc)
       assert_equal(custom_entry_args[4], entry.compression_method)
-      assert_equal(custom_entry_args[5], entry.size)
-      assert_equal(custom_entry_args[6], entry.time)
+      assert_equal(custom_entry_args[5], entry.compression_level)
+      assert_equal(custom_entry_args[6], entry.size)
+      assert_equal(custom_entry_args[7], entry.time)
 
       zf.get_output_stream('entry.bin') do |os|
         os.write(::File.open('test/data/generated/5entry.zip', 'rb').read)
@@ -188,7 +192,7 @@ def test_cleans_up_tempfiles_after_close
     assert_equal(false, File.exist?(@tempfile_path))
   end
 
-  def test_add
+  def test_add_default_compression
     src_file = 'test/data/file2.txt'
     entry_name = 'newEntryName.rb'
     assert(::File.exist?(src_file))
@@ -197,9 +201,84 @@ def test_add
     zf.close
 
     zf_read = ::Zip::File.new(EMPTY_FILENAME)
+    entry = zf_read.entries.first
     assert_equal('', zf_read.comment)
     assert_equal(1, zf_read.entries.length)
     assert_equal(entry_name, zf_read.entries.first.name)
+    assert_equal(File.size(src_file), entry.size)
+    assert_equal(8_764, entry.compressed_size)
+    AssertEntry.assert_contents(src_file,
+                                zf_read.get_input_stream(entry_name, &:read))
+  end
+
+  def test_add_best_compression
+    src_file = 'test/data/file2.txt'
+    entry_name = 'newEntryName.rb'
+    assert(::File.exist?(src_file))
+    zf = ::Zip::File.new(EMPTY_FILENAME, ::Zip::File::CREATE, false, { compression_level: Zlib::BEST_COMPRESSION })
+    zf.add(entry_name, src_file)
+    zf.close
+
+    zf_read = ::Zip::File.new(EMPTY_FILENAME)
+    entry = zf_read.entries.first
+    assert_equal(1, zf_read.entries.length)
+    assert_equal(File.size(src_file), entry.size)
+    assert_equal(8_658, entry.compressed_size)
+    AssertEntry.assert_contents(src_file,
+                                zf_read.get_input_stream(entry_name, &:read))
+  end
+
+  def test_add_best_compression_as_default
+    ::Zip.default_compression = Zlib::BEST_COMPRESSION
+
+    src_file = 'test/data/file2.txt'
+    entry_name = 'newEntryName.rb'
+    assert(::File.exist?(src_file))
+    zf = ::Zip::File.new(EMPTY_FILENAME, ::Zip::File::CREATE)
+    zf.add(entry_name, src_file)
+    zf.close
+
+    zf_read = ::Zip::File.new(EMPTY_FILENAME)
+    entry = zf_read.entries.first
+    assert_equal(1, zf_read.entries.length)
+    assert_equal(File.size(src_file), entry.size)
+    assert_equal(8_658, entry.compressed_size)
+    AssertEntry.assert_contents(src_file,
+                                zf_read.get_input_stream(entry_name, &:read))
+  end
+
+  def test_add_best_speed
+    src_file = 'test/data/file2.txt'
+    entry_name = 'newEntryName.rb'
+    assert(::File.exist?(src_file))
+    zf = ::Zip::File.new(EMPTY_FILENAME, ::Zip::File::CREATE, false, { compression_level: Zlib::BEST_SPEED })
+    zf.add(entry_name, src_file)
+    zf.close
+
+    zf_read = ::Zip::File.new(EMPTY_FILENAME)
+    entry = zf_read.entries.first
+    assert_equal(1, zf_read.entries.length)
+    assert_equal(File.size(src_file), entry.size)
+    assert_equal(10_938, entry.compressed_size)
+    AssertEntry.assert_contents(src_file,
+                                zf_read.get_input_stream(entry_name, &:read))
+  end
+
+  def test_add_best_speed_as_default
+    ::Zip.default_compression = Zlib::BEST_SPEED
+
+    src_file = 'test/data/file2.txt'
+    entry_name = 'newEntryName.rb'
+    assert(::File.exist?(src_file))
+    zf = ::Zip::File.new(EMPTY_FILENAME, ::Zip::File::CREATE)
+    zf.add(entry_name, src_file)
+    zf.close
+
+    zf_read = ::Zip::File.new(EMPTY_FILENAME)
+    entry = zf_read.entries.first
+    assert_equal(1, zf_read.entries.length)
+    assert_equal(File.size(src_file), entry.size)
+    assert_equal(10_938, entry.compressed_size)
     AssertEntry.assert_contents(src_file,
                                 zf_read.get_input_stream(entry_name, &:read))
   end
diff --git a/test/local_entry_test.rb b/test/local_entry_test.rb
--- a/test/local_entry_test.rb
+++ b/test/local_entry_test.rb
@@ -5,7 +5,7 @@ class ZipLocalEntryTest < MiniTest::Test
   LEH_FILE = 'test/data/generated/localEntryHeader.bin'
 
   def teardown
-    ::Zip.write_zip64_support = false
+    ::Zip.reset!
   end
 
   def test_read_local_entry_header_of_first_test_zip_entry
@@ -53,9 +53,11 @@ def test_read_local_entry_from_truncated_zip_file
   end
 
   def test_write_entry
-    entry = ::Zip::Entry.new('file.zip', 'entry_name', 'my little comment',
-                             'thisIsSomeExtraInformation', 100, 987_654,
-                             ::Zip::Entry::DEFLATED, 400)
+    entry = ::Zip::Entry.new(
+      'file.zip', 'entry_name', comment: 'my little comment', size: 400,
+      extra: 'thisIsSomeExtraInformation', compressed_size: 100, crc: 987_654
+    )
+
     write_to_file(LEH_FILE, CEH_FILE, entry)
     local_entry, central_entry = read_from_file(LEH_FILE, CEH_FILE)
     assert(
@@ -68,9 +70,10 @@ def test_write_entry
 
   def test_write_entry_with_zip64
     ::Zip.write_zip64_support = true
-    entry = ::Zip::Entry.new('file.zip', 'entry_name', 'my little comment',
-                             'thisIsSomeExtraInformation', 100, 987_654,
-                             ::Zip::Entry::DEFLATED, 400)
+    entry = ::Zip::Entry.new(
+      'file.zip', 'entry_name', comment: 'my little comment', size: 400,
+      extra: 'thisIsSomeExtraInformation', compressed_size: 100, crc: 987_654
+    )
 
     write_to_file(LEH_FILE, CEH_FILE, entry)
     local_entry, central_entry = read_from_file(LEH_FILE, CEH_FILE)
@@ -92,10 +95,12 @@ def test_write_entry_with_zip64
 
   def test_write_64entry
     ::Zip.write_zip64_support = true
-    entry = ::Zip::Entry.new('bigfile.zip', 'entry_name', 'my little equine',
-                             'malformed extra field because why not',
-                             0x7766554433221100, 0xDEADBEEF, ::Zip::Entry::DEFLATED,
-                             0x9988776655443322)
+    entry = ::Zip::Entry.new(
+      'bigfile.zip', 'entry_name', comment: 'my little equine',
+      extra: 'malformed extra field because why not', size: 0x9988776655443322,
+      compressed_size: 0x7766554433221100, crc: 0xDEADBEEF
+    )
+
     write_to_file(LEH_FILE, CEH_FILE, entry)
     local_entry, central_entry = read_from_file(LEH_FILE, CEH_FILE)
     compare_local_entry_headers(entry, local_entry)
diff --git a/test/output_stream_test.rb b/test/output_stream_test.rb
--- a/test/output_stream_test.rb
+++ b/test/output_stream_test.rb
@@ -92,7 +92,9 @@ def test_put_next_entry
   def test_put_next_entry_using_zip_entry_creates_entries_with_correct_timestamps
     file = ::File.open('test/data/file2.txt', 'rb')
     ::Zip::OutputStream.open(TEST_ZIP.zip_name) do |zos|
-      zip_entry = ::Zip::Entry.new(zos, file.path, '', '', 0, 0, ::Zip::Entry::DEFLATED, 0, ::Zip::DOSTime.at(file.mtime))
+      zip_entry = ::Zip::Entry.new(
+        zos, file.path, time: ::Zip::DOSTime.at(file.mtime)
+      )
       zos.put_next_entry(zip_entry)
       zos << file.read
     end
diff --git a/test/test_helper.rb b/test/test_helper.rb
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -218,6 +218,7 @@ module ZipEntryData
   TEST_CRC = 325_324
   TEST_EXTRA = 'Some data here'
   TEST_COMPRESSIONMETHOD = ::Zip::Entry::DEFLATED
+  TEST_COMPRESSIONLEVEL = ::Zip.default_compression
   TEST_NAME = 'entry name'
   TEST_SIZE = 8432
   TEST_ISDIRECTORY = false
diff --git a/test/unicode_file_names_and_comments_test.rb b/test/unicode_file_names_and_comments_test.rb
--- a/test/unicode_file_names_and_comments_test.rb
+++ b/test/unicode_file_names_and_comments_test.rb
@@ -3,6 +3,10 @@
 class ZipUnicodeFileNamesAndComments < MiniTest::Test
   FILENAME = File.join(File.dirname(__FILE__), 'test1.zip')
 
+  def teardown
+    ::Zip.reset!
+  end
+
   def test_unicode_file_name
     file_entrys = ['текстовыйфайл.txt', 'Résumé.txt', '슬레이어스휘.txt']
     directory_entrys = ['папка/текстовыйфайл.txt', 'Résumé/Résumé.txt', '슬레이어스휘/슬레이어스휘.txt']