blob: 72ce64552693b7eea56a9a01a6901c4a3cf52681 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2013 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
* Remy Chi Jian Suen <remy.suen@gmail.com> - Bug 243347 TarFile should not throw NPE in finalize()
*******************************************************************************/
package org.eclipse.pde.api.tools.internal.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.zip.GZIPInputStream;
/**
* Reads a .tar or .tar.gz archive file, providing an index enumeration and
* allows for accessing an InputStream for arbitrary files in the archive.
*/
public class TarFile {
private static class TarInputStream extends FilterInputStream {
private int nextEntry = 0;
private int nextEOF = 0;
private int filepos = 0;
private int bytesread = 0;
private TarEntry firstEntry = null;
private String longLinkName = null;
/**
* Creates a new tar input stream on the given input stream.
*
* @param in input stream
* @throws TarException
* @throws IOException
*/
public TarInputStream(InputStream in) throws TarException, IOException {
super(in);
// Read in the first TarEntry to make sure
// the input is a valid tar file stream.
firstEntry = getNextEntry();
}
/**
* Create a new tar input stream, skipping ahead to the given entry in
* the file.
*
* @param in input stream
* @param entry skips to this entry in the file
* @throws TarException
* @throws IOException
*/
TarInputStream(InputStream in, TarEntry entry) throws TarException, IOException {
super(in);
skipToEntry(entry);
}
/**
* The checksum of a tar file header is simply the sum of the bytes in
* the header.
*
* @param header
* @return checksum
*/
private long headerChecksum(byte[] header) {
long sum = 0;
for (int i = 0; i < 512; i++) {
sum += header[i] & 0xff;
}
return sum;
}
/**
* Skips ahead to the position of the given entry in the file.
*
* @param entry
* @returns false if the entry has already been passed
* @throws TarException
* @throws IOException
*/
boolean skipToEntry(TarEntry entry) throws TarException, IOException {
int bytestoskip = entry.filepos - bytesread;
if (bytestoskip < 0) {
return false;
}
while (bytestoskip > 0) {
long ret = in.skip(bytestoskip);
if (ret < 0) {
throw new IOException("early end of stream"); //$NON-NLS-1$
}
bytestoskip -= ret;
bytesread += ret;
}
filepos = entry.filepos;
nextEntry = 0;
nextEOF = 0;
// Read next header to seek to file data.
getNextEntry();
return true;
}
/**
* Returns true if the header checksum is correct.
*
* @param header
* @return true if this header has a valid checksum
*/
private boolean isValidTarHeader(byte[] header) {
long fileChecksum, calculatedChecksum;
int pos, i;
pos = 148;
StringBuilder checksumString = new StringBuilder();
for (i = 0; i < 8; i++) {
if (header[pos + i] == ' ') {
continue;
}
if (header[pos + i] == 0 || !Character.isDigit((char) header[pos + i])) {
break;
}
checksumString.append((char) header[pos + i]);
}
if (checksumString.length() == 0) {
return false;
}
if (checksumString.charAt(0) != '0') {
checksumString.insert(0, '0');
}
try {
fileChecksum = Long.decode(checksumString.toString()).longValue();
} catch (NumberFormatException exception) {
// This is not valid if it cannot be parsed
return false;
}
// Blank out the checksum.
for (i = 0; i < 8; i++) {
header[pos + i] = ' ';
}
calculatedChecksum = headerChecksum(header);
return (fileChecksum == calculatedChecksum);
}
/**
* Returns the next entry in the tar file. Does not handle GNU @LongLink
* extensions.
*
* @return the next entry in the tar file
* @throws TarException
* @throws IOException
*/
TarEntry getNextEntryInternal() throws TarException, IOException {
byte[] header = new byte[512];
int pos = 0;
int i;
if (firstEntry != null) {
TarEntry entryReturn = firstEntry;
firstEntry = null;
return entryReturn;
}
while (nextEntry > 0) {
long ret = in.skip(nextEntry);
if (ret < 0) {
throw new IOException("early end of stream"); //$NON-NLS-1$
}
nextEntry -= ret;
bytesread += ret;
}
int bytestoread = 512;
while (bytestoread > 0) {
int ret = super.read(header, 512 - bytestoread, bytestoread);
if (ret < 0) {
throw new IOException("early end of stream"); //$NON-NLS-1$
}
bytestoread -= ret;
bytesread += ret;
}
// If we have a header of all zeros, this marks the end of the file.
if (headerChecksum(header) == 0) {
// We are at the end of the file.
if (filepos > 0) {
return null;
}
// Invalid stream.
throw new TarException("not in tar format"); //$NON-NLS-1$
}
// Validate checksum.
if (!isValidTarHeader(header)) {
throw new TarException("not in tar format"); //$NON-NLS-1$
}
while (pos < 100 && header[pos] != 0) {
pos++;
}
String name = new String(header, 0, pos, "UTF8"); //$NON-NLS-1$
// Prepend the prefix here.
pos = 345;
if (header[pos] != 0) {
while (pos < 500 && header[pos] != 0) {
pos++;
}
String prefix = new String(header, 345, pos - 345, "UTF8"); //$NON-NLS-1$
name = prefix + "/" + name; //$NON-NLS-1$
}
TarEntry entry;
if (longLinkName != null) {
entry = new TarEntry(longLinkName, filepos);
longLinkName = null;
} else {
entry = new TarEntry(name, filepos);
}
if (header[156] != 0) {
entry.setFileType(header[156]);
}
pos = 100;
StringBuilder mode = new StringBuilder();
for (i = 0; i < 8; i++) {
if (header[pos + i] == 0) {
break;
}
if (header[pos + i] == ' ') {
continue;
}
mode.append((char) header[pos + i]);
}
if (mode.length() > 0 && mode.charAt(0) != '0') {
mode.insert(0, '0');
}
try {
long fileMode = Long.decode(mode.toString()).longValue();
entry.setMode(fileMode);
} catch (NumberFormatException nfe) {
throw new TarException("Not a valid tar format", nfe); //$NON-NLS-1$
}
pos = 100 + 24;
StringBuilder size = new StringBuilder();
for (i = 0; i < 12; i++) {
if (header[pos + i] == 0) {
break;
}
if (header[pos + i] == ' ') {
continue;
}
size.append((char) header[pos + i]);
}
if (size.charAt(0) != '0') {
size.insert(0, '0');
}
int fileSize;
try {
fileSize = Integer.decode(size.toString()).intValue();
} catch (NumberFormatException nfe) {
throw new TarException("Not a valid tar format", nfe); //$NON-NLS-1$
}
entry.setSize(fileSize);
nextEOF = fileSize;
if (fileSize % 512 > 0) {
nextEntry = fileSize + (512 - (fileSize % 512));
} else {
nextEntry = fileSize;
}
filepos += (nextEntry + 512);
return entry;
}
/**
* Moves ahead to the next file in the tar archive and returns a
* TarEntry object describing it.
*
* @return the next entry in the tar file
* @throws TarException
* @throws IOException
*/
public TarEntry getNextEntry() throws TarException, IOException {
TarEntry entry = getNextEntryInternal();
if (entry != null && entry.getName().equals("././@LongLink")) { //$NON-NLS-1$
// This is a GNU extension for doing long filenames.
// We get a file called ././@LongLink which just contains
// the real pathname.
byte[] longNameData = new byte[(int) entry.getSize()];
int lbytesread = 0;
while (lbytesread < longNameData.length) {
int cur = read(longNameData, lbytesread, longNameData.length - lbytesread);
if (cur < 0) {
throw new IOException("early end of stream"); //$NON-NLS-1$
}
lbytesread += cur;
}
int pos = 0;
while (pos < longNameData.length && longNameData[pos] != 0) {
pos++;
}
longLinkName = new String(longNameData, 0, pos, "UTF8"); //$NON-NLS-1$
return getNextEntryInternal();
}
return entry;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (nextEOF == 0) {
return -1;
}
int size = super.read(b, off, (len > nextEOF ? nextEOF : len));
nextEntry -= size;
nextEOF -= size;
bytesread += size;
return size;
}
@Override
public int read() throws IOException {
byte[] data = new byte[1];
int size = read(data, 0, 1);
if (size < 0) {
return size;
}
return data[0];
}
}
private File file;
TarInputStream entryEnumerationStream;
TarEntry curEntry;
private TarInputStream entryStream;
private InputStream internalEntryStream;
/**
* Create a new TarFile for the given file.
*
* @param file
* @throws TarException
* @throws IOException
*/
public TarFile(File file) throws TarException, IOException {
this.file = file;
InputStream in = new FileInputStream(file);
// First, check if it's a GZIPInputStream.
try {
in = new GZIPInputStream(in);
} catch (IOException e) {
// If it is not compressed we close
// the old one and recreate
in.close();
in = new FileInputStream(file);
}
try {
entryEnumerationStream = new TarInputStream(in);
} catch (TarException ex) {
in.close();
throw ex;
}
curEntry = entryEnumerationStream.getNextEntry();
}
/**
* Close the tar file input stream.
*
* @throws IOException if the file cannot be successfully closed
*/
public void close() throws IOException {
if (entryEnumerationStream != null) {
entryEnumerationStream.close();
}
if (internalEntryStream != null) {
internalEntryStream.close();
}
}
/**
* Create a new TarFile for the given path name.
*
* @param filename
* @throws TarException
* @throws IOException
*/
public TarFile(String filename) throws TarException, IOException {
this(new File(filename));
}
/**
* Returns an enumeration cataloguing the tar archive.
*
* @return enumeration of all files in the archive
*/
public Enumeration<?> entries() {
return new Enumeration<Object>() {
@Override
public boolean hasMoreElements() {
return (curEntry != null);
}
@Override
public Object nextElement() {
TarEntry oldEntry = curEntry;
try {
curEntry = entryEnumerationStream.getNextEntry();
} catch (TarException e) {
curEntry = null;
} catch (IOException e) {
curEntry = null;
}
return oldEntry;
}
};
}
/**
* Returns a new InputStream for the given file in the tar archive.
*
* @param entry
* @return an input stream for the given file
* @throws TarException
* @throws IOException
*/
public InputStream getInputStream(TarEntry entry) throws TarException, IOException {
if (entryStream == null || !entryStream.skipToEntry(entry)) {
if (internalEntryStream != null) {
internalEntryStream.close();
}
internalEntryStream = new FileInputStream(file);
// First, check if it's a GZIPInputStream.
try {
internalEntryStream = new GZIPInputStream(internalEntryStream);
} catch (IOException e) {
// If it is not compressed we close
// the old one and recreate
internalEntryStream.close();
internalEntryStream = new FileInputStream(file);
}
entryStream = new TarInputStream(internalEntryStream, entry) {
@Override
public void close() {
// Ignore close() since we want to reuse the stream.
}
};
}
return entryStream;
}
/**
* Returns the path name of the file this archive represents.
*
* @return path
*/
public String getName() {
return file.getPath();
}
@Override
protected void finalize() throws Throwable {
close();
}
}