zip_kit 6.3.2 → 6.3.4

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: 2ff5f4284066004d435d36f28020e6ef2279fe0abe36c520436db3bd39d7608a
4
- data.tar.gz: ac9f9c4312c632410cee6ffb98c2ec471f9de16e5260793a30df8ac28639218d
3
+ metadata.gz: f513e5300c8daa205994e7aa5a957c2270ea7378fab0dd13f57effd8f431d1ea
4
+ data.tar.gz: 9873a993a94a45339e9a1f383862ed1cc942db2fd23f4d09f6da70705afcf123
5
5
  SHA512:
6
- metadata.gz: 34389f0a2d38a532af341c7694fcd10c2bbfb2f819ce9d1168afd51fbfc815b8c4ef0b36929f286f3ad31ead98479b07b871ec35d1d8f5adefbf2bb2717f3f58
7
- data.tar.gz: 821151643cc5adafd9fe3446412e9d9de60ff049f6bab3ca1e9a7c0dc24dbcef0688b8bc0ab1fa51d88feb85892b3707ccbf8483e41c532680c2e9ddb073e823
6
+ metadata.gz: 4e3ace1ea348f7232d04932981bb5a3d4fb2f1f5e32f01523d5420bff97cc119b7a54bc9bd0f2269f92785167fa2acfdf1e013551def137d69f02e559deae244
7
+ data.tar.gz: 41c9791db83d11845dfff560526bf4d32cfaf7a0be99933a20ef2677d2027064bd7dd8f1071ac6a7b1864362c6cefa439728b422dcdb9c7529394233ad1dd85b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## 6.3.4
2
+
3
+ * Fix a bug whereby `rollback!` would cause an exception without any entries having been written yet (rollback on first entry).
4
+
5
+ ## 6.3.3
6
+
7
+ * Make sure `Writable#<<` converts the strings it is given into binary if they are not already in binary. This fixes an issue where `Heuristic` would suddenly start forwarding strings as-is to downstream callees. There is a lot of spots where the string-to-write gets forwarded and converting in every single one will be quite wasteful, but it can be handy to do in a few key places.
8
+ * Make sure `WritableBuffer#<<` converts the strings it is given into binary if they are not already in binary. This helps prevent an issue where the receiving object the buffer flushes to is in a different encoding than binary (and all of our use cases assume bytes anyway, except for filenames).
9
+ * When rescuing a failed `write_file`, differentiate between `#close`
10
+ and `#release_resources_on_failure!`. Closing a Writable can still try
11
+ to do things to the Streamer output, it can try to write to the destination
12
+ IO which is no longer accepting writes and so on. What we do want is to
13
+ safely destroy the zlib deflaters.
14
+
1
15
  ## 6.3.2
2
16
 
3
17
  * Make sure `rollback!` correctly works with `write_file` and the original exception gets re-raised from `write_file` if
data/Rakefile CHANGED
@@ -3,7 +3,6 @@
3
3
  require "bundler/gem_tasks"
4
4
  require "rspec/core/rake_task"
5
5
  require "yard"
6
- require "rubocop/rake_task"
7
6
  require "standard/rake"
8
7
 
9
8
  task :format do
@@ -16,6 +15,7 @@ RSpec::Core::RakeTask.new(:spec)
16
15
 
17
16
  task :generate_typedefs do
18
17
  `bundle exec sord rbi/zip_kit.rbi`
18
+ `bundle exec sord rbi/zip_kit.rbs`
19
19
  end
20
20
 
21
21
  task default: [:spec, :standard, :generate_typedefs]
@@ -5,6 +5,7 @@
5
5
  # interchangeable with the StoredWriter in terms of interface.
6
6
  class ZipKit::Streamer::DeflatedWriter
7
7
  include ZipKit::WriteShovel
8
+ include ZipKit::ZlibCleanup
8
9
 
9
10
  # The amount of bytes we will buffer before computing the intermediate
10
11
  # CRC32 checksums. Benchmarks show that the optimum is 64KB (see
@@ -42,4 +43,8 @@ class ZipKit::Streamer::DeflatedWriter
42
43
  ensure
43
44
  @deflater.close
44
45
  end
46
+
47
+ def release_resources_on_failure!
48
+ safely_dispose_of_incomplete_deflater(@deflater)
49
+ end
45
50
  end
