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   *   https://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          // Check binary values
86          if (signature[0] == 0x71 && (signature[1] & 0xFF) == 0xc7 || signature[1] == 0x71 && (signature[0] & 0xFF) == 0xc7) {
87              return true;
88          }
89          // Check Ascii (String) values
90          // 3037 3037 30nn
91          if (signature[0] != 0x30) {
92              return false;
93          }
94          if (signature[1] != 0x37) {
95              return false;
96          }
97          if (signature[2] != 0x30) {
98              return false;
99          }
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 }