/**
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* muCommander is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.commons.file.archive.tar;
import com.mucommander.commons.file.*;
import com.mucommander.commons.file.archive.AbstractROArchiveFile;
import com.mucommander.commons.file.archive.ArchiveEntry;
import com.mucommander.commons.file.archive.ArchiveEntryIterator;
import com.mucommander.commons.file.archive.tar.provider.TarEntry;
import com.mucommander.commons.file.archive.tar.provider.TarInputStream;
import com.mucommander.commons.io.StreamUtils;
import com.mucommander.commons.util.StringUtils;
import org.apache.tools.bzip2.CBZip2InputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.GZIPInputStream;
/**
* TarArchiveFile provides read-only access to archives in the Tar/Tgz format.
*
* <p>The actual decompression work is performed by the <code>Apache Ant</code> library under the terms of the
* Apache Software License.</p>
*
* @see com.mucommander.commons.file.archive.tar.TarFormatProvider
* @author Maxence Bernard
*/
public class TarArchiveFile extends AbstractROArchiveFile {
private static final Logger LOGGER = LoggerFactory.getLogger(TarArchiveFile.class);
/**
* Creates a TarArchiveFile on of the given file.
*
* @param file the underlying archive file
*/
public TarArchiveFile(AbstractFile file) {
super(file);
}
/**
* Returns a TarInputStream which can be used to read TAR entries.
*
* @param entryOffset offset from the start of the archive to an entry. Must be a multiple of recordSize, or
* <code>0</code> to start at the first entry.
* @return a TarInputStream which can be used to read TAR entries
* @throws IOException if an error occurred while create the stream
* @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem,
* or is not implemented.
*/
private TarInputStream createTarStream(long entryOffset) throws IOException, UnsupportedFileOperationException {
InputStream in = file.getInputStream();
String name = getCustomExtension() != null ? getCustomExtension() : getName();
// Gzip-compressed file
if(StringUtils.endsWithIgnoreCase(name, "tgz") || StringUtils.endsWithIgnoreCase(name, "tar.gz"))
// Note: this will fail for gz/tgz entries inside a tar file (IOException: Not in GZIP format),
// why is a complete mystery: the gz/tgz entry can be extracted and then properly browsed
in = new GZIPInputStream(in);
// Bzip2-compressed file
else if(StringUtils.endsWithIgnoreCase(name, "tbz2") || StringUtils.endsWithIgnoreCase(name, "tar.bz2")) {
try {
// Skips the 2 magic bytes 'BZ', as required by CBZip2InputStream. Quoted from CBZip2InputStream's Javadoc:
// "Although BZip2 headers are marked with the magic 'Bz'. this constructor expects the next byte in the
// stream to be the first one after the magic. Thus callers have to skip the first two bytes. Otherwise
// this constructor will throw an exception."
StreamUtils.skipFully(in, 2);
// Quoted from CBZip2InputStream's Javadoc:
// "CBZip2InputStream reads bytes from the compressed source stream via the single byte {@link java.io.InputStream#read()
// read()} method exclusively. Thus you should consider to use a buffered source stream."
in = new CBZip2InputStream(new BufferedInputStream(in));
}
catch(Exception e) {
// CBZip2InputStream is known to throw NullPointerException if file is not properly Bzip2-encoded
// so we need to catch those and throw them as IOException
LOGGER.info("Exception caught while creating CBZip2InputStream, throwing IOException", e);
throw new IOException();
}
}
return new TarInputStream(in, entryOffset);
}
////////////////////////////////////////
// AbstractArchiveFile implementation //
////////////////////////////////////////
@Override
public ArchiveEntryIterator getEntryIterator() throws IOException, UnsupportedFileOperationException {
return new TarEntryIterator(createTarStream(0));
}
@Override
public InputStream getEntryInputStream(ArchiveEntry entry, ArchiveEntryIterator entryIterator) throws IOException, UnsupportedFileOperationException {
if(entry.isDirectory())
throw new IOException();
// Optimization: first check if the specified iterator is positionned at the beginning of the entry.
// This will typically be the case if an iterator is being used to read all the archive's entries
// (unpack operation). In that case, we save the cost of looking for the entry in the archive, which is all
// the more expensive if the TAR archive is GZipped.
if(entryIterator!=null && (entryIterator instanceof TarEntryIterator)) {
ArchiveEntry currentEntry = ((TarEntryIterator)entryIterator).getCurrentEntry();
if(currentEntry.getPath().equals(entry.getPath())) {
// The entry/tar stream is wrapped in a FilterInputStream where #close is implemented as a no-op:
// we don't want the TarInputStream to be closed when the caller closes the entry's stream.
return new FilterInputStream(((TarEntryIterator)entryIterator).getTarInputStream()) {
@Override
public void close() throws IOException {
// No-op
}
};
}
// This is not the one, look for the entry from the beginning of the archive
}
// Iterate through the archive until we've found the entry
TarEntry tarEntry = (TarEntry)entry.getEntryObject();
if(tarEntry!=null) {
TarInputStream tin = createTarStream(tarEntry.getOffset());
tin.getNextEntry();
return tin;
}
throw new IOException("Unknown TAR entry: "+entry.getName());
}
}