001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.compress.archivers.zip; 018 019import java.io.Closeable; 020import java.io.DataOutput; 021import java.io.IOException; 022import java.io.InputStream; 023import java.io.OutputStream; 024import java.nio.ByteBuffer; 025import java.nio.channels.SeekableByteChannel; 026import java.util.zip.CRC32; 027import java.util.zip.Deflater; 028import java.util.zip.ZipEntry; 029 030import org.apache.commons.compress.parallel.ScatterGatherBackingStore; 031 032/** 033 * Encapsulates a {@link Deflater} and crc calculator, handling multiple types of output streams. Currently {@link java.util.zip.ZipEntry#DEFLATED} and 034 * {@link java.util.zip.ZipEntry#STORED} are the only supported compression methods. 035 * 036 * @since 1.10 037 */ 038public abstract class StreamCompressor implements Closeable { 039 040 private static final class DataOutputCompressor extends StreamCompressor { 041 private final DataOutput raf; 042 043 DataOutputCompressor(final Deflater deflater, final DataOutput raf) { 044 super(deflater); 045 this.raf = raf; 046 } 047 048 @Override 049 protected void writeOut(final byte[] data, final int offset, final int length) throws IOException { 050 raf.write(data, offset, length); 051 } 052 } 053 054 private static final class OutputStreamCompressor extends StreamCompressor { 055 private final OutputStream os; 056 057 OutputStreamCompressor(final Deflater deflater, final OutputStream os) { 058 super(deflater); 059 this.os = os; 060 } 061 062 @Override 063 protected void writeOut(final byte[] data, final int offset, final int length) throws IOException { 064 os.write(data, offset, length); 065 } 066 } 067 068 private static final class ScatterGatherBackingStoreCompressor extends StreamCompressor { 069 private final ScatterGatherBackingStore bs; 070 071 ScatterGatherBackingStoreCompressor(final Deflater deflater, final ScatterGatherBackingStore bs) { 072 super(deflater); 073 this.bs = bs; 074 } 075 076 @Override 077 protected void writeOut(final byte[] data, final int offset, final int length) throws IOException { 078 bs.writeOut(data, offset, length); 079 } 080 } 081 082 private static final class SeekableByteChannelCompressor extends StreamCompressor { 083 private final SeekableByteChannel channel; 084 085 SeekableByteChannelCompressor(final Deflater deflater, final SeekableByteChannel channel) { 086 super(deflater); 087 this.channel = channel; 088 } 089 090 @Override 091 protected void writeOut(final byte[] data, final int offset, final int length) throws IOException { 092 channel.write(ByteBuffer.wrap(data, offset, length)); 093 } 094 } 095 096 /** 097 * Apparently Deflater.setInput gets slowed down a lot on Sun JVMs when it gets handed a huge buffer. See 098 * https://issues.apache.org/bugzilla/show_bug.cgi?id=45396 099 * 100 * Using a buffer size of {@value} bytes proved to be a good compromise 101 */ 102 private static final int DEFLATER_BLOCK_SIZE = 8192; 103 private static final int BUFFER_SIZE = 4096; 104 105 /** 106 * Creates a stream compressor with the given compression level. 107 * 108 * @param os The DataOutput to receive output 109 * @param deflater The deflater to use for the compressor 110 * @return A stream compressor 111 */ 112 static StreamCompressor create(final DataOutput os, final Deflater deflater) { 113 return new DataOutputCompressor(deflater, os); 114 } 115 116 /** 117 * Creates a stream compressor with the given compression level. 118 * 119 * @param compressionLevel The {@link Deflater} compression level 120 * @param bs The ScatterGatherBackingStore to receive output 121 * @return A stream compressor 122 */ 123 public static StreamCompressor create(final int compressionLevel, final ScatterGatherBackingStore bs) { 124 final Deflater deflater = new Deflater(compressionLevel, true); 125 return new ScatterGatherBackingStoreCompressor(deflater, bs); 126 } 127 128 /** 129 * Creates a stream compressor with the default compression level. 130 * 131 * @param os The stream to receive output 132 * @return A stream compressor 133 */ 134 static StreamCompressor create(final OutputStream os) { 135 return create(os, new Deflater(Deflater.DEFAULT_COMPRESSION, true)); 136 } 137 138 /** 139 * Creates a stream compressor with the given compression level. 140 * 141 * @param os The stream to receive output 142 * @param deflater The deflater to use 143 * @return A stream compressor 144 */ 145 static StreamCompressor create(final OutputStream os, final Deflater deflater) { 146 return new OutputStreamCompressor(deflater, os); 147 } 148 149 /** 150 * Creates a stream compressor with the default compression level. 151 * 152 * @param bs The ScatterGatherBackingStore to receive output 153 * @return A stream compressor 154 */ 155 public static StreamCompressor create(final ScatterGatherBackingStore bs) { 156 return create(Deflater.DEFAULT_COMPRESSION, bs); 157 } 158 159 /** 160 * Creates a stream compressor with the given compression level. 161 * 162 * @param os The SeekableByteChannel to receive output 163 * @param deflater The deflater to use for the compressor 164 * @return A stream compressor 165 * @since 1.13 166 */ 167 static StreamCompressor create(final SeekableByteChannel os, final Deflater deflater) { 168 return new SeekableByteChannelCompressor(deflater, os); 169 } 170 171 private final Deflater deflater; 172 173 private final CRC32 crc = new CRC32(); 174 175 private long writtenToOutputStreamForLastEntry; 176 177 private long sourcePayloadLength; 178 179 private long totalWrittenToOutputStream; 180 181 private final byte[] outputBuffer = new byte[BUFFER_SIZE]; 182 183 private final byte[] readerBuf = new byte[BUFFER_SIZE]; 184 185 StreamCompressor(final Deflater deflater) { 186 this.deflater = deflater; 187 } 188 189 @Override 190 public void close() throws IOException { 191 deflater.end(); 192 } 193 194 void deflate() throws IOException { 195 final int len = deflater.deflate(outputBuffer, 0, outputBuffer.length); 196 if (len > 0) { 197 writeCounted(outputBuffer, 0, len); 198 } 199 } 200 201 /** 202 * Deflates the given source using the supplied compression method 203 * 204 * @param source The source to compress 205 * @param method The #ZipArchiveEntry compression method 206 * @throws IOException When failures happen 207 */ 208 209 public void deflate(final InputStream source, final int method) throws IOException { 210 reset(); 211 int length; 212 213 while ((length = source.read(readerBuf, 0, readerBuf.length)) >= 0) { 214 write(readerBuf, 0, length, method); 215 } 216 if (method == ZipEntry.DEFLATED) { 217 flushDeflater(); 218 } 219 } 220 221 private void deflateUntilInputIsNeeded() throws IOException { 222 while (!deflater.needsInput()) { 223 deflate(); 224 } 225 } 226 227 void flushDeflater() throws IOException { 228 deflater.finish(); 229 while (!deflater.finished()) { 230 deflate(); 231 } 232 } 233 234 /** 235 * Gets the number of bytes read from the source stream 236 * 237 * @return The number of bytes read, never negative 238 */ 239 public long getBytesRead() { 240 return sourcePayloadLength; 241 } 242 243 /** 244 * Gets the number of bytes written to the output for the last entry 245 * 246 * @return The number of bytes, never negative 247 */ 248 public long getBytesWrittenForLastEntry() { 249 return writtenToOutputStreamForLastEntry; 250 } 251 252 /** 253 * Gets the crc32 of the last deflated file 254 * 255 * @return the crc32 256 */ 257 258 public long getCrc32() { 259 return crc.getValue(); 260 } 261 262 /** 263 * Gets the total number of bytes written to the output for all files 264 * 265 * @return The number of bytes, never negative 266 */ 267 public long getTotalBytesWritten() { 268 return totalWrittenToOutputStream; 269 } 270 271 void reset() { 272 crc.reset(); 273 deflater.reset(); 274 sourcePayloadLength = 0; 275 writtenToOutputStreamForLastEntry = 0; 276 } 277 278 /** 279 * Writes bytes to ZIP entry. 280 * 281 * @param b the byte array to write 282 * @param offset the start position to write from 283 * @param length the number of bytes to write 284 * @param method the comrpession method to use 285 * @return the number of bytes written to the stream this time 286 * @throws IOException on error 287 */ 288 long write(final byte[] b, final int offset, final int length, final int method) throws IOException { 289 final long current = writtenToOutputStreamForLastEntry; 290 crc.update(b, offset, length); 291 if (method == ZipEntry.DEFLATED) { 292 writeDeflated(b, offset, length); 293 } else { 294 writeCounted(b, offset, length); 295 } 296 sourcePayloadLength += length; 297 return writtenToOutputStreamForLastEntry - current; 298 } 299 300 public void writeCounted(final byte[] data) throws IOException { 301 writeCounted(data, 0, data.length); 302 } 303 304 public void writeCounted(final byte[] data, final int offset, final int length) throws IOException { 305 writeOut(data, offset, length); 306 writtenToOutputStreamForLastEntry += length; 307 totalWrittenToOutputStream += length; 308 } 309 310 private void writeDeflated(final byte[] b, final int offset, final int length) throws IOException { 311 if (length > 0 && !deflater.finished()) { 312 if (length <= DEFLATER_BLOCK_SIZE) { 313 deflater.setInput(b, offset, length); 314 deflateUntilInputIsNeeded(); 315 } else { 316 final int fullblocks = length / DEFLATER_BLOCK_SIZE; 317 for (int i = 0; i < fullblocks; i++) { 318 deflater.setInput(b, offset + i * DEFLATER_BLOCK_SIZE, DEFLATER_BLOCK_SIZE); 319 deflateUntilInputIsNeeded(); 320 } 321 final int done = fullblocks * DEFLATER_BLOCK_SIZE; 322 if (done < length) { 323 deflater.setInput(b, offset + done, length - done); 324 deflateUntilInputIsNeeded(); 325 } 326 } 327 } 328 } 329 330 protected abstract void writeOut(byte[] data, int offset, int length) throws IOException; 331}