Skip to content

Commit e7cc776

Browse files
committed
Continue
1 parent b480d30 commit e7cc776

File tree

2 files changed

+42
-17
lines changed

2 files changed

+42
-17
lines changed

lib/zip_kit/streamer.rb

+14-3
Original file line numberDiff line numberDiff line change
@@ -497,14 +497,17 @@ def update_last_entry_and_write_data_descriptor(crc32:, compressed_size:, uncomp
497497
# end
498498
# @return [Integer] position in the output stream / ZIP archive
499499
def rollback!
500-
removed_entry = @files.pop
501-
return @out.tell unless removed_entry
500+
@files.pop if @remove_last_file_at_rollback = false
502501

502+
# Recreate the path set from remaining entries (PathSet does not support cheap deletes yet)
503503
@path_set.clear
504504
@files.each do |e|
505505
@path_set.add_directory_or_file_path(e.filename) unless e.filler?
506506
end
507-
@files << Filler.new(@out.tell - removed_entry.local_header_offset)
507+
508+
# Create filler for the truncated or unusable local file entry that did get written into the output
509+
filler_size_bytes = @out.tell - @offset_before_last_local_file_header
510+
@files << Filler.new(filler_size_bytes)
508511

509512
@out.tell
510513
end
@@ -559,6 +562,12 @@ def add_file_and_write_local_header(
559562
unix_permissions:
560563
)
561564

565+
# Set state needed for proper rollback later. If write_local_file_header
566+
# does manage to write _some_ bytes, but fails later (we write in tiny bits sometimes)
567+
# we should be able to create a filler from this offset on when we
568+
@offset_before_last_local_file_header = @out.tell
569+
@remove_last_file_at_rollback = false
570+
562571
# Clean backslashes
563572
filename = remove_backslash(filename)
564573
raise UnknownMode, "Unknown compression mode #{storage_mode}" unless [STORED, DEFLATED].include?(storage_mode)
@@ -604,9 +613,11 @@ def add_file_and_write_local_header(
604613
mtime: e.mtime,
605614
filename: e.filename,
606615
storage_mode: e.storage_mode)
616+
607617
e.bytes_used_for_local_header = @out.tell - e.local_header_offset
608618

609619
@files << e
620+
@remove_last_file_at_rollback = true
610621
end
611622

612623
def remove_backslash(filename)

spec/zip_kit/streamer_spec.rb

+28-14
Original file line numberDiff line numberDiff line change
@@ -640,25 +640,39 @@ def stream_with_just_write.write(bytes)
640640
expect(per_filename["stored.txt"]).to eq("this is attempt 2")
641641
end
642642

643-
it "correctly rolls back if an exception is raised after the local entry has been written in write_file" do
643+
it "correctly rolls back if an exception is raised from Writable#close when using write_file" do
644644
# A Unicode string will not be happy about binary writes
645645
# and will raise an exception. The exception won't be raised when
646-
# starting the writes, but in `#close` of the Writable. If this is not handled correctly,
647-
# the exception we will get raised won't be the original exception (Encoding::CompatibilityError that
648-
# we need to see - to know what went wrong inside the writing block) but a duplicate zip entry exception.
646+
# starting the writes, but in `#close` of the Writable with write_file. When the size of the byte
647+
# stream is small, the decision whether to call write_stored_file or write_deflated_file is going to
648+
# be deferred until the Writable gets close() called on it. In that situation a correct rollback
649+
# must be performed.
650+
# If this is not handled correctly, the exception we will get raised won't be the original exception
651+
# (Encoding::CompatibilityError that we need to see - to know what went wrong inside the writing block)
652+
# but a PathSet::Conflict duplicate zip entry exception. Also, the IO offsets will be out of whack.
649653
# To cause this, we need something that will raise during `Writable#close` - we will use a Unicode string
650654
# for that purpose. See https://github.com/julik/zip_kit/issues/15
651-
uniсode_str_buf = "Ж"
652-
described_class.open(uniсode_str_buf) do |zip|
653-
4.times do
654-
begin
655-
zip.write_file("deflated.txt") do |sink|
656-
sink.write("x")
657-
end
658-
rescue => e
659-
expect(e).to be_kind_of(Encoding::CompatibilityError)
655+
uniсode_str_buf = +"Ж"
656+
657+
zip = described_class.new(uniсode_str_buf)
658+
4.times do
659+
bytes_before_partial_write = uniсode_str_buf.bytesize
660+
expect {
661+
zip.write_file("deflated.txt") do |sink|
662+
sink.write("x")
660663
end
661-
end
664+
}.to raise_error(Encoding::CompatibilityError) # Should not be a PathSet::Conflict
665+
666+
# Ensure there was a partial write
667+
expect(uniсode_str_buf.bytesize - bytes_before_partial_write).to be > 0
662668
end
669+
670+
# We must force the string into binary so that the ZIP can be closed - we
671+
# want to write out the EOCD since we want to make sure there is no byte offset
672+
# mismatch (fillers have been correctly produced)
673+
uniсode_str_buf.force_encoding(Encoding::BINARY)
674+
expect {
675+
zip.close
676+
}.not_to raise_error # Offset validation should pass
663677
end
664678
end

0 commit comments

Comments
 (0)