de.schlichtherle.io.archive.tar.TarInputArchive Maven / Gradle / Ivy
Show all versions of truezip Show documentation
/*
* Copyright (C) 2006-2010 Schlichtherle IT Services
*
* 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 de.schlichtherle.io.archive.tar;
import de.schlichtherle.io.*;
import de.schlichtherle.io.File;
import de.schlichtherle.io.archive.spi.*;
import de.schlichtherle.io.util.*;
import java.io.*;
import java.util.*;
import java.util.zip.*;
import org.apache.tools.tar.*;
/**
* Presents a {@link TarInputStream} as a randomly accessible archive.
*
* Warning:
* The constructor of this class extracts each entry in the archive to a
* temporary file.
* This may be very time and space consuming for large archives, but is
* the fastest implementation for subsequent random access, since there
* is no way the archive driver could predict the client application's
* behaviour.
*
* @author Christian Schlichtherle
* @version $Id$
* @since TrueZIP 6.0
*/
public class TarInputArchive implements InputArchive, TarConstants {
private static final byte[] NULL_RECORD = new byte[TarBuffer.DEFAULT_RCDSIZE];
private static final int CHECKSUM_OFFSET
= NAMELEN + MODELEN + UIDLEN + GIDLEN + SIZELEN + MODTIMELEN;
/** Prefix for temporary files created by the multiplexer. */
private static final String TEMP_FILE_PREFIX = TarDriver.TEMP_FILE_PREFIX;
/**
* Maps entry names to tar entries [String -> TarEntry].
*/
private final Map entries = new LinkedHashMap();
private InputArchiveMetaData metaData;
/**
* Extracts the entire TAR input stream into a temporary directory in order
* to allow subsequent random access to its entries.
*
* @param in The input stream from which this input archive file should be
* initialized. This stream is not used by any of the methods in
* this class after the constructor has terminated and is
* never closed!
* So it is safe and recommended to close it upon termination
* of this constructor.
*/
public TarInputArchive(final InputStream in)
throws IOException {
final TarInputStream tin = createValidatedTarInputStream(in);
try {
org.apache.tools.tar.TarEntry tinEntry;
while ((tinEntry = tin.getNextEntry()) != null) {
final String name = getName(tinEntry);
TarEntry entry;
if (tinEntry.isDirectory()) {
entry = new TarEntry(tinEntry);
} else {
final java.io.File tmp;
try {
entry = (TarEntry) entries.get(name);
tmp = entry != null
? entry.getFile()
: Temps.createTempFile(TEMP_FILE_PREFIX);
try {
final java.io.FileOutputStream out
= new java.io.FileOutputStream(tmp);
try {
File.cat(tin, out); // use high performance pump (async I/O)
} finally {
out.close();
}
} catch (IOException ex) {
final boolean ok = tmp.delete();
assert ok;
throw ex;
}
} catch (InputIOException ex) {
throw ex;
} catch (IOException ex) {
throw new TransientIOException(
new TempFileException(tinEntry, ex));
}
entry = new TarEntry(tinEntry, tmp);
}
entry.setName(name); // use normalized name
entries.put(name, entry);
}
} catch (IOException failure) {
close0();
throw failure;
}
}
/**
* Returns the fixed name of the given TAR entry, ensuring that it ends
* with a {@link ArchiveEntry#SEPARATOR} if it's a directory.
*
* @param entry the TAR entry.
* @return the fixed name of the given TAR entry.
* @see Issue TRUEZIP-62
*/
private static String getName(org.apache.tools.tar.TarEntry entry) {
final String name = Paths.normalize(entry.getName(), ArchiveEntry.SEPARATOR_CHAR);
return entry.isDirectory() && !name.endsWith(ArchiveEntry.SEPARATOR) ? name + ArchiveEntry.SEPARATOR_CHAR : name;
}
/**
* Returns a newly created and validated {@link TarInputStream}.
* This method performs a simple validation by computing the checksum
* for the first record only.
* This method is required because the {@code TarInputStream}
* unfortunately does not do any validation!
*/
private static TarInputStream createValidatedTarInputStream(
final InputStream in)
throws IOException {
final byte[] buf = new byte[TarBuffer.DEFAULT_RCDSIZE];
final InputStream vin = readAhead(in, buf);
// If the record is the null record, the TAR file is empty and we're
// done with validating.
if (!Arrays.equals(buf, NULL_RECORD)) {
final long expected = TarUtils.parseOctal(buf, CHECKSUM_OFFSET, 8);
for (int i = 0; i < 8; i++)
buf[CHECKSUM_OFFSET + i] = ' ';
final long is = TarUtils.computeCheckSum(buf);
if (expected != is)
throw new IOException(
"Illegal initial record in TAR file: Expected checksum " + expected + ", is " + is + "!");
}
return new TarInputStream(
vin, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE);
}
/**
* Fills {@code buf} with data from the given input stream and
* returns an input stream from which you can still read all data,
* including the data in buf.
*
* @param in The stream to read from. May not be {@code null}.
* @param buf The buffer to fill entirely with data.
* @return A stream which holds all the data {@code in} did.
* @throws IOException If {@code buf} couldn't get filled entirely.
*/
static InputStream readAhead(final InputStream in, final byte[] buf)
throws IOException {
// Unfortunately, in Sun's J2SE 1.4.2_12 implementation,
// InflaterInputStream pretends to support marking, but actually it
// doesn't - hence we need to filter this special case.
// This issue has been fixed in Sun's J2SE 1.5.0-b64 implementation.
if (!(in instanceof InflaterInputStream) && in.markSupported()) {
in.mark(buf.length);
readFully(in, buf);
in.reset();
return in;
} else {
final PushbackInputStream pin
= new PushbackInputStream(in, buf.length);
readFully(pin, buf);
pin.unread(buf);
return pin;
}
}
private static void readFully(final InputStream in, final byte[] buf)
throws IOException {
final int l = buf.length;
int n = 0;
do {
final int r = in.read(buf, n, l - n);
if (r == -1)
throw new EOFException();
n += r;
} while (n < l);
}
public int getNumArchiveEntries() {
return entries.size();
}
public Enumeration getArchiveEntries() {
return Collections.enumeration(entries.values());
}
public ArchiveEntry getArchiveEntry(String entryName) {
return (TarEntry) entries.get(entryName);
}
public InputStream getInputStream(
final ArchiveEntry entry,
final ArchiveEntry dstEntry)
throws IOException {
return new java.io.FileInputStream(((TarEntry) entry).getFile());
}
public void close() throws IOException {
close0();
}
private void close0() throws IOException {
final Collection values = entries.values();
for (final Iterator i = values.iterator(); i.hasNext(); i.remove()) {
final TarEntry entry = (TarEntry) i.next();
final java.io.File file = entry.getFile();
if (file == null) {
assert entry.isDirectory();
continue;
}
assert file.exists();
if (!file.delete()) {
// Windoze: The temp file is still open for reading by one
// or more entry input streams.
file.deleteOnExit();
}
}
}
//
// Metadata stuff.
//
public InputArchiveMetaData getMetaData() {
return metaData;
}
public void setMetaData(InputArchiveMetaData metaData) {
this.metaData = metaData;
}
//
// Member class.
//
/**
* This needs to be a {@link FileNotFoundException} in order to signal that
* the TAR is simply not accessible and not necessarily a false positive.
*/
private static final class TempFileException extends FileNotFoundException {
private TempFileException(
final org.apache.tools.tar.TarEntry entry,
final IOException cause) {
super(entry.getName() + " (couldn't create temp file for archive entry)");
super.initCause(cause);
}
}
}