    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 */
    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.Charset;
    026import java.util.zip.CRC32;
    027import java.util.zip.Deflater;
    028import java.util.zip.GZIPOutputStream;
    029
    030import org.apache.commons.compress.compressors.CompressorOutputStream;
    031
    032/**
    033 * Compressed output stream using the gzip format. This implementation improves over the standard {@link GZIPOutputStream} class by allowing the configuration
    034 * of the compression level and the header metadata (file name, comment, modification time, operating system and extra flags).
    035 *
    036 * @see <a href="https://datatracker.ietf.org/doc/html/rfc1952">RFC 1952 GZIP File Format Specification</a>
    037 */
    038public class GzipCompressorOutputStream extends CompressorOutputStream<OutputStream> {
    039
    040    /** Deflater used to compress the data */
    041    private final Deflater deflater;
    042
    043    /** The buffer receiving the compressed data from the deflater */
    044    private final byte[] deflateBuffer;
    045
    046    /** The checksum of the uncompressed data */
    047    private final CRC32 crc = new CRC32();
    048
    049    /**
    050     * Creates a gzip compressed output stream with the default parameters.
    051     *
    052     * @param out the stream to compress to
    053     * @throws IOException if writing fails
    054     */
    055    public GzipCompressorOutputStream(final OutputStream out) throws IOException {
    056        this(out, new GzipParameters());
    057    }
    058
    059    /**
    060     * Creates a gzip compressed output stream with the specified parameters.
    061     *
    062     * @param out        the stream to compress to
    063     * @param parameters the parameters to use
    064     * @throws IOException if writing fails
    065     * @since 1.7
    066     */
    067    public GzipCompressorOutputStream(final OutputStream out, final GzipParameters parameters) throws IOException {
    068        super(out);
    069        this.deflater = new Deflater(parameters.getCompressionLevel(), true);
    070        this.deflater.setStrategy(parameters.getDeflateStrategy());
    071        this.deflateBuffer = new byte[parameters.getBufferSize()];
    072        writeMemberHeader(parameters);
    073    }
    074
    075    @Override
    076    public void close() throws IOException {
    077        if (!isClosed()) {
    078            try {
    079                finish();
    080            } finally {
    081                deflater.end();
    082                super.close();
    083            }
    084        }
    085    }
    086
    087    private void deflate() throws IOException {
    088        final int length = deflater.deflate(deflateBuffer, 0, deflateBuffer.length);
    089        if (length > 0) {
    090            out.write(deflateBuffer, 0, length);
    091        }
    092    }
    093
    094    /**
    095     * Finishes writing compressed data to the underlying stream without closing it.
    096     *
    097     * @throws IOException on error
    098     * @since 1.7
    099     */
    100    @Override
    101    public void finish() throws IOException {
    102        if (!deflater.finished()) {
    103            deflater.finish();
    104            while (!deflater.finished()) {
    105                deflate();
    106            }
    107            writeMemberTrailer();
    108            deflater.reset();
    109        }
    110    }
    111
    112    /**
    113     * {@inheritDoc}
    114     *
    115     * @since 1.1
    116     */
    117    @Override
    118    public void write(final byte[] buffer) throws IOException {
    119        write(buffer, 0, buffer.length);
    120    }
    121
    122    /**
    123     * {@inheritDoc}
    124     *
    125     * @since 1.1
    126     */
    127    @Override
    128    public void write(final byte[] buffer, final int offset, final int length) throws IOException {
    129        checkOpen();
    130        if (deflater.finished()) {
    131            throw new IOException("Cannot write more data, the end of the compressed data stream has been reached.");
    132        }
    133        if (length > 0) {
    134            deflater.setInput(buffer, offset, length);
    135            while (!deflater.needsInput()) {
    136                deflate();
    137            }
    138            crc.update(buffer, offset, length);
    139        }
    140    }
    141
    142    @Override
    143    public void write(final int b) throws IOException {
    144        write(new byte[] { (byte) (b & 0xff) }, 0, 1);
    145    }
    146
    147    /**
    148     * Writes a C-style string, a NUL-terminated string, encoded with the {@code charset}.
    149     *
    150     * @param value The String to write.
    151     * @param charset Specifies the Charset to use.
    152     * @throws IOException if an I/O error occurs.
    153     */
    154    private void writeC(final String value, final Charset charset) throws IOException {
    155        if (value != null) {
    156            final byte[] ba = value.getBytes(charset);
    157            out.write(ba);
    158            out.write(0);
    159            crc.update(ba);
    160            crc.update(0);
    161        }
    162    }
    163
    164    private void writeMemberHeader(final GzipParameters parameters) throws IOException {
    165        final String fileName = parameters.getFileName();
    166        final String comment = parameters.getComment();
    167        final byte[] extra = parameters.getExtraField() != null ? parameters.getExtraField().toByteArray() : null;
    168        final ByteBuffer buffer = ByteBuffer.allocate(10);
    169        buffer.order(ByteOrder.LITTLE_ENDIAN);
    170        buffer.put((byte) GzipUtils.ID1);
    171        buffer.put((byte) GzipUtils.ID2);
    172        buffer.put((byte) Deflater.DEFLATED); // compression method (8: deflate)
    173        buffer.put((byte) ((extra != null ? GzipUtils.FEXTRA : 0)
    174                | (fileName != null ? GzipUtils.FNAME : 0)
    175                | (comment != null ? GzipUtils.FCOMMENT : 0)
    176                | (parameters.getHeaderCRC() ? GzipUtils.FHCRC : 0)
    177        )); // flags
    178        buffer.putInt((int) parameters.getModificationInstant().getEpochSecond());
    179        // extra flags
    180        final int compressionLevel = parameters.getCompressionLevel();
    181        if (compressionLevel == Deflater.BEST_COMPRESSION) {
    182            buffer.put(GzipUtils.XFL_MAX_COMPRESSION);
    183        } else if (compressionLevel == Deflater.BEST_SPEED) {
    184            buffer.put(GzipUtils.XFL_MAX_SPEED);
    185        } else {
    186            buffer.put(GzipUtils.XFL_UNKNOWN);
    187        }
    188        buffer.put((byte) parameters.getOperatingSystem());
    189        out.write(buffer.array());
    190        crc.update(buffer.array());
    191        if (extra != null) {
    192            out.write(extra.length & 0xff); // little endian
    193            out.write(extra.length >>> 8 & 0xff);
    194            out.write(extra);
    195            crc.update(extra.length & 0xff);
    196            crc.update(extra.length >>> 8 & 0xff);
    197            crc.update(extra);
    198        }
    199        writeC(fileName, parameters.getFileNameCharset());
    200        writeC(comment, parameters.getFileNameCharset());
    201        if (parameters.getHeaderCRC()) {
    202            final int v = (int) crc.getValue() & 0xffff;
    203            out.write(v & 0xff);
    204            out.write(v >>> 8 & 0xff);
    205        }
    206        crc.reset();
    207    }
    208
    209    /**
    210     * Writes the member trailer.
    211     * <pre>
    212     *      0   1   2   3   4   5   6   7
    213     *   +---+---+---+---+---+---+---+---+
    214     *   |     CRC32     |     ISIZE     |
    215     *   +---+---+---+---+---+---+---+---+
    216     * </pre>
    217     *
    218     * @throws IOException if an I/O error occurs.
    219     */
    220    private void writeMemberTrailer() throws IOException {
    221        final ByteBuffer buffer = ByteBuffer.allocate(8);
    222        buffer.order(ByteOrder.LITTLE_ENDIAN);
    223        buffer.putInt((int) crc.getValue());
    224        buffer.putInt(deflater.getTotalIn());
    225        out.write(buffer.array());
    226    }
    227
    228}