001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.commons.compress.compressors.snappy;
020
021import java.io.IOException;
022import java.io.OutputStream;
023
024import org.apache.commons.compress.compressors.CompressorOutputStream;
025import org.apache.commons.compress.compressors.lz77support.LZ77Compressor;
026import org.apache.commons.compress.compressors.lz77support.Parameters;
027import org.apache.commons.compress.utils.ByteUtils;
028
029/**
030 * CompressorOutputStream for the raw Snappy format.
031 *
032 * <p>This implementation uses an internal buffer in order to handle
033 * the back-references that are at the heart of the LZ77 algorithm.
034 * The size of the buffer must be at least as big as the biggest
035 * offset used in the compressed stream.  The current version of the
036 * Snappy algorithm as defined by Google works on 32k blocks and
037 * doesn't contain offsets bigger than 32k which is the default block
038 * size used by this class.</p>
039 *
040 * <p>The raw Snappy format requires the uncompressed size to be
041 * written at the beginning of the stream using a varint
042 * representation, i.e. the number of bytes needed to write the
043 * information is not known before the uncompressed size is
044 * known. We've chosen to make the uncompressedSize a parameter of the
045 * constructor in favor of buffering the whole output until the size
046 * is known. When using the {@link FramedSnappyCompressorOutputStream}
047 * this limitation is taken care of by the warpping framing
048 * format.</p>
049 *
050 * @see <a href="https://github.com/google/snappy/blob/master/format_description.txt">Snappy compressed format description</a>
051 * @since 1.14
052 * @NotThreadSafe
053 */
054public class SnappyCompressorOutputStream extends CompressorOutputStream {
055    // literal length is stored as (len - 1) either inside the tag
056    // (six bits minus four flags) or in 1 to 4 bytes after the tag
057    private static final int MAX_LITERAL_SIZE_WITHOUT_SIZE_BYTES = 60;
058    private static final int MAX_LITERAL_SIZE_WITH_ONE_SIZE_BYTE = 1 << 8;
059    private static final int MAX_LITERAL_SIZE_WITH_TWO_SIZE_BYTES = 1 << 16;
060
061    private static final int MAX_LITERAL_SIZE_WITH_THREE_SIZE_BYTES = 1 << 24;
062
063    private static final int ONE_SIZE_BYTE_MARKER = 60 << 2;
064
065    private static final int TWO_SIZE_BYTE_MARKER = 61 << 2;
066
067    private static final int THREE_SIZE_BYTE_MARKER = 62 << 2;
068
069    private static final int FOUR_SIZE_BYTE_MARKER = 63 << 2;
070
071    // Back-references ("copies") have their offset/size information
072    // in two, three or five bytes.
073    private static final int MIN_MATCH_LENGTH_WITH_ONE_OFFSET_BYTE = 4;
074
075    private static final int MAX_MATCH_LENGTH_WITH_ONE_OFFSET_BYTE = 11;
076
077    private static final int MAX_OFFSET_WITH_ONE_OFFSET_BYTE = 1 << 11 - 1;
078
079    private static final int MAX_OFFSET_WITH_TWO_OFFSET_BYTES = 1 << 16 - 1;
080
081    private static final int ONE_BYTE_COPY_TAG = 1;
082
083    private static final int TWO_BYTE_COPY_TAG = 2;
084    private static final int FOUR_BYTE_COPY_TAG = 3;
085    // technically the format could use shorter matches but with a
086    // length of three the offset would be encoded as at least two
087    // bytes in addition to the tag, so yield no compression at all
088    private static final int MIN_MATCH_LENGTH = 4;
089    // Snappy stores the match length in six bits of the tag
090    private static final int MAX_MATCH_LENGTH = 64;
091
092    /**
093     * Returns a builder correctly configured for the Snappy algorithm using the gven block size.
094     * @param blockSize the block size.
095     * @return a builder correctly configured for the Snappy algorithm using the gven block size
096     */
097    public static Parameters.Builder createParameterBuilder(final int blockSize) {
098        // the max offset and max literal length defined by the format
099        // are 2^32 - 1 and 2^32 respectively - with blockSize being
100        // an integer we will never exceed that
101        return Parameters.builder(blockSize)
102            .withMinBackReferenceLength(MIN_MATCH_LENGTH)
103            .withMaxBackReferenceLength(MAX_MATCH_LENGTH)
104            .withMaxOffset(blockSize)
105            .withMaxLiteralLength(blockSize);
106    }
107    private final LZ77Compressor compressor;
108    private final OutputStream os;
109    private final ByteUtils.ByteConsumer consumer;
110
111    // used in one-arg write method
112    private final byte[] oneByte = new byte[1];
113
114    private boolean finished;
115
116    /**
117     * Constructor using the default block size of 32k.
118     *
119     * @param os the outputstream to write compressed data to
120     * @param uncompressedSize the uncompressed size of data
121     * @throws IOException if writing of the size fails
122     */
123    public SnappyCompressorOutputStream(final OutputStream os, final long uncompressedSize) throws IOException {
124        this(os, uncompressedSize, SnappyCompressorInputStream.DEFAULT_BLOCK_SIZE);
125    }
126
127    /**
128     * Constructor using a configurable block size.
129     *
130     * @param os the outputstream to write compressed data to
131     * @param uncompressedSize the uncompressed size of data
132     * @param blockSize the block size used - must be a power of two
133     * @throws IOException if writing of the size fails
134     */
135    public SnappyCompressorOutputStream(final OutputStream os, final long uncompressedSize, final int blockSize)
136        throws IOException {
137        this(os, uncompressedSize, createParameterBuilder(blockSize).build());
138    }
139
140    /**
141     * Constructor providing full control over the underlying LZ77 compressor.
142     *
143     * @param os the outputstream to write compressed data to
144     * @param uncompressedSize the uncompressed size of data
145     * @param params the parameters to use by the compressor - note
146     * that the format itself imposes some limits like a maximum match
147     * length of 64 bytes
148     * @throws IOException if writing of the size fails
149     */
150    public SnappyCompressorOutputStream(final OutputStream os, final long uncompressedSize, final Parameters params)
151        throws IOException {
152        this.os = os;
153        consumer = new ByteUtils.OutputStreamByteConsumer(os);
154        compressor = new LZ77Compressor(params, block -> {
155            switch (block.getType()) {
156            case LITERAL:
157                writeLiteralBlock((LZ77Compressor.LiteralBlock) block);
158                break;
159            case BACK_REFERENCE:
160                writeBackReference((LZ77Compressor.BackReference) block);
161                break;
162            case EOD:
163                break;
164            }
165        });
166        writeUncompressedSize(uncompressedSize);
167    }
168
169    @Override
170    public void close() throws IOException {
171        try {
172            finish();
173        } finally {
174            os.close();
175        }
176    }
177
178    /**
179     * Compresses all remaining data and writes it to the stream,
180     * doesn't close the underlying stream.
181     * @throws IOException if an error occurs
182     */
183    public void finish() throws IOException {
184        if (!finished) {
185            compressor.finish();
186            finished = true;
187        }
188    }
189
190    @Override
191    public void write(final byte[] data, final int off, final int len) throws IOException {
192        compressor.compress(data, off, len);
193    }
194
195    @Override
196    public void write(final int b) throws IOException {
197        oneByte[0] = (byte) (b & 0xff);
198        write(oneByte);
199    }
200    private void writeBackReference(final LZ77Compressor.BackReference block) throws IOException {
201        final int len = block.getLength();
202        final int offset = block.getOffset();
203        if (len >= MIN_MATCH_LENGTH_WITH_ONE_OFFSET_BYTE && len <= MAX_MATCH_LENGTH_WITH_ONE_OFFSET_BYTE
204            && offset <= MAX_OFFSET_WITH_ONE_OFFSET_BYTE) {
205            writeBackReferenceWithOneOffsetByte(len, offset);
206        } else if (offset < MAX_OFFSET_WITH_TWO_OFFSET_BYTES) {
207            writeBackReferenceWithTwoOffsetBytes(len, offset);
208        } else {
209            writeBackReferenceWithFourOffsetBytes(len, offset);
210        }
211    }
212    private void writeBackReferenceWithFourOffsetBytes(final int len, final int offset) throws IOException {
213        writeBackReferenceWithLittleEndianOffset(FOUR_BYTE_COPY_TAG, 4, len, offset);
214    }
215    private void writeBackReferenceWithLittleEndianOffset(final int tag, final int offsetBytes, final int len, final int offset)
216        throws IOException {
217        os.write(tag | ((len - 1) << 2));
218        writeLittleEndian(offsetBytes, offset);
219    }
220
221    private void writeBackReferenceWithOneOffsetByte(final int len, final int offset) throws IOException {
222        os.write(ONE_BYTE_COPY_TAG | ((len - 4) << 2) | ((offset & 0x700) >> 3));
223        os.write(offset & 0xff);
224    }
225    private void writeBackReferenceWithTwoOffsetBytes(final int len, final int offset) throws IOException {
226        writeBackReferenceWithLittleEndianOffset(TWO_BYTE_COPY_TAG, 2, len, offset);
227    }
228    private void writeLiteralBlock(final LZ77Compressor.LiteralBlock block) throws IOException {
229        final int len = block.getLength();
230        if (len <= MAX_LITERAL_SIZE_WITHOUT_SIZE_BYTES) {
231            writeLiteralBlockNoSizeBytes(block, len);
232        } else if (len <= MAX_LITERAL_SIZE_WITH_ONE_SIZE_BYTE) {
233            writeLiteralBlockOneSizeByte(block, len);
234        } else if (len <= MAX_LITERAL_SIZE_WITH_TWO_SIZE_BYTES) {
235            writeLiteralBlockTwoSizeBytes(block, len);
236        } else if (len <= MAX_LITERAL_SIZE_WITH_THREE_SIZE_BYTES) {
237            writeLiteralBlockThreeSizeBytes(block, len);
238        } else {
239            writeLiteralBlockFourSizeBytes(block, len);
240        }
241    }
242
243    private void writeLiteralBlockFourSizeBytes(final LZ77Compressor.LiteralBlock block, final int len) throws IOException {
244        writeLiteralBlockWithSize(FOUR_SIZE_BYTE_MARKER, 4, len, block);
245    }
246
247    private void writeLiteralBlockNoSizeBytes(final LZ77Compressor.LiteralBlock block, final int len) throws IOException {
248        writeLiteralBlockWithSize(len - 1 << 2, 0, len, block);
249    }
250
251    private void writeLiteralBlockOneSizeByte(final LZ77Compressor.LiteralBlock block, final int len) throws IOException {
252        writeLiteralBlockWithSize(ONE_SIZE_BYTE_MARKER, 1, len, block);
253    }
254
255    private void writeLiteralBlockThreeSizeBytes(final LZ77Compressor.LiteralBlock block, final int len) throws IOException {
256        writeLiteralBlockWithSize(THREE_SIZE_BYTE_MARKER, 3, len, block);
257    }
258
259    private void writeLiteralBlockTwoSizeBytes(final LZ77Compressor.LiteralBlock block, final int len) throws IOException {
260        writeLiteralBlockWithSize(TWO_SIZE_BYTE_MARKER, 2, len, block);
261    }
262
263    private void writeLiteralBlockWithSize(final int tagByte, final int sizeBytes, final int len, final LZ77Compressor.LiteralBlock block)
264        throws IOException {
265        os.write(tagByte);
266        writeLittleEndian(sizeBytes, len - 1);
267        os.write(block.getData(), block.getOffset(), len);
268    }
269    private void writeLittleEndian(final int numBytes, final int num) throws IOException {
270        ByteUtils.toLittleEndian(consumer, num, numBytes);
271    }
272
273    private void writeUncompressedSize(long uncompressedSize) throws IOException {
274        boolean more;
275        do {
276            int currentByte = (int) (uncompressedSize & 0x7F);
277            more = uncompressedSize > currentByte;
278            if (more) {
279                currentByte |= 0x80;
280            }
281            os.write(currentByte);
282            uncompressedSize >>= 7;
283        } while (more);
284    }
285}