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.FilterInputStream;
027import java.io.IOException;
028import java.io.InputStream;
029import 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 */
038public 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}