1 /*
2 * Copyright (C) 2014 Square, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 @file:JvmName("-GzipSourceExtensions")
18 @file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
19
20 package okio
21
22 import java.io.EOFException
23 import java.io.IOException
24 import java.util.zip.CRC32
25 import java.util.zip.Inflater
26
27 /**
28 * A source that uses [GZIP](http://www.ietf.org/rfc/rfc1952.txt) to
29 * decompress data read from another source.
30 */
31 class GzipSource(source: Source) : Source {
32
33 /** The current section. Always progresses forward. */
34 private var section = SECTION_HEADER
35
36 /**
37 * Our source should yield a GZIP header (which we consume directly), followed
38 * by deflated bytes (which we consume via an InflaterSource), followed by a
39 * GZIP trailer (which we also consume directly).
40 */
41 private val source = RealBufferedSource(source)
42
43 /** The inflater used to decompress the deflated body. */
44 private val inflater = Inflater(true)
45
46 /**
47 * The inflater source takes care of moving data between compressed source and
48 * decompressed sink buffers.
49 */
50 private val inflaterSource = InflaterSource(this.source, inflater)
51
52 /** Checksum used to check both the GZIP header and decompressed body. */
53 private val crc = CRC32()
54
55 @Throws(IOException::class)
readnull56 override fun read(sink: Buffer, byteCount: Long): Long {
57 require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
58 if (byteCount == 0L) return 0L
59
60 // If we haven't consumed the header, we must consume it before anything else.
61 if (section == SECTION_HEADER) {
62 consumeHeader()
63 section = SECTION_BODY
64 }
65
66 // Attempt to read at least a byte of the body. If we do, we're done.
67 if (section == SECTION_BODY) {
68 val offset = sink.size
69 val result = inflaterSource.read(sink, byteCount)
70 if (result != -1L) {
71 updateCrc(sink, offset, result)
72 return result
73 }
74 section = SECTION_TRAILER
75 }
76
77 // The body is exhausted; time to read the trailer. We always consume the
78 // trailer before returning a -1 exhausted result; that way if you read to
79 // the end of a GzipSource you guarantee that the CRC has been checked.
80 if (section == SECTION_TRAILER) {
81 consumeTrailer()
82 section = SECTION_DONE
83
84 // Gzip streams self-terminate: they return -1 before their underlying
85 // source returns -1. Here we attempt to force the underlying stream to
86 // return -1 which may trigger it to release its resources. If it doesn't
87 // return -1, then our Gzip data finished prematurely!
88 if (!source.exhausted()) {
89 throw IOException("gzip finished without exhausting source")
90 }
91 }
92
93 return -1
94 }
95
96 @Throws(IOException::class)
consumeHeadernull97 private fun consumeHeader() {
98 // Read the 10-byte header. We peek at the flags byte first so we know if we
99 // need to CRC the entire header. Then we read the magic ID1ID2 sequence.
100 // We can skip everything else in the first 10 bytes.
101 // +---+---+---+---+---+---+---+---+---+---+
102 // |ID1|ID2|CM |FLG| MTIME |XFL|OS | (more-->)
103 // +---+---+---+---+---+---+---+---+---+---+
104 source.require(10)
105 val flags = source.buffer[3].toInt()
106 val fhcrc = flags.getBit(FHCRC)
107 if (fhcrc) updateCrc(source.buffer, 0, 10)
108
109 val id1id2 = source.readShort()
110 checkEqual("ID1ID2", 0x1f8b, id1id2.toInt())
111 source.skip(8)
112
113 // Skip optional extra fields.
114 // +---+---+=================================+
115 // | XLEN |...XLEN bytes of "extra field"...| (more-->)
116 // +---+---+=================================+
117 if (flags.getBit(FEXTRA)) {
118 source.require(2)
119 if (fhcrc) updateCrc(source.buffer, 0, 2)
120 val xlen = (source.buffer.readShortLe().toInt() and 0xffff).toLong()
121 source.require(xlen)
122 if (fhcrc) updateCrc(source.buffer, 0, xlen)
123 source.skip(xlen)
124 }
125
126 // Skip an optional 0-terminated name.
127 // +=========================================+
128 // |...original file name, zero-terminated...| (more-->)
129 // +=========================================+
130 if (flags.getBit(FNAME)) {
131 val index = source.indexOf(0)
132 if (index == -1L) throw EOFException()
133 if (fhcrc) updateCrc(source.buffer, 0, index + 1)
134 source.skip(index + 1)
135 }
136
137 // Skip an optional 0-terminated comment.
138 // +===================================+
139 // |...file comment, zero-terminated...| (more-->)
140 // +===================================+
141 if (flags.getBit(FCOMMENT)) {
142 val index = source.indexOf(0)
143 if (index == -1L) throw EOFException()
144 if (fhcrc) updateCrc(source.buffer, 0, index + 1)
145 source.skip(index + 1)
146 }
147
148 // Confirm the optional header CRC.
149 // +---+---+
150 // | CRC16 |
151 // +---+---+
152 if (fhcrc) {
153 checkEqual("FHCRC", source.readShortLe().toInt(), crc.value.toShort().toInt())
154 crc.reset()
155 }
156 }
157
158 @Throws(IOException::class)
consumeTrailernull159 private fun consumeTrailer() {
160 // Read the eight-byte trailer. Confirm the body's CRC and size.
161 // +---+---+---+---+---+---+---+---+
162 // | CRC32 | ISIZE |
163 // +---+---+---+---+---+---+---+---+
164 checkEqual("CRC", source.readIntLe(), crc.value.toInt())
165 checkEqual("ISIZE", source.readIntLe(), inflater.bytesWritten.toInt())
166 }
167
timeoutnull168 override fun timeout(): Timeout = source.timeout()
169
170 @Throws(IOException::class)
171 override fun close() = inflaterSource.close()
172
173 /** Updates the CRC with the given bytes. */
174 private fun updateCrc(buffer: Buffer, offset: Long, byteCount: Long) {
175 var offset = offset
176 var byteCount = byteCount
177 // Skip segments that we aren't checksumming.
178 var s = buffer.head!!
179 while (offset >= s.limit - s.pos) {
180 offset -= s.limit - s.pos
181 s = s.next!!
182 }
183
184 // Checksum one segment at a time.
185 while (byteCount > 0) {
186 val pos = (s.pos + offset).toInt()
187 val toUpdate = minOf(s.limit - pos, byteCount).toInt()
188 crc.update(s.data, pos, toUpdate)
189 byteCount -= toUpdate
190 offset = 0
191 s = s.next!!
192 }
193 }
194
checkEqualnull195 private fun checkEqual(name: String, expected: Int, actual: Int) {
196 if (actual != expected) {
197 throw IOException("%s: actual 0x%08x != expected 0x%08x".format(name, actual, expected))
198 }
199 }
200 }
201
getBitnull202 private inline fun Int.getBit(bit: Int) = this shr bit and 1 == 1
203
204 private const val FHCRC = 1
205 private const val FEXTRA = 2
206 private const val FNAME = 3
207 private const val FCOMMENT = 4
208
209 private const val SECTION_HEADER: Byte = 0
210 private const val SECTION_BODY: Byte = 1
211 private const val SECTION_TRAILER: Byte = 2
212 private const val SECTION_DONE: Byte = 3
213
214 /**
215 * Returns a [GzipSource] that gzip-decompresses this [Source] while reading.
216 *
217 * @see GzipSource
218 */
219 inline fun Source.gzip() = GzipSource(this)
220