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