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