@@ -13,6 +13,8 @@ require "zlib"
13
13
  # on the Streamer passed into it once it knows which compression
14
14
  # method should be applied
15
15
  class ZipKit::Streamer::Heuristic < ZipKit::Streamer::Writable
16
+ include ZipKit::ZlibCleanup
17
+
16
18
  BYTES_WRITTEN_THRESHOLD = 128 * 1024
17
19
  MINIMUM_VIABLE_COMPRESSION = 0.75
18
20
 
@@ -21,7 +23,7 @@ class ZipKit::Streamer::Heuristic < ZipKit::Streamer::Writable
21
23
  @filename = filename
22
24
  @write_file_options = write_file_options
23
25
 
24
- @buf = StringIO.new.binmode
26
+ @buf = +"".b # Just use a mutable String
25
27
  @deflater = ::Zlib::Deflate.new(Zlib::DEFAULT_COMPRESSION, -::Zlib::MAX_WBITS)
26
28
  @bytes_deflated = 0
27
29
 
@@ -35,7 +37,7 @@ class ZipKit::Streamer::Heuristic < ZipKit::Streamer::Writable
35
37
  else
36
38
  @buf << bytes
37
39
  @deflater.deflate(bytes) { |chunk| @bytes_deflated += chunk.bytesize }
38
- decide if @buf.size > BYTES_WRITTEN_THRESHOLD
40
+ decide if @buf.bytesize > BYTES_WRITTEN_THRESHOLD
39
41
  end
40
42
  self
41
43
  end
@@ -48,13 +50,18 @@ class ZipKit::Streamer::Heuristic < ZipKit::Streamer::Writable
48
50
  @winner.close
49
51
  end
50
52
 
53
+ def release_resources_on_failure!
54
+ safely_dispose_of_incomplete_deflater(@deflater)
55
+ @winner&.release_resources_on_failure!
56
+ end
57
+
51
58
  private def decide
52
59
  # Finish and then close the deflater - it has likely buffered some data
53
60
  @bytes_deflated += @deflater.finish.bytesize until @deflater.finished?
54
61
 
55
62
  # If the deflated version is smaller than the stored one
56
63
  # - use deflate, otherwise stored
57
- ratio = @bytes_deflated / @buf.size.to_f
64
+ ratio = @bytes_deflated / @buf.bytesize.to_f
58
65
  @winner = if ratio <= MINIMUM_VIABLE_COMPRESSION
59
66
  @streamer.write_deflated_file(@filename, **@write_file_options)
60
67
  else
@@ -62,9 +69,8 @@ class ZipKit::Streamer::Heuristic < ZipKit::Streamer::Writable
62
69
  end
63
70
 
64
71
  # Copy the buffered uncompressed data into the newly initialized writable
65
- @buf.rewind
66
- IO.copy_stream(@buf, @winner)
67
- @buf.truncate(0)
72
+ @winner << @buf
73
+ @buf.clear
68
74
  ensure
69
75
  @deflater.close
70
76
  end
@@ -36,4 +36,8 @@ class ZipKit::Streamer::StoredWriter
36
36
  @crc.flush
37
37
  {crc32: @crc_compute.to_i, compressed_size: @io.tell, uncompressed_size: @io.tell}
38
38
  end
39
+
40
+ def release_resources_on_failure!
41
+ # Nothing to do
42
+ end
39
43
  end
@@ -18,11 +18,11 @@ class ZipKit::Streamer::Writable
18
18
 
19
19
  # Writes the given data to the output stream
20
20
  #
21
- # @param d[String] the binary string to write (part of the uncompressed file)
21
+ # @param string[String] the string to write (part of the uncompressed file)
22
22
  # @return [self]
23
- def <<(d)
23
+ def <<(string)
24
24
  raise "Trying to write to a closed Writable" if @closed
25
- @writer << d
25
+ @writer << string.b
26
26
  self
27
27
  end
28
28
 
@@ -33,4 +33,10 @@ class ZipKit::Streamer::Writable
33
33
  @streamer.update_last_entry_and_write_data_descriptor(**@writer.finish)
34
34
  @closed = true
35
35
  end
36
+
37
+ def release_resources_on_failure!
38
+ return if @closed
39
+ @closed = true
40
+ @writer.release_resources_on_failure!
41
+ end
36
42
  end
@@ -273,6 +273,11 @@ class ZipKit::Streamer
273
273
  # output (using `IO.copy_stream` is a good approach).
