    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     */
    019    package org.apache.commons.compress.archivers.cpio;
    020
    021    import java.io.File;
    022    import java.io.FilterOutputStream;
    023    import java.io.IOException;
    024    import java.io.OutputStream;
    025    import java.util.HashMap;
    026
    027    import org.apache.commons.compress.archivers.ArchiveEntry;
    028    import org.apache.commons.compress.archivers.ArchiveOutputStream;
    029    import org.apache.commons.compress.utils.ArchiveUtils;
    030
    031    /**
    032     * CPIOArchiveOutputStream is a stream for writing CPIO streams. All formats of
    033     * CPIO are supported (old ASCII, old binary, new portable format and the new
    034     * portable format with CRC).
    035     * <p/>
    036     * <p/>
    037     * An entry can be written by creating an instance of CpioArchiveEntry and fill
    038     * it with the necessary values and put it into the CPIO stream. Afterwards
    039     * write the contents of the file into the CPIO stream. Either close the stream
    040     * by calling finish() or put a next entry into the cpio stream.
    041     * <p/>
    042     * <code><pre>
    043     * CpioArchiveOutputStream out = new CpioArchiveOutputStream(
    044     *         new FileOutputStream(new File("test.cpio")));
    045     * CpioArchiveEntry entry = new CpioArchiveEntry();
    046     * entry.setName("testfile");
    047     * String contents = &quot;12345&quot;;
    048     * entry.setFileSize(contents.length());
    049     * entry.setMode(CpioConstants.C_ISREG); // regular file
    050     * ... set other attributes, e.g. time, number of links
    051     * out.putNextEntry(entry);
    052     * out.write(testContents.getBytes());
    053     * out.close();
    054     * </pre></code>
    055     * <p/>
    056     * Note: This implementation should be compatible to cpio 2.5
    057     *
    058     * This class uses mutable fields and is not considered threadsafe.
    059     *
    060     * based on code from the jRPM project (jrpm.sourceforge.net)
    061     */
    062    public class CpioArchiveOutputStream extends ArchiveOutputStream implements
    063            CpioConstants {
    064
    065        private CpioArchiveEntry entry;
    066
    067        private boolean closed = false;
    068
    069        /** indicates if this archive is finished */
    070        private boolean finished;
    071
    072        /**
    073         * See {@link CpioArchiveEntry#setFormat(short)} for possible values.
    074         */
    075        private final short entryFormat;
    076
    077        private final HashMap names = new HashMap();
    078
    079        private long crc = 0;
    080
    081        private long written;
    082
    083        private final OutputStream out;
    084
    085        /**
    086         * Construct the cpio output stream with a specified format
    087         *
    088         * @param out
    089         *            The cpio stream
    090         * @param format
    091         *            The format of the stream
    092         */
    093        public CpioArchiveOutputStream(final OutputStream out, final short format) {
    094            this.out = new FilterOutputStream(out);
    095            switch (format) {
    096            case FORMAT_NEW:
    097            case FORMAT_NEW_CRC:
    098            case FORMAT_OLD_ASCII:
    099            case FORMAT_OLD_BINARY:
    100                break;
    101            default:
    102                throw new IllegalArgumentException("Unknown format: "+format);
    103
    104            }
    105            this.entryFormat = format;
    106        }
    107
    108        /**
    109         * Construct the cpio output stream. The format for this CPIO stream is the
    110         * "new" format
    111         *
    112         * @param out
    113         *            The cpio stream
    114         */
    115        public CpioArchiveOutputStream(final OutputStream out) {
    116            this(out, FORMAT_NEW);
    117        }
    118
    119        /**
    120         * Check to make sure that this stream has not been closed
    121         *
    122         * @throws IOException
    123         *             if the stream is already closed
    124         */
    125        private void ensureOpen() throws IOException {
    126            if (this.closed) {
    127                throw new IOException("Stream closed");
    128            }
    129        }
    130
    131        /**
    132         * Begins writing a new CPIO file entry and positions the stream to the
    133         * start of the entry data. Closes the current entry if still active. The
    134         * current time will be used if the entry has no set modification time and
    135         * the default header format will be used if no other format is specified in
    136         * the entry.
    137         *
    138         * @param entry
    139         *            the CPIO cpioEntry to be written
    140         * @throws IOException
    141         *             if an I/O error has occurred or if a CPIO file error has
    142         *             occurred
    143         * @throws ClassCastException if entry is not an instance of CpioArchiveEntry
    144         */
    145        public void putArchiveEntry(ArchiveEntry entry) throws IOException {
    146            if(finished) {
    147                throw new IOException("Stream has already been finished");
    148            }
    149
    150            CpioArchiveEntry e = (CpioArchiveEntry) entry;
    151            ensureOpen();
    152            if (this.entry != null) {
    153                closeArchiveEntry(); // close previous entry
    154            }
    155            if (e.getTime() == -1) {
    156                e.setTime(System.currentTimeMillis());
    157            }
    158
    159            final short format = e.getFormat();
    160            if (format != this.entryFormat){
    161                throw new IOException("Header format: "+format+" does not match existing format: "+this.entryFormat);
    162            }
    163
    164            if (this.names.put(e.getName(), e) != null) {
    165                throw new IOException("duplicate entry: " + e.getName());
    166            }
    167
    168            writeHeader(e);
    169            this.entry = e;
    170            this.written = 0;
    171        }
    172
    173        private void writeHeader(final CpioArchiveEntry e) throws IOException {
    174            switch (e.getFormat()) {
    175            case FORMAT_NEW:
    176                out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW));
    177                writeNewEntry(e);
    178                break;
    179            case FORMAT_NEW_CRC:
    180                out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW_CRC));
    181                writeNewEntry(e);
    182                break;
    183            case FORMAT_OLD_ASCII:
    184                out.write(ArchiveUtils.toAsciiBytes(MAGIC_OLD_ASCII));
    185                writeOldAsciiEntry(e);
    186                break;
    187            case FORMAT_OLD_BINARY:
    188                boolean swapHalfWord = true;
    189                writeBinaryLong(MAGIC_OLD_BINARY, 2, swapHalfWord);
    190                writeOldBinaryEntry(e, swapHalfWord);
    191                break;
    192            }
    193        }
    194
    195        private void writeNewEntry(final CpioArchiveEntry entry) throws IOException {
    196            writeAsciiLong(entry.getInode(), 8, 16);
    197            writeAsciiLong(entry.getMode(), 8, 16);
    198            writeAsciiLong(entry.getUID(), 8, 16);
    199            writeAsciiLong(entry.getGID(), 8, 16);
    200            writeAsciiLong(entry.getNumberOfLinks(), 8, 16);
    201            writeAsciiLong(entry.getTime(), 8, 16);
    202            writeAsciiLong(entry.getSize(), 8, 16);
    203            writeAsciiLong(entry.getDeviceMaj(), 8, 16);
    204            writeAsciiLong(entry.getDeviceMin(), 8, 16);
    205            writeAsciiLong(entry.getRemoteDeviceMaj(), 8, 16);
    206            writeAsciiLong(entry.getRemoteDeviceMin(), 8, 16);
    207            writeAsciiLong(entry.getName().length() + 1, 8, 16);
    208            writeAsciiLong(entry.getChksum(), 8, 16);
    209            writeCString(entry.getName());
    210            pad(entry.getHeaderPadCount());
    211        }
    212
    213        private void writeOldAsciiEntry(final CpioArchiveEntry entry)
    214                throws IOException {
    215            writeAsciiLong(entry.getDevice(), 6, 8);
    216            writeAsciiLong(entry.getInode(), 6, 8);
    217            writeAsciiLong(entry.getMode(), 6, 8);
    218            writeAsciiLong(entry.getUID(), 6, 8);
    219            writeAsciiLong(entry.getGID(), 6, 8);
    220            writeAsciiLong(entry.getNumberOfLinks(), 6, 8);
    221            writeAsciiLong(entry.getRemoteDevice(), 6, 8);
    222            writeAsciiLong(entry.getTime(), 11, 8);
    223            writeAsciiLong(entry.getName().length() + 1, 6, 8);
    224            writeAsciiLong(entry.getSize(), 11, 8);
    225            writeCString(entry.getName());
    226        }
    227
    228        private void writeOldBinaryEntry(final CpioArchiveEntry entry,
    229                final boolean swapHalfWord) throws IOException {
    230            writeBinaryLong(entry.getDevice(), 2, swapHalfWord);
    231            writeBinaryLong(entry.getInode(), 2, swapHalfWord);
    232            writeBinaryLong(entry.getMode(), 2, swapHalfWord);
    233            writeBinaryLong(entry.getUID(), 2, swapHalfWord);
    234            writeBinaryLong(entry.getGID(), 2, swapHalfWord);
    235            writeBinaryLong(entry.getNumberOfLinks(), 2, swapHalfWord);
    236            writeBinaryLong(entry.getRemoteDevice(), 2, swapHalfWord);
    237            writeBinaryLong(entry.getTime(), 4, swapHalfWord);
    238            writeBinaryLong(entry.getName().length() + 1, 2, swapHalfWord);
    239            writeBinaryLong(entry.getSize(), 4, swapHalfWord);
    240            writeCString(entry.getName());
    241            pad(entry.getHeaderPadCount());
    242        }
    243
    244        /*(non-Javadoc)
    245         *
    246         * @see
    247         * org.apache.commons.compress.archivers.ArchiveOutputStream#closeArchiveEntry
    248         * ()
    249         */
    250        public void closeArchiveEntry() throws IOException {
    251            if(finished) {
    252                throw new IOException("Stream has already been finished");
    253            }
    254
    255            ensureOpen();
    256
    257            if (entry == null) {
    258                throw new IOException("Trying to close non-existent entry");
    259            }
    260
    261            if (this.entry.getSize() != this.written) {
    262                throw new IOException("invalid entry size (expected "
    263                        + this.entry.getSize() + " but got " + this.written
    264                        + " bytes)");
    265            }
    266            pad(this.entry.getDataPadCount());
    267            if (this.entry.getFormat() == FORMAT_NEW_CRC) {
    268                if (this.crc != this.entry.getChksum()) {
    269                    throw new IOException("CRC Error");
    270                }
    271            }
    272            this.entry = null;
    273            this.crc = 0;
    274            this.written = 0;
    275        }
    276
    277        /**
    278         * Writes an array of bytes to the current CPIO entry data. This method will
    279         * block until all the bytes are written.
    280         *
    281         * @param b
    282         *            the data to be written
    283         * @param off
    284         *            the start offset in the data
    285         * @param len
    286         *            the number of bytes that are written
    287         * @throws IOException
    288         *             if an I/O error has occurred or if a CPIO file error has
    289         *             occurred
    290         */
    291        public void write(final byte[] b, final int off, final int len)
    292                throws IOException {
    293            ensureOpen();
    294            if (off < 0 || len < 0 || off > b.length - len) {
    295                throw new IndexOutOfBoundsException();
    296            } else if (len == 0) {
    297                return;
    298            }
    299
    300            if (this.entry == null) {
    301                throw new IOException("no current CPIO entry");
    302            }
    303            if (this.written + len > this.entry.getSize()) {
    304                throw new IOException("attempt to write past end of STORED entry");
    305            }
    306            out.write(b, off, len);
    307            this.written += len;
    308            if (this.entry.getFormat() == FORMAT_NEW_CRC) {
    309                for (int pos = 0; pos < len; pos++) {
    310                    this.crc += b[pos] & 0xFF;
    311                }
    312            }
    313            count(len);
    314        }
    315
    316        /**
    317         * Finishes writing the contents of the CPIO output stream without closing
    318         * the underlying stream. Use this method when applying multiple filters in
    319         * succession to the same output stream.
    320         *
    321         * @throws IOException
    322         *             if an I/O exception has occurred or if a CPIO file error has
    323         *             occurred
    324         */
    325        public void finish() throws IOException {
    326            ensureOpen();
    327            if (finished) {
    328                throw new IOException("This archive has already been finished");
    329            }
    330
    331            if (this.entry != null) {
    332                throw new IOException("This archive contains unclosed entries.");
    333            }
    334            this.entry = new CpioArchiveEntry(this.entryFormat);
    335            this.entry.setName(CPIO_TRAILER);
    336            this.entry.setNumberOfLinks(1);
    337            writeHeader(this.entry);
    338            closeArchiveEntry();
    339
    340            finished = true;
    341        }
    342
    343        /**
    344         * Closes the CPIO output stream as well as the stream being filtered.
    345         *
    346         * @throws IOException
    347         *             if an I/O error has occurred or if a CPIO file error has
    348         *             occurred
    349         */
    350        public void close() throws IOException {
    351            if(!finished) {
    352                finish();
    353            }
    354
    355            if (!this.closed) {
    356                out.close();
    357                this.closed = true;
    358            }
    359        }
    360
    361        private void pad(int count) throws IOException{
    362            if (count > 0){
    363                byte buff[] = new byte[count];
    364                out.write(buff);
    365            }
    366        }
    367
    368        private void writeBinaryLong(final long number, final int length,
    369                final boolean swapHalfWord) throws IOException {
    370            byte tmp[] = CpioUtil.long2byteArray(number, length, swapHalfWord);
    371            out.write(tmp);
    372        }
    373
    374        private void writeAsciiLong(final long number, final int length,
    375                final int radix) throws IOException {
    376            StringBuffer tmp = new StringBuffer();
    377            String tmpStr;
    378            if (radix == 16) {
    379                tmp.append(Long.toHexString(number));
    380            } else if (radix == 8) {
    381                tmp.append(Long.toOctalString(number));
    382            } else {
    383                tmp.append(Long.toString(number));
    384            }
    385
    386            if (tmp.length() <= length) {
    387                long insertLength = length - tmp.length();
    388                for (int pos = 0; pos < insertLength; pos++) {
    389                    tmp.insert(0, "0");
    390                }
    391                tmpStr = tmp.toString();
    392            } else {
    393                tmpStr = tmp.substring(tmp.length() - length);
    394            }
    395            out.write(ArchiveUtils.toAsciiBytes(tmpStr));
    396        }
    397
    398        /**
    399         * Writes an ASCII string to the stream followed by \0
    400         * @param str the String to write
    401         * @throws IOException if the string couldn't be written
    402         */
    403        private void writeCString(final String str) throws IOException {
    404            out.write(ArchiveUtils.toAsciiBytes(str));
    405            out.write('\0');
    406        }
    407
    408        /**
    409         * Creates a new ArchiveEntry. The entryName must be an ASCII encoded string.
    410         *
    411         * @see org.apache.commons.compress.archivers.ArchiveOutputStream#createArchiveEntry(java.io.File, java.lang.String)
    412         */
    413        public ArchiveEntry createArchiveEntry(File inputFile, String entryName)
    414                throws IOException {
    415            if(finished) {
    416                throw new IOException("Stream has already been finished");
    417            }
    418            return new CpioArchiveEntry(inputFile, entryName);
    419        }
    420
    421    }