1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, 13 * software distributed under the License is distributed on an 14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 * KIND, either express or implied. See the License for the 16 * specific language governing permissions and limitations 17 * under the License. 18 */ 19 package org.apache.commons.compress.archivers.cpio; 20 21 import java.io.File; 22 import java.io.IOException; 23 import java.io.OutputStream; 24 import java.nio.ByteBuffer; 25 import java.util.Arrays; 26 import java.util.HashMap; 27 28 import org.apache.commons.compress.archivers.ArchiveEntry; 29 import org.apache.commons.compress.archivers.ArchiveOutputStream; 30 import org.apache.commons.compress.archivers.zip.ZipEncoding; 31 import org.apache.commons.compress.archivers.zip.ZipEncodingHelper; 32 import org.apache.commons.compress.utils.ArchiveUtils; 33 import org.apache.commons.compress.utils.CharsetNames; 34 35 /** 36 * CpioArchiveOutputStream is a stream for writing CPIO streams. All formats of 37 * CPIO are supported (old ASCII, old binary, new portable format and the new 38 * portable format with CRC). 39 * 40 * <p>An entry can be written by creating an instance of CpioArchiveEntry and fill 41 * it with the necessary values and put it into the CPIO stream. Afterwards 42 * write the contents of the file into the CPIO stream. Either close the stream 43 * by calling finish() or put a next entry into the cpio stream.</p> 44 * 45 * <pre> 46 * CpioArchiveOutputStream out = new CpioArchiveOutputStream( 47 * new FileOutputStream(new File("test.cpio"))); 48 * CpioArchiveEntry entry = new CpioArchiveEntry(); 49 * entry.setName("testfile"); 50 * String contents = "12345"; 51 * entry.setFileSize(contents.length()); 52 * entry.setMode(CpioConstants.C_ISREG); // regular file 53 * ... set other attributes, e.g. time, number of links 54 * out.putArchiveEntry(entry); 55 * out.write(testContents.getBytes()); 56 * out.close(); 57 * </pre> 58 * 59 * <p>Note: This implementation should be compatible to cpio 2.5</p> 60 * 61 * <p>This class uses mutable fields and is not considered threadsafe.</p> 62 * 63 * <p>based on code from the jRPM project (jrpm.sourceforge.net)</p> 64 */ 65 public class CpioArchiveOutputStream extends ArchiveOutputStream implements 66 CpioConstants { 67 68 private CpioArchiveEntry entry; 69 70 private boolean closed = false; 71 72 /** indicates if this archive is finished */ 73 private boolean finished; 74 75 /** 76 * See {@link CpioArchiveEntry#setFormat(short)} for possible values. 77 */ 78 private final short entryFormat; 79 80 private final HashMap<String, CpioArchiveEntry> names = 81 new HashMap<>(); 82 83 private long crc = 0; 84 85 private long written; 86 87 private final OutputStream out; 88 89 private final int blockSize; 90 91 private long nextArtificalDeviceAndInode = 1; 92 93 /** 94 * The encoding to use for filenames and labels. 95 */ 96 private final ZipEncoding zipEncoding; 97 98 // the provided encoding (for unit tests) 99 final String encoding; 100 101 /** 102 * Construct the cpio output stream with a specified format, a 103 * blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE} and 104 * using ASCII as the file name encoding. 105 * 106 * @param out 107 * The cpio stream 108 * @param format 109 * The format of the stream 110 */ CpioArchiveOutputStream(final OutputStream out, final short format)111 public CpioArchiveOutputStream(final OutputStream out, final short format) { 112 this(out, format, BLOCK_SIZE, CharsetNames.US_ASCII); 113 } 114 115 /** 116 * Construct the cpio output stream with a specified format using 117 * ASCII as the file name encoding. 118 * 119 * @param out 120 * The cpio stream 121 * @param format 122 * The format of the stream 123 * @param blockSize 124 * The block size of the archive. 125 * 126 * @since 1.1 127 */ CpioArchiveOutputStream(final OutputStream out, final short format, final int blockSize)128 public CpioArchiveOutputStream(final OutputStream out, final short format, 129 final int blockSize) { 130 this(out, format, blockSize, CharsetNames.US_ASCII); 131 } 132 133 /** 134 * Construct the cpio output stream with a specified format using 135 * ASCII as the file name encoding. 136 * 137 * @param out 138 * The cpio stream 139 * @param format 140 * The format of the stream 141 * @param blockSize 142 * The block size of the archive. 143 * @param encoding 144 * The encoding of file names to write - use null for 145 * the platform's default. 146 * 147 * @since 1.6 148 */ CpioArchiveOutputStream(final OutputStream out, final short format, final int blockSize, final String encoding)149 public CpioArchiveOutputStream(final OutputStream out, final short format, 150 final int blockSize, final String encoding) { 151 this.out = out; 152 switch (format) { 153 case FORMAT_NEW: 154 case FORMAT_NEW_CRC: 155 case FORMAT_OLD_ASCII: 156 case FORMAT_OLD_BINARY: 157 break; 158 default: 159 throw new IllegalArgumentException("Unknown format: "+format); 160 161 } 162 this.entryFormat = format; 163 this.blockSize = blockSize; 164 this.encoding = encoding; 165 this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); 166 } 167 168 /** 169 * Construct the cpio output stream. The format for this CPIO stream is the 170 * "new" format using ASCII encoding for file names 171 * 172 * @param out 173 * The cpio stream 174 */ CpioArchiveOutputStream(final OutputStream out)175 public CpioArchiveOutputStream(final OutputStream out) { 176 this(out, FORMAT_NEW); 177 } 178 179 /** 180 * Construct 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 */ CpioArchiveOutputStream(final OutputStream out, final String encoding)190 public CpioArchiveOutputStream(final OutputStream out, final String encoding) { 191 this(out, FORMAT_NEW, BLOCK_SIZE, encoding); 192 } 193 194 /** 195 * Check to make sure that this stream has not been closed 196 * 197 * @throws IOException 198 * if the stream is already closed 199 */ ensureOpen()200 private void ensureOpen() throws IOException { 201 if (this.closed) { 202 throw new IOException("Stream closed"); 203 } 204 } 205 206 /** 207 * Begins writing a new CPIO file entry and positions the stream to the 208 * start of the entry data. Closes the current entry if still active. The 209 * current time will be used if the entry has no set modification time and 210 * the default header format will be used if no other format is specified in 211 * the entry. 212 * 213 * @param entry 214 * the CPIO cpioEntry to be written 215 * @throws IOException 216 * if an I/O error has occurred or if a CPIO file error has 217 * occurred 218 * @throws ClassCastException if entry is not an instance of CpioArchiveEntry 219 */ 220 @Override putArchiveEntry(final ArchiveEntry entry)221 public void putArchiveEntry(final ArchiveEntry entry) throws IOException { 222 if(finished) { 223 throw new IOException("Stream has already been finished"); 224 } 225 226 final CpioArchiveEntry e = (CpioArchiveEntry) entry; 227 ensureOpen(); 228 if (this.entry != null) { 229 closeArchiveEntry(); // close previous entry 230 } 231 if (e.getTime() == -1) { 232 e.setTime(System.currentTimeMillis() / 1000); 233 } 234 235 final short format = e.getFormat(); 236 if (format != this.entryFormat){ 237 throw new IOException("Header format: "+format+" does not match existing format: "+this.entryFormat); 238 } 239 240 if (this.names.put(e.getName(), e) != null) { 241 throw new IOException("duplicate entry: " + e.getName()); 242 } 243 244 writeHeader(e); 245 this.entry = e; 246 this.written = 0; 247 } 248 writeHeader(final CpioArchiveEntry e)249 private void writeHeader(final CpioArchiveEntry e) throws IOException { 250 switch (e.getFormat()) { 251 case FORMAT_NEW: 252 out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW)); 253 count(6); 254 writeNewEntry(e); 255 break; 256 case FORMAT_NEW_CRC: 257 out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW_CRC)); 258 count(6); 259 writeNewEntry(e); 260 break; 261 case FORMAT_OLD_ASCII: 262 out.write(ArchiveUtils.toAsciiBytes(MAGIC_OLD_ASCII)); 263 count(6); 264 writeOldAsciiEntry(e); 265 break; 266 case FORMAT_OLD_BINARY: 267 final boolean swapHalfWord = true; 268 writeBinaryLong(MAGIC_OLD_BINARY, 2, swapHalfWord); 269 writeOldBinaryEntry(e, swapHalfWord); 270 break; 271 default: 272 throw new IOException("unknown format " + e.getFormat()); 273 } 274 } 275 writeNewEntry(final CpioArchiveEntry entry)276 private void writeNewEntry(final CpioArchiveEntry entry) throws IOException { 277 long inode = entry.getInode(); 278 long devMin = entry.getDeviceMin(); 279 if (CPIO_TRAILER.equals(entry.getName())) { 280 inode = devMin = 0; 281 } else { 282 if (inode == 0 && devMin == 0) { 283 inode = nextArtificalDeviceAndInode & 0xFFFFFFFF; 284 devMin = (nextArtificalDeviceAndInode++ >> 32) & 0xFFFFFFFF; 285 } else { 286 nextArtificalDeviceAndInode = 287 Math.max(nextArtificalDeviceAndInode, 288 inode + 0x100000000L * devMin) + 1; 289 } 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 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 writeOldAsciiEntry(final CpioArchiveEntry entry)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 { 317 if (inode == 0 && device == 0) { 318 inode = nextArtificalDeviceAndInode & 0777777; 319 device = (nextArtificalDeviceAndInode++ >> 18) & 0777777; 320 } else { 321 nextArtificalDeviceAndInode = 322 Math.max(nextArtificalDeviceAndInode, 323 inode + 01000000 * device) + 1; 324 } 325 } 326 327 writeAsciiLong(device, 6, 8); 328 writeAsciiLong(inode, 6, 8); 329 writeAsciiLong(entry.getMode(), 6, 8); 330 writeAsciiLong(entry.getUID(), 6, 8); 331 writeAsciiLong(entry.getGID(), 6, 8); 332 writeAsciiLong(entry.getNumberOfLinks(), 6, 8); 333 writeAsciiLong(entry.getRemoteDevice(), 6, 8); 334 writeAsciiLong(entry.getTime(), 11, 8); 335 byte[] name = encode(entry.getName()); 336 writeAsciiLong(name.length + 1L, 6, 8); 337 writeAsciiLong(entry.getSize(), 11, 8); 338 writeCString(name); 339 } 340 writeOldBinaryEntry(final CpioArchiveEntry entry, final boolean swapHalfWord)341 private void writeOldBinaryEntry(final CpioArchiveEntry entry, 342 final boolean swapHalfWord) throws IOException { 343 long inode = entry.getInode(); 344 long device = entry.getDevice(); 345 if (CPIO_TRAILER.equals(entry.getName())) { 346 inode = device = 0; 347 } else { 348 if (inode == 0 && device == 0) { 349 inode = nextArtificalDeviceAndInode & 0xFFFF; 350 device = (nextArtificalDeviceAndInode++ >> 16) & 0xFFFF; 351 } else { 352 nextArtificalDeviceAndInode = 353 Math.max(nextArtificalDeviceAndInode, 354 inode + 0x10000 * device) + 1; 355 } 356 } 357 358 writeBinaryLong(device, 2, swapHalfWord); 359 writeBinaryLong(inode, 2, swapHalfWord); 360 writeBinaryLong(entry.getMode(), 2, swapHalfWord); 361 writeBinaryLong(entry.getUID(), 2, swapHalfWord); 362 writeBinaryLong(entry.getGID(), 2, swapHalfWord); 363 writeBinaryLong(entry.getNumberOfLinks(), 2, swapHalfWord); 364 writeBinaryLong(entry.getRemoteDevice(), 2, swapHalfWord); 365 writeBinaryLong(entry.getTime(), 4, swapHalfWord); 366 byte[] name = encode(entry.getName()); 367 writeBinaryLong(name.length + 1L, 2, swapHalfWord); 368 writeBinaryLong(entry.getSize(), 4, swapHalfWord); 369 writeCString(name); 370 pad(entry.getHeaderPadCount(name.length)); 371 } 372 373 /*(non-Javadoc) 374 * 375 * @see 376 * org.apache.commons.compress.archivers.ArchiveOutputStream#closeArchiveEntry 377 * () 378 */ 379 @Override closeArchiveEntry()380 public void closeArchiveEntry() throws IOException { 381 if(finished) { 382 throw new IOException("Stream has already been finished"); 383 } 384 385 ensureOpen(); 386 387 if (entry == null) { 388 throw new IOException("Trying to close non-existent entry"); 389 } 390 391 if (this.entry.getSize() != this.written) { 392 throw new IOException("invalid entry size (expected " 393 + this.entry.getSize() + " but got " + this.written 394 + " bytes)"); 395 } 396 pad(this.entry.getDataPadCount()); 397 if (this.entry.getFormat() == FORMAT_NEW_CRC 398 && this.crc != this.entry.getChksum()) { 399 throw new IOException("CRC Error"); 400 } 401 this.entry = null; 402 this.crc = 0; 403 this.written = 0; 404 } 405 406 /** 407 * Writes an array of bytes to the current CPIO entry data. This method will 408 * block until all the bytes are written. 409 * 410 * @param b 411 * the data to be written 412 * @param off 413 * the start offset in the data 414 * @param len 415 * the number of bytes that are written 416 * @throws IOException 417 * if an I/O error has occurred or if a CPIO file error has 418 * occurred 419 */ 420 @Override write(final byte[] b, final int off, final int len)421 public void write(final byte[] b, final int off, final int len) 422 throws IOException { 423 ensureOpen(); 424 if (off < 0 || len < 0 || off > b.length - len) { 425 throw new IndexOutOfBoundsException(); 426 } else if (len == 0) { 427 return; 428 } 429 430 if (this.entry == null) { 431 throw new IOException("no current CPIO entry"); 432 } 433 if (this.written + len > this.entry.getSize()) { 434 throw new IOException("attempt to write past end of STORED entry"); 435 } 436 out.write(b, off, len); 437 this.written += len; 438 if (this.entry.getFormat() == FORMAT_NEW_CRC) { 439 for (int pos = 0; pos < len; pos++) { 440 this.crc += b[pos] & 0xFF; 441 this.crc &= 0xFFFFFFFFL; 442 } 443 } 444 count(len); 445 } 446 447 /** 448 * Finishes writing the contents of the CPIO output stream without closing 449 * the underlying stream. Use this method when applying multiple filters in 450 * succession to the same output stream. 451 * 452 * @throws IOException 453 * if an I/O exception has occurred or if a CPIO file error has 454 * occurred 455 */ 456 @Override finish()457 public void finish() throws IOException { 458 ensureOpen(); 459 if (finished) { 460 throw new IOException("This archive has already been finished"); 461 } 462 463 if (this.entry != null) { 464 throw new IOException("This archive contains unclosed entries."); 465 } 466 this.entry = new CpioArchiveEntry(this.entryFormat); 467 this.entry.setName(CPIO_TRAILER); 468 this.entry.setNumberOfLinks(1); 469 writeHeader(this.entry); 470 closeArchiveEntry(); 471 472 final int lengthOfLastBlock = (int) (getBytesWritten() % blockSize); 473 if (lengthOfLastBlock != 0) { 474 pad(blockSize - lengthOfLastBlock); 475 } 476 477 finished = true; 478 } 479 480 /** 481 * Closes the CPIO output stream as well as the stream being filtered. 482 * 483 * @throws IOException 484 * if an I/O error has occurred or if a CPIO file error has 485 * occurred 486 */ 487 @Override close()488 public void close() throws IOException { 489 try { 490 if (!finished) { 491 finish(); 492 } 493 } finally { 494 if (!this.closed) { 495 out.close(); 496 this.closed = true; 497 } 498 } 499 } 500 pad(final int count)501 private void pad(final int count) throws IOException{ 502 if (count > 0){ 503 final byte buff[] = new byte[count]; 504 out.write(buff); 505 count(count); 506 } 507 } 508 writeBinaryLong(final long number, final int length, final boolean swapHalfWord)509 private void writeBinaryLong(final long number, final int length, 510 final boolean swapHalfWord) throws IOException { 511 final byte tmp[] = CpioUtil.long2byteArray(number, length, swapHalfWord); 512 out.write(tmp); 513 count(tmp.length); 514 } 515 writeAsciiLong(final long number, final int length, final int radix)516 private void writeAsciiLong(final long number, final int length, 517 final int radix) throws IOException { 518 final StringBuilder tmp = new StringBuilder(); 519 String tmpStr; 520 if (radix == 16) { 521 tmp.append(Long.toHexString(number)); 522 } else if (radix == 8) { 523 tmp.append(Long.toOctalString(number)); 524 } else { 525 tmp.append(Long.toString(number)); 526 } 527 528 if (tmp.length() <= length) { 529 final int insertLength = length - tmp.length(); 530 for (int pos = 0; pos < insertLength; pos++) { 531 tmp.insert(0, "0"); 532 } 533 tmpStr = tmp.toString(); 534 } else { 535 tmpStr = tmp.substring(tmp.length() - length); 536 } 537 final byte[] b = ArchiveUtils.toAsciiBytes(tmpStr); 538 out.write(b); 539 count(b.length); 540 } 541 542 /** 543 * Encodes the given string using the configured encoding. 544 * 545 * @param str the String to write 546 * @throws IOException if the string couldn't be written 547 * @return result of encoding the string 548 */ encode(final String str)549 private byte[] encode(final String str) throws IOException { 550 final ByteBuffer buf = zipEncoding.encode(str); 551 final int len = buf.limit() - buf.position(); 552 return Arrays.copyOfRange(buf.array(), buf.arrayOffset(), buf.arrayOffset() + len); 553 } 554 555 /** 556 * Writes an encoded string to the stream followed by \0 557 * @param str the String to write 558 * @throws IOException if the string couldn't be written 559 */ writeCString(byte[] str)560 private void writeCString(byte[] str) throws IOException { 561 out.write(str); 562 out.write('\0'); 563 count(str.length + 1); 564 } 565 566 /** 567 * Creates a new ArchiveEntry. The entryName must be an ASCII encoded string. 568 * 569 * @see org.apache.commons.compress.archivers.ArchiveOutputStream#createArchiveEntry(java.io.File, java.lang.String) 570 */ 571 @Override createArchiveEntry(final File inputFile, final String entryName)572 public ArchiveEntry createArchiveEntry(final File inputFile, final String entryName) 573 throws IOException { 574 if(finished) { 575 throw new IOException("Stream has already been finished"); 576 } 577 return new CpioArchiveEntry(inputFile, entryName); 578 } 579 580 } 581