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