View Javadoc
1   /*
2    * This file is part of dependency-check-core.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *
16   * Copyright (c) 2013 Jeremy Long. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.utils;
19  
20  import java.io.BufferedInputStream;
21  import java.io.Closeable;
22  import java.io.File;
23  import java.io.FileInputStream;
24  import java.io.FileNotFoundException;
25  import java.io.FileOutputStream;
26  import java.io.FilenameFilter;
27  import java.io.IOException;
28  import java.util.zip.ZipEntry;
29  import java.util.zip.ZipInputStream;
30  
31  import org.apache.commons.compress.archivers.ArchiveEntry;
32  import org.apache.commons.compress.archivers.ArchiveInputStream;
33  import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
34  import org.apache.commons.compress.utils.IOUtils;
35  import org.owasp.dependencycheck.Engine;
36  import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
37  import org.owasp.dependencycheck.analyzer.exception.ArchiveExtractionException;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  
41  /**
42   * Set of utilities to extract files from archives.
43   *
44   * @author Jeremy Long
45   */
46  public final class ExtractionUtil {
47  
48      /**
49       * The logger.
50       */
51      private static final Logger LOGGER = LoggerFactory.getLogger(ExtractionUtil.class);
52  
53      /**
54       * Private constructor for a utility class.
55       */
56      private ExtractionUtil() {
57      }
58  
59      /**
60       * Extracts the contents of an archive into the specified directory.
61       *
62       * @param archive an archive file such as a WAR or EAR
63       * @param extractTo a directory to extract the contents to
64       * @throws ExtractionException thrown if an exception occurs while extracting the files
65       */
66      public static void extractFiles(File archive, File extractTo) throws ExtractionException {
67          extractFiles(archive, extractTo, null);
68      }
69  
70      /**
71       * Extracts the contents of an archive into the specified directory. The files are only extracted if they are supported by the
72       * analyzers loaded into the specified engine. If the engine is specified as null then all files are extracted.
73       *
74       * @param archive an archive file such as a WAR or EAR
75       * @param extractTo a directory to extract the contents to
76       * @param engine the scanning engine
77       * @throws ExtractionException thrown if there is an error extracting the files
78       */
79      public static void extractFiles(File archive, File extractTo, Engine engine) throws ExtractionException {
80          if (archive == null || extractTo == null) {
81              return;
82          }
83  
84          FileInputStream fis = null;
85          ZipInputStream zis = null;
86  
87          try {
88              fis = new FileInputStream(archive);
89          } catch (FileNotFoundException ex) {
90              LOGGER.debug("", ex);
91              throw new ExtractionException("Archive file was not found.", ex);
92          }
93          zis = new ZipInputStream(new BufferedInputStream(fis));
94          ZipEntry entry;
95          try {
96              while ((entry = zis.getNextEntry()) != null) {
97                  if (entry.isDirectory()) {
98                      final File d = new File(extractTo, entry.getName());
99                      if (!d.exists() && !d.mkdirs()) {
100                         final String msg = String.format("Unable to create '%s'.", d.getAbsolutePath());
101                         throw new ExtractionException(msg);
102                     }
103                 } else {
104                     final File file = new File(extractTo, entry.getName());
105                     if (engine == null || engine.accept(file)) {
106                         FileOutputStream fos = null;
107                         try {
108                             fos = new FileOutputStream(file);
109                             IOUtils.copy(zis, fos);
110                         } catch (FileNotFoundException ex) {
111                             LOGGER.debug("", ex);
112                             final String msg = String.format("Unable to find file '%s'.", file.getName());
113                             throw new ExtractionException(msg, ex);
114                         } catch (IOException ex) {
115                             LOGGER.debug("", ex);
116                             final String msg = String.format("IO Exception while parsing file '%s'.", file.getName());
117                             throw new ExtractionException(msg, ex);
118                         } finally {
119                             closeStream(fos);
120                         }
121                     }
122                 }
123             }
124         } catch (IOException ex) {
125             final String msg = String.format("Exception reading archive '%s'.", archive.getName());
126             LOGGER.debug("", ex);
127             throw new ExtractionException(msg, ex);
128         } finally {
129             closeStream(zis);
130         }
131     }
132 
133     /**
134      * Extracts the contents of an archive into the specified directory.
135      *
136      * @param archive an archive file such as a WAR or EAR
137      * @param destination a directory to extract the contents to
138      * @param filter determines which files get extracted
139      * @throws ExtractionException thrown if the archive is not found
140      */
141     public static void extractFilesUsingFilter(File archive, File destination,
142             FilenameFilter filter) throws ExtractionException {
143         if (archive == null || destination == null) {
144             return;
145         }
146 
147         FileInputStream fis = null;
148         try {
149             fis = new FileInputStream(archive);
150         } catch (FileNotFoundException ex) {
151             LOGGER.debug("", ex);
152             throw new ExtractionException("Archive file was not found.", ex);
153         }
154         try {
155             extractArchive(new ZipArchiveInputStream(new BufferedInputStream(
156                     fis)), destination, filter);
157         } catch (ArchiveExtractionException ex) {
158             LOGGER.warn("Exception extracting archive '{}'.", archive.getName());
159             LOGGER.debug("", ex);
160         } finally {
161             try {
162                 fis.close();
163             } catch (IOException ex) {
164                 LOGGER.debug("", ex);
165             }
166         }
167     }
168 
169     /**
170      * Extracts files from an archive.
171      *
172      * @param input the archive to extract files from
173      * @param destination the location to write the files too
174      * @param filter determines which files get extracted
175      * @throws ArchiveExtractionException thrown if there is an exception extracting files from the archive
176      */
177     private static void extractArchive(ArchiveInputStream input,
178             File destination, FilenameFilter filter)
179             throws ArchiveExtractionException {
180         ArchiveEntry entry;
181         try {
182             while ((entry = input.getNextEntry()) != null) {
183                 if (entry.isDirectory()) {
184                     final File dir = new File(destination, entry.getName());
185                     if (!dir.exists() && !dir.mkdirs()) {
186                         final String msg = String.format(
187                                 "Unable to create directory '%s'.",
188                                 dir.getAbsolutePath());
189                         throw new AnalysisException(msg);
190                     }
191                 } else {
192                     extractFile(input, destination, filter, entry);
193                 }
194             }
195         } catch (IOException ex) {
196             throw new ArchiveExtractionException(ex);
197         } catch (Throwable ex) {
198             throw new ArchiveExtractionException(ex);
199         } finally {
200             closeStream(input);
201         }
202     }
203 
204     /**
205      * Extracts a file from an archive (input stream) and correctly builds the directory structure.
206      *
207      * @param input the archive input stream
208      * @param destination where to write the file
209      * @param filter the file filter to apply to the files being extracted
210      * @param entry the entry from the archive to extract
211      * @throws ExtractionException thrown if there is an error reading from the archive stream
212      */
213     private static void extractFile(ArchiveInputStream input, File destination,
214             FilenameFilter filter, ArchiveEntry entry) throws ExtractionException {
215         final File file = new File(destination, entry.getName());
216         if (filter.accept(file.getParentFile(), file.getName())) {
217             LOGGER.debug("Extracting '{}'",
218                     file.getPath());
219             FileOutputStream fos = null;
220             try {
221                 createParentFile(file);
222                 fos = new FileOutputStream(file);
223                 IOUtils.copy(input, fos);
224             } catch (FileNotFoundException ex) {
225                 LOGGER.debug("", ex);
226                 final String msg = String.format("Unable to find file '%s'.",
227                         file.getName());
228                 throw new ExtractionException(msg, ex);
229             } catch (IOException ex) {
230                 LOGGER.debug("", ex);
231                 final String msg = String
232                         .format("IO Exception while parsing file '%s'.",
233                                 file.getName());
234                 throw new ExtractionException(msg, ex);
235             } finally {
236                 closeStream(fos);
237             }
238         }
239     }
240 
241     /**
242      * Closes the stream.
243      *
244      * @param stream the stream to close
245      */
246     private static void closeStream(Closeable stream) {
247         if (stream != null) {
248             try {
249                 stream.close();
250             } catch (IOException ex) {
251                 LOGGER.trace("", ex);
252             }
253         }
254     }
255 
256     /**
257      * Ensures the parent path is correctly created on disk so that the file can be extracted to the correct location.
258      *
259      * @param file the file path
260      * @throws ExtractionException thrown if the parent paths could not be created
261      */
262     private static void createParentFile(final File file)
263             throws ExtractionException {
264         final File parent = file.getParentFile();
265         if (!parent.isDirectory() && !parent.mkdirs()) {
266             final String msg = String.format(
267                     "Unable to build directory '%s'.",
268                     parent.getAbsolutePath());
269             throw new ExtractionException(msg);
270         }
271     }
272 }