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}