
de.schlichtherle.truezip.fs.archive.tar.TarInputShop Maven / Gradle / Ivy
/*
* Copyright (C) 2006-2011 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.truezip.fs.archive.tar;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.DefaultAnnotation;
import de.schlichtherle.truezip.entry.EntryName;
import de.schlichtherle.truezip.io.Streams;
import de.schlichtherle.truezip.rof.ReadOnlyFile;
import de.schlichtherle.truezip.socket.InputSocket;
import de.schlichtherle.truezip.socket.InputShop;
import de.schlichtherle.truezip.socket.IOPool;
import de.schlichtherle.truezip.socket.IOPool.Entry;
import java.io.OutputStream;
import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.tools.tar.TarBuffer;
import org.apache.tools.tar.TarEntry;
import org.apache.tools.tar.TarInputStream;
import org.apache.tools.tar.TarUtils;
import static de.schlichtherle.truezip.entry.EntryName.SEPARATOR;
import static de.schlichtherle.truezip.entry.EntryName.SEPARATOR_CHAR;
import static org.apache.tools.tar.TarConstants.GIDLEN;
import static org.apache.tools.tar.TarConstants.MODELEN;
import static org.apache.tools.tar.TarConstants.MODTIMELEN;
import static org.apache.tools.tar.TarConstants.NAMELEN;
import static org.apache.tools.tar.TarConstants.SIZELEN;
import static org.apache.tools.tar.TarConstants.UIDLEN;
/**
* 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.
*
* @see TarOutputShop
* @author Christian Schlichtherle
* @version $Id$
*/
@DefaultAnnotation(NonNull.class)
public class TarInputShop
implements InputShop {
private static final byte[] NULL_RECORD = new byte[TarBuffer.DEFAULT_RCDSIZE];
private static final int CHECKSUM_OFFSET
= NAMELEN + MODELEN + UIDLEN + GIDLEN + SIZELEN + MODTIMELEN;
/** Maps entry names to I/O pool entries. */
private final Map
entries = new LinkedHashMap();
/**
* 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 TarInputShop(final TarDriver driver, final InputStream in)
throws IOException {
final TarInputStream tin = newValidatedTarInputStream(in);
final IOPool> pool = driver.getPool();
try {
TarEntry tinEntry;
while (null != (tinEntry = tin.getNextEntry())) {
final String name = getName(tinEntry);
TarArchiveEntry entry = entries.get(name);
if (null != entry)
entry.release();
entry = new TarArchiveEntry(name, tinEntry);
if (!tinEntry.isDirectory()) {
final Entry> temp = pool.allocate();
entry.setTemp(temp);
try {
final OutputStream
out = temp.getOutputSocket().newOutputStream();
try {
Streams.cat(tin, out);
} finally {
out.close();
}
} catch (IOException ex) {
temp.release();
throw ex;
}
}
entries.put(name, entry);
}
} catch (IOException ex) {
close0();
throw ex;
}
}
/**
* Returns the fixed name of the given TAR entry, ensuring that it ends
* with a {@link EntryName#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(TarEntry entry) {
final String name = entry.getName();
return entry.isDirectory() && !name.endsWith(SEPARATOR) ? name + 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 newValidatedTarInputStream(
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 {
if (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 (0 >= r)
throw new EOFException();
n += r;
} while (n < l);
}
@Override
public final int getSize() {
return entries.size();
}
@Override
public final Iterator iterator() {
return entries.values().iterator();
}
@Override
public final TarArchiveEntry getEntry(String name) {
return entries.get(name);
}
@Override
public InputSocket getInputSocket(final String name) {
if (null == name)
throw new NullPointerException();
class Input extends InputSocket {
@Override
public TarArchiveEntry getLocalTarget() throws IOException {
final TarArchiveEntry entry = getEntry(name);
if (null == entry)
throw new FileNotFoundException(name + " (entry not found)");
if (entry.isDirectory())
throw new FileNotFoundException(name + " (cannot read directories)");
return entry;
}
@Override
public ReadOnlyFile newReadOnlyFile() throws IOException {
return getLocalTarget().getTemp().getInputSocket().newReadOnlyFile();
}
@Override
public InputStream newInputStream()
throws IOException {
return getLocalTarget().getTemp().getInputSocket().newInputStream();
}
} // class Input
return new Input();
}
@Override
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 TarArchiveEntry entry = i.next();
entry.release();
}
}
}