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