-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
reader.cr
159 lines (128 loc) · 4.27 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
# A read-only `IO` object to decompress data in the gzip format.
#
# Instances of this class wrap another IO object. When you read from this instance
# instance, it reads data from the underlying IO, decompresses it, and returns
# it to the caller.
#
# NOTE: A gzip stream can contain zero or more members. If it contains
# no members, `header` will be `nil`. If it contains one or more
# members, only the first header will be recorded here. This is
# because gzipping multiple members is not common as one usually
# combines gzip with tar. If, however, multiple members are present
# then reading from this reader will return the concatenation of
# all the members.
#
# ### Example: decompress a gzip file
#
# ```
# require "compress/gzip"
#
# File.write("file.gzip", Bytes[31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 76, 74, 6, 0, 194, 65, 36, 53, 3, 0, 0, 0])
#
# string = File.open("file.gzip") do |file|
# Compress::Gzip::Reader.open(file) do |gzip|
# gzip.gets_to_end
# end
# end
# string # => "abc"
# ```
class Compress::Gzip::Reader < IO
include IO::Buffered
# Whether to close the enclosed `IO` when closing this reader.
property? sync_close = false
# Returns `true` if this reader is closed.
getter? closed = false
# Returns the first header in the gzip stream, if any.
getter header : Header?
@flate_io : Compress::Deflate::Reader?
# Creates a new reader from the given *io*.
def initialize(@io : IO, @sync_close = false)
# CRC32 of written data
@crc32 = ::Digest::CRC32.initial
# Total size of the original (uncompressed) input data modulo 2^32.
@isize = 0_u32
first_byte = @io.read_byte
# A gzip file could be empty (have no members), so
# we account for that case
return unless first_byte
@header = Header.new(first_byte, @io)
@flate_io = Compress::Deflate::Reader.new(@io)
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
# See `IO#read`.
def unbuffered_read(slice : Bytes) : Int32
check_open
return 0 if slice.empty?
while true
flate_io = @flate_io
return 0 unless flate_io
read_bytes = flate_io.read(slice)
if read_bytes == 0
crc32 = @io.read_bytes(UInt32, IO::ByteFormat::LittleEndian)
isize = @io.read_bytes(UInt32, IO::ByteFormat::LittleEndian)
if crc32 != @crc32
raise Compress::Gzip::Error.new("CRC32 checksum mismatch")
end
if isize != @isize
raise Compress::Gzip::Error.new("isize mismatch")
end
# Reset checksum and total size for next entry
@crc32 = ::Digest::CRC32.initial
@isize = 0_u32
# Check if another header with data comes
first_byte = @io.read_byte
if first_byte
Header.new(first_byte, @io)
@flate_io = Compress::Deflate::Reader.new(@io)
else
@flate_io = nil
break
end
else
# Update CRC32 and total data size
@crc32 = ::Digest::CRC32.update(slice[0, read_bytes], @crc32)
# Using wrapping addition here because isize is only 32 bits wide but
# uncompressed data size can be bigger.
@isize &+= read_bytes
break
end
end
read_bytes
end
# Always raises `IO::Error` because this is a read-only `IO`.
def unbuffered_write(slice : Bytes) : Nil
raise IO::Error.new("Can't write to Compress::Gzip::Reader")
end
def unbuffered_flush : NoReturn
raise IO::Error.new "Can't flush Compress::Gzip::Reader"
end
# Closes this reader.
def unbuffered_close : Nil
return if @closed
@closed = true
@flate_io.try &.close
@io.close if @sync_close
end
def unbuffered_rewind : Nil
check_open
@io.rewind
@header = nil
@flate_io = nil
initialize(@io, @sync_close)
end
end