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  package org.apache.commons.compress.compressors.lz4;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  
24  import org.apache.commons.compress.compressors.lz77support.AbstractLZ77CompressorInputStream;
25  import org.apache.commons.compress.utils.ByteUtils;
26  
27  /**
28   * CompressorInputStream for the LZ4 block format.
29   *
30   * @see <a href="https://lz4.github.io/lz4/lz4_Block_format.html">LZ4 Block Format Description</a>
31   * @since 1.14
32   * @NotThreadSafe
33   */
34  public class BlockLZ4CompressorInputStream extends AbstractLZ77CompressorInputStream {
35  
36      private enum State {
37          NO_BLOCK, IN_LITERAL, LOOKING_FOR_BACK_REFERENCE, IN_BACK_REFERENCE, EOF
38      }
39  
40      static final int WINDOW_SIZE = 1 << 16;
41      static final int SIZE_BITS = 4;
42      static final int BACK_REFERENCE_SIZE_MASK = (1 << SIZE_BITS) - 1;
43  
44      static final int LITERAL_SIZE_MASK = BACK_REFERENCE_SIZE_MASK << SIZE_BITS;
45  
46      /** Back-Reference-size part of the block starting byte. */
47      private int nextBackReferenceSize;
48  
49      /** Current state of the stream */
50      private State state = State.NO_BLOCK;
51  
52      /**
53       * Creates a new LZ4 input stream.
54       *
55       * @param is An InputStream to read compressed data from
56       */
57      public BlockLZ4CompressorInputStream(final InputStream is) {
58          super(is, WINDOW_SIZE);
59      }
60  
61      /**
62       * @return false if there is no more back-reference - this means this is the last block of the stream.
63       */
64      private boolean initializeBackReference() throws IOException {
65          int backReferenceOffset;
66          try {
67              backReferenceOffset = (int) ByteUtils.fromLittleEndian(supplier, 2);
68          } catch (final IOException ex) {
69              if (nextBackReferenceSize == 0) { // the last block has no back-reference
70                  return false;
71              }
72              throw ex;
73          }
74          long backReferenceSize = nextBackReferenceSize;
75          if (nextBackReferenceSize == BACK_REFERENCE_SIZE_MASK) {
76              backReferenceSize += readSizeBytes();
77          }
78          // minimal match length 4 is encoded as 0
79          if (backReferenceSize < 0) {
80              throw new IOException("Illegal block with a negative match length found");
81          }
82          try {
83              startBackReference(backReferenceOffset, backReferenceSize + 4);
84          } catch (final IllegalArgumentException ex) {
85              throw new IOException("Illegal block with bad offset found", ex);
86          }
87          state = State.IN_BACK_REFERENCE;
88          return true;
89      }
90  
91      /**
92       * {@inheritDoc}
93       */
94      @Override
95      public int read(final byte[] b, final int off, final int len) throws IOException {
96          if (len == 0) {
97              return 0;
98          }
99          switch (state) {
100         case EOF:
101             return -1;
102         case NO_BLOCK: // NOSONAR - fallthrough intended
103             readSizes();
104             /* FALLTHROUGH */
105         case IN_LITERAL:
106             final int litLen = readLiteral(b, off, len);
107             if (!hasMoreDataInBlock()) {
108                 state = State.LOOKING_FOR_BACK_REFERENCE;
109             }
110             return litLen > 0 ? litLen : read(b, off, len);
111         case LOOKING_FOR_BACK_REFERENCE: // NOSONAR - fallthrough intended
112             if (!initializeBackReference()) {
113                 state = State.EOF;
114                 return -1;
115             }
116             /* FALLTHROUGH */
117         case IN_BACK_REFERENCE:
118             final int backReferenceLen = readBackReference(b, off, len);
119             if (!hasMoreDataInBlock()) {
120                 state = State.NO_BLOCK;
121             }
122             return backReferenceLen > 0 ? backReferenceLen : read(b, off, len);
123         default:
124             throw new IOException("Unknown stream state " + state);
125         }
126     }
127 
128     private long readSizeBytes() throws IOException {
129         long accum = 0;
130         int nextByte;
131         do {
132             nextByte = readOneByte();
133             if (nextByte == -1) {
134                 throw new IOException("Premature end of stream while parsing length");
135             }
136             accum += nextByte;
137         } while (nextByte == 255);
138         return accum;
139     }
140 
141     private void readSizes() throws IOException {
142         final int nextBlock = readOneByte();
143         if (nextBlock == -1) {
144             throw new IOException("Premature end of stream while looking for next block");
145         }
146         nextBackReferenceSize = nextBlock & BACK_REFERENCE_SIZE_MASK;
147         long literalSizePart = (nextBlock & LITERAL_SIZE_MASK) >> SIZE_BITS;
148         if (literalSizePart == BACK_REFERENCE_SIZE_MASK) {
149             literalSizePart += readSizeBytes();
150         }
151         if (literalSizePart < 0) {
152             throw new IOException("Illegal block with a negative literal size found");
153         }
154         startLiteral(literalSizePart);
155         state = State.IN_LITERAL;
156     }
157 }