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