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.snappy;
20  
21  import java.io.ByteArrayOutputStream;
22  import java.io.IOException;
23  import java.io.OutputStream;
24  
25  import org.apache.commons.codec.digest.PureJavaCrc32C;
26  import org.apache.commons.compress.compressors.CompressorOutputStream;
27  import org.apache.commons.compress.compressors.lz77support.Parameters;
28  import org.apache.commons.compress.utils.ByteUtils;
29  
30  /**
31   * CompressorOutputStream for the framing Snappy format.
32   *
33   * <p>
34   * Based on the "spec" in the version "Last revised: 2013-10-25"
35   * </p>
36   *
37   * @see <a href="https://github.com/google/snappy/blob/master/framing_format.txt">Snappy framing format description</a>
38   * @since 1.14
39   * @NotThreadSafe
40   */
41  public class FramedSnappyCompressorOutputStream extends CompressorOutputStream<OutputStream> {
42      // see spec:
43      // > However, we place an additional restriction that the uncompressed data
44      // > in a chunk must be no longer than 65,536 bytes. This allows consumers to
45      // > easily use small fixed-size buffers.
46      private static final int MAX_COMPRESSED_BUFFER_SIZE = 1 << 16;
47  
48      static long mask(long x) {
49          // ugly, maybe we should just have used ints and deal with the
50          // overflow
51          x = x >> 15 | x << 17;
52          x += FramedSnappyCompressorInputStream.MASK_OFFSET;
53          x &= 0xffffFFFFL;
54          return x;
55      }
56  
57      private final Parameters params;
58      private final PureJavaCrc32C checksum = new PureJavaCrc32C();
59      // used in one-arg write method
60      private final byte[] oneByte = new byte[1];
61      private final byte[] buffer = new byte[MAX_COMPRESSED_BUFFER_SIZE];
62  
63      private int currentIndex;
64  
65      private final ByteUtils.ByteConsumer consumer;
66  
67      /**
68       * Constructs a new output stream that compresses snappy-framed-compressed data to the specified output stream.
69       *
70       * @param out the OutputStream to which to write the compressed data
71       * @throws IOException if writing the signature fails
72       */
73      public FramedSnappyCompressorOutputStream(final OutputStream out) throws IOException {
74          this(out, SnappyCompressorOutputStream.createParameterBuilder(SnappyCompressorInputStream.DEFAULT_BLOCK_SIZE).build());
75      }
76  
77      /**
78       * Constructs a new output stream that compresses snappy-framed-compressed data to the specified output stream.
79       *
80       * @param out    the OutputStream to which to write the compressed data
81       * @param params parameters used to fine-tune compression, in particular to balance compression ratio vs compression speed.
82       * @throws IOException if writing the signature fails
83       */
84      public FramedSnappyCompressorOutputStream(final OutputStream out, final Parameters params) throws IOException {
85          super(out);
86          this.params = params;
87          consumer = new ByteUtils.OutputStreamByteConsumer(out);
88          out.write(FramedSnappyCompressorInputStream.SZ_SIGNATURE);
89      }
90  
91      @Override
92      public void close() throws IOException {
93          try {
94              finish();
95          } finally {
96              out.close();
97          }
98      }
99  
100     /**
101      * Compresses all remaining data and writes it to the stream, doesn't close the underlying stream.
102      *
103      * @throws IOException if an error occurs
104      */
105     public void finish() throws IOException {
106         flushBuffer();
107     }
108 
109     private void flushBuffer() throws IOException {
110         if (currentIndex == 0) {
111             return;
112         }
113         out.write(FramedSnappyCompressorInputStream.COMPRESSED_CHUNK_TYPE);
114         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
115         try (OutputStream o = new SnappyCompressorOutputStream(baos, currentIndex, params)) {
116             o.write(buffer, 0, currentIndex);
117         }
118         final byte[] b = baos.toByteArray();
119         writeLittleEndian(3, b.length + 4L /* CRC */);
120         writeCrc();
121         out.write(b);
122         currentIndex = 0;
123     }
124 
125     @Override
126     public void write(final byte[] data, int off, int len) throws IOException {
127         int blockDataRemaining = buffer.length - currentIndex;
128         while (len > 0) {
129             final int copyLen = Math.min(len, blockDataRemaining);
130             System.arraycopy(data, off, buffer, currentIndex, copyLen);
131             off += copyLen;
132             blockDataRemaining -= copyLen;
133             len -= copyLen;
134             currentIndex += copyLen;
135             if (blockDataRemaining == 0) {
136                 flushBuffer();
137                 blockDataRemaining = buffer.length;
138             }
139         }
140     }
141 
142     @Override
143     public void write(final int b) throws IOException {
144         oneByte[0] = (byte) (b & 0xff);
145         write(oneByte);
146     }
147 
148     private void writeCrc() throws IOException {
149         checksum.update(buffer, 0, currentIndex);
150         writeLittleEndian(4, mask(checksum.getValue()));
151         checksum.reset();
152     }
153 
154     private void writeLittleEndian(final int numBytes, final long num) throws IOException {
155         ByteUtils.toLittleEndian(consumer, num, numBytes);
156     }
157 }