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    
024    package org.apache.activemq.console.command.store.tar;
025    
026    import java.io.InputStream;
027    import java.io.OutputStream;
028    import java.io.IOException;
029    import 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    
044    public 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    }