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}