    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 ArchiveEntryBiConsumer<T extends ArchiveEntry> {
    054        void accept(T entry, OutputStream out) throws IOException;
    055    }
    056
    057    @FunctionalInterface
    058    private interface ArchiveEntrySupplier<T extends ArchiveEntry> {
    059        T get() 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            expand(ArchiveStreamFactory.detect(inputStream), archive, targetDirectory);
    203        }
    204    }
    205
    206    /**
    207     * Expands {@code archive} into {@code targetDirectory}.
    208     *
    209     * @param archive the file to expand
    210     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    211     * @throws IOException if an I/O error occurs
    212     */
    213    public void expand(final SevenZFile archive, final File targetDirectory) throws IOException {
    214        expand(archive, toPath(targetDirectory));
    215    }
    216
    217    /**
    218     * Expands {@code archive} into {@code targetDirectory}.
    219     *
    220     * @param archive the file to expand
    221     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    222     * @throws IOException if an I/O error occurs
    223     * @since 1.22
    224     */
    225    public void expand(final SevenZFile archive, final Path targetDirectory)
    226        throws IOException {
    227        expand(archive::getNextEntry, (entry, out) -> {
    228            final byte[] buffer = new byte[8192];
    229            int n;
    230            while (-1 != (n = archive.read(buffer))) {
    231                if (out != null) {
    232                    out.write(buffer, 0, n);
    233                }
    234            }
    235        }, targetDirectory);
    236    }
    237
    238    /**
    239     * Expands {@code archive} into {@code targetDirectory}.
    240     *
    241     * @param archive the file to expand
    242     * @param targetDirectory the target directory
    243     * @param format the archive format. This uses the same format as
    244     * accepted by {@link ArchiveStreamFactory}.
    245     * @throws IOException if an I/O error occurs
    246     * @throws ArchiveException if the archive cannot be read for other reasons
    247     */
    248    public void expand(final String format, final File archive, final File targetDirectory) throws IOException, ArchiveException {
    249        expand(format, archive.toPath(), toPath(targetDirectory));
    250    }
    251
    252    /**
    253     * Expands {@code archive} into {@code targetDirectory}.
    254     *
    255     * <p>This method creates a wrapper around the archive stream
    256     * which is never closed and thus leaks resources, please use
    257     * {@link #expand(String,InputStream,File,CloseableConsumer)}
    258     * instead.</p>
    259     *
    260     * @param archive the file to expand
    261     * @param targetDirectory the target directory
    262     * @param format the archive format. This uses the same format as
    263     * accepted by {@link ArchiveStreamFactory}.
    264     * @throws IOException if an I/O error occurs
    265     * @throws ArchiveException if the archive cannot be read for other reasons
    266     * @deprecated this method leaks resources
    267     */
    268    @Deprecated
    269    public void expand(final String format, final InputStream archive, final File targetDirectory)
    270        throws IOException, ArchiveException {
    271        expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
    272    }
    273
    274    /**
    275     * Expands {@code archive} into {@code targetDirectory}.
    276     *
    277     * <p>This method creates a wrapper around the archive stream and
    278     * the caller of this method is responsible for closing it -
    279     * probably at the same time as closing the stream itself. The
    280     * caller is informed about the wrapper object via the {@code
    281     * closeableConsumer} callback as soon as it is no longer needed
    282     * by this class.</p>
    283     *
    284     * @param archive the file to expand
    285     * @param targetDirectory the target directory
    286     * @param format the archive format. This uses the same format as
    287     * accepted by {@link ArchiveStreamFactory}.
    288     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
    289     * @throws IOException if an I/O error occurs
    290     * @throws ArchiveException if the archive cannot be read for other reasons
    291     * @since 1.19
    292     */
    293    public void expand(final String format, final InputStream archive, final File targetDirectory, final CloseableConsumer closeableConsumer)
    294        throws IOException, ArchiveException {
    295        expand(format, archive, toPath(targetDirectory), closeableConsumer);
    296    }
    297
    298    /**
    299     * Expands {@code archive} into {@code targetDirectory}.
    300     *
    301     * <p>This method creates a wrapper around the archive stream and
    302     * the caller of this method is responsible for closing it -
    303     * probably at the same time as closing the stream itself. The
    304     * caller is informed about the wrapper object via the {@code
    305     * closeableConsumer} callback as soon as it is no longer needed
    306     * by this class.</p>
    307     *
    308     * @param archive the file to expand
    309     * @param targetDirectory the target directory
    310     * @param format the archive format. This uses the same format as
    311     * accepted by {@link ArchiveStreamFactory}.
    312     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
    313     * @throws IOException if an I/O error occurs
    314     * @throws ArchiveException if the archive cannot be read for other reasons
    315     * @since 1.22
    316     */
    317    public void expand(final String format, final InputStream archive, final Path targetDirectory, final CloseableConsumer closeableConsumer)
    318            throws IOException, ArchiveException {
    319        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
    320            ArchiveInputStream<?> archiveInputStream = ArchiveStreamFactory.DEFAULT.createArchiveInputStream(format, archive);
    321            expand(c.track(archiveInputStream), targetDirectory);
    322        }
    323    }
    324
    325    /**
    326     * Expands {@code archive} into {@code targetDirectory}.
    327     *
    328     * @param archive the file to expand
    329     * @param targetDirectory the target directory
    330     * @param format the archive format. This uses the same format as
    331     * accepted by {@link ArchiveStreamFactory}.
    332     * @throws IOException if an I/O error occurs
    333     * @throws ArchiveException if the archive cannot be read for other reasons
    334     * @since 1.22
    335     */
    336    public void expand(final String format, final Path archive, final Path targetDirectory) throws IOException, ArchiveException {
    337        if (prefersSeekableByteChannel(format)) {
    338            try (SeekableByteChannel channel = FileChannel.open(archive, StandardOpenOption.READ)) {
    339                expand(format, channel, targetDirectory, CloseableConsumer.CLOSING_CONSUMER);
    340            }
    341            return;
    342        }
    343        try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(archive))) {
    344            expand(format, inputStream, targetDirectory, CloseableConsumer.CLOSING_CONSUMER);
    345        }
    346    }
    347
    348    /**
    349     * Expands {@code archive} into {@code targetDirectory}.
    350     *
    351     * <p>This method creates a wrapper around the archive channel
    352     * which is never closed and thus leaks resources, please use
    353     * {@link #expand(String,SeekableByteChannel,File,CloseableConsumer)}
    354     * instead.</p>
    355     *
    356     * @param archive the file to expand
    357     * @param targetDirectory the target directory
    358     * @param format the archive format. This uses the same format as
    359     * accepted by {@link ArchiveStreamFactory}.
    360     * @throws IOException if an I/O error occurs
    361     * @throws ArchiveException if the archive cannot be read for other reasons
    362     * @deprecated this method leaks resources
    363     */
    364    @Deprecated
    365    public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory)
    366        throws IOException, ArchiveException {
    367        expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
    368    }
    369
    370    /**
    371     * Expands {@code archive} into {@code targetDirectory}.
    372     *
    373     * <p>This method creates a wrapper around the archive channel and
    374     * the caller of this method is responsible for closing it -
    375     * probably at the same time as closing the channel itself. The
    376     * caller is informed about the wrapper object via the {@code
    377     * closeableConsumer} callback as soon as it is no longer needed
    378     * by this class.</p>
    379     *
    380     * @param archive the file to expand
    381     * @param targetDirectory the target directory
    382     * @param format the archive format. This uses the same format as
    383     * accepted by {@link ArchiveStreamFactory}.
    384     * @param closeableConsumer is informed about the stream wrapped around the passed in channel
    385     * @throws IOException if an I/O error occurs
    386     * @throws ArchiveException if the archive cannot be read for other reasons
    387     * @since 1.19
    388     */
    389    public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory, final CloseableConsumer closeableConsumer)
    390        throws IOException, ArchiveException {
    391        expand(format, archive, toPath(targetDirectory), closeableConsumer);
    392    }
    393
    394    /**
    395     * Expands {@code archive} into {@code targetDirectory}.
    396     *
    397     * <p>This method creates a wrapper around the archive channel and
    398     * the caller of this method is responsible for closing it -
    399     * probably at the same time as closing the channel itself. The
    400     * caller is informed about the wrapper object via the {@code
    401     * closeableConsumer} callback as soon as it is no longer needed
    402     * by this class.</p>
    403     *
    404     * @param archive the file to expand
    405     * @param targetDirectory the target directory
    406     * @param format the archive format. This uses the same format as
    407     * accepted by {@link ArchiveStreamFactory}.
    408     * @param closeableConsumer is informed about the stream wrapped around the passed in channel
    409     * @throws IOException if an I/O error occurs
    410     * @throws ArchiveException if the archive cannot be read for other reasons
    411     * @since 1.22
    412     */
    413    public void expand(final String format, final SeekableByteChannel archive, final Path targetDirectory,
    414        final CloseableConsumer closeableConsumer)
    415        throws IOException, ArchiveException {
    416        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
    417        if (!prefersSeekableByteChannel(format)) {
    418            expand(format, c.track(Channels.newInputStream(archive)), targetDirectory, CloseableConsumer.NULL_CONSUMER);
    419        } else if (ArchiveStreamFactory.TAR.equalsIgnoreCase(format)) {
    420            expand(c.track(new TarFile(archive)), targetDirectory);
    421        } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
    422            expand(c.track(new ZipFile(archive)), targetDirectory);
    423        } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
    424            expand(c.track(new SevenZFile(archive)), targetDirectory);
    425        } else {
    426            // never reached as prefersSeekableByteChannel only returns true for TAR, ZIP and 7z
    427            throw new ArchiveException("Don't know how to handle format " + format);
    428        }
    429        }
    430    }
    431
    432    /**
    433     * Expands {@code archive} into {@code targetDirectory}.
    434     *
    435     * @param archive the file to expand
    436     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    437     * @throws IOException if an I/O error occurs
    438     * @since 1.21
    439     */
    440    public void expand(final TarFile archive, final File targetDirectory) throws IOException {
    441        expand(archive, toPath(targetDirectory));
    442    }
    443
    444    /**
    445     * Expands {@code archive} into {@code targetDirectory}.
    446     *
    447     * @param archive the file to expand
    448     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    449     * @throws IOException if an I/O error occurs
    450     * @since 1.22
    451     */
    452    public void expand(final TarFile archive, final Path targetDirectory)
    453        throws IOException {
    454        final Iterator<TarArchiveEntry> entryIterator = archive.getEntries().iterator();
    455        expand(() -> entryIterator.hasNext() ? entryIterator.next() : null,
    456            (entry, out) -> {
    457            try (InputStream in = archive.getInputStream(entry)) {
    458                IOUtils.copy(in, out);
    459            }
    460        }, targetDirectory);
    461    }
    462
    463    /**
    464     * Expands {@code archive} into {@code targetDirectory}.
    465     *
    466     * @param archive the file to expand
    467     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    468     * @throws IOException if an I/O error occurs
    469     */
    470    public void expand(final ZipFile archive, final File targetDirectory) throws IOException {
    471        expand(archive, toPath(targetDirectory));
    472    }
    473
    474    /**
    475     * Expands {@code archive} into {@code targetDirectory}.
    476     *
    477     * @param archive the file to expand
    478     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    479     * @throws IOException if an I/O error occurs
    480     * @since 1.22
    481     */
    482    public void expand(final ZipFile archive, final Path targetDirectory)
    483        throws IOException {
    484        final Enumeration<ZipArchiveEntry> entries = archive.getEntries();
    485        expand(() -> {
    486            ZipArchiveEntry next = entries.hasMoreElements() ? entries.nextElement() : null;
    487            while (next != null && !archive.canReadEntryData(next)) {
    488                next = entries.hasMoreElements() ? entries.nextElement() : null;
    489            }
    490            return next;
    491        }, (entry, out) -> {
    492            try (InputStream in = archive.getInputStream(entry)) {
    493                IOUtils.copy(in, out);
    494            }
    495        }, targetDirectory);
    496    }
    497
    498    private boolean prefersSeekableByteChannel(final String format) {
    499        return ArchiveStreamFactory.TAR.equalsIgnoreCase(format)
    500            || ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)
    501            || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format);
    502    }
    503
    504    private Path toPath(final File targetDirectory) {
    505        return targetDirectory != null ? targetDirectory.toPath() : null;
    506    }
    507
    508}