-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
reader.cr
174 lines (152 loc) · 4.46 KB
/
reader.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
require "./file_info"
# Reads zip file entries sequentially from an `IO`.
#
# NOTE: Entries might not have correct values
# for crc32, compressed_size, uncompressed_size and comment,
# because when reading a zip file directly from a stream this
# information might be stored later in the zip stream.
# If you need this information, consider using `Zip::File`.
#
# ### Example
#
# ```
# File.open("./file.zip") do |file|
# Zip::Reader.open(file) do |zip|
# zip.each_entry do |entry|
# p entry.filename
# p entry.file?
# p entry.dir?
# p entry.io.gets_to_end
# end
# end
# end
# ```
class Zip::Reader
# Whether to close the enclosed `IO` when closing this reader.
property? sync_close = false
# Returns `true` if this reader is closed.
getter? closed = false
# Creates a new reader from the given *io*.
def initialize(@io : IO, @sync_close = false)
@reached_end = false
@read_data_descriptor = true
end
# Creates a new reader from the given *filename*.
def self.new(filename : String)
new(::File.new(filename), sync_close: true)
end
# Creates a new reader from the given *io*, yields it to the given block,
# and closes it at the end.
def self.open(io : IO, sync_close = false)
reader = new(io, sync_close: sync_close)
yield reader ensure reader.close
end
# Creates a new reader from the given *filename*, yields it to the given block,
# and closes it at the end.
def self.open(filename : String)
reader = new(filename)
yield reader ensure reader.close
end
# Reads the next entry in the zip, or `nil` if there
# are no more entries.
#
# After reading a next entry, previous entries can no
# longer be read (their `IO` will be closed.)
def next_entry : Entry?
return nil if @reached_end
if last_entry = @last_entry
last_entry.close
skip_data_descriptor(last_entry)
end
while true
signature = read UInt32
case signature
when FileInfo::SIGNATURE
# Found file info signature
break
when FileInfo::DATA_DESCRIPTOR_SIGNATURE
if last_entry && !@read_data_descriptor
# Consider the case where a data descriptor comes after
# a STORED entry: skip data descriptor and expect file signature next
read_data_descriptor(last_entry)
next
else
raise Error.new("Unexpected data descriptor when reading zip")
end
else
# Other signature: we are done with entries (next comes metadata)
@reached_end = true
return nil
end
end
@last_entry = Entry.new(@io)
end
# Yields each entry in the zip to the given block.
def each_entry
while entry = next_entry
yield entry
end
end
# Closes this zip reader.
def close
return if @closed
@closed = true
@io.close if @sync_close
end
private def skip_data_descriptor(entry)
if entry.compression_method.deflated? && entry.bit_3_set?
# The data descriptor signature is optional: if we
# find it, we read the data descriptor info normally;
# otherwise, the first four bytes are the crc32 value.
signature = read UInt32
if signature == FileInfo::DATA_DESCRIPTOR_SIGNATURE
read_data_descriptor(entry)
else
read_data_descriptor(entry, crc32: signature)
end
@read_data_descriptor = true
else
@read_data_descriptor = false
verify_checksum(entry)
end
end
private def read_data_descriptor(entry, crc32 = nil)
entry.crc32 = crc32 || (read UInt32)
entry.compressed_size = read UInt32
entry.uncompressed_size = read UInt32
verify_checksum(entry)
end
private def verify_checksum(entry)
if entry.crc32 != entry.checksum_io.crc32
raise Zip::Error.new("Checksum failed for entry #{entry.filename} (expected #{entry.crc32}, got #{entry.checksum_io.crc32}")
end
end
private def read(type)
@io.read_bytes(type, IO::ByteFormat::LittleEndian)
end
# A entry inside a `Zip::Reader`.
#
# Use the `io` method to read from it.
class Entry
include FileInfo
# :nodoc:
def initialize(io)
super(at_file_header: io)
@io = ChecksumReader.new(decompressor_for(io), @filename)
@closed = false
end
# Returns an `IO` to the entry's data.
def io : IO
@io
end
protected def checksum_io
@io
end
protected def close
return if @closed
@closed = true
@io.skip_to_end
@io.close
end
end
end