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}