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    }