View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   https://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.commons.compress.compressors.gzip;
20  
21  import java.io.IOException;
22  import java.io.OutputStream;
23  import java.nio.ByteBuffer;
24  import java.nio.ByteOrder;
25  import java.nio.charset.Charset;
26  import java.util.zip.CRC32;
27  import java.util.zip.Deflater;
28  import java.util.zip.GZIPOutputStream;
29  
30  import org.apache.commons.compress.compressors.CompressorOutputStream;
31  
32  /**
33   * Compressed output stream using the gzip format. This implementation improves over the standard {@link GZIPOutputStream} class by allowing the configuration
34   * of the compression level and the header metadata (file name, comment, modification time, operating system and extra flags).
35   *
36   * @see <a href="https://datatracker.ietf.org/doc/html/rfc1952">RFC 1952 GZIP File Format Specification</a>
37   */
38  public class GzipCompressorOutputStream extends CompressorOutputStream<OutputStream> {
39  
40      /** Deflater used to compress the data */
41      private final Deflater deflater;
42  
43      /** The buffer receiving the compressed data from the deflater */
44      private final byte[] deflateBuffer;
45  
46      /** The checksum of the uncompressed data */
47      private final CRC32 crc = new CRC32();
48  
49      /**
50       * Creates a gzip compressed output stream with the default parameters.
51       *
52       * @param out the stream to compress to
53       * @throws IOException if writing fails
54       */
55      public GzipCompressorOutputStream(final OutputStream out) throws IOException {
56          this(out, new GzipParameters());
57      }
58  
59      /**
60       * Creates a gzip compressed output stream with the specified parameters.
61       *
62       * @param out        the stream to compress to
63       * @param parameters the parameters to use
64       * @throws IOException if writing fails
65       * @since 1.7
66       */
67      public GzipCompressorOutputStream(final OutputStream out, final GzipParameters parameters) throws IOException {
68          super(out);
69          this.deflater = new Deflater(parameters.getCompressionLevel(), true);
70          this.deflater.setStrategy(parameters.getDeflateStrategy());
71          this.deflateBuffer = new byte[parameters.getBufferSize()];
72          writeMemberHeader(parameters);
73      }
74  
75      @Override
76      public void close() throws IOException {
77          if (!isClosed()) {
78              try {
79                  finish();
80              } finally {
81                  deflater.end();
82                  super.close();
83              }
84          }
85      }
86  
87      private void deflate() throws IOException {
88          final int length = deflater.deflate(deflateBuffer, 0, deflateBuffer.length);
89          if (length > 0) {
90              out.write(deflateBuffer, 0, length);
91          }
92      }
93  
94      /**
95       * Finishes writing compressed data to the underlying stream without closing it.
96       *
97       * @throws IOException on error
98       * @since 1.7
99       */
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 }