001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one or more
003 *  contributor license agreements.  See the NOTICE file distributed with
004 *  this work for additional information regarding copyright ownership.
005 *  The ASF licenses this file to You under the Apache License, Version 2.0
006 *  (the "License"); you may not use this file except in compliance with
007 *  the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 *  Unless required by applicable law or agreed to in writing, software
012 *  distributed under the License is distributed on an "AS IS" BASIS,
013 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 *  See the License for the specific language governing permissions and
015 *  limitations under the License.
016 *
017 */
018
019/*
020 * This package is based on the work done by Timothy Gerard Endres
021 * (time@ice.com) to whom the Ant project is very grateful for his great code.
022 */
023
024package org.apache.activemq.console.command.store.tar;
025
026import java.io.InputStream;
027import java.io.OutputStream;
028import java.io.IOException;
029import java.util.Arrays;
030
031/**
032 * The TarBuffer class implements the tar archive concept
033 * of a buffered input stream. This concept goes back to the
034 * days of blocked tape drives and special io devices. In the
035 * Java universe, the only real function that this class
036 * performs is to ensure that files have the correct "block"
037 * size, or other tars will complain.
038 * <p>
039 * You should never have a need to access this class directly.
040 * TarBuffers are created by Tar IO Streams.
041 *
042 */
043
044public class TarBuffer {
045
046    /** Default record size */
047    public static final int DEFAULT_RCDSIZE = (512);
048
049    /** Default block size */
050    public static final int DEFAULT_BLKSIZE = (DEFAULT_RCDSIZE * 20);
051
052    private InputStream     inStream;
053    private OutputStream    outStream;
054    private byte[]          blockBuffer;
055    private int             currBlkIdx;
056    private int             currRecIdx;
057    private int             blockSize;
058    private int             recordSize;
059    private int             recsPerBlock;
060    private boolean         debug;
061
062    /**
063     * Constructor for a TarBuffer on an input stream.
064     * @param inStream the input stream to use
065     */
066    public TarBuffer(InputStream inStream) {
067        this(inStream, TarBuffer.DEFAULT_BLKSIZE);
068    }
069
070    /**
071     * Constructor for a TarBuffer on an input stream.
072     * @param inStream the input stream to use
073     * @param blockSize the block size to use
074     */
075    public TarBuffer(InputStream inStream, int blockSize) {
076        this(inStream, blockSize, TarBuffer.DEFAULT_RCDSIZE);
077    }
078
079    /**
080     * Constructor for a TarBuffer on an input stream.
081     * @param inStream the input stream to use
082     * @param blockSize the block size to use
083     * @param recordSize the record size to use
084     */
085    public TarBuffer(InputStream inStream, int blockSize, int recordSize) {
086        this.inStream = inStream;
087        this.outStream = null;
088
089        this.initialize(blockSize, recordSize);
090    }
091
092    /**
093     * Constructor for a TarBuffer on an output stream.
094     * @param outStream the output stream to use
095     */
096    public TarBuffer(OutputStream outStream) {
097        this(outStream, TarBuffer.DEFAULT_BLKSIZE);
098    }
099
100    /**
101     * Constructor for a TarBuffer on an output stream.
102     * @param outStream the output stream to use
103     * @param blockSize the block size to use
104     */
105    public TarBuffer(OutputStream outStream, int blockSize) {
106        this(outStream, blockSize, TarBuffer.DEFAULT_RCDSIZE);
107    }
108
109    /**
110     * Constructor for a TarBuffer on an output stream.
111     * @param outStream the output stream to use
112     * @param blockSize the block size to use
113     * @param recordSize the record size to use
114     */
115    public TarBuffer(OutputStream outStream, int blockSize, int recordSize) {
116        this.inStream = null;
117        this.outStream = outStream;
118
119        this.initialize(blockSize, recordSize);
120    }
121
122    /**
123     * Initialization common to all constructors.
124     */
125    private void initialize(int blockSize, int recordSize) {
126        this.debug = false;
127        this.blockSize = blockSize;
128        this.recordSize = recordSize;
129        this.recsPerBlock = (this.blockSize / this.recordSize);
130        this.blockBuffer = new byte[this.blockSize];
131
132        if (this.inStream != null) {
133            this.currBlkIdx = -1;
134            this.currRecIdx = this.recsPerBlock;
135        } else {
136            this.currBlkIdx = 0;
137            this.currRecIdx = 0;
138        }
139    }
140
141    /**
142     * Get the TAR Buffer's block size. Blocks consist of multiple records.
143     * @return the block size
144     */
145    public int getBlockSize() {
146        return this.blockSize;
147    }
148
149    /**
150     * Get the TAR Buffer's record size.
151     * @return the record size
152     */
153    public int getRecordSize() {
154        return this.recordSize;
155    }
156
157    /**
158     * Set the debugging flag for the buffer.
159     *
160     * @param debug If true, print debugging output.
161     */
162    public void setDebug(boolean debug) {
163        this.debug = debug;
164    }
165
166    /**
167     * Determine if an archive record indicate End of Archive. End of
168     * archive is indicated by a record that consists entirely of null bytes.
169     *
170     * @param record The record data to check.
171     * @return true if the record data is an End of Archive
172     */
173    public boolean isEOFRecord(byte[] record) {
174        for (int i = 0, sz = getRecordSize(); i < sz; ++i) {
175            if (record[i] != 0) {
176                return false;
177            }
178        }
179
180        return true;
181    }
182
183    /**
184     * Skip over a record on the input stream.
185     * @throws IOException on error
186     */
187    public void skipRecord() throws IOException {
188        if (debug) {
189            System.err.println("SkipRecord: recIdx = " + currRecIdx
190                               + " blkIdx = " + currBlkIdx);
191        }
192
193        if (inStream == null) {
194            throw new IOException("reading (via skip) from an output buffer");
195        }
196
197        if (currRecIdx >= recsPerBlock) {
198            if (!readBlock()) {
199                return;    // UNDONE
200            }
201        }
202
203        currRecIdx++;
204    }
205
206    /**
207     * Read a record from the input stream and return the data.
208     *
209     * @return The record data.
210     * @throws IOException on error
211     */
212    public byte[] readRecord() throws IOException {
213        if (debug) {
214            System.err.println("ReadRecord: recIdx = " + currRecIdx
215                               + " blkIdx = " + currBlkIdx);
216        }
217
218        if (inStream == null) {
219            throw new IOException("reading from an output buffer");
220        }
221
222        if (currRecIdx >= recsPerBlock) {
223            if (!readBlock()) {
224                return null;
225            }
226        }
227
228        byte[] result = new byte[recordSize];
229
230        System.arraycopy(blockBuffer,
231                         (currRecIdx * recordSize), result, 0,
232                         recordSize);
233
234        currRecIdx++;
235
236        return result;
237    }
238
239    /**
240     * @return false if End-Of-File, else true
241     */
242    private boolean readBlock() throws IOException {
243        if (debug) {
244            System.err.println("ReadBlock: blkIdx = " + currBlkIdx);
245        }
246
247        if (inStream == null) {
248            throw new IOException("reading from an output buffer");
249        }
250
251        currRecIdx = 0;
252
253        int offset = 0;
254        int bytesNeeded = blockSize;
255
256        while (bytesNeeded > 0) {
257            long numBytes = inStream.read(blockBuffer, offset,
258                                               bytesNeeded);
259
260            //
261            // NOTE
262            // We have fit EOF, and the block is not full!
263            //
264            // This is a broken archive. It does not follow the standard
265            // blocking algorithm. However, because we are generous, and
266            // it requires little effort, we will simply ignore the error
267            // and continue as if the entire block were read. This does
268            // not appear to break anything upstream. We used to return
269            // false in this case.
270            //
271            // Thanks to 'Yohann.Roussel@alcatel.fr' for this fix.
272            //
273            if (numBytes == -1) {
274                if (offset == 0) {
275                    // Ensure that we do not read gigabytes of zeros
276                    // for a corrupt tar file.
277                    // See http://issues.apache.org/bugzilla/show_bug.cgi?id=39924
278                    return false;
279                }
280                // However, just leaving the unread portion of the buffer dirty does
281                // cause problems in some cases.  This problem is described in
282                // http://issues.apache.org/bugzilla/show_bug.cgi?id=29877
283                //
284                // The solution is to fill the unused portion of the buffer with zeros.
285
286                Arrays.fill(blockBuffer, offset, offset + bytesNeeded, (byte) 0);
287
288                break;
289            }
290
291            offset += numBytes;
292            bytesNeeded -= numBytes;
293
294            if (numBytes != blockSize) {
295                if (debug) {
296                    System.err.println("ReadBlock: INCOMPLETE READ "
297                                       + numBytes + " of " + blockSize
298                                       + " bytes read.");
299                }
300            }
301        }
302
303        currBlkIdx++;
304
305        return true;
306    }
307
308    /**
309     * Get the current block number, zero based.
310     *
311     * @return The current zero based block number.
312     */
313    public int getCurrentBlockNum() {
314        return currBlkIdx;
315    }
316
317    /**
318     * Get the current record number, within the current block, zero based.
319     * Thus, current offset = (currentBlockNum * recsPerBlk) + currentRecNum.
320     *
321     * @return The current zero based record number.
322     */
323    public int getCurrentRecordNum() {
324        return currRecIdx - 1;
325    }
326
327    /**
328     * Write an archive record to the archive.
329     *
330     * @param record The record data to write to the archive.
331     * @throws IOException on error
332     */
333    public void writeRecord(byte[] record) throws IOException {
334        if (debug) {
335            System.err.println("WriteRecord: recIdx = " + currRecIdx
336                               + " blkIdx = " + currBlkIdx);
337        }
338
339        if (outStream == null) {
340            throw new IOException("writing to an input buffer");
341        }
342
343        if (record.length != recordSize) {
344            throw new IOException("record to write has length '"
345                                  + record.length
346                                  + "' which is not the record size of '"
347                                  + recordSize + "'");
348        }
349
350        if (currRecIdx >= recsPerBlock) {
351            writeBlock();
352        }
353
354        System.arraycopy(record, 0, blockBuffer,
355                         (currRecIdx * recordSize),
356                         recordSize);
357
358        currRecIdx++;
359    }
360
361    /**
362     * Write an archive record to the archive, where the record may be
363     * inside of a larger array buffer. The buffer must be "offset plus
364     * record size" long.
365     *
366     * @param buf The buffer containing the record data to write.
367     * @param offset The offset of the record data within buf.
368     * @throws IOException on error
369     */
370    public void writeRecord(byte[] buf, int offset) throws IOException {
371        if (debug) {
372            System.err.println("WriteRecord: recIdx = " + currRecIdx
373                               + " blkIdx = " + currBlkIdx);
374        }
375
376        if (outStream == null) {
377            throw new IOException("writing to an input buffer");
378        }
379
380        if ((offset + recordSize) > buf.length) {
381            throw new IOException("record has length '" + buf.length
382                                  + "' with offset '" + offset
383                                  + "' which is less than the record size of '"
384                                  + recordSize + "'");
385        }
386
387        if (currRecIdx >= recsPerBlock) {
388            writeBlock();
389        }
390
391        System.arraycopy(buf, offset, blockBuffer,
392                         (currRecIdx * recordSize),
393                         recordSize);
394
395        currRecIdx++;
396    }
397
398    /**
399     * Write a TarBuffer block to the archive.
400     */
401    private void writeBlock() throws IOException {
402        if (debug) {
403            System.err.println("WriteBlock: blkIdx = " + currBlkIdx);
404        }
405
406        if (outStream == null) {
407            throw new IOException("writing to an input buffer");
408        }
409
410        outStream.write(blockBuffer, 0, blockSize);
411        outStream.flush();
412
413        currRecIdx = 0;
414        currBlkIdx++;
415        Arrays.fill(blockBuffer, (byte) 0);
416    }
417
418    /**
419     * Flush the current data block if it has any data in it.
420     */
421    void flushBlock() throws IOException {
422        if (debug) {
423            System.err.println("TarBuffer.flushBlock() called.");
424        }
425
426        if (outStream == null) {
427            throw new IOException("writing to an input buffer");
428        }
429
430        if (currRecIdx > 0) {
431            writeBlock();
432        }
433    }
434
435    /**
436     * Close the TarBuffer. If this is an output buffer, also flush the
437     * current block before closing.
438     * @throws IOException on error
439     */
440    public void close() throws IOException {
441        if (debug) {
442            System.err.println("TarBuffer.closeBuffer().");
443        }
444
445        if (outStream != null) {
446            flushBlock();
447
448            if (outStream != System.out
449                    && outStream != System.err) {
450                outStream.close();
451
452                outStream = null;
453            }
454        } else if (inStream != null) {
455            if (inStream != System.in) {
456                inStream.close();
457
458                inStream = null;
459            }
460        }
461    }
462}