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}