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