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; 071 072 private CpioArchiveEntry entry; 073 074 private long entryBytesRead; 075 076 private boolean entryEOF; 077 078 private final byte[] tmpbuf = new byte[4096]; 079 080 private long crc; 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 + "]. Occurred 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 } 310 if (len == 0) { 311 return 0; 312 } 313 314 if (this.entry == null || this.entryEOF) { 315 return -1; 316 } 317 if (this.entryBytesRead == this.entry.getSize()) { 318 skip(entry.getDataPadCount()); 319 this.entryEOF = true; 320 if (this.entry.getFormat() == FORMAT_NEW_CRC 321 && this.crc != this.entry.getChksum()) { 322 throw new IOException("CRC Error. Occurred at byte: " 323 + getBytesRead()); 324 } 325 return -1; // EOF for this entry 326 } 327 final int tmplength = (int) Math.min(len, this.entry.getSize() 328 - this.entryBytesRead); 329 if (tmplength < 0) { 330 return -1; 331 } 332 333 final int tmpread = readFully(b, off, tmplength); 334 if (this.entry.getFormat() == FORMAT_NEW_CRC) { 335 for (int pos = 0; pos < tmpread; pos++) { 336 this.crc += b[pos] & 0xFF; 337 this.crc &= 0xFFFFFFFFL; 338 } 339 } 340 if (tmpread > 0) { 341 this.entryBytesRead += tmpread; 342 } 343 344 return tmpread; 345 } 346 347 private final int readFully(final byte[] b, final int off, final int len) 348 throws IOException { 349 final int count = IOUtils.readFully(in, b, off, len); 350 count(count); 351 if (count < len) { 352 throw new EOFException(); 353 } 354 return count; 355 } 356 357 private final byte[] readRange(final int len) 358 throws IOException { 359 final byte[] b = IOUtils.readRange(in, len); 360 count(b.length); 361 if (b.length < len) { 362 throw new EOFException(); 363 } 364 return b; 365 } 366 367 private long readBinaryLong(final int length, final boolean swapHalfWord) 368 throws IOException { 369 final byte[] tmp = readRange(length); 370 return CpioUtil.byteArray2long(tmp, swapHalfWord); 371 } 372 373 private long readAsciiLong(final int length, final int radix) 374 throws IOException { 375 final byte[] tmpBuffer = readRange(length); 376 return Long.parseLong(ArchiveUtils.toAsciiString(tmpBuffer), radix); 377 } 378 379 private CpioArchiveEntry readNewEntry(final boolean hasCrc) 380 throws IOException { 381 final CpioArchiveEntry ret; 382 if (hasCrc) { 383 ret = new CpioArchiveEntry(FORMAT_NEW_CRC); 384 } else { 385 ret = new CpioArchiveEntry(FORMAT_NEW); 386 } 387 388 ret.setInode(readAsciiLong(8, 16)); 389 final long mode = readAsciiLong(8, 16); 390 if (CpioUtil.fileType(mode) != 0){ // mode is initialized to 0 391 ret.setMode(mode); 392 } 393 ret.setUID(readAsciiLong(8, 16)); 394 ret.setGID(readAsciiLong(8, 16)); 395 ret.setNumberOfLinks(readAsciiLong(8, 16)); 396 ret.setTime(readAsciiLong(8, 16)); 397 ret.setSize(readAsciiLong(8, 16)); 398 if (ret.getSize() < 0) { 399 throw new IOException("Found illegal entry with negative length"); 400 } 401 ret.setDeviceMaj(readAsciiLong(8, 16)); 402 ret.setDeviceMin(readAsciiLong(8, 16)); 403 ret.setRemoteDeviceMaj(readAsciiLong(8, 16)); 404 ret.setRemoteDeviceMin(readAsciiLong(8, 16)); 405 final long namesize = readAsciiLong(8, 16); 406 if (namesize < 0) { 407 throw new IOException("Found illegal entry with negative name length"); 408 } 409 ret.setChksum(readAsciiLong(8, 16)); 410 final String name = readCString((int) namesize); 411 ret.setName(name); 412 if (CpioUtil.fileType(mode) == 0 && !name.equals(CPIO_TRAILER)){ 413 throw new IOException("Mode 0 only allowed in the trailer. Found entry name: " 414 + ArchiveUtils.sanitize(name) 415 + " Occurred at byte: " + getBytesRead()); 416 } 417 skip(ret.getHeaderPadCount(namesize - 1)); 418 419 return ret; 420 } 421 422 private CpioArchiveEntry readOldAsciiEntry() throws IOException { 423 final CpioArchiveEntry ret = new CpioArchiveEntry(FORMAT_OLD_ASCII); 424 425 ret.setDevice(readAsciiLong(6, 8)); 426 ret.setInode(readAsciiLong(6, 8)); 427 final long mode = readAsciiLong(6, 8); 428 if (CpioUtil.fileType(mode) != 0) { 429 ret.setMode(mode); 430 } 431 ret.setUID(readAsciiLong(6, 8)); 432 ret.setGID(readAsciiLong(6, 8)); 433 ret.setNumberOfLinks(readAsciiLong(6, 8)); 434 ret.setRemoteDevice(readAsciiLong(6, 8)); 435 ret.setTime(readAsciiLong(11, 8)); 436 final long namesize = readAsciiLong(6, 8); 437 if (namesize < 0) { 438 throw new IOException("Found illegal entry with negative name length"); 439 } 440 ret.setSize(readAsciiLong(11, 8)); 441 if (ret.getSize() < 0) { 442 throw new IOException("Found illegal entry with negative length"); 443 } 444 final String name = readCString((int) namesize); 445 ret.setName(name); 446 if (CpioUtil.fileType(mode) == 0 && !name.equals(CPIO_TRAILER)){ 447 throw new IOException("Mode 0 only allowed in the trailer. Found entry: " 448 + ArchiveUtils.sanitize(name) 449 + " Occurred at byte: " + getBytesRead()); 450 } 451 452 return ret; 453 } 454 455 private CpioArchiveEntry readOldBinaryEntry(final boolean swapHalfWord) 456 throws IOException { 457 final CpioArchiveEntry ret = new CpioArchiveEntry(FORMAT_OLD_BINARY); 458 459 ret.setDevice(readBinaryLong(2, swapHalfWord)); 460 ret.setInode(readBinaryLong(2, swapHalfWord)); 461 final long mode = readBinaryLong(2, swapHalfWord); 462 if (CpioUtil.fileType(mode) != 0){ 463 ret.setMode(mode); 464 } 465 ret.setUID(readBinaryLong(2, swapHalfWord)); 466 ret.setGID(readBinaryLong(2, swapHalfWord)); 467 ret.setNumberOfLinks(readBinaryLong(2, swapHalfWord)); 468 ret.setRemoteDevice(readBinaryLong(2, swapHalfWord)); 469 ret.setTime(readBinaryLong(4, swapHalfWord)); 470 final long namesize = readBinaryLong(2, swapHalfWord); 471 if (namesize < 0) { 472 throw new IOException("Found illegal entry with negative name length"); 473 } 474 ret.setSize(readBinaryLong(4, swapHalfWord)); 475 if (ret.getSize() < 0) { 476 throw new IOException("Found illegal entry with negative length"); 477 } 478 final String name = readCString((int) namesize); 479 ret.setName(name); 480 if (CpioUtil.fileType(mode) == 0 && !name.equals(CPIO_TRAILER)){ 481 throw new IOException("Mode 0 only allowed in the trailer. Found entry: " 482 + ArchiveUtils.sanitize(name) 483 + "Occurred at byte: " + getBytesRead()); 484 } 485 skip(ret.getHeaderPadCount(namesize - 1)); 486 487 return ret; 488 } 489 490 private String readCString(final int length) throws IOException { 491 // don't include trailing NUL in file name to decode 492 final byte[] tmpBuffer = readRange(length - 1); 493 if (this.in.read() == -1) { 494 throw new EOFException(); 495 } 496 return zipEncoding.decode(tmpBuffer); 497 } 498 499 /** 500 * Skips specified number of bytes in the current CPIO entry. 501 * 502 * @param n 503 * the number of bytes to skip 504 * @return the actual number of bytes skipped 505 * @throws IOException 506 * if an I/O error has occurred 507 * @throws IllegalArgumentException 508 * if n < 0 509 */ 510 @Override 511 public long skip(final long n) throws IOException { 512 if (n < 0) { 513 throw new IllegalArgumentException("Negative skip length"); 514 } 515 ensureOpen(); 516 final int max = (int) Math.min(n, Integer.MAX_VALUE); 517 int total = 0; 518 519 while (total < max) { 520 int len = max - total; 521 if (len > this.tmpbuf.length) { 522 len = this.tmpbuf.length; 523 } 524 len = read(this.tmpbuf, 0, len); 525 if (len == -1) { 526 this.entryEOF = true; 527 break; 528 } 529 total += len; 530 } 531 return total; 532 } 533 534 @Override 535 public ArchiveEntry getNextEntry() throws IOException { 536 return getNextCPIOEntry(); 537 } 538 539 /** 540 * Skips the padding zeros written after the TRAILER!!! entry. 541 */ 542 private void skipRemainderOfLastBlock() throws IOException { 543 final long readFromLastBlock = getBytesRead() % blockSize; 544 long remainingBytes = readFromLastBlock == 0 ? 0 545 : blockSize - readFromLastBlock; 546 while (remainingBytes > 0) { 547 final long skipped = skip(blockSize - readFromLastBlock); 548 if (skipped <= 0) { 549 break; 550 } 551 remainingBytes -= skipped; 552 } 553 } 554 555 /** 556 * Checks if the signature matches one of the following magic values: 557 * 558 * Strings: 559 * 560 * "070701" - MAGIC_NEW 561 * "070702" - MAGIC_NEW_CRC 562 * "070707" - MAGIC_OLD_ASCII 563 * 564 * Octal Binary value: 565 * 566 * 070707 - MAGIC_OLD_BINARY (held as a short) = 0x71C7 or 0xC771 567 * @param signature data to match 568 * @param length length of data 569 * @return whether the buffer seems to contain CPIO data 570 */ 571 public static boolean matches(final byte[] signature, final int length) { 572 if (length < 6) { 573 return false; 574 } 575 576 // Check binary values 577 if (signature[0] == 0x71 && (signature[1] & 0xFF) == 0xc7) { 578 return true; 579 } 580 if (signature[1] == 0x71 && (signature[0] & 0xFF) == 0xc7) { 581 return true; 582 } 583 584 // Check Ascii (String) values 585 // 3037 3037 30nn 586 if (signature[0] != 0x30) { 587 return false; 588 } 589 if (signature[1] != 0x37) { 590 return false; 591 } 592 if (signature[2] != 0x30) { 593 return false; 594 } 595 if (signature[3] != 0x37) { 596 return false; 597 } 598 if (signature[4] != 0x30) { 599 return false; 600 } 601 // Check last byte 602 if (signature[5] == 0x31) { 603 return true; 604 } 605 if (signature[5] == 0x32) { 606 return true; 607 } 608 if (signature[5] == 0x37) { 609 return true; 610 } 611 612 return false; 613 } 614}