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.EOFException; 022import java.io.IOException; 023import java.io.InputStream; 024 025import org.apache.commons.compress.archivers.ArchiveEntry; 026import org.apache.commons.compress.archivers.ArchiveInputStream; 027import org.apache.commons.compress.archivers.zip.ZipEncoding; 028import org.apache.commons.compress.archivers.zip.ZipEncodingHelper; 029import org.apache.commons.compress.utils.ArchiveUtils; 030import org.apache.commons.compress.utils.CharsetNames; 031import org.apache.commons.compress.utils.IOUtils; 032 033/** 034 * CpioArchiveInputStream is a stream for reading cpio streams. All formats of 035 * cpio are supported (old ascii, old binary, new portable format and the new 036 * portable format with crc). 037 * 038 * <p> 039 * The stream can be read by extracting a cpio entry (containing all 040 * information about an entry) and afterwards reading from the stream the file 041 * specified by the entry. 042 * </p> 043 * <pre> 044 * CpioArchiveInputStream cpioIn = new CpioArchiveInputStream( 045 * Files.newInputStream(Paths.get("test.cpio"))); 046 * CpioArchiveEntry cpioEntry; 047 * 048 * while ((cpioEntry = cpioIn.getNextEntry()) != null) { 049 * System.out.println(cpioEntry.getName()); 050 * int tmp; 051 * StringBuilder buf = new StringBuilder(); 052 * while ((tmp = cpIn.read()) != -1) { 053 * buf.append((char) tmp); 054 * } 055 * System.out.println(buf.toString()); 056 * } 057 * cpioIn.close(); 058 * </pre> 059 * <p> 060 * Note: This implementation should be compatible to cpio 2.5 061 * 062 * <p>This class uses mutable fields and is not considered to be threadsafe. 063 * 064 * <p>Based on code from the jRPM project (jrpm.sourceforge.net) 065 */ 066 067public class CpioArchiveInputStream extends ArchiveInputStream implements 068 CpioConstants { 069 070 /** 071 * Checks if the signature matches one of the following magic values: 072 * 073 * Strings: 074 * 075 * "070701" - MAGIC_NEW 076 * "070702" - MAGIC_NEW_CRC 077 * "070707" - MAGIC_OLD_ASCII 078 * 079 * Octal Binary value: 080 * 081 * 070707 - MAGIC_OLD_BINARY (held as a short) = 0x71C7 or 0xC771 082 * @param signature data to match 083 * @param length length of data 084 * @return whether the buffer seems to contain CPIO data 085 */ 086 public static boolean matches(final byte[] signature, final int length) { 087 if (length < 6) { 088 return false; 089 } 090 091 // Check binary values 092 if (signature[0] == 0x71 && (signature[1] & 0xFF) == 0xc7) { 093 return true; 094 } 095 if (signature[1] == 0x71 && (signature[0] & 0xFF) == 0xc7) { 096 return true; 097 } 098 099 // Check Ascii (String) values 100 // 3037 3037 30nn 101 if (signature[0] != 0x30) { 102 return false; 103 } 104 if (signature[1] != 0x37) { 105 return false; 106 } 107 if (signature[2] != 0x30) { 108 return false; 109 } 110 if (signature[3] != 0x37) { 111 return false; 112 } 113 if (signature[4] != 0x30) { 114 return false; 115 } 116 // Check last byte 117 if (signature[5] == 0x31) { 118 return true; 119 } 120 if (signature[5] == 0x32) { 121 return true; 122 } 123 if (signature[5] == 0x37) { 124 return true; 125 } 126 127 return false; 128 } 129 130 private boolean closed; 131 132 private CpioArchiveEntry entry; 133 134 private long entryBytesRead; 135 136 private boolean entryEOF; 137 138 private final byte[] tmpbuf = new byte[4096]; 139 140 private long crc; 141 142 private final InputStream in; 143 // cached buffers - must only be used locally in the class (COMPRESS-172 - reduce garbage collection) 144 private final byte[] twoBytesBuf = new byte[2]; 145 private final byte[] fourBytesBuf = new byte[4]; 146 147 private final byte[] sixBytesBuf = new byte[6]; 148 149 private final int blockSize; 150 151 /** 152 * The encoding to use for file names and labels. 153 */ 154 private final ZipEncoding zipEncoding; 155 156 // the provided encoding (for unit tests) 157 final String encoding; 158 159 /** 160 * Construct the cpio input stream with a blocksize of {@link 161 * CpioConstants#BLOCK_SIZE BLOCK_SIZE} and expecting ASCII file 162 * names. 163 * 164 * @param in 165 * The cpio stream 166 */ 167 public CpioArchiveInputStream(final InputStream in) { 168 this(in, BLOCK_SIZE, CharsetNames.US_ASCII); 169 } 170 171 /** 172 * Construct the cpio input stream with a blocksize of {@link 173 * CpioConstants#BLOCK_SIZE BLOCK_SIZE} expecting ASCII file 174 * names. 175 * 176 * @param in 177 * The cpio stream 178 * @param blockSize 179 * The block size of the archive. 180 * @since 1.5 181 */ 182 public CpioArchiveInputStream(final InputStream in, final int blockSize) { 183 this(in, blockSize, CharsetNames.US_ASCII); 184 } 185 186 /** 187 * Construct the cpio input stream with a blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE}. 188 * 189 * @param in 190 * The cpio stream 191 * @param blockSize 192 * The block size of the archive. 193 * @param encoding 194 * The encoding of file names to expect - use null for 195 * the platform's default. 196 * @throws IllegalArgumentException if {@code blockSize} is not bigger than 0 197 * @since 1.6 198 */ 199 public CpioArchiveInputStream(final InputStream in, final int blockSize, final String encoding) { 200 this.in = in; 201 if (blockSize <= 0) { 202 throw new IllegalArgumentException("blockSize must be bigger than 0"); 203 } 204 this.blockSize = blockSize; 205 this.encoding = encoding; 206 this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); 207 } 208 209 /** 210 * Construct the cpio input stream with a blocksize of {@link 211 * CpioConstants#BLOCK_SIZE BLOCK_SIZE}. 212 * 213 * @param in 214 * The cpio stream 215 * @param encoding 216 * The encoding of file names to expect - use null for 217 * the platform's default. 218 * @since 1.6 219 */ 220 public CpioArchiveInputStream(final InputStream in, final String encoding) { 221 this(in, BLOCK_SIZE, encoding); 222 } 223 224 /** 225 * Returns 0 after EOF has reached for the current entry data, otherwise 226 * always return 1. 227 * <p> 228 * Programs should not count on this method to return the actual number of 229 * bytes that could be read without blocking. 230 * 231 * @return 1 before EOF and 0 after EOF has reached for current entry. 232 * @throws IOException 233 * if an I/O error has occurred or if a CPIO file error has 234 * occurred 235 */ 236 @Override 237 public int available() throws IOException { 238 ensureOpen(); 239 if (this.entryEOF) { 240 return 0; 241 } 242 return 1; 243 } 244 245 /** 246 * Closes the CPIO input stream. 247 * 248 * @throws IOException 249 * if an I/O error has occurred 250 */ 251 @Override 252 public void close() throws IOException { 253 if (!this.closed) { 254 in.close(); 255 this.closed = true; 256 } 257 } 258 259 /** 260 * Closes the current CPIO entry and positions the stream for reading the 261 * next entry. 262 * 263 * @throws IOException 264 * if an I/O error has occurred or if a CPIO file error has 265 * occurred 266 */ 267 private void closeEntry() throws IOException { 268 // the skip implementation of this class will not skip more 269 // than Integer.MAX_VALUE bytes 270 while (skip((long) Integer.MAX_VALUE) == Integer.MAX_VALUE) { // NOPMD NOSONAR 271 // do nothing 272 } 273 } 274 275 /** 276 * Check to make sure that this stream has not been closed 277 * 278 * @throws IOException 279 * if the stream is already closed 280 */ 281 private void ensureOpen() throws IOException { 282 if (this.closed) { 283 throw new IOException("Stream closed"); 284 } 285 } 286 287 /** 288 * Reads the next CPIO file entry and positions stream at the beginning of 289 * the entry data. 290 * 291 * @return the CpioArchiveEntry just read 292 * @throws IOException 293 * if an I/O error has occurred or if a CPIO file error has 294 * occurred 295 */ 296 public CpioArchiveEntry getNextCPIOEntry() throws IOException { 297 ensureOpen(); 298 if (this.entry != null) { 299 closeEntry(); 300 } 301 readFully(twoBytesBuf, 0, twoBytesBuf.length); 302 if (CpioUtil.byteArray2long(twoBytesBuf, false) == MAGIC_OLD_BINARY) { 303 this.entry = readOldBinaryEntry(false); 304 } else if (CpioUtil.byteArray2long(twoBytesBuf, true) 305 == MAGIC_OLD_BINARY) { 306 this.entry = readOldBinaryEntry(true); 307 } else { 308 System.arraycopy(twoBytesBuf, 0, sixBytesBuf, 0, 309 twoBytesBuf.length); 310 readFully(sixBytesBuf, twoBytesBuf.length, 311 fourBytesBuf.length); 312 final String magicString = ArchiveUtils.toAsciiString(sixBytesBuf); 313 switch (magicString) { 314 case MAGIC_NEW: 315 this.entry = readNewEntry(false); 316 break; 317 case MAGIC_NEW_CRC: 318 this.entry = readNewEntry(true); 319 break; 320 case MAGIC_OLD_ASCII: 321 this.entry = readOldAsciiEntry(); 322 break; 323 default: 324 throw new IOException("Unknown magic [" + magicString + "]. Occurred at byte: " + getBytesRead()); 325 } 326 } 327 328 this.entryBytesRead = 0; 329 this.entryEOF = false; 330 this.crc = 0; 331 332 if (this.entry.getName().equals(CPIO_TRAILER)) { 333 this.entryEOF = true; 334 skipRemainderOfLastBlock(); 335 return null; 336 } 337 return this.entry; 338 } 339 340 @Override 341 public ArchiveEntry getNextEntry() throws IOException { 342 return getNextCPIOEntry(); 343 } 344 345 /** 346 * Reads from the current CPIO entry into an array of bytes. Blocks until 347 * some input is available. 348 * 349 * @param b 350 * the buffer into which the data is read 351 * @param off 352 * the start offset of the data 353 * @param len 354 * the maximum number of bytes read 355 * @return the actual number of bytes read, or -1 if the end of the entry is 356 * reached 357 * @throws IOException 358 * if an I/O error has occurred or if a CPIO file error has 359 * occurred 360 */ 361 @Override 362 public int read(final byte[] b, final int off, final int len) 363 throws IOException { 364 ensureOpen(); 365 if (off < 0 || len < 0 || off > b.length - len) { 366 throw new IndexOutOfBoundsException(); 367 } 368 if (len == 0) { 369 return 0; 370 } 371 372 if (this.entry == null || this.entryEOF) { 373 return -1; 374 } 375 if (this.entryBytesRead == this.entry.getSize()) { 376 skip(entry.getDataPadCount()); 377 this.entryEOF = true; 378 if (this.entry.getFormat() == FORMAT_NEW_CRC 379 && this.crc != this.entry.getChksum()) { 380 throw new IOException("CRC Error. Occurred at byte: " 381 + getBytesRead()); 382 } 383 return -1; // EOF for this entry 384 } 385 final int tmplength = (int) Math.min(len, this.entry.getSize() 386 - this.entryBytesRead); 387 if (tmplength < 0) { 388 return -1; 389 } 390 391 final int tmpread = readFully(b, off, tmplength); 392 if (this.entry.getFormat() == FORMAT_NEW_CRC) { 393 for (int pos = 0; pos < tmpread; pos++) { 394 this.crc += b[pos] & 0xFF; 395 this.crc &= 0xFFFFFFFFL; 396 } 397 } 398 if (tmpread > 0) { 399 this.entryBytesRead += tmpread; 400 } 401 402 return tmpread; 403 } 404 405 private long readAsciiLong(final int length, final int radix) 406 throws IOException { 407 final byte[] tmpBuffer = readRange(length); 408 return Long.parseLong(ArchiveUtils.toAsciiString(tmpBuffer), radix); 409 } 410 411 private long readBinaryLong(final int length, final boolean swapHalfWord) 412 throws IOException { 413 final byte[] tmp = readRange(length); 414 return CpioUtil.byteArray2long(tmp, swapHalfWord); 415 } 416 417 private String readCString(final int length) throws IOException { 418 // don't include trailing NUL in file name to decode 419 final byte[] tmpBuffer = readRange(length - 1); 420 if (this.in.read() == -1) { 421 throw new EOFException(); 422 } 423 return zipEncoding.decode(tmpBuffer); 424 } 425 426 private final int readFully(final byte[] b, final int off, final int len) 427 throws IOException { 428 final int count = IOUtils.readFully(in, b, off, len); 429 count(count); 430 if (count < len) { 431 throw new EOFException(); 432 } 433 return count; 434 } 435 436 private CpioArchiveEntry readNewEntry(final boolean hasCrc) 437 throws IOException { 438 final CpioArchiveEntry ret; 439 if (hasCrc) { 440 ret = new CpioArchiveEntry(FORMAT_NEW_CRC); 441 } else { 442 ret = new CpioArchiveEntry(FORMAT_NEW); 443 } 444 445 ret.setInode(readAsciiLong(8, 16)); 446 final long mode = readAsciiLong(8, 16); 447 if (CpioUtil.fileType(mode) != 0){ // mode is initialized to 0 448 ret.setMode(mode); 449 } 450 ret.setUID(readAsciiLong(8, 16)); 451 ret.setGID(readAsciiLong(8, 16)); 452 ret.setNumberOfLinks(readAsciiLong(8, 16)); 453 ret.setTime(readAsciiLong(8, 16)); 454 ret.setSize(readAsciiLong(8, 16)); 455 if (ret.getSize() < 0) { 456 throw new IOException("Found illegal entry with negative length"); 457 } 458 ret.setDeviceMaj(readAsciiLong(8, 16)); 459 ret.setDeviceMin(readAsciiLong(8, 16)); 460 ret.setRemoteDeviceMaj(readAsciiLong(8, 16)); 461 ret.setRemoteDeviceMin(readAsciiLong(8, 16)); 462 final long namesize = readAsciiLong(8, 16); 463 if (namesize < 0) { 464 throw new IOException("Found illegal entry with negative name length"); 465 } 466 ret.setChksum(readAsciiLong(8, 16)); 467 final String name = readCString((int) namesize); 468 ret.setName(name); 469 if (CpioUtil.fileType(mode) == 0 && !name.equals(CPIO_TRAILER)){ 470 throw new IOException("Mode 0 only allowed in the trailer. Found entry name: " 471 + ArchiveUtils.sanitize(name) 472 + " Occurred at byte: " + getBytesRead()); 473 } 474 skip(ret.getHeaderPadCount(namesize - 1)); 475 476 return ret; 477 } 478 479 private CpioArchiveEntry readOldAsciiEntry() throws IOException { 480 final CpioArchiveEntry ret = new CpioArchiveEntry(FORMAT_OLD_ASCII); 481 482 ret.setDevice(readAsciiLong(6, 8)); 483 ret.setInode(readAsciiLong(6, 8)); 484 final long mode = readAsciiLong(6, 8); 485 if (CpioUtil.fileType(mode) != 0) { 486 ret.setMode(mode); 487 } 488 ret.setUID(readAsciiLong(6, 8)); 489 ret.setGID(readAsciiLong(6, 8)); 490 ret.setNumberOfLinks(readAsciiLong(6, 8)); 491 ret.setRemoteDevice(readAsciiLong(6, 8)); 492 ret.setTime(readAsciiLong(11, 8)); 493 final long namesize = readAsciiLong(6, 8); 494 if (namesize < 0) { 495 throw new IOException("Found illegal entry with negative name length"); 496 } 497 ret.setSize(readAsciiLong(11, 8)); 498 if (ret.getSize() < 0) { 499 throw new IOException("Found illegal entry with negative length"); 500 } 501 final String name = readCString((int) namesize); 502 ret.setName(name); 503 if (CpioUtil.fileType(mode) == 0 && !name.equals(CPIO_TRAILER)){ 504 throw new IOException("Mode 0 only allowed in the trailer. Found entry: " 505 + ArchiveUtils.sanitize(name) 506 + " Occurred at byte: " + getBytesRead()); 507 } 508 509 return ret; 510 } 511 512 private CpioArchiveEntry readOldBinaryEntry(final boolean swapHalfWord) 513 throws IOException { 514 final CpioArchiveEntry ret = new CpioArchiveEntry(FORMAT_OLD_BINARY); 515 516 ret.setDevice(readBinaryLong(2, swapHalfWord)); 517 ret.setInode(readBinaryLong(2, swapHalfWord)); 518 final long mode = readBinaryLong(2, swapHalfWord); 519 if (CpioUtil.fileType(mode) != 0){ 520 ret.setMode(mode); 521 } 522 ret.setUID(readBinaryLong(2, swapHalfWord)); 523 ret.setGID(readBinaryLong(2, swapHalfWord)); 524 ret.setNumberOfLinks(readBinaryLong(2, swapHalfWord)); 525 ret.setRemoteDevice(readBinaryLong(2, swapHalfWord)); 526 ret.setTime(readBinaryLong(4, swapHalfWord)); 527 final long namesize = readBinaryLong(2, swapHalfWord); 528 if (namesize < 0) { 529 throw new IOException("Found illegal entry with negative name length"); 530 } 531 ret.setSize(readBinaryLong(4, swapHalfWord)); 532 if (ret.getSize() < 0) { 533 throw new IOException("Found illegal entry with negative length"); 534 } 535 final String name = readCString((int) namesize); 536 ret.setName(name); 537 if (CpioUtil.fileType(mode) == 0 && !name.equals(CPIO_TRAILER)){ 538 throw new IOException("Mode 0 only allowed in the trailer. Found entry: " 539 + ArchiveUtils.sanitize(name) 540 + "Occurred at byte: " + getBytesRead()); 541 } 542 skip(ret.getHeaderPadCount(namesize - 1)); 543 544 return ret; 545 } 546 547 private final byte[] readRange(final int len) 548 throws IOException { 549 final byte[] b = IOUtils.readRange(in, len); 550 count(b.length); 551 if (b.length < len) { 552 throw new EOFException(); 553 } 554 return b; 555 } 556 557 private void skip(final int bytes) throws IOException{ 558 // bytes cannot be more than 3 bytes 559 if (bytes > 0) { 560 readFully(fourBytesBuf, 0, bytes); 561 } 562 } 563 564 /** 565 * Skips specified number of bytes in the current CPIO entry. 566 * 567 * @param n 568 * the number of bytes to skip 569 * @return the actual number of bytes skipped 570 * @throws IOException 571 * if an I/O error has occurred 572 * @throws IllegalArgumentException 573 * if n < 0 574 */ 575 @Override 576 public long skip(final long n) throws IOException { 577 if (n < 0) { 578 throw new IllegalArgumentException("Negative skip length"); 579 } 580 ensureOpen(); 581 final int max = (int) Math.min(n, Integer.MAX_VALUE); 582 int total = 0; 583 584 while (total < max) { 585 int len = max - total; 586 if (len > this.tmpbuf.length) { 587 len = this.tmpbuf.length; 588 } 589 len = read(this.tmpbuf, 0, len); 590 if (len == -1) { 591 this.entryEOF = true; 592 break; 593 } 594 total += len; 595 } 596 return total; 597 } 598 599 /** 600 * Skips the padding zeros written after the TRAILER!!! entry. 601 */ 602 private void skipRemainderOfLastBlock() throws IOException { 603 final long readFromLastBlock = getBytesRead() % blockSize; 604 long remainingBytes = readFromLastBlock == 0 ? 0 605 : blockSize - readFromLastBlock; 606 while (remainingBytes > 0) { 607 final long skipped = skip(blockSize - readFromLastBlock); 608 if (skipped <= 0) { 609 break; 610 } 611 remainingBytes -= skipped; 612 } 613 } 614}