    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.Path;
    031import java.nio.file.StandardOpenOption;
    032import java.util.Enumeration;
    033import java.util.Iterator;
    034
    035import org.apache.commons.compress.archivers.ArchiveEntry;
    036import org.apache.commons.compress.archivers.ArchiveException;
    037import org.apache.commons.compress.archivers.ArchiveInputStream;
    038import org.apache.commons.compress.archivers.ArchiveStreamFactory;
    039import org.apache.commons.compress.archivers.sevenz.SevenZFile;
    040import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
    041import org.apache.commons.compress.archivers.tar.TarFile;
    042import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
    043import org.apache.commons.compress.archivers.zip.ZipFile;
    044import org.apache.commons.compress.utils.IOUtils;
    045
    046/**
    047 * Provides a high level API for expanding archives.
    048 * @since 1.17
    049 */
    050public class Expander {
    051
    052    @FunctionalInterface
    053    private interface ArchiveEntrySupplier<T extends ArchiveEntry> {
    054        T get() throws IOException;
    055    }
    056
    057    @FunctionalInterface
    058    private interface ArchiveEntryBiConsumer<T extends ArchiveEntry> {
    059        void accept(T entry, OutputStream out) throws IOException;
    060    }
    061
    062    /**
    063     * @param targetDirectory May be null to simulate output to dev/null on Linux and NUL on Windows.
    064     */
    065    private <T extends ArchiveEntry> void expand(final ArchiveEntrySupplier<T> supplier, final ArchiveEntryBiConsumer<T> writer, final Path targetDirectory)
    066        throws IOException {
    067        final boolean nullTarget = targetDirectory == null;
    068        final Path targetDirPath = nullTarget ? null : targetDirectory.normalize();
    069        T nextEntry = supplier.get();
    070        while (nextEntry != null) {
    071            final Path targetPath = nullTarget ? null : targetDirectory.resolve(nextEntry.getName());
    072            // check if targetDirectory and f are the same path - this may
    073            // happen if the nextEntry.getName() is "./"
    074            if (!nullTarget && !targetPath.normalize().startsWith(targetDirPath) && !Files.isSameFile(targetDirectory, targetPath)) {
    075                throw new IOException("Expanding " + nextEntry.getName() + " would create file outside of " + targetDirectory);
    076            }
    077            if (nextEntry.isDirectory()) {
    078                if (!nullTarget && !Files.isDirectory(targetPath) && Files.createDirectories(targetPath) == null) {
    079                    throw new IOException("Failed to create directory " + targetPath);
    080                }
    081            } else {
    082                final Path parent = nullTarget ? null : targetPath.getParent();
    083                if (!nullTarget && !Files.isDirectory(parent) && Files.createDirectories(parent) == null) {
    084                    throw new IOException("Failed to create directory " + parent);
    085                }
    086                if (nullTarget) {
    087                    writer.accept(nextEntry, null);
    088                } else {
    089                    try (OutputStream outputStream = Files.newOutputStream(targetPath)) {
    090                        writer.accept(nextEntry, outputStream);
    091                    }
    092                }
    093            }
    094            nextEntry = supplier.get();
    095        }
    096    }
    097
    098    /**
    099     * Expands {@code archive} into {@code targetDirectory}.
    100     *
    101     * @param archive the file to expand
    102     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    103     * @throws IOException if an I/O error occurs
    104     */
    105    public void expand(final ArchiveInputStream archive, final File targetDirectory) throws IOException {
    106        expand(archive, toPath(targetDirectory));
    107    }
    108
    109    /**
    110     * Expands {@code archive} into {@code targetDirectory}.
    111     *
    112     * @param archive the file to expand
    113     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    114     * @throws IOException if an I/O error occurs
    115     * @since 1.22
    116     */
    117    public void expand(final ArchiveInputStream archive, final Path targetDirectory) throws IOException {
    118        expand(() -> {
    119            ArchiveEntry next = archive.getNextEntry();
    120            while (next != null && !archive.canReadEntryData(next)) {
    121                next = archive.getNextEntry();
    122            }
    123            return next;
    124        }, (entry, out) -> IOUtils.copy(archive, out), targetDirectory);
    125    }
    126
    127    /**
    128     * Expands {@code archive} into {@code targetDirectory}.
    129     *
    130     * <p>Tries to auto-detect the archive's format.</p>
    131     *
    132     * @param archive the file to expand
    133     * @param targetDirectory the target directory
    134     * @throws IOException if an I/O error occurs
    135     * @throws ArchiveException if the archive cannot be read for other reasons
    136     */
    137    public void expand(final File archive, final File targetDirectory) throws IOException, ArchiveException {
    138        expand(archive.toPath(), toPath(targetDirectory));
    139    }
    140
    141    /**
    142     * Expands {@code archive} into {@code targetDirectory}.
    143     *
    144     * <p>Tries to auto-detect the archive's format.</p>
    145     *
    146     * <p>This method creates a wrapper around the archive stream
    147     * which is never closed and thus leaks resources, please use
    148     * {@link #expand(InputStream,File,CloseableConsumer)}
    149     * instead.</p>
    150     *
    151     * @param archive the file to expand
    152     * @param targetDirectory the target directory
    153     * @throws IOException if an I/O error occurs
    154     * @throws ArchiveException if the archive cannot be read for other reasons
    155     * @deprecated this method leaks resources
    156     */
    157    @Deprecated
    158    public void expand(final InputStream archive, final File targetDirectory) throws IOException, ArchiveException {
    159        expand(archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
    160    }
    161
    162    /**
    163     * Expands {@code archive} into {@code targetDirectory}.
    164     *
    165     * <p>Tries to auto-detect the archive's format.</p>
    166     *
    167     * <p>This method creates a wrapper around the archive stream and
    168     * the caller of this method is responsible for closing it -
    169     * probably at the same time as closing the stream itself. The
    170     * caller is informed about the wrapper object via the {@code
    171     * closeableConsumer} callback as soon as it is no longer needed
    172     * by this class.</p>
    173     *
    174     * @param archive the file to expand
    175     * @param targetDirectory the target directory
    176     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
    177     * @throws IOException if an I/O error occurs
    178     * @throws ArchiveException if the archive cannot be read for other reasons
    179     * @since 1.19
    180     */
    181    public void expand(final InputStream archive, final File targetDirectory, final CloseableConsumer closeableConsumer)
    182        throws IOException, ArchiveException {
    183        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
    184            expand(c.track(ArchiveStreamFactory.DEFAULT.createArchiveInputStream(archive)),
    185                targetDirectory);
    186        }
    187    }
    188
    189    /**
    190     * Expands {@code archive} into {@code targetDirectory}.
    191     *
    192     * <p>Tries to auto-detect the archive's format.</p>
    193     *
    194     * @param archive the file to expand
    195     * @param targetDirectory the target directory
    196     * @throws IOException if an I/O error occurs
    197     * @throws ArchiveException if the archive cannot be read for other reasons
    198     * @since 1.22
    199     */
    200    public void expand(final Path archive, final Path targetDirectory) throws IOException, ArchiveException {
    201        try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(archive))) {
    202            String format = ArchiveStreamFactory.detect(inputStream);
    203            expand(format, archive, targetDirectory);
    204        }
    205    }
    206
    207    /**
    208     * Expands {@code archive} into {@code targetDirectory}.
    209     *
    210     * @param archive the file to expand
    211     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    212     * @throws IOException if an I/O error occurs
    213     */
    214    public void expand(final SevenZFile archive, final File targetDirectory) throws IOException {
    215        expand(archive, toPath(targetDirectory));
    216    }
    217
    218    /**
    219     * Expands {@code archive} into {@code targetDirectory}.
    220     *
    221     * @param archive the file to expand
    222     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    223     * @throws IOException if an I/O error occurs
    224     * @since 1.22
    225     */
    226    public void expand(final SevenZFile archive, final Path targetDirectory)
    227        throws IOException {
    228        expand(archive::getNextEntry, (entry, out) -> {
    229            final byte[] buffer = new byte[8192];
    230            int n;
    231            while (-1 != (n = archive.read(buffer))) {
    232                if (out != null) {
    233                    out.write(buffer, 0, n);
    234                }
    235            }
    236        }, targetDirectory);
    237    }
    238
    239    /**
    240     * Expands {@code archive} into {@code targetDirectory}.
    241     *
    242     * @param archive the file to expand
    243     * @param targetDirectory the target directory
    244     * @param format the archive format. This uses the same format as
    245     * accepted by {@link ArchiveStreamFactory}.
    246     * @throws IOException if an I/O error occurs
    247     * @throws ArchiveException if the archive cannot be read for other reasons
    248     */
    249    public void expand(final String format, final File archive, final File targetDirectory) throws IOException, ArchiveException {
    250        expand(format, archive.toPath(), toPath(targetDirectory));
    251    }
    252
    253    /**
    254     * Expands {@code archive} into {@code targetDirectory}.
    255     *
    256     * <p>This method creates a wrapper around the archive stream
    257     * which is never closed and thus leaks resources, please use
    258     * {@link #expand(String,InputStream,File,CloseableConsumer)}
    259     * instead.</p>
    260     *
    261     * @param archive the file to expand
    262     * @param targetDirectory the target directory
    263     * @param format the archive format. This uses the same format as
    264     * accepted by {@link ArchiveStreamFactory}.
    265     * @throws IOException if an I/O error occurs
    266     * @throws ArchiveException if the archive cannot be read for other reasons
    267     * @deprecated this method leaks resources
    268     */
    269    @Deprecated
    270    public void expand(final String format, final InputStream archive, final File targetDirectory)
    271        throws IOException, ArchiveException {
    272        expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
    273    }
    274
    275    /**
    276     * Expands {@code archive} into {@code targetDirectory}.
    277     *
    278     * <p>This method creates a wrapper around the archive stream and
    279     * the caller of this method is responsible for closing it -
    280     * probably at the same time as closing the stream itself. The
    281     * caller is informed about the wrapper object via the {@code
    282     * closeableConsumer} callback as soon as it is no longer needed
    283     * by this class.</p>
    284     *
    285     * @param archive the file to expand
    286     * @param targetDirectory the target directory
    287     * @param format the archive format. This uses the same format as
    288     * accepted by {@link ArchiveStreamFactory}.
    289     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
    290     * @throws IOException if an I/O error occurs
    291     * @throws ArchiveException if the archive cannot be read for other reasons
    292     * @since 1.19
    293     */
    294    public void expand(final String format, final InputStream archive, final File targetDirectory, final CloseableConsumer closeableConsumer)
    295        throws IOException, ArchiveException {
    296        expand(format, archive, toPath(targetDirectory), closeableConsumer);
    297    }
    298
    299    /**
    300     * Expands {@code archive} into {@code targetDirectory}.
    301     *
    302     * <p>This method creates a wrapper around the archive stream and
    303     * the caller of this method is responsible for closing it -
    304     * probably at the same time as closing the stream itself. The
    305     * caller is informed about the wrapper object via the {@code
    306     * closeableConsumer} callback as soon as it is no longer needed
    307     * by this class.</p>
    308     *
    309     * @param archive the file to expand
    310     * @param targetDirectory the target directory
    311     * @param format the archive format. This uses the same format as
    312     * accepted by {@link ArchiveStreamFactory}.
    313     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
    314     * @throws IOException if an I/O error occurs
    315     * @throws ArchiveException if the archive cannot be read for other reasons
    316     * @since 1.22
    317     */
    318    public void expand(final String format, final InputStream archive, final Path targetDirectory, final CloseableConsumer closeableConsumer)
    319        throws IOException, ArchiveException {
    320        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
    321            expand(c.track(ArchiveStreamFactory.DEFAULT.createArchiveInputStream(format, archive)),
    322                targetDirectory);
    323        }
    324    }
    325
    326    /**
    327     * Expands {@code archive} into {@code targetDirectory}.
    328     *
    329     * @param archive the file to expand
    330     * @param targetDirectory the target directory
    331     * @param format the archive format. This uses the same format as
    332     * accepted by {@link ArchiveStreamFactory}.
    333     * @throws IOException if an I/O error occurs
    334     * @throws ArchiveException if the archive cannot be read for other reasons
    335     * @since 1.22
    336     */
    337    public void expand(final String format, final Path archive, final Path targetDirectory) throws IOException, ArchiveException {
    338        if (prefersSeekableByteChannel(format)) {
    339            try (SeekableByteChannel channel = FileChannel.open(archive, StandardOpenOption.READ)) {
    340                expand(format, channel, targetDirectory, CloseableConsumer.CLOSING_CONSUMER);
    341            }
    342            return;
    343        }
    344        try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(archive))) {
    345            expand(format, inputStream, targetDirectory, CloseableConsumer.CLOSING_CONSUMER);
    346        }
    347    }
    348
    349    /**
    350     * Expands {@code archive} into {@code targetDirectory}.
    351     *
    352     * <p>This method creates a wrapper around the archive channel
    353     * which is never closed and thus leaks resources, please use
    354     * {@link #expand(String,SeekableByteChannel,File,CloseableConsumer)}
    355     * instead.</p>
    356     *
    357     * @param archive the file to expand
    358     * @param targetDirectory the target directory
    359     * @param format the archive format. This uses the same format as
    360     * accepted by {@link ArchiveStreamFactory}.
    361     * @throws IOException if an I/O error occurs
    362     * @throws ArchiveException if the archive cannot be read for other reasons
    363     * @deprecated this method leaks resources
    364     */
    365    @Deprecated
    366    public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory)
    367        throws IOException, ArchiveException {
    368        expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
    369    }
    370
    371    /**
    372     * Expands {@code archive} into {@code targetDirectory}.
    373     *
    374     * <p>This method creates a wrapper around the archive channel and
    375     * the caller of this method is responsible for closing it -
    376     * probably at the same time as closing the channel itself. The
    377     * caller is informed about the wrapper object via the {@code
    378     * closeableConsumer} callback as soon as it is no longer needed
    379     * by this class.</p>
    380     *
    381     * @param archive the file to expand
    382     * @param targetDirectory the target directory
    383     * @param format the archive format. This uses the same format as
    384     * accepted by {@link ArchiveStreamFactory}.
    385     * @param closeableConsumer is informed about the stream wrapped around the passed in channel
    386     * @throws IOException if an I/O error occurs
    387     * @throws ArchiveException if the archive cannot be read for other reasons
    388     * @since 1.19
    389     */
    390    public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory, final CloseableConsumer closeableConsumer)
    391        throws IOException, ArchiveException {
    392        expand(format, archive, toPath(targetDirectory), closeableConsumer);
    393    }
    394
    395    /**
    396     * Expands {@code archive} into {@code targetDirectory}.
    397     *
    398     * <p>This method creates a wrapper around the archive channel and
    399     * the caller of this method is responsible for closing it -
    400     * probably at the same time as closing the channel itself. The
    401     * caller is informed about the wrapper object via the {@code
    402     * closeableConsumer} callback as soon as it is no longer needed
    403     * by this class.</p>
    404     *
    405     * @param archive the file to expand
    406     * @param targetDirectory the target directory
    407     * @param format the archive format. This uses the same format as
    408     * accepted by {@link ArchiveStreamFactory}.
    409     * @param closeableConsumer is informed about the stream wrapped around the passed in channel
    410     * @throws IOException if an I/O error occurs
    411     * @throws ArchiveException if the archive cannot be read for other reasons
    412     * @since 1.22
    413     */
    414    public void expand(final String format, final SeekableByteChannel archive, final Path targetDirectory,
    415        final CloseableConsumer closeableConsumer)
    416        throws IOException, ArchiveException {
    417        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
    418        if (!prefersSeekableByteChannel(format)) {
    419            expand(format, c.track(Channels.newInputStream(archive)), targetDirectory, CloseableConsumer.NULL_CONSUMER);
    420        } else if (ArchiveStreamFactory.TAR.equalsIgnoreCase(format)) {
    421            expand(c.track(new TarFile(archive)), targetDirectory);
    422        } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
    423            expand(c.track(new ZipFile(archive)), targetDirectory);
    424        } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
    425            expand(c.track(new SevenZFile(archive)), targetDirectory);
    426        } else {
    427            // never reached as prefersSeekableByteChannel only returns true for TAR, ZIP and 7z
    428            throw new ArchiveException("Don't know how to handle format " + format);
    429        }
    430        }
    431    }
    432
    433    /**
    434     * Expands {@code archive} into {@code targetDirectory}.
    435     *
    436     * @param archive the file to expand
    437     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    438     * @throws IOException if an I/O error occurs
    439     * @since 1.21
    440     */
    441    public void expand(final TarFile archive, final File targetDirectory) throws IOException {
    442        expand(archive, toPath(targetDirectory));
    443    }
    444
    445    /**
    446     * Expands {@code archive} into {@code targetDirectory}.
    447     *
    448     * @param archive the file to expand
    449     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    450     * @throws IOException if an I/O error occurs
    451     * @since 1.22
    452     */
    453    public void expand(final TarFile archive, final Path targetDirectory)
    454        throws IOException {
    455        final Iterator<TarArchiveEntry> entryIterator = archive.getEntries().iterator();
    456        expand(() -> entryIterator.hasNext() ? entryIterator.next() : null,
    457            (entry, out) -> {
    458            try (InputStream in = archive.getInputStream(entry)) {
    459                IOUtils.copy(in, out);
    460            }
    461        }, targetDirectory);
    462    }
    463
    464    /**
    465     * Expands {@code archive} into {@code targetDirectory}.
    466     *
    467     * @param archive the file to expand
    468     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    469     * @throws IOException if an I/O error occurs
    470     */
    471    public void expand(final ZipFile archive, final File targetDirectory) throws IOException {
    472        expand(archive, toPath(targetDirectory));
    473    }
    474
    475    /**
    476     * Expands {@code archive} into {@code targetDirectory}.
    477     *
    478     * @param archive the file to expand
    479     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    480     * @throws IOException if an I/O error occurs
    481     * @since 1.22
    482     */
    483    public void expand(final ZipFile archive, final Path targetDirectory)
    484        throws IOException {
    485        final Enumeration<ZipArchiveEntry> entries = archive.getEntries();
    486        expand(() -> {
    487            ZipArchiveEntry next = entries.hasMoreElements() ? entries.nextElement() : null;
    488            while (next != null && !archive.canReadEntryData(next)) {
    489                next = entries.hasMoreElements() ? entries.nextElement() : null;
    490            }
    491            return next;
    492        }, (entry, out) -> {
    493            try (InputStream in = archive.getInputStream(entry)) {
    494                IOUtils.copy(in, out);
    495            }
    496        }, targetDirectory);
    497    }
    498
    499    private boolean prefersSeekableByteChannel(final String format) {
    500        return ArchiveStreamFactory.TAR.equalsIgnoreCase(format)
    501            || ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)
    502            || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format);
    503    }
    504
    505    private Path toPath(final File targetDirectory) {
    506        return targetDirectory != null ? targetDirectory.toPath() : null;
    507    }
    508
    509}