+/**
+ * Copyright 2012 Kamran Zafar
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.kamranzafar.jtar;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * @author Kamran Zafar
+ *
+ */
+public class TarInputStream extends FilterInputStream {
+
+ private static final int SKIP_BUFFER_SIZE = 2048;
+ private TarEntry currentEntry;
+ private long currentFileSize;
+ private long bytesRead;
+ private boolean defaultSkip = false;
+
+ public TarInputStream(InputStream in) {
+ super(in);
+ currentFileSize = 0;
+ bytesRead = 0;
+ }
+
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+
+ /**
+ * Not supported
+ *
+ */
+ @Override
+ public synchronized void mark(int readlimit) {
+ }
+
+ /**
+ * Not supported
+ *
+ */
+ @Override
+ public synchronized void reset() throws IOException {
+ throw new IOException("mark/reset not supported");
+ }
+
+ /**
+ * Read a byte
+ *
+ * @see java.io.FilterInputStream#read()
+ */
+ @Override
+ public int read() throws IOException {
+ byte[] buf = new byte[1];
+
+ int res = this.read(buf, 0, 1);
+
+ if (res != -1) {
+ return 0xFF & buf[0];
+ }
+
+ return res;
+ }
+
+ /**
+ * Checks if the bytes being read exceed the entry size and adjusts the byte
+ * array length. Updates the byte counters
+ *
+ *
+ * @see java.io.FilterInputStream#read(byte[], int, int)
+ */
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ if (currentEntry != null) {
+ if (currentFileSize == currentEntry.getSize()) {
+ return -1;
+ } else if ((currentEntry.getSize() - currentFileSize) < len) {
+ len = (int) (currentEntry.getSize() - currentFileSize);
+ }
+ }
+
+ int br = super.read(b, off, len);
+
+ if (br != -1) {
+ if (currentEntry != null) {
+ currentFileSize += br;
+ }
+
+ bytesRead += br;
+ }
+
+ return br;
+ }
+
+ /**
+ * Returns the next entry in the tar file
+ *
+ * @return TarEntry
+ * @throws IOException
+ */
+ public TarEntry getNextEntry() throws IOException {
+ closeCurrentEntry();
+
+ byte[] header = new byte[TarConstants.HEADER_BLOCK];
+ byte[] theader = new byte[TarConstants.HEADER_BLOCK];
+ int tr = 0;
+
+ // Read full header
+ while (tr < TarConstants.HEADER_BLOCK) {
+ int res = read(theader, 0, TarConstants.HEADER_BLOCK - tr);
+
+ if (res < 0) {
+ break;
+ }
+
+ System.arraycopy(theader, 0, header, tr, res);
+ tr += res;
+ }
+
+ // Check if record is null
+ boolean eof = true;
+ for (byte b : header) {
+ if (b != 0) {
+ eof = false;
+ break;
+ }
+ }
+
+ if (!eof) {
+ currentEntry = new TarEntry(header);
+ }
+
+ return currentEntry;
+ }
+
+ /**
+ * Returns the current offset (in bytes) from the beginning of the stream.
+ * This can be used to find out at which point in a tar file an entry's content begins, for instance.
+ */
+ public long getCurrentOffset() {
+ return bytesRead;
+ }
+
+ /**
+ * Closes the current tar entry
+ *
+ * @throws IOException
+ */
+ protected void closeCurrentEntry() throws IOException {
+ if (currentEntry != null) {
+ if (currentEntry.getSize() > currentFileSize) {
+ // Not fully read, skip rest of the bytes
+ long bs = 0;
+ while (bs < currentEntry.getSize() - currentFileSize) {
+ long res = skip(currentEntry.getSize() - currentFileSize - bs);
+
+ if (res == 0 && currentEntry.getSize() - currentFileSize > 0) {
+ // I suspect file corruption
+ throw new IOException("Possible tar file corruption");
+ }
+
+ bs += res;
+ }
+ }
+
+ currentEntry = null;
+ currentFileSize = 0L;
+ skipPad();
+ }
+ }
+
+ /**
+ * Skips the pad at the end of each tar entry file content
+ *
+ * @throws IOException
+ */
+ protected void skipPad() throws IOException {
+ if (bytesRead > 0) {
+ int extra = (int) (bytesRead % TarConstants.DATA_BLOCK);
+
+ if (extra > 0) {
+ long bs = 0;
+ while (bs < TarConstants.DATA_BLOCK - extra) {
+ long res = skip(TarConstants.DATA_BLOCK - extra - bs);
+ bs += res;
+ }
+ }
+ }
+ }
+
+ /**
+ * Skips 'n' bytes on the InputStream<br>
+ * Overrides default implementation of skip
+ *
+ */
+ @Override
+ public long skip(long n) throws IOException {
+ if (defaultSkip) {
+ // use skip method of parent stream
+ // may not work if skip not implemented by parent
+ long bs = super.skip(n);
+ bytesRead += bs;
+
+ return bs;
+ }
+
+ if (n <= 0) {
+ return 0;
+ }
+
+ long left = n;
+ byte[] sBuff = new byte[SKIP_BUFFER_SIZE];
+
+ while (left > 0) {
+ int res = read(sBuff, 0, (int) (left < SKIP_BUFFER_SIZE ? left : SKIP_BUFFER_SIZE));
+ if (res < 0) {
+ break;
+ }
+ left -= res;
+ }
+
+ return n - left;
+ }
+
+ public boolean isDefaultSkip() {
+ return defaultSkip;
+ }
+
+ public void setDefaultSkip(boolean defaultSkip) {
+ this.defaultSkip = defaultSkip;
+ }
+}