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