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     public boolean isUseJvmChmod() {
245         return useJvmChmod;
246     }
247 
248     /**
249      * <b>jvm chmod won't set group level permissions !</b>
250      *
251      * @since 1.1
252      */
253     @Override
254     public void setUseJvmChmod(final boolean useJvmChmod) {
255         this.useJvmChmod = useJvmChmod;
256     }
257 
258     /**
259      * @since 1.1
260      */
261     @Override
262     public boolean isIgnorePermissions() {
263         return ignorePermissions;
264     }
265 
266     /**
267      * @since 1.1
268      */
269     @Override
270     public void setIgnorePermissions(final boolean ignorePermissions) {
271         this.ignorePermissions = ignorePermissions;
272     }
273 
274     protected void extractFile(
275             final File srcF,
276             final File dir,
277             final InputStream compressedInputStream,
278             String entryName,
279             final Date entryDate,
280             final boolean isDirectory,
281             final Integer mode,
282             String symlinkDestination,
283             final FileMapper[] fileMappers)
284             throws IOException, ArchiverException {
285         if (fileMappers != null) {
286             for (final FileMapper fileMapper : fileMappers) {
287                 entryName = fileMapper.getMappedFileName(entryName);
288             }
289         }
290 
291         // Hmm. Symlinks re-evaluate back to the original file here. Unsure if this is a good thing...
292         final File targetFileName = FileUtils.resolveFile(dir, entryName);
293 
294         // Make sure that the resolved path of the extracted file doesn't escape the destination directory
295         // getCanonicalFile().toPath() is used instead of getCanonicalPath() (returns String),
296         // because "/opt/directory".startsWith("/opt/dir") would return false negative.
297         Path canonicalDirPath = dir.getCanonicalFile().toPath();
298         Path canonicalDestPath = targetFileName.getCanonicalFile().toPath();
299 
300         if (!canonicalDestPath.startsWith(canonicalDirPath)) {
301             throw new ArchiverException("Entry is outside of the target directory (" + entryName + ")");
302         }
303 
304         // don't allow override target symlink by standard file
305         if (StringUtils.isEmpty(symlinkDestination) && Files.isSymbolicLink(canonicalDestPath)) {
306             throw new ArchiverException("Entry is outside of the target directory (" + entryName + ")");
307         }
308 
309         try {
310             if (!shouldExtractEntry(dir, targetFileName, entryName, entryDate)) {
311                 return;
312             }
313 
314             // create intermediary directories - sometimes zip don't add them
315             final File dirF = targetFileName.getParentFile();
316             if (dirF != null) {
317                 dirF.mkdirs();
318             }
319 
320             if (!StringUtils.isEmpty(symlinkDestination)) {
321                 SymlinkUtils.createSymbolicLink(targetFileName, new File(symlinkDestination));
322             } else if (isDirectory) {
323                 targetFileName.mkdirs();
324             } else {
325                 Files.copy(compressedInputStream, targetFileName.toPath(), REPLACE_EXISTING);
326             }
327 
328             targetFileName.setLastModified(entryDate.getTime());
329 
330             if (!isIgnorePermissions() && mode != null && !isDirectory) {
331                 ArchiveEntryUtils.chmod(targetFileName, mode);
332             }
333         } catch (final FileNotFoundException ex) {
334             getLogger().warn("Unable to expand to file " + targetFileName.getPath());
335         }
336     }
337 
338     /**
339      * Counter for casing message emitted, visible for testing.
340      */
341     final AtomicInteger casingMessageEmitted = new AtomicInteger(0);
342 
343     // Visible for testing
344     protected boolean shouldExtractEntry(File targetDirectory, File targetFileName, String entryName, Date entryDate)
345             throws IOException {
346         //     entryname  | entrydate | filename   | filedate | behavior
347         // (1) readme.txt | 1970      | -          | -        | always extract if the file does not exist
348         // (2) readme.txt | 1970      | readme.txt | 2020     | do not overwrite unless isOverwrite() is true
349         // (3) readme.txt | 2020      | readme.txt | 1970     | always override when the file is older than the archive
350         // entry
351         // (4) README.txt | 1970      | readme.txt | 2020     | case-insensitive filesystem: warn + do not overwrite
352         // unless isOverwrite()
353         //                                                      case-sensitive filesystem: extract without warning
354         // (5) README.txt | 2020      | readme.txt | 1970     | case-insensitive filesystem: warn + overwrite because
355         // entry is newer
356         //                                                      case-sensitive filesystem: extract without warning
357 
358         // The canonical file name follows the name of the archive entry, but takes into account the case-
359         // sensitivity of the filesystem. So on a case-sensitive file system, file.exists() returns false for
360         // scenario (4) and (5).
361         // No matter the case sensitivity of the file system, file.exists() returns false when there is no file with the
362         // same name (1).
363         if (!targetFileName.exists()) {
364             return true;
365         }
366 
367         boolean entryIsDirectory =
368                 entryName.endsWith("/"); // directory entries always end with '/', regardless of the OS.
369         String canonicalDestPath = targetFileName.getCanonicalPath();
370         String suffix = (entryIsDirectory ? "/" : "");
371         String relativeCanonicalDestPath =
372                 canonicalDestPath.replace(targetDirectory.getCanonicalPath() + File.separatorChar, "") + suffix;
373         boolean fileOnDiskIsOlderThanEntry = targetFileName.lastModified() < entryDate.getTime();
374         boolean differentCasing =
375                 !normalizedFileSeparator(entryName).equals(normalizedFileSeparator(relativeCanonicalDestPath));
376 
377         // Warn for case (4) and (5) if the file system is case-insensitive
378         if (differentCasing) {
379             String casingMessage = String.format(
380                     Locale.ENGLISH,
381                     "Archive entry '%s' and existing file '%s' names differ only by case."
382                             + " This may lead to an unexpected outcome on case-insensitive filesystems.",
383                     entryName,
384                     canonicalDestPath);
385             getLogger().warn(casingMessage);
386             casingMessageEmitted.incrementAndGet();
387         }
388 
389         // Override the existing file if isOverwrite() is true or if the file on disk is older than the one in the
390         // archive
391         return isOverwrite() || fileOnDiskIsOlderThanEntry;
392     }
393 
394     private String normalizedFileSeparator(String pathOrEntry) {
395         return pathOrEntry.replace("/", File.separator);
396     }
397 }