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