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.File;
22 import java.io.IOException;
23 import java.io.OutputStream;
24 import java.nio.ByteBuffer;
25 import java.util.Arrays;
26 import java.util.HashMap;
27 
28 import org.apache.commons.compress.archivers.ArchiveEntry;
29 import org.apache.commons.compress.archivers.ArchiveOutputStream;
30 import org.apache.commons.compress.archivers.zip.ZipEncoding;
31 import org.apache.commons.compress.archivers.zip.ZipEncodingHelper;
32 import org.apache.commons.compress.utils.ArchiveUtils;
33 import org.apache.commons.compress.utils.CharsetNames;
34 
35 /**
36  * CpioArchiveOutputStream is a stream for writing CPIO streams. All formats of
37  * CPIO are supported (old ASCII, old binary, new portable format and the new
38  * portable format with CRC).
39  *
40  * <p>An entry can be written by creating an instance of CpioArchiveEntry and fill
41  * it with the necessary values and put it into the CPIO stream. Afterwards
42  * write the contents of the file into the CPIO stream. Either close the stream
43  * by calling finish() or put a next entry into the cpio stream.</p>
44  *
45  * <pre>
46  * CpioArchiveOutputStream out = new CpioArchiveOutputStream(
47  *         new FileOutputStream(new File("test.cpio")));
48  * CpioArchiveEntry entry = new CpioArchiveEntry();
49  * entry.setName("testfile");
50  * String contents = &quot;12345&quot;;
51  * entry.setFileSize(contents.length());
52  * entry.setMode(CpioConstants.C_ISREG); // regular file
53  * ... set other attributes, e.g. time, number of links
54  * out.putArchiveEntry(entry);
55  * out.write(testContents.getBytes());
56  * out.close();
57  * </pre>
58  *
59  * <p>Note: This implementation should be compatible to cpio 2.5</p>
60  *
61  * <p>This class uses mutable fields and is not considered threadsafe.</p>
62  *
63  * <p>based on code from the jRPM project (jrpm.sourceforge.net)</p>
64  */
65 public class CpioArchiveOutputStream extends ArchiveOutputStream implements
66         CpioConstants {
67 
68     private CpioArchiveEntry entry;
69 
70     private boolean closed = false;
71 
72     /** indicates if this archive is finished */
73     private boolean finished;
74 
75     /**
76      * See {@link CpioArchiveEntry#setFormat(short)} for possible values.
77      */
78     private final short entryFormat;
79 
80     private final HashMap<String, CpioArchiveEntry> names =
81         new HashMap<>();
82 
83     private long crc = 0;
84 
85     private long written;
86 
87     private final OutputStream out;
88 
89     private final int blockSize;
90 
91     private long nextArtificalDeviceAndInode = 1;
92 
93     /**
94      * The encoding to use for filenames and labels.
95      */
96     private final ZipEncoding zipEncoding;
97 
98     // the provided encoding (for unit tests)
99     final String encoding;
100 
101     /**
102      * Construct the cpio output stream with a specified format, a
103      * blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE} and
104      * using ASCII as the file name encoding.
105      *
106      * @param out
107      *            The cpio stream
108      * @param format
109      *            The format of the stream
110      */
CpioArchiveOutputStream(final OutputStream out, final short format)111     public CpioArchiveOutputStream(final OutputStream out, final short format) {
112         this(out, format, BLOCK_SIZE, CharsetNames.US_ASCII);
113     }
114 
115     /**
116      * Construct the cpio output stream with a specified format using
117      * ASCII as the file name encoding.
118      *
119      * @param out
120      *            The cpio stream
121      * @param format
122      *            The format of the stream
123      * @param blockSize
124      *            The block size of the archive.
125      *
126      * @since 1.1
127      */
CpioArchiveOutputStream(final OutputStream out, final short format, final int blockSize)128     public CpioArchiveOutputStream(final OutputStream out, final short format,
129                                    final int blockSize) {
130         this(out, format, blockSize, CharsetNames.US_ASCII);
131     }
132 
133     /**
134      * Construct the cpio output stream with a specified format using
135      * ASCII as the file name encoding.
136      *
137      * @param out
138      *            The cpio stream
139      * @param format
140      *            The format of the stream
141      * @param blockSize
142      *            The block size of the archive.
143      * @param encoding
144      *            The encoding of file names to write - use null for
145      *            the platform's default.
146      *
147      * @since 1.6
148      */
CpioArchiveOutputStream(final OutputStream out, final short format, final int blockSize, final String encoding)149     public CpioArchiveOutputStream(final OutputStream out, final short format,
150                                    final int blockSize, final String encoding) {
151         this.out = out;
152         switch (format) {
153         case FORMAT_NEW:
154         case FORMAT_NEW_CRC:
155         case FORMAT_OLD_ASCII:
156         case FORMAT_OLD_BINARY:
157             break;
158         default:
159             throw new IllegalArgumentException("Unknown format: "+format);
160 
161         }
162         this.entryFormat = format;
163         this.blockSize = blockSize;
164         this.encoding = encoding;
165         this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
166     }
167 
168     /**
169      * Construct the cpio output stream. The format for this CPIO stream is the
170      * "new" format using ASCII encoding for file names
171      *
172      * @param out
173      *            The cpio stream
174      */
CpioArchiveOutputStream(final OutputStream out)175     public CpioArchiveOutputStream(final OutputStream out) {
176         this(out, FORMAT_NEW);
177     }
178 
179     /**
180      * Construct the cpio output stream. The format for this CPIO stream is the
181      * "new" format.
182      *
183      * @param out
184      *            The cpio stream
185      * @param encoding
186      *            The encoding of file names to write - use null for
187      *            the platform's default.
188      * @since 1.6
189      */
CpioArchiveOutputStream(final OutputStream out, final String encoding)190     public CpioArchiveOutputStream(final OutputStream out, final String encoding) {
191         this(out, FORMAT_NEW, BLOCK_SIZE, encoding);
192     }
193 
194     /**
195      * Check to make sure that this stream has not been closed
196      *
197      * @throws IOException
198      *             if the stream is already closed
199      */
ensureOpen()200     private void ensureOpen() throws IOException {
201         if (this.closed) {
202             throw new IOException("Stream closed");
203         }
204     }
205 
206     /**
207      * Begins writing a new CPIO file entry and positions the stream to the
208      * start of the entry data. Closes the current entry if still active. The
209      * current time will be used if the entry has no set modification time and
210      * the default header format will be used if no other format is specified in
211      * the entry.
212      *
213      * @param entry
214      *            the CPIO cpioEntry to be written
215      * @throws IOException
216      *             if an I/O error has occurred or if a CPIO file error has
217      *             occurred
218      * @throws ClassCastException if entry is not an instance of CpioArchiveEntry
219      */
220     @Override
putArchiveEntry(final ArchiveEntry entry)221     public void putArchiveEntry(final ArchiveEntry entry) throws IOException {
222         if(finished) {
223             throw new IOException("Stream has already been finished");
224         }
225 
226         final CpioArchiveEntry e = (CpioArchiveEntry) entry;
227         ensureOpen();
228         if (this.entry != null) {
229             closeArchiveEntry(); // close previous entry
230         }
231         if (e.getTime() == -1) {
232             e.setTime(System.currentTimeMillis() / 1000);
233         }
234 
235         final short format = e.getFormat();
236         if (format != this.entryFormat){
237             throw new IOException("Header format: "+format+" does not match existing format: "+this.entryFormat);
238         }
239 
240         if (this.names.put(e.getName(), e) != null) {
241             throw new IOException("duplicate entry: " + e.getName());
242         }
243 
244         writeHeader(e);
245         this.entry = e;
246         this.written = 0;
247     }
248 
writeHeader(final CpioArchiveEntry e)249     private void writeHeader(final CpioArchiveEntry e) throws IOException {
250         switch (e.getFormat()) {
251         case FORMAT_NEW:
252             out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW));
253             count(6);
254             writeNewEntry(e);
255             break;
256         case FORMAT_NEW_CRC:
257             out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW_CRC));
258             count(6);
259             writeNewEntry(e);
260             break;
261         case FORMAT_OLD_ASCII:
262             out.write(ArchiveUtils.toAsciiBytes(MAGIC_OLD_ASCII));
263             count(6);
264             writeOldAsciiEntry(e);
265             break;
266         case FORMAT_OLD_BINARY:
267             final boolean swapHalfWord = true;
268             writeBinaryLong(MAGIC_OLD_BINARY, 2, swapHalfWord);
269             writeOldBinaryEntry(e, swapHalfWord);
270             break;
271         default:
272             throw new IOException("unknown format " + e.getFormat());
273         }
274     }
275 
writeNewEntry(final CpioArchiveEntry entry)276     private void writeNewEntry(final CpioArchiveEntry entry) throws IOException {
277         long inode = entry.getInode();
278         long devMin = entry.getDeviceMin();
279         if (CPIO_TRAILER.equals(entry.getName())) {
280             inode = devMin = 0;
281         } else {
282             if (inode == 0 && devMin == 0) {
283                 inode = nextArtificalDeviceAndInode & 0xFFFFFFFF;
284                 devMin = (nextArtificalDeviceAndInode++ >> 32) & 0xFFFFFFFF;
285             } else {
286                 nextArtificalDeviceAndInode =
287                     Math.max(nextArtificalDeviceAndInode,
288                              inode + 0x100000000L * devMin) + 1;
289             }
290         }
291 
292         writeAsciiLong(inode, 8, 16);
293         writeAsciiLong(entry.getMode(), 8, 16);
294         writeAsciiLong(entry.getUID(), 8, 16);
295         writeAsciiLong(entry.getGID(), 8, 16);
296         writeAsciiLong(entry.getNumberOfLinks(), 8, 16);
297         writeAsciiLong(entry.getTime(), 8, 16);
298         writeAsciiLong(entry.getSize(), 8, 16);
299         writeAsciiLong(entry.getDeviceMaj(), 8, 16);
300         writeAsciiLong(devMin, 8, 16);
301         writeAsciiLong(entry.getRemoteDeviceMaj(), 8, 16);
302         writeAsciiLong(entry.getRemoteDeviceMin(), 8, 16);
303         byte[] name = encode(entry.getName());
304         writeAsciiLong(name.length + 1L, 8, 16);
305         writeAsciiLong(entry.getChksum(), 8, 16);
306         writeCString(name);
307         pad(entry.getHeaderPadCount(name.length));
308     }
309 
writeOldAsciiEntry(final CpioArchiveEntry entry)310     private void writeOldAsciiEntry(final CpioArchiveEntry entry)
311             throws IOException {
312         long inode = entry.getInode();
313         long device = entry.getDevice();
314         if (CPIO_TRAILER.equals(entry.getName())) {
315             inode = device = 0;
316         } else {
317             if (inode == 0 && device == 0) {
318                 inode = nextArtificalDeviceAndInode & 0777777;
319                 device = (nextArtificalDeviceAndInode++ >> 18) & 0777777;
320             } else {
321                 nextArtificalDeviceAndInode =
322                     Math.max(nextArtificalDeviceAndInode,
323                              inode + 01000000 * device) + 1;
324             }
325         }
326 
327         writeAsciiLong(device, 6, 8);
328         writeAsciiLong(inode, 6, 8);
329         writeAsciiLong(entry.getMode(), 6, 8);
330         writeAsciiLong(entry.getUID(), 6, 8);
331         writeAsciiLong(entry.getGID(), 6, 8);
332         writeAsciiLong(entry.getNumberOfLinks(), 6, 8);
333         writeAsciiLong(entry.getRemoteDevice(), 6, 8);
334         writeAsciiLong(entry.getTime(), 11, 8);
335         byte[] name = encode(entry.getName());
336         writeAsciiLong(name.length + 1L, 6, 8);
337         writeAsciiLong(entry.getSize(), 11, 8);
338         writeCString(name);
339     }
340 
writeOldBinaryEntry(final CpioArchiveEntry entry, final boolean swapHalfWord)341     private void writeOldBinaryEntry(final CpioArchiveEntry entry,
342             final boolean swapHalfWord) throws IOException {
343         long inode = entry.getInode();
344         long device = entry.getDevice();
345         if (CPIO_TRAILER.equals(entry.getName())) {
346             inode = device = 0;
347         } else {
348             if (inode == 0 && device == 0) {
349                 inode = nextArtificalDeviceAndInode & 0xFFFF;
350                 device = (nextArtificalDeviceAndInode++ >> 16) & 0xFFFF;
351             } else {
352                 nextArtificalDeviceAndInode =
353                     Math.max(nextArtificalDeviceAndInode,
354                              inode + 0x10000 * device) + 1;
355             }
356         }
357 
358         writeBinaryLong(device, 2, swapHalfWord);
359         writeBinaryLong(inode, 2, swapHalfWord);
360         writeBinaryLong(entry.getMode(), 2, swapHalfWord);
361         writeBinaryLong(entry.getUID(), 2, swapHalfWord);
362         writeBinaryLong(entry.getGID(), 2, swapHalfWord);
363         writeBinaryLong(entry.getNumberOfLinks(), 2, swapHalfWord);
364         writeBinaryLong(entry.getRemoteDevice(), 2, swapHalfWord);
365         writeBinaryLong(entry.getTime(), 4, swapHalfWord);
366         byte[] name = encode(entry.getName());
367         writeBinaryLong(name.length + 1L, 2, swapHalfWord);
368         writeBinaryLong(entry.getSize(), 4, swapHalfWord);
369         writeCString(name);
370         pad(entry.getHeaderPadCount(name.length));
371     }
372 
373     /*(non-Javadoc)
374      *
375      * @see
376      * org.apache.commons.compress.archivers.ArchiveOutputStream#closeArchiveEntry
377      * ()
378      */
379     @Override
closeArchiveEntry()380     public void closeArchiveEntry() throws IOException {
381         if(finished) {
382             throw new IOException("Stream has already been finished");
383         }
384 
385         ensureOpen();
386 
387         if (entry == null) {
388             throw new IOException("Trying to close non-existent entry");
389         }
390 
391         if (this.entry.getSize() != this.written) {
392             throw new IOException("invalid entry size (expected "
393                     + this.entry.getSize() + " but got " + this.written
394                     + " bytes)");
395         }
396         pad(this.entry.getDataPadCount());
397         if (this.entry.getFormat() == FORMAT_NEW_CRC
398             && this.crc != this.entry.getChksum()) {
399             throw new IOException("CRC Error");
400         }
401         this.entry = null;
402         this.crc = 0;
403         this.written = 0;
404     }
405 
406     /**
407      * Writes an array of bytes to the current CPIO entry data. This method will
408      * block until all the bytes are written.
409      *
410      * @param b
411      *            the data to be written
412      * @param off
413      *            the start offset in the data
414      * @param len
415      *            the number of bytes that are written
416      * @throws IOException
417      *             if an I/O error has occurred or if a CPIO file error has
418      *             occurred
419      */
420     @Override
write(final byte[] b, final int off, final int len)421     public void write(final byte[] b, final int off, final int len)
422             throws IOException {
423         ensureOpen();
424         if (off < 0 || len < 0 || off > b.length - len) {
425             throw new IndexOutOfBoundsException();
426         } else if (len == 0) {
427             return;
428         }
429 
430         if (this.entry == null) {
431             throw new IOException("no current CPIO entry");
432         }
433         if (this.written + len > this.entry.getSize()) {
434             throw new IOException("attempt to write past end of STORED entry");
435         }
436         out.write(b, off, len);
437         this.written += len;
438         if (this.entry.getFormat() == FORMAT_NEW_CRC) {
439             for (int pos = 0; pos < len; pos++) {
440                 this.crc += b[pos] & 0xFF;
441                 this.crc &= 0xFFFFFFFFL;
442             }
443         }
444         count(len);
445     }
446 
447     /**
448      * Finishes writing the contents of the CPIO output stream without closing
449      * the underlying stream. Use this method when applying multiple filters in
450      * succession to the same output stream.
451      *
452      * @throws IOException
453      *             if an I/O exception has occurred or if a CPIO file error has
454      *             occurred
455      */
456     @Override
finish()457     public void finish() throws IOException {
458         ensureOpen();
459         if (finished) {
460             throw new IOException("This archive has already been finished");
461         }
462 
463         if (this.entry != null) {
464             throw new IOException("This archive contains unclosed entries.");
465         }
466         this.entry = new CpioArchiveEntry(this.entryFormat);
467         this.entry.setName(CPIO_TRAILER);
468         this.entry.setNumberOfLinks(1);
469         writeHeader(this.entry);
470         closeArchiveEntry();
471 
472         final int lengthOfLastBlock = (int) (getBytesWritten() % blockSize);
473         if (lengthOfLastBlock != 0) {
474             pad(blockSize - lengthOfLastBlock);
475         }
476 
477         finished = true;
478     }
479 
480     /**
481      * Closes the CPIO output stream as well as the stream being filtered.
482      *
483      * @throws IOException
484      *             if an I/O error has occurred or if a CPIO file error has
485      *             occurred
486      */
487     @Override
close()488     public void close() throws IOException {
489         try {
490             if (!finished) {
491                 finish();
492             }
493         } finally {
494             if (!this.closed) {
495                 out.close();
496                 this.closed = true;
497             }
498         }
499     }
500 
pad(final int count)501     private void pad(final int count) throws IOException{
502         if (count > 0){
503             final byte buff[] = new byte[count];
504             out.write(buff);
505             count(count);
506         }
507     }
508 
writeBinaryLong(final long number, final int length, final boolean swapHalfWord)509     private void writeBinaryLong(final long number, final int length,
510             final boolean swapHalfWord) throws IOException {
511         final byte tmp[] = CpioUtil.long2byteArray(number, length, swapHalfWord);
512         out.write(tmp);
513         count(tmp.length);
514     }
515 
writeAsciiLong(final long number, final int length, final int radix)516     private void writeAsciiLong(final long number, final int length,
517             final int radix) throws IOException {
518         final StringBuilder tmp = new StringBuilder();
519         String tmpStr;
520         if (radix == 16) {
521             tmp.append(Long.toHexString(number));
522         } else if (radix == 8) {
523             tmp.append(Long.toOctalString(number));
524         } else {
525             tmp.append(Long.toString(number));
526         }
527 
528         if (tmp.length() <= length) {
529             final int insertLength = length - tmp.length();
530             for (int pos = 0; pos < insertLength; pos++) {
531                 tmp.insert(0, "0");
532             }
533             tmpStr = tmp.toString();
534         } else {
535             tmpStr = tmp.substring(tmp.length() - length);
536         }
537         final byte[] b = ArchiveUtils.toAsciiBytes(tmpStr);
538         out.write(b);
539         count(b.length);
540     }
541 
542     /**
543      * Encodes the given string using the configured encoding.
544      *
545      * @param str the String to write
546      * @throws IOException if the string couldn't be written
547      * @return result of encoding the string
548      */
encode(final String str)549     private byte[] encode(final String str) throws IOException {
550         final ByteBuffer buf = zipEncoding.encode(str);
551         final int len = buf.limit() - buf.position();
552         return Arrays.copyOfRange(buf.array(), buf.arrayOffset(), buf.arrayOffset() + len);
553     }
554 
555     /**
556      * Writes an encoded string to the stream followed by \0
557      * @param str the String to write
558      * @throws IOException if the string couldn't be written
559      */
writeCString(byte[] str)560     private void writeCString(byte[] str) throws IOException {
561         out.write(str);
562         out.write('\0');
563         count(str.length + 1);
564     }
565 
566     /**
567      * Creates a new ArchiveEntry. The entryName must be an ASCII encoded string.
568      *
569      * @see org.apache.commons.compress.archivers.ArchiveOutputStream#createArchiveEntry(java.io.File, java.lang.String)
570      */
571     @Override
createArchiveEntry(final File inputFile, final String entryName)572     public ArchiveEntry createArchiveEntry(final File inputFile, final String entryName)
573             throws IOException {
574         if(finished) {
575             throw new IOException("Stream has already been finished");
576         }
577         return new CpioArchiveEntry(inputFile, entryName);
578     }
579 
580 }
581