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.DataFormatException;
     21 import java.util.zip.Inflater;
     22 
     23 /**
     24  * A source that uses <a href="http://tools.ietf.org/html/rfc1951">DEFLATE</a>
     25  * to decompress data read from another source.
     26  */
     27 public final class InflaterSource implements Source {
     28   private final BufferedSource source;
     29   private final Inflater inflater;
     30 
     31   /**
     32    * When we call Inflater.setInput(), the inflater keeps our byte array until
     33    * it needs input again. This tracks how many bytes the inflater is currently
     34    * holding on to.
     35    */
     36   private int bufferBytesHeldByInflater;
     37   private boolean closed;
     38 
     39   public InflaterSource(Source source, Inflater inflater) {
     40     this(Okio.buffer(source), inflater);
     41   }
     42 
     43   /**
     44    * This package-private constructor shares a buffer with its trusted caller.
     45    * In general we can't share a BufferedSource because the inflater holds input
     46    * bytes until they are inflated.
     47    */
     48   InflaterSource(BufferedSource source, Inflater inflater) {
     49     if (source == null) throw new IllegalArgumentException("source == null");
     50     if (inflater == null) throw new IllegalArgumentException("inflater == null");
     51     this.source = source;
     52     this.inflater = inflater;
     53   }
     54 
     55   @Override public long read(
     56       Buffer sink, long byteCount) throws IOException {
     57     if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
     58     if (closed) throw new IllegalStateException("closed");
     59     if (byteCount == 0) return 0;
     60 
     61     while (true) {
     62       boolean sourceExhausted = refill();
     63 
     64       // Decompress the inflater's compressed data into the sink.
     65       try {
     66         Segment tail = sink.writableSegment(1);
     67         int bytesInflated = inflater.inflate(tail.data, tail.limit, Segment.SIZE - tail.limit);
     68         if (bytesInflated > 0) {
     69           tail.limit += bytesInflated;
     70           sink.size += bytesInflated;
     71           return bytesInflated;
     72         }
     73         if (inflater.finished() || inflater.needsDictionary()) {
     74           releaseInflatedBytes();
     75           if (tail.pos == tail.limit) {
     76             // We allocated a tail segment, but didn't end up needing it. Recycle!
     77             sink.head = tail.pop();
     78             SegmentPool.recycle(tail);
     79           }
     80           return -1;
     81         }
     82         if (sourceExhausted) throw new EOFException("source exhausted prematurely");
     83       } catch (DataFormatException e) {
     84         throw new IOException(e);
     85       }
     86     }
     87   }
     88 
     89   /**
     90    * Refills the inflater with compressed data if it needs input. (And only if
     91    * it needs input). Returns true if the inflater required input but the source
     92    * was exhausted.
     93    */
     94   public boolean refill() throws IOException {
     95     if (!inflater.needsInput()) return false;
     96 
     97     releaseInflatedBytes();
     98     if (inflater.getRemaining() != 0) throw new IllegalStateException("?"); // TODO: possible?
     99 
    100     // If there are compressed bytes in the source, assign them to the inflater.
    101     if (source.exhausted()) return true;
    102 
    103     // Assign buffer bytes to the inflater.
    104     Segment head = source.buffer().head;
    105     bufferBytesHeldByInflater = head.limit - head.pos;
    106     inflater.setInput(head.data, head.pos, bufferBytesHeldByInflater);
    107     return false;
    108   }
    109 
    110   /** When the inflater has processed compressed data, remove it from the buffer. */
    111   private void releaseInflatedBytes() throws IOException {
    112     if (bufferBytesHeldByInflater == 0) return;
    113     int toRelease = bufferBytesHeldByInflater - inflater.getRemaining();
    114     bufferBytesHeldByInflater -= toRelease;
    115     source.skip(toRelease);
    116   }
    117 
    118   @Override public Timeout timeout() {
    119     return source.timeout();
    120   }
    121 
    122   @Override public void close() throws IOException {
    123     if (closed) return;
    124     inflater.end();
    125     closed = true;
    126     source.close();
    127   }
    128 }
    129