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