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