    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.gzip;
    020
    021import java.io.IOException;
    022import java.io.OutputStream;
    023import java.nio.ByteBuffer;
    024import java.nio.ByteOrder;
    025import java.nio.charset.StandardCharsets;
    026import java.util.zip.CRC32;
    027import java.util.zip.Deflater;
    028import java.util.zip.GZIPInputStream;
    029import java.util.zip.GZIPOutputStream;
    030
    031import org.apache.commons.compress.compressors.CompressorOutputStream;
    032
    033/**
    034 * Compressed output stream using the gzip format. This implementation improves
    035 * over the standard {@link GZIPOutputStream} class by allowing
    036 * the configuration of the compression level and the header metadata (file name,
    037 * comment, modification time, operating system and extra flags).
    038 *
    039 * @see <a href="https://tools.ietf.org/html/rfc1952">GZIP File Format Specification</a>
    040 */
    041public class GzipCompressorOutputStream extends CompressorOutputStream {
    042
    043    /** Header flag indicating a file name follows the header */
    044    private static final int FNAME = 1 << 3;
    045
    046    /** Header flag indicating a comment follows the header */
    047    private static final int FCOMMENT = 1 << 4;
    048
    049    /** The underlying stream */
    050    private final OutputStream out;
    051
    052    /** Deflater used to compress the data */
    053    private final Deflater deflater;
    054
    055    /** The buffer receiving the compressed data from the deflater */
    056    private final byte[] deflateBuffer;
    057
    058    /** Indicates if the stream has been closed */
    059    private boolean closed;
    060
    061    /** The checksum of the uncompressed data */
    062    private final CRC32 crc = new CRC32();
    063
    064    /**
    065     * Creates a gzip compressed output stream with the default parameters.
    066     * @param out the stream to compress to
    067     * @throws IOException if writing fails
    068     */
    069    public GzipCompressorOutputStream(final OutputStream out) throws IOException {
    070        this(out, new GzipParameters());
    071    }
    072
    073    /**
    074     * Creates a gzip compressed output stream with the specified parameters.
    075     * @param out the stream to compress to
    076     * @param parameters the parameters to use
    077     * @throws IOException if writing fails
    078     *
    079     * @since 1.7
    080     */
    081    public GzipCompressorOutputStream(final OutputStream out, final GzipParameters parameters) throws IOException {
    082        this.out = out;
    083        this.deflater = new Deflater(parameters.getCompressionLevel(), true);
    084        this.deflateBuffer = new byte[parameters.getBufferSize()];
    085        writeHeader(parameters);
    086    }
    087
    088    private void writeHeader(final GzipParameters parameters) throws IOException {
    089        final String filename = parameters.getFilename();
    090        final String comment = parameters.getComment();
    091
    092        final ByteBuffer buffer = ByteBuffer.allocate(10);
    093        buffer.order(ByteOrder.LITTLE_ENDIAN);
    094        buffer.putShort((short) GZIPInputStream.GZIP_MAGIC);
    095        buffer.put((byte) Deflater.DEFLATED); // compression method (8: deflate)
    096        buffer.put((byte) ((filename != null ? FNAME : 0) | (comment != null ? FCOMMENT : 0))); // flags
    097        buffer.putInt((int) (parameters.getModificationTime() / 1000));
    098
    099        // extra flags
    100        final int compressionLevel = parameters.getCompressionLevel();
    101        if (compressionLevel == Deflater.BEST_COMPRESSION) {
    102            buffer.put((byte) 2);
    103        } else if (compressionLevel == Deflater.BEST_SPEED) {
    104            buffer.put((byte) 4);
    105        } else {
    106            buffer.put((byte) 0);
    107        }
    108
    109        buffer.put((byte) parameters.getOperatingSystem());
    110
    111        out.write(buffer.array());
    112
    113        if (filename != null) {
    114            out.write(filename.getBytes(StandardCharsets.ISO_8859_1));
    115            out.write(0);
    116        }
    117
    118        if (comment != null) {
    119            out.write(comment.getBytes(StandardCharsets.ISO_8859_1));
    120            out.write(0);
    121        }
    122    }
    123
    124    private void writeTrailer() throws IOException {
    125        final ByteBuffer buffer = ByteBuffer.allocate(8);
    126        buffer.order(ByteOrder.LITTLE_ENDIAN);
    127        buffer.putInt((int) crc.getValue());
    128        buffer.putInt(deflater.getTotalIn());
    129
    130        out.write(buffer.array());
    131    }
    132
    133    @Override
    134    public void write(final int b) throws IOException {
    135        write(new byte[]{(byte) (b & 0xff)}, 0, 1);
    136    }
    137
    138    /**
    139     * {@inheritDoc}
    140     *
    141     * @since 1.1
    142     */
    143    @Override
    144    public void write(final byte[] buffer) throws IOException {
    145        write(buffer, 0, buffer.length);
    146    }
    147
    148    /**
    149     * {@inheritDoc}
    150     *
    151     * @since 1.1
    152     */
    153    @Override
    154    public void write(final byte[] buffer, final int offset, final int length) throws IOException {
    155        if (deflater.finished()) {
    156            throw new IOException("Cannot write more data, the end of the compressed data stream has been reached");
    157
    158        }
    159        if (length > 0) {
    160            deflater.setInput(buffer, offset, length);
    161
    162            while (!deflater.needsInput()) {
    163                deflate();
    164            }
    165
    166            crc.update(buffer, offset, length);
    167        }
    168    }
    169
    170    private void deflate() throws IOException {
    171        final int length = deflater.deflate(deflateBuffer, 0, deflateBuffer.length);
    172        if (length > 0) {
    173            out.write(deflateBuffer, 0, length);
    174        }
    175    }
    176
    177    /**
    178     * Finishes writing compressed data to the underlying stream without closing it.
    179     *
    180     * @since 1.7
    181     * @throws IOException on error
    182     */
    183    public void finish() throws IOException {
    184        if (!deflater.finished()) {
    185            deflater.finish();
    186
    187            while (!deflater.finished()) {
    188                deflate();
    189            }
    190
    191            writeTrailer();
    192        }
    193    }
    194
    195    /**
    196     * {@inheritDoc}
    197     *
    198     * @since 1.7
    199     */
    200    @Override
    201    public void flush() throws IOException {
    202        out.flush();
    203    }
    204
    205    @Override
    206    public void close() throws IOException {
    207        if (!closed) {
    208            try {
    209                finish();
    210            } finally {
    211                deflater.end();
    212                out.close();
    213                closed = true;
    214            }
    215        }
    216    }
    217
    218}