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