xref: /aosp_15_r20/external/okio/okio/src/jvmMain/kotlin/okio/GzipSource.kt (revision f9742813c14b702d71392179818a9e591da8620c)
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