rt/aot/src/main/java/org/apidesign/bck2brwsr/aot/Bck2BrwsrJars.java
author Jaroslav Tulach <jaroslav.tulach@apidesign.org>
Mon, 16 Jan 2017 04:04:22 +0100
changeset 1979 0f100539ce6c
parent 1886 c4d37f95adf2
child 1980 97ea0f369e18
permissions -rw-r--r--
More defensive checks shielding against JARs without manifest
     1 /**
     2  * Back 2 Browser Bytecode Translator
     3  * Copyright (C) 2012-2015 Jaroslav Tulach <jaroslav.tulach@apidesign.org>
     4  *
     5  * This program is free software: you can redistribute it and/or modify
     6  * it under the terms of the GNU General Public License as published by
     7  * the Free Software Foundation, version 2 of the License.
     8  *
     9  * This program is distributed in the hope that it will be useful,
    10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  * GNU General Public License for more details.
    13  *
    14  * You should have received a copy of the GNU General Public License
    15  * along with this program. Look for COPYING file in the top folder.
    16  * If not, see http://opensource.org/licenses/GPL-2.0.
    17  */
    18 package org.apidesign.bck2brwsr.aot;
    19 
    20 import java.io.BufferedReader;
    21 import java.io.ByteArrayInputStream;
    22 import java.io.File;
    23 import java.io.FileInputStream;
    24 import java.io.IOException;
    25 import java.io.InputStream;
    26 import java.io.InputStreamReader;
    27 import java.net.URL;
    28 import java.util.ArrayList;
    29 import java.util.Enumeration;
    30 import java.util.HashMap;
    31 import java.util.HashSet;
    32 import java.util.List;
    33 import java.util.Map;
    34 import java.util.Set;
    35 import java.util.jar.Attributes;
    36 import java.util.jar.JarEntry;
    37 import java.util.jar.JarFile;
    38 import java.util.jar.Manifest;
    39 import java.util.logging.Level;
    40 import java.util.logging.Logger;
    41 import java.util.zip.ZipEntry;
    42 import org.apidesign.vm4brwsr.Bck2Brwsr;
    43 
    44 /** Utilities to process JAR files and set a compiler
    45  * up.
    46  *
    47  * @since 0.9
    48  * @author Jaroslav Tulach
    49  */
    50 public final class Bck2BrwsrJars {
    51     private static final Logger LOG = Logger.getLogger(Bck2BrwsrJars.class.getName());
    52 
    53     private Bck2BrwsrJars() {
    54     }
    55     
    56     /** Creates new compiler pre-configured from the content of 
    57      * provided JAR file. The compiler will compile all classes.
    58      * The system understands OSGi manifest entries and NetBeans
    59      * module system manifest entries and will export
    60      * all packages that are exported in the JAR file. The system
    61      * also recognizes META-INF/services and makes sure the class names
    62      * are not mangled.
    63      * 
    64      * @param c the compiler to {@link Bck2Brwsr#addClasses(java.lang.String...) add classes},
    65      *    {@link Bck2Brwsr#addResources(java.lang.String...) add resources} and
    66      *    {@link Bck2Brwsr#addExported(java.lang.String...) exported objects} to.
    67      *    Can be <code>null</code> - in such case an 
    68      *    {@link Bck2Brwsr#newCompiler() empty compiler} is constructed.
    69      * @param jar the file to process
    70      * @return newly configured compiler
    71      * @throws IOException if something goes wrong
    72      */
    73     public static Bck2Brwsr configureFrom(Bck2Brwsr c, File jar) throws IOException {
    74         return configureFrom(c, jar, null);
    75     }
    76     
    77     /** Creates new compiler pre-configured from the content of 
    78      * provided JAR file. The compiler will compile all classes.
    79      * The system understands OSGi manifest entries and NetBeans
    80      * module system manifest entries and will export
    81      * all packages that are exported in the JAR file. The system
    82      * also recognizes META-INF/services and makes sure the class names
    83      * are not mangled.
    84      * 
    85      * @param c the compiler to {@link Bck2Brwsr#addClasses(java.lang.String...) add classes},
    86      *    {@link Bck2Brwsr#addResources(java.lang.String...) add resources} and
    87      *    {@link Bck2Brwsr#addExported(java.lang.String...) exported objects} to.
    88      *    Can be <code>null</code> - in such case an 
    89      *    {@link Bck2Brwsr#newCompiler() empty compiler} is constructed.
    90      * @param jar the file to process
    91      * @param classpath additional resources to make available during
    92      *   compilation, but not include them in the generated JavaScript
    93      * @return newly configured compiler
    94      * @throws IOException if something goes wrong
    95      * @since 0.11
    96      */
    97     public static Bck2Brwsr configureFrom(
    98         Bck2Brwsr c, File jar, final ClassLoader classpath
    99     ) throws IOException {
   100         return configureFrom(c, jar, classpath, true);
   101     }
   102     
   103     /** Creates new compiler pre-configured from the content of 
   104      * provided JAR file. The compiler will compile all classes.
   105      * The system understands OSGi manifest entries and NetBeans
   106      * module system manifest entries and will export
   107      * all packages that are exported in the JAR file. The system
   108      * also recognizes META-INF/services and makes sure the class names
   109      * are not mangled.
   110      * 
   111      * @param c the compiler to {@link Bck2Brwsr#addClasses(java.lang.String...) add classes},
   112      *    {@link Bck2Brwsr#addResources(java.lang.String...) add resources} and
   113      *    {@link Bck2Brwsr#addExported(java.lang.String...) exported objects} to.
   114      *    Can be <code>null</code> - in such case an 
   115      *    {@link Bck2Brwsr#newCompiler() empty compiler} is constructed.
   116      * @param jar the file to process
   117      * @param classpath additional resources to make available during
   118      *   compilation, but not include them in the generated JavaScript
   119      * @param ignoreBootClassPath should we ignore classes on bootclasspath?
   120      * @return newly configured compiler
   121      * @throws IOException if something goes wrong
   122      * @since 0.14
   123      */
   124     public static Bck2Brwsr configureFrom(
   125         Bck2Brwsr c, File jar, final ClassLoader classpath, final boolean ignoreBootClassPath
   126     ) throws IOException {
   127         if (jar.isDirectory()) {
   128             return configureDir(ignoreBootClassPath, c, jar, classpath);
   129         }
   130         final JarFile jf = new JarFile(jar);
   131         final List<String> classes = new ArrayList<>();
   132         List<String> resources = new ArrayList<>();
   133         Set<String> exported = new HashSet<>();
   134         class JarRes extends EmulationResources implements Bck2Brwsr.Resources {
   135             JarRes() {
   136                 super(ignoreBootClassPath, classpath, classes);
   137             }
   138             @Override
   139             public InputStream get(String resource) throws IOException {
   140                 InputStream is = getConverted(resource);
   141                 if (is != null) {
   142                     return is;
   143                 }
   144                 if (resource.startsWith("/")) {
   145                     resource = resource.substring(1);
   146                 }
   147                 ZipEntry ze = jf.getEntry(resource);
   148                 if (ze != null) {
   149                     is = jf.getInputStream(ze);
   150                 }
   151                 return is == null ? super.get(resource) : is;
   152             }
   153         }
   154         JarRes jarRes = new JarRes();
   155 
   156         listJAR(jf, jarRes, resources, exported);
   157         
   158         String cp = jf.getManifest().getMainAttributes().getValue("Class-Path"); // NOI18N
   159         String[] parts = cp == null ? new String[0] : cp.split(" ");
   160 
   161         if (c == null) {
   162             c = Bck2Brwsr.newCompiler();
   163         }
   164         
   165         return c
   166             .library(parts)
   167             .addClasses(classes.toArray(new String[classes.size()]))
   168             .addExported(exported.toArray(new String[exported.size()]))
   169             .addResources(resources.toArray(new String[resources.size()]))
   170             .resources(jarRes);
   171     }
   172     
   173     private static void listJAR(
   174         JarFile j, EmulationResources classes,
   175         List<String> resources, Set<String> keep
   176     ) throws IOException {
   177         Enumeration<JarEntry> en = j.entries();
   178         while (en.hasMoreElements()) {
   179             JarEntry e = en.nextElement();
   180             final String n = e.getName();
   181             if (n.endsWith("/")) {
   182                 continue;
   183             }
   184             if (n.startsWith("META-INF/maven/")) {
   185                 continue;
   186             }
   187             int last = n.lastIndexOf('/');
   188             String pkg = n.substring(0, last + 1);
   189             if (pkg.startsWith("java/")) {
   190                 keep.add(pkg);
   191             }
   192             if (n.endsWith(".class")) {
   193                 classes.addClassResource(n);
   194             } else {
   195                 resources.add(n);
   196                 if (n.startsWith("META-INF/services/") && keep != null) {
   197                     BufferedReader r = new BufferedReader(new InputStreamReader(j.getInputStream(e)));
   198                     for (;;) {
   199                         String l = r.readLine();
   200                         if (l == null) {
   201                             break;
   202                         }
   203                         if (l.startsWith("#")) {
   204                             continue;
   205                         }
   206                         keep.add(l.replace('.', '/'));
   207                     }
   208                     r.close();
   209                 }
   210             }
   211         }
   212         if (keep != null) {
   213             final Manifest manifest = j.getManifest();
   214             if (manifest != null) {
   215                 final Attributes mainAttr = manifest.getMainAttributes();
   216                 if (mainAttr != null) {
   217                     exportPublicPackages(mainAttr, keep);
   218                 }
   219             }
   220         }
   221     }
   222 
   223     static void exportPublicPackages(final Attributes mainAttr, Set<String> keep) {
   224         String exp = mainAttr.getValue("Export-Package"); // NOI18N
   225         if (exp != null) {
   226             for (String def : exp.split(",")) {
   227                 for (String sep : def.split(";")) {
   228                     keep.add(sep.replace('.', '/') + "/");
   229                     break;
   230                 }
   231             }
   232             return;
   233         }
   234         exp = mainAttr.getValue("OpenIDE-Module-Public-Packages");
   235         if (exp != null) {
   236             for (String def : exp.split(",")) {
   237                 def = def.trim();
   238                 if (def.endsWith(".*")) {
   239                     keep.add(def.substring(0, def.length() - 1).replace('.', '/'));
   240                 }
   241             }
   242         }
   243     }
   244     
   245     static byte[] readFrom(InputStream is) throws IOException {
   246         int expLen = is.available();
   247         if (expLen < 1) {
   248             expLen = 1;
   249         }
   250         byte[] arr = new byte[expLen];
   251         int pos = 0;
   252         for (;;) {
   253             int read = is.read(arr, pos, arr.length - pos);
   254             if (read == -1) {
   255                 break;
   256             }
   257             pos += read;
   258             if (pos == arr.length) {
   259                 byte[] tmp = new byte[arr.length * 2];
   260                 System.arraycopy(arr, 0, tmp, 0, arr.length);
   261                 arr = tmp;
   262             }
   263         }
   264         if (pos != arr.length) {
   265             byte[] tmp = new byte[pos];
   266             System.arraycopy(arr, 0, tmp, 0, pos);
   267             arr = tmp;
   268         }
   269         return arr;
   270     }
   271     
   272 
   273     static class EmulationResources implements Bck2Brwsr.Resources {
   274         private final List<String> classes;
   275         private final Map<String,byte[]> converted = new HashMap<>();
   276         private final BytecodeProcessor proc;
   277         private final ClassLoader cp;
   278         private final boolean ignoreBootClassPath;
   279 
   280         protected EmulationResources(boolean ignoreBootClassPath, ClassLoader cp, List<String> classes) {
   281             this.ignoreBootClassPath = ignoreBootClassPath;
   282             this.classes = classes;
   283             this.cp = cp != null ? cp : Bck2BrwsrJars.class.getClassLoader();
   284             BytecodeProcessor p;
   285             try {
   286                 Class<?> bpClass = Class.forName("org.apidesign.bck2brwsr.aot.RetroLambda");
   287                 p = (BytecodeProcessor) bpClass.newInstance();
   288             } catch (Throwable t) {
   289                 p = null;
   290             }
   291             this.proc = p;
   292         }
   293 
   294         protected final InputStream getConverted(String name) throws IOException {
   295             byte[] arr = converted.get(name);
   296             if (arr != null) {
   297                 return new ByteArrayInputStream(arr);
   298             }
   299             return null;
   300         }
   301         
   302         @Override
   303         public InputStream get(String name) throws IOException {
   304             InputStream is = getConverted(name);
   305             if (is != null) {
   306                 return is;
   307             }
   308             return getFromCp(name);
   309         }
   310 
   311         private InputStream getFromCp(String name) throws IOException {
   312             Enumeration<URL> en = cp.getResources(name);
   313             URL u = null;
   314             while (en.hasMoreElements()) {
   315                 u = en.nextElement();
   316             }
   317             if (u == null) {
   318                 LOG.log(Level.FINE, "Cannot find {0}", name);
   319                 return null;
   320             }
   321             if (ignoreBootClassPath && u.toExternalForm().contains("/rt.jar!")) {
   322                 LOG.log(Level.WARNING, "No bootdelegation for {0}", name);
   323                 return null;
   324             }
   325             return u.openStream();
   326         }
   327 
   328         private final class NoConvRes implements Bck2Brwsr.Resources {
   329             @Override
   330             public InputStream get(String resource) throws IOException {
   331                 return getFromCp(resource);
   332             }
   333         }
   334 
   335         private void addClassResource(String n) throws IOException {
   336             if (proc != null) {
   337                 try (InputStream is = this.get(n)) {
   338                     Map<String, byte[]> conv = proc.process(n, readFrom(is), new NoConvRes());
   339                     if (conv != null) {
   340                         boolean found = false;
   341                         for (Map.Entry<String, byte[]> entrySet : conv.entrySet()) {
   342                             String res = entrySet.getKey();
   343                             byte[] bytes = entrySet.getValue();
   344                             if (res.equals(n)) {
   345                                 found = true;
   346                             }
   347                             assert res.endsWith(".class") : "Wrong resource: " + res;
   348                             converted.put(res, bytes);
   349                             classes.add(res.substring(0, res.length() - 6));
   350                         }
   351                         if (!found) {
   352                             throw new IOException("Cannot find " + n + " among " + conv);
   353                         }
   354                         return;
   355                     }
   356                 }
   357             }
   358             classes.add(n.substring(0, n.length() - 6));
   359         }
   360     }
   361     
   362     private static Bck2Brwsr configureDir(final boolean ignoreBootClassPath, Bck2Brwsr c, final File dir, ClassLoader cp) throws IOException {
   363         List<String> arr = new ArrayList<>();
   364         List<String> classes = new ArrayList<>();
   365         class DirRes extends EmulationResources {
   366             public DirRes(ClassLoader cp, List<String> classes) {
   367                 super(ignoreBootClassPath, cp, classes);
   368             }
   369 
   370             @Override
   371             public InputStream get(String name) throws IOException {
   372                 InputStream is = super.get(name);
   373                 if (is != null) {
   374                     return is;
   375                 }
   376                 File r = new File(dir, name.replace('/', File.separatorChar));
   377                 if (r.exists()) {
   378                     return new FileInputStream(r);
   379                 }
   380                 return null;
   381             }
   382         }
   383         DirRes dirRes = new DirRes(cp, classes);
   384         listDir(dir, null, dirRes, arr);
   385         if (c == null) {
   386             c = Bck2Brwsr.newCompiler();
   387         }
   388         return c
   389         .addRootClasses(classes.toArray(new String[0]))
   390         .addResources(arr.toArray(new String[0]))
   391         .library()
   392         //.obfuscation(ObfuscationLevel.FULL)
   393         .resources(dirRes);
   394     }
   395 
   396     private static void listDir(
   397         File f, String pref, EmulationResources res, List<String> resources
   398     ) throws IOException {
   399         File[] arr = f.listFiles();
   400         if (arr == null) {
   401             if (f.getName().endsWith(".class")) {
   402                 res.addClassResource(pref + f.getName());
   403             } else {
   404                 resources.add(pref + f.getName());
   405             }
   406         } else {
   407             for (File ch : arr) {
   408                 listDir(ch, pref == null ? "" : pref + f.getName() + "/", res, resources);
   409             }
   410         }
   411     }
   412     
   413 }