1/* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package java.util.zip; 19 20import java.io.ByteArrayOutputStream; 21import java.io.IOException; 22import java.io.OutputStream; 23import java.nio.charset.StandardCharsets; 24import java.util.Arrays; 25import java.util.HashSet; 26import libcore.util.EmptyArray; 27 28/** 29 * Used to write (compress) data into zip files. 30 * 31 * <p>{@code ZipOutputStream} is used to write {@link ZipEntry}s to the underlying 32 * stream. Output from {@code ZipOutputStream} can be read using {@link ZipFile} 33 * or {@link ZipInputStream}. 34 * 35 * <p>While {@code DeflaterOutputStream} can write compressed zip file 36 * entries, this extension can write uncompressed entries as well. 37 * Use {@link ZipEntry#setMethod} or {@link #setMethod} with the {@link ZipEntry#STORED} flag. 38 * 39 * <h3>Example</h3> 40 * <p>Using {@code ZipOutputStream} is a little more complicated than {@link GZIPOutputStream} 41 * because zip files are containers that can contain multiple files. This code creates a zip 42 * file containing several files, similar to the {@code zip(1)} utility. 43 * <pre> 44 * OutputStream os = ... 45 * ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(os)); 46 * try { 47 * for (int i = 0; i < fileCount; ++i) { 48 * String filename = ... 49 * byte[] bytes = ... 50 * ZipEntry entry = new ZipEntry(filename); 51 * zos.putNextEntry(entry); 52 * zos.write(bytes); 53 * zos.closeEntry(); 54 * } 55 * } finally { 56 * zos.close(); 57 * } 58 * </pre> 59 */ 60public class ZipOutputStream extends DeflaterOutputStream implements ZipConstants { 61 62 /** 63 * Indicates deflated entries. 64 */ 65 public static final int DEFLATED = 8; 66 67 /** 68 * Indicates uncompressed entries. 69 */ 70 public static final int STORED = 0; 71 72 private static final int ZIP_VERSION_2_0 = 20; // Zip specification version 2.0. 73 74 private byte[] commentBytes = EmptyArray.BYTE; 75 76 private final HashSet<String> entries = new HashSet<String>(); 77 78 private int defaultCompressionMethod = DEFLATED; 79 80 private int compressionLevel = Deflater.DEFAULT_COMPRESSION; 81 82 private ByteArrayOutputStream cDir = new ByteArrayOutputStream(); 83 84 private ZipEntry currentEntry; 85 86 private final CRC32 crc = new CRC32(); 87 88 private int offset = 0, curOffset = 0; 89 90 /** The charset-encoded name for the current entry. */ 91 private byte[] nameBytes; 92 93 /** The charset-encoded comment for the current entry. */ 94 private byte[] entryCommentBytes; 95 96 /** 97 * Constructs a new {@code ZipOutputStream} that writes a zip file to the given 98 * {@code OutputStream}. 99 * 100 * <p>UTF-8 will be used to encode the file comment, entry names and comments. 101 */ 102 public ZipOutputStream(OutputStream os) { 103 super(os, new Deflater(Deflater.DEFAULT_COMPRESSION, true)); 104 } 105 106 /** 107 * Closes the current {@code ZipEntry}, if any, and the underlying output 108 * stream. If the stream is already closed this method does nothing. 109 * 110 * @throws IOException 111 * If an error occurs closing the stream. 112 */ 113 @Override 114 public void close() throws IOException { 115 // don't call super.close() because that calls finish() conditionally 116 if (out != null) { 117 finish(); 118 def.end(); 119 out.close(); 120 out = null; 121 } 122 } 123 124 /** 125 * Closes the current {@code ZipEntry}. Any entry terminal data is written 126 * to the underlying stream. 127 * 128 * @throws IOException 129 * If an error occurs closing the entry. 130 */ 131 public void closeEntry() throws IOException { 132 checkOpen(); 133 if (currentEntry == null) { 134 return; 135 } 136 if (currentEntry.getMethod() == DEFLATED) { 137 super.finish(); 138 } 139 140 // Verify values for STORED types 141 if (currentEntry.getMethod() == STORED) { 142 if (crc.getValue() != currentEntry.crc) { 143 throw new ZipException("CRC mismatch"); 144 } 145 if (currentEntry.size != crc.tbytes) { 146 throw new ZipException("Size mismatch"); 147 } 148 } 149 curOffset = LOCHDR; 150 151 // Write the DataDescriptor 152 if (currentEntry.getMethod() != STORED) { 153 curOffset += EXTHDR; 154 writeLong(out, EXTSIG); 155 writeLong(out, currentEntry.crc = crc.getValue()); 156 writeLong(out, currentEntry.compressedSize = def.getTotalOut()); 157 writeLong(out, currentEntry.size = def.getTotalIn()); 158 } 159 // Update the CentralDirectory 160 // http://www.pkware.com/documents/casestudies/APPNOTE.TXT 161 int flags = currentEntry.getMethod() == STORED ? 0 : ZipFile.GPBF_DATA_DESCRIPTOR_FLAG; 162 // Since gingerbread, we always set the UTF-8 flag on individual files if appropriate. 163 // Some tools insist that the central directory have the UTF-8 flag. 164 // http://code.google.com/p/android/issues/detail?id=20214 165 flags |= ZipFile.GPBF_UTF8_FLAG; 166 writeLong(cDir, CENSIG); 167 writeShort(cDir, ZIP_VERSION_2_0); // Version this file was made by. 168 writeShort(cDir, ZIP_VERSION_2_0); // Minimum version needed to extract. 169 writeShort(cDir, flags); 170 writeShort(cDir, currentEntry.getMethod()); 171 writeShort(cDir, currentEntry.time); 172 writeShort(cDir, currentEntry.modDate); 173 writeLong(cDir, crc.getValue()); 174 if (currentEntry.getMethod() == DEFLATED) { 175 curOffset += writeLong(cDir, def.getTotalOut()); 176 writeLong(cDir, def.getTotalIn()); 177 } else { 178 curOffset += writeLong(cDir, crc.tbytes); 179 writeLong(cDir, crc.tbytes); 180 } 181 curOffset += writeShort(cDir, nameBytes.length); 182 if (currentEntry.extra != null) { 183 curOffset += writeShort(cDir, currentEntry.extra.length); 184 } else { 185 writeShort(cDir, 0); 186 } 187 188 writeShort(cDir, entryCommentBytes.length); // Comment length. 189 writeShort(cDir, 0); // Disk Start 190 writeShort(cDir, 0); // Internal File Attributes 191 writeLong(cDir, 0); // External File Attributes 192 writeLong(cDir, offset); 193 cDir.write(nameBytes); 194 nameBytes = null; 195 if (currentEntry.extra != null) { 196 cDir.write(currentEntry.extra); 197 } 198 offset += curOffset; 199 if (entryCommentBytes.length > 0) { 200 cDir.write(entryCommentBytes); 201 entryCommentBytes = EmptyArray.BYTE; 202 } 203 currentEntry = null; 204 crc.reset(); 205 def.reset(); 206 done = false; 207 } 208 209 /** 210 * Indicates that all entries have been written to the stream. Any terminal 211 * information is written to the underlying stream. 212 * 213 * @throws IOException 214 * if an error occurs while terminating the stream. 215 */ 216 @Override 217 public void finish() throws IOException { 218 // TODO: is there a bug here? why not checkOpen? 219 if (out == null) { 220 throw new IOException("Stream is closed"); 221 } 222 if (cDir == null) { 223 return; 224 } 225 if (entries.isEmpty()) { 226 throw new ZipException("No entries"); 227 } 228 if (currentEntry != null) { 229 closeEntry(); 230 } 231 int cdirSize = cDir.size(); 232 // Write Central Dir End 233 writeLong(cDir, ENDSIG); 234 writeShort(cDir, 0); // Disk Number 235 writeShort(cDir, 0); // Start Disk 236 writeShort(cDir, entries.size()); // Number of entries 237 writeShort(cDir, entries.size()); // Number of entries 238 writeLong(cDir, cdirSize); // Size of central dir 239 writeLong(cDir, offset); // Offset of central dir 240 writeShort(cDir, commentBytes.length); 241 if (commentBytes.length > 0) { 242 cDir.write(commentBytes); 243 } 244 // Write the central directory. 245 cDir.writeTo(out); 246 cDir = null; 247 } 248 249 /** 250 * Writes entry information to the underlying stream. Data associated with 251 * the entry can then be written using {@code write()}. After data is 252 * written {@code closeEntry()} must be called to complete the writing of 253 * the entry to the underlying stream. 254 * 255 * @param ze 256 * the {@code ZipEntry} to store. 257 * @throws IOException 258 * If an error occurs storing the entry. 259 * @see #write 260 */ 261 public void putNextEntry(ZipEntry ze) throws IOException { 262 if (currentEntry != null) { 263 closeEntry(); 264 } 265 266 // Did this ZipEntry specify a method, or should we use the default? 267 int method = ze.getMethod(); 268 if (method == -1) { 269 method = defaultCompressionMethod; 270 } 271 272 // If the method is STORED, check that the ZipEntry was configured appropriately. 273 if (method == STORED) { 274 if (ze.getCompressedSize() == -1) { 275 ze.setCompressedSize(ze.getSize()); 276 } else if (ze.getSize() == -1) { 277 ze.setSize(ze.getCompressedSize()); 278 } 279 if (ze.getCrc() == -1) { 280 throw new ZipException("STORED entry missing CRC"); 281 } 282 if (ze.getSize() == -1) { 283 throw new ZipException("STORED entry missing size"); 284 } 285 if (ze.size != ze.compressedSize) { 286 throw new ZipException("STORED entry size/compressed size mismatch"); 287 } 288 } 289 290 checkOpen(); 291 292 if (entries.contains(ze.name)) { 293 throw new ZipException("Entry already exists: " + ze.name); 294 } 295 if (entries.size() == 64*1024-1) { 296 // TODO: support Zip64. 297 throw new ZipException("Too many entries for the zip file format's 16-bit entry count"); 298 } 299 nameBytes = ze.name.getBytes(StandardCharsets.UTF_8); 300 checkSizeIsWithinShort("Name", nameBytes); 301 entryCommentBytes = EmptyArray.BYTE; 302 if (ze.comment != null) { 303 entryCommentBytes = ze.comment.getBytes(StandardCharsets.UTF_8); 304 // The comment is not written out until the entry is finished, but it is validated here 305 // to fail-fast. 306 checkSizeIsWithinShort("Comment", entryCommentBytes); 307 } 308 309 def.setLevel(compressionLevel); 310 ze.setMethod(method); 311 312 currentEntry = ze; 313 entries.add(currentEntry.name); 314 315 // Local file header. 316 // http://www.pkware.com/documents/casestudies/APPNOTE.TXT 317 int flags = (method == STORED) ? 0 : ZipFile.GPBF_DATA_DESCRIPTOR_FLAG; 318 // Java always outputs UTF-8 filenames. (Before Java 7, the RI didn't set this flag and used 319 // modified UTF-8. From Java 7, when using UTF_8 it sets this flag and uses normal UTF-8.) 320 flags |= ZipFile.GPBF_UTF8_FLAG; 321 writeLong(out, LOCSIG); // Entry header 322 writeShort(out, ZIP_VERSION_2_0); // Minimum version needed to extract. 323 writeShort(out, flags); 324 writeShort(out, method); 325 if (currentEntry.getTime() == -1) { 326 currentEntry.setTime(System.currentTimeMillis()); 327 } 328 writeShort(out, currentEntry.time); 329 writeShort(out, currentEntry.modDate); 330 331 if (method == STORED) { 332 writeLong(out, currentEntry.crc); 333 writeLong(out, currentEntry.size); 334 writeLong(out, currentEntry.size); 335 } else { 336 writeLong(out, 0); 337 writeLong(out, 0); 338 writeLong(out, 0); 339 } 340 writeShort(out, nameBytes.length); 341 if (currentEntry.extra != null) { 342 writeShort(out, currentEntry.extra.length); 343 } else { 344 writeShort(out, 0); 345 } 346 out.write(nameBytes); 347 if (currentEntry.extra != null) { 348 out.write(currentEntry.extra); 349 } 350 } 351 352 /** 353 * Sets the comment associated with the file being written. See {@link ZipFile#getComment}. 354 * @throws IllegalArgumentException if the comment is >= 64 Ki encoded bytes. 355 */ 356 public void setComment(String comment) { 357 if (comment == null) { 358 this.commentBytes = EmptyArray.BYTE; 359 return; 360 } 361 362 byte[] newCommentBytes = comment.getBytes(StandardCharsets.UTF_8); 363 checkSizeIsWithinShort("Comment", newCommentBytes); 364 this.commentBytes = newCommentBytes; 365 } 366 367 /** 368 * Sets the <a href="Deflater.html#compression_level">compression level</a> to be used 369 * for writing entry data. 370 */ 371 public void setLevel(int level) { 372 if (level < Deflater.DEFAULT_COMPRESSION || level > Deflater.BEST_COMPRESSION) { 373 throw new IllegalArgumentException("Bad level: " + level); 374 } 375 compressionLevel = level; 376 } 377 378 /** 379 * Sets the default compression method to be used when a {@code ZipEntry} doesn't 380 * explicitly specify a method. See {@link ZipEntry#setMethod} for more details. 381 */ 382 public void setMethod(int method) { 383 if (method != STORED && method != DEFLATED) { 384 throw new IllegalArgumentException("Bad method: " + method); 385 } 386 defaultCompressionMethod = method; 387 } 388 389 private long writeLong(OutputStream os, long i) throws IOException { 390 // Write out the long value as an unsigned int 391 os.write((int) (i & 0xFF)); 392 os.write((int) (i >> 8) & 0xFF); 393 os.write((int) (i >> 16) & 0xFF); 394 os.write((int) (i >> 24) & 0xFF); 395 return i; 396 } 397 398 private int writeShort(OutputStream os, int i) throws IOException { 399 os.write(i & 0xFF); 400 os.write((i >> 8) & 0xFF); 401 return i; 402 } 403 404 /** 405 * Writes data for the current entry to the underlying stream. 406 * 407 * @throws IOException 408 * If an error occurs writing to the stream 409 */ 410 @Override 411 public void write(byte[] buffer, int offset, int byteCount) throws IOException { 412 Arrays.checkOffsetAndCount(buffer.length, offset, byteCount); 413 if (currentEntry == null) { 414 throw new ZipException("No active entry"); 415 } 416 417 if (currentEntry.getMethod() == STORED) { 418 out.write(buffer, offset, byteCount); 419 } else { 420 super.write(buffer, offset, byteCount); 421 } 422 crc.update(buffer, offset, byteCount); 423 } 424 425 private void checkOpen() throws IOException { 426 if (cDir == null) { 427 throw new IOException("Stream is closed"); 428 } 429 } 430 431 private void checkSizeIsWithinShort(String property, byte[] bytes) { 432 if (bytes.length > 0xffff) { 433 throw new IllegalArgumentException(property + " too long in UTF-8:" + bytes.length + 434 " bytes"); 435 } 436 } 437} 438