diff --git a/arduino-core/src/cc/arduino/utils/ArchiveExtractor.java b/arduino-core/src/cc/arduino/utils/ArchiveExtractor.java
new file mode 100644
index 000000000..95f87c9f1
--- /dev/null
+++ b/arduino-core/src/cc/arduino/utils/ArchiveExtractor.java
@@ -0,0 +1,273 @@
+/*
+ * This file is part of Arduino.
+ *
+ * Copyright 2014 Arduino LLC (http://www.arduino.cc/)
+ *
+ * Arduino is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * As a special exception, you may use this file as part of a free software
+ * library without restriction. Specifically, if other files instantiate
+ * templates or use macros or inline functions from this file, or you compile
+ * this file and link it with other files to produce an executable, this
+ * file does not by itself cause the resulting executable to be covered by
+ * the GNU General Public License. This exception does not however
+ * invalidate any other reasons why the executable file might be covered by
+ * the GNU General Public License.
+ */
+package cc.arduino.utils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.compress.archivers.ArchiveEntry;
+import org.apache.commons.compress.archivers.ArchiveInputStream;
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
+import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+
+import cc.arduino.os.FileNativeUtils;
+
+public class ArchiveExtractor {
+
+ /**
+ * Extract source into destFolder. source file archive
+ * format is autodetected from file extension.
+ *
+ * @param archiveFile
+ * @param destFolder
+ * @throws IOException
+ */
+ public static void extract(File archiveFile, File destFolder)
+ throws IOException {
+ extract(archiveFile, destFolder, 0);
+ }
+
+ /**
+ * Extract source into destFolder. source file archive
+ * format is autodetected from file extension.
+ *
+ * @param archiveFile
+ * Archive file to extract
+ * @param destFolder
+ * Destination folder
+ * @param stripPath
+ * Number of path elements to strip from the paths contained in the
+ * archived files
+ * @throws IOException
+ */
+ public static void extract(File archiveFile, File destFolder, int stripPath)
+ throws IOException {
+
+ // Folders timestamps must be set at the end of archive extraction
+ // (because creating a file in a folder alters the folder's timestamp)
+ Map foldersTimestamps = new HashMap();
+
+ ArchiveInputStream in = null;
+ try {
+
+ // Create an ArchiveInputStream with the correct archiving algorithm
+ if (archiveFile.getName().endsWith("tar.bz2")) {
+ InputStream fin = new FileInputStream(archiveFile);
+ fin = new BZip2CompressorInputStream(fin);
+ in = new TarArchiveInputStream(fin);
+ } else if (archiveFile.getName().endsWith("zip")) {
+ InputStream fin = new FileInputStream(archiveFile);
+ in = new ZipArchiveInputStream(fin);
+ } else if (archiveFile.getName().endsWith("tar.gz")) {
+ InputStream fin = new FileInputStream(archiveFile);
+ fin = new GzipCompressorInputStream(fin);
+ in = new TarArchiveInputStream(fin);
+ } else if (archiveFile.getName().endsWith("tar")) {
+ InputStream fin = new FileInputStream(archiveFile);
+ in = new TarArchiveInputStream(fin);
+ } else {
+ throw new IOException("Archive format not supported.");
+ }
+
+ String pathPrefix = "";
+
+ // Cycle through all the archive entries
+ while (true) {
+ ArchiveEntry entry = in.getNextEntry();
+ if (entry == null)
+ break;
+
+ // Extract entry info
+ long size = entry.getSize();
+ String name = entry.getName();
+ boolean isDirectory = entry.isDirectory();
+ boolean isLink = false;
+ boolean isSymLink = false;
+ String linkName = null;
+ Integer mode = null;
+ long modifiedTime = entry.getLastModifiedDate().getTime();
+
+ {
+ // Skip MacOSX metadata
+ // http://superuser.com/questions/61185/why-do-i-get-files-like-foo-in-my-tarball-on-os-x
+ int slash = name.lastIndexOf('/');
+ if (slash == -1) {
+ if (name.startsWith("._"))
+ continue;
+ } else {
+ if (name.substring(slash + 1).startsWith("._"))
+ continue;
+ }
+ }
+
+ // Skip git metadata
+ // http://www.unix.com/unix-for-dummies-questions-and-answers/124958-file-pax_global_header-means-what.html
+ if (name.contains("pax_global_header"))
+ continue;
+
+ if (entry instanceof TarArchiveEntry) {
+ TarArchiveEntry tarEntry = (TarArchiveEntry) entry;
+ mode = tarEntry.getMode();
+ isLink = tarEntry.isLink();
+ isSymLink = tarEntry.isSymbolicLink();
+ linkName = tarEntry.getLinkName();
+ }
+
+ // On the first archive entry, if requested, detect the common path
+ // prefix to be stripped from filenames
+ if (stripPath > 0 && pathPrefix.isEmpty()) {
+ int slash = 0;
+ while (stripPath > 0) {
+ slash = name.indexOf("/", slash);
+ if (slash == -1)
+ throw new IOException(
+ "Invalid archive: it must contains a single root folder");
+ slash++;
+ stripPath--;
+ }
+ pathPrefix = name.substring(0, slash);
+ }
+
+ // Strip the common path prefix when requested
+ if (!name.startsWith(pathPrefix))
+ throw new IOException(
+ "Invalid archive: it must contains a single root folder while file "
+ + name + " is outside " + pathPrefix);
+ name = name.substring(pathPrefix.length());
+ if (name.isEmpty())
+ continue;
+ File outputFile = new File(destFolder, name);
+
+ File outputLinkFile = null;
+ if (isLink) {
+ if (!linkName.startsWith(pathPrefix)) {
+ throw new IOException(
+ "Invalid archive: it must contains a single root folder while file "
+ + linkName + " is outside " + pathPrefix);
+ }
+ linkName = linkName.substring(pathPrefix.length());
+ outputLinkFile = new File(destFolder, linkName);
+ }
+ if (isSymLink) {
+ // Symbolic links are referenced with relative paths
+ outputLinkFile = new File(linkName);
+ if (outputLinkFile.isAbsolute())
+ throw new IOException(
+ "Invalid archive: it contains a symbolic link with absolute path '"
+ + outputLinkFile + "'");
+ }
+
+ // Safety check
+ if (isDirectory) {
+ if (outputFile.isFile())
+ throw new IOException("Can't create folder " + outputFile
+ + ", a file with the same name exists!");
+ } else {
+ // - isLink
+ // - isSymLink
+ // - anything else
+ if (outputFile.exists())
+ throw new IOException("Can't extract file " + outputFile
+ + ", file already exists!");
+ }
+
+ // Extract the entry
+ if (isDirectory) {
+ if (!outputFile.exists())
+ if (!outputFile.mkdirs())
+ throw new IOException("Could not create folder: " + outputFile);
+ foldersTimestamps.put(outputFile, modifiedTime);
+ } else if (isLink) {
+ FileNativeUtils.link(outputLinkFile, outputFile);
+ } else if (isSymLink) {
+ FileNativeUtils.symlink(outputLinkFile, outputFile);
+ outputFile.setLastModified(modifiedTime);
+ } else {
+ // Create the containing folder if not exists
+ if (!outputFile.getParentFile().isDirectory())
+ outputFile.getParentFile().mkdirs();
+ copyStreamToFile(in, size, outputFile);
+ outputFile.setLastModified(modifiedTime);
+ }
+
+ // Set file/folder permission
+ if (mode != null && !isSymLink)
+ FileNativeUtils.chmod(outputFile, mode);
+ }
+ } finally {
+ if (in != null)
+ in.close();
+ }
+
+ // Set folders timestamps
+ for (File folder : foldersTimestamps.keySet()) {
+ folder.setLastModified(foldersTimestamps.get(folder));
+ }
+ }
+
+ private static void copyStreamToFile(InputStream in, long size,
+ File outputFile)
+ throws FileNotFoundException, IOException {
+ FileOutputStream fos = new FileOutputStream(outputFile);
+ try {
+ // if size is not available, copy until EOF...
+ if (size == -1) {
+ byte buffer[] = new byte[4096];
+ int l;
+ while ((l = in.read(buffer)) != -1) {
+ fos.write(buffer, 0, l);
+ }
+ return;
+ }
+
+ // ...else copy just the needed amount of bytes
+ byte buffer[] = new byte[4096];
+ while (size > 0) {
+ int l = in.read(buffer);
+ if (l <= 0)
+ throw new IOException("Error while extracting file "
+ + outputFile.getAbsolutePath());
+ fos.write(buffer, 0, l);
+ size -= l;
+ }
+ } finally {
+ fos.close();
+ }
+ }
+
+}