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 if (inode == 0 && devMin == 0) { 284 inode = nextArtificalDeviceAndInode & 0xFFFFFFFF; 285 devMin = (nextArtificalDeviceAndInode++ >> 32) & 0xFFFFFFFF; 286 } else { 287 nextArtificalDeviceAndInode = 288 Math.max(nextArtificalDeviceAndInode, 289 inode + 0x100000000L * devMin) + 1; 290 } 291 292 writeAsciiLong(inode, 8, 16); 293 writeAsciiLong(entry.getMode(), 8, 16); 294 writeAsciiLong(entry.getUID(), 8, 16); 295 writeAsciiLong(entry.getGID(), 8, 16); 296 writeAsciiLong(entry.getNumberOfLinks(), 8, 16); 297 writeAsciiLong(entry.getTime(), 8, 16); 298 writeAsciiLong(entry.getSize(), 8, 16); 299 writeAsciiLong(entry.getDeviceMaj(), 8, 16); 300 writeAsciiLong(devMin, 8, 16); 301 writeAsciiLong(entry.getRemoteDeviceMaj(), 8, 16); 302 writeAsciiLong(entry.getRemoteDeviceMin(), 8, 16); 303 final byte[] name = encode(entry.getName()); 304 writeAsciiLong(name.length + 1L, 8, 16); 305 writeAsciiLong(entry.getChksum(), 8, 16); 306 writeCString(name); 307 pad(entry.getHeaderPadCount(name.length)); 308 } 309 310 private void writeOldAsciiEntry(final CpioArchiveEntry entry) 311 throws IOException { 312 long inode = entry.getInode(); 313 long device = entry.getDevice(); 314 if (CPIO_TRAILER.equals(entry.getName())) { 315 inode = device = 0; 316 } else if (inode == 0 && device == 0) { 317 inode = nextArtificalDeviceAndInode & 0777777; 318 device = (nextArtificalDeviceAndInode++ >> 18) & 0777777; 319 } else { 320 nextArtificalDeviceAndInode = 321 Math.max(nextArtificalDeviceAndInode, 322 inode + 01000000 * device) + 1; 323 } 324 325 writeAsciiLong(device, 6, 8); 326 writeAsciiLong(inode, 6, 8); 327 writeAsciiLong(entry.getMode(), 6, 8); 328 writeAsciiLong(entry.getUID(), 6, 8); 329 writeAsciiLong(entry.getGID(), 6, 8); 330 writeAsciiLong(entry.getNumberOfLinks(), 6, 8); 331 writeAsciiLong(entry.getRemoteDevice(), 6, 8); 332 writeAsciiLong(entry.getTime(), 11, 8); 333 final byte[] name = encode(entry.getName()); 334 writeAsciiLong(name.length + 1L, 6, 8); 335 writeAsciiLong(entry.getSize(), 11, 8); 336 writeCString(name); 337 } 338 339 private void writeOldBinaryEntry(final CpioArchiveEntry entry, 340 final boolean swapHalfWord) throws IOException { 341 long inode = entry.getInode(); 342 long device = entry.getDevice(); 343 if (CPIO_TRAILER.equals(entry.getName())) { 344 inode = device = 0; 345 } else if (inode == 0 && device == 0) { 346 inode = nextArtificalDeviceAndInode & 0xFFFF; 347 device = (nextArtificalDeviceAndInode++ >> 16) & 0xFFFF; 348 } else { 349 nextArtificalDeviceAndInode = 350 Math.max(nextArtificalDeviceAndInode, 351 inode + 0x10000 * device) + 1; 352 } 353 354 writeBinaryLong(device, 2, swapHalfWord); 355 writeBinaryLong(inode, 2, swapHalfWord); 356 writeBinaryLong(entry.getMode(), 2, swapHalfWord); 357 writeBinaryLong(entry.getUID(), 2, swapHalfWord); 358 writeBinaryLong(entry.getGID(), 2, swapHalfWord); 359 writeBinaryLong(entry.getNumberOfLinks(), 2, swapHalfWord); 360 writeBinaryLong(entry.getRemoteDevice(), 2, swapHalfWord); 361 writeBinaryLong(entry.getTime(), 4, swapHalfWord); 362 final byte[] name = encode(entry.getName()); 363 writeBinaryLong(name.length + 1L, 2, swapHalfWord); 364 writeBinaryLong(entry.getSize(), 4, swapHalfWord); 365 writeCString(name); 366 pad(entry.getHeaderPadCount(name.length)); 367 } 368 369 /*(non-Javadoc) 370 * 371 * @see 372 * org.apache.commons.compress.archivers.ArchiveOutputStream#closeArchiveEntry 373 * () 374 */ 375 @Override 376 public void closeArchiveEntry() throws IOException { 377 if(finished) { 378 throw new IOException("Stream has already been finished"); 379 } 380 381 ensureOpen(); 382 383 if (entry == null) { 384 throw new IOException("Trying to close non-existent entry"); 385 } 386 387 if (this.entry.getSize() != this.written) { 388 throw new IOException("Invalid entry size (expected " 389 + this.entry.getSize() + " but got " + this.written 390 + " bytes)"); 391 } 392 pad(this.entry.getDataPadCount()); 393 if (this.entry.getFormat() == FORMAT_NEW_CRC 394 && this.crc != this.entry.getChksum()) { 395 throw new IOException("CRC Error"); 396 } 397 this.entry = null; 398 this.crc = 0; 399 this.written = 0; 400 } 401 402 /** 403 * Writes an array of bytes to the current CPIO entry data. This method will 404 * block until all the bytes are written. 405 * 406 * @param b 407 * the data to be written 408 * @param off 409 * the start offset in the data 410 * @param len 411 * the number of bytes that are written 412 * @throws IOException 413 * if an I/O error has occurred or if a CPIO file error has 414 * occurred 415 */ 416 @Override 417 public void write(final byte[] b, final int off, final int len) 418 throws IOException { 419 ensureOpen(); 420 if (off < 0 || len < 0 || off > b.length - len) { 421 throw new IndexOutOfBoundsException(); 422 } 423 if (len == 0) { 424 return; 425 } 426 427 if (this.entry == null) { 428 throw new IOException("No current CPIO entry"); 429 } 430 if (this.written + len > this.entry.getSize()) { 431 throw new IOException("Attempt to write past end of STORED entry"); 432 } 433 out.write(b, off, len); 434 this.written += len; 435 if (this.entry.getFormat() == FORMAT_NEW_CRC) { 436 for (int pos = 0; pos < len; pos++) { 437 this.crc += b[pos] & 0xFF; 438 this.crc &= 0xFFFFFFFFL; 439 } 440 } 441 count(len); 442 } 443 444 /** 445 * Finishes writing the contents of the CPIO output stream without closing 446 * the underlying stream. Use this method when applying multiple filters in 447 * succession to the same output stream. 448 * 449 * @throws IOException 450 * if an I/O exception has occurred or if a CPIO file error has 451 * occurred 452 */ 453 @Override 454 public void finish() throws IOException { 455 ensureOpen(); 456 if (finished) { 457 throw new IOException("This archive has already been finished"); 458 } 459 460 if (this.entry != null) { 461 throw new IOException("This archive contains unclosed entries."); 462 } 463 this.entry = new CpioArchiveEntry(this.entryFormat); 464 this.entry.setName(CPIO_TRAILER); 465 this.entry.setNumberOfLinks(1); 466 writeHeader(this.entry); 467 closeArchiveEntry(); 468 469 final int lengthOfLastBlock = (int) (getBytesWritten() % blockSize); 470 if (lengthOfLastBlock != 0) { 471 pad(blockSize - lengthOfLastBlock); 472 } 473 474 finished = true; 475 } 476 477 /** 478 * Closes the CPIO output stream as well as the stream being filtered. 479 * 480 * @throws IOException 481 * if an I/O error has occurred or if a CPIO file error has 482 * occurred 483 */ 484 @Override 485 public void close() throws IOException { 486 try { 487 if (!finished) { 488 finish(); 489 } 490 } finally { 491 if (!this.closed) { 492 out.close(); 493 this.closed = true; 494 } 495 } 496 } 497 498 private void pad(final int count) throws IOException{ 499 if (count > 0){ 500 final byte[] buff = new byte[count]; 501 out.write(buff); 502 count(count); 503 } 504 } 505 506 private void writeBinaryLong(final long number, final int length, 507 final boolean swapHalfWord) throws IOException { 508 final byte[] tmp = CpioUtil.long2byteArray(number, length, swapHalfWord); 509 out.write(tmp); 510 count(tmp.length); 511 } 512 513 private void writeAsciiLong(final long number, final int length, 514 final int radix) throws IOException { 515 final StringBuilder tmp = new StringBuilder(); 516 final String tmpStr; 517 if (radix == 16) { 518 tmp.append(Long.toHexString(number)); 519 } else if (radix == 8) { 520 tmp.append(Long.toOctalString(number)); 521 } else { 522 tmp.append(number); 523 } 524 525 if (tmp.length() <= length) { 526 final int insertLength = length - tmp.length(); 527 for (int pos = 0; pos < insertLength; pos++) { 528 tmp.insert(0, "0"); 529 } 530 tmpStr = tmp.toString(); 531 } else { 532 tmpStr = tmp.substring(tmp.length() - length); 533 } 534 final byte[] b = ArchiveUtils.toAsciiBytes(tmpStr); 535 out.write(b); 536 count(b.length); 537 } 538 539 /** 540 * Encodes the given string using the configured encoding. 541 * 542 * @param str the String to write 543 * @throws IOException if the string couldn't be written 544 * @return result of encoding the string 545 */ 546 private byte[] encode(final String str) throws IOException { 547 final ByteBuffer buf = zipEncoding.encode(str); 548 final int len = buf.limit() - buf.position(); 549 return Arrays.copyOfRange(buf.array(), buf.arrayOffset(), buf.arrayOffset() + len); 550 } 551 552 /** 553 * Writes an encoded string to the stream followed by \0 554 * @param str the String to write 555 * @throws IOException if the string couldn't be written 556 */ 557 private void writeCString(final byte[] str) throws IOException { 558 out.write(str); 559 out.write('\0'); 560 count(str.length + 1); 561 } 562 563 /** 564 * Creates a new ArchiveEntry. The entryName must be an ASCII encoded string. 565 * 566 * @see org.apache.commons.compress.archivers.ArchiveOutputStream#createArchiveEntry(java.io.File, String) 567 */ 568 @Override 569 public ArchiveEntry createArchiveEntry(final File inputFile, final String entryName) 570 throws IOException { 571 if(finished) { 572 throw new IOException("Stream has already been finished"); 573 } 574 return new CpioArchiveEntry(inputFile, entryName); 575 } 576 577 /** 578 * Creates a new ArchiveEntry. The entryName must be an ASCII encoded string. 579 * 580 * @see org.apache.commons.compress.archivers.ArchiveOutputStream#createArchiveEntry(java.io.File, String) 581 */ 582 @Override 583 public ArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) 584 throws IOException { 585 if(finished) { 586 throw new IOException("Stream has already been finished"); 587 } 588 return new CpioArchiveEntry(inputPath, entryName, options); 589 } 590 591}