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 TarInputStream reads a UNIX tar archive as an InputStream.
024 * methods are provided to position at each successive entry in
025 * the archive, and the read each entry as a normal input stream
026 * using read().
027 *
028 * Kerry Menzel <kmenzel@cfl.rr.com> Contributed the code to support
029 * file sizes greater than 2GB (longs versus ints).
030 *
031 *
032 * @version $Revision: 1.9 $
033 * @author Timothy Gerard Endres, <time@gjt.org>
034 * @see TarBuffer
035 * @see TarHeader
036 * @see TarEntry
037 */
038
039
040public
041class           TarInputStream
042extends         FilterInputStream
043        {
044        protected boolean                       debug;
045        protected boolean                       hasHitEOF;
046
047        protected long                          entrySize;
048        protected long                          entryOffset;
049
050        protected byte[]                        oneBuf;
051        protected byte[]                        readBuf;
052
053        protected TarBuffer                     buffer;
054
055        protected TarEntry                      currEntry;
056
057        protected EntryFactory          eFactory;
058
059
060        public
061        TarInputStream( InputStream is )
062                {
063                this( is, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE );
064                }
065
066        public
067        TarInputStream( InputStream is, int blockSize )
068                {
069                this( is, blockSize, TarBuffer.DEFAULT_RCDSIZE );
070                }
071
072        public
073        TarInputStream( InputStream is, int blockSize, int recordSize )
074                {
075                super( is );
076
077                this.buffer = new TarBuffer( is, blockSize, recordSize );
078
079                this.readBuf = null;
080                this.oneBuf = new byte[1];
081                this.debug = false;
082                this.hasHitEOF = false;
083                this.eFactory = null;
084                }
085
086        /**
087         * Sets the debugging flag.
088         *
089         * @param debugF True to turn on debugging.
090         */
091        public void
092        setDebug( boolean debugF )
093                {
094                this.debug = debugF;
095                }
096
097        /**
098         * Sets the debugging flag.
099         *
100         * @param debugF True to turn on debugging.
101         */
102        public void
103        setEntryFactory( EntryFactory factory )
104                {
105                this.eFactory = factory;
106                }
107
108        /**
109         * Sets the debugging flag in this stream's TarBuffer.
110         *
111         * @param debugF True to turn on debugging.
112         */
113        public void
114        setBufferDebug( boolean debug )
115                {
116                this.buffer.setDebug( debug );
117                }
118
119        /**
120         * Closes this stream. Calls the TarBuffer's close() method.
121         */
122        public void
123        close()
124                throws IOException
125                {
126                this.buffer.close();
127                }
128
129        /**
130         * Get the record size being used by this stream's TarBuffer.
131         *
132         * @return The TarBuffer record size.
133         */
134        public int
135        getRecordSize()
136                {
137                return this.buffer.getRecordSize();
138                }
139
140        /**
141         * Get the available data that can be read from the current
142         * entry in the archive. This does not indicate how much data
143         * is left in the entire archive, only in the current entry.
144         * This value is determined from the entry's size header field
145         * and the amount of data already read from the current entry.
146         * 
147         *
148         * @return The number of available bytes for the current entry.
149         */
150        public int
151        available()
152                throws IOException
153                {
154                return (int)(this.entrySize - this.entryOffset);
155                }
156
157        /**
158         * Skip bytes in the input buffer. This skips bytes in the
159         * current entry's data, not the entire archive, and will
160         * stop at the end of the current entry's data if the number
161         * to skip extends beyond that point.
162         *
163         * @param numToSkip The number of bytes to skip.
164         * @return The actual number of bytes skipped.
165         */
166        public long
167        skip( long numToSkip )
168                throws IOException
169                {
170                // REVIEW
171                // This is horribly inefficient, but it ensures that we
172                // properly skip over bytes via the TarBuffer...
173                //
174
175                byte[] skipBuf = new byte[ 8 * 1024 ];
176        long num = numToSkip;
177                for ( ; num > 0 ; )
178                        {
179                        int numRead =
180                                this.read( skipBuf, 0,
181                                        ( num > skipBuf.length ? skipBuf.length : (int) num ) );
182
183                        if ( numRead == -1 )
184                                break;
185
186                        num -= numRead;
187                        }
188
189                return ( numToSkip - num );
190                }
191
192        /**
193         * Since we do not support marking just yet, we return false.
194         *
195         * @return False.
196         */
197        public boolean
198        markSupported()
199                {
200                return false;
201                }
202
203        /**
204         * Since we do not support marking just yet, we do nothing.
205         *
206         * @param markLimit The limit to mark.
207         */
208        public void
209        mark( int markLimit )
210                {
211                }
212
213        /**
214         * Since we do not support marking just yet, we do nothing.
215         */
216        public void
217        reset()
218                {
219                }
220
221        /**
222         * Get the number of bytes into the current TarEntry.
223         * This method returns the number of bytes that have been read
224         * from the current TarEntry's data.
225         *
226         * @returns The current entry offset.
227         */
228
229        public long
230        getEntryPosition()
231                {
232                return this.entryOffset;
233                }
234
235        /**
236         * Get the number of bytes into the stream we are currently at.
237         * This method accounts for the blocking stream that tar uses,
238         * so it represents the actual position in input stream, as
239         * opposed to the place where the tar archive parsing is.
240         *
241         * @returns The current file pointer.
242         */
243
244        public long
245        getStreamPosition()
246                {
247                return ( buffer.getBlockSize() * buffer.getCurrentBlockNum() )
248                                        + buffer.getCurrentRecordNum();
249                }
250
251        /**
252         * Get the next entry in this tar archive. This will skip
253         * over any remaining data in the current entry, if there
254         * is one, and place the input stream at the header of the
255         * next entry, and read the header and instantiate a new
256         * TarEntry from the header bytes and return that entry.
257         * If there are no more entries in the archive, null will
258         * be returned to indicate that the end of the archive has
259         * been reached.
260         *
261         * @return The next TarEntry in the archive, or null.
262         */
263        public TarEntry
264        getNextEntry()
265                throws IOException
266                {
267                if ( this.hasHitEOF )
268                        return null;
269
270                if ( this.currEntry != null )
271                        {
272                        long numToSkip = (this.entrySize - this.entryOffset);
273
274                        if ( this.debug )
275                        System.err.println
276                                ( "TarInputStream: SKIP currENTRY '"
277                                + this.currEntry.getName() + "' SZ "
278                                + this.entrySize + " OFF " + this.entryOffset
279                                + "  skipping " + numToSkip + " bytes" );
280
281                        if ( numToSkip > 0 )
282                                {
283                                this.skip( numToSkip );
284                                }
285
286                        this.readBuf = null;
287                        }
288
289                byte[] headerBuf = this.buffer.readRecord();
290
291                if ( headerBuf == null )
292                        {
293                        if ( this.debug )
294                                {
295                                System.err.println( "READ NULL RECORD" );
296                                }
297
298                        this.hasHitEOF = true;
299                        }
300                else if ( this.buffer.isEOFRecord( headerBuf ) )
301                        {
302                        if ( this.debug )
303                                {
304                                System.err.println( "READ EOF RECORD" );
305                                }
306
307                        this.hasHitEOF = true;
308                        }
309
310                if ( this.hasHitEOF )
311                        {
312                        this.currEntry = null;
313                        }
314                else
315                        {
316                        try {
317                                if ( this.eFactory == null )
318                                        {
319                                        this.currEntry = new TarEntry( headerBuf );
320                                        }
321                                else
322                                        {
323                                        this.currEntry =
324                                                this.eFactory.createEntry( headerBuf );
325                                        }
326
327                                if ( this.debug )
328                                System.err.println
329                                        ( "TarInputStream: SET CURRENTRY '"
330                                                + this.currEntry.getName()
331                                                + "' size = " + this.currEntry.getSize() );
332
333                                this.entryOffset = 0;
334                                this.entrySize = this.currEntry.getSize();
335                                }
336                        catch ( InvalidHeaderException ex )
337                                {
338                                this.entrySize = 0;
339                                this.entryOffset = 0;
340                                this.currEntry = null;
341                                throw new InvalidHeaderException
342                                        ( "bad header in block "
343                                                + this.buffer.getCurrentBlockNum()
344                                                + " record "
345                                                + this.buffer.getCurrentRecordNum()
346                                                + ", " + ex.getMessage() );
347                                }
348                        }
349
350                return this.currEntry;
351                }
352
353        /**
354         * Reads a byte from the current tar archive entry.
355         *
356         * This method simply calls read( byte[], int, int ).
357         *
358         * @return The byte read, or -1 at EOF.
359         */
360        public int
361        read()
362                throws IOException
363                {
364                int num = this.read( this.oneBuf, 0, 1 );
365                if ( num == -1 )
366                        return num;
367                else
368                        return (int) this.oneBuf[0];
369                }
370
371        /**
372         * Reads bytes from the current tar archive entry.
373         *
374         * This method simply calls read( byte[], int, int ).
375         *
376         * @param buf The buffer into which to place bytes read.
377         * @return The number of bytes read, or -1 at EOF.
378         */
379        public int
380        read( byte[] buf )
381                throws IOException
382                {
383                return this.read( buf, 0, buf.length );
384                }
385
386        /**
387         * Reads bytes from the current tar archive entry.
388         *
389         * This method is aware of the boundaries of the current
390         * entry in the archive and will deal with them as if they
391         * were this stream's start and EOF.
392         *
393         * @param buf The buffer into which to place bytes read.
394         * @param offset The offset at which to place bytes read.
395         * @param numToRead The number of bytes to read.
396         * @return The number of bytes read, or -1 at EOF.
397         */
398        public int
399        read( byte[] buf, int offset, int numToRead )
400                throws IOException
401                {
402                int totalRead = 0;
403
404                if ( this.entryOffset >= this.entrySize )
405                        return -1;
406
407                if ( (numToRead + this.entryOffset) > this.entrySize )
408                        {
409                        numToRead = (int) (this.entrySize - this.entryOffset);
410                        }
411
412                if ( this.readBuf != null )
413                        {
414                        int sz = ( numToRead > this.readBuf.length )
415                                                ? this.readBuf.length : numToRead;
416
417                        System.arraycopy( this.readBuf, 0, buf, offset, sz );
418
419                        if ( sz >= this.readBuf.length )
420                                {
421                                this.readBuf = null;
422                                }
423                        else
424                                {
425                                int newLen = this.readBuf.length - sz;
426                                byte[] newBuf = new byte[ newLen ];
427                                System.arraycopy( this.readBuf, sz, newBuf, 0, newLen );
428                                this.readBuf = newBuf;
429                                }
430
431                        totalRead += sz;
432                        numToRead -= sz;
433                        offset += sz;
434                        }
435
436                for ( ; numToRead > 0 ; )
437                        {
438                        byte[] rec = this.buffer.readRecord();
439                        if ( rec == null )
440                                {
441                                // Unexpected EOF!
442                                throw new IOException
443                                        ( "unexpected EOF with " + numToRead + " bytes unread" );
444                                }
445
446                        int sz = numToRead;
447                        int recLen = rec.length;
448
449                        if ( recLen > sz )
450                                {
451                                System.arraycopy( rec, 0, buf, offset, sz );
452                                this.readBuf = new byte[ recLen - sz ];
453                                System.arraycopy( rec, sz, this.readBuf, 0, recLen - sz );
454                                }
455                        else
456                                {
457                                sz = recLen;
458                                System.arraycopy( rec, 0, buf, offset, recLen );
459                                }
460
461                        totalRead += sz;
462                        numToRead -= sz;
463                        offset += sz;
464                        }
465
466                this.entryOffset += totalRead;
467
468                return totalRead;
469                }
470
471        /**
472         * Copies the contents of the current tar archive entry directly into
473         * an output stream.
474         *
475         * @param out The OutputStream into which to write the entry's data.
476         */
477        public void
478        copyEntryContents( OutputStream out )
479                throws IOException
480                {
481                byte[] buf = new byte[ 32 * 1024 ];
482
483                for ( ; ; )
484                        {
485                        int numRead = this.read( buf, 0, buf.length );
486                        if ( numRead == -1 )
487                                break;
488                        out.write( buf, 0, numRead );
489                        }
490                }
491
492        /**
493         * This interface is provided, with the method setEntryFactory(), to allow
494         * the programmer to have their own TarEntry subclass instantiated for the
495         * entries return from getNextEntry().
496         */
497
498        public
499        interface       EntryFactory
500                {
501                public TarEntry
502                        createEntry( String name );
503
504                public TarEntry
505                        createEntry( File path )
506                                throws InvalidHeaderException;
507
508                public TarEntry
509                        createEntry( byte[] headerBuf )
510                                throws InvalidHeaderException;
511                }
512
513        public
514        class           EntryAdapter
515        implements      EntryFactory
516                {
517                public TarEntry
518                createEntry( String name )
519                        {
520                        return new TarEntry( name );
521                        }
522
523                public TarEntry
524                createEntry( File path )
525                        throws InvalidHeaderException
526                        {
527                        return new TarEntry( path );
528                        }
529
530                public TarEntry
531                createEntry( byte[] headerBuf )
532                        throws InvalidHeaderException
533                        {
534                        return new TarEntry( headerBuf );
535                        }
536                }
537
538        }
539
540