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<>(); 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 zipEncoding; 096 097 // the provided encoding (for unit tests) 098 final String encoding; 099 100 /** 101 * Construct the cpio output stream with a specified format, a 102 * blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE} and 103 * using ASCII as the file name encoding. 104 * 105 * @param out 106 * The cpio stream 107 * @param format 108 * The format of the stream 109 */ 110 public CpioArchiveOutputStream(final OutputStream out, final short format) { 111 this(out, format, BLOCK_SIZE, CharsetNames.US_ASCII); 112 } 113 114 /** 115 * Construct the cpio output stream with a specified format using 116 * ASCII as the file name encoding. 117 * 118 * @param out 119 * The cpio stream 120 * @param format 121 * The format of the stream 122 * @param blockSize 123 * The block size of the archive. 124 * 125 * @since 1.1 126 */ 127 public CpioArchiveOutputStream(final OutputStream out, final short format, 128 final int blockSize) { 129 this(out, format, blockSize, CharsetNames.US_ASCII); 130 } 131 132 /** 133 * Construct the cpio output stream with a specified format using 134 * ASCII as the file name encoding. 135 * 136 * @param out 137 * The cpio stream 138 * @param format 139 * The format of the stream 140 * @param blockSize 141 * The block size of the archive. 142 * @param encoding 143 * The encoding of file names to write - use null for 144 * the platform's default. 145 * 146 * @since 1.6 147 */ 148 public CpioArchiveOutputStream(final OutputStream out, final short format, 149 final int blockSize, final String encoding) { 150 this.out = out; 151 switch (format) { 152 case FORMAT_NEW: 153 case FORMAT_NEW_CRC: 154 case FORMAT_OLD_ASCII: 155 case FORMAT_OLD_BINARY: 156 break; 157 default: 158 throw new IllegalArgumentException("Unknown format: "+format); 159 160 } 161 this.entryFormat = format; 162 this.blockSize = blockSize; 163 this.encoding = encoding; 164 this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); 165 } 166 167 /** 168 * Construct the cpio output stream. The format for this CPIO stream is the 169 * "new" format using ASCII encoding for file names 170 * 171 * @param out 172 * The cpio stream 173 */ 174 public CpioArchiveOutputStream(final OutputStream out) { 175 this(out, FORMAT_NEW); 176 } 177 178 /** 179 * Construct the cpio output stream. The format for this CPIO stream is the 180 * "new" format. 181 * 182 * @param out 183 * The cpio stream 184 * @param encoding 185 * The encoding of file names to write - use null for 186 * the platform's default. 187 * @since 1.6 188 */ 189 public CpioArchiveOutputStream(final OutputStream out, final String encoding) { 190 this(out, FORMAT_NEW, BLOCK_SIZE, encoding); 191 } 192 193 /** 194 * Check to make sure that this stream has not been closed 195 * 196 * @throws IOException 197 * if the stream is already closed 198 */ 199 private void ensureOpen() throws IOException { 200 if (this.closed) { 201 throw new IOException("Stream closed"); 202 } 203 } 204 205 /** 206 * Begins writing a new CPIO file entry and positions the stream to the 207 * start of the entry data. Closes the current entry if still active. The 208 * current time will be used if the entry has no set modification time and 209 * the default header format will be used if no other format is specified in 210 * the entry. 211 * 212 * @param entry 213 * the CPIO cpioEntry to be written 214 * @throws IOException 215 * if an I/O error has occurred or if a CPIO file error has 216 * occurred 217 * @throws ClassCastException if entry is not an instance of CpioArchiveEntry 218 */ 219 @Override 220 public void putArchiveEntry(final ArchiveEntry entry) throws IOException { 221 if(finished) { 222 throw new IOException("Stream has already been finished"); 223 } 224 225 final CpioArchiveEntry e = (CpioArchiveEntry) entry; 226 ensureOpen(); 227 if (this.entry != null) { 228 closeArchiveEntry(); // close previous entry 229 } 230 if (e.getTime() == -1) { 231 e.setTime(System.currentTimeMillis() / 1000); 232 } 233 234 final short format = e.getFormat(); 235 if (format != this.entryFormat){ 236 throw new IOException("Header format: "+format+" does not match existing format: "+this.entryFormat); 237 } 238 239 if (this.names.put(e.getName(), e) != null) { 240 throw new IOException("duplicate entry: " + e.getName()); 241 } 242 243 writeHeader(e); 244 this.entry = e; 245 this.written = 0; 246 } 247 248 private void writeHeader(final CpioArchiveEntry e) throws IOException { 249 switch (e.getFormat()) { 250 case FORMAT_NEW: 251 out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW)); 252 count(6); 253 writeNewEntry(e); 254 break; 255 case FORMAT_NEW_CRC: 256 out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW_CRC)); 257 count(6); 258 writeNewEntry(e); 259 break; 260 case FORMAT_OLD_ASCII: 261 out.write(ArchiveUtils.toAsciiBytes(MAGIC_OLD_ASCII)); 262 count(6); 263 writeOldAsciiEntry(e); 264 break; 265 case FORMAT_OLD_BINARY: 266 final boolean swapHalfWord = true; 267 writeBinaryLong(MAGIC_OLD_BINARY, 2, swapHalfWord); 268 writeOldBinaryEntry(e, swapHalfWord); 269 break; 270 default: 271 throw new IOException("unknown format " + e.getFormat()); 272 } 273 } 274 275 private void writeNewEntry(final CpioArchiveEntry entry) throws IOException { 276 long inode = entry.getInode(); 277 long devMin = entry.getDeviceMin(); 278 if (CPIO_TRAILER.equals(entry.getName())) { 279 inode = devMin = 0; 280 } else { 281 if (inode == 0 && devMin == 0) { 282 inode = nextArtificalDeviceAndInode & 0xFFFFFFFF; 283 devMin = (nextArtificalDeviceAndInode++ >> 32) & 0xFFFFFFFF; 284 } else { 285 nextArtificalDeviceAndInode = 286 Math.max(nextArtificalDeviceAndInode, 287 inode + 0x100000000L * devMin) + 1; 288 } 289 } 290 291 writeAsciiLong(inode, 8, 16); 292 writeAsciiLong(entry.getMode(), 8, 16); 293 writeAsciiLong(entry.getUID(), 8, 16); 294 writeAsciiLong(entry.getGID(), 8, 16); 295 writeAsciiLong(entry.getNumberOfLinks(), 8, 16); 296 writeAsciiLong(entry.getTime(), 8, 16); 297 writeAsciiLong(entry.getSize(), 8, 16); 298 writeAsciiLong(entry.getDeviceMaj(), 8, 16); 299 writeAsciiLong(devMin, 8, 16); 300 writeAsciiLong(entry.getRemoteDeviceMaj(), 8, 16); 301 writeAsciiLong(entry.getRemoteDeviceMin(), 8, 16); 302 writeAsciiLong(entry.getName().length() + 1L, 8, 16); 303 writeAsciiLong(entry.getChksum(), 8, 16); 304 writeCString(entry.getName()); 305 pad(entry.getHeaderPadCount()); 306 } 307 308 private void writeOldAsciiEntry(final CpioArchiveEntry entry) 309 throws IOException { 310 long inode = entry.getInode(); 311 long device = entry.getDevice(); 312 if (CPIO_TRAILER.equals(entry.getName())) { 313 inode = device = 0; 314 } else { 315 if (inode == 0 && device == 0) { 316 inode = nextArtificalDeviceAndInode & 0777777; 317 device = (nextArtificalDeviceAndInode++ >> 18) & 0777777; 318 } else { 319 nextArtificalDeviceAndInode = 320 Math.max(nextArtificalDeviceAndInode, 321 inode + 01000000 * device) + 1; 322 } 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 writeAsciiLong(entry.getName().length() + 1L, 6, 8); 334 writeAsciiLong(entry.getSize(), 11, 8); 335 writeCString(entry.getName()); 336 } 337 338 private void writeOldBinaryEntry(final CpioArchiveEntry entry, 339 final boolean swapHalfWord) throws IOException { 340 long inode = entry.getInode(); 341 long device = entry.getDevice(); 342 if (CPIO_TRAILER.equals(entry.getName())) { 343 inode = device = 0; 344 } else { 345 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 355 writeBinaryLong(device, 2, swapHalfWord); 356 writeBinaryLong(inode, 2, swapHalfWord); 357 writeBinaryLong(entry.getMode(), 2, swapHalfWord); 358 writeBinaryLong(entry.getUID(), 2, swapHalfWord); 359 writeBinaryLong(entry.getGID(), 2, swapHalfWord); 360 writeBinaryLong(entry.getNumberOfLinks(), 2, swapHalfWord); 361 writeBinaryLong(entry.getRemoteDevice(), 2, swapHalfWord); 362 writeBinaryLong(entry.getTime(), 4, swapHalfWord); 363 writeBinaryLong(entry.getName().length() + 1L, 2, swapHalfWord); 364 writeBinaryLong(entry.getSize(), 4, swapHalfWord); 365 writeCString(entry.getName()); 366 pad(entry.getHeaderPadCount()); 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 } else if (len == 0) { 423 return; 424 } 425 426 if (this.entry == null) { 427 throw new IOException("no current CPIO entry"); 428 } 429 if (this.written + len > this.entry.getSize()) { 430 throw new IOException("attempt to write past end of STORED entry"); 431 } 432 out.write(b, off, len); 433 this.written += len; 434 if (this.entry.getFormat() == FORMAT_NEW_CRC) { 435 for (int pos = 0; pos < len; pos++) { 436 this.crc += b[pos] & 0xFF; 437 this.crc &= 0xFFFFFFFFL; 438 } 439 } 440 count(len); 441 } 442 443 /** 444 * Finishes writing the contents of the CPIO output stream without closing 445 * the underlying stream. Use this method when applying multiple filters in 446 * succession to the same output stream. 447 * 448 * @throws IOException 449 * if an I/O exception has occurred or if a CPIO file error has 450 * occurred 451 */ 452 @Override 453 public void finish() throws IOException { 454 ensureOpen(); 455 if (finished) { 456 throw new IOException("This archive has already been finished"); 457 } 458 459 if (this.entry != null) { 460 throw new IOException("This archive contains unclosed entries."); 461 } 462 this.entry = new CpioArchiveEntry(this.entryFormat); 463 this.entry.setName(CPIO_TRAILER); 464 this.entry.setNumberOfLinks(1); 465 writeHeader(this.entry); 466 closeArchiveEntry(); 467 468 final int lengthOfLastBlock = (int) (getBytesWritten() % blockSize); 469 if (lengthOfLastBlock != 0) { 470 pad(blockSize - lengthOfLastBlock); 471 } 472 473 finished = true; 474 } 475 476 /** 477 * Closes the CPIO output stream as well as the stream being filtered. 478 * 479 * @throws IOException 480 * if an I/O error has occurred or if a CPIO file error has 481 * occurred 482 */ 483 @Override 484 public void close() throws IOException { 485 if(!finished) { 486 finish(); 487 } 488 489 if (!this.closed) { 490 out.close(); 491 this.closed = true; 492 } 493 } 494 495 private void pad(final int count) throws IOException{ 496 if (count > 0){ 497 final byte buff[] = new byte[count]; 498 out.write(buff); 499 count(count); 500 } 501 } 502 503 private void writeBinaryLong(final long number, final int length, 504 final boolean swapHalfWord) throws IOException { 505 final byte tmp[] = CpioUtil.long2byteArray(number, length, swapHalfWord); 506 out.write(tmp); 507 count(tmp.length); 508 } 509 510 private void writeAsciiLong(final long number, final int length, 511 final int radix) throws IOException { 512 final StringBuilder tmp = new StringBuilder(); 513 String tmpStr; 514 if (radix == 16) { 515 tmp.append(Long.toHexString(number)); 516 } else if (radix == 8) { 517 tmp.append(Long.toOctalString(number)); 518 } else { 519 tmp.append(Long.toString(number)); 520 } 521 522 if (tmp.length() <= length) { 523 final int insertLength = length - tmp.length(); 524 for (int pos = 0; pos < insertLength; pos++) { 525 tmp.insert(0, "0"); 526 } 527 tmpStr = tmp.toString(); 528 } else { 529 tmpStr = tmp.substring(tmp.length() - length); 530 } 531 final byte[] b = ArchiveUtils.toAsciiBytes(tmpStr); 532 out.write(b); 533 count(b.length); 534 } 535 536 /** 537 * Writes an ASCII string to the stream followed by \0 538 * @param str the String to write 539 * @throws IOException if the string couldn't be written 540 */ 541 private void writeCString(final String str) throws IOException { 542 final ByteBuffer buf = zipEncoding.encode(str); 543 final int len = buf.limit() - buf.position(); 544 out.write(buf.array(), buf.arrayOffset(), len); 545 out.write('\0'); 546 count(len + 1); 547 } 548 549 /** 550 * Creates a new ArchiveEntry. The entryName must be an ASCII encoded string. 551 * 552 * @see org.apache.commons.compress.archivers.ArchiveOutputStream#createArchiveEntry(java.io.File, java.lang.String) 553 */ 554 @Override 555 public ArchiveEntry createArchiveEntry(final File inputFile, final String entryName) 556 throws IOException { 557 if(finished) { 558 throw new IOException("Stream has already been finished"); 559 } 560 return new CpioArchiveEntry(inputFile, entryName); 561 } 562 563}