View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.commons.compress.archivers.cpio;
20  
21  import java.io.EOFException;
22  import java.io.IOException;
23  import java.io.InputStream;
24  
25  import org.apache.commons.compress.archivers.ArchiveInputStream;
26  import org.apache.commons.compress.archivers.zip.ZipEncoding;
27  import org.apache.commons.compress.archivers.zip.ZipEncodingHelper;
28  import org.apache.commons.compress.utils.ArchiveUtils;
29  import org.apache.commons.compress.utils.IOUtils;
30  import org.apache.commons.compress.utils.ParsingUtils;
31  
32  /**
33   * CpioArchiveInputStream is a stream for reading cpio streams. All formats of cpio are supported (old ascii, old binary, new portable format and the new
34   * portable format with crc).
35   * <p>
36   * 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
37   * the entry.
38   * </p>
39   * <pre>
40   * CpioArchiveInputStream cpioIn = new CpioArchiveInputStream(Files.newInputStream(Paths.get(&quot;test.cpio&quot;)));
41   * CpioArchiveEntry cpioEntry;
42   *
43   * while ((cpioEntry = cpioIn.getNextEntry()) != null) {
44   *     System.out.println(cpioEntry.getName());
45   *     int tmp;
46   *     StringBuilder buf = new StringBuilder();
47   *     while ((tmp = cpIn.read()) != -1) {
48   *         buf.append((char) tmp);
49   *     }
50   *     System.out.println(buf.toString());
51   * }
52   * cpioIn.close();
53   * </pre>
54   * <p>
55   * Note: This implementation should be compatible to cpio 2.5
56   * </p>
57   * <p>
58   * This class uses mutable fields and is not considered to be threadsafe.
59   * </p>
60   * <p>
61   * Based on code from the jRPM project (jrpm.sourceforge.net)
62   * </p>
63   */
64  public class CpioArchiveInputStream extends ArchiveInputStream<CpioArchiveEntry> implements CpioConstants {
65  
66      /**
67       * Checks if the signature matches one of the following magic values:
68       *
69       * Strings:
70       *
71       * "070701" - MAGIC_NEW "070702" - MAGIC_NEW_CRC "070707" - MAGIC_OLD_ASCII
72       *
73       * Octal Binary value:
74       *
75       * 070707 - MAGIC_OLD_BINARY (held as a short) = 0x71C7 or 0xC771
76       *
77       * @param signature data to match
78       * @param length    length of data
79       * @return whether the buffer seems to contain CPIO data
80       */
81      public static boolean matches(final byte[] signature, final int length) {
82          if (length < 6) {
83              return false;
84          }
85  
86          // Check binary values
87          if (signature[0] == 0x71 && (signature[1] & 0xFF) == 0xc7) {
88              return true;
89          }
90          if (signature[1] == 0x71 && (signature[0] & 0xFF) == 0xc7) {
91              return true;
92          }
93  
94          // Check Ascii (String) values
95          // 3037 3037 30nn
96          if (signature[0] != 0x30) {
97              return false;
98          }
99          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 }