ExplodingInputStream.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one
  3.  * or more contributor license agreements.  See the NOTICE file
  4.  * distributed with this work for additional information
  5.  * regarding copyright ownership.  The ASF licenses this file
  6.  * to you under the Apache License, Version 2.0 (the
  7.  * "License"); you may not use this file except in compliance
  8.  * with the License.  You may obtain a copy of the License at
  9.  *
  10.  * http://www.apache.org/licenses/LICENSE-2.0
  11.  *
  12.  * Unless required by applicable law or agreed to in writing,
  13.  * software distributed under the License is distributed on an
  14.  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15.  * KIND, either express or implied.  See the License for the
  16.  * specific language governing permissions and limitations
  17.  * under the License.
  18.  */

  19. package org.apache.commons.compress.archivers.zip;

  20. import java.io.IOException;
  21. import java.io.InputStream;

  22. import org.apache.commons.compress.utils.ExactMath;
  23. import org.apache.commons.compress.utils.InputStreamStatistics;
  24. import org.apache.commons.io.input.BoundedInputStream;
  25. import org.apache.commons.io.input.CloseShieldInputStream;

  26. /**
  27.  * The implode compression method was added to PKZIP 1.01 released in 1989. It was then dropped from PKZIP 2.0 released in 1993 in favor of the deflate method.
  28.  * <p>
  29.  * The algorithm is described in the ZIP File Format Specification.
  30.  *
  31.  * @see <a href="https://www.pkware.com/documents/casestudies/APPNOTE.TXT">ZIP File Format Specification</a>
  32.  *
  33.  * @since 1.7
  34.  */
  35. final class ExplodingInputStream extends InputStream implements InputStreamStatistics {

  36.     /** The underlying stream containing the compressed data */
  37.     private final InputStream in;

  38.     /** The stream of bits read from the input stream */
  39.     private BitStream bits;

  40.     /** The size of the sliding dictionary (4K or 8K) */
  41.     private final int dictionarySize;

  42.     /** The number of Shannon-Fano trees (2 or 3) */
  43.     private final int numberOfTrees;

  44.     private final int minimumMatchLength;

  45.     /** The binary tree containing the 256 encoded literals (null when only two trees are used) */
  46.     private BinaryTree literalTree;

  47.     /** The binary tree containing the 64 encoded lengths */
  48.     private BinaryTree lengthTree;

  49.     /** The binary tree containing the 64 encoded distances */
  50.     private BinaryTree distanceTree;

  51.     /** Output buffer holding the decompressed data */
  52.     private final CircularBuffer buffer = new CircularBuffer(32 * 1024);

  53.     private long uncompressedCount;

  54.     private long treeSizes;

  55.     /**
  56.      * Constructs a new stream decompressing the content of the specified stream using the explode algorithm.
  57.      *
  58.      * @param dictionarySize the size of the sliding dictionary (4096 or 8192)
  59.      * @param numberOfTrees  the number of trees (2 or 3)
  60.      * @param in             the compressed data stream
  61.      */
  62.     ExplodingInputStream(final int dictionarySize, final int numberOfTrees, final InputStream in) {
  63.         if (dictionarySize != 4096 && dictionarySize != 8192) {
  64.             throw new IllegalArgumentException("The dictionary size must be 4096 or 8192");
  65.         }
  66.         if (numberOfTrees != 2 && numberOfTrees != 3) {
  67.             throw new IllegalArgumentException("The number of trees must be 2 or 3");
  68.         }
  69.         this.dictionarySize = dictionarySize;
  70.         this.numberOfTrees = numberOfTrees;
  71.         this.minimumMatchLength = numberOfTrees;
  72.         this.in = in;
  73.     }

  74.     /**
  75.      * @since 1.17
  76.      */
  77.     @Override
  78.     public void close() throws IOException {
  79.         in.close();
  80.     }

  81.     /**
  82.      * Fill the sliding dictionary with more data.
  83.      *
  84.      * @throws IOException on error.
  85.      */
  86.     private void fillBuffer() throws IOException {
  87.         init();

  88.         final int bit = bits.nextBit();
  89.         if (bit == -1) {
  90.             // EOF
  91.             return;
  92.         }
  93.         if (bit == 1) {
  94.             // literal value
  95.             final int literal;
  96.             if (literalTree != null) {
  97.                 literal = literalTree.read(bits);
  98.             } else {
  99.                 literal = bits.nextByte();
  100.             }

  101.             if (literal == -1) {
  102.                 // end of stream reached, nothing left to decode
  103.                 return;
  104.             }

  105.             buffer.put(literal);

  106.         } else {
  107.             // back reference
  108.             final int distanceLowSize = dictionarySize == 4096 ? 6 : 7;
  109.             final int distanceLow = (int) bits.nextBits(distanceLowSize);
  110.             final int distanceHigh = distanceTree.read(bits);
  111.             if (distanceHigh == -1 && distanceLow <= 0) {
  112.                 // end of stream reached, nothing left to decode
  113.                 return;
  114.             }
  115.             final int distance = distanceHigh << distanceLowSize | distanceLow;

  116.             int length = lengthTree.read(bits);
  117.             if (length == 63) {
  118.                 final long nextByte = bits.nextBits(8);
  119.                 if (nextByte == -1) {
  120.                     // EOF
  121.                     return;
  122.                 }
  123.                 length = ExactMath.add(length, nextByte);
  124.             }
  125.             length += minimumMatchLength;

  126.             buffer.copy(distance + 1, length);
  127.         }
  128.     }

  129.     /**
  130.      * @since 1.17
  131.      */
  132.     @Override
  133.     public long getCompressedCount() {
  134.         return bits.getBytesRead() + treeSizes;
  135.     }

  136.     /**
  137.      * @since 1.17
  138.      */
  139.     @Override
  140.     public long getUncompressedCount() {
  141.         return uncompressedCount;
  142.     }

  143.     /**
  144.      * Reads the encoded binary trees and prepares the bit stream.
  145.      *
  146.      * @throws IOException
  147.      */
  148.     private void init() throws IOException {
  149.         if (bits == null) {
  150.             // we do not want to close in
  151.             try (BoundedInputStream cis = BoundedInputStream.builder().setInputStream(CloseShieldInputStream.wrap(in)).get()) {
  152.                 if (numberOfTrees == 3) {
  153.                     literalTree = BinaryTree.decode(cis, 256);
  154.                 }

  155.                 lengthTree = BinaryTree.decode(cis, 64);
  156.                 distanceTree = BinaryTree.decode(cis, 64);
  157.                 treeSizes += cis.getCount();
  158.             }

  159.             bits = new BitStream(in);
  160.         }
  161.     }

  162.     @Override
  163.     public int read() throws IOException {
  164.         if (!buffer.available()) {
  165.             try {
  166.                 fillBuffer();
  167.             } catch (final IllegalArgumentException ex) {
  168.                 throw new IOException("bad IMPLODE stream", ex);
  169.             }
  170.         }

  171.         final int ret = buffer.get();
  172.         if (ret > -1) {
  173.             uncompressedCount++;
  174.         }
  175.         return ret;
  176.     }

  177. }