    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.tar;
    020
    021    import java.io.File;
    022    import java.io.IOException;
    023    import java.io.OutputStream;
    024    import org.apache.commons.compress.archivers.ArchiveEntry;
    025    import org.apache.commons.compress.archivers.ArchiveOutputStream;
    026    import org.apache.commons.compress.utils.ArchiveUtils;
    027
    028    /**
    029     * The TarOutputStream writes a UNIX tar archive as an OutputStream.
    030     * Methods are provided to put entries, and then write their contents
    031     * by writing to this stream using write().
    032     * @NotThreadSafe
    033     */
    034    public class TarArchiveOutputStream extends ArchiveOutputStream {
    035        /** Fail if a long file name is required in the archive. */
    036        public static final int LONGFILE_ERROR = 0;
    037
    038        /** Long paths will be truncated in the archive. */
    039        public static final int LONGFILE_TRUNCATE = 1;
    040
    041        /** GNU tar extensions are used to store long file names in the archive. */
    042        public static final int LONGFILE_GNU = 2;
    043
    044        private long      currSize;
    045        private String    currName;
    046        private long      currBytes;
    047        private final byte[]    recordBuf;
    048        private int       assemLen;
    049        private final byte[]    assemBuf;
    050        protected final TarBuffer buffer;
    051        private int       longFileMode = LONGFILE_ERROR;
    052
    053        private boolean closed = false;
    054
    055        /** Indicates if putArchiveEntry has been called without closeArchiveEntry */
    056        private boolean haveUnclosedEntry = false;
    057
    058        /** indicates if this archive is finished */
    059        private boolean finished = false;
    060
    061        private final OutputStream out;
    062
    063        /**
    064         * Constructor for TarInputStream.
    065         * @param os the output stream to use
    066         */
    067        public TarArchiveOutputStream(OutputStream os) {
    068            this(os, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE);
    069        }
    070
    071        /**
    072         * Constructor for TarInputStream.
    073         * @param os the output stream to use
    074         * @param blockSize the block size to use
    075         */
    076        public TarArchiveOutputStream(OutputStream os, int blockSize) {
    077            this(os, blockSize, TarBuffer.DEFAULT_RCDSIZE);
    078        }
    079
    080        /**
    081         * Constructor for TarInputStream.
    082         * @param os the output stream to use
    083         * @param blockSize the block size to use
    084         * @param recordSize the record size to use
    085         */
    086        public TarArchiveOutputStream(OutputStream os, int blockSize, int recordSize) {
    087            out = os;
    088
    089            this.buffer = new TarBuffer(os, blockSize, recordSize);
    090            this.assemLen = 0;
    091            this.assemBuf = new byte[recordSize];
    092            this.recordBuf = new byte[recordSize];
    093        }
    094
    095        /**
    096         * Set the long file mode.
    097         * This can be LONGFILE_ERROR(0), LONGFILE_TRUNCATE(1) or LONGFILE_GNU(2).
    098         * This specifies the treatment of long file names (names >= TarConstants.NAMELEN).
    099         * Default is LONGFILE_ERROR.
    100         * @param longFileMode the mode to use
    101         */
    102        public void setLongFileMode(int longFileMode) {
    103            this.longFileMode = longFileMode;
    104        }
    105
    106
    107        /**
    108         * Ends the TAR archive without closing the underlying OutputStream.
    109         *
    110         * An archive consists of a series of file entries terminated by an
    111         * end-of-archive entry, which consists of two 512 blocks of zero bytes.
    112         * POSIX.1 requires two EOF records, like some other implementations.
    113         *
    114         * @throws IOException on error
    115         */
    116        public void finish() throws IOException {
    117            if (finished) {
    118                throw new IOException("This archive has already been finished");
    119            }
    120
    121            if(haveUnclosedEntry) {
    122                throw new IOException("This archives contains unclosed entries.");
    123            }
    124            writeEOFRecord();
    125            writeEOFRecord();
    126            buffer.flushBlock();
    127            finished = true;
    128        }
    129
    130        /**
    131         * Closes the underlying OutputStream.
    132         * @throws IOException on error
    133         */
    134        public void close() throws IOException {
    135            if(!finished) {
    136                finish();
    137            }
    138
    139            if (!closed) {
    140                buffer.close();
    141                out.close();
    142                closed = true;
    143            }
    144        }
    145
    146        /**
    147         * Get the record size being used by this stream's TarBuffer.
    148         *
    149         * @return The TarBuffer record size.
    150         */
    151        public int getRecordSize() {
    152            return buffer.getRecordSize();
    153        }
    154
    155        /**
    156         * Put an entry on the output stream. This writes the entry's
    157         * header record and positions the output stream for writing
    158         * the contents of the entry. Once this method is called, the
    159         * stream is ready for calls to write() to write the entry's
    160         * contents. Once the contents are written, closeArchiveEntry()
    161         * <B>MUST</B> be called to ensure that all buffered data
    162         * is completely written to the output stream.
    163         *
    164         * @param archiveEntry The TarEntry to be written to the archive.
    165         * @throws IOException on error
    166         * @throws ClassCastException if archiveEntry is not an instance of TarArchiveEntry
    167         */
    168        public void putArchiveEntry(ArchiveEntry archiveEntry) throws IOException {
    169            if(finished) {
    170                throw new IOException("Stream has already been finished");
    171            }
    172            TarArchiveEntry entry = (TarArchiveEntry) archiveEntry;
    173            if (entry.getName().length() >= TarConstants.NAMELEN) {
    174
    175                if (longFileMode == LONGFILE_GNU) {
    176                    // create a TarEntry for the LongLink, the contents
    177                    // of which are the entry's name
    178                    TarArchiveEntry longLinkEntry = new TarArchiveEntry(TarConstants.GNU_LONGLINK,
    179                                                                        TarConstants.LF_GNUTYPE_LONGNAME);
    180
    181                    final byte[] nameBytes = ArchiveUtils.toAsciiBytes(entry.getName());
    182                    longLinkEntry.setSize(nameBytes.length + 1); // +1 for NUL
    183                    putArchiveEntry(longLinkEntry);
    184                    write(nameBytes);
    185                    write(0); // NUL terminator
    186                    closeArchiveEntry();
    187                } else if (longFileMode != LONGFILE_TRUNCATE) {
    188                    throw new RuntimeException("file name '" + entry.getName()
    189                                               + "' is too long ( > "
    190                                               + TarConstants.NAMELEN + " bytes)");
    191                }
    192            }
    193
    194            entry.writeEntryHeader(recordBuf);
    195            buffer.writeRecord(recordBuf);
    196
    197            currBytes = 0;
    198
    199            if (entry.isDirectory()) {
    200                currSize = 0;
    201            } else {
    202                currSize = entry.getSize();
    203            }
    204            currName = entry.getName();
    205            haveUnclosedEntry = true;
    206        }
    207
    208        /**
    209         * Close an entry. This method MUST be called for all file
    210         * entries that contain data. The reason is that we must
    211         * buffer data written to the stream in order to satisfy
    212         * the buffer's record based writes. Thus, there may be
    213         * data fragments still being assembled that must be written
    214         * to the output stream before this entry is closed and the
    215         * next entry written.
    216         * @throws IOException on error
    217         */
    218        public void closeArchiveEntry() throws IOException {
    219            if(finished) {
    220                throw new IOException("Stream has already been finished");
    221            }
    222            if (!haveUnclosedEntry){
    223                throw new IOException("No current entry to close");
    224            }
    225            if (assemLen > 0) {
    226                for (int i = assemLen; i < assemBuf.length; ++i) {
    227                    assemBuf[i] = 0;
    228                }
    229
    230                buffer.writeRecord(assemBuf);
    231
    232                currBytes += assemLen;
    233                assemLen = 0;
    234            }
    235
    236            if (currBytes < currSize) {
    237                throw new IOException("entry '" + currName + "' closed at '"
    238                                      + currBytes
    239                                      + "' before the '" + currSize
    240                                      + "' bytes specified in the header were written");
    241            }
    242            haveUnclosedEntry = false;
    243        }
    244
    245        /**
    246         * Writes bytes to the current tar archive entry. This method
    247         * is aware of the current entry and will throw an exception if
    248         * you attempt to write bytes past the length specified for the
    249         * current entry. The method is also (painfully) aware of the
    250         * record buffering required by TarBuffer, and manages buffers
    251         * that are not a multiple of recordsize in length, including
    252         * assembling records from small buffers.
    253         *
    254         * @param wBuf The buffer to write to the archive.
    255         * @param wOffset The offset in the buffer from which to get bytes.
    256         * @param numToWrite The number of bytes to write.
    257         * @throws IOException on error
    258         */
    259        public void write(byte[] wBuf, int wOffset, int numToWrite) throws IOException {
    260            if ((currBytes + numToWrite) > currSize) {
    261                throw new IOException("request to write '" + numToWrite
    262                                      + "' bytes exceeds size in header of '"
    263                                      + currSize + "' bytes for entry '"
    264                                      + currName + "'");
    265
    266                //
    267                // We have to deal with assembly!!!
    268                // The programmer can be writing little 32 byte chunks for all
    269                // we know, and we must assemble complete records for writing.
    270                // REVIEW Maybe this should be in TarBuffer? Could that help to
    271                // eliminate some of the buffer copying.
    272                //
    273            }
    274
    275            if (assemLen > 0) {
    276                if ((assemLen + numToWrite) >= recordBuf.length) {
    277                    int aLen = recordBuf.length - assemLen;
    278
    279                    System.arraycopy(assemBuf, 0, recordBuf, 0,
    280                                     assemLen);
    281                    System.arraycopy(wBuf, wOffset, recordBuf,
    282                                     assemLen, aLen);
    283                    buffer.writeRecord(recordBuf);
    284
    285                    currBytes += recordBuf.length;
    286                    wOffset += aLen;
    287                    numToWrite -= aLen;
    288                    assemLen = 0;
    289                } else {
    290                    System.arraycopy(wBuf, wOffset, assemBuf, assemLen,
    291                                     numToWrite);
    292
    293                    wOffset += numToWrite;
    294                    assemLen += numToWrite;
    295                    numToWrite = 0;
    296                }
    297            }
    298
    299            //
    300            // When we get here we have EITHER:
    301            // o An empty "assemble" buffer.
    302            // o No bytes to write (numToWrite == 0)
    303            //
    304            while (numToWrite > 0) {
    305                if (numToWrite < recordBuf.length) {
    306                    System.arraycopy(wBuf, wOffset, assemBuf, assemLen,
    307                                     numToWrite);
    308
    309                    assemLen += numToWrite;
    310
    311                    break;
    312                }
    313
    314                buffer.writeRecord(wBuf, wOffset);
    315
    316                int num = recordBuf.length;
    317
    318                currBytes += num;
    319                numToWrite -= num;
    320                wOffset += num;
    321            }
    322
    323            count(numToWrite);
    324        }
    325
    326        /**
    327         * Write an EOF (end of archive) record to the tar archive.
    328         * An EOF record consists of a record of all zeros.
    329         */
    330        private void writeEOFRecord() throws IOException {
    331            for (int i = 0; i < recordBuf.length; ++i) {
    332                recordBuf[i] = 0;
    333            }
    334
    335            buffer.writeRecord(recordBuf);
    336        }
    337
    338        // used to be implemented via FilterOutputStream
    339        public void flush() throws IOException {
    340            out.flush();
    341        }
    342
    343        /** {@inheritDoc} */
    344        public ArchiveEntry createArchiveEntry(File inputFile, String entryName)
    345                throws IOException {
    346            if(finished) {
    347                throw new IOException("Stream has already been finished");
    348            }
    349            return new TarArchiveEntry(inputFile, entryName);
    350        }
    351    }