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 18 package java.util.zip; 19 20 import java.io.ByteArrayOutputStream; 21 import java.io.IOException; 22 import java.io.OutputStream; 23 import java.nio.charset.StandardCharsets; 24 import java.util.Arrays; 25 import java.util.HashSet; 26 import 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 */ 60 public 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, nameLength; 89 90 private byte[] nameBytes; 91 92 /** 93 * Constructs a new {@code ZipOutputStream} that writes a zip file 94 * to the given {@code OutputStream}. 95 */ 96 public ZipOutputStream(OutputStream os) { 97 super(os, new Deflater(Deflater.DEFAULT_COMPRESSION, true)); 98 } 99 100 /** 101 * Closes the current {@code ZipEntry}, if any, and the underlying output 102 * stream. If the stream is already closed this method does nothing. 103 * 104 * @throws IOException 105 * If an error occurs closing the stream. 106 */ 107 @Override 108 public void close() throws IOException { 109 // don't call super.close() because that calls finish() conditionally 110 if (out != null) { 111 finish(); 112 def.end(); 113 out.close(); 114 out = null; 115 } 116 } 117 118 /** 119 * Closes the current {@code ZipEntry}. Any entry terminal data is written 120 * to the underlying stream. 121 * 122 * @throws IOException 123 * If an error occurs closing the entry. 124 */ 125 public void closeEntry() throws IOException { 126 checkOpen(); 127 if (currentEntry == null) { 128 return; 129 } 130 if (currentEntry.getMethod() == DEFLATED) { 131 super.finish(); 132 } 133 134 // Verify values for STORED types 135 if (currentEntry.getMethod() == STORED) { 136 if (crc.getValue() != currentEntry.crc) { 137 throw new ZipException("CRC mismatch"); 138 } 139 if (currentEntry.size != crc.tbytes) { 140 throw new ZipException("Size mismatch"); 141 } 142 } 143 curOffset = LOCHDR; 144 145 // Write the DataDescriptor 146 if (currentEntry.getMethod() != STORED) { 147 curOffset += EXTHDR; 148 writeLong(out, EXTSIG); 149 writeLong(out, currentEntry.crc = crc.getValue()); 150 writeLong(out, currentEntry.compressedSize = def.getTotalOut()); 151 writeLong(out, currentEntry.size = def.getTotalIn()); 152 } 153 // Update the CentralDirectory 154 // http://www.pkware.com/documents/casestudies/APPNOTE.TXT 155 int flags = currentEntry.getMethod() == STORED ? 0 : ZipFile.GPBF_DATA_DESCRIPTOR_FLAG; 156 // Since gingerbread, we always set the UTF-8 flag on individual files. 157 // Some tools insist that the central directory also have the UTF-8 flag. 158 // http://code.google.com/p/android/issues/detail?id=20214 159 flags |= ZipFile.GPBF_UTF8_FLAG; 160 writeLong(cDir, CENSIG); 161 writeShort(cDir, ZIP_VERSION_2_0); // Version this file was made by. 162 writeShort(cDir, ZIP_VERSION_2_0); // Minimum version needed to extract. 163 writeShort(cDir, flags); 164 writeShort(cDir, currentEntry.getMethod()); 165 writeShort(cDir, currentEntry.time); 166 writeShort(cDir, currentEntry.modDate); 167 writeLong(cDir, crc.getValue()); 168 if (currentEntry.getMethod() == DEFLATED) { 169 curOffset += writeLong(cDir, def.getTotalOut()); 170 writeLong(cDir, def.getTotalIn()); 171 } else { 172 curOffset += writeLong(cDir, crc.tbytes); 173 writeLong(cDir, crc.tbytes); 174 } 175 curOffset += writeShort(cDir, nameLength); 176 if (currentEntry.extra != null) { 177 curOffset += writeShort(cDir, currentEntry.extra.length); 178 } else { 179 writeShort(cDir, 0); 180 } 181 182 String comment = currentEntry.getComment(); 183 byte[] commentBytes = EmptyArray.BYTE; 184 if (comment != null) { 185 commentBytes = comment.getBytes(StandardCharsets.UTF_8); 186 } 187 writeShort(cDir, commentBytes.length); // Comment length. 188 writeShort(cDir, 0); // Disk Start 189 writeShort(cDir, 0); // Internal File Attributes 190 writeLong(cDir, 0); // External File Attributes 191 writeLong(cDir, offset); 192 cDir.write(nameBytes); 193 nameBytes = null; 194 if (currentEntry.extra != null) { 195 cDir.write(currentEntry.extra); 196 } 197 offset += curOffset; 198 if (commentBytes.length > 0) { 199 cDir.write(commentBytes); 200 } 201 currentEntry = null; 202 crc.reset(); 203 def.reset(); 204 done = false; 205 } 206 207 /** 208 * Indicates that all entries have been written to the stream. Any terminal 209 * information is written to the underlying stream. 210 * 211 * @throws IOException 212 * if an error occurs while terminating the stream. 213 */ 214 @Override 215 public void finish() throws IOException { 216 // TODO: is there a bug here? why not checkOpen? 217 if (out == null) { 218 throw new IOException("Stream is closed"); 219 } 220 if (cDir == null) { 221 return; 222 } 223 if (entries.isEmpty()) { 224 throw new ZipException("No entries"); 225 } 226 if (currentEntry != null) { 227 closeEntry(); 228 } 229 int cdirSize = cDir.size(); 230 // Write Central Dir End 231 writeLong(cDir, ENDSIG); 232 writeShort(cDir, 0); // Disk Number 233 writeShort(cDir, 0); // Start Disk 234 writeShort(cDir, entries.size()); // Number of entries 235 writeShort(cDir, entries.size()); // Number of entries 236 writeLong(cDir, cdirSize); // Size of central dir 237 writeLong(cDir, offset); // Offset of central dir 238 writeShort(cDir, commentBytes.length); 239 if (commentBytes.length > 0) { 240 cDir.write(commentBytes); 241 } 242 // Write the central directory. 243 cDir.writeTo(out); 244 cDir = null; 245 } 246 247 /** 248 * Writes entry information to the underlying stream. Data associated with 249 * the entry can then be written using {@code write()}. After data is 250 * written {@code closeEntry()} must be called to complete the writing of 251 * the entry to the underlying stream. 252 * 253 * @param ze 254 * the {@code ZipEntry} to store. 255 * @throws IOException 256 * If an error occurs storing the entry. 257 * @see #write 258 */ 259 public void putNextEntry(ZipEntry ze) throws IOException { 260 if (currentEntry != null) { 261 closeEntry(); 262 } 263 264 // Did this ZipEntry specify a method, or should we use the default? 265 int method = ze.getMethod(); 266 if (method == -1) { 267 method = defaultCompressionMethod; 268 } 269 270 // If the method is STORED, check that the ZipEntry was configured appropriately. 271 if (method == STORED) { 272 if (ze.getCompressedSize() == -1) { 273 ze.setCompressedSize(ze.getSize()); 274 } else if (ze.getSize() == -1) { 275 ze.setSize(ze.getCompressedSize()); 276 } 277 if (ze.getCrc() == -1) { 278 throw new ZipException("STORED entry missing CRC"); 279 } 280 if (ze.getSize() == -1) { 281 throw new ZipException("STORED entry missing size"); 282 } 283 if (ze.size != ze.compressedSize) { 284 throw new ZipException("STORED entry size/compressed size mismatch"); 285 } 286 } 287 288 checkOpen(); 289 290 if (entries.contains(ze.name)) { 291 throw new ZipException("Entry already exists: " + ze.name); 292 } 293 if (entries.size() == 64*1024-1) { 294 // TODO: support Zip64. 295 throw new ZipException("Too many entries for the zip file format's 16-bit entry count"); 296 } 297 nameBytes = ze.name.getBytes(StandardCharsets.UTF_8); 298 nameLength = nameBytes.length; 299 if (nameLength > 0xffff) { 300 throw new IllegalArgumentException("Name too long: " + nameLength + " UTF-8 bytes"); 301 } 302 303 def.setLevel(compressionLevel); 304 ze.setMethod(method); 305 306 currentEntry = ze; 307 entries.add(currentEntry.name); 308 309 // Local file header. 310 // http://www.pkware.com/documents/casestudies/APPNOTE.TXT 311 int flags = (method == STORED) ? 0 : ZipFile.GPBF_DATA_DESCRIPTOR_FLAG; 312 // Java always outputs UTF-8 filenames. (Before Java 7, the RI didn't set this flag and used 313 // modified UTF-8. From Java 7, it sets this flag and uses normal UTF-8.) 314 flags |= ZipFile.GPBF_UTF8_FLAG; 315 writeLong(out, LOCSIG); // Entry header 316 writeShort(out, ZIP_VERSION_2_0); // Minimum version needed to extract. 317 writeShort(out, flags); 318 writeShort(out, method); 319 if (currentEntry.getTime() == -1) { 320 currentEntry.setTime(System.currentTimeMillis()); 321 } 322 writeShort(out, currentEntry.time); 323 writeShort(out, currentEntry.modDate); 324 325 if (method == STORED) { 326 writeLong(out, currentEntry.crc); 327 writeLong(out, currentEntry.size); 328 writeLong(out, currentEntry.size); 329 } else { 330 writeLong(out, 0); 331 writeLong(out, 0); 332 writeLong(out, 0); 333 } 334 writeShort(out, nameLength); 335 if (currentEntry.extra != null) { 336 writeShort(out, currentEntry.extra.length); 337 } else { 338 writeShort(out, 0); 339 } 340 out.write(nameBytes); 341 if (currentEntry.extra != null) { 342 out.write(currentEntry.extra); 343 } 344 } 345 346 /** 347 * Sets the comment associated with the file being written. See {@link ZipFile#getComment}. 348 * @throws IllegalArgumentException if the comment is >= 64 Ki UTF-8 bytes. 349 */ 350 public void setComment(String comment) { 351 if (comment == null) { 352 this.commentBytes = null; 353 return; 354 } 355 356 byte[] newCommentBytes = comment.getBytes(StandardCharsets.UTF_8); 357 if (newCommentBytes.length > 0xffff) { 358 throw new IllegalArgumentException("Comment too long: " + newCommentBytes.length + " bytes"); 359 } 360 this.commentBytes = newCommentBytes; 361 } 362 363 /** 364 * Sets the <a href="Deflater.html#compression_level">compression level</a> to be used 365 * for writing entry data. 366 */ 367 public void setLevel(int level) { 368 if (level < Deflater.DEFAULT_COMPRESSION || level > Deflater.BEST_COMPRESSION) { 369 throw new IllegalArgumentException("Bad level: " + level); 370 } 371 compressionLevel = level; 372 } 373 374 /** 375 * Sets the default compression method to be used when a {@code ZipEntry} doesn't 376 * explicitly specify a method. See {@link ZipEntry#setMethod} for more details. 377 */ 378 public void setMethod(int method) { 379 if (method != STORED && method != DEFLATED) { 380 throw new IllegalArgumentException("Bad method: " + method); 381 } 382 defaultCompressionMethod = method; 383 } 384 385 private long writeLong(OutputStream os, long i) throws IOException { 386 // Write out the long value as an unsigned int 387 os.write((int) (i & 0xFF)); 388 os.write((int) (i >> 8) & 0xFF); 389 os.write((int) (i >> 16) & 0xFF); 390 os.write((int) (i >> 24) & 0xFF); 391 return i; 392 } 393 394 private int writeShort(OutputStream os, int i) throws IOException { 395 os.write(i & 0xFF); 396 os.write((i >> 8) & 0xFF); 397 return i; 398 } 399 400 /** 401 * Writes data for the current entry to the underlying stream. 402 * 403 * @exception IOException 404 * If an error occurs writing to the stream 405 */ 406 @Override 407 public void write(byte[] buffer, int offset, int byteCount) throws IOException { 408 Arrays.checkOffsetAndCount(buffer.length, offset, byteCount); 409 if (currentEntry == null) { 410 throw new ZipException("No active entry"); 411 } 412 413 if (currentEntry.getMethod() == STORED) { 414 out.write(buffer, offset, byteCount); 415 } else { 416 super.write(buffer, offset, byteCount); 417 } 418 crc.update(buffer, offset, byteCount); 419 } 420 421 private void checkOpen() throws IOException { 422 if (cDir == null) { 423 throw new IOException("Stream is closed"); 424 } 425 } 426 } 427