    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one
    003 * or more contributor license agreements.  See the NOTICE file
    004 * distributed with this work for additional information
    005 * regarding copyright ownership.  The ASF licenses this file
    006 * to you under the Apache License, Version 2.0 (the
    007 * "License"); you may not use this file except in compliance
    008 * with the License.  You may obtain a copy of the License at
    009 *
    010 * http://www.apache.org/licenses/LICENSE-2.0
    011 *
    012 * Unless required by applicable law or agreed to in writing,
    013 * software distributed under the License is distributed on an
    014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    015 * KIND, either express or implied.  See the License for the
    016 * specific language governing permissions and limitations
    017 * under the License.
    018 */
    019package org.apache.commons.compress.archivers.examples;
    020
    021import java.io.BufferedInputStream;
    022import java.io.File;
    023import java.io.IOException;
    024import java.io.InputStream;
    025import java.io.OutputStream;
    026import java.nio.channels.Channels;
    027import java.nio.channels.FileChannel;
    028import java.nio.channels.SeekableByteChannel;
    029import java.nio.file.Files;
    030import java.nio.file.StandardOpenOption;
    031import java.util.Enumeration;
    032import java.util.Iterator;
    033
    034import org.apache.commons.compress.archivers.ArchiveEntry;
    035import org.apache.commons.compress.archivers.ArchiveException;
    036import org.apache.commons.compress.archivers.ArchiveInputStream;
    037import org.apache.commons.compress.archivers.ArchiveStreamFactory;
    038import org.apache.commons.compress.archivers.sevenz.SevenZFile;
    039import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
    040import org.apache.commons.compress.archivers.tar.TarFile;
    041import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
    042import org.apache.commons.compress.archivers.zip.ZipFile;
    043import org.apache.commons.compress.utils.IOUtils;
    044
    045/**
    046 * Provides a high level API for expanding archives.
    047 * @since 1.17
    048 */
    049public class Expander {
    050
    051    private interface ArchiveEntrySupplier {
    052        ArchiveEntry getNextReadableEntry() throws IOException;
    053    }
    054
    055    private interface EntryWriter {
    056        void writeEntryDataTo(ArchiveEntry entry, OutputStream out) throws IOException;
    057    }
    058
    059    /**
    060     * Expands {@code archive} into {@code targetDirectory}.
    061     *
    062     * <p>Tries to auto-detect the archive's format.</p>
    063     *
    064     * @param archive the file to expand
    065     * @param targetDirectory the directory to write to
    066     * @throws IOException if an I/O error occurs
    067     * @throws ArchiveException if the archive cannot be read for other reasons
    068     */
    069    public void expand(final File archive, final File targetDirectory) throws IOException, ArchiveException {
    070        String format = null;
    071        try (InputStream i = new BufferedInputStream(Files.newInputStream(archive.toPath()))) {
    072            format = ArchiveStreamFactory.detect(i);
    073        }
    074        expand(format, archive, targetDirectory);
    075    }
    076
    077    /**
    078     * Expands {@code archive} into {@code targetDirectory}.
    079     *
    080     * @param archive the file to expand
    081     * @param targetDirectory the directory to write to
    082     * @param format the archive format. This uses the same format as
    083     * accepted by {@link ArchiveStreamFactory}.
    084     * @throws IOException if an I/O error occurs
    085     * @throws ArchiveException if the archive cannot be read for other reasons
    086     */
    087    public void expand(final String format, final File archive, final File targetDirectory) throws IOException, ArchiveException {
    088        if (prefersSeekableByteChannel(format)) {
    089            try (SeekableByteChannel c = FileChannel.open(archive.toPath(), StandardOpenOption.READ)) {
    090                expand(format, c, targetDirectory, CloseableConsumer.CLOSING_CONSUMER);
    091            }
    092            return;
    093        }
    094        try (InputStream i = new BufferedInputStream(Files.newInputStream(archive.toPath()))) {
    095            expand(format, i, targetDirectory, CloseableConsumer.CLOSING_CONSUMER);
    096        }
    097    }
    098
    099    /**
    100     * Expands {@code archive} into {@code targetDirectory}.
    101     *
    102     * <p>Tries to auto-detect the archive's format.</p>
    103     *
    104     * <p>This method creates a wrapper around the archive stream
    105     * which is never closed and thus leaks resources, please use
    106     * {@link #expand(InputStream,File,CloseableConsumer)}
    107     * instead.</p>
    108     *
    109     * @param archive the file to expand
    110     * @param targetDirectory the directory to write to
    111     * @throws IOException if an I/O error occurs
    112     * @throws ArchiveException if the archive cannot be read for other reasons
    113     * @deprecated this method leaks resources
    114     */
    115    @Deprecated
    116    public void expand(final InputStream archive, final File targetDirectory) throws IOException, ArchiveException {
    117        expand(archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
    118    }
    119
    120    /**
    121     * Expands {@code archive} into {@code targetDirectory}.
    122     *
    123     * <p>Tries to auto-detect the archive's format.</p>
    124     *
    125     * <p>This method creates a wrapper around the archive stream and
    126     * the caller of this method is responsible for closing it -
    127     * probably at the same time as closing the stream itself. The
    128     * caller is informed about the wrapper object via the {@code
    129     * closeableConsumer} callback as soon as it is no longer needed
    130     * by this class.</p>
    131     *
    132     * @param archive the file to expand
    133     * @param targetDirectory the directory to write to
    134     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
    135     * @throws IOException if an I/O error occurs
    136     * @throws ArchiveException if the archive cannot be read for other reasons
    137     * @since 1.19
    138     */
    139    public void expand(final InputStream archive, final File targetDirectory, final CloseableConsumer closeableConsumer)
    140        throws IOException, ArchiveException {
    141        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
    142            expand(c.track(ArchiveStreamFactory.DEFAULT.createArchiveInputStream(archive)),
    143                targetDirectory);
    144        }
    145    }
    146
    147    /**
    148     * Expands {@code archive} into {@code targetDirectory}.
    149     *
    150     * <p>This method creates a wrapper around the archive stream
    151     * which is never closed and thus leaks resources, please use
    152     * {@link #expand(String,InputStream,File,CloseableConsumer)}
    153     * instead.</p>
    154     *
    155     * @param archive the file to expand
    156     * @param targetDirectory the directory to write to
    157     * @param format the archive format. This uses the same format as
    158     * accepted by {@link ArchiveStreamFactory}.
    159     * @throws IOException if an I/O error occurs
    160     * @throws ArchiveException if the archive cannot be read for other reasons
    161     * @deprecated this method leaks resources
    162     */
    163    @Deprecated
    164    public void expand(final String format, final InputStream archive, final File targetDirectory)
    165        throws IOException, ArchiveException {
    166        expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
    167    }
    168
    169    /**
    170     * Expands {@code archive} into {@code targetDirectory}.
    171     *
    172     * <p>This method creates a wrapper around the archive stream and
    173     * the caller of this method is responsible for closing it -
    174     * probably at the same time as closing the stream itself. The
    175     * caller is informed about the wrapper object via the {@code
    176     * closeableConsumer} callback as soon as it is no longer needed
    177     * by this class.</p>
    178     *
    179     * @param archive the file to expand
    180     * @param targetDirectory the directory to write to
    181     * @param format the archive format. This uses the same format as
    182     * accepted by {@link ArchiveStreamFactory}.
    183     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
    184     * @throws IOException if an I/O error occurs
    185     * @throws ArchiveException if the archive cannot be read for other reasons
    186     * @since 1.19
    187     */
    188    public void expand(final String format, final InputStream archive, final File targetDirectory, final CloseableConsumer closeableConsumer)
    189        throws IOException, ArchiveException {
    190        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
    191            expand(c.track(ArchiveStreamFactory.DEFAULT.createArchiveInputStream(format, archive)),
    192                targetDirectory);
    193        }
    194    }
    195
    196    /**
    197     * Expands {@code archive} into {@code targetDirectory}.
    198     *
    199     * <p>This method creates a wrapper around the archive channel
    200     * which is never closed and thus leaks resources, please use
    201     * {@link #expand(String,SeekableByteChannel,File,CloseableConsumer)}
    202     * instead.</p>
    203     *
    204     * @param archive the file to expand
    205     * @param targetDirectory the directory to write to
    206     * @param format the archive format. This uses the same format as
    207     * accepted by {@link ArchiveStreamFactory}.
    208     * @throws IOException if an I/O error occurs
    209     * @throws ArchiveException if the archive cannot be read for other reasons
    210     * @deprecated this method leaks resources
    211     */
    212    @Deprecated
    213    public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory)
    214        throws IOException, ArchiveException {
    215        expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
    216    }
    217
    218    /**
    219     * Expands {@code archive} into {@code targetDirectory}.
    220     *
    221     * <p>This method creates a wrapper around the archive channel and
    222     * the caller of this method is responsible for closing it -
    223     * probably at the same time as closing the channel itself. The
    224     * caller is informed about the wrapper object via the {@code
    225     * closeableConsumer} callback as soon as it is no longer needed
    226     * by this class.</p>
    227     *
    228     * @param archive the file to expand
    229     * @param targetDirectory the directory to write to
    230     * @param format the archive format. This uses the same format as
    231     * accepted by {@link ArchiveStreamFactory}.
    232     * @param closeableConsumer is informed about the stream wrapped around the passed in channel
    233     * @throws IOException if an I/O error occurs
    234     * @throws ArchiveException if the archive cannot be read for other reasons
    235     * @since 1.19
    236     */
    237    public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory,
    238        final CloseableConsumer closeableConsumer)
    239        throws IOException, ArchiveException {
    240        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
    241        if (!prefersSeekableByteChannel(format)) {
    242            expand(format, c.track(Channels.newInputStream(archive)), targetDirectory);
    243        } else if (ArchiveStreamFactory.TAR.equalsIgnoreCase(format)) {
    244            expand(c.track(new TarFile(archive)), targetDirectory);
    245        } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
    246            expand(c.track(new ZipFile(archive)), targetDirectory);
    247        } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
    248            expand(c.track(new SevenZFile(archive)), targetDirectory);
    249        } else {
    250            // never reached as prefersSeekableByteChannel only returns true for TAR, ZIP and 7z
    251            throw new ArchiveException("Don't know how to handle format " + format);
    252        }
    253        }
    254    }
    255
    256    /**
    257     * Expands {@code archive} into {@code targetDirectory}.
    258     *
    259     * @param archive the file to expand
    260     * @param targetDirectory the directory to write to
    261     * @throws IOException if an I/O error occurs
    262     * @throws ArchiveException if the archive cannot be read for other reasons
    263     */
    264    public void expand(final ArchiveInputStream archive, final File targetDirectory)
    265        throws IOException, ArchiveException {
    266        expand(() -> {
    267            ArchiveEntry next = archive.getNextEntry();
    268            while (next != null && !archive.canReadEntryData(next)) {
    269                next = archive.getNextEntry();
    270            }
    271            return next;
    272        }, (entry, out) -> IOUtils.copy(archive, out), targetDirectory);
    273    }
    274
    275    /**
    276     * Expands {@code archive} into {@code targetDirectory}.
    277     *
    278     * @param archive the file to expand
    279     * @param targetDirectory the directory to write to
    280     * @throws IOException if an I/O error occurs
    281     * @throws ArchiveException if the archive cannot be read for other reasons
    282     * @since 1.21
    283     */
    284    public void expand(final TarFile archive, final File targetDirectory)
    285        throws IOException, ArchiveException {
    286        final Iterator<TarArchiveEntry> entryIterator = archive.getEntries().iterator();
    287        expand(() -> entryIterator.hasNext() ? entryIterator.next() : null,
    288            (entry, out) -> {
    289            try (InputStream in = archive.getInputStream((TarArchiveEntry) entry)) {
    290                IOUtils.copy(in, out);
    291            }
    292        }, targetDirectory);
    293    }
    294
    295    /**
    296     * Expands {@code archive} into {@code targetDirectory}.
    297     *
    298     * @param archive the file to expand
    299     * @param targetDirectory the directory to write to
    300     * @throws IOException if an I/O error occurs
    301     * @throws ArchiveException if the archive cannot be read for other reasons
    302     */
    303    public void expand(final ZipFile archive, final File targetDirectory)
    304        throws IOException, ArchiveException {
    305        final Enumeration<ZipArchiveEntry> entries = archive.getEntries();
    306        expand(() -> {
    307            ZipArchiveEntry next = entries.hasMoreElements() ? entries.nextElement() : null;
    308            while (next != null && !archive.canReadEntryData(next)) {
    309                next = entries.hasMoreElements() ? entries.nextElement() : null;
    310            }
    311            return next;
    312        }, (entry, out) -> {
    313            try (InputStream in = archive.getInputStream((ZipArchiveEntry) entry)) {
    314                IOUtils.copy(in, out);
    315            }
    316        }, targetDirectory);
    317    }
    318
    319    /**
    320     * Expands {@code archive} into {@code targetDirectory}.
    321     *
    322     * @param archive the file to expand
    323     * @param targetDirectory the directory to write to
    324     * @throws IOException if an I/O error occurs
    325     * @throws ArchiveException if the archive cannot be read for other reasons
    326     */
    327    public void expand(final SevenZFile archive, final File targetDirectory)
    328        throws IOException, ArchiveException {
    329        expand(archive::getNextEntry, (entry, out) -> {
    330            final byte[] buffer = new byte[8192];
    331            int n;
    332            while (-1 != (n = archive.read(buffer))) {
    333                out.write(buffer, 0, n);
    334            }
    335        }, targetDirectory);
    336    }
    337
    338    private boolean prefersSeekableByteChannel(final String format) {
    339        return ArchiveStreamFactory.TAR.equalsIgnoreCase(format)
    340            || ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)
    341            || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format);
    342    }
    343
    344    private void expand(final ArchiveEntrySupplier supplier, final EntryWriter writer, final File targetDirectory)
    345        throws IOException {
    346        String targetDirPath = targetDirectory.getCanonicalPath();
    347        if (!targetDirPath.endsWith(File.separator)) {
    348            targetDirPath += File.separator;
    349        }
    350        ArchiveEntry nextEntry = supplier.getNextReadableEntry();
    351        while (nextEntry != null) {
    352            final File f = new File(targetDirectory, nextEntry.getName());
    353            if (!f.getCanonicalPath().startsWith(targetDirPath)) {
    354                throw new IOException("Expanding " + nextEntry.getName()
    355                    + " would create file outside of " + targetDirectory);
    356            }
    357            if (nextEntry.isDirectory()) {
    358                if (!f.isDirectory() && !f.mkdirs()) {
    359                    throw new IOException("Failed to create directory " + f);
    360                }
    361            } else {
    362                final File parent = f.getParentFile();
    363                if (!parent.isDirectory() && !parent.mkdirs()) {
    364                    throw new IOException("Failed to create directory " + parent);
    365                }
    366                try (OutputStream o = Files.newOutputStream(f.toPath())) {
    367                    writer.writeEntryDataTo(nextEntry, o);
    368                }
    369            }
    370            nextEntry = supplier.getNextReadableEntry();
    371        }
    372    }
    373
    374}