View Javadoc
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  
20  package org.apache.commons.compress.archivers.zip;
21  
22  import java.io.IOException;
23  import java.io.InputStream;
24  
25  import org.apache.commons.compress.utils.ExactMath;
26  import org.apache.commons.compress.utils.InputStreamStatistics;
27  import org.apache.commons.io.input.CloseShieldInputStream;
28  import org.apache.commons.io.input.CountingInputStream;
29  
30  /**
31   * 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.
32   * <p>
33   * The algorithm is described in the ZIP File Format Specification.
34   *
35   * @see <a href="https://www.pkware.com/documents/casestudies/APPNOTE.TXT">ZIP File Format Specification</a>
36   *
37   * @since 1.7
38   */
39  final class ExplodingInputStream extends InputStream implements InputStreamStatistics {
40  
41      /** The underlying stream containing the compressed data */
42      private final InputStream in;
43  
44      /** The stream of bits read from the input stream */
45      private BitStream bits;
46  
47      /** The size of the sliding dictionary (4K or 8K) */
48      private final int dictionarySize;
49  
50      /** The number of Shannon-Fano trees (2 or 3) */
51      private final int numberOfTrees;
52  
53      private final int minimumMatchLength;
54  
55      /** The binary tree containing the 256 encoded literals (null when only two trees are used) */
56      private BinaryTree literalTree;
57  
58      /** The binary tree containing the 64 encoded lengths */
59      private BinaryTree lengthTree;
60  
61      /** The binary tree containing the 64 encoded distances */
62      private BinaryTree distanceTree;
63  
64      /** Output buffer holding the decompressed data */
65      private final CircularBuffer buffer = new CircularBuffer(32 * 1024);
66  
67      private long uncompressedCount;
68  
69      private long treeSizes;
70  
71      /**
72       * Constructs a new stream decompressing the content of the specified stream using the explode algorithm.
73       *
74       * @param dictionarySize the size of the sliding dictionary (4096 or 8192)
75       * @param numberOfTrees  the number of trees (2 or 3)
76       * @param in             the compressed data stream
77       */
78      ExplodingInputStream(final int dictionarySize, final int numberOfTrees, final InputStream in) {
79          if (dictionarySize != 4096 && dictionarySize != 8192) {
80              throw new IllegalArgumentException("The dictionary size must be 4096 or 8192");
81          }
82          if (numberOfTrees != 2 && numberOfTrees != 3) {
83              throw new IllegalArgumentException("The number of trees must be 2 or 3");
84          }
85          this.dictionarySize = dictionarySize;
86          this.numberOfTrees = numberOfTrees;
87          this.minimumMatchLength = numberOfTrees;
88          this.in = in;
89      }
90  
91      /**
92       * @since 1.17
93       */
94      @Override
95      public void close() throws IOException {
96          in.close();
97      }
98  
99      /**
100      * Fill the sliding dictionary with more data.
101      *
102      * @throws IOException on error.
103      */
104     private void fillBuffer() throws IOException {
105         init();
106 
107         final int bit = bits.nextBit();
108         if (bit == -1) {
109             // EOF
110             return;
111         }
112         if (bit == 1) {
113             // literal value
114             final int literal;
115             if (literalTree != null) {
116                 literal = literalTree.read(bits);
117             } else {
118                 literal = bits.nextByte();
119             }
120 
121             if (literal == -1) {
122                 // end of stream reached, nothing left to decode
123                 return;
124             }
125 
126             buffer.put(literal);
127 
128         } else {
129             // back reference
130             final int distanceLowSize = dictionarySize == 4096 ? 6 : 7;
131             final int distanceLow = (int) bits.nextBits(distanceLowSize);
132             final int distanceHigh = distanceTree.read(bits);
133             if (distanceHigh == -1 && distanceLow <= 0) {
134                 // end of stream reached, nothing left to decode
135                 return;
136             }
137             final int distance = distanceHigh << distanceLowSize | distanceLow;
138 
139             int length = lengthTree.read(bits);
140             if (length == 63) {
141                 final long nextByte = bits.nextBits(8);
142                 if (nextByte == -1) {
143                     // EOF
144                     return;
145                 }
146                 length = ExactMath.add(length, nextByte);
147             }
148             length += minimumMatchLength;
149 
150             buffer.copy(distance + 1, length);
151         }
152     }
153 
154     /**
155      * @since 1.17
156      */
157     @Override
158     public long getCompressedCount() {
159         return bits.getBytesRead() + treeSizes;
160     }
161 
162     /**
163      * @since 1.17
164      */
165     @Override
166     public long getUncompressedCount() {
167         return uncompressedCount;
168     }
169 
170     /**
171      * Reads the encoded binary trees and prepares the bit stream.
172      *
173      * @throws IOException
174      */
175     private void init() throws IOException {
176         if (bits == null) {
177             // we do not want to close in
178             try (CountingInputStream i = new CountingInputStream(CloseShieldInputStream.wrap(in))) {
179                 if (numberOfTrees == 3) {
180                     literalTree = BinaryTree.decode(i, 256);
181                 }
182 
183                 lengthTree = BinaryTree.decode(i, 64);
184                 distanceTree = BinaryTree.decode(i, 64);
185                 treeSizes += i.getByteCount();
186             }
187 
188             bits = new BitStream(in);
189         }
190     }
191 
192     @Override
193     public int read() throws IOException {
194         if (!buffer.available()) {
195             try {
196                 fillBuffer();
197             } catch (final IllegalArgumentException ex) {
198                 throw new IOException("bad IMPLODE stream", ex);
199             }
200         }
201 
202         final int ret = buffer.get();
203         if (ret > -1) {
204             uncompressedCount++;
205         }
206         return ret;
207     }
208 
209 }