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}