View Javadoc
1   /**
2    *
3    * Copyright 2004 The Apache Software Foundation
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    * http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.codehaus.plexus.archiver;
18  
19  import java.io.File;
20  import java.io.FileNotFoundException;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.nio.file.Files;
24  import java.nio.file.Path;
25  import java.util.ArrayList;
26  import java.util.Date;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.concurrent.atomic.AtomicInteger;
30  
31  import org.codehaus.plexus.archiver.util.ArchiveEntryUtils;
32  import org.codehaus.plexus.components.io.attributes.SymlinkUtils;
33  import org.codehaus.plexus.components.io.filemappers.FileMapper;
34  import org.codehaus.plexus.components.io.fileselectors.FileSelector;
35  import org.codehaus.plexus.components.io.resources.PlexusIoResource;
36  import org.codehaus.plexus.util.FileUtils;
37  import org.codehaus.plexus.util.StringUtils;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  
41  import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
42  
43  // TODO there should really be constructors which take the source file.
44  
45  /**
46   * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
47   */
48  public abstract class AbstractUnArchiver implements UnArchiver, FinalizerEnabled {
49      private final Logger logger = LoggerFactory.getLogger(getClass());
50  
51      protected Logger getLogger() {
52          return logger;
53      }
54  
55      private File destDirectory;
56  
57      private File destFile;
58  
59      private File sourceFile;
60  
61      private boolean overwrite = true;
62  
63      private FileMapper[] fileMappers;
64  
65      private List<ArchiveFinalizer> finalizers;
66  
67      private FileSelector[] fileSelectors;
68  
69      /**
70       * since 2.3 is on by default
71       *
72       * @since 1.1
73       */
74      private boolean useJvmChmod = true;
75  
76      /**
77       * @since 1.1
78       */
79      private boolean ignorePermissions = false;
80  
81      public AbstractUnArchiver() {
82          // no op
83      }
84  
85      public AbstractUnArchiver(final File sourceFile) {
86          this.sourceFile = sourceFile;
87      }
88  
89      @Override
90      public File getDestDirectory() {
91          return destDirectory;
92      }
93  
94      @Override
95      public void setDestDirectory(final File destDirectory) {
96          this.destDirectory = destDirectory;
97      }
98  
99      @Override
100     public File getDestFile() {
101         return destFile;
102     }
103 
104     @Override
105     public void setDestFile(final File destFile) {
106         this.destFile = destFile;
107     }
108 
109     @Override
110     public File getSourceFile() {
111         return sourceFile;
112     }
113 
114     @Override
115     public void setSourceFile(final File sourceFile) {
116         this.sourceFile = sourceFile;
117     }
118 
119     @Override
120     public boolean isOverwrite() {
121         return overwrite;
122     }
123 
124     @Override
125     public void setOverwrite(final boolean b) {
126         overwrite = b;
127     }
128 
129     @Override
130     public FileMapper[] getFileMappers() {
131         return fileMappers;
132     }
133 
134     @Override
135     public void setFileMappers(final FileMapper[] fileMappers) {
136         this.fileMappers = fileMappers;
137     }
138 
139     @Override
140     public final void extract() throws ArchiverException {
141         validate();
142         execute();
143         runArchiveFinalizers();
144     }
145 
146     @Override
147     public final void extract(final String path, final File outputDirectory) throws ArchiverException {
148         validate(path, outputDirectory);
149         execute(path, outputDirectory);
150         runArchiveFinalizers();
151     }
152 
153     @Override
154     public void addArchiveFinalizer(final ArchiveFinalizer finalizer) {
155         if (finalizers == null) {
156             finalizers = new ArrayList<>();
157         }
158 
159         finalizers.add(finalizer);
160     }
161 
162     @Override
163     public void setArchiveFinalizers(final List<ArchiveFinalizer> archiveFinalizers) {
164         finalizers = archiveFinalizers;
165     }
166 
167     private void runArchiveFinalizers() throws ArchiverException {
168         if (finalizers != null) {
169             for (ArchiveFinalizer finalizer : finalizers) {
170                 finalizer.finalizeArchiveExtraction(this);
171             }
172         }
173     }
174 
175     protected void validate(final String path, final File outputDirectory) {}
176 
177     protected void validate() throws ArchiverException {
178         if (sourceFile == null) {
179             throw new ArchiverException("The source file isn't defined.");
180         }
181 
182         if (sourceFile.isDirectory()) {
183             throw new ArchiverException("The source must not be a directory.");
184         }
185 
186         if (!sourceFile.exists()) {
187             throw new ArchiverException("The source file " + sourceFile + " doesn't exist.");
188         }
189 
190         if (destDirectory == null && destFile == null) {
191             throw new ArchiverException("The destination isn't defined.");
192         }
193 
194         if (destDirectory != null && destFile != null) {
195             throw new ArchiverException("You must choose between a destination directory and a destination file.");
196         }
197 
198         if (destDirectory != null && !destDirectory.isDirectory()) {
199             destFile = destDirectory;
200             destDirectory = null;
201         }
202 
203         if (destFile != null && destFile.isDirectory()) {
204             destDirectory = destFile;
205             destFile = null;
206         }
207     }
208 
209     @Override
210     public void setFileSelectors(final FileSelector[] fileSelectors) {
211         this.fileSelectors = fileSelectors;
212     }
213 
214     @Override
215     public FileSelector[] getFileSelectors() {
216         return fileSelectors;
217     }
218 
219     protected boolean isSelected(final String fileName, final PlexusIoResource fileInfo) throws ArchiverException {
220         if (fileSelectors != null) {
221             for (FileSelector fileSelector : fileSelectors) {
222                 try {
223 
224                     if (!fileSelector.isSelected(fileInfo)) {
225                         return false;
226                     }
227                 } catch (final IOException e) {
228                     throw new ArchiverException(
229                             "Failed to check, whether " + fileInfo.getName() + " is selected: " + e.getMessage(), e);
230                 }
231             }
232         }
233         return true;
234     }
235 
236     protected abstract void execute() throws ArchiverException;
237 
238     protected abstract void execute(String path, File outputDirectory) throws ArchiverException;
239 
240     /**
241      * @since 1.1
242      */
243     @Override
244     @Deprecated
245     public boolean isUseJvmChmod() {
246         return useJvmChmod;
247     }
248 
249     /**
250      * <b>jvm chmod won't set group level permissions !</b>
251      *
252      * @since 1.1
253      */
254     @Override
255     @Deprecated
256     public void setUseJvmChmod(final boolean useJvmChmod) {
257         this.useJvmChmod = useJvmChmod;
258     }
259 
260     /**
261      * @since 1.1
262      */
263     @Override
264     public boolean isIgnorePermissions() {
265         return ignorePermissions;
266     }
267 
268     /**
269      * @since 1.1
270      */
271     @Override
272     public void setIgnorePermissions(final boolean ignorePermissions) {
273         this.ignorePermissions = ignorePermissions;
274     }
275 
276     protected void extractFile(
277             final File srcF,
278             final File dir,
279             final InputStream compressedInputStream,
280             String entryName,
281             final Date entryDate,
282             final boolean isDirectory,
283             final Integer mode,
284             String symlinkDestination,
285             final FileMapper[] fileMappers)
286             throws IOException, ArchiverException {
287         if (fileMappers != null) {
288             for (final FileMapper fileMapper : fileMappers) {
289                 entryName = fileMapper.getMappedFileName(entryName);
290             }
291         }
292 
293         // Hmm. Symlinks re-evaluate back to the original file here. Unsure if this is a good thing...
294         final File targetFileName = FileUtils.resolveFile(dir, entryName);
295 
296         // Make sure that the resolved path of the extracted file doesn't escape the destination directory
297         // getCanonicalFile().toPath() is used instead of getCanonicalPath() (returns String),
298         // because "/opt/directory".startsWith("/opt/dir") would return false negative.
299         Path canonicalDirPath = dir.getCanonicalFile().toPath();
300         Path canonicalDestPath = targetFileName.getCanonicalFile().toPath();
301 
302         if (!canonicalDestPath.startsWith(canonicalDirPath)) {
303             throw new ArchiverException("Entry is outside of the target directory (" + entryName + ")");
304         }
305 
306         // don't allow override target symlink by standard file
307         if (StringUtils.isEmpty(symlinkDestination) && Files.isSymbolicLink(canonicalDestPath)) {
308             throw new ArchiverException("Entry is outside of the target directory (" + entryName + ")");
309         }
310 
311         try {
312             if (!shouldExtractEntry(dir, targetFileName, entryName, entryDate)) {
313                 return;
314             }
315 
316             // create intermediary directories - sometimes zip don't add them
317             final File dirF = targetFileName.getParentFile();
318             if (dirF != null) {
319                 dirF.mkdirs();
320             }
321 
322             if (!StringUtils.isEmpty(symlinkDestination)) {
323                 SymlinkUtils.createSymbolicLink(targetFileName, new File(symlinkDestination));
324             } else if (isDirectory) {
325                 targetFileName.mkdirs();
326             } else {
327                 Files.copy(compressedInputStream, targetFileName.toPath(), REPLACE_EXISTING);
328             }
329 
330             targetFileName.setLastModified(entryDate.getTime());
331 
332             if (!isIgnorePermissions() && mode != null && !isDirectory) {
333                 ArchiveEntryUtils.chmod(targetFileName, mode);
334             }
335         } catch (final FileNotFoundException ex) {
336             getLogger().warn("Unable to expand to file " + targetFileName.getPath());
337         }
338     }
339 
340     /**
341      * Counter for casing message emitted, visible for testing.
342      */
343     final AtomicInteger casingMessageEmitted = new AtomicInteger(0);
344 
345     // Visible for testing
346     protected boolean shouldExtractEntry(File targetDirectory, File targetFileName, String entryName, Date entryDate)
347             throws IOException {
348         //     entryname  | entrydate | filename   | filedate | behavior
349         // (1) readme.txt | 1970      | -          | -        | always extract if the file does not exist
350         // (2) readme.txt | 1970      | readme.txt | 2020     | do not overwrite unless isOverwrite() is true
351         // (3) readme.txt | 2020      | readme.txt | 1970     | always override when the file is older than the archive
352         // entry
353         // (4) README.txt | 1970      | readme.txt | 2020     | case-insensitive filesystem: warn + do not overwrite
354         // unless isOverwrite()
355         //                                                      case-sensitive filesystem: extract without warning
356         // (5) README.txt | 2020      | readme.txt | 1970     | case-insensitive filesystem: warn + overwrite because
357         // entry is newer
358         //                                                      case-sensitive filesystem: extract without warning
359 
360         // The canonical file name follows the name of the archive entry, but takes into account the case-
361         // sensitivity of the filesystem. So on a case-sensitive file system, file.exists() returns false for
362         // scenario (4) and (5).
363         // No matter the case sensitivity of the file system, file.exists() returns false when there is no file with the
364         // same name (1).
365         if (!targetFileName.exists()) {
366             return true;
367         }
368 
369         boolean entryIsDirectory =
370                 entryName.endsWith("/"); // directory entries always end with '/', regardless of the OS.
371         String canonicalDestPath = targetFileName.getCanonicalPath();
372         String suffix = (entryIsDirectory ? "/" : "");
373         String relativeCanonicalDestPath =
374                 canonicalDestPath.replace(targetDirectory.getCanonicalPath() + File.separatorChar, "") + suffix;
375         boolean fileOnDiskIsOlderThanEntry = targetFileName.lastModified() < entryDate.getTime();
376         boolean differentCasing =
377                 !normalizedFileSeparator(entryName).equals(normalizedFileSeparator(relativeCanonicalDestPath));
378 
379         // Warn for case (4) and (5) if the file system is case-insensitive
380         if (differentCasing) {
381             String casingMessage = String.format(
382                     Locale.ENGLISH,
383                     "Archive entry '%s' and existing file '%s' names differ only by case."
384                             + " This may lead to an unexpected outcome on case-insensitive filesystems.",
385                     entryName,
386                     canonicalDestPath);
387             getLogger().warn(casingMessage);
388             casingMessageEmitted.incrementAndGet();
389         }
390 
391         // Override the existing file if isOverwrite() is true or if the file on disk is older than the one in the
392         // archive
393         return isOverwrite() || fileOnDiskIsOlderThanEntry;
394     }
395 
396     private String normalizedFileSeparator(String pathOrEntry) {
397         return pathOrEntry.replace("/", File.separator);
398     }
399 }