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