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