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