    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            finished = true;
    127        }
    128
    129        /**
    130         * Closes the underlying OutputStream.
    131         * @throws IOException on error
    132         */
    133        public void close() throws IOException {
    134            if(!finished) {
    135                finish();
    136            }
    137
    138            if (!closed) {
    139                buffer.close();
    140                out.close();
    141                closed = true;
    142            }
    143        }
    144
    145        /**
    146         * Get the record size being used by this stream's TarBuffer.
    147         *
    148         * @return The TarBuffer record size.
    149         */
    150        public int getRecordSize() {
    151            return buffer.getRecordSize();
    152        }
    153
    154        /**
    155         * Put an entry on the output stream. This writes the entry's
    156         * header record and positions the output stream for writing
    157         * the contents of the entry. Once this method is called, the
    158         * stream is ready for calls to write() to write the entry's
    159         * contents. Once the contents are written, closeArchiveEntry()
    160         * <B>MUST</B> be called to ensure that all buffered data
    161         * is completely written to the output stream.
    162         *
    163         * @param archiveEntry The TarEntry to be written to the archive.
    164         * @throws IOException on error
    165         * @throws ClassCastException if archiveEntry is not an instance of TarArchiveEntry
    166         */
    167        public void putArchiveEntry(ArchiveEntry archiveEntry) throws IOException {
    168            if(finished) {
    169                throw new IOException("Stream has already been finished");
    170            }
    171            TarArchiveEntry entry = (TarArchiveEntry) archiveEntry;
    172            if (entry.getName().length() >= TarConstants.NAMELEN) {
    173
    174                if (longFileMode == LONGFILE_GNU) {
    175                    // create a TarEntry for the LongLink, the contents
    176                    // of which are the entry's name
    177                    TarArchiveEntry longLinkEntry = new TarArchiveEntry(TarConstants.GNU_LONGLINK,
    178                                                                        TarConstants.LF_GNUTYPE_LONGNAME);
    179
    180                    final byte[] nameBytes = ArchiveUtils.toAsciiBytes(entry.getName());
    181                    longLinkEntry.setSize(nameBytes.length + 1); // +1 for NUL
    182                    putArchiveEntry(longLinkEntry);
    183                    write(nameBytes);
    184                    write(0); // NUL terminator
    185                    closeArchiveEntry();
    186                } else if (longFileMode != LONGFILE_TRUNCATE) {
    187                    throw new RuntimeException("file name '" + entry.getName()
    188                                               + "' is too long ( > "
    189                                               + TarConstants.NAMELEN + " bytes)");
    190                }
    191            }
    192
    193            entry.writeEntryHeader(recordBuf);
    194            buffer.writeRecord(recordBuf);
    195
    196            currBytes = 0;
    197
    198            if (entry.isDirectory()) {
    199                currSize = 0;
    200            } else {
    201                currSize = entry.getSize();
    202            }
    203            currName = entry.getName();
    204            haveUnclosedEntry = true;
    205        }
    206
    207        /**
    208         * Close an entry. This method MUST be called for all file
    209         * entries that contain data. The reason is that we must
    210         * buffer data written to the stream in order to satisfy
    211         * the buffer's record based writes. Thus, there may be
    212         * data fragments still being assembled that must be written
    213         * to the output stream before this entry is closed and the
    214         * next entry written.
    215         * @throws IOException on error
    216         */
    217        public void closeArchiveEntry() throws IOException {
    218            if(finished) {
    219                throw new IOException("Stream has already been finished");
    220            }
    221            if (!haveUnclosedEntry){
    222                throw new IOException("No current entry to close");
    223            }
    224            if (assemLen > 0) {
    225                for (int i = assemLen; i < assemBuf.length; ++i) {
    226                    assemBuf[i] = 0;
    227                }
    228
    229                buffer.writeRecord(assemBuf);
    230
    231                currBytes += assemLen;
    232                assemLen = 0;
    233            }
    234
    235            if (currBytes < currSize) {
    236                throw new IOException("entry '" + currName + "' closed at '"
    237                                      + currBytes
    238                                      + "' before the '" + currSize
    239                                      + "' bytes specified in the header were written");
    240            }
    241            haveUnclosedEntry = false;
    242        }
    243
    244        /**
    245         * Writes bytes to the current tar archive entry. This method
    246         * is aware of the current entry and will throw an exception if
    247         * you attempt to write bytes past the length specified for the
    248         * current entry. The method is also (painfully) aware of the
    249         * record buffering required by TarBuffer, and manages buffers
    250         * that are not a multiple of recordsize in length, including
    251         * assembling records from small buffers.
    252         *
    253         * @param wBuf The buffer to write to the archive.
    254         * @param wOffset The offset in the buffer from which to get bytes.
    255         * @param numToWrite The number of bytes to write.
    256         * @throws IOException on error
    257         */
    258        public void write(byte[] wBuf, int wOffset, int numToWrite) throws IOException {
    259            if ((currBytes + numToWrite) > currSize) {
    260                throw new IOException("request to write '" + numToWrite
    261                                      + "' bytes exceeds size in header of '"
    262                                      + currSize + "' bytes for entry '"
    263                                      + currName + "'");
    264
    265                //
    266                // We have to deal with assembly!!!
    267                // The programmer can be writing little 32 byte chunks for all
    268                // we know, and we must assemble complete records for writing.
    269                // REVIEW Maybe this should be in TarBuffer? Could that help to
    270                // eliminate some of the buffer copying.
    271                //
    272            }
    273
    274            if (assemLen > 0) {
    275                if ((assemLen + numToWrite) >= recordBuf.length) {
    276                    int aLen = recordBuf.length - assemLen;
    277
    278                    System.arraycopy(assemBuf, 0, recordBuf, 0,
    279                                     assemLen);
    280                    System.arraycopy(wBuf, wOffset, recordBuf,
    281                                     assemLen, aLen);
    282                    buffer.writeRecord(recordBuf);
    283
    284                    currBytes += recordBuf.length;
    285                    wOffset += aLen;
    286                    numToWrite -= aLen;
    287                    assemLen = 0;
    288                } else {
    289                    System.arraycopy(wBuf, wOffset, assemBuf, assemLen,
    290                                     numToWrite);
    291
    292                    wOffset += numToWrite;
    293                    assemLen += numToWrite;
    294                    numToWrite = 0;
    295                }
    296            }
    297
    298            //
    299            // When we get here we have EITHER:
    300            // o An empty "assemble" buffer.
    301            // o No bytes to write (numToWrite == 0)
    302            //
    303            while (numToWrite > 0) {
    304                if (numToWrite < recordBuf.length) {
    305                    System.arraycopy(wBuf, wOffset, assemBuf, assemLen,
    306                                     numToWrite);
    307
    308                    assemLen += numToWrite;
    309
    310                    break;
    311                }
    312
    313                buffer.writeRecord(wBuf, wOffset);
    314
    315                int num = recordBuf.length;
    316
    317                currBytes += num;
    318                numToWrite -= num;
    319                wOffset += num;
    320            }
    321
    322            count(numToWrite);
    323        }
    324
    325        /**
    326         * Write an EOF (end of archive) record to the tar archive.
    327         * An EOF record consists of a record of all zeros.
    328         */
    329        private void writeEOFRecord() throws IOException {
    330            for (int i = 0; i < recordBuf.length; ++i) {
    331                recordBuf[i] = 0;
    332            }
    333
    334            buffer.writeRecord(recordBuf);
    335        }
    336
    337        // used to be implemented via FilterOutputStream
    338        public void flush() throws IOException {
    339            out.flush();
    340        }
    341
    342        public ArchiveEntry createArchiveEntry(File inputFile, String entryName)
    343                throws IOException {
    344            if(finished) {
    345                throw new IOException("Stream has already been finished");
    346            }
    347            return new TarArchiveEntry(inputFile, entryName);
    348        }
    349    }