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