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