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