de.schlichtherle.truezip.fs.archive.tar.TarInputShop Maven / Gradle / Ivy
Show all versions of truezip-driver-tar Show documentation
/*
* Copyright (C) 2005-2015 Schlichtherle IT Services.
* All rights reserved. Use is subject to license terms.
*/
package de.schlichtherle.truezip.fs.archive.tar;
import de.schlichtherle.truezip.entry.Entry.Type;
import static de.schlichtherle.truezip.entry.Entry.Type.DIRECTORY;
import static de.schlichtherle.truezip.entry.Entry.Type.FILE;
import de.schlichtherle.truezip.fs.FsCharsetArchiveDriver;
import de.schlichtherle.truezip.io.SequentialIOException;
import de.schlichtherle.truezip.io.SequentialIOExceptionBuilder;
import de.schlichtherle.truezip.io.Streams;
import de.schlichtherle.truezip.rof.ReadOnlyFile;
import de.schlichtherle.truezip.socket.IOEntry;
import de.schlichtherle.truezip.socket.IOPool;
import de.schlichtherle.truezip.socket.IOPool.Entry;
import de.schlichtherle.truezip.socket.InputShop;
import de.schlichtherle.truezip.socket.InputSocket;
import static de.schlichtherle.truezip.util.HashMaps.OVERHEAD_SIZE;
import static de.schlichtherle.truezip.util.HashMaps.initialCapacity;
import de.schlichtherle.truezip.util.JSE7;
import edu.umd.cs.findbugs.annotations.CreatesObligation;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PushbackInputStream;
import java.nio.channels.SeekableByteChannel;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.annotation.CheckForNull;
import javax.annotation.WillNotClose;
import javax.annotation.concurrent.NotThreadSafe;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import static org.apache.commons.compress.archivers.tar.TarConstants.DEFAULT_BLKSIZE;
import static org.apache.commons.compress.archivers.tar.TarConstants.DEFAULT_RCDSIZE;
import static org.apache.commons.compress.archivers.tar.TarConstants.GIDLEN;
import static org.apache.commons.compress.archivers.tar.TarConstants.MODELEN;
import static org.apache.commons.compress.archivers.tar.TarConstants.MODTIMELEN;
import static org.apache.commons.compress.archivers.tar.TarConstants.NAMELEN;
import static org.apache.commons.compress.archivers.tar.TarConstants.SIZELEN;
import static org.apache.commons.compress.archivers.tar.TarConstants.UIDLEN;
import org.apache.commons.compress.archivers.tar.TarUtils;
/**
* An input shop for reading TAR files.
*
* Note that 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.
*
* @see TarOutputShop
* @author Christian Schlichtherle
*/
@NotThreadSafe
public class TarInputShop
implements InputShop {
private static final byte[] NULL_RECORD = new byte[DEFAULT_RCDSIZE];
private static final int CHECKSUM_OFFSET
= NAMELEN + MODELEN + UIDLEN + GIDLEN + SIZELEN + MODTIMELEN;
/** HashMaps entry names to I/O pool entries. */
private final Map
entries = new LinkedHashMap(
initialCapacity(OVERHEAD_SIZE));
private final TarDriver driver;
@CreatesObligation
public TarInputShop(final TarDriver driver,
final @WillNotClose InputStream in)
throws EOFException, IOException {
if (null == (this.driver = driver))
throw new NullPointerException();
try {
unpack(newValidatedTarArchiveInputStream(in));
} catch (final IOException ex) {
try {
close0();
} catch (final Throwable ex2) {
if (JSE7.AVAILABLE) ex.addSuppressed(ex2);
}
throw ex;
}
}
private void unpack(final @WillNotClose TarArchiveInputStream tain)
throws IOException {
final TarDriver driver = this.driver;
final IOPool> pool = driver.getPool();
for ( TarArchiveEntry tinEntry;
null != (tinEntry = tain.getNextTarEntry()); ) {
final String name = name(tinEntry);
TarDriverEntry entry = entries.get(name);
if (null != entry)
entry.release();
entry = driver.newEntry(name, tinEntry);
if (!tinEntry.isDirectory()) {
final Entry> buffer = pool.allocate();
entry.setTemp(buffer);
try {
final OutputStream out = buffer.getOutputSocket().newOutputStream();
IOException ex = null;
try {
Streams.cat(tain, out);
} catch (final IOException ex2) {
ex = ex2;
throw ex2;
} finally {
try {
out.close();
} catch (final IOException ex2) {
if (null == ex) throw ex2;
if (JSE7.AVAILABLE) ex.addSuppressed(ex2);
}
}
} catch (final IOException ex) {
try {
buffer.release();
} catch (final IOException ex2) {
if (JSE7.AVAILABLE) ex.addSuppressed(ex2);
}
throw ex;
}
}
entries.put(name, entry);
}
}
private static String name(final TarArchiveEntry entry) {
final String name = entry.getName();
final Type type = entry.isDirectory() ? DIRECTORY : FILE;
return FsCharsetArchiveDriver.toZipOrTarEntryName(name, type);
}
/**
* Returns a newly created and validated {@link TarArchiveInputStream}.
* This method performs a simple validation by computing the checksum
* for the first record only.
* This method is required because the {@code TarArchiveInputStream}
* unfortunately does not do any validation!
*
* @param in the stream to read from.
* @return A stream which holds all the data {@code in} did.
* @throws EOFException on unexpected end-of-file.
* @throws IOException on any I/O error.
*/
private TarArchiveInputStream
newValidatedTarArchiveInputStream(final InputStream in)
throws EOFException, IOException {
final byte[] buf = new byte[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;
try {
expected = TarUtils.parseOctal(buf, CHECKSUM_OFFSET, 8);
} catch (IllegalArgumentException ex) {
throw new IOException("Invalid initial record in TAR file!", ex);
}
for (int i = 0; i < 8; i++)
buf[CHECKSUM_OFFSET + i] = ' ';
final long actual = TarUtils.computeCheckSum(buf);
if (expected != actual)
throw new IOException(
"Invalid initial record in TAR file: Expected / actual checksum : "
+ expected + " / " + actual + "!");
}
return new TarArchiveInputStream( vin,
DEFAULT_BLKSIZE,
DEFAULT_RCDSIZE,
driver.getEncoding());
}
/**
* 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 EOFException on unexpected end-of-file.
* @throws IOException on any I/O error.
*/
static InputStream readAhead(final InputStream in, final byte[] buf)
throws EOFException, IOException {
if (in.markSupported()) {
in.mark(buf.length);
new DataInputStream(in).readFully(buf);
in.reset();
return in;
} else {
final PushbackInputStream pin
= new PushbackInputStream(in, buf.length);
new DataInputStream(pin).readFully(buf);
pin.unread(buf);
return pin;
}
}
@Override
public final int getSize() {
return entries.size();
}
@Override
public final Iterator iterator() {
return Collections.unmodifiableCollection(entries.values()).iterator();
}
@Override
public final @CheckForNull TarDriverEntry getEntry(String name) {
return entries.get(name);
}
@Override
public InputSocket getInputSocket(final String name) {
if (null == name)
throw new NullPointerException();
final class Input extends InputSocket {
@Override
public TarDriverEntry getLocalTarget() throws IOException {
final TarDriverEntry entry = getEntry(name);
if (null == entry)
throw new FileNotFoundException(name + " (entry not found)");
if (entry.isDirectory())
throw new FileNotFoundException(name + " (cannot read directory entries)");
return entry;
}
@Override
public ReadOnlyFile newReadOnlyFile() throws IOException {
return getInputSocket().newReadOnlyFile();
}
@Override
public SeekableByteChannel newSeekableByteChannel() throws IOException {
return getInputSocket().newSeekableByteChannel();
}
@Override
public InputStream newInputStream()
throws IOException {
return getInputSocket().newInputStream();
}
InputSocket extends IOEntry>> getInputSocket()
throws IOException {
return getLocalTarget().getTemp().getInputSocket();
}
} // Input
return new Input();
}
@Override
public void close() throws IOException {
close0();
}
private void close0() throws IOException {
SequentialIOExceptionBuilder
builder = SequentialIOExceptionBuilder.create(IOException.class);
for (final Iterator i = entries.values().iterator();
i.hasNext();
i.remove()) {
try {
i.next().release();
} catch (final IOException ex) {
builder.warn(ex);
}
}
builder.check();
}
}