274
274
  # @return [ZipKit::Streamer::Writable] without a block - the Writable sink which has to be closed manually
275
275
  def write_file(filename, modification_time: Time.now.utc, unix_permissions: nil, &blk)
276
+ # Reset rollback state when starting a new entry attempt, so that if this entry
277
+ # fails before writing a header, rollback! won't use stale values from a previous entry
278
+ @offset_before_last_local_file_header = nil
279
+ @remove_last_file_at_rollback = false
280
+
276
281
  writable = ZipKit::Streamer::Heuristic.new(self, filename, modification_time: modification_time, unix_permissions: unix_permissions)
277
282
  yield_or_return_writable(writable, &blk)
278
283
  end
@@ -510,8 +515,13 @@ class ZipKit::Streamer
510
515
  end
511
516
 
512
517
  # Create filler for the truncated or unusable local file entry that did get written into the output
513
- filler_size_bytes = @out.tell - @offset_before_last_local_file_header
514
- @files << Filler.new(filler_size_bytes)
518
+ # Only create a filler if a local file header was actually written (indicated by
519
+ # @offset_before_last_local_file_header being set). If it's nil, no header was written,
520
+ # so there's nothing to create a filler for.
521
+ if @offset_before_last_local_file_header
522
+ filler_size_bytes = @out.tell - @offset_before_last_local_file_header
523
+ @files << Filler.new(filler_size_bytes)
524
+ end
515
525
 
516
526
  @out.tell
517
527
  end
@@ -524,7 +534,7 @@ class ZipKit::Streamer
524
534
  yield(writable)
525
535
  writable.close
526
536
  rescue
527
- writable.close
537
+ writable.release_resources_on_failure!
528
538
  rollback!
529
539
  raise
530
540
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ZipKit
4
- VERSION = "6.3.2"
4
+ VERSION = "6.3.4"
5
5
  end
@@ -42,14 +42,14 @@ class ZipKit::WriteBuffer
42
42
  # Appends the given data to the write buffer, and flushes the buffer into the
43
43
  # writable if the buffer size exceeds the `buffer_size` given at initialization
44
44
  #
45
- # @param data[String] data to be written
45
+ # @param string[String] data to be written
46
46
  # @return self
47
- def <<(data)
48
- if data.bytesize >= @buffer_size
49
- flush unless @buf.empty? # <- this is were we can output less than @buffer_size
50
- @writable << data
47
+ def <<(string)
48
+ if string.bytesize >= @buffer_size
49
+ flush # <- this is were we can output less than @buffer_size
50
+ @writable << string.b
51
51
  else
52
- @buf << data
52
+ @buf << string.b
53
53
  flush if @buf.bytesize >= @buffer_size
54
54
  end
55
55
  self
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZipKit::ZlibCleanup
4
+ # This method is used to flush and close the native zlib handles
5
+ # should an archiving routine encounter an error. This is necessary,
6
+ # since otherwise unclosed deflaters may hang around in memory
7
+ # indefinitely, creating leaks.
8
+ #
9
+ # @param [Zlib::Deflater?]deflater the deflater to safely finish and close
10
+ # @return void
11
+ def safely_dispose_of_incomplete_deflater(deflater)
12
+ return unless deflater
13
+
14
+ # It can be a bit tricky to close and dealloc the deflater correctly.
15
+ # We want to do the right things for it to be GCd, including the
16
+ # native zlib handle. Also, leaving zlib handles dangling around
17
+ # creates warnings with "...with N bytes remaining to read", which are an
18
+ # eyesore. But they are there for a reason - so that we don't forget to do
19
+ # exactly this.
20
+ if !deflater.closed? && !deflater.finished?
21
+ deflater.finish until deflater.finished?
22
+ end
23
+ deflater.close unless deflater.closed?
24
+ end
25
+ end
data/lib/zip_kit.rb CHANGED
@@ -24,6 +24,7 @@ module ZipKit
24
24
  autoload :WriteShovel, File.dirname(__FILE__) + "/zip_kit/write_shovel.rb"
25
25
  autoload :RackChunkedBody, File.dirname(__FILE__) + "/zip_kit/rack_chunked_body.rb"
26
26
  autoload :RackTempfileBody, File.dirname(__FILE__) + "/zip_kit/rack_tempfile_body.rb"
