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    }