From 4b7ff492bcfa7ec57c5223dfafc8050c0e46f567 Mon Sep 17 00:00:00 2001
From: Martino Facchin <m.facchin@arduino.cc>
Date: Fri, 17 Mar 2017 09:31:27 +0100
Subject: [PATCH] Enable zipped sketch with embedded libraries

Works in conjunction with https://github.com/facchinm/arduino-builder/tree/inoz_experiment
---
 app/src/processing/app/Base.java              |   3 +-
 app/src/processing/app/Editor.java            |  14 ++-
 arduino-core/src/processing/app/Sketch.java   |  85 +++++++++++++-
 .../src/processing/app/tools/ZipDeflater.java | 109 ++++++++++++++++++
 4 files changed, 202 insertions(+), 9 deletions(-)
 create mode 100644 arduino-core/src/processing/app/tools/ZipDeflater.java

diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java
index c930cc85f96..ca352c3a584 100644
--- a/app/src/processing/app/Base.java
+++ b/app/src/processing/app/Base.java
@@ -771,7 +771,8 @@ public void handleOpenPrompt() throws Exception {
     fd.setFilenameFilter(new FilenameFilter() {
       public boolean accept(File dir, String name) {
         return name.toLowerCase().endsWith(".ino")
-                || name.toLowerCase().endsWith(".pde");
+                || name.toLowerCase().endsWith(".pde")
+                || name.toLowerCase().endsWith(".inz");
       }
     });
 
diff --git a/app/src/processing/app/Editor.java b/app/src/processing/app/Editor.java
index 7d26271953b..2e96d3bc0f3 100644
--- a/app/src/processing/app/Editor.java
+++ b/app/src/processing/app/Editor.java
@@ -157,7 +157,7 @@ public boolean test(SketchController sketch) {
 
   static volatile AbstractMonitor serialMonitor;
   static AbstractMonitor serialPlotter;
-  
+
   final EditorHeader header;
   EditorStatus status;
   EditorConsole console;
@@ -247,7 +247,7 @@ public void windowDeactivated(WindowEvent e) {
 
     //PdeKeywords keywords = new PdeKeywords();
     //sketchbook = new Sketchbook(this);
-    
+
     buildMenuBar();
 
     // For rev 0120, placing things inside a JPanel
@@ -1862,7 +1862,9 @@ protected boolean handleOpenInternal(File sketchFile) {
     File file = Sketch.checkSketchFile(sketchFile);
 
     if (file == null) {
-      if (!fileName.endsWith(".ino") && !fileName.endsWith(".pde")) {
+      if (fileName.endsWith(".inz")) {
+        file = sketchFile;
+      } else if (!fileName.endsWith(".ino") && !fileName.endsWith(".pde")) {
 
         Base.showWarning(tr("Bad file selected"), tr("Arduino can only open its own sketches\n" +
           "and other files ending in .ino or .pde"), null);
@@ -2288,7 +2290,7 @@ public void handleSerial() {
         return;
       }
     }
-  
+
     if (serialMonitor != null) {
       // The serial monitor already exists
 
@@ -2385,7 +2387,7 @@ public void handleSerial() {
     } while (serialMonitor.requiresAuthorization() && !success);
 
   }
-  
+
   public void handlePlotter() {
     if(serialMonitor != null) {
       if(serialMonitor.isClosed()) {
@@ -2395,7 +2397,7 @@ public void handlePlotter() {
         return;
       }
     }
-  
+
     if (serialPlotter != null) {
       // The serial plotter already exists
 
diff --git a/arduino-core/src/processing/app/Sketch.java b/arduino-core/src/processing/app/Sketch.java
index 6c417403ec9..1410902f9cc 100644
--- a/arduino-core/src/processing/app/Sketch.java
+++ b/arduino-core/src/processing/app/Sketch.java
@@ -2,13 +2,20 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.io.FileInputStream;
 import java.nio.file.Files;
 import java.util.*;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import java.io.FileOutputStream;
 
 import cc.arduino.files.DeleteFilesOnShutdown;
 import processing.app.helpers.FileUtils;
+import processing.app.tools.ZipDeflater;
+
+import org.apache.commons.compress.utils.IOUtils;
 
 import static processing.app.I18n.tr;
 
@@ -17,8 +24,10 @@
  */
 public class Sketch {
   public static final String DEFAULT_SKETCH_EXTENSION = "ino";
+  public static final String DEFAULT_SKETCH_EXTENSION_COMPRESSED = "inoz";
   public static final List<String> OLD_SKETCH_EXTENSIONS = Arrays.asList("pde");
-  public static final List<String> SKETCH_EXTENSIONS = Stream.concat(Stream.of(DEFAULT_SKETCH_EXTENSION), OLD_SKETCH_EXTENSIONS.stream()).collect(Collectors.toList());
+  public static final List<String> DEFAULT_SKETCH_EXTENSIONS = Stream.concat(Stream.of(DEFAULT_SKETCH_EXTENSION), Stream.of(DEFAULT_SKETCH_EXTENSION_COMPRESSED)).collect(Collectors.toList());
+  public static final List<String> SKETCH_EXTENSIONS = Stream.concat(DEFAULT_SKETCH_EXTENSIONS.stream(), OLD_SKETCH_EXTENSIONS.stream()).collect(Collectors.toList());
   public static final List<String> OTHER_ALLOWED_EXTENSIONS = Arrays.asList("c", "cpp", "h", "hh", "hpp", "s");
   public static final List<String> EXTENSIONS = Stream.concat(SKETCH_EXTENSIONS.stream(), OTHER_ALLOWED_EXTENSIONS.stream()).collect(Collectors.toList());
 
@@ -27,6 +36,8 @@ public class Sketch {
    */
   private File folder;
 
+  private File compressedSketch = null;
+
   private List<SketchFile> files = new ArrayList<>();
 
   private File buildPath;
@@ -50,10 +61,70 @@ public int compare(SketchFile x, SketchFile y) {
    *          Any file inside the sketch directory.
    */
   Sketch(File file) throws IOException {
-    folder = file.getParentFile();
+    if (file.getName().endsWith("inz")) {
+      //extract it in a temp folder and assign the
+      compressedSketch = file;
+      File tmpFolder = FileUtils.createTempFolder();
+      ZipDeflater zipDeflater = new ZipDeflater(file, tmpFolder);
+      zipDeflater.deflate();
+      String basename = FileUtils.splitFilename(file.getName()).basename;
+      folder = new File(tmpFolder, basename);
+    } else {
+      folder = file.getParentFile();
+    }
     files = listSketchFiles(true);
   }
 
+  public void buildZip(File dir, String sofar,
+                       ZipOutputStream zos) throws IOException {
+    String files[] = dir.list();
+    if (files == null) {
+      throw new IOException("Unable to list files from " + dir);
+    }
+    for (int i = 0; i < files.length; i++) {
+      if (files[i].equals(".") ||
+          files[i].equals("..")) continue;
+
+      File sub = new File(dir, files[i]);
+      String nowfar = (sofar == null) ?
+        files[i] : (sofar + "/" + files[i]);
+
+      if (sub.isDirectory()) {
+        // directories are empty entries and have / at the end
+        ZipEntry entry = new ZipEntry(nowfar + "/");
+        //System.out.println(entry);
+        zos.putNextEntry(entry);
+        zos.closeEntry();
+        buildZip(sub, nowfar, zos);
+
+      } else {
+        ZipEntry entry = new ZipEntry(nowfar);
+        entry.setTime(sub.lastModified());
+        zos.putNextEntry(entry);
+        zos.write(loadBytesRaw(sub));
+        zos.closeEntry();
+      }
+    }
+  }
+
+  static public byte[] loadBytesRaw(File file) throws IOException {
+    int size = (int) file.length();
+    FileInputStream input = null;
+    try {
+      input = new FileInputStream(file);
+      byte buffer[] = new byte[size];
+      int offset = 0;
+      int bytesRead;
+      while ((bytesRead = input.read(buffer, offset, size - offset)) != -1) {
+        offset += bytesRead;
+        if (bytesRead == 0) break;
+      }
+      return buffer;
+    } finally {
+      IOUtils.closeQuietly(input);
+    }
+  }
+
   static public File checkSketchFile(File file) {
     // check to make sure that this .pde file is
     // in a folder of the same name
@@ -137,6 +208,13 @@ public void save() throws IOException {
       if (file.isModified())
         file.save();
     }
+    String basename = files.get(0).getPrettyName();
+    if (compressedSketch != null) {
+      ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(compressedSketch));
+      buildZip(folder, basename, zos);
+      zos.flush();
+      IOUtils.closeQuietly(zos);
+    }
   }
 
   public int getCodeCount() {
@@ -151,6 +229,9 @@ public SketchFile[] getFiles() {
    * Returns a file object for the primary .pde of this sketch.
    */
   public SketchFile getPrimaryFile() {
+    if (compressedSketch != null) {
+      return new SketchFile(this, compressedSketch);
+    }
     return files.get(0);
   }
 
diff --git a/arduino-core/src/processing/app/tools/ZipDeflater.java b/arduino-core/src/processing/app/tools/ZipDeflater.java
new file mode 100644
index 00000000000..1425d880247
--- /dev/null
+++ b/arduino-core/src/processing/app/tools/ZipDeflater.java
@@ -0,0 +1,109 @@
+package processing.app.tools;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Enumeration;
+import java.util.Random;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+
+import org.apache.commons.compress.utils.IOUtils;
+import processing.app.helpers.FileUtils;
+
+public class ZipDeflater {
+
+  private final ZipFile zipFile;
+  private final File destFolder;
+  private final Random random;
+  private final File file;
+
+  public ZipDeflater(File file, File destFolder) throws ZipException, IOException {
+    this.file = file;
+    this.destFolder = destFolder;
+    this.zipFile = new ZipFile(file);
+    this.random = new Random();
+  }
+
+  public void deflate() throws IOException {
+    String tmpFolderName = folderNameFromZip() + random.nextInt(1000000);
+
+    File tmpFolder = new File(destFolder, tmpFolderName);
+
+    if (!tmpFolder.mkdir()) {
+      throw new IOException("Unable to create folder " + tmpFolderName);
+    }
+
+    Enumeration<? extends ZipEntry> entries = zipFile.entries();
+    while (entries.hasMoreElements()) {
+      ZipEntry entry = entries.nextElement();
+      ensureFoldersOfEntryExist(tmpFolder, entry);
+      File entryFile = new File(tmpFolder, entry.getName());
+      if (entry.isDirectory()) {
+        entryFile.mkdir();
+      } else {
+        FileOutputStream fos = null;
+        InputStream zipInputStream = null;
+        try {
+          fos = new FileOutputStream(entryFile);
+          zipInputStream = zipFile.getInputStream(entry);
+          byte[] buffer = new byte[1024 * 4];
+          int len = -1;
+          while ((len = zipInputStream.read(buffer)) != -1) {
+            fos.write(buffer, 0, len);
+          }
+        } finally {
+          IOUtils.closeQuietly(fos);
+          IOUtils.closeQuietly(zipInputStream);
+        }
+      }
+    }
+
+    deleteUndesiredFoldersAndFiles(tmpFolder);
+
+    // Test.zip may or may not contain Test folder. If it does, we keep it. If not, we use zip name.
+    ensureOneLevelFolder(tmpFolder);
+  }
+
+  private void deleteUndesiredFoldersAndFiles(File folder) {
+    for (File file : folder.listFiles()) {
+      if (file.isDirectory() && "__MACOSX".equals(file.getName())) {
+        FileUtils.recursiveDelete(file);
+      } else if (file.getName().startsWith(".")) {
+        FileUtils.recursiveDelete(file);
+      }
+    }
+  }
+
+  private void ensureFoldersOfEntryExist(File folder, ZipEntry entry) {
+    String[] parts = entry.getName().split("/");
+    File current = folder;
+    for (int i = 0; i < parts.length - 1; i++) {
+      current = new File(current, parts[i]);
+      current.mkdir();
+    }
+  }
+
+  private void ensureOneLevelFolder(File folder) {
+    File[] files = folder.listFiles();
+
+    if (files.length != 1) {
+      folder.renameTo(new File(folder.getParentFile(), folderNameFromZip()));
+      return;
+    }
+
+    files[0].renameTo(new File(folder.getParentFile(), files[0].getName()));
+    FileUtils.recursiveDelete(folder);
+  }
+
+  private String folderNameFromZip() {
+    String filename = file.getName();
+    if (filename.lastIndexOf(".") != -1) {
+      filename = filename.substring(0, filename.lastIndexOf("."));
+    }
+    return filename;
+  }
+
+}