27
+ autoload :ZlibCleanup, File.dirname(__FILE__) + "/zip_kit/zlib_cleanup.rb"
27
28
 
28
29
  require_relative "zip_kit/railtie" if defined?(::Rails)
29
30
  end
data/rbi/zip_kit.rbi CHANGED
@@ -1,6 +1,6 @@
1
1
  # typed: strong
2
2
  module ZipKit
3
- VERSION = T.let("6.3.1", T.untyped)
3
+ VERSION = T.let("6.3.4", T.untyped)
4
4
 
5
5
  class Railtie < Rails::Railtie
6
6
  end
@@ -106,8 +106,12 @@ module ZipKit
106
106
  # Is used to write ZIP archives without having to read them back or to overwrite
107
107
  # data. It outputs into any object that supports `<<` or `write`, namely:
108
108
  #
109
- # An `Array`, `File`, `IO`, `Socket` and even `String` all can be output destinations
110
- # for the `Streamer`.
109
+ # * `Array` - will contain binary strings
110
+ # * `File` - data will be written to it as it gets generated
111
+ # * `IO` (`Socket`, `StringIO`) - data gets written into it
112
+ # * `String` - in binary encoding and unfrozen - also makes a decent output target
113
+ #
114
+ # or anything else that responds to `#<<` or `#write`.
111
115
  #
112
116
  # You can also combine output through the `Streamer` with direct output to the destination,
113
117
  # all while preserving the correct offsets in the ZIP file structures. This allows usage
@@ -514,6 +518,10 @@ module ZipKit
514
518
  # is likely already on the wire. However, excluding the entry from the central directory of the ZIP
515
519
  # file will allow better-behaved ZIP unarchivers to extract the entries which did store correctly,
516
520
  # provided they read the ZIP from the central directory and not straight-ahead.
521
+ # Rolling back does not perform any writes.
522
+ #
523
+ # `rollback!` gets called for you if an exception is raised inside the block of `write_file`,
524
+ # `write_deflated_file` and `write_stored_file`.
517
525
  #
518
526
  # _@return_ — position in the output stream / ZIP archive
519
527
  #
@@ -670,9 +678,9 @@ module ZipKit
670
678
 
671
679
  # Writes the given data to the output stream
672
680
  #
673
- # _@param_ `d` — the binary string to write (part of the uncompressed file)
674
- sig { params(d: String).returns(T.self_type) }
675
- def <<(d); end
681
+ # _@param_ `string` — the string to write (part of the uncompressed file)
682
+ sig { params(string: String).returns(T.self_type) }
683
+ def <<(string); end
676
684
 
677
685
  # sord omit - no YARD return type given, using untyped
678
686
  # Flushes the writer and recovers the CRC32/size values. It then calls
@@ -680,6 +688,10 @@ module ZipKit
680
688
  sig { returns(T.untyped) }
681
689
  def close; end
682
690
 
691
+ # sord omit - no YARD return type given, using untyped
692
+ sig { returns(T.untyped) }
693
+ def release_resources_on_failure!; end
694
+
683
695
  # Writes the given data to the output stream. Allows the object to be used as
684
696
  # a target for `IO.copy_stream(from, to)`
685
697
  #
@@ -701,6 +713,7 @@ module ZipKit
701
713
  # on the Streamer passed into it once it knows which compression
702
714
  # method should be applied
703
715
  class Heuristic < ZipKit::Streamer::Writable
716
+ include ZipKit::ZlibCleanup
704
717
  BYTES_WRITTEN_THRESHOLD = T.let(128 * 1024, T.untyped)
705
718
  MINIMUM_VIABLE_COMPRESSION = T.let(0.75, T.untyped)
706
719
 
@@ -718,9 +731,25 @@ module ZipKit
718
731
  sig { returns(T.untyped) }
719
732
  def close; end
720
733
 
734
+ # sord omit - no YARD return type given, using untyped
735
+ sig { returns(T.untyped) }
736
+ def release_resources_on_failure!; end
737
+
721
738
  # sord omit - no YARD return type given, using untyped
722
739
  sig { returns(T.untyped) }
723
740
  def decide; end
