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