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    }