Home | History | Annotate | Download | only in okio
      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 package okio;
     17 
     18 import java.io.EOFException;
     19 import java.io.IOException;
     20 import java.util.zip.CRC32;
     21 import java.util.zip.Inflater;
     22 
     23 public final class GzipSource implements Source {
     24   private static final byte FHCRC = 1;
     25   private static final byte FEXTRA = 2;
     26   private static final byte FNAME = 3;
     27   private static final byte FCOMMENT = 4;
     28 
     29   private static final byte SECTION_HEADER = 0;
     30   private static final byte SECTION_BODY = 1;
     31   private static final byte SECTION_TRAILER = 2;
     32   private static final byte SECTION_DONE = 3;
     33 
     34   /** The current section. Always progresses forward. */
     35   private int section = SECTION_HEADER;
     36 
     37   /**
     38    * Our source should yield a GZIP header (which we consume directly), followed
     39    * by deflated bytes (which we consume via an InflaterSource), followed by a
     40    * GZIP trailer (which we also consume directly).
     41    */
     42   private final BufferedSource source;
     43 
     44   /** The inflater used to decompress the deflated body. */
     45   private final Inflater inflater;
     46 
     47   /**
     48    * The inflater source takes care of moving data between compressed source and
     49    * decompressed sink buffers.
     50    */
     51   private final InflaterSource inflaterSource;
     52 
     53   /** Checksum used to check both the GZIP header and decompressed body. */
     54   private final CRC32 crc = new CRC32();
     55 
     56   public GzipSource(Source source) throws IOException {
     57     this.inflater = new Inflater(true);
     58     this.source = Okio.buffer(source);
     59     this.inflaterSource = new InflaterSource(this.source, inflater);
     60   }
     61 
     62   @Override public long read(OkBuffer sink, long byteCount) throws IOException {
     63     if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
     64     if (byteCount == 0) return 0;
     65 
     66     // If we haven't consumed the header, we must consume it before anything else.
     67     if (section == SECTION_HEADER) {
     68       consumeHeader();
     69       section = SECTION_BODY;
     70     }
     71 
     72     // Attempt to read at least a byte of the body. If we do, we're done.
     73     if (section == SECTION_BODY) {
     74       long offset = sink.size;
     75       long result = inflaterSource.read(sink, byteCount);
     76       if (result != -1) {
     77         updateCrc(sink, offset, result);
     78         return result;
     79       }
     80       section = SECTION_TRAILER;
     81     }
     82 
     83     // The body is exhausted; time to read the trailer. We always consume the
     84     // trailer before returning a -1 exhausted result; that way if you read to
     85     // the end of a GzipSource you guarantee that the CRC has been checked.
     86     if (section == SECTION_TRAILER) {
     87       consumeTrailer();
     88       section = SECTION_DONE;
     89 
     90       // Gzip streams self-terminate: they return -1 before their underlying
     91       // source returns -1. Here we attempt to force the underlying stream to
     92       // return -1 which may trigger it to release its resources. If it doesn't
     93       // return -1, then our Gzip data finished prematurely!
     94       if (!source.exhausted()) {
     95         throw new IOException("gzip finished without exhausting source");
     96       }
     97     }
     98 
     99     return -1;
    100   }
    101 
    102   private void consumeHeader() throws IOException {
    103     // Read the 10-byte header. We peek at the flags byte first so we know if we
    104     // need to CRC the entire header. Then we read the magic ID1ID2 sequence.
    105     // We can skip everything else in the first 10 bytes.
    106     // +---+---+---+---+---+---+---+---+---+---+
    107     // |ID1|ID2|CM |FLG|     MTIME     |XFL|OS | (more-->)
    108     // +---+---+---+---+---+---+---+---+---+---+
    109     source.require(10);
    110     byte flags = source.buffer().getByte(3);
    111     boolean fhcrc = ((flags >> FHCRC) & 1) == 1;
    112     if (fhcrc) updateCrc(source.buffer(), 0, 10);
    113 
    114     short id1id2 = source.readShort();
    115     checkEqual("ID1ID2", (short) 0x1f8b, id1id2);
    116     source.skip(8);
    117 
    118     // Skip optional extra fields.
    119     // +---+---+=================================+
    120     // | XLEN  |...XLEN bytes of "extra field"...| (more-->)
    121     // +---+---+=================================+
    122     if (((flags >> FEXTRA) & 1) == 1) {
    123       source.require(2);
    124       if (fhcrc) updateCrc(source.buffer(), 0, 2);
    125       int xlen = source.buffer().readShortLe();
    126       source.require(xlen);
    127       if (fhcrc) updateCrc(source.buffer(), 0, xlen);
    128       source.skip(xlen);
    129     }
    130 
    131     // Skip an optional 0-terminated name.
    132     // +=========================================+
    133     // |...original file name, zero-terminated...| (more-->)
    134     // +=========================================+
    135     if (((flags >> FNAME) & 1) == 1) {
    136       long index = source.indexOf((byte) 0);
    137       if (index == -1) throw new EOFException();
    138       if (fhcrc) updateCrc(source.buffer(), 0, index + 1);
    139       source.skip(index + 1);
    140     }
    141 
    142     // Skip an optional 0-terminated comment.
    143     // +===================================+
    144     // |...file comment, zero-terminated...| (more-->)
    145     // +===================================+
    146     if (((flags >> FCOMMENT) & 1) == 1) {
    147       long index = source.indexOf((byte) 0);
    148       if (index == -1) throw new EOFException();
    149       if (fhcrc) updateCrc(source.buffer(), 0, index + 1);
    150       source.skip(index + 1);
    151     }
    152 
    153     // Confirm the optional header CRC.
    154     // +---+---+
    155     // | CRC16 |
    156     // +---+---+
    157     if (fhcrc) {
    158       checkEqual("FHCRC", source.readShortLe(), (short) crc.getValue());
    159       crc.reset();
    160     }
    161   }
    162 
    163   private void consumeTrailer() throws IOException {
    164     // Read the eight-byte trailer. Confirm the body's CRC and size.
    165     // +---+---+---+---+---+---+---+---+
    166     // |     CRC32     |     ISIZE     |
    167     // +---+---+---+---+---+---+---+---+
    168     checkEqual("CRC", source.readIntLe(), (int) crc.getValue());
    169     checkEqual("ISIZE", source.readIntLe(), inflater.getTotalOut());
    170   }
    171 
    172   @Override public Source deadline(Deadline deadline) {
    173     source.deadline(deadline);
    174     return this;
    175   }
    176 
    177   @Override public void close() throws IOException {
    178     inflaterSource.close();
    179   }
    180 
    181   /** Updates the CRC with the given bytes. */
    182   private void updateCrc(OkBuffer buffer, long offset, long byteCount) {
    183     for (Segment s = buffer.head; byteCount > 0; s = s.next) {
    184       int segmentByteCount = s.limit - s.pos;
    185       if (offset < segmentByteCount) {
    186         int toUpdate = (int) Math.min(byteCount, segmentByteCount - offset);
    187         crc.update(s.data, (int) (s.pos + offset), toUpdate);
    188         byteCount -= toUpdate;
    189       }
    190       offset -= segmentByteCount; // Track the offset of the current segment.
    191     }
    192   }
    193 
    194   private void checkEqual(String name, int expected, int actual) throws IOException {
    195     if (actual != expected) {
    196       throw new IOException(String.format(
    197           "%s: actual 0x%08x != expected 0x%08x", name, actual, expected));
    198     }
    199   }
    200 }
    201