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.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  
34  /**
35   * CpioArchiveOutputStream is a stream for writing CPIO streams. All formats of CPIO are supported (old ASCII, old binary, new portable format and the new
36   * portable format with CRC).
37   *
38   * <p>
39   * 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
40   * 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.
41   * </p>
42   *
43   * <pre>
44   * CpioArchiveOutputStream out = new CpioArchiveOutputStream(
45   *         new FileOutputStream(new File("test.cpio")));
46   * CpioArchiveEntry entry = new CpioArchiveEntry();
47   * entry.setName("testfile");
48   * String contents = &quot;12345&quot;;
49   * entry.setFileSize(contents.length());
50   * entry.setMode(CpioConstants.C_ISREG); // regular file
51   * ... set other attributes, for example time, number of links
52   * out.putArchiveEntry(entry);
53   * out.write(testContents.getBytes());
54   * out.close();
55   * </pre>
56   *
57   * <p>
58   * Note: This implementation should be compatible to cpio 2.5
59   * </p>
60   *
61   * <p>
62   * This class uses mutable fields and is not considered threadsafe.
63   * </p>
64   *
65   * <p>
66   * based on code from the jRPM project (jrpm.sourceforge.net)
67   * </p>
68   */
69  public class CpioArchiveOutputStream extends ArchiveOutputStream<CpioArchiveEntry> implements CpioConstants {
70  
71      /**
72       * The NUL character.
73       */
74      private static final char NUL = '\0';
75  
76      private CpioArchiveEntry entry;
77  
78      /**
79       * See {@link CpioArchiveEntry#CpioArchiveEntry(short)} for possible values.
80       */
81      private final short entryFormat;
82  
83      private final HashMap<String, CpioArchiveEntry> names = new HashMap<>();
84  
85      private long crc;
86  
87      private long written;
88  
89      private final int blockSize;
90  
91      private long nextArtificalDeviceAndInode = 1;
92  
93      /**
94       * The encoding to use for file names and labels.
95       */
96      private final ZipEncoding zipEncoding;
97  
98      // the provided encoding (for unit tests)
99      final String charsetName;
100 
101     /**
102      * Constructs the cpio output stream. The format for this CPIO stream is the "new" format using ASCII encoding for file names
103      *
104      * @param out The cpio stream
105      */
106     public CpioArchiveOutputStream(final OutputStream out) {
107         this(out, FORMAT_NEW);
108     }
109 
110     /**
111      * 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
112      * encoding.
113      *
114      * @param out    The cpio stream
115      * @param format The format of the stream
116      */
117     public CpioArchiveOutputStream(final OutputStream out, final short format) {
118         this(out, format, BLOCK_SIZE, CpioUtil.DEFAULT_CHARSET_NAME);
119     }
120 
121     /**
122      * Constructs the cpio output stream with a specified format using ASCII as the file name encoding.
123      *
124      * @param out       The cpio stream
125      * @param format    The format of the stream
126      * @param blockSize The block size of the archive.
127      * @since 1.1
128      */
129     public CpioArchiveOutputStream(final OutputStream out, final short format, final int blockSize) {
130         this(out, format, blockSize, CpioUtil.DEFAULT_CHARSET_NAME);
131     }
132 
133     /**
134      * Constructs the cpio output stream with a specified format using ASCII as the file name encoding.
135      *
136      * @param out       The cpio stream
137      * @param format    The format of the stream
138      * @param blockSize The block size of the archive.
139      * @param encoding  The encoding of file names to write - use null for the platform's default.
140      * @since 1.6
141      */
142     public CpioArchiveOutputStream(final OutputStream out, final short format, final int blockSize, final String encoding) {
143         super(out);
144         switch (format) {
145         case FORMAT_NEW:
146         case FORMAT_NEW_CRC:
147         case FORMAT_OLD_ASCII:
148         case FORMAT_OLD_BINARY:
149             break;
150         default:
151             throw new IllegalArgumentException("Unknown format: " + format);
152 
153         }
154         this.entryFormat = format;
155         this.blockSize = blockSize;
156         this.charsetName = encoding;
157         this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
158     }
159 
160     /**
161      * Constructs the cpio output stream. The format for this CPIO stream is the "new" format.
162      *
163      * @param out      The cpio stream
164      * @param encoding The encoding of file names to write - use null for the platform's default.
165      * @since 1.6
166      */
167     public CpioArchiveOutputStream(final OutputStream out, final String encoding) {
168         this(out, FORMAT_NEW, BLOCK_SIZE, encoding);
169     }
170 
171     /**
172      * Closes the CPIO output stream as well as the stream being filtered.
173      *
174      * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred
175      */
176     @Override
177     public void close() throws IOException {
178         try {
179             if (!isFinished()) {
180                 finish();
181             }
182         } finally {
183             super.close();
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             out.write(new byte[count]);
277             count(count);
278         }
279     }
280 
281     /**
282      * 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
283      * 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.
284      *
285      * @param entry the CPIO cpioEntry to be written
286      * @throws IOException        if an I/O error has occurred or if a CPIO file error has occurred
287      * @throws ClassCastException if entry is not an instance of CpioArchiveEntry
288      */
289     @Override
290     public void putArchiveEntry(final CpioArchiveEntry entry) throws IOException {
291         checkFinished();
292         checkOpen();
293         if (this.entry != null) {
294             closeArchiveEntry(); // close previous entry
295         }
296         if (entry.getTime() == -1) {
297             entry.setTime(System.currentTimeMillis() / 1000);
298         }
299 
300         final short format = entry.getFormat();
301         if (format != this.entryFormat) {
302             throw new IOException("Header format: " + format + " does not match existing format: " + this.entryFormat);
303         }
304 
305         if (this.names.put(entry.getName(), entry) != null) {
306             throw new IOException("Duplicate entry: " + entry.getName());
307         }
308 
309         writeHeader(entry);
310         this.entry = entry;
311         this.written = 0;
312     }
313 
314     /**
315      * Writes an array of bytes to the current CPIO entry data. This method will block until all the bytes are written.
316      *
317      * @param b   the data to be written
318      * @param off the start offset in the data
319      * @param len the number of bytes that are written
320      * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred
321      */
322     @Override
323     public void write(final byte[] b, final int off, final int len) throws IOException {
324         checkOpen();
325         if (off < 0 || len < 0 || off > b.length - len) {
326             throw new IndexOutOfBoundsException();
327         }
328         if (len == 0) {
329             return;
330         }
331 
332         if (this.entry == null) {
333             throw new IOException("No current CPIO entry");
334         }
335         if (this.written + len > this.entry.getSize()) {
336             throw new IOException("Attempt to write past end of STORED entry");
337         }
338         out.write(b, off, len);
339         this.written += len;
340         if (this.entry.getFormat() == FORMAT_NEW_CRC) {
341             for (int pos = 0; pos < len; pos++) {
342                 this.crc += b[pos] & 0xFF;
343                 this.crc &= 0xFFFFFFFFL;
344             }
345         }
346         count(len);
347     }
348 
349     private void writeAsciiLong(final long number, final int length, final int radix) throws IOException {
350         final StringBuilder tmp = new StringBuilder();
351         final String tmpStr;
352         if (radix == 16) {
353             tmp.append(Long.toHexString(number));
354         } else if (radix == 8) {
355             tmp.append(Long.toOctalString(number));
356         } else {
357             tmp.append(number);
358         }
359 
360         if (tmp.length() <= length) {
361             final int insertLength = length - tmp.length();
362             for (int pos = 0; pos < insertLength; pos++) {
363                 tmp.insert(0, "0");
364             }
365             tmpStr = tmp.toString();
366         } else {
367             tmpStr = tmp.substring(tmp.length() - length);
368         }
369         final byte[] b = writeUsAsciiRaw(tmpStr);
370         count(b.length);
371     }
372 
373     private void writeBinaryLong(final long number, final int length, final boolean swapHalfWord) throws IOException {
374         final byte[] tmp = CpioUtil.long2byteArray(number, length, swapHalfWord);
375         out.write(tmp);
376         count(tmp.length);
377     }
378 
379     /**
380      * Writes an encoded string to the stream followed by \0
381      *
382      * @param str the String to write
383      * @throws IOException if the string couldn't be written
384      */
385     private void writeCString(final byte[] str) throws IOException {
386         out.write(str);
387         out.write(NUL);
388         count(str.length + 1);
389     }
390 
391     private void writeHeader(final CpioArchiveEntry e) throws IOException {
392         switch (e.getFormat()) {
393         case FORMAT_NEW:
394             writeUsAsciiRaw(MAGIC_NEW);
395             count(6);
396             writeNewEntry(e);
397             break;
398         case FORMAT_NEW_CRC:
399             writeUsAsciiRaw(MAGIC_NEW_CRC);
400             count(6);
401             writeNewEntry(e);
402             break;
403         case FORMAT_OLD_ASCII:
404             writeUsAsciiRaw(MAGIC_OLD_ASCII);
405             count(6);
406             writeOldAsciiEntry(e);
407             break;
408         case FORMAT_OLD_BINARY:
409             final boolean swapHalfWord = true;
410             writeBinaryLong(MAGIC_OLD_BINARY, 2, swapHalfWord);
411             writeOldBinaryEntry(e, swapHalfWord);
412             break;
413         default:
414             throw new IOException("Unknown format " + e.getFormat());
415         }
416     }
417 
418     private void writeNewEntry(final CpioArchiveEntry entry) throws IOException {
419         long inode = entry.getInode();
420         long devMin = entry.getDeviceMin();
421         if (CPIO_TRAILER.equals(entry.getName())) {
422             inode = devMin = 0;
423         } else if (inode == 0 && devMin == 0) {
424             inode = nextArtificalDeviceAndInode & 0xFFFFFFFF;
425             devMin = nextArtificalDeviceAndInode++ >> 32 & 0xFFFFFFFF;
426         } else {
427             nextArtificalDeviceAndInode = Math.max(nextArtificalDeviceAndInode, inode + 0x100000000L * devMin) + 1;
428         }
429 
430         writeAsciiLong(inode, 8, 16);
431         writeAsciiLong(entry.getMode(), 8, 16);
432         writeAsciiLong(entry.getUID(), 8, 16);
433         writeAsciiLong(entry.getGID(), 8, 16);
434         writeAsciiLong(entry.getNumberOfLinks(), 8, 16);
435         writeAsciiLong(entry.getTime(), 8, 16);
436         writeAsciiLong(entry.getSize(), 8, 16);
437         writeAsciiLong(entry.getDeviceMaj(), 8, 16);
438         writeAsciiLong(devMin, 8, 16);
439         writeAsciiLong(entry.getRemoteDeviceMaj(), 8, 16);
440         writeAsciiLong(entry.getRemoteDeviceMin(), 8, 16);
441         final byte[] name = encode(entry.getName());
442         writeAsciiLong(name.length + 1L, 8, 16);
443         writeAsciiLong(entry.getChksum(), 8, 16);
444         writeCString(name);
445         pad(entry.getHeaderPadCount(name.length));
446     }
447 
448     private void writeOldAsciiEntry(final CpioArchiveEntry entry) throws IOException {
449         long inode = entry.getInode();
450         long device = entry.getDevice();
451         if (CPIO_TRAILER.equals(entry.getName())) {
452             inode = device = 0;
453         } else if (inode == 0 && device == 0) {
454             inode = nextArtificalDeviceAndInode & 0777777;
455             device = nextArtificalDeviceAndInode++ >> 18 & 0777777;
456         } else {
457             nextArtificalDeviceAndInode = Math.max(nextArtificalDeviceAndInode, inode + 01000000 * device) + 1;
458         }
459 
460         writeAsciiLong(device, 6, 8);
461         writeAsciiLong(inode, 6, 8);
462         writeAsciiLong(entry.getMode(), 6, 8);
463         writeAsciiLong(entry.getUID(), 6, 8);
464         writeAsciiLong(entry.getGID(), 6, 8);
465         writeAsciiLong(entry.getNumberOfLinks(), 6, 8);
466         writeAsciiLong(entry.getRemoteDevice(), 6, 8);
467         writeAsciiLong(entry.getTime(), 11, 8);
468         final byte[] name = encode(entry.getName());
469         writeAsciiLong(name.length + 1L, 6, 8);
470         writeAsciiLong(entry.getSize(), 11, 8);
471         writeCString(name);
472     }
473 
474     private void writeOldBinaryEntry(final CpioArchiveEntry entry, final boolean swapHalfWord) throws IOException {
475         long inode = entry.getInode();
476         long device = entry.getDevice();
477         if (CPIO_TRAILER.equals(entry.getName())) {
478             inode = device = 0;
479         } else if (inode == 0 && device == 0) {
480             inode = nextArtificalDeviceAndInode & 0xFFFF;
481             device = nextArtificalDeviceAndInode++ >> 16 & 0xFFFF;
482         } else {
483             nextArtificalDeviceAndInode = Math.max(nextArtificalDeviceAndInode, inode + 0x10000 * device) + 1;
484         }
485 
486         writeBinaryLong(device, 2, swapHalfWord);
487         writeBinaryLong(inode, 2, swapHalfWord);
488         writeBinaryLong(entry.getMode(), 2, swapHalfWord);
489         writeBinaryLong(entry.getUID(), 2, swapHalfWord);
490         writeBinaryLong(entry.getGID(), 2, swapHalfWord);
491         writeBinaryLong(entry.getNumberOfLinks(), 2, swapHalfWord);
492         writeBinaryLong(entry.getRemoteDevice(), 2, swapHalfWord);
493         writeBinaryLong(entry.getTime(), 4, swapHalfWord);
494         final byte[] name = encode(entry.getName());
495         writeBinaryLong(name.length + 1L, 2, swapHalfWord);
496         writeBinaryLong(entry.getSize(), 4, swapHalfWord);
497         writeCString(name);
498         pad(entry.getHeaderPadCount(name.length));
499     }
500 
501 }