741
+
742
+ # sord warn - "Zlib::Deflater?" does not appear to be a type
743
+ # This method is used to flush and close the native zlib handles
744
+ # should an archiving routine encounter an error. This is necessary,
745
+ # since otherwise unclosed deflaters may hang around in memory
746
+ # indefinitely, creating leaks.
747
+ #
748
+ # _@param_ `deflater` — the deflater to safely finish and close
749
+ #
750
+ # _@return_ — void
751
+ sig { params(deflater: SORD_ERROR_ZlibDeflater).returns(T.untyped) }
752
+ def safely_dispose_of_incomplete_deflater(deflater); end
724
753
  end
725
754
 
726
755
  # Sends writes to the given `io`, and also registers all the data passing
@@ -749,6 +778,10 @@ module ZipKit
749
778
  sig { returns(T::Hash[T.untyped, T.untyped]) }
750
779
  def finish; end
751
780
 
781
+ # sord omit - no YARD return type given, using untyped
782
+ sig { returns(T.untyped) }
783
+ def release_resources_on_failure!; end
784
+
752
785
  # Writes the given data to the output stream. Allows the object to be used as
753
786
  # a target for `IO.copy_stream(from, to)`
754
787
  #
@@ -764,6 +797,7 @@ module ZipKit
764
797
  # interchangeable with the StoredWriter in terms of interface.
765
798
  class DeflatedWriter
766
799
  include ZipKit::WriteShovel
800
+ include ZipKit::ZlibCleanup
767
801
  CRC32_BUFFER_SIZE = T.let(64 * 1024, T.untyped)
768
802
 
769
803
  # sord omit - no YARD type given for "io", using untyped
@@ -787,6 +821,22 @@ module ZipKit
787
821
  sig { returns(T::Hash[T.untyped, T.untyped]) }
788
822
  def finish; end
789
823
 
824
+ # sord omit - no YARD return type given, using untyped
825
+ sig { returns(T.untyped) }
826
+ def release_resources_on_failure!; end
827
+
828
+ # sord warn - "Zlib::Deflater?" does not appear to be a type
829
+ # This method is used to flush and close the native zlib handles
830
+ # should an archiving routine encounter an error. This is necessary,
831
+ # since otherwise unclosed deflaters may hang around in memory
832
+ # indefinitely, creating leaks.
833
+ #
834
+ # _@param_ `deflater` — the deflater to safely finish and close
835
+ #
836
+ # _@return_ — void
837
+ sig { params(deflater: SORD_ERROR_ZlibDeflater).returns(T.untyped) }
838
+ def safely_dispose_of_incomplete_deflater(deflater); end
839
+
790
840
  # Writes the given data to the output stream. Allows the object to be used as
791
841
  # a target for `IO.copy_stream(from, to)`
792
842
  #
@@ -1712,11 +1762,11 @@ end, T.untyped)
1712
1762
  # Appends the given data to the write buffer, and flushes the buffer into the
1713
1763
  # writable if the buffer size exceeds the `buffer_size` given at initialization
1714
1764
  #
1715
- # _@param_ `data` — data to be written
1765
+ # _@param_ `string` — data to be written
1716
1766
  #
1717
1767
  # _@return_ — self
1718
- sig { params(data: String).returns(T.untyped) }
1719
- def <<(data); end
1768
+ sig { params(string: String).returns(T.untyped) }
1769
+ def <<(string); end
1720
1770
 
1721
1771
  # Explicitly flushes the buffer if it contains anything
1722
1772
  #
@@ -1745,6 +1795,20 @@ end, T.untyped)
1745
1795
  def write(bytes); end
1746
1796
  end
1747
1797
 
1798
+ module ZlibCleanup
1799
+ # sord warn - "Zlib::Deflater?" does not appear to be a type
1800
+ # This method is used to flush and close the native zlib handles
1801
+ # should an archiving routine encounter an error. This is necessary,
1802
+ # since otherwise unclosed deflaters may hang around in memory
1803
+ # indefinitely, creating leaks.
1804
+ #
1805
+ # _@param_ `deflater` — the deflater to safely finish and close
1806
+ #
1807
+ # _@return_ — void
1808
+ sig { params(deflater: SORD_ERROR_ZlibDeflater).returns(T.untyped) }
1809
+ def safely_dispose_of_incomplete_deflater(deflater); end
1810
+ end
1811
+
1748
1812
  # Permits Deflate compression in independent blocks. The workflow is as follows:
1749
1813
  #
1750
1814
  # * Run every block to compress through deflate_chunk, remove the header,