CpioArchiveInputStream.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one
  3.  * or more contributor license agreements.  See the NOTICE file
  4.  * distributed with this work for additional information
  5.  * regarding copyright ownership.  The ASF licenses this file
  6.  * to you under the Apache License, Version 2.0 (the
  7.  * "License"); you may not use this file except in compliance
  8.  * with the License.  You may obtain a copy of the License at
  9.  *
  10.  * http://www.apache.org/licenses/LICENSE-2.0
  11.  *
  12.  * Unless required by applicable law or agreed to in writing,
  13.  * software distributed under the License is distributed on an
  14.  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15.  * KIND, either express or implied.  See the License for the
  16.  * specific language governing permissions and limitations
  17.  * under the License.
  18.  */
  19. package org.apache.commons.compress.archivers.cpio;

  20. import java.io.EOFException;
  21. import java.io.IOException;
  22. import java.io.InputStream;

  23. import org.apache.commons.compress.archivers.ArchiveInputStream;
  24. import org.apache.commons.compress.archivers.zip.ZipEncoding;
  25. import org.apache.commons.compress.archivers.zip.ZipEncodingHelper;
  26. import org.apache.commons.compress.utils.ArchiveUtils;
  27. import org.apache.commons.compress.utils.IOUtils;
  28. import org.apache.commons.compress.utils.ParsingUtils;

  29. /**
  30.  * CpioArchiveInputStream is a stream for reading cpio streams. All formats of cpio are supported (old ascii, old binary, new portable format and the new
  31.  * portable format with crc).
  32.  * <p>
  33.  * 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
  34.  * the entry.
  35.  * </p>
  36.  * <pre>
  37.  * CpioArchiveInputStream cpioIn = new CpioArchiveInputStream(Files.newInputStream(Paths.get(&quot;test.cpio&quot;)));
  38.  * CpioArchiveEntry cpioEntry;
  39.  *
  40.  * while ((cpioEntry = cpioIn.getNextEntry()) != null) {
  41.  *     System.out.println(cpioEntry.getName());
  42.  *     int tmp;
  43.  *     StringBuilder buf = new StringBuilder();
  44.  *     while ((tmp = cpIn.read()) != -1) {
  45.  *         buf.append((char) tmp);
  46.  *     }
  47.  *     System.out.println(buf.toString());
  48.  * }
  49.  * cpioIn.close();
  50.  * </pre>
  51.  * <p>
  52.  * Note: This implementation should be compatible to cpio 2.5
  53.  * </p>
  54.  * <p>
  55.  * This class uses mutable fields and is not considered to be threadsafe.
  56.  * </p>
  57.  * <p>
  58.  * Based on code from the jRPM project (jrpm.sourceforge.net)
  59.  * </p>
  60.  */
  61. public class CpioArchiveInputStream extends ArchiveInputStream<CpioArchiveEntry> implements CpioConstants {

  62.     /**
  63.      * Checks if the signature matches one of the following magic values:
  64.      *
  65.      * Strings:
  66.      *
  67.      * "070701" - MAGIC_NEW "070702" - MAGIC_NEW_CRC "070707" - MAGIC_OLD_ASCII
  68.      *
  69.      * Octal Binary value:
  70.      *
  71.      * 070707 - MAGIC_OLD_BINARY (held as a short) = 0x71C7 or 0xC771
  72.      *
  73.      * @param signature data to match
  74.      * @param length    length of data
  75.      * @return whether the buffer seems to contain CPIO data
  76.      */
  77.     public static boolean matches(final byte[] signature, final int length) {
  78.         if (length < 6) {
  79.             return false;
  80.         }

  81.         // Check binary values
  82.         if (signature[0] == 0x71 && (signature[1] & 0xFF) == 0xc7) {
  83.             return true;
  84.         }
  85.         if (signature[1] == 0x71 && (signature[0] & 0xFF) == 0xc7) {
  86.             return true;
  87.         }

  88.         // Check Ascii (String) values
  89.         // 3037 3037 30nn
  90.         if (signature[0] != 0x30) {
  91.             return false;
  92.         }
  93.         if (signature[1] != 0x37) {
  94.             return false;
  95.         }
  96.         if (signature[2] != 0x30) {
  97.             return false;
  98.         }
  99.         if (signature[3] != 0x37) {
  100.             return false;
  101.         }
  102.         if (signature[4] != 0x30) {
  103.             return false;
  104.         }
  105.         // Check last byte
  106.         if (signature[5] == 0x31) {
  107.             return true;
  108.         }
  109.         if (signature[5] == 0x32) {
  110.             return true;
  111.         }
  112.         if (signature[5] == 0x37) {
  113.             return true;
  114.         }

  115.         return false;
  116.     }

  117.     private boolean closed;

  118.     private CpioArchiveEntry entry;

  119.     private long entryBytesRead;

  120.     private boolean entryEOF;

  121.     private final byte[] tmpbuf = new byte[4096];

  122.     private long crc;

  123.     /** Cached buffer - must only be used locally in the class (COMPRESS-172 - reduce garbage collection). */
  124.     private final byte[] twoBytesBuf = new byte[2];

  125.     /** Cached buffer - must only be used locally in the class (COMPRESS-172 - reduce garbage collection). */
  126.     private final byte[] fourBytesBuf = new byte[4];

  127.     private final byte[] sixBytesBuf = new byte[6];

  128.     private final int blockSize;

  129.     /**
  130.      * The encoding to use for file names and labels.
  131.      */
  132.     private final ZipEncoding zipEncoding;

  133.     /**
  134.      * Constructs the cpio input stream with a blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE} and expecting ASCII file names.
  135.      *
  136.      * @param in The cpio stream
  137.      */
  138.     public CpioArchiveInputStream(final InputStream in) {
  139.         this(in, BLOCK_SIZE, CpioUtil.DEFAULT_CHARSET_NAME);
  140.     }

  141.     /**
  142.      * Constructs the cpio input stream with a blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE} expecting ASCII file names.
  143.      *
  144.      * @param in        The cpio stream
  145.      * @param blockSize The block size of the archive.
  146.      * @since 1.5
  147.      */
  148.     public CpioArchiveInputStream(final InputStream in, final int blockSize) {
  149.         this(in, blockSize, CpioUtil.DEFAULT_CHARSET_NAME);
  150.     }

  151.     /**
  152.      * Constructs the cpio input stream with a blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE}.
  153.      *
  154.      * @param in        The cpio stream
  155.      * @param blockSize The block size of the archive.
  156.      * @param encoding  The encoding of file names to expect - use null for the platform's default.
  157.      * @throws IllegalArgumentException if {@code blockSize} is not bigger than 0
  158.      * @since 1.6
  159.      */
  160.     public CpioArchiveInputStream(final InputStream in, final int blockSize, final String encoding) {
  161.         super(in, encoding);
  162.         this.in = in;
  163.         if (blockSize <= 0) {
  164.             throw new IllegalArgumentException("blockSize must be bigger than 0");
  165.         }
  166.         this.blockSize = blockSize;
  167.         this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
  168.     }

  169.     /**
  170.      * Constructs the cpio input stream with a blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE}.
  171.      *
  172.      * @param in       The cpio stream
  173.      * @param encoding The encoding of file names to expect - use null for the platform's default.
  174.      * @since 1.6
  175.      */
  176.     public CpioArchiveInputStream(final InputStream in, final String encoding) {
  177.         this(in, BLOCK_SIZE, encoding);
  178.     }

  179.     /**
  180.      * Returns 0 after EOF has reached for the current entry data, otherwise always return 1.
  181.      * <p>
  182.      * Programs should not count on this method to return the actual number of bytes that could be read without blocking.
  183.      * </p>
  184.      *
  185.      * @return 1 before EOF and 0 after EOF has reached for current entry.
  186.      * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred
  187.      */
  188.     @Override
  189.     public int available() throws IOException {
  190.         ensureOpen();
  191.         if (this.entryEOF) {
  192.             return 0;
  193.         }
  194.         return 1;
  195.     }

  196.     /**
  197.      * Closes the CPIO input stream.
  198.      *
  199.      * @throws IOException if an I/O error has occurred
  200.      */
  201.     @Override
  202.     public void close() throws IOException {
  203.         if (!this.closed) {
  204.             in.close();
  205.             this.closed = true;
  206.         }
  207.     }

  208.     /**
  209.      * Closes the current CPIO entry and positions the stream for reading the next entry.
  210.      *
  211.      * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred
  212.      */
  213.     private void closeEntry() throws IOException {
  214.         // the skip implementation of this class will not skip more
  215.         // than Integer.MAX_VALUE bytes
  216.         while (skip((long) Integer.MAX_VALUE) == Integer.MAX_VALUE) { // NOPMD NOSONAR
  217.             // do nothing
  218.         }
  219.     }

  220.     /**
  221.      * Check to make sure that this stream has not been closed
  222.      *
  223.      * @throws IOException if the stream is already closed
  224.      */
  225.     private void ensureOpen() throws IOException {
  226.         if (this.closed) {
  227.             throw new IOException("Stream closed");
  228.         }
  229.     }

  230.     /**
  231.      * Reads the next CPIO file entry and positions stream at the beginning of the entry data.
  232.      *
  233.      * @return the CpioArchiveEntry just read
  234.      * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred
  235.      * @deprecated Use {@link #getNextEntry()}.
  236.      */
  237.     @Deprecated
  238.     public CpioArchiveEntry getNextCPIOEntry() throws IOException {
  239.         ensureOpen();
  240.         if (this.entry != null) {
  241.             closeEntry();
  242.         }
  243.         readFully(twoBytesBuf, 0, twoBytesBuf.length);
  244.         if (CpioUtil.byteArray2long(twoBytesBuf, false) == MAGIC_OLD_BINARY) {
  245.             this.entry = readOldBinaryEntry(false);
  246.         } else if (CpioUtil.byteArray2long(twoBytesBuf, true) == MAGIC_OLD_BINARY) {
  247.             this.entry = readOldBinaryEntry(true);
  248.         } else {
  249.             System.arraycopy(twoBytesBuf, 0, sixBytesBuf, 0, twoBytesBuf.length);
  250.             readFully(sixBytesBuf, twoBytesBuf.length, fourBytesBuf.length);
  251.             final String magicString = ArchiveUtils.toAsciiString(sixBytesBuf);
  252.             switch (magicString) {
  253.             case MAGIC_NEW:
  254.                 this.entry = readNewEntry(false);
  255.                 break;
  256.             case MAGIC_NEW_CRC:
  257.                 this.entry = readNewEntry(true);
  258.                 break;
  259.             case MAGIC_OLD_ASCII:
  260.                 this.entry = readOldAsciiEntry();
  261.                 break;
  262.             default:
  263.                 throw new IOException("Unknown magic [" + magicString + "]. Occurred at byte: " + getBytesRead());
  264.             }
  265.         }

  266.         this.entryBytesRead = 0;
  267.         this.entryEOF = false;
  268.         this.crc = 0;

  269.         if (this.entry.getName().equals(CPIO_TRAILER)) {
  270.             this.entryEOF = true;
  271.             skipRemainderOfLastBlock();
  272.             return null;
  273.         }
  274.         return this.entry;
  275.     }

  276.     @Override
  277.     public CpioArchiveEntry getNextEntry() throws IOException {
  278.         return getNextCPIOEntry();
  279.     }

  280.     /**
  281.      * Reads from the current CPIO entry into an array of bytes. Blocks until some input is available.
  282.      *
  283.      * @param b   the buffer into which the data is read
  284.      * @param off the start offset of the data
  285.      * @param len the maximum number of bytes read
  286.      * @return the actual number of bytes read, or -1 if the end of the entry is reached
  287.      * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred
  288.      */
  289.     @Override
  290.     public int read(final byte[] b, final int off, final int len) throws IOException {
  291.         ensureOpen();
  292.         if (off < 0 || len < 0 || off > b.length - len) {
  293.             throw new IndexOutOfBoundsException();
  294.         }
  295.         if (len == 0) {
  296.             return 0;
  297.         }

  298.         if (this.entry == null || this.entryEOF) {
  299.             return -1;
  300.         }
  301.         if (this.entryBytesRead == this.entry.getSize()) {
  302.             skip(entry.getDataPadCount());
  303.             this.entryEOF = true;
  304.             if (this.entry.getFormat() == FORMAT_NEW_CRC && this.crc != this.entry.getChksum()) {
  305.                 throw new IOException("CRC Error. Occurred at byte: " + getBytesRead());
  306.             }
  307.             return -1; // EOF for this entry
  308.         }
  309.         final int tmplength = (int) Math.min(len, this.entry.getSize() - this.entryBytesRead);
  310.         if (tmplength < 0) {
  311.             return -1;
  312.         }

  313.         final int tmpread = readFully(b, off, tmplength);
  314.         if (this.entry.getFormat() == FORMAT_NEW_CRC) {
  315.             for (int pos = 0; pos < tmpread; pos++) {
  316.                 this.crc += b[pos] & 0xFF;
  317.                 this.crc &= 0xFFFFFFFFL;
  318.             }
  319.         }
  320.         if (tmpread > 0) {
  321.             this.entryBytesRead += tmpread;
  322.         }

  323.         return tmpread;
  324.     }

  325.     private long readAsciiLong(final int length, final int radix) throws IOException {
  326.         final byte[] tmpBuffer = readRange(length);
  327.         return ParsingUtils.parseLongValue(ArchiveUtils.toAsciiString(tmpBuffer), radix);
  328.     }

  329.     private long readBinaryLong(final int length, final boolean swapHalfWord) throws IOException {
  330.         final byte[] tmp = readRange(length);
  331.         return CpioUtil.byteArray2long(tmp, swapHalfWord);
  332.     }

  333.     private String readCString(final int length) throws IOException {
  334.         // don't include trailing NUL in file name to decode
  335.         final byte[] tmpBuffer = readRange(length - 1);
  336.         if (this.in.read() == -1) {
  337.             throw new EOFException();
  338.         }
  339.         return zipEncoding.decode(tmpBuffer);
  340.     }

  341.     private int readFully(final byte[] b, final int off, final int len) throws IOException {
  342.         final int count = IOUtils.readFully(in, b, off, len);
  343.         count(count);
  344.         if (count < len) {
  345.             throw new EOFException();
  346.         }
  347.         return count;
  348.     }

  349.     private CpioArchiveEntry readNewEntry(final boolean hasCrc) throws IOException {
  350.         final CpioArchiveEntry ret;
  351.         if (hasCrc) {
  352.             ret = new CpioArchiveEntry(FORMAT_NEW_CRC);
  353.         } else {
  354.             ret = new CpioArchiveEntry(FORMAT_NEW);
  355.         }

  356.         ret.setInode(readAsciiLong(8, 16));
  357.         final long mode = readAsciiLong(8, 16);
  358.         if (CpioUtil.fileType(mode) != 0) { // mode is initialized to 0
  359.             ret.setMode(mode);
  360.         }
  361.         ret.setUID(readAsciiLong(8, 16));
  362.         ret.setGID(readAsciiLong(8, 16));
  363.         ret.setNumberOfLinks(readAsciiLong(8, 16));
  364.         ret.setTime(readAsciiLong(8, 16));
  365.         ret.setSize(readAsciiLong(8, 16));
  366.         if (ret.getSize() < 0) {
  367.             throw new IOException("Found illegal entry with negative length");
  368.         }
  369.         ret.setDeviceMaj(readAsciiLong(8, 16));
  370.         ret.setDeviceMin(readAsciiLong(8, 16));
  371.         ret.setRemoteDeviceMaj(readAsciiLong(8, 16));
  372.         ret.setRemoteDeviceMin(readAsciiLong(8, 16));
  373.         final long namesize = readAsciiLong(8, 16);
  374.         if (namesize < 0) {
  375.             throw new IOException("Found illegal entry with negative name length");
  376.         }
  377.         ret.setChksum(readAsciiLong(8, 16));
  378.         final String name = readCString((int) namesize);
  379.         ret.setName(name);
  380.         if (CpioUtil.fileType(mode) == 0 && !name.equals(CPIO_TRAILER)) {
  381.             throw new IOException(
  382.                     "Mode 0 only allowed in the trailer. Found entry name: " + ArchiveUtils.sanitize(name) + " Occurred at byte: " + getBytesRead());
  383.         }
  384.         skip(ret.getHeaderPadCount(namesize - 1));

  385.         return ret;
  386.     }

  387.     private CpioArchiveEntry readOldAsciiEntry() throws IOException {
  388.         final CpioArchiveEntry ret = new CpioArchiveEntry(FORMAT_OLD_ASCII);

  389.         ret.setDevice(readAsciiLong(6, 8));
  390.         ret.setInode(readAsciiLong(6, 8));
  391.         final long mode = readAsciiLong(6, 8);
  392.         if (CpioUtil.fileType(mode) != 0) {
  393.             ret.setMode(mode);
  394.         }
  395.         ret.setUID(readAsciiLong(6, 8));
  396.         ret.setGID(readAsciiLong(6, 8));
  397.         ret.setNumberOfLinks(readAsciiLong(6, 8));
  398.         ret.setRemoteDevice(readAsciiLong(6, 8));
  399.         ret.setTime(readAsciiLong(11, 8));
  400.         final long namesize = readAsciiLong(6, 8);
  401.         if (namesize < 0) {
  402.             throw new IOException("Found illegal entry with negative name length");
  403.         }
  404.         ret.setSize(readAsciiLong(11, 8));
  405.         if (ret.getSize() < 0) {
  406.             throw new IOException("Found illegal entry with negative length");
  407.         }
  408.         final String name = readCString((int) namesize);
  409.         ret.setName(name);
  410.         if (CpioUtil.fileType(mode) == 0 && !name.equals(CPIO_TRAILER)) {
  411.             throw new IOException("Mode 0 only allowed in the trailer. Found entry: " + ArchiveUtils.sanitize(name) + " Occurred at byte: " + getBytesRead());
  412.         }

  413.         return ret;
  414.     }

  415.     private CpioArchiveEntry readOldBinaryEntry(final boolean swapHalfWord) throws IOException {
  416.         final CpioArchiveEntry ret = new CpioArchiveEntry(FORMAT_OLD_BINARY);

  417.         ret.setDevice(readBinaryLong(2, swapHalfWord));
  418.         ret.setInode(readBinaryLong(2, swapHalfWord));
  419.         final long mode = readBinaryLong(2, swapHalfWord);
  420.         if (CpioUtil.fileType(mode) != 0) {
  421.             ret.setMode(mode);
  422.         }
  423.         ret.setUID(readBinaryLong(2, swapHalfWord));
  424.         ret.setGID(readBinaryLong(2, swapHalfWord));
  425.         ret.setNumberOfLinks(readBinaryLong(2, swapHalfWord));
  426.         ret.setRemoteDevice(readBinaryLong(2, swapHalfWord));
  427.         ret.setTime(readBinaryLong(4, swapHalfWord));
  428.         final long namesize = readBinaryLong(2, swapHalfWord);
  429.         if (namesize < 0) {
  430.             throw new IOException("Found illegal entry with negative name length");
  431.         }
  432.         ret.setSize(readBinaryLong(4, swapHalfWord));
  433.         if (ret.getSize() < 0) {
  434.             throw new IOException("Found illegal entry with negative length");
  435.         }
  436.         final String name = readCString((int) namesize);
  437.         ret.setName(name);
  438.         if (CpioUtil.fileType(mode) == 0 && !name.equals(CPIO_TRAILER)) {
  439.             throw new IOException("Mode 0 only allowed in the trailer. Found entry: " + ArchiveUtils.sanitize(name) + "Occurred at byte: " + getBytesRead());
  440.         }
  441.         skip(ret.getHeaderPadCount(namesize - 1));

  442.         return ret;
  443.     }

  444.     private byte[] readRange(final int len) throws IOException {
  445.         final byte[] b = IOUtils.readRange(in, len);
  446.         count(b.length);
  447.         if (b.length < len) {
  448.             throw new EOFException();
  449.         }
  450.         return b;
  451.     }

  452.     private void skip(final int bytes) throws IOException {
  453.         // bytes cannot be more than 3 bytes
  454.         if (bytes > 0) {
  455.             readFully(fourBytesBuf, 0, bytes);
  456.         }
  457.     }

  458.     /**
  459.      * Skips specified number of bytes in the current CPIO entry.
  460.      *
  461.      * @param n the number of bytes to skip
  462.      * @return the actual number of bytes skipped
  463.      * @throws IOException              if an I/O error has occurred
  464.      * @throws IllegalArgumentException if n &lt; 0
  465.      */
  466.     @Override
  467.     public long skip(final long n) throws IOException {
  468.         if (n < 0) {
  469.             throw new IllegalArgumentException("Negative skip length");
  470.         }
  471.         ensureOpen();
  472.         final int max = (int) Math.min(n, Integer.MAX_VALUE);
  473.         int total = 0;

  474.         while (total < max) {
  475.             int len = max - total;
  476.             if (len > this.tmpbuf.length) {
  477.                 len = this.tmpbuf.length;
  478.             }
  479.             len = read(this.tmpbuf, 0, len);
  480.             if (len == -1) {
  481.                 this.entryEOF = true;
  482.                 break;
  483.             }
  484.             total += len;
  485.         }
  486.         return total;
  487.     }

  488.     /**
  489.      * Skips the padding zeros written after the TRAILER!!! entry.
  490.      */
  491.     private void skipRemainderOfLastBlock() throws IOException {
  492.         final long readFromLastBlock = getBytesRead() % blockSize;
  493.         long remainingBytes = readFromLastBlock == 0 ? 0 : blockSize - readFromLastBlock;
  494.         while (remainingBytes > 0) {
  495.             final long skipped = skip(blockSize - readFromLastBlock);
  496.             if (skipped <= 0) {
  497.                 break;
  498.             }
  499.             remainingBytes -= skipped;
  500.         }
  501.     }
  502. }