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