zip_tricks 2.8.1 → 3.0.0
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 +4 -4
- data/Gemfile +3 -3
- data/IMPLEMENTATION_DETAILS.md +2 -10
- data/README.md +62 -59
- data/examples/archive_size_estimate.rb +4 -4
- data/examples/rack_application.rb +3 -5
- data/lib/zip_tricks/block_deflate.rb +21 -0
- data/lib/zip_tricks/file_reader.rb +491 -0
- data/lib/zip_tricks/null_writer.rb +7 -2
- data/lib/zip_tricks/rack_body.rb +3 -3
- data/lib/zip_tricks/remote_io.rb +30 -20
- data/lib/zip_tricks/remote_uncap.rb +10 -10
- data/lib/zip_tricks/size_estimator.rb +64 -0
- data/lib/zip_tricks/stream_crc32.rb +2 -2
- data/lib/zip_tricks/streamer/deflated_writer.rb +26 -0
- data/lib/zip_tricks/streamer/entry.rb +21 -0
- data/lib/zip_tricks/streamer/stored_writer.rb +25 -0
- data/lib/zip_tricks/streamer/writable.rb +20 -0
- data/lib/zip_tricks/streamer.rb +172 -66
- data/lib/zip_tricks/zip_writer.rb +346 -0
- data/lib/zip_tricks.rb +1 -4
- data/spec/spec_helper.rb +1 -38
- data/spec/zip_tricks/file_reader_spec.rb +47 -0
- data/spec/zip_tricks/rack_body_spec.rb +2 -2
- data/spec/zip_tricks/remote_io_spec.rb +8 -20
- data/spec/zip_tricks/remote_uncap_spec.rb +4 -4
- data/spec/zip_tricks/size_estimator_spec.rb +31 -0
- data/spec/zip_tricks/streamer_spec.rb +59 -36
- data/spec/zip_tricks/zip_writer_spec.rb +408 -0
- data/zip_tricks.gemspec +20 -14
- metadata +33 -16
- data/lib/zip_tricks/manifest.rb +0 -85
- data/lib/zip_tricks/microzip.rb +0 -339
- data/lib/zip_tricks/stored_size_estimator.rb +0 -44
- data/spec/zip_tricks/manifest_spec.rb +0 -60
- data/spec/zip_tricks/microzip_interop_spec.rb +0 -48
- data/spec/zip_tricks/microzip_spec.rb +0 -546
- data/spec/zip_tricks/stored_size_estimator_spec.rb +0 -22
@@ -26,7 +26,7 @@ class ZipTricks::StreamCRC32
|
|
26
26
|
|
27
27
|
# Returns the CRC32 value computed so far
|
28
28
|
#
|
29
|
-
# @return
|
29
|
+
# @return [Fixnum] the updated CRC32 value for all the blobs so far
|
30
30
|
def to_i
|
31
31
|
@crc
|
32
32
|
end
|
@@ -36,7 +36,7 @@ class ZipTricks::StreamCRC32
|
|
36
36
|
#
|
37
37
|
# @param crc32[Fixnum] the CRC32 value to append
|
38
38
|
# @param blob_size[Fixnum] the size of the daata the `crc32` is computed from
|
39
|
-
# @return
|
39
|
+
# @return [Fixnum] the updated CRC32 value for all the blobs so far
|
40
40
|
def append(crc32, blob_size)
|
41
41
|
@crc = Zlib.crc32_combine(@crc, crc32, blob_size)
|
42
42
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class ZipTricks::Streamer::DeflatedWriter
|
2
|
+
def initialize(io)
|
3
|
+
@io = io
|
4
|
+
@uncompressed_size = 0
|
5
|
+
@started_at = @io.tell
|
6
|
+
@crc = ZipTricks::StreamCRC32.new
|
7
|
+
@bytes_since_last_flush = 0
|
8
|
+
end
|
9
|
+
|
10
|
+
def finish
|
11
|
+
ZipTricks::BlockDeflate.write_terminator(@io)
|
12
|
+
[@crc.to_i, @io.tell - @started_at, @uncompressed_size]
|
13
|
+
end
|
14
|
+
|
15
|
+
def <<(data)
|
16
|
+
@uncompressed_size += data.bytesize
|
17
|
+
@io << ZipTricks::BlockDeflate.deflate_chunk(data)
|
18
|
+
@crc << data
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def write(data)
|
23
|
+
self << data
|
24
|
+
data.bytesize
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Is used internally by Streamer to keep track of entries in the archive during writing.
|
2
|
+
# Normally you will not have to use this class directly
|
3
|
+
class ZipTricks::Streamer::Entry < Struct.new(:filename, :crc32, :compressed_size, :uncompressed_size, :storage_mode, :mtime, :use_data_descriptor)
|
4
|
+
def initialize(*)
|
5
|
+
super
|
6
|
+
filename.force_encoding(Encoding::UTF_8)
|
7
|
+
@requires_efs_flag = !(filename.encode(Encoding::ASCII) rescue false)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Set the general purpose flags for the entry. We care about is the EFS
|
11
|
+
# bit (bit 11) which should be set if the filename is UTF8. If it is, we need to set the
|
12
|
+
# bit so that the unarchiving application knows that the filename in the archive is UTF-8
|
13
|
+
# encoded, and not some DOS default. For ASCII entries it does not matter.
|
14
|
+
# Additionally, we care about bit 3 which toggles the use of the postfix data descriptor.
|
15
|
+
def gp_flags
|
16
|
+
flag = 0b00000000000
|
17
|
+
flag |= 0b100000000000 if @requires_efs_flag # bit 11
|
18
|
+
flag |= 0x0008 if use_data_descriptor # bit 3
|
19
|
+
flag
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class ZipTricks::Streamer::StoredWriter
|
2
|
+
def initialize(io)
|
3
|
+
@io = io
|
4
|
+
@uncompressed_size = 0
|
5
|
+
@compressed_size = 0
|
6
|
+
@started_at = @io.tell
|
7
|
+
@crc = ZipTricks::StreamCRC32.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def <<(data)
|
11
|
+
@io << data
|
12
|
+
@crc << data
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def write(data)
|
17
|
+
self << data
|
18
|
+
data.bytesize
|
19
|
+
end
|
20
|
+
|
21
|
+
def finish
|
22
|
+
size = @io.tell - @started_at
|
23
|
+
[@crc.to_i, size, size]
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Gets yielded from the writing methods of the CompressingStreamer
|
2
|
+
# and accepts the data being written into the ZIP
|
3
|
+
class ZipTricks::Streamer::Writable
|
4
|
+
# Initializes a new Writable with the object it delegates the writes to.
|
5
|
+
# Normally you would not need to use this method directly
|
6
|
+
def initialize(writer)
|
7
|
+
@writer = writer
|
8
|
+
end
|
9
|
+
# Writes the given data to the output stream
|
10
|
+
#
|
11
|
+
# @param d[String] the binary string to write (part of the uncompressed file)
|
12
|
+
# @return [self]
|
13
|
+
def <<(d); @writer << d; self; end
|
14
|
+
|
15
|
+
# Writes the given data to the output stream
|
16
|
+
#
|
17
|
+
# @param d[String] the binary string to write (part of the uncompressed file)
|
18
|
+
# @return [Fixnum] the number of bytes written
|
19
|
+
def write(d); @writer << d; end
|
20
|
+
end
|
data/lib/zip_tricks/streamer.rb
CHANGED
@@ -8,17 +8,61 @@
|
|
8
8
|
# For stored entries, you need to know the CRC32 (as a uint) and the filesize upfront,
|
9
9
|
# before the writing of the entry body starts.
|
10
10
|
#
|
11
|
-
#
|
12
|
-
#
|
11
|
+
# Any object that responds to `<<` can be used as the Streamer target - you can use
|
12
|
+
# a String, an Array, a Socket or a File, at your leisure.
|
13
|
+
#
|
14
|
+
# ## Using the Streamer with runtime compression
|
15
|
+
#
|
16
|
+
# You can use the Streamer with data descriptors (the CRC32 and the sizes will be
|
17
|
+
# written after the file data). This allows non-rewinding on-the-fly compression.
|
18
|
+
# If you are compressing large files, the Deflater object that the Streamer controls
|
19
|
+
# will be regularly flushed to prevent memory inflation.
|
20
|
+
#
|
21
|
+
# ZipTricks::Streamer.open(file_socket_or_string) do |zip|
|
22
|
+
# zip.write_stored_file('mov.mp4') do |sink|
|
23
|
+
# File.open('mov.mp4', 'rb'){|source| IO.copy_stream(source, sink) }
|
24
|
+
# end
|
25
|
+
# zip.write_deflated_file('long-novel.txt') do |sink|
|
26
|
+
# File.open('novel.txt', 'rb'){|source| IO.copy_stream(source, sink) }
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# The central directory will be written automatically at the end of the block.
|
31
|
+
#
|
32
|
+
# ## Using the Streamer with entries of known size and having a known CRC32 checksum
|
33
|
+
#
|
34
|
+
# Streamer allows "IO splicing" - in this mode it will only control the metadata output,
|
35
|
+
# but you can write the data to the socket/file outside of the Streamer. For example, when
|
36
|
+
# using the sendfile gem:
|
37
|
+
#
|
38
|
+
# ZipTricks::Streamer.open(socket) do | zip |
|
39
|
+
# zip.add_stored_entry(filename: "myfile1.bin", size: 9090821, crc32: 12485)
|
40
|
+
# zip.simulate_write(tempfile1.size)
|
41
|
+
# socket.sendfile(tempfile1)
|
42
|
+
# zip.add_stored_entry(filename: "myfile2.bin", size: 458678, crc32: 89568)
|
43
|
+
# zip.simulate_write(tempfile2.size)
|
44
|
+
# socket.sendfile(tempfile2)
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# Note that you need to use `simulate_write` to let the
|
48
|
+
# The central directory will be written automatically at the end of the block.
|
13
49
|
class ZipTricks::Streamer
|
50
|
+
require_relative 'streamer/deflated_writer'
|
51
|
+
require_relative 'streamer/writable'
|
52
|
+
require_relative 'streamer/stored_writer'
|
53
|
+
require_relative 'streamer/entry'
|
54
|
+
|
55
|
+
STORED = 0
|
56
|
+
DEFLATED = 8
|
57
|
+
|
14
58
|
EntryBodySizeMismatch = Class.new(StandardError)
|
15
59
|
InvalidOutput = Class.new(ArgumentError)
|
60
|
+
Overflow = Class.new(StandardError)
|
61
|
+
PathError = Class.new(StandardError)
|
62
|
+
DuplicateFilenames = Class.new(StandardError)
|
63
|
+
UnknownMode = Class.new(StandardError)
|
16
64
|
|
17
|
-
|
18
|
-
EFS = 0b100000000000
|
19
|
-
|
20
|
-
# Default general purpose flags for each entry.
|
21
|
-
DEFAULT_GP_FLAGS = 0b00000000000
|
65
|
+
private_constant :DeflatedWriter, :StoredWriter, :STORED, :DEFLATED
|
22
66
|
|
23
67
|
# Creates a new Streamer on top of the given IO-ish object and yields it. Once the given block
|
24
68
|
# returns, the Streamer will have it's `close` method called, which will write out the central
|
@@ -34,21 +78,17 @@ class ZipTricks::Streamer
|
|
34
78
|
|
35
79
|
# Creates a new Streamer on top of the given IO-ish object.
|
36
80
|
#
|
37
|
-
# @param stream [IO] the destination IO for the ZIP (should respond to
|
81
|
+
# @param stream [IO] the destination IO for the ZIP (should respond to `<<`)
|
38
82
|
def initialize(stream)
|
39
|
-
raise InvalidOutput, "The stream
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
@zip = ZipTricks::Microzip.new
|
83
|
+
raise InvalidOutput, "The stream must respond to #<<" unless stream.respond_to?(:<<)
|
84
|
+
unless stream.respond_to?(:tell) && stream.respond_to?(:advance_position_by)
|
85
|
+
stream = ZipTricks::WriteAndTell.new(stream)
|
86
|
+
end
|
44
87
|
|
45
|
-
@
|
46
|
-
@
|
47
|
-
@
|
48
|
-
@
|
49
|
-
@state_monitor.permit_transition :in_entry_body => :in_entry_header
|
50
|
-
@state_monitor.permit_transition :in_entry_body => :in_central_directory
|
51
|
-
@state_monitor.permit_transition :in_central_directory => :closed
|
88
|
+
@out = stream
|
89
|
+
@files = []
|
90
|
+
@local_header_offsets = []
|
91
|
+
@writer = create_writer
|
52
92
|
end
|
53
93
|
|
54
94
|
# Writes a part of a zip entry body (actual binary data of the entry) into the output stream.
|
@@ -56,9 +96,7 @@ class ZipTricks::Streamer
|
|
56
96
|
# @param binary_data [String] a String in binary encoding
|
57
97
|
# @return self
|
58
98
|
def <<(binary_data)
|
59
|
-
@
|
60
|
-
@output_stream << binary_data
|
61
|
-
@bytes_written_for_entry += binary_data.bytesize
|
99
|
+
@out << binary_data
|
62
100
|
self
|
63
101
|
end
|
64
102
|
|
@@ -69,7 +107,7 @@ class ZipTricks::Streamer
|
|
69
107
|
# @param binary_data [String] a String in binary encoding
|
70
108
|
# @return [Fixnum] the number of bytes written
|
71
109
|
def write(binary_data)
|
72
|
-
|
110
|
+
@out << binary_data
|
73
111
|
binary_data.bytesize
|
74
112
|
end
|
75
113
|
|
@@ -80,10 +118,8 @@ class ZipTricks::Streamer
|
|
80
118
|
# @param num_bytes [Numeric] how many bytes are going to be written bypassing the Streamer
|
81
119
|
# @return [Numeric] position in the output stream / ZIP archive
|
82
120
|
def simulate_write(num_bytes)
|
83
|
-
@
|
84
|
-
@
|
85
|
-
@bytes_written_for_entry += num_bytes
|
86
|
-
@output_stream.tell
|
121
|
+
@out.advance_position_by(num_bytes)
|
122
|
+
@out.tell
|
87
123
|
end
|
88
124
|
|
89
125
|
# Writes out the local header for an entry (file in the ZIP) that is using the deflated storage model (is compressed).
|
@@ -92,67 +128,137 @@ class ZipTricks::Streamer
|
|
92
128
|
# Note that the deflated body that is going to be written into the output has to be _precompressed_ (pre-deflated)
|
93
129
|
# before writing it into the Streamer, because otherwise it is impossible to know it's size upfront.
|
94
130
|
#
|
95
|
-
# @param
|
131
|
+
# @param filename [String] the name of the file in the entry
|
132
|
+
# @param compressed_size [Fixnum] the size of the compressed entry that is going to be written into the archive
|
96
133
|
# @param uncompressed_size [Fixnum] the size of the entry when uncompressed, in bytes
|
97
134
|
# @param crc32 [Fixnum] the CRC32 checksum of the entry when uncompressed
|
98
|
-
# @param compressed_size [Fixnum] the size of the compressed entry that is going to be written into the archive
|
99
135
|
# @return [Fixnum] the offset the output IO is at after writing the entry header
|
100
|
-
def add_compressed_entry(
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
@expected_bytes_for_entry = compressed_size
|
105
|
-
@bytes_written_for_entry = 0
|
106
|
-
@output_stream.tell
|
136
|
+
def add_compressed_entry(filename:, compressed_size:, uncompressed_size:, crc32:)
|
137
|
+
add_file_and_write_local_header(filename: filename, crc32: crc32, storage_mode: DEFLATED,
|
138
|
+
compressed_size: compressed_size, uncompressed_size: uncompressed_size)
|
139
|
+
@out.tell
|
107
140
|
end
|
108
141
|
|
109
142
|
# Writes out the local header for an entry (file in the ZIP) that is using the stored storage model (is stored as-is).
|
110
143
|
# Once this method is called, the `<<` method has to be called one or more times to write the actual contents of the body.
|
111
144
|
#
|
112
|
-
# @param
|
113
|
-
# @param
|
145
|
+
# @param filename [String] the name of the file in the entry
|
146
|
+
# @param size [Fixnum] the size of the file when uncompressed, in bytes
|
114
147
|
# @param crc32 [Fixnum] the CRC32 checksum of the entry when uncompressed
|
115
148
|
# @return [Fixnum] the offset the output IO is at after writing the entry header
|
116
|
-
def add_stored_entry(
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
@bytes_written_for_entry = 0
|
121
|
-
@expected_bytes_for_entry = uncompressed_size
|
122
|
-
@output_stream.tell
|
149
|
+
def add_stored_entry(filename:, size:, crc32:)
|
150
|
+
add_file_and_write_local_header(filename: filename, crc32: crc32, storage_mode: STORED,
|
151
|
+
compressed_size: size, uncompressed_size: size)
|
152
|
+
@out.tell
|
123
153
|
end
|
124
154
|
|
125
|
-
#
|
126
|
-
#
|
155
|
+
# Opens the stream for a stored file in the archive, and yields a writer for that file to the block.
|
156
|
+
# Once the write completes, a data descriptor will be written with the actual compressed/uncompressed
|
157
|
+
# sizes and the CRC32 checksum.
|
127
158
|
#
|
128
|
-
#
|
159
|
+
# @param filename[String] the name of the file in the archive
|
160
|
+
# @yield [#<<, #write] an object that the file contents must be written to
|
161
|
+
def write_stored_file(filename)
|
162
|
+
add_file_and_write_local_header(filename: filename, storage_mode: STORED,
|
163
|
+
use_data_descriptor: true, crc32: 0, compressed_size: 0, uncompressed_size: 0)
|
164
|
+
|
165
|
+
w = StoredWriter.new(@out)
|
166
|
+
yield(Writable.new(w))
|
167
|
+
crc, comp, uncomp = w.finish
|
168
|
+
|
169
|
+
# Save the information into the entry for when the time comes to write out the central directory
|
170
|
+
last_entry = @files[-1]
|
171
|
+
last_entry.crc32 = crc
|
172
|
+
last_entry.compressed_size = comp
|
173
|
+
last_entry.uncompressed_size = uncomp
|
174
|
+
|
175
|
+
@writer.write_data_descriptor(io: @out, crc32: crc, compressed_size: comp, uncompressed_size: uncomp)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Opens the stream for a deflated file in the archive, and yields a writer for that file to the block.
|
179
|
+
# Once the write completes, a data descriptor will be written with the actual compressed/uncompressed
|
180
|
+
# sizes and the CRC32 checksum.
|
129
181
|
#
|
130
|
-
# @
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
182
|
+
# @param filename[String] the name of the file in the archive
|
183
|
+
# @yield [#<<, #write] an object that the file contents must be written to
|
184
|
+
def write_deflated_file(filename)
|
185
|
+
add_file_and_write_local_header(filename: filename, storage_mode: DEFLATED,
|
186
|
+
use_data_descriptor: true, crc32: 0, compressed_size: 0, uncompressed_size: 0)
|
187
|
+
|
188
|
+
w = DeflatedWriter.new(@out)
|
189
|
+
yield(Writable.new(w))
|
190
|
+
crc, comp, uncomp = w.finish
|
191
|
+
|
192
|
+
# Save the information into the entry for when the time comes to write out the central directory
|
193
|
+
last_entry = @files[-1]
|
194
|
+
last_entry.crc32 = crc
|
195
|
+
last_entry.compressed_size = comp
|
196
|
+
last_entry.uncompressed_size = uncomp
|
197
|
+
write_data_descriptor_for_last_entry
|
135
198
|
end
|
136
199
|
|
137
|
-
# Closes the archive. Writes the central directory
|
138
|
-
#
|
200
|
+
# Closes the archive. Writes the central directory, and switches the writer into
|
201
|
+
# a state where it can no longer be written to.
|
139
202
|
#
|
140
203
|
# Once this method is called, the `Streamer` should be discarded (the ZIP archive is complete).
|
141
204
|
#
|
142
205
|
# @return [Fixnum] the offset the output IO is at after closing the archive
|
143
206
|
def close
|
144
|
-
|
145
|
-
@
|
146
|
-
|
147
|
-
|
207
|
+
# Record the central directory offset, so that it can be written into the EOCD record
|
208
|
+
cdir_starts_at = @out.tell
|
209
|
+
|
210
|
+
# Write out the central directory entries, one for each file
|
211
|
+
@files.each_with_index do |entry, i|
|
212
|
+
header_loc = @local_header_offsets.fetch(i)
|
213
|
+
@writer.write_central_directory_file_header(io: @out, local_file_header_location: header_loc,
|
214
|
+
gp_flags: entry.gp_flags, storage_mode: entry.storage_mode,
|
215
|
+
compressed_size: entry.compressed_size, uncompressed_size: entry.uncompressed_size,
|
216
|
+
mtime: entry.mtime, crc32: entry.crc32, filename: entry.filename) #, external_attrs: DEFAULT_EXTERNAL_ATTRS)
|
217
|
+
end
|
218
|
+
|
219
|
+
# Record the central directory size, for the EOCDR
|
220
|
+
cdir_size = @out.tell - cdir_starts_at
|
148
221
|
|
222
|
+
# Write out the EOCDR
|
223
|
+
@writer. write_end_of_central_directory(io: @out, start_of_central_directory_location: cdir_starts_at,
|
224
|
+
central_directory_size: cdir_size, num_files_in_archive: @files.length)
|
225
|
+
@out.tell
|
226
|
+
end
|
227
|
+
|
228
|
+
# Sets up the ZipWriter with wrappers if necessary. The method is called once, when the Streamer
|
229
|
+
# gets instantiated - the Writer then gets reused. This method is primarily there so that you
|
230
|
+
# can override it.
|
231
|
+
#
|
232
|
+
# @return [ZipTricks::ZipWriter] the writer to perform writes with
|
233
|
+
def create_writer
|
234
|
+
ZipTricks::ZipWriter.new
|
235
|
+
end
|
236
|
+
|
149
237
|
private
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
if @
|
154
|
-
|
155
|
-
raise EntryBodySizeMismatch, msg
|
238
|
+
|
239
|
+
def add_file_and_write_local_header(filename:, crc32:, storage_mode:, compressed_size:,
|
240
|
+
uncompressed_size:, use_data_descriptor: false)
|
241
|
+
if @files.any?{|e| e.filename == filename }
|
242
|
+
raise DuplicateFilenames, "Filename #{filename.inspect} already used in the archive"
|
156
243
|
end
|
244
|
+
|
245
|
+
raise UnknownMode, "Unknown compression mode #{storage_mode}" unless [STORED, DEFLATED].include?(storage_mode)
|
246
|
+
raise Overflow, "Filename is too long" if filename.bytesize > 0xFFFF
|
247
|
+
raise PathError, "Paths in ZIP may only contain forward slashes (UNIX separators)" if filename.include?('\\')
|
248
|
+
|
249
|
+
@check_compressed_size_after_leaving_body = !use_data_descriptor
|
250
|
+
@bytes_written_for_entry = 0
|
251
|
+
@expected_bytes_for_entry = compressed_size
|
252
|
+
|
253
|
+
e = Entry.new(filename, crc32, compressed_size, uncompressed_size, storage_mode, mtime=Time.now.utc, use_data_descriptor)
|
254
|
+
@files << e
|
255
|
+
@local_header_offsets << @out.tell
|
256
|
+
@writer.write_local_file_header(io: @out, gp_flags: e.gp_flags, crc32: e.crc32, compressed_size: e.compressed_size,
|
257
|
+
uncompressed_size: e.uncompressed_size, mtime: e.mtime, filename: e.filename, storage_mode: e.storage_mode)
|
258
|
+
end
|
259
|
+
|
260
|
+
def write_data_descriptor_for_last_entry
|
261
|
+
e = @files.fetch(-1)
|
262
|
+
@writer.write_data_descriptor(io: @out, crc32: 0, compressed_size: e.compressed_size, uncompressed_size: e.uncompressed_size)
|
157
263
|
end
|
158
264
|
end
|