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