    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}