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