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