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