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