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