001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.commons.compress.archivers.cpio;
020
021import java.io.File;
022import java.io.IOException;
023import java.io.OutputStream;
024import java.nio.ByteBuffer;
025import java.nio.file.LinkOption;
026import java.nio.file.Path;
027import java.util.Arrays;
028import java.util.HashMap;
029
030import org.apache.commons.compress.archivers.ArchiveEntry;
031import org.apache.commons.compress.archivers.ArchiveOutputStream;
032import org.apache.commons.compress.archivers.zip.ZipEncoding;
033import org.apache.commons.compress.archivers.zip.ZipEncodingHelper;
034import org.apache.commons.compress.utils.ArchiveUtils;
035import org.apache.commons.compress.utils.CharsetNames;
036
037/**
038 * CpioArchiveOutputStream is a stream for writing CPIO streams. All formats of
039 * CPIO are supported (old ASCII, old binary, new portable format and the new
040 * portable format with CRC).
041 *
042 * <p>An entry can be written by creating an instance of CpioArchiveEntry and fill
043 * it with the necessary values and put it into the CPIO stream. Afterwards
044 * write the contents of the file into the CPIO stream. Either close the stream
045 * by calling finish() or put a next entry into the cpio stream.</p>
046 *
047 * <pre>
048 * CpioArchiveOutputStream out = new CpioArchiveOutputStream(
049 *         new FileOutputStream(new File("test.cpio")));
050 * CpioArchiveEntry entry = new CpioArchiveEntry();
051 * entry.setName("testfile");
052 * String contents = &quot;12345&quot;;
053 * entry.setFileSize(contents.length());
054 * entry.setMode(CpioConstants.C_ISREG); // regular file
055 * ... set other attributes, e.g. time, number of links
056 * out.putArchiveEntry(entry);
057 * out.write(testContents.getBytes());
058 * out.close();
059 * </pre>
060 *
061 * <p>Note: This implementation should be compatible to cpio 2.5</p>
062 *
063 * <p>This class uses mutable fields and is not considered threadsafe.</p>
064 *
065 * <p>based on code from the jRPM project (jrpm.sourceforge.net)</p>
066 */
067public class CpioArchiveOutputStream extends ArchiveOutputStream implements
068        CpioConstants {
069
070    private CpioArchiveEntry entry;
071
072    private boolean closed;
073
074    /** indicates if this archive is finished */
075    private boolean finished;
076
077    /**
078     * See {@link CpioArchiveEntry#CpioArchiveEntry(short)} for possible values.
079     */
080    private final short entryFormat;
081
082    private final HashMap<String, CpioArchiveEntry> names =
083        new HashMap<>();
084
085    private long crc;
086
087    private long written;
088
089    private final OutputStream out;
090
091    private final int blockSize;
092
093    private long nextArtificalDeviceAndInode = 1;
094
095    /**
096     * The encoding to use for file names and labels.
097     */
098    private final ZipEncoding zipEncoding;
099
100    // the provided encoding (for unit tests)
101    final String encoding;
102
103    /**
104     * Construct the cpio output stream. The format for this CPIO stream is the
105     * "new" format using ASCII encoding for file names
106     *
107     * @param out
108     *            The cpio stream
109     */
110    public CpioArchiveOutputStream(final OutputStream out) {
111        this(out, FORMAT_NEW);
112    }
113
114    /**
115     * Construct the cpio output stream with a specified format, a
116     * blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE} and
117     * using ASCII as the file name encoding.
118     *
119     * @param out
120     *            The cpio stream
121     * @param format
122     *            The format of the stream
123     */
124    public CpioArchiveOutputStream(final OutputStream out, final short format) {
125        this(out, format, BLOCK_SIZE, CharsetNames.US_ASCII);
126    }
127
128    /**
129     * Construct the cpio output stream with a specified format using
130     * ASCII as the file name encoding.
131     *
132     * @param out
133     *            The cpio stream
134     * @param format
135     *            The format of the stream
136     * @param blockSize
137     *            The block size of the archive.
138     *
139     * @since 1.1
140     */
141    public CpioArchiveOutputStream(final OutputStream out, final short format,
142                                   final int blockSize) {
143        this(out, format, blockSize, CharsetNames.US_ASCII);
144    }
145
146    /**
147     * Construct the cpio output stream with a specified format using
148     * ASCII as the file name encoding.
149     *
150     * @param out
151     *            The cpio stream
152     * @param format
153     *            The format of the stream
154     * @param blockSize
155     *            The block size of the archive.
156     * @param encoding
157     *            The encoding of file names to write - use null for
158     *            the platform's default.
159     *
160     * @since 1.6
161     */
162    public CpioArchiveOutputStream(final OutputStream out, final short format,
163                                   final int blockSize, final String encoding) {
164        this.out = out;
165        switch (format) {
166        case FORMAT_NEW:
167        case FORMAT_NEW_CRC:
168        case FORMAT_OLD_ASCII:
169        case FORMAT_OLD_BINARY:
170            break;
171        default:
172            throw new IllegalArgumentException("Unknown format: "+format);
173
174        }
175        this.entryFormat = format;
176        this.blockSize = blockSize;
177        this.encoding = encoding;
178        this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
179    }
180
181    /**
182     * Construct the cpio output stream. The format for this CPIO stream is the
183     * "new" format.
184     *
185     * @param out
186     *            The cpio stream
187     * @param encoding
188     *            The encoding of file names to write - use null for
189     *            the platform's default.
190     * @since 1.6
191     */
192    public CpioArchiveOutputStream(final OutputStream out, final String encoding) {
193        this(out, FORMAT_NEW, BLOCK_SIZE, encoding);
194    }
195
196    /**
197     * Closes the CPIO output stream as well as the stream being filtered.
198     *
199     * @throws IOException
200     *             if an I/O error has occurred or if a CPIO file error has
201     *             occurred
202     */
203    @Override
204    public void close() throws IOException {
205        try {
206            if (!finished) {
207                finish();
208            }
209        } finally {
210            if (!this.closed) {
211                out.close();
212                this.closed = true;
213            }
214        }
215    }
216
217    /*(non-Javadoc)
218     *
219     * @see
220     * org.apache.commons.compress.archivers.ArchiveOutputStream#closeArchiveEntry
221     * ()
222     */
223    @Override
224    public void closeArchiveEntry() throws IOException {
225        if (finished) {
226            throw new IOException("Stream has already been finished");
227        }
228
229        ensureOpen();
230
231        if (entry == null) {
232            throw new IOException("Trying to close non-existent entry");
233        }
234
235        if (this.entry.getSize() != this.written) {
236            throw new IOException("Invalid entry size (expected "
237                    + this.entry.getSize() + " but got " + this.written
238                    + " bytes)");
239        }
240        pad(this.entry.getDataPadCount());
241        if (this.entry.getFormat() == FORMAT_NEW_CRC
242            && this.crc != this.entry.getChksum()) {
243            throw new IOException("CRC Error");
244        }
245        this.entry = null;
246        this.crc = 0;
247        this.written = 0;
248    }
249
250    /**
251     * Creates a new ArchiveEntry. The entryName must be an ASCII encoded string.
252     *
253     * @see org.apache.commons.compress.archivers.ArchiveOutputStream#createArchiveEntry(java.io.File, String)
254     */
255    @Override
256    public ArchiveEntry createArchiveEntry(final File inputFile, final String entryName)
257            throws IOException {
258        if (finished) {
259            throw new IOException("Stream has already been finished");
260        }
261        return new CpioArchiveEntry(inputFile, entryName);
262    }
263
264    /**
265     * Creates a new ArchiveEntry. The entryName must be an ASCII encoded string.
266     *
267     * @see org.apache.commons.compress.archivers.ArchiveOutputStream#createArchiveEntry(java.io.File, String)
268     */
269    @Override
270    public ArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options)
271            throws IOException {
272        if (finished) {
273            throw new IOException("Stream has already been finished");
274        }
275        return new CpioArchiveEntry(inputPath, entryName, options);
276    }
277
278    /**
279     * Encodes the given string using the configured encoding.
280     *
281     * @param str the String to write
282     * @throws IOException if the string couldn't be written
283     * @return result of encoding the string
284     */
285    private byte[] encode(final String str) throws IOException {
286        final ByteBuffer buf = zipEncoding.encode(str);
287        final int len = buf.limit() - buf.position();
288        return Arrays.copyOfRange(buf.array(), buf.arrayOffset(), buf.arrayOffset() + len);
289    }
290
291    /**
292     * Check to make sure that this stream has not been closed
293     *
294     * @throws IOException
295     *             if the stream is already closed
296     */
297    private void ensureOpen() throws IOException {
298        if (this.closed) {
299            throw new IOException("Stream closed");
300        }
301    }
302
303    /**
304     * Finishes writing the contents of the CPIO output stream without closing
305     * the underlying stream. Use this method when applying multiple filters in
306     * succession to the same output stream.
307     *
308     * @throws IOException
309     *             if an I/O exception has occurred or if a CPIO file error has
310     *             occurred
311     */
312    @Override
313    public void finish() throws IOException {
314        ensureOpen();
315        if (finished) {
316            throw new IOException("This archive has already been finished");
317        }
318
319        if (this.entry != null) {
320            throw new IOException("This archive contains unclosed entries.");
321        }
322        this.entry = new CpioArchiveEntry(this.entryFormat);
323        this.entry.setName(CPIO_TRAILER);
324        this.entry.setNumberOfLinks(1);
325        writeHeader(this.entry);
326        closeArchiveEntry();
327
328        final int lengthOfLastBlock = (int) (getBytesWritten() % blockSize);
329        if (lengthOfLastBlock != 0) {
330            pad(blockSize - lengthOfLastBlock);
331        }
332
333        finished = true;
334    }
335
336    private void pad(final int count) throws IOException{
337        if (count > 0){
338            final byte[] buff = new byte[count];
339            out.write(buff);
340            count(count);
341        }
342    }
343
344    /**
345     * Begins writing a new CPIO file entry and positions the stream to the
346     * start of the entry data. Closes the current entry if still active. The
347     * current time will be used if the entry has no set modification time and
348     * the default header format will be used if no other format is specified in
349     * the entry.
350     *
351     * @param entry
352     *            the CPIO cpioEntry to be written
353     * @throws IOException
354     *             if an I/O error has occurred or if a CPIO file error has
355     *             occurred
356     * @throws ClassCastException if entry is not an instance of CpioArchiveEntry
357     */
358    @Override
359    public void putArchiveEntry(final ArchiveEntry entry) throws IOException {
360        if (finished) {
361            throw new IOException("Stream has already been finished");
362        }
363
364        final CpioArchiveEntry e = (CpioArchiveEntry) entry;
365        ensureOpen();
366        if (this.entry != null) {
367            closeArchiveEntry(); // close previous entry
368        }
369        if (e.getTime() == -1) {
370            e.setTime(System.currentTimeMillis() / 1000);
371        }
372
373        final short format = e.getFormat();
374        if (format != this.entryFormat){
375            throw new IOException("Header format: "+format+" does not match existing format: "+this.entryFormat);
376        }
377
378        if (this.names.put(e.getName(), e) != null) {
379            throw new IOException("Duplicate entry: " + e.getName());
380        }
381
382        writeHeader(e);
383        this.entry = e;
384        this.written = 0;
385    }
386
387    /**
388     * Writes an array of bytes to the current CPIO entry data. This method will
389     * block until all the bytes are written.
390     *
391     * @param b
392     *            the data to be written
393     * @param off
394     *            the start offset in the data
395     * @param len
396     *            the number of bytes that are written
397     * @throws IOException
398     *             if an I/O error has occurred or if a CPIO file error has
399     *             occurred
400     */
401    @Override
402    public void write(final byte[] b, final int off, final int len)
403            throws IOException {
404        ensureOpen();
405        if (off < 0 || len < 0 || off > b.length - len) {
406            throw new IndexOutOfBoundsException();
407        }
408        if (len == 0) {
409            return;
410        }
411
412        if (this.entry == null) {
413            throw new IOException("No current CPIO entry");
414        }
415        if (this.written + len > this.entry.getSize()) {
416            throw new IOException("Attempt to write past end of STORED entry");
417        }
418        out.write(b, off, len);
419        this.written += len;
420        if (this.entry.getFormat() == FORMAT_NEW_CRC) {
421            for (int pos = 0; pos < len; pos++) {
422                this.crc += b[pos] & 0xFF;
423                this.crc &= 0xFFFFFFFFL;
424            }
425        }
426        count(len);
427    }
428
429    private void writeAsciiLong(final long number, final int length,
430            final int radix) throws IOException {
431        final StringBuilder tmp = new StringBuilder();
432        final String tmpStr;
433        if (radix == 16) {
434            tmp.append(Long.toHexString(number));
435        } else if (radix == 8) {
436            tmp.append(Long.toOctalString(number));
437        } else {
438            tmp.append(number);
439        }
440
441        if (tmp.length() <= length) {
442            final int insertLength = length - tmp.length();
443            for (int pos = 0; pos < insertLength; pos++) {
444                tmp.insert(0, "0");
445            }
446            tmpStr = tmp.toString();
447        } else {
448            tmpStr = tmp.substring(tmp.length() - length);
449        }
450        final byte[] b = ArchiveUtils.toAsciiBytes(tmpStr);
451        out.write(b);
452        count(b.length);
453    }
454
455    private void writeBinaryLong(final long number, final int length,
456            final boolean swapHalfWord) throws IOException {
457        final byte[] tmp = CpioUtil.long2byteArray(number, length, swapHalfWord);
458        out.write(tmp);
459        count(tmp.length);
460    }
461
462    /**
463     * Writes an encoded string to the stream followed by \0
464     * @param str the String to write
465     * @throws IOException if the string couldn't be written
466     */
467    private void writeCString(final byte[] str) throws IOException {
468        out.write(str);
469        out.write('\0');
470        count(str.length + 1);
471    }
472
473    private void writeHeader(final CpioArchiveEntry e) throws IOException {
474        switch (e.getFormat()) {
475        case FORMAT_NEW:
476            out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW));
477            count(6);
478            writeNewEntry(e);
479            break;
480        case FORMAT_NEW_CRC:
481            out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW_CRC));
482            count(6);
483            writeNewEntry(e);
484            break;
485        case FORMAT_OLD_ASCII:
486            out.write(ArchiveUtils.toAsciiBytes(MAGIC_OLD_ASCII));
487            count(6);
488            writeOldAsciiEntry(e);
489            break;
490        case FORMAT_OLD_BINARY:
491            final boolean swapHalfWord = true;
492            writeBinaryLong(MAGIC_OLD_BINARY, 2, swapHalfWord);
493            writeOldBinaryEntry(e, swapHalfWord);
494            break;
495        default:
496            throw new IOException("Unknown format " + e.getFormat());
497        }
498    }
499
500    private void writeNewEntry(final CpioArchiveEntry entry) throws IOException {
501        long inode = entry.getInode();
502        long devMin = entry.getDeviceMin();
503        if (CPIO_TRAILER.equals(entry.getName())) {
504            inode = devMin = 0;
505        } else if (inode == 0 && devMin == 0) {
506            inode = nextArtificalDeviceAndInode & 0xFFFFFFFF;
507            devMin = (nextArtificalDeviceAndInode++ >> 32) & 0xFFFFFFFF;
508        } else {
509            nextArtificalDeviceAndInode =
510                Math.max(nextArtificalDeviceAndInode,
511                         inode + 0x100000000L * devMin) + 1;
512        }
513
514        writeAsciiLong(inode, 8, 16);
515        writeAsciiLong(entry.getMode(), 8, 16);
516        writeAsciiLong(entry.getUID(), 8, 16);
517        writeAsciiLong(entry.getGID(), 8, 16);
518        writeAsciiLong(entry.getNumberOfLinks(), 8, 16);
519        writeAsciiLong(entry.getTime(), 8, 16);
520        writeAsciiLong(entry.getSize(), 8, 16);
521        writeAsciiLong(entry.getDeviceMaj(), 8, 16);
522        writeAsciiLong(devMin, 8, 16);
523        writeAsciiLong(entry.getRemoteDeviceMaj(), 8, 16);
524        writeAsciiLong(entry.getRemoteDeviceMin(), 8, 16);
525        final byte[] name = encode(entry.getName());
526        writeAsciiLong(name.length + 1L, 8, 16);
527        writeAsciiLong(entry.getChksum(), 8, 16);
528        writeCString(name);
529        pad(entry.getHeaderPadCount(name.length));
530    }
531
532    private void writeOldAsciiEntry(final CpioArchiveEntry entry)
533            throws IOException {
534        long inode = entry.getInode();
535        long device = entry.getDevice();
536        if (CPIO_TRAILER.equals(entry.getName())) {
537            inode = device = 0;
538        } else if (inode == 0 && device == 0) {
539            inode = nextArtificalDeviceAndInode & 0777777;
540            device = (nextArtificalDeviceAndInode++ >> 18) & 0777777;
541        } else {
542            nextArtificalDeviceAndInode =
543                Math.max(nextArtificalDeviceAndInode,
544                         inode + 01000000 * device) + 1;
545        }
546
547        writeAsciiLong(device, 6, 8);
548        writeAsciiLong(inode, 6, 8);
549        writeAsciiLong(entry.getMode(), 6, 8);
550        writeAsciiLong(entry.getUID(), 6, 8);
551        writeAsciiLong(entry.getGID(), 6, 8);
552        writeAsciiLong(entry.getNumberOfLinks(), 6, 8);
553        writeAsciiLong(entry.getRemoteDevice(), 6, 8);
554        writeAsciiLong(entry.getTime(), 11, 8);
555        final byte[] name = encode(entry.getName());
556        writeAsciiLong(name.length + 1L, 6, 8);
557        writeAsciiLong(entry.getSize(), 11, 8);
558        writeCString(name);
559    }
560
561    private void writeOldBinaryEntry(final CpioArchiveEntry entry,
562            final boolean swapHalfWord) throws IOException {
563        long inode = entry.getInode();
564        long device = entry.getDevice();
565        if (CPIO_TRAILER.equals(entry.getName())) {
566            inode = device = 0;
567        } else if (inode == 0 && device == 0) {
568            inode = nextArtificalDeviceAndInode & 0xFFFF;
569            device = (nextArtificalDeviceAndInode++ >> 16) & 0xFFFF;
570        } else {
571            nextArtificalDeviceAndInode =
572                Math.max(nextArtificalDeviceAndInode,
573                         inode + 0x10000 * device) + 1;
574        }
575
576        writeBinaryLong(device, 2, swapHalfWord);
577        writeBinaryLong(inode, 2, swapHalfWord);
578        writeBinaryLong(entry.getMode(), 2, swapHalfWord);
579        writeBinaryLong(entry.getUID(), 2, swapHalfWord);
580        writeBinaryLong(entry.getGID(), 2, swapHalfWord);
581        writeBinaryLong(entry.getNumberOfLinks(), 2, swapHalfWord);
582        writeBinaryLong(entry.getRemoteDevice(), 2, swapHalfWord);
583        writeBinaryLong(entry.getTime(), 4, swapHalfWord);
584        final byte[] name = encode(entry.getName());
585        writeBinaryLong(name.length + 1L, 2, swapHalfWord);
586        writeBinaryLong(entry.getSize(), 4, swapHalfWord);
587        writeCString(name);
588        pad(entry.getHeaderPadCount(name.length));
589    }
590
591}