Repository: rubyzip/rubyzip (ruby)

Issue:
Can't set time with get_output_stream
I'm trying to put an entry with some arbitrary timestamp into the zip file, but it doesn't work, it always writes `Time.now`:
```ruby
require 'zip'
time = Time.new '2020-01-01' # some random date
Zip::File.open 'foo.zip', create: true do |zip|
  e = Zip::Entry.new zip.name, 'some_file'
  e.time = Zip::DOSTime.from_time time
  zip.get_output_stream(e) {|s| s.puts 'foobar' }
end
```
But unzip reports the current date:
```
$ unzip -l foo.zip 
Archive:  foo.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
        7  11-06-2021 17:56   some_file
---------                     -------
        7                     1 file
```

It looks like `File#get_output_stream` adds a `StreamableStream` to `@entry_set`, but later in `OutputStream#put_next_entry` it checks `entry_name.kind_of?(Entry)`, which will be false and it creates a new `Entry` ignoring everything I've set up in the entry. The same happens if I don't create the entry manually but pass the time to `#get_output_stream`.
`File#add` works correctly, but it requires creating a temporary file and manually setting the last modification date on it.
Using ruby 3.0.2p107 and rubyzip 2.3.2.


Discussion:
Hi @u3shit, thanks for reporting this.

I think I see what's going wrong here: presumably the type mismatch between `StreamableStream` and `Entry`. I wasn't around when this was written, so I don't know the subtleties of why `StreamableStream` is a delegate, rather than a subclass, of `Entry`, but just naively widening the `kind_of?` check you mention above causes plenty of other errors.

I can see a way to a fix though, so I'll get this sorted as soon as I can.

Patch:
diff --git a/lib/zip/entry.rb b/lib/zip/entry.rb
--- a/lib/zip/entry.rb
+++ b/lib/zip/entry.rb
@@ -83,14 +83,21 @@ def initialize(
       @ftype = name_is_directory? ? :directory : :file
 
       @zipfile            = zipfile
-      @comment            = comment
-      @compression_method = compression_method
-      @compression_level  = compression_level
-
-      @compressed_size    = compressed_size
-      @crc                = crc
-      @size               = size
-      @time               = time
+      @comment            = comment || ''
+      @compression_method = compression_method || DEFLATED
+      @compression_level  = compression_level || ::Zip.default_compression
+
+      @compressed_size    = compressed_size || 0
+      @crc                = crc || 0
+      @size               = size || 0
+      @time               = case time
+                            when ::Zip::DOSTime
+                              time
+                            when Time
+                              ::Zip::DOSTime.from_time(time)
+                            else
+                              ::Zip::DOSTime.now
+                            end
       @extra              =
         extra.kind_of?(ExtraField) ? extra : ExtraField.new(extra.to_s)
 
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
@@ -100,15 +100,15 @@ def put_next_entry(
     )
       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, comment: comment,
-                      extra: extra, compression_method: compression_method,
-                      compression_level: level
-                    )
-                  end
+      new_entry =
+        if entry_name.kind_of?(Entry) || entry_name.kind_of?(StreamableStream)
+          entry_name
+        else
+          Entry.new(
+            @file_name, entry_name.to_s, comment: comment, extra: extra,
+            compression_method: compression_method, compression_level: level
+          )
+        end
 
       init_next_entry(new_entry)
       @current_entry = new_entry


Test patch:
diff --git a/test/entry_test.rb b/test/entry_test.rb
--- a/test/entry_test.rb
+++ b/test/entry_test.rb
@@ -26,7 +26,10 @@ def test_constructor_and_getters
     assert_equal(TEST_COMPRESSIONMETHOD, entry.compression_method)
     assert_equal(TEST_NAME, entry.name)
     assert_equal(TEST_SIZE, entry.size)
-    assert_equal(TEST_TIME, entry.time)
+
+    # Reverse times when testing because we need to use DOSTime#== for the
+    # comparison, not Time#==.
+    assert_equal(entry.time, TEST_TIME)
   end
 
   def test_is_directory_and_is_file
diff --git a/test/file_test.rb b/test/file_test.rb
--- a/test/file_test.rb
+++ b/test/file_test.rb
@@ -95,7 +95,10 @@ def test_get_output_stream
         custom_entry_args[:compression_level], entry.compression_level
       )
       assert_equal(custom_entry_args[:size], entry.size)
-      assert_equal(custom_entry_args[:time], entry.time)
+
+      # Reverse times when testing because we need to use DOSTime#== for the
+      # comparison, not Time#==.
+      assert_equal(entry.time, custom_entry_args[:time])
 
       zf.get_output_stream('entry.bin') do |os|
         os.write(::File.open('test/data/generated/5entry.zip', 'rb').read)
@@ -110,6 +113,24 @@ def test_get_output_stream
     end
   end
 
+  def test_get_output_stream_with_entry
+    Dir.mktmpdir do |tmp|
+      test_zip = File.join(tmp, 'test.zip')
+      time = Time.new(1999, 12, 31)
+
+      ::Zip::File.open(test_zip, create: true) do |zip|
+        entry = ::Zip::Entry.new(zip.name, 'entry.txt', time: time)
+        zip.get_output_stream(entry) { |out| out.puts 'CONTENT!' }
+      end
+
+      ::Zip::File.open(test_zip) do |zip|
+        # Reverse times when testing because we need to use DOSTime#== for the
+        # comparison, not Time#==.
+        assert_equal(zip.get_entry('entry.txt').time, time)
+      end
+    end
+  end
+
   def test_open_buffer_with_string
     data = File.read('test/data/rubycode.zip', mode: 'rb')
     string = data.dup