-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
reader.cr
138 lines (114 loc) · 3.68 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
# 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 "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|
# Gzip::Reader.open(file) do |gzip|
# gzip.gets_to_end
# end
# end
# string # => "abc"
# ```
class Gzip::Reader
include IO
# 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 : Flate::Reader?
# Creates a new reader from the given *io*.
def initialize(@io : IO, @sync_close = false)
@crc32 = CRC32.initial # CRC32 of written data
@isize = 0_u32 # Total size of written data
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 = Flate::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 read(slice : Bytes)
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 Gzip::Error.new("CRC32 checksum mismatch")
end
if isize != @isize
raise Gzip::Error.new("isize mismatch")
end
# Reset checksum and total size for next entry
@crc32 = 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 = Flate::Reader.new(@io)
else
@flate_io = nil
break
end
else
# Update CRC32 and total data size
@crc32 = CRC32.update(slice[0, read_bytes], @crc32)
@isize += read_bytes
break
end
end
read_bytes
end
# Always raises `IO::Error` because this is a read-only `IO`.
def write(slice : Bytes) : Nil
raise IO::Error.new("Can't write to Gzip::Reader")
end
# Closes this reader.
def close
return if @closed
@closed = true
@flate_io.try &.close
@io.close if @sync_close
end
end