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