001/*
002** Authored by Timothy Gerard Endres
003** <mailto:time@gjt.org>  <http://www.trustice.com>
004** 
005** This work has been placed into the public domain.
006** You may use this work in any way and for any purpose you wish.
007**
008** THIS SOFTWARE IS PROVIDED AS-IS WITHOUT WARRANTY OF ANY KIND,
009** NOT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY. THE AUTHOR
010** OF THIS SOFTWARE, ASSUMES _NO_ RESPONSIBILITY FOR ANY
011** CONSEQUENCE RESULTING FROM THE USE, MODIFICATION, OR
012** REDISTRIBUTION OF THIS SOFTWARE. 
013** 
014*/
015
016package com.ice.tar;
017
018import java.io.*;
019import javax.activation.*;
020
021
022/**
023 * The TarOutputStream writes a UNIX tar archive as an OutputStream.
024 * Methods are provided to put entries, and then write their contents
025 * by writing to this stream using write().
026 *
027 * Kerry Menzel <kmenzel@cfl.rr.com> Contributed the code to support
028 * file sizes greater than 2GB (longs versus ints).
029 *
030 * @version $Revision: 1.8 $
031 * @author Timothy Gerard Endres, <time@gjt.org>
032 * @see TarBuffer
033 * @see TarHeader
034 * @see TarEntry
035 */
036
037
038public
039class           TarOutputStream
040extends         FilterOutputStream
041        {
042        protected boolean                       debug;
043        protected long                          currSize;
044        protected long                          currBytes;
045        protected byte[]                        oneBuf;
046        protected byte[]                        recordBuf;
047        protected int                           assemLen;
048        protected byte[]                        assemBuf;
049        protected TarBuffer                     buffer;
050
051
052        public
053        TarOutputStream( OutputStream os )
054                {
055                this( os, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE );
056                }
057
058        public
059        TarOutputStream( OutputStream os, int blockSize )
060                {
061                this( os, blockSize, TarBuffer.DEFAULT_RCDSIZE );
062                }
063
064        public
065        TarOutputStream( OutputStream os, int blockSize, int recordSize )
066                {
067                super( os );
068
069                this.buffer = new TarBuffer( os, blockSize, recordSize );
070                
071                this.debug = false;
072                this.assemLen = 0;
073                this.assemBuf = new byte[ recordSize ];
074                this.recordBuf = new byte[ recordSize ];
075                this.oneBuf = new byte[1];
076                }
077
078        /**
079         * Sets the debugging flag.
080         *
081         * @param debugF True to turn on debugging.
082         */
083        public void
084        setDebug( boolean debugF )
085                {
086                this.debug = debugF;
087                }
088
089        /**
090         * Sets the debugging flag in this stream's TarBuffer.
091         *
092         * @param debugF True to turn on debugging.
093         */
094        public void
095        setBufferDebug( boolean debug )
096                {
097                this.buffer.setDebug( debug );
098                }
099
100        /**
101         * Ends the TAR archive without closing the underlying OutputStream.
102         * The result is that the EOF record of nulls is written.
103         */
104
105        public void
106        finish()
107                throws IOException
108                {
109                this.writeEOFRecord();
110                }
111
112        /**
113         * Ends the TAR archive and closes the underlying OutputStream.
114         * This means that finish() is called followed by calling the
115         * TarBuffer's close().
116         */
117
118        public void
119        close()
120                throws IOException
121                {
122                this.finish();
123                this.buffer.close();
124                }
125
126        /**
127         * Get the record size being used by this stream's TarBuffer.
128         *
129         * @return The TarBuffer record size.
130         */
131        public int
132        getRecordSize()
133                {
134                return this.buffer.getRecordSize();
135                }
136
137        /**
138         * Put an entry on the output stream. This writes the entry's
139         * header record and positions the output stream for writing
140         * the contents of the entry. Once this method is called, the
141         * stream is ready for calls to write() to write the entry's
142         * contents. Once the contents are written, closeEntry()
143         * <B>MUST</B> be called to ensure that all buffered data
144         * is completely written to the output stream.
145         *
146         * @param entry The TarEntry to be written to the archive.
147         */
148        public void
149        putNextEntry( TarEntry entry )
150                throws IOException
151                {
152                StringBuffer name = entry.getHeader().name;
153
154                // NOTE
155                // This check is not adequate, because the maximum file length that
156                // can be placed into a POSIX (ustar) header depends on the precise
157                // locations of the path elements (slashes) within the file's full
158                // pathname. For this reason, writeEntryHeader() can still throw an
159                // InvalidHeaderException if the file's full pathname will not fit
160                // in the header.
161
162                if (    ( entry.isUnixTarFormat()
163                                        && name.length() > TarHeader.NAMELEN )
164                        ||
165                                ( ! entry.isUnixTarFormat()
166                                        && name.length() > (TarHeader.NAMELEN + TarHeader.PREFIXLEN) )
167                        )
168                        {
169                        throw new InvalidHeaderException
170                                ( "file name '"
171                                        + name
172                                        + "' is too long ( "
173                                        + name.length()
174                                        + " > "
175                                        + ( entry.isUnixTarFormat()
176                                                ? TarHeader.NAMELEN
177                                                : (TarHeader.NAMELEN + TarHeader.PREFIXLEN) )
178                                        + " bytes )" );
179                        }
180
181                entry.writeEntryHeader( this.recordBuf );
182
183                this.buffer.writeRecord( this.recordBuf );
184
185                this.currBytes = 0;
186
187                if ( entry.isDirectory() )
188                        this.currSize = 0;
189                else
190                        this.currSize = entry.getSize();
191                }
192
193        /**
194         * Close an entry. This method MUST be called for all file
195         * entries that contain data. The reason is that we must
196         * buffer data written to the stream in order to satisfy
197         * the buffer's record based writes. Thus, there may be
198         * data fragments still being assembled that must be written
199         * to the output stream before this entry is closed and the
200         * next entry written.
201         */
202        public void
203        closeEntry()
204                throws IOException
205                {
206                if ( this.assemLen > 0 )
207                        {
208                        for ( int i = this.assemLen ; i < this.assemBuf.length ; ++i )
209                                this.assemBuf[i] = 0;
210
211                        this.buffer.writeRecord( this.assemBuf );
212
213                        this.currBytes += this.assemLen;
214                        this.assemLen = 0;
215                        }
216
217                if ( this.currBytes < this.currSize )
218                        throw new IOException
219                                ( "entry closed at '" + this.currBytes
220                                        + "' before the '" + this.currSize
221                                        + "' bytes specified in the header were written" );
222                }
223
224        /**
225         * Writes a byte to the current tar archive entry.
226         *
227         * This method simply calls read( byte[], int, int ).
228         *
229         * @param b The byte written.
230         */
231        public void
232        write( int b )
233                throws IOException
234                {
235                this.oneBuf[0] = (byte) b;
236                this.write( this.oneBuf, 0, 1 );
237                }
238
239        /**
240         * Writes bytes to the current tar archive entry.
241         *
242         * This method simply calls read( byte[], int, int ).
243         *
244         * @param wBuf The buffer to write to the archive.
245         * @return The number of bytes read, or -1 at EOF.
246         */
247        public void
248        write( byte[] wBuf )
249                throws IOException
250                {
251                this.write( wBuf, 0, wBuf.length );
252                }
253
254        /**
255         * Writes bytes to the current tar archive entry. This method
256         * is aware of the current entry and will throw an exception if
257         * you attempt to write bytes past the length specified for the
258         * current entry. The method is also (painfully) aware of the
259         * record buffering required by TarBuffer, and manages buffers
260         * that are not a multiple of recordsize in length, including
261         * assembling records from small buffers.
262         *
263         * This method simply calls read( byte[], int, int ).
264         *
265         * @param wBuf The buffer to write to the archive.
266         * @param wOffset The offset in the buffer from which to get bytes.
267         * @param numToWrite The number of bytes to write.
268         */
269        public void
270        write( byte[] wBuf, int wOffset, int numToWrite )
271                throws IOException
272                {
273                if ( (this.currBytes + numToWrite) > this.currSize )
274                        throw new IOException
275                                ( "request to write '" + numToWrite
276                                        + "' bytes exceeds size in header of '"
277                                        + this.currSize + "' bytes" );
278
279                //
280                // We have to deal with assembly!!!
281                // The programmer can be writing little 32 byte chunks for all
282                // we know, and we must assemble complete records for writing.
283                // REVIEW Maybe this should be in TarBuffer? Could that help to
284                //        eliminate some of the buffer copying.
285                //
286                if ( this.assemLen > 0 )
287                        {
288                        if ( (this.assemLen + numToWrite ) >= this.recordBuf.length )
289                                {
290                                int aLen = this.recordBuf.length - this.assemLen;
291
292                                System.arraycopy
293                                        ( this.assemBuf, 0, this.recordBuf, 0, this.assemLen );
294
295                                System.arraycopy
296                                        ( wBuf, wOffset, this.recordBuf, this.assemLen, aLen );
297
298                                this.buffer.writeRecord( this.recordBuf );
299
300                                this.currBytes += this.recordBuf.length;
301
302                                wOffset += aLen;
303                                numToWrite -= aLen;
304                                this.assemLen = 0;
305                                }
306                        else // ( (this.assemLen + numToWrite ) < this.recordBuf.length )
307                                {
308                                System.arraycopy
309                                        ( wBuf, wOffset, this.assemBuf,
310                                                this.assemLen, numToWrite );
311                                wOffset += numToWrite;
312                                this.assemLen += numToWrite; 
313                                numToWrite -= numToWrite;
314                                }
315                        }
316
317                //
318                // When we get here we have EITHER:
319                //   o An empty "assemble" buffer.
320                //   o No bytes to write (numToWrite == 0)
321                //
322
323                for ( ; numToWrite > 0 ; )
324                        {
325                        if ( numToWrite < this.recordBuf.length )
326                                {
327                                System.arraycopy
328                                        ( wBuf, wOffset, this.assemBuf, this.assemLen, numToWrite );
329                                this.assemLen += numToWrite;
330                                break;
331                                }
332
333                        this.buffer.writeRecord( wBuf, wOffset );
334
335                        long num = this.recordBuf.length;
336                        this.currBytes += num;
337                        numToWrite -= num;
338                        wOffset += num;
339                        }
340                }
341
342        /**
343         * Write an EOF (end of archive) record to the tar archive.
344         * An EOF record consists of a record of all zeros.
345         */
346        private void
347        writeEOFRecord()
348                throws IOException
349                {
350                for ( int i = 0 ; i < this.recordBuf.length ; ++i )
351                        this.recordBuf[i] = 0;
352                this.buffer.writeRecord( this.recordBuf );
353                }
354
355        }
356