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.FilterInputStream;
027    import java.io.IOException;
028    import java.io.InputStream;
029    import java.io.OutputStream;
030    
031    /**
032     * The TarInputStream reads a UNIX tar archive as an InputStream.
033     * methods are provided to position at each successive entry in
034     * the archive, and the read each entry as a normal input stream
035     * using read().
036     *
037     */
038    public class TarInputStream extends FilterInputStream {
039        private static final int SMALL_BUFFER_SIZE = 256;
040        private static final int BUFFER_SIZE = 8 * 1024;
041        private static final int LARGE_BUFFER_SIZE = 32 * 1024;
042        private static final int BYTE_MASK = 0xFF;
043    
044        // CheckStyle:VisibilityModifier OFF - bc
045        protected boolean debug;
046        protected boolean hasHitEOF;
047        protected long entrySize;
048        protected long entryOffset;
049        protected byte[] readBuf;
050        protected TarBuffer buffer;
051        protected TarEntry currEntry;
052    
053        /**
054         * This contents of this array is not used at all in this class,
055         * it is only here to avoid repreated object creation during calls
056         * to the no-arg read method.
057         */
058        protected byte[] oneBuf;
059    
060        // CheckStyle:VisibilityModifier ON
061    
062        /**
063         * Constructor for TarInputStream.
064         * @param is the input stream to use
065         */
066        public TarInputStream(InputStream is) {
067            this(is, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE);
068        }
069    
070        /**
071         * Constructor for TarInputStream.
072         * @param is the input stream to use
073         * @param blockSize the block size to use
074         */
075        public TarInputStream(InputStream is, int blockSize) {
076            this(is, blockSize, TarBuffer.DEFAULT_RCDSIZE);
077        }
078    
079        /**
080         * Constructor for TarInputStream.
081         * @param is the input stream to use
082         * @param blockSize the block size to use
083         * @param recordSize the record size to use
084         */
085        public TarInputStream(InputStream is, int blockSize, int recordSize) {
086            super(is);
087    
088            this.buffer = new TarBuffer(is, blockSize, recordSize);
089            this.readBuf = null;
090            this.oneBuf = new byte[1];
091            this.debug = false;
092            this.hasHitEOF = false;
093        }
094    
095        /**
096         * Sets the debugging flag.
097         *
098         * @param debug True to turn on debugging.
099         */
100        public void setDebug(boolean debug) {
101            this.debug = debug;
102            buffer.setDebug(debug);
103        }
104    
105        /**
106         * Closes this stream. Calls the TarBuffer's close() method.
107         * @throws IOException on error
108         */
109        public void close() throws IOException {
110            buffer.close();
111        }
112    
113        /**
114         * Get the record size being used by this stream's TarBuffer.
115         *
116         * @return The TarBuffer record size.
117         */
118        public int getRecordSize() {
119            return buffer.getRecordSize();
120        }
121    
122        /**
123         * Get the available data that can be read from the current
124         * entry in the archive. This does not indicate how much data
125         * is left in the entire archive, only in the current entry.
126         * This value is determined from the entry's size header field
127         * and the amount of data already read from the current entry.
128         * Integer.MAX_VALUE is returen in case more than Integer.MAX_VALUE
129         * bytes are left in the current entry in the archive.
130         *
131         * @return The number of available bytes for the current entry.
132         * @throws IOException for signature
133         */
134        public int available() throws IOException {
135            if (entrySize - entryOffset > Integer.MAX_VALUE) {
136                return Integer.MAX_VALUE;
137            }
138            return (int) (entrySize - entryOffset);
139        }
140    
141        /**
142         * Skip bytes in the input buffer. This skips bytes in the
143         * current entry's data, not the entire archive, and will
144         * stop at the end of the current entry's data if the number
145         * to skip extends beyond that point.
146         *
147         * @param numToSkip The number of bytes to skip.
148         * @return the number actually skipped
149         * @throws IOException on error
150         */
151        public long skip(long numToSkip) throws IOException {
152            // REVIEW
153            // This is horribly inefficient, but it ensures that we
154            // properly skip over bytes via the TarBuffer...
155            //
156            byte[] skipBuf = new byte[BUFFER_SIZE];
157            long skip = numToSkip;
158            while (skip > 0) {
159                int realSkip = (int) (skip > skipBuf.length ? skipBuf.length : skip);
160                int numRead = read(skipBuf, 0, realSkip);
161                if (numRead == -1) {
162                    break;
163                }
164                skip -= numRead;
165            }
166            return (numToSkip - skip);
167        }
168    
169        /**
170         * Since we do not support marking just yet, we return false.
171         *
172         * @return False.
173         */
174        public boolean markSupported() {
175            return false;
176        }
177    
178        /**
179         * Since we do not support marking just yet, we do nothing.
180         *
181         * @param markLimit The limit to mark.
182         */
183        public void mark(int markLimit) {
184        }
185    
186        /**
187         * Since we do not support marking just yet, we do nothing.
188         */
189        public void reset() {
190        }
191    
192        /**
193         * Get the next entry in this tar archive. This will skip
194         * over any remaining data in the current entry, if there
195         * is one, and place the input stream at the header of the
196         * next entry, and read the header and instantiate a new
197         * TarEntry from the header bytes and return that entry.
198         * If there are no more entries in the archive, null will
199         * be returned to indicate that the end of the archive has
200         * been reached.
201         *
202         * @return The next TarEntry in the archive, or null.
203         * @throws IOException on error
204         */
205        public TarEntry getNextEntry() throws IOException {
206            if (hasHitEOF) {
207                return null;
208            }
209    
210            if (currEntry != null) {
211                long numToSkip = entrySize - entryOffset;
212    
213                if (debug) {
214                    System.err.println("TarInputStream: SKIP currENTRY '"
215                            + currEntry.getName() + "' SZ "
216                            + entrySize + " OFF "
217                            + entryOffset + "  skipping "
218                            + numToSkip + " bytes");
219                }
220    
221                while (numToSkip > 0) {
222                    long skipped = skip(numToSkip);
223                    if (skipped <= 0) {
224                        throw new RuntimeException("failed to skip current tar"
225                                                   + " entry");
226                    }
227                    numToSkip -= skipped;
228                }
229    
230                readBuf = null;
231            }
232    
233            byte[] headerBuf = buffer.readRecord();
234    
235            if (headerBuf == null) {
236                if (debug) {
237                    System.err.println("READ NULL RECORD");
238                }
239                hasHitEOF = true;
240            } else if (buffer.isEOFRecord(headerBuf)) {
241                if (debug) {
242                    System.err.println("READ EOF RECORD");
243                }
244                hasHitEOF = true;
245            }
246    
247            if (hasHitEOF) {
248                currEntry = null;
249            } else {
250                currEntry = new TarEntry(headerBuf);
251    
252                if (debug) {
253                    System.err.println("TarInputStream: SET CURRENTRY '"
254                            + currEntry.getName()
255                            + "' size = "
256                            + currEntry.getSize());
257                }
258    
259                entryOffset = 0;
260    
261                entrySize = currEntry.getSize();
262            }
263    
264            if (currEntry != null && currEntry.isGNULongNameEntry()) {
265                // read in the name
266                StringBuffer longName = new StringBuffer();
267                byte[] buf = new byte[SMALL_BUFFER_SIZE];
268                int length = 0;
269                while ((length = read(buf)) >= 0) {
270                    longName.append(new String(buf, 0, length));
271                }
272                getNextEntry();
273                if (currEntry == null) {
274                    // Bugzilla: 40334
275                    // Malformed tar file - long entry name not followed by entry
276                    return null;
277                }
278                // remove trailing null terminator
279                if (longName.length() > 0
280                    && longName.charAt(longName.length() - 1) == 0) {
281                    longName.deleteCharAt(longName.length() - 1);
282                }
283                currEntry.setName(longName.toString());
284            }
285    
286            return currEntry;
287        }
288    
289        /**
290         * Reads a byte from the current tar archive entry.
291         *
292         * This method simply calls read( byte[], int, int ).
293         *
294         * @return The byte read, or -1 at EOF.
295         * @throws IOException on error
296         */
297        public int read() throws IOException {
298            int num = read(oneBuf, 0, 1);
299            return num == -1 ? -1 : ((int) oneBuf[0]) & BYTE_MASK;
300        }
301    
302        /**
303         * Reads bytes from the current tar archive entry.
304         *
305         * This method is aware of the boundaries of the current
306         * entry in the archive and will deal with them as if they
307         * were this stream's start and EOF.
308         *
309         * @param buf The buffer into which to place bytes read.
310         * @param offset The offset at which to place bytes read.
311         * @param numToRead The number of bytes to read.
312         * @return The number of bytes read, or -1 at EOF.
313         * @throws IOException on error
314         */
315        public int read(byte[] buf, int offset, int numToRead) throws IOException {
316            int totalRead = 0;
317    
318            if (entryOffset >= entrySize) {
319                return -1;
320            }
321    
322            if ((numToRead + entryOffset) > entrySize) {
323                numToRead = (int) (entrySize - entryOffset);
324            }
325    
326            if (readBuf != null) {
327                int sz = (numToRead > readBuf.length) ? readBuf.length
328                        : numToRead;
329    
330                System.arraycopy(readBuf, 0, buf, offset, sz);
331    
332                if (sz >= readBuf.length) {
333                    readBuf = null;
334                } else {
335                    int newLen = readBuf.length - sz;
336                    byte[] newBuf = new byte[newLen];
337    
338                    System.arraycopy(readBuf, sz, newBuf, 0, newLen);
339    
340                    readBuf = newBuf;
341                }
342    
343                totalRead += sz;
344                numToRead -= sz;
345                offset += sz;
346            }
347    
348            while (numToRead > 0) {
349                byte[] rec = buffer.readRecord();
350    
351                if (rec == null) {
352                    // Unexpected EOF!
353                    throw new IOException("unexpected EOF with " + numToRead
354                            + " bytes unread");
355                }
356    
357                int sz = numToRead;
358                int recLen = rec.length;
359    
360                if (recLen > sz) {
361                    System.arraycopy(rec, 0, buf, offset, sz);
362    
363                    readBuf = new byte[recLen - sz];
364    
365                    System.arraycopy(rec, sz, readBuf, 0, recLen - sz);
366                } else {
367                    sz = recLen;
368    
369                    System.arraycopy(rec, 0, buf, offset, recLen);
370                }
371    
372                totalRead += sz;
373                numToRead -= sz;
374                offset += sz;
375            }
376    
377            entryOffset += totalRead;
378    
379            return totalRead;
380        }
381    
382        /**
383         * Copies the contents of the current tar archive entry directly into
384         * an output stream.
385         *
386         * @param out The OutputStream into which to write the entry's data.
387         * @throws IOException on error
388         */
389        public void copyEntryContents(OutputStream out) throws IOException {
390            byte[] buf = new byte[LARGE_BUFFER_SIZE];
391    
392            while (true) {
393                int numRead = read(buf, 0, buf.length);
394    
395                if (numRead == -1) {
396                    break;
397                }
398    
399                out.write(buf, 0, numRead);
400            }
401        }
402    }