View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    * 
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   * 
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.vfs.provider.tar;
18  
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.io.OutputStream;
22  import java.util.Arrays;
23  
24  /***
25   * The TarBuffer class implements the tar archive concept of a buffered input
26   * stream. This concept goes back to the days of blocked tape drives and special
27   * io devices. In the Java universe, the only real function that this class
28   * performs is to ensure that files have the correct "block" size, or other tars
29   * will complain. <p>
30   *
31   * You should never have a need to access this class directly. TarBuffers are
32   * created by Tar IO Streams.
33   *
34   * @author <a href="mailto:time@ice.com">Timothy Gerard Endres</a>
35   * @author <a href="mailto:peter@apache.org">Peter Donald</a>
36   * @version $Revision: 480428 $ $Date: 2006-11-29 07:15:24 +0100 (Mi, 29 Nov 2006) $
37   */
38  class TarBuffer
39  {
40      public static final int DEFAULT_RECORDSIZE = ( 512 );
41      public static final int DEFAULT_BLOCKSIZE = ( DEFAULT_RECORDSIZE * 20 );
42  
43      private byte[] m_blockBuffer;
44      private int m_blockSize;
45      private int m_currBlkIdx;
46      private int m_currRecIdx;
47      private boolean m_debug;
48  
49      private InputStream m_input;
50      private OutputStream m_output;
51      private int m_recordSize;
52      private int m_recsPerBlock;
53  
54      TarBuffer( final InputStream input )
55      {
56          this( input, TarBuffer.DEFAULT_BLOCKSIZE );
57      }
58  
59      TarBuffer( final InputStream input, final int blockSize )
60      {
61          this( input, blockSize, TarBuffer.DEFAULT_RECORDSIZE );
62      }
63  
64      TarBuffer( final InputStream input,
65                        final int blockSize,
66                        final int recordSize )
67      {
68          m_input = input;
69          initialize( blockSize, recordSize );
70      }
71  
72      TarBuffer( final OutputStream output )
73      {
74          this( output, TarBuffer.DEFAULT_BLOCKSIZE );
75      }
76  
77      TarBuffer( final OutputStream output, final int blockSize )
78      {
79          this( output, blockSize, TarBuffer.DEFAULT_RECORDSIZE );
80      }
81  
82      TarBuffer( final OutputStream output,
83                        final int blockSize,
84                        final int recordSize )
85      {
86          m_output = output;
87          initialize( blockSize, recordSize );
88      }
89  
90      /***
91       * Set the debugging flag for the buffer.
92       *
93       * @param debug If true, print debugging output.
94       */
95      public void setDebug( final boolean debug )
96      {
97          m_debug = debug;
98      }
99  
100     /***
101      * Get the TAR Buffer's block size. Blocks consist of multiple records.
102      *
103      * @return The BlockSize value
104      */
105     public int getBlockSize()
106     {
107         return m_blockSize;
108     }
109 
110     /***
111      * Get the current block number, zero based.
112      *
113      * @return The current zero based block number.
114      */
115     public int getCurrentBlockNum()
116     {
117         return m_currBlkIdx;
118     }
119 
120     /***
121      * Get the current record number, within the current block, zero based.
122      * Thus, current offset = (currentBlockNum * recsPerBlk) + currentRecNum.
123      *
124      * @return The current zero based record number.
125      */
126     public int getCurrentRecordNum()
127     {
128         return m_currRecIdx - 1;
129     }
130 
131     /***
132      * Get the TAR Buffer's record size.
133      *
134      * @return The RecordSize value
135      */
136     public int getRecordSize()
137     {
138         return m_recordSize;
139     }
140 
141     /***
142      * Determine if an archive record indicate End of Archive. End of archive is
143      * indicated by a record that consists entirely of null bytes.
144      *
145      * @param record The record data to check.
146      * @return The EOFRecord value
147      */
148     public boolean isEOFRecord( final byte[] record )
149     {
150         final int size = getRecordSize();
151         for( int i = 0; i < size; ++i )
152         {
153             if( record[ i ] != 0 )
154             {
155                 return false;
156             }
157         }
158 
159         return true;
160     }
161 
162     /***
163      * Close the TarBuffer. If this is an output buffer, also flush the current
164      * block before closing.
165      */
166     public void close()
167         throws IOException
168     {
169         if( m_debug )
170         {
171             debug( "TarBuffer.closeBuffer()." );
172         }
173 
174         if( null != m_output )
175         {
176             flushBlock();
177 
178             if( m_output != System.out && m_output != System.err )
179             {
180                 m_output.close();
181                 m_output = null;
182             }
183         }
184         else if( m_input != null )
185         {
186             if( m_input != System.in )
187             {
188                 m_input.close();
189                 m_input = null;
190             }
191         }
192     }
193 
194     /***
195      * Read a record from the input stream and return the data.
196      *
197      * @return The record data.
198      * @exception IOException Description of Exception
199      */
200     public byte[] readRecord()
201         throws IOException
202     {
203         if( m_debug )
204         {
205             final String message = "ReadRecord: recIdx = " + m_currRecIdx +
206                 " blkIdx = " + m_currBlkIdx;
207             debug( message );
208         }
209 
210         if( null == m_input )
211         {
212             final String message = "reading from an output buffer";
213             throw new IOException( message );
214         }
215 
216         if( m_currRecIdx >= m_recsPerBlock )
217         {
218             if( !readBlock() )
219             {
220                 return null;
221             }
222         }
223 
224         final byte[] result = new byte[ m_recordSize ];
225         System.arraycopy( m_blockBuffer,
226                           ( m_currRecIdx * m_recordSize ),
227                           result,
228                           0,
229                           m_recordSize );
230 
231         m_currRecIdx++;
232 
233         return result;
234     }
235 
236     /***
237      * Skip over a record on the input stream.
238      */
239     public void skipRecord()
240         throws IOException
241     {
242         if( m_debug )
243         {
244             final String message = "SkipRecord: recIdx = " + m_currRecIdx +
245                 " blkIdx = " + m_currBlkIdx;
246             debug( message );
247         }
248 
249         if( null == m_input )
250         {
251             final String message = "reading (via skip) from an output buffer";
252             throw new IOException( message );
253         }
254 
255         if( m_currRecIdx >= m_recsPerBlock )
256         {
257             if( !readBlock() )
258             {
259                 return;// UNDONE
260             }
261         }
262 
263         m_currRecIdx++;
264     }
265 
266     /***
267      * Write an archive record to the archive.
268      *
269      * @param record The record data to write to the archive.
270      */
271     public void writeRecord( final byte[] record )
272         throws IOException
273     {
274         if( m_debug )
275         {
276             final String message = "WriteRecord: recIdx = " + m_currRecIdx +
277                 " blkIdx = " + m_currBlkIdx;
278             debug( message );
279         }
280 
281         if( null == m_output )
282         {
283             final String message = "writing to an input buffer";
284             throw new IOException( message );
285         }
286 
287         if( record.length != m_recordSize )
288         {
289             final String message = "record to write has length '" +
290                 record.length + "' which is not the record size of '" +
291                 m_recordSize + "'";
292             throw new IOException( message );
293         }
294 
295         if( m_currRecIdx >= m_recsPerBlock )
296         {
297             writeBlock();
298         }
299 
300         System.arraycopy( record,
301                           0,
302                           m_blockBuffer,
303                           ( m_currRecIdx * m_recordSize ),
304                           m_recordSize );
305 
306         m_currRecIdx++;
307     }
308 
309     /***
310      * Write an archive record to the archive, where the record may be inside of
311      * a larger array buffer. The buffer must be "offset plus record size" long.
312      *
313      * @param buffer The buffer containing the record data to write.
314      * @param offset The offset of the record data within buf.
315      */
316     public void writeRecord( final byte[] buffer, final int offset )
317         throws IOException
318     {
319         if( m_debug )
320         {
321             final String message = "WriteRecord: recIdx = " + m_currRecIdx +
322                 " blkIdx = " + m_currBlkIdx;
323             debug( message );
324         }
325 
326         if( null == m_output )
327         {
328             final String message = "writing to an input buffer";
329             throw new IOException( message );
330         }
331 
332         if( ( offset + m_recordSize ) > buffer.length )
333         {
334             final String message = "record has length '" + buffer.length +
335                 "' with offset '" + offset + "' which is less than the record size of '" +
336                 m_recordSize + "'";
337             throw new IOException( message );
338         }
339 
340         if( m_currRecIdx >= m_recsPerBlock )
341         {
342             writeBlock();
343         }
344 
345         System.arraycopy( buffer,
346                           offset,
347                           m_blockBuffer,
348                           ( m_currRecIdx * m_recordSize ),
349                           m_recordSize );
350 
351         m_currRecIdx++;
352     }
353 
354     /***
355      * Flush the current data block if it has any data in it.
356      */
357     private void flushBlock()
358         throws IOException
359     {
360         if( m_debug )
361         {
362             final String message = "TarBuffer.flushBlock() called.";
363             debug( message );
364         }
365 
366         if( m_output == null )
367         {
368             final String message = "writing to an input buffer";
369             throw new IOException( message );
370         }
371 
372         if( m_currRecIdx > 0 )
373         {
374             writeBlock();
375         }
376     }
377 
378     /***
379      * Initialization common to all constructors.
380      */
381     private void initialize( final int blockSize, final int recordSize )
382     {
383         m_debug = false;
384         m_blockSize = blockSize;
385         m_recordSize = recordSize;
386         m_recsPerBlock = ( m_blockSize / m_recordSize );
387         m_blockBuffer = new byte[ m_blockSize ];
388 
389         if( null != m_input )
390         {
391             m_currBlkIdx = -1;
392             m_currRecIdx = m_recsPerBlock;
393         }
394         else
395         {
396             m_currBlkIdx = 0;
397             m_currRecIdx = 0;
398         }
399     }
400 
401     /***
402      * @return false if End-Of-File, else true
403      */
404     private boolean readBlock()
405         throws IOException
406     {
407         if( m_debug )
408         {
409             final String message = "ReadBlock: blkIdx = " + m_currBlkIdx;
410             debug( message );
411         }
412 
413         if( null == m_input )
414         {
415             final String message = "reading from an output buffer";
416             throw new IOException( message );
417         }
418 
419         m_currRecIdx = 0;
420 
421         int offset = 0;
422         int bytesNeeded = m_blockSize;
423 
424         while( bytesNeeded > 0 )
425         {
426             final long numBytes = m_input.read( m_blockBuffer, offset, bytesNeeded );
427 
428             //
429             // NOTE
430             // We have fit EOF, and the block is not full!
431             //
432             // This is a broken archive. It does not follow the standard
433             // blocking algorithm. However, because we are generous, and
434             // it requires little effort, we will simply ignore the error
435             // and continue as if the entire block were read. This does
436             // not appear to break anything upstream. We used to return
437             // false in this case.
438             //
439             // Thanks to 'Yohann.Roussel@alcatel.fr' for this fix.
440             //
441             if( numBytes == -1 )
442             {
443                 // However, just leaving the unread portion of the buffer dirty does
444                 // cause problems in some cases.  This problem is described in
445                 // http://issues.apache.org/bugzilla/show_bug.cgi?id=29877
446                 //
447                 // The solution is to fill the unused portion of the buffer with zeros.
448 
449                 Arrays.fill(m_blockBuffer, offset, offset + bytesNeeded, (byte) 0);
450 
451                 break;
452             }
453 
454             offset += numBytes;
455             bytesNeeded -= numBytes;
456 
457             if( numBytes != m_blockSize )
458             {
459                 if( m_debug )
460                 {
461                     System.err.println( "ReadBlock: INCOMPLETE READ "
462                                         + numBytes + " of " + m_blockSize
463                                         + " bytes read." );
464                 }
465             }
466         }
467 
468         m_currBlkIdx++;
469 
470         return true;
471     }
472 
473     /***
474      * Write a TarBuffer block to the archive.
475      *
476      * @exception IOException Description of Exception
477      */
478     private void writeBlock()
479         throws IOException
480     {
481         if( m_debug )
482         {
483             final String message = "WriteBlock: blkIdx = " + m_currBlkIdx;
484             debug( message );
485         }
486 
487         if( null == m_output )
488         {
489             final String message = "writing to an input buffer";
490             throw new IOException( message );
491         }
492 
493         m_output.write( m_blockBuffer, 0, m_blockSize );
494         m_output.flush();
495 
496         m_currRecIdx = 0;
497         m_currBlkIdx++;
498     }
499 
500     protected void debug( final String message )
501     {
502         if( m_debug )
503         {
504             System.err.println( message );
505         }
506     }
507 }