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