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}