1 /*
2  * This file is part of Arduino.
3  *
4  * Copyright 2014 Arduino LLC (http://www.arduino.cc/)
5  *
6  * Arduino is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19  *
20  * As a special exception, you may use this file as part of a free software
21  * library without restriction.  Specifically, if other files instantiate
22  * templates or use macros or inline functions from this file, or you compile
23  * this file and link it with other files to produce an executable, this
24  * file does not by itself cause the resulting executable to be covered by
25  * the GNU General Public License.  This exception does not however
26  * invalidate any other reasons why the executable file might be covered by
27  * the GNU General Public License.
28  */
29 
30 package cc.arduino.utils;
31 
32 import org.apache.commons.compress.archivers.ArchiveEntry;
33 import org.apache.commons.compress.archivers.ArchiveInputStream;
34 import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
35 import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
36 import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
37 import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
38 import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
39 import org.apache.commons.compress.utils.IOUtils;
40 import processing.app.I18n;
41 import processing.app.Platform;
42 
43 import java.io.*;
44 import java.util.HashMap;
45 import java.util.Map;
46 
47 import static processing.app.I18n.tr;
48 
49 public class ArchiveExtractor {
50 
51   private final Platform platform;
52 
ArchiveExtractor(Platform platform)53   public ArchiveExtractor(Platform platform) {
54     assert platform != null;
55     this.platform = platform;
56   }
57 
58   /**
59    * Extract <b>source</b> into <b>destFolder</b>. <b>source</b> file archive
60    * format is autodetected from file extension.
61    *
62    * @param archiveFile
63    * @param destFolder
64    * @throws IOException
65    */
extract(File archiveFile, File destFolder)66   public void extract(File archiveFile, File destFolder) throws IOException, InterruptedException {
67     extract(archiveFile, destFolder, 0);
68   }
69 
70   /**
71    * Extract <b>source</b> into <b>destFolder</b>. <b>source</b> file archive
72    * format is autodetected from file extension.
73    *
74    * @param archiveFile Archive file to extract
75    * @param destFolder  Destination folder
76    * @param stripPath   Number of path elements to strip from the paths contained in the
77    *                    archived files
78    * @throws IOException
79    */
extract(File archiveFile, File destFolder, int stripPath)80   public void extract(File archiveFile, File destFolder, int stripPath) throws IOException, InterruptedException {
81     extract(archiveFile, destFolder, stripPath, false);
82   }
83 
84 
extract(File archiveFile, File destFolder, int stripPath, boolean overwrite)85   public void extract(File archiveFile, File destFolder, int stripPath, boolean overwrite) throws IOException, InterruptedException {
86 
87     // Folders timestamps must be set at the end of archive extraction
88     // (because creating a file in a folder alters the folder's timestamp)
89     Map<File, Long> foldersTimestamps = new HashMap<>();
90 
91     ArchiveInputStream in = null;
92     try {
93 
94       // Create an ArchiveInputStream with the correct archiving algorithm
95       if (archiveFile.getName().endsWith("tar.bz2")) {
96         in = new TarArchiveInputStream(new BZip2CompressorInputStream(new FileInputStream(archiveFile)));
97       } else if (archiveFile.getName().endsWith("zip")) {
98         in = new ZipArchiveInputStream(new FileInputStream(archiveFile));
99       } else if (archiveFile.getName().endsWith("tar.gz")) {
100         in = new TarArchiveInputStream(new GzipCompressorInputStream(new FileInputStream(archiveFile)));
101       } else if (archiveFile.getName().endsWith("tar")) {
102         in = new TarArchiveInputStream(new FileInputStream(archiveFile));
103       } else {
104         throw new IOException("Archive format not supported.");
105       }
106 
107       String pathPrefix = "";
108 
109       Map<File, File> hardLinks = new HashMap<>();
110       Map<File, Integer> hardLinksMode = new HashMap<>();
111       Map<File, String> symLinks = new HashMap<>();
112       Map<File, Long> symLinksModifiedTimes = new HashMap<>();
113 
114       // Cycle through all the archive entries
115       while (true) {
116         ArchiveEntry entry = in.getNextEntry();
117         if (entry == null) {
118           break;
119         }
120 
121         // Extract entry info
122         long size = entry.getSize();
123         String name = entry.getName();
124         boolean isDirectory = entry.isDirectory();
125         boolean isLink = false;
126         boolean isSymLink = false;
127         String linkName = null;
128         Integer mode = null;
129         long modifiedTime = entry.getLastModifiedDate().getTime();
130 
131         {
132           // Skip MacOSX metadata
133           // http://superuser.com/questions/61185/why-do-i-get-files-like-foo-in-my-tarball-on-os-x
134           int slash = name.lastIndexOf('/');
135           if (slash == -1) {
136             if (name.startsWith("._")) {
137               continue;
138             }
139           } else {
140             if (name.substring(slash + 1).startsWith("._")) {
141               continue;
142             }
143           }
144         }
145 
146         // Skip git metadata
147         // http://www.unix.com/unix-for-dummies-questions-and-answers/124958-file-pax_global_header-means-what.html
148         if (name.contains("pax_global_header")) {
149           continue;
150         }
151 
152         if (entry instanceof TarArchiveEntry) {
153           TarArchiveEntry tarEntry = (TarArchiveEntry) entry;
154           mode = tarEntry.getMode();
155           isLink = tarEntry.isLink();
156           isSymLink = tarEntry.isSymbolicLink();
157           linkName = tarEntry.getLinkName();
158         }
159 
160         // On the first archive entry, if requested, detect the common path
161         // prefix to be stripped from filenames
162         if (stripPath > 0 && pathPrefix.isEmpty()) {
163           int slash = 0;
164           while (stripPath > 0) {
165             slash = name.indexOf("/", slash);
166             if (slash == -1) {
167               throw new IOException("Invalid archive: it must contain a single root folder");
168             }
169             slash++;
170             stripPath--;
171           }
172           pathPrefix = name.substring(0, slash);
173         }
174 
175         // Strip the common path prefix when requested
176         if (!name.startsWith(pathPrefix)) {
177           throw new IOException("Invalid archive: it must contain a single root folder while file " + name + " is outside " + pathPrefix);
178         }
179         name = name.substring(pathPrefix.length());
180         if (name.isEmpty()) {
181           continue;
182         }
183         File outputFile = new File(destFolder, name);
184 
185         File outputLinkedFile = null;
186         if (isLink) {
187           if (!linkName.startsWith(pathPrefix)) {
188             throw new IOException("Invalid archive: it must contain a single root folder while file " + linkName + " is outside " + pathPrefix);
189           }
190           linkName = linkName.substring(pathPrefix.length());
191           outputLinkedFile = new File(destFolder, linkName);
192         }
193         if (isSymLink) {
194           // Symbolic links are referenced with relative paths
195           outputLinkedFile = new File(linkName);
196           if (outputLinkedFile.isAbsolute()) {
197             System.err.println(I18n.format(tr("Warning: file {0} links to an absolute path {1}"), outputFile, outputLinkedFile));
198             System.err.println();
199           }
200         }
201 
202         // Safety check
203         if (isDirectory) {
204           if (outputFile.isFile() && !overwrite) {
205             throw new IOException("Can't create folder " + outputFile + ", a file with the same name exists!");
206           }
207         } else {
208           // - isLink
209           // - isSymLink
210           // - anything else
211           if (outputFile.exists() && !overwrite) {
212             throw new IOException("Can't extract file " + outputFile + ", file already exists!");
213           }
214         }
215 
216         // Extract the entry
217         if (isDirectory) {
218           if (!outputFile.exists() && !outputFile.mkdirs()) {
219             throw new IOException("Could not create folder: " + outputFile);
220           }
221           foldersTimestamps.put(outputFile, modifiedTime);
222         } else if (isLink) {
223           hardLinks.put(outputFile, outputLinkedFile);
224           hardLinksMode.put(outputFile, mode);
225         } else if (isSymLink) {
226           symLinks.put(outputFile, linkName);
227           symLinksModifiedTimes.put(outputFile, modifiedTime);
228         } else {
229           // Create the containing folder if not exists
230           if (!outputFile.getParentFile().isDirectory()) {
231             outputFile.getParentFile().mkdirs();
232           }
233           copyStreamToFile(in, size, outputFile);
234           outputFile.setLastModified(modifiedTime);
235         }
236 
237         // Set file/folder permission
238         if (mode != null && !isSymLink && outputFile.exists()) {
239           platform.chmod(outputFile, mode);
240         }
241       }
242 
243       for (Map.Entry<File, File> entry : hardLinks.entrySet()) {
244         if (entry.getKey().exists() && overwrite) {
245           entry.getKey().delete();
246         }
247         platform.link(entry.getValue(), entry.getKey());
248         Integer mode = hardLinksMode.get(entry.getKey());
249         if (mode != null) {
250           platform.chmod(entry.getKey(), mode);
251         }
252       }
253 
254       for (Map.Entry<File, String> entry : symLinks.entrySet()) {
255         if (entry.getKey().exists() && overwrite) {
256           entry.getKey().delete();
257         }
258         platform.symlink(entry.getValue(), entry.getKey());
259         entry.getKey().setLastModified(symLinksModifiedTimes.get(entry.getKey()));
260       }
261 
262     } finally {
263       IOUtils.closeQuietly(in);
264     }
265 
266     // Set folders timestamps
267     for (File folder : foldersTimestamps.keySet()) {
268       folder.setLastModified(foldersTimestamps.get(folder));
269     }
270   }
271 
copyStreamToFile(InputStream in, long size, File outputFile)272   private static void copyStreamToFile(InputStream in, long size, File outputFile) throws IOException {
273     FileOutputStream fos = null;
274     try {
275       fos = new FileOutputStream(outputFile);
276       // if size is not available, copy until EOF...
277       if (size == -1) {
278         byte buffer[] = new byte[4096];
279         int length;
280         while ((length = in.read(buffer)) != -1) {
281           fos.write(buffer, 0, length);
282         }
283         return;
284       }
285 
286       // ...else copy just the needed amount of bytes
287       byte buffer[] = new byte[4096];
288       while (size > 0) {
289         int length = in.read(buffer);
290         if (length <= 0) {
291           throw new IOException("Error while extracting file " + outputFile.getAbsolutePath());
292         }
293         fos.write(buffer, 0, length);
294         size -= length;
295       }
296     } finally {
297       IOUtils.closeQuietly(fos);
298     }
299   }
300 
301 }
302