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