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);
088            }
089            return;
090        }
091        try (InputStream i = new BufferedInputStream(Files.newInputStream(archive.toPath()))) {
092            expand(format, i, targetDirectory);
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     * @param archive the file to expand
102     * @param targetDirectory the directory to write to
103     * @throws IOException if an I/O error occurs
104     * @throws ArchiveException if the archive cannot be read for other reasons
105     */
106    public void expand(InputStream archive, File targetDirectory) throws IOException, ArchiveException {
107        expand(new ArchiveStreamFactory().createArchiveInputStream(archive), targetDirectory);
108    }
109
110    /**
111     * Expands {@code archive} into {@code targetDirectory}.
112     *
113     * @param archive the file to expand
114     * @param targetDirectory the directory to write to
115     * @param format the archive format. This uses the same format as
116     * accepted by {@link ArchiveStreamFactory}.
117     * @throws IOException if an I/O error occurs
118     * @throws ArchiveException if the archive cannot be read for other reasons
119     */
120    public void expand(String format, InputStream archive, File targetDirectory)
121        throws IOException, ArchiveException {
122        expand(new ArchiveStreamFactory().createArchiveInputStream(format, archive), targetDirectory);
123    }
124
125    /**
126     * Expands {@code archive} into {@code targetDirectory}.
127     *
128     * @param archive the file to expand
129     * @param targetDirectory the directory to write to
130     * @param format the archive format. This uses the same format as
131     * accepted by {@link ArchiveStreamFactory}.
132     * @throws IOException if an I/O error occurs
133     * @throws ArchiveException if the archive cannot be read for other reasons
134     */
135    public void expand(String format, SeekableByteChannel archive, File targetDirectory)
136        throws IOException, ArchiveException {
137        if (!prefersSeekableByteChannel(format)) {
138            expand(format, Channels.newInputStream(archive), targetDirectory);
139        } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
140            expand(new ZipFile(archive), targetDirectory);
141        } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
142            expand(new SevenZFile(archive), targetDirectory);
143        } else {
144            // never reached as prefersSeekableByteChannel only returns true for ZIP and 7z
145            throw new ArchiveException("don't know how to handle format " + format);
146        }
147    }
148
149    /**
150     * Expands {@code archive} into {@code targetDirectory}.
151     *
152     * @param archive the file to expand
153     * @param targetDirectory the directory to write to
154     * @throws IOException if an I/O error occurs
155     * @throws ArchiveException if the archive cannot be read for other reasons
156     */
157    public void expand(final ArchiveInputStream archive, File targetDirectory)
158        throws IOException, ArchiveException {
159        expand(new ArchiveEntrySupplier() {
160            @Override
161            public ArchiveEntry getNextReadableEntry() throws IOException {
162                ArchiveEntry next = archive.getNextEntry();
163                while (next != null && !archive.canReadEntryData(next)) {
164                    next = archive.getNextEntry();
165                }
166                return next;
167            }
168        }, new EntryWriter() {
169            @Override
170            public void writeEntryDataTo(ArchiveEntry entry, OutputStream out) throws IOException {
171                IOUtils.copy(archive, out);
172            }
173        }, targetDirectory);
174    }
175
176    /**
177     * Expands {@code archive} into {@code targetDirectory}.
178     *
179     * @param archive the file to expand
180     * @param targetDirectory the directory to write to
181     * @throws IOException if an I/O error occurs
182     * @throws ArchiveException if the archive cannot be read for other reasons
183     */
184    public void expand(final ZipFile archive, File targetDirectory)
185        throws IOException, ArchiveException {
186        final Enumeration<ZipArchiveEntry> entries = archive.getEntries();
187        expand(new ArchiveEntrySupplier() {
188            @Override
189            public ArchiveEntry getNextReadableEntry() throws IOException {
190                ZipArchiveEntry next = entries.hasMoreElements() ? entries.nextElement() : null;
191                while (next != null && !archive.canReadEntryData(next)) {
192                    next = entries.hasMoreElements() ? entries.nextElement() : null;
193                }
194                return next;
195            }
196        }, new EntryWriter() {
197            @Override
198            public void writeEntryDataTo(ArchiveEntry entry, OutputStream out) throws IOException {
199                try (InputStream in = archive.getInputStream((ZipArchiveEntry) entry)) {
200                    IOUtils.copy(in, out);
201                }
202            }
203        }, targetDirectory);
204    }
205
206    /**
207     * Expands {@code archive} into {@code targetDirectory}.
208     *
209     * @param archive the file to expand
210     * @param targetDirectory the directory to write to
211     * @throws IOException if an I/O error occurs
212     * @throws ArchiveException if the archive cannot be read for other reasons
213     */
214    public void expand(final SevenZFile archive, File targetDirectory)
215        throws IOException, ArchiveException {
216        expand(new ArchiveEntrySupplier() {
217            @Override
218            public ArchiveEntry getNextReadableEntry() throws IOException {
219                return archive.getNextEntry();
220            }
221        }, new EntryWriter() {
222            @Override
223            public void writeEntryDataTo(ArchiveEntry entry, OutputStream out) throws IOException {
224                final byte[] buffer = new byte[8024];
225                int n = 0;
226                long count = 0;
227                while (-1 != (n = archive.read(buffer))) {
228                    out.write(buffer, 0, n);
229                    count += n;
230                }
231            }
232        }, targetDirectory);
233    }
234
235    private boolean prefersSeekableByteChannel(String format) {
236        return ArchiveStreamFactory.ZIP.equalsIgnoreCase(format) || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format);
237    }
238
239    private void expand(ArchiveEntrySupplier supplier, EntryWriter writer, File targetDirectory)
240        throws IOException {
241        String targetDirPath = targetDirectory.getCanonicalPath();
242        if (!targetDirPath.endsWith(File.separator)) {
243            targetDirPath += File.separatorChar;
244        }
245        ArchiveEntry nextEntry = supplier.getNextReadableEntry();
246        while (nextEntry != null) {
247            File f = new File(targetDirectory, nextEntry.getName());
248            if (!f.getCanonicalPath().startsWith(targetDirPath)) {
249                throw new IOException("expanding " + nextEntry.getName()
250                    + " would create file outside of " + targetDirectory);
251            }
252            if (nextEntry.isDirectory()) {
253                if (!f.isDirectory() && !f.mkdirs()) {
254                    throw new IOException("failed to create directory " + f);
255                }
256            } else {
257                File parent = f.getParentFile();
258                if (!parent.isDirectory() && !parent.mkdirs()) {
259                    throw new IOException("failed to create directory " + parent);
260                }
261                try (OutputStream o = Files.newOutputStream(f.toPath())) {
262                    writer.writeEntryDataTo(nextEntry, o);
263                }
264            }
265            nextEntry = supplier.getNextReadableEntry();
266        }
267    }
268
269}