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.StandardOpenOption;
031import java.util.Enumeration;
032import java.util.Iterator;
033
034import org.apache.commons.compress.archivers.ArchiveEntry;
035import org.apache.commons.compress.archivers.ArchiveException;
036import org.apache.commons.compress.archivers.ArchiveInputStream;
037import org.apache.commons.compress.archivers.ArchiveStreamFactory;
038import org.apache.commons.compress.archivers.sevenz.SevenZFile;
039import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
040import org.apache.commons.compress.archivers.tar.TarFile;
041import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
042import org.apache.commons.compress.archivers.zip.ZipFile;
043import org.apache.commons.compress.utils.IOUtils;
044
045/**
046 * Provides a high level API for expanding archives.
047 * @since 1.17
048 */
049public class Expander {
050
051    private interface ArchiveEntrySupplier {
052        ArchiveEntry getNextReadableEntry() throws IOException;
053    }
054
055    private interface EntryWriter {
056        void writeEntryDataTo(ArchiveEntry entry, OutputStream out) throws IOException;
057    }
058
059    /**
060     * Expands {@code archive} into {@code targetDirectory}.
061     *
062     * <p>Tries to auto-detect the archive's format.</p>
063     *
064     * @param archive the file to expand
065     * @param targetDirectory the directory to write to
066     * @throws IOException if an I/O error occurs
067     * @throws ArchiveException if the archive cannot be read for other reasons
068     */
069    public void expand(final File archive, final File targetDirectory) throws IOException, ArchiveException {
070        String format = null;
071        try (InputStream i = new BufferedInputStream(Files.newInputStream(archive.toPath()))) {
072            format = ArchiveStreamFactory.detect(i);
073        }
074        expand(format, archive, targetDirectory);
075    }
076
077    /**
078     * Expands {@code archive} into {@code targetDirectory}.
079     *
080     * @param archive the file to expand
081     * @param targetDirectory the directory to write to
082     * @param format the archive format. This uses the same format as
083     * accepted by {@link ArchiveStreamFactory}.
084     * @throws IOException if an I/O error occurs
085     * @throws ArchiveException if the archive cannot be read for other reasons
086     */
087    public void expand(final String format, final File archive, final File targetDirectory) throws IOException, ArchiveException {
088        if (prefersSeekableByteChannel(format)) {
089            try (SeekableByteChannel c = FileChannel.open(archive.toPath(), StandardOpenOption.READ)) {
090                expand(format, c, targetDirectory, CloseableConsumer.CLOSING_CONSUMER);
091            }
092            return;
093        }
094        try (InputStream i = new BufferedInputStream(Files.newInputStream(archive.toPath()))) {
095            expand(format, i, targetDirectory, CloseableConsumer.CLOSING_CONSUMER);
096        }
097    }
098
099    /**
100     * Expands {@code archive} into {@code targetDirectory}.
101     *
102     * <p>Tries to auto-detect the archive's format.</p>
103     *
104     * <p>This method creates a wrapper around the archive stream
105     * which is never closed and thus leaks resources, please use
106     * {@link #expand(InputStream,File,CloseableConsumer)}
107     * instead.</p>
108     *
109     * @param archive the file to expand
110     * @param targetDirectory the directory to write to
111     * @throws IOException if an I/O error occurs
112     * @throws ArchiveException if the archive cannot be read for other reasons
113     * @deprecated this method leaks resources
114     */
115    @Deprecated
116    public void expand(final InputStream archive, final File targetDirectory) throws IOException, ArchiveException {
117        expand(archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
118    }
119
120    /**
121     * Expands {@code archive} into {@code targetDirectory}.
122     *
123     * <p>Tries to auto-detect the archive's format.</p>
124     *
125     * <p>This method creates a wrapper around the archive stream and
126     * the caller of this method is responsible for closing it -
127     * probably at the same time as closing the stream itself. The
128     * caller is informed about the wrapper object via the {@code
129     * closeableConsumer} callback as soon as it is no longer needed
130     * by this class.</p>
131     *
132     * @param archive the file to expand
133     * @param targetDirectory the directory to write to
134     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
135     * @throws IOException if an I/O error occurs
136     * @throws ArchiveException if the archive cannot be read for other reasons
137     * @since 1.19
138     */
139    public void expand(final InputStream archive, final File targetDirectory, final CloseableConsumer closeableConsumer)
140        throws IOException, ArchiveException {
141        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
142            expand(c.track(ArchiveStreamFactory.DEFAULT.createArchiveInputStream(archive)),
143                targetDirectory);
144        }
145    }
146
147    /**
148     * Expands {@code archive} into {@code targetDirectory}.
149     *
150     * <p>This method creates a wrapper around the archive stream
151     * which is never closed and thus leaks resources, please use
152     * {@link #expand(String,InputStream,File,CloseableConsumer)}
153     * instead.</p>
154     *
155     * @param archive the file to expand
156     * @param targetDirectory the directory to write to
157     * @param format the archive format. This uses the same format as
158     * accepted by {@link ArchiveStreamFactory}.
159     * @throws IOException if an I/O error occurs
160     * @throws ArchiveException if the archive cannot be read for other reasons
161     * @deprecated this method leaks resources
162     */
163    @Deprecated
164    public void expand(final String format, final InputStream archive, final File targetDirectory)
165        throws IOException, ArchiveException {
166        expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
167    }
168
169    /**
170     * Expands {@code archive} into {@code targetDirectory}.
171     *
172     * <p>This method creates a wrapper around the archive stream and
173     * the caller of this method is responsible for closing it -
174     * probably at the same time as closing the stream itself. The
175     * caller is informed about the wrapper object via the {@code
176     * closeableConsumer} callback as soon as it is no longer needed
177     * by this class.</p>
178     *
179     * @param archive the file to expand
180     * @param targetDirectory the directory to write to
181     * @param format the archive format. This uses the same format as
182     * accepted by {@link ArchiveStreamFactory}.
183     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
184     * @throws IOException if an I/O error occurs
185     * @throws ArchiveException if the archive cannot be read for other reasons
186     * @since 1.19
187     */
188    public void expand(final String format, final InputStream archive, final File targetDirectory, final CloseableConsumer closeableConsumer)
189        throws IOException, ArchiveException {
190        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
191            expand(c.track(ArchiveStreamFactory.DEFAULT.createArchiveInputStream(format, archive)),
192                targetDirectory);
193        }
194    }
195
196    /**
197     * Expands {@code archive} into {@code targetDirectory}.
198     *
199     * <p>This method creates a wrapper around the archive channel
200     * which is never closed and thus leaks resources, please use
201     * {@link #expand(String,SeekableByteChannel,File,CloseableConsumer)}
202     * instead.</p>
203     *
204     * @param archive the file to expand
205     * @param targetDirectory the directory to write to
206     * @param format the archive format. This uses the same format as
207     * accepted by {@link ArchiveStreamFactory}.
208     * @throws IOException if an I/O error occurs
209     * @throws ArchiveException if the archive cannot be read for other reasons
210     * @deprecated this method leaks resources
211     */
212    @Deprecated
213    public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory)
214        throws IOException, ArchiveException {
215        expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
216    }
217
218    /**
219     * Expands {@code archive} into {@code targetDirectory}.
220     *
221     * <p>This method creates a wrapper around the archive channel and
222     * the caller of this method is responsible for closing it -
223     * probably at the same time as closing the channel itself. The
224     * caller is informed about the wrapper object via the {@code
225     * closeableConsumer} callback as soon as it is no longer needed
226     * by this class.</p>
227     *
228     * @param archive the file to expand
229     * @param targetDirectory the directory to write to
230     * @param format the archive format. This uses the same format as
231     * accepted by {@link ArchiveStreamFactory}.
232     * @param closeableConsumer is informed about the stream wrapped around the passed in channel
233     * @throws IOException if an I/O error occurs
234     * @throws ArchiveException if the archive cannot be read for other reasons
235     * @since 1.19
236     */
237    public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory,
238        final CloseableConsumer closeableConsumer)
239        throws IOException, ArchiveException {
240        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
241        if (!prefersSeekableByteChannel(format)) {
242            expand(format, c.track(Channels.newInputStream(archive)), targetDirectory);
243        } else if (ArchiveStreamFactory.TAR.equalsIgnoreCase(format)) {
244            expand(c.track(new TarFile(archive)), targetDirectory);
245        } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
246            expand(c.track(new ZipFile(archive)), targetDirectory);
247        } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
248            expand(c.track(new SevenZFile(archive)), targetDirectory);
249        } else {
250            // never reached as prefersSeekableByteChannel only returns true for TAR, ZIP and 7z
251            throw new ArchiveException("Don't know how to handle format " + format);
252        }
253        }
254    }
255
256    /**
257     * Expands {@code archive} into {@code targetDirectory}.
258     *
259     * @param archive the file to expand
260     * @param targetDirectory the directory to write to
261     * @throws IOException if an I/O error occurs
262     * @throws ArchiveException if the archive cannot be read for other reasons
263     */
264    public void expand(final ArchiveInputStream archive, final File targetDirectory)
265        throws IOException, ArchiveException {
266        expand(() -> {
267            ArchiveEntry next = archive.getNextEntry();
268            while (next != null && !archive.canReadEntryData(next)) {
269                next = archive.getNextEntry();
270            }
271            return next;
272        }, (entry, out) -> IOUtils.copy(archive, out), targetDirectory);
273    }
274
275    /**
276     * Expands {@code archive} into {@code targetDirectory}.
277     *
278     * @param archive the file to expand
279     * @param targetDirectory the directory to write to
280     * @throws IOException if an I/O error occurs
281     * @throws ArchiveException if the archive cannot be read for other reasons
282     * @since 1.21
283     */
284    public void expand(final TarFile archive, final File targetDirectory)
285        throws IOException, ArchiveException {
286        final Iterator<TarArchiveEntry> entryIterator = archive.getEntries().iterator();
287        expand(() -> entryIterator.hasNext() ? entryIterator.next() : null,
288            (entry, out) -> {
289            try (InputStream in = archive.getInputStream((TarArchiveEntry) entry)) {
290                IOUtils.copy(in, out);
291            }
292        }, targetDirectory);
293    }
294
295    /**
296     * Expands {@code archive} into {@code targetDirectory}.
297     *
298     * @param archive the file to expand
299     * @param targetDirectory the directory to write to
300     * @throws IOException if an I/O error occurs
301     * @throws ArchiveException if the archive cannot be read for other reasons
302     */
303    public void expand(final ZipFile archive, final File targetDirectory)
304        throws IOException, ArchiveException {
305        final Enumeration<ZipArchiveEntry> entries = archive.getEntries();
306        expand(() -> {
307            ZipArchiveEntry next = entries.hasMoreElements() ? entries.nextElement() : null;
308            while (next != null && !archive.canReadEntryData(next)) {
309                next = entries.hasMoreElements() ? entries.nextElement() : null;
310            }
311            return next;
312        }, (entry, out) -> {
313            try (InputStream in = archive.getInputStream((ZipArchiveEntry) entry)) {
314                IOUtils.copy(in, out);
315            }
316        }, targetDirectory);
317    }
318
319    /**
320     * Expands {@code archive} into {@code targetDirectory}.
321     *
322     * @param archive the file to expand
323     * @param targetDirectory the directory to write to
324     * @throws IOException if an I/O error occurs
325     * @throws ArchiveException if the archive cannot be read for other reasons
326     */
327    public void expand(final SevenZFile archive, final File targetDirectory)
328        throws IOException, ArchiveException {
329        expand(archive::getNextEntry, (entry, out) -> {
330            final byte[] buffer = new byte[8192];
331            int n;
332            while (-1 != (n = archive.read(buffer))) {
333                out.write(buffer, 0, n);
334            }
335        }, targetDirectory);
336    }
337
338    private boolean prefersSeekableByteChannel(final String format) {
339        return ArchiveStreamFactory.TAR.equalsIgnoreCase(format)
340            || ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)
341            || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format);
342    }
343
344    private void expand(final ArchiveEntrySupplier supplier, final EntryWriter writer, final File targetDirectory)
345        throws IOException {
346        String targetDirPath = targetDirectory.getCanonicalPath();
347        if (!targetDirPath.endsWith(File.separator)) {
348            targetDirPath += File.separator;
349        }
350        ArchiveEntry nextEntry = supplier.getNextReadableEntry();
351        while (nextEntry != null) {
352            final File f = new File(targetDirectory, nextEntry.getName());
353            if (!f.getCanonicalPath().startsWith(targetDirPath)) {
354                throw new IOException("Expanding " + nextEntry.getName()
355                    + " would create file outside of " + targetDirectory);
356            }
357            if (nextEntry.isDirectory()) {
358                if (!f.isDirectory() && !f.mkdirs()) {
359                    throw new IOException("Failed to create directory " + f);
360                }
361            } else {
362                final File parent = f.getParentFile();
363                if (!parent.isDirectory() && !parent.mkdirs()) {
364                    throw new IOException("Failed to create directory " + parent);
365                }
366                try (OutputStream o = Files.newOutputStream(f.toPath())) {
367                    writer.writeEntryDataTo(nextEntry, o);
368                }
369            }
370            nextEntry = supplier.getNextReadableEntry();
371        }
372    }
373
374}