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 */ 019 package org.apache.commons.compress.archivers.cpio; 020 021 import java.io.File; 022 import java.io.IOException; 023 import java.io.OutputStream; 024 import java.util.HashMap; 025 026 import org.apache.commons.compress.archivers.ArchiveEntry; 027 import org.apache.commons.compress.archivers.ArchiveOutputStream; 028 import org.apache.commons.compress.utils.ArchiveUtils; 029 030 /** 031 * CPIOArchiveOutputStream is a stream for writing CPIO streams. All formats of 032 * CPIO are supported (old ASCII, old binary, new portable format and the new 033 * portable format with CRC). 034 * <p/> 035 * <p/> 036 * An entry can be written by creating an instance of CpioArchiveEntry and fill 037 * it with the necessary values and put it into the CPIO stream. Afterwards 038 * write the contents of the file into the CPIO stream. Either close the stream 039 * by calling finish() or put a next entry into the cpio stream. 040 * <p/> 041 * <code><pre> 042 * CpioArchiveOutputStream out = new CpioArchiveOutputStream( 043 * new FileOutputStream(new File("test.cpio"))); 044 * CpioArchiveEntry entry = new CpioArchiveEntry(); 045 * entry.setName("testfile"); 046 * String contents = "12345"; 047 * entry.setFileSize(contents.length()); 048 * entry.setMode(CpioConstants.C_ISREG); // regular file 049 * ... set other attributes, e.g. time, number of links 050 * out.putArchiveEntry(entry); 051 * out.write(testContents.getBytes()); 052 * out.close(); 053 * </pre></code> 054 * <p/> 055 * Note: This implementation should be compatible to cpio 2.5 056 * 057 * This class uses mutable fields and is not considered threadsafe. 058 * 059 * based on code from the jRPM project (jrpm.sourceforge.net) 060 */ 061 public class CpioArchiveOutputStream extends ArchiveOutputStream implements 062 CpioConstants { 063 064 private CpioArchiveEntry entry; 065 066 private boolean closed = false; 067 068 /** indicates if this archive is finished */ 069 private boolean finished; 070 071 /** 072 * See {@link CpioArchiveEntry#setFormat(short)} for possible values. 073 */ 074 private final short entryFormat; 075 076 private final HashMap names = new HashMap(); 077 078 private long crc = 0; 079 080 private long written; 081 082 private final OutputStream out; 083 084 private final int blockSize; 085 086 private long nextArtificalDeviceAndInode = 1; 087 088 /** 089 * Construct the cpio output stream with a specified format and a 090 * blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE}. 091 * 092 * @param out 093 * The cpio stream 094 * @param format 095 * The format of the stream 096 */ 097 public CpioArchiveOutputStream(final OutputStream out, final short format) { 098 this(out, format, BLOCK_SIZE); 099 } 100 101 /** 102 * Construct the cpio output stream with a specified format 103 * 104 * @param out 105 * The cpio stream 106 * @param format 107 * The format of the stream 108 * @param blockSize 109 * The block size of the archive. 110 * 111 * @since Apache Commons Compress 1.1 112 */ 113 public CpioArchiveOutputStream(final OutputStream out, final short format, 114 final int blockSize) { 115 this.out = out; 116 switch (format) { 117 case FORMAT_NEW: 118 case FORMAT_NEW_CRC: 119 case FORMAT_OLD_ASCII: 120 case FORMAT_OLD_BINARY: 121 break; 122 default: 123 throw new IllegalArgumentException("Unknown format: "+format); 124 125 } 126 this.entryFormat = format; 127 this.blockSize = blockSize; 128 } 129 130 /** 131 * Construct the cpio output stream. The format for this CPIO stream is the 132 * "new" format 133 * 134 * @param out 135 * The cpio stream 136 */ 137 public CpioArchiveOutputStream(final OutputStream out) { 138 this(out, FORMAT_NEW); 139 } 140 141 /** 142 * Check to make sure that this stream has not been closed 143 * 144 * @throws IOException 145 * if the stream is already closed 146 */ 147 private void ensureOpen() throws IOException { 148 if (this.closed) { 149 throw new IOException("Stream closed"); 150 } 151 } 152 153 /** 154 * Begins writing a new CPIO file entry and positions the stream to the 155 * start of the entry data. Closes the current entry if still active. The 156 * current time will be used if the entry has no set modification time and 157 * the default header format will be used if no other format is specified in 158 * the entry. 159 * 160 * @param entry 161 * the CPIO cpioEntry to be written 162 * @throws IOException 163 * if an I/O error has occurred or if a CPIO file error has 164 * occurred 165 * @throws ClassCastException if entry is not an instance of CpioArchiveEntry 166 */ 167 public void putArchiveEntry(ArchiveEntry entry) throws IOException { 168 if(finished) { 169 throw new IOException("Stream has already been finished"); 170 } 171 172 CpioArchiveEntry e = (CpioArchiveEntry) entry; 173 ensureOpen(); 174 if (this.entry != null) { 175 closeArchiveEntry(); // close previous entry 176 } 177 if (e.getTime() == -1) { 178 e.setTime(System.currentTimeMillis() / 1000); 179 } 180 181 final short format = e.getFormat(); 182 if (format != this.entryFormat){ 183 throw new IOException("Header format: "+format+" does not match existing format: "+this.entryFormat); 184 } 185 186 if (this.names.put(e.getName(), e) != null) { 187 throw new IOException("duplicate entry: " + e.getName()); 188 } 189 190 writeHeader(e); 191 this.entry = e; 192 this.written = 0; 193 } 194 195 private void writeHeader(final CpioArchiveEntry e) throws IOException { 196 switch (e.getFormat()) { 197 case FORMAT_NEW: 198 out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW)); 199 count(6); 200 writeNewEntry(e); 201 break; 202 case FORMAT_NEW_CRC: 203 out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW_CRC)); 204 count(6); 205 writeNewEntry(e); 206 break; 207 case FORMAT_OLD_ASCII: 208 out.write(ArchiveUtils.toAsciiBytes(MAGIC_OLD_ASCII)); 209 count(6); 210 writeOldAsciiEntry(e); 211 break; 212 case FORMAT_OLD_BINARY: 213 boolean swapHalfWord = true; 214 writeBinaryLong(MAGIC_OLD_BINARY, 2, swapHalfWord); 215 writeOldBinaryEntry(e, swapHalfWord); 216 break; 217 } 218 } 219 220 private void writeNewEntry(final CpioArchiveEntry entry) throws IOException { 221 long inode = entry.getInode(); 222 long devMin = entry.getDeviceMin(); 223 if (CPIO_TRAILER.equals(entry.getName())) { 224 inode = devMin = 0; 225 } else { 226 if (inode == 0 && devMin == 0) { 227 inode = nextArtificalDeviceAndInode & 0xFFFFFFFF; 228 devMin = (nextArtificalDeviceAndInode++ >> 32) & 0xFFFFFFFF; 229 } else { 230 nextArtificalDeviceAndInode = 231 Math.max(nextArtificalDeviceAndInode, 232 inode + 0x100000000L * devMin) + 1; 233 } 234 } 235 236 writeAsciiLong(inode, 8, 16); 237 writeAsciiLong(entry.getMode(), 8, 16); 238 writeAsciiLong(entry.getUID(), 8, 16); 239 writeAsciiLong(entry.getGID(), 8, 16); 240 writeAsciiLong(entry.getNumberOfLinks(), 8, 16); 241 writeAsciiLong(entry.getTime(), 8, 16); 242 writeAsciiLong(entry.getSize(), 8, 16); 243 writeAsciiLong(entry.getDeviceMaj(), 8, 16); 244 writeAsciiLong(devMin, 8, 16); 245 writeAsciiLong(entry.getRemoteDeviceMaj(), 8, 16); 246 writeAsciiLong(entry.getRemoteDeviceMin(), 8, 16); 247 writeAsciiLong(entry.getName().length() + 1, 8, 16); 248 writeAsciiLong(entry.getChksum(), 8, 16); 249 writeCString(entry.getName()); 250 pad(entry.getHeaderPadCount()); 251 } 252 253 private void writeOldAsciiEntry(final CpioArchiveEntry entry) 254 throws IOException { 255 long inode = entry.getInode(); 256 long device = entry.getDevice(); 257 if (CPIO_TRAILER.equals(entry.getName())) { 258 inode = device = 0; 259 } else { 260 if (inode == 0 && device == 0) { 261 inode = nextArtificalDeviceAndInode & 0777777; 262 device = (nextArtificalDeviceAndInode++ >> 18) & 0777777; 263 } else { 264 nextArtificalDeviceAndInode = 265 Math.max(nextArtificalDeviceAndInode, 266 inode + 01000000 * device) + 1; 267 } 268 } 269 270 writeAsciiLong(device, 6, 8); 271 writeAsciiLong(inode, 6, 8); 272 writeAsciiLong(entry.getMode(), 6, 8); 273 writeAsciiLong(entry.getUID(), 6, 8); 274 writeAsciiLong(entry.getGID(), 6, 8); 275 writeAsciiLong(entry.getNumberOfLinks(), 6, 8); 276 writeAsciiLong(entry.getRemoteDevice(), 6, 8); 277 writeAsciiLong(entry.getTime(), 11, 8); 278 writeAsciiLong(entry.getName().length() + 1, 6, 8); 279 writeAsciiLong(entry.getSize(), 11, 8); 280 writeCString(entry.getName()); 281 } 282 283 private void writeOldBinaryEntry(final CpioArchiveEntry entry, 284 final boolean swapHalfWord) throws IOException { 285 long inode = entry.getInode(); 286 long device = entry.getDevice(); 287 if (CPIO_TRAILER.equals(entry.getName())) { 288 inode = device = 0; 289 } else { 290 if (inode == 0 && device == 0) { 291 inode = nextArtificalDeviceAndInode & 0xFFFF; 292 device = (nextArtificalDeviceAndInode++ >> 16) & 0xFFFF; 293 } else { 294 nextArtificalDeviceAndInode = 295 Math.max(nextArtificalDeviceAndInode, 296 inode + 0x10000 * device) + 1; 297 } 298 } 299 300 writeBinaryLong(device, 2, swapHalfWord); 301 writeBinaryLong(inode, 2, swapHalfWord); 302 writeBinaryLong(entry.getMode(), 2, swapHalfWord); 303 writeBinaryLong(entry.getUID(), 2, swapHalfWord); 304 writeBinaryLong(entry.getGID(), 2, swapHalfWord); 305 writeBinaryLong(entry.getNumberOfLinks(), 2, swapHalfWord); 306 writeBinaryLong(entry.getRemoteDevice(), 2, swapHalfWord); 307 writeBinaryLong(entry.getTime(), 4, swapHalfWord); 308 writeBinaryLong(entry.getName().length() + 1, 2, swapHalfWord); 309 writeBinaryLong(entry.getSize(), 4, swapHalfWord); 310 writeCString(entry.getName()); 311 pad(entry.getHeaderPadCount()); 312 } 313 314 /*(non-Javadoc) 315 * 316 * @see 317 * org.apache.commons.compress.archivers.ArchiveOutputStream#closeArchiveEntry 318 * () 319 */ 320 public void closeArchiveEntry() throws IOException { 321 if(finished) { 322 throw new IOException("Stream has already been finished"); 323 } 324 325 ensureOpen(); 326 327 if (entry == null) { 328 throw new IOException("Trying to close non-existent entry"); 329 } 330 331 if (this.entry.getSize() != this.written) { 332 throw new IOException("invalid entry size (expected " 333 + this.entry.getSize() + " but got " + this.written 334 + " bytes)"); 335 } 336 pad(this.entry.getDataPadCount()); 337 if (this.entry.getFormat() == FORMAT_NEW_CRC) { 338 if (this.crc != this.entry.getChksum()) { 339 throw new IOException("CRC Error"); 340 } 341 } 342 this.entry = null; 343 this.crc = 0; 344 this.written = 0; 345 } 346 347 /** 348 * Writes an array of bytes to the current CPIO entry data. This method will 349 * block until all the bytes are written. 350 * 351 * @param b 352 * the data to be written 353 * @param off 354 * the start offset in the data 355 * @param len 356 * the number of bytes that are written 357 * @throws IOException 358 * if an I/O error has occurred or if a CPIO file error has 359 * occurred 360 */ 361 public void write(final byte[] b, final int off, final int len) 362 throws IOException { 363 ensureOpen(); 364 if (off < 0 || len < 0 || off > b.length - len) { 365 throw new IndexOutOfBoundsException(); 366 } else if (len == 0) { 367 return; 368 } 369 370 if (this.entry == null) { 371 throw new IOException("no current CPIO entry"); 372 } 373 if (this.written + len > this.entry.getSize()) { 374 throw new IOException("attempt to write past end of STORED entry"); 375 } 376 out.write(b, off, len); 377 this.written += len; 378 if (this.entry.getFormat() == FORMAT_NEW_CRC) { 379 for (int pos = 0; pos < len; pos++) { 380 this.crc += b[pos] & 0xFF; 381 } 382 } 383 count(len); 384 } 385 386 /** 387 * Finishes writing the contents of the CPIO output stream without closing 388 * the underlying stream. Use this method when applying multiple filters in 389 * succession to the same output stream. 390 * 391 * @throws IOException 392 * if an I/O exception has occurred or if a CPIO file error has 393 * occurred 394 */ 395 public void finish() throws IOException { 396 ensureOpen(); 397 if (finished) { 398 throw new IOException("This archive has already been finished"); 399 } 400 401 if (this.entry != null) { 402 throw new IOException("This archive contains unclosed entries."); 403 } 404 this.entry = new CpioArchiveEntry(this.entryFormat); 405 this.entry.setName(CPIO_TRAILER); 406 this.entry.setNumberOfLinks(1); 407 writeHeader(this.entry); 408 closeArchiveEntry(); 409 410 int lengthOfLastBlock = (int) (getBytesWritten() % blockSize); 411 if (lengthOfLastBlock != 0) { 412 pad(blockSize - lengthOfLastBlock); 413 } 414 415 finished = true; 416 } 417 418 /** 419 * Closes the CPIO output stream as well as the stream being filtered. 420 * 421 * @throws IOException 422 * if an I/O error has occurred or if a CPIO file error has 423 * occurred 424 */ 425 public void close() throws IOException { 426 if(!finished) { 427 finish(); 428 } 429 430 if (!this.closed) { 431 out.close(); 432 this.closed = true; 433 } 434 } 435 436 private void pad(int count) throws IOException{ 437 if (count > 0){ 438 byte buff[] = new byte[count]; 439 out.write(buff); 440 count(count); 441 } 442 } 443 444 private void writeBinaryLong(final long number, final int length, 445 final boolean swapHalfWord) throws IOException { 446 byte tmp[] = CpioUtil.long2byteArray(number, length, swapHalfWord); 447 out.write(tmp); 448 count(tmp.length); 449 } 450 451 private void writeAsciiLong(final long number, final int length, 452 final int radix) throws IOException { 453 StringBuffer tmp = new StringBuffer(); 454 String tmpStr; 455 if (radix == 16) { 456 tmp.append(Long.toHexString(number)); 457 } else if (radix == 8) { 458 tmp.append(Long.toOctalString(number)); 459 } else { 460 tmp.append(Long.toString(number)); 461 } 462 463 if (tmp.length() <= length) { 464 long insertLength = length - tmp.length(); 465 for (int pos = 0; pos < insertLength; pos++) { 466 tmp.insert(0, "0"); 467 } 468 tmpStr = tmp.toString(); 469 } else { 470 tmpStr = tmp.substring(tmp.length() - length); 471 } 472 byte[] b = ArchiveUtils.toAsciiBytes(tmpStr); 473 out.write(b); 474 count(b.length); 475 } 476 477 /** 478 * Writes an ASCII string to the stream followed by \0 479 * @param str the String to write 480 * @throws IOException if the string couldn't be written 481 */ 482 private void writeCString(final String str) throws IOException { 483 byte[] b = ArchiveUtils.toAsciiBytes(str); 484 out.write(b); 485 out.write('\0'); 486 count(b.length + 1); 487 } 488 489 /** 490 * Creates a new ArchiveEntry. The entryName must be an ASCII encoded string. 491 * 492 * @see org.apache.commons.compress.archivers.ArchiveOutputStream#createArchiveEntry(java.io.File, java.lang.String) 493 */ 494 public ArchiveEntry createArchiveEntry(File inputFile, String entryName) 495 throws IOException { 496 if(finished) { 497 throw new IOException("Stream has already been finished"); 498 } 499 return new CpioArchiveEntry(inputFile, entryName); 500 } 501 502 }