    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 *   https://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 */
    019
    020package org.apache.commons.compress.archivers.zip;
    021
    022import java.io.Closeable;
    023import java.io.DataOutput;
    024import java.io.IOException;
    025import java.io.InputStream;
    026import java.io.OutputStream;
    027import java.nio.ByteBuffer;
    028import java.nio.channels.SeekableByteChannel;
    029import java.util.zip.CRC32;
    030import java.util.zip.Deflater;
    031import java.util.zip.ZipEntry;
    032
    033import org.apache.commons.compress.parallel.ScatterGatherBackingStore;
    034
    035/**
    036 * Encapsulates a {@link Deflater} and CRC calculator, handling multiple types of output streams. Currently {@link java.util.zip.ZipEntry#DEFLATED} and
    037 * {@link java.util.zip.ZipEntry#STORED} are the only supported compression methods.
    038 *
    039 * @since 1.10
    040 */
    041public abstract class StreamCompressor implements Closeable {
    042
    043    private static final class DataOutputCompressor extends StreamCompressor {
    044
    045        private final DataOutput raf;
    046
    047        DataOutputCompressor(final Deflater deflater, final DataOutput raf) {
    048            super(deflater);
    049            this.raf = raf;
    050        }
    051
    052        @Override
    053        protected void writeOut(final byte[] data, final int offset, final int length) throws IOException {
    054            raf.write(data, offset, length);
    055        }
    056    }
    057
    058    private static final class OutputStreamCompressor extends StreamCompressor {
    059
    060        private final OutputStream os;
    061
    062        OutputStreamCompressor(final Deflater deflater, final OutputStream os) {
    063            super(deflater);
    064            this.os = os;
    065        }
    066
    067        @Override
    068        protected void writeOut(final byte[] data, final int offset, final int length) throws IOException {
    069            os.write(data, offset, length);
    070        }
    071    }
    072
    073    private static final class ScatterGatherBackingStoreCompressor extends StreamCompressor {
    074
    075        private final ScatterGatherBackingStore bs;
    076
    077        ScatterGatherBackingStoreCompressor(final Deflater deflater, final ScatterGatherBackingStore bs) {
    078            super(deflater);
    079            this.bs = bs;
    080        }
    081
    082        @Override
    083        protected void writeOut(final byte[] data, final int offset, final int length) throws IOException {
    084            bs.writeOut(data, offset, length);
    085        }
    086    }
    087
    088    private static final class SeekableByteChannelCompressor extends StreamCompressor {
    089
    090        private final SeekableByteChannel channel;
    091
    092        SeekableByteChannelCompressor(final Deflater deflater, final SeekableByteChannel channel) {
    093            super(deflater);
    094            this.channel = channel;
    095        }
    096
    097        @Override
    098        protected void writeOut(final byte[] data, final int offset, final int length) throws IOException {
    099            channel.write(ByteBuffer.wrap(data, offset, length));
    100        }
    101    }
    102
    103    /**
    104     * Apparently Deflater.setInput gets slowed down a lot on Sun JVMs when it gets handed a huge buffer. See
    105     * https://issues.apache.org/bugzilla/show_bug.cgi?id=45396
    106     *
    107     * Using a buffer size of {@value} bytes proved to be a good compromise
    108     */
    109    private static final int DEFLATER_BLOCK_SIZE = 8192;
    110    private static final int BUFFER_SIZE = 4096;
    111
    112    /**
    113     * Creates a stream compressor with the given compression level.
    114     *
    115     * @param os       The DataOutput to receive output
    116     * @param deflater The deflater to use for the compressor
    117     * @return A stream compressor
    118     */
    119    static StreamCompressor create(final DataOutput os, final Deflater deflater) {
    120        return new DataOutputCompressor(deflater, os);
    121    }
    122
    123    /**
    124     * Creates a stream compressor with the given compression level.
    125     *
    126     * @param compressionLevel The {@link Deflater} compression level
    127     * @param bs               The ScatterGatherBackingStore to receive output
    128     * @return A stream compressor
    129     */
    130    public static StreamCompressor create(final int compressionLevel, final ScatterGatherBackingStore bs) {
    131        final Deflater deflater = new Deflater(compressionLevel, true);
    132        return new ScatterGatherBackingStoreCompressor(deflater, bs);
    133    }
    134
    135    /**
    136     * Creates a stream compressor with the default compression level.
    137     *
    138     * @param os The stream to receive output
    139     * @return A stream compressor
    140     */
    141    static StreamCompressor create(final OutputStream os) {
    142        return create(os, new Deflater(Deflater.DEFAULT_COMPRESSION, true));
    143    }
    144
    145    /**
    146     * Creates a stream compressor with the given compression level.
    147     *
    148     * @param os       The stream to receive output
    149     * @param deflater The deflater to use
    150     * @return A stream compressor
    151     */
    152    static StreamCompressor create(final OutputStream os, final Deflater deflater) {
    153        return new OutputStreamCompressor(deflater, os);
    154    }
    155
    156    /**
    157     * Creates a stream compressor with the default compression level.
    158     *
    159     * @param bs The ScatterGatherBackingStore to receive output
    160     * @return A stream compressor
    161     */
    162    public static StreamCompressor create(final ScatterGatherBackingStore bs) {
    163        return create(Deflater.DEFAULT_COMPRESSION, bs);
    164    }
    165
    166    /**
    167     * Creates a stream compressor with the given compression level.
    168     *
    169     * @param os       The SeekableByteChannel to receive output
    170     * @param deflater The deflater to use for the compressor
    171     * @return A stream compressor
    172     * @since 1.13
    173     */
    174    static StreamCompressor create(final SeekableByteChannel os, final Deflater deflater) {
    175        return new SeekableByteChannelCompressor(deflater, os);
    176    }
    177
    178    private final Deflater deflater;
    179    private final CRC32 crc = new CRC32();
    180    private long writtenToOutputStreamForLastEntry;
    181    private long sourcePayloadLength;
    182    private long totalWrittenToOutputStream;
    183    private final byte[] outputBuffer = new byte[BUFFER_SIZE];
    184    private final byte[] readerBuf = new byte[BUFFER_SIZE];
    185
    186    StreamCompressor(final Deflater deflater) {
    187        this.deflater = deflater;
    188    }
    189
    190    @Override
    191    public void close() throws IOException {
    192        deflater.end();
    193    }
    194
    195    void deflate() throws IOException {
    196        final int len = deflater.deflate(outputBuffer, 0, outputBuffer.length);
    197        if (len > 0) {
    198            writeCounted(outputBuffer, 0, len);
    199        }
    200    }
    201
    202    /**
    203     * Deflates the given source using the supplied compression method
    204     *
    205     * @param source The source to compress
    206     * @param method The #ZipArchiveEntry compression method
    207     * @throws IOException When failures happen
    208     */
    209    public void deflate(final InputStream source, final int method) throws IOException {
    210        reset();
    211        int length;
    212        while ((length = source.read(readerBuf, 0, readerBuf.length)) >= 0) {
    213            write(readerBuf, 0, length, method);
    214        }
    215        if (method == ZipEntry.DEFLATED) {
    216            flushDeflater();
    217        }
    218    }
    219
    220    private void deflateUntilInputIsNeeded() throws IOException {
    221        while (!deflater.needsInput()) {
    222            deflate();
    223        }
    224    }
    225
    226    void flushDeflater() throws IOException {
    227        deflater.finish();
    228        while (!deflater.finished()) {
    229            deflate();
    230        }
    231    }
    232
    233    /**
    234     * Gets the number of bytes read from the source stream
    235     *
    236     * @return The number of bytes read, never negative
    237     */
    238    public long getBytesRead() {
    239        return sourcePayloadLength;
    240    }
    241
    242    /**
    243     * Gets the number of bytes written to the output for the last entry
    244     *
    245     * @return The number of bytes, never negative
    246     */
    247    public long getBytesWrittenForLastEntry() {
    248        return writtenToOutputStreamForLastEntry;
    249    }
    250
    251    /**
    252     * Gets the CRC-32 of the last deflated file
    253     *
    254     * @return the CRC-32
    255     */
    256    public long getCrc32() {
    257        return crc.getValue();
    258    }
    259
    260    /**
    261     * Gets the total number of bytes written to the output for all files
    262     *
    263     * @return The number of bytes, never negative
    264     */
    265    public long getTotalBytesWritten() {
    266        return totalWrittenToOutputStream;
    267    }
    268
    269    void reset() {
    270        crc.reset();
    271        deflater.reset();
    272        sourcePayloadLength = 0;
    273        writtenToOutputStreamForLastEntry = 0;
    274    }
    275
    276    /**
    277     * Writes bytes to ZIP entry.
    278     *
    279     * @param b      the byte array to write
    280     * @param offset the start position to write from
    281     * @param length the number of bytes to write
    282     * @param method the comrpession method to use
    283     * @return the number of bytes written to the stream this time
    284     * @throws IOException on error
    285     */
    286    long write(final byte[] b, final int offset, final int length, final int method) throws IOException {
    287        final long current = writtenToOutputStreamForLastEntry;
    288        crc.update(b, offset, length);
    289        if (method == ZipEntry.DEFLATED) {
    290            writeDeflated(b, offset, length);
    291        } else {
    292            writeCounted(b, offset, length);
    293        }
    294        sourcePayloadLength += length;
    295        return writtenToOutputStreamForLastEntry - current;
    296    }
    297
    298    /**
    299     * Writes the specified byte array to the output stream.
    300     *
    301     * @param data   the data.
    302     * @exception IOException if an I/O error occurs.
    303     */
    304    public void writeCounted(final byte[] data) throws IOException {
    305        writeCounted(data, 0, data.length);
    306    }
    307
    308    /**
    309     * Writes {@code len} bytes from the specified byte array starting at offset {@code off} to the output stream.
    310     *
    311     * @param data   the data.
    312     * @param offset the start offset in the data.
    313     * @param length the number of bytes to write.
    314     * @exception IOException if an I/O error occurs.
    315     */
    316    public void writeCounted(final byte[] data, final int offset, final int length) throws IOException {
    317        writeOut(data, offset, length);
    318        writtenToOutputStreamForLastEntry += length;
    319        totalWrittenToOutputStream += length;
    320    }
    321
    322    private void writeDeflated(final byte[] b, final int offset, final int length) throws IOException {
    323        if (length > 0 && !deflater.finished()) {
    324            if (length <= DEFLATER_BLOCK_SIZE) {
    325                deflater.setInput(b, offset, length);
    326                deflateUntilInputIsNeeded();
    327            } else {
    328                final int fullblocks = length / DEFLATER_BLOCK_SIZE;
    329                for (int i = 0; i < fullblocks; i++) {
    330                    deflater.setInput(b, offset + i * DEFLATER_BLOCK_SIZE, DEFLATER_BLOCK_SIZE);
    331                    deflateUntilInputIsNeeded();
    332                }
    333                final int done = fullblocks * DEFLATER_BLOCK_SIZE;
    334                if (done < length) {
    335                    deflater.setInput(b, offset + done, length - done);
    336                    deflateUntilInputIsNeeded();
    337                }
    338            }
    339        }
    340    }
    341
    342    /**
    343     * Writes {@code len} bytes from the specified byte array starting at offset {@code off} to the output stream.
    344     *
    345     * @param data   the data.
    346     * @param offset the start offset in the data.
    347     * @param length the number of bytes to write.
    348     * @exception IOException if an I/O error occurs.
    349     */
    350    protected abstract void writeOut(byte[] data, int offset, int length) throws IOException;
    351}