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