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