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