1 /*
   2  * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package jdk.tools.jlink.internal;
  26 
  27 import java.io.BufferedOutputStream;
  28 import java.io.ByteArrayOutputStream;
  29 import java.io.DataOutputStream;
  30 import java.io.IOException;
  31 import java.io.InputStream;
  32 import java.io.OutputStream;
  33 import java.nio.ByteOrder;
  34 import java.nio.file.Files;
  35 import java.nio.file.Path;
  36 import java.util.ArrayList;
  37 import java.util.HashMap;
  38 import java.util.HashSet;
  39 import java.util.List;
  40 import java.util.Map;
  41 import java.util.Objects;
  42 import java.util.Set;
  43 import java.util.stream.Collectors;
  44 import java.util.stream.Stream;
  45 import jdk.tools.jlink.internal.Archive.Entry;
  46 import jdk.tools.jlink.internal.Archive.Entry.EntryType;
  47 import jdk.tools.jlink.internal.PoolImpl.CompressedModuleData;
  48 import jdk.tools.jlink.plugin.ExecutableImage;
  49 import jdk.tools.jlink.plugin.Pool;
  50 import jdk.tools.jlink.plugin.Pool.ModuleData;
  51 import jdk.tools.jlink.plugin.Pool.ModuleDataType;
  52 
  53 /**
  54  * An image (native endian.)
  55  * <pre>{@code
  56  * {
  57  *   u4 magic;
  58  *   u2 major_version;
  59  *   u2 minor_version;
  60  *   u4 resource_count;
  61  *   u4 table_length;
  62  *   u4 location_attributes_size;
  63  *   u4 strings_size;
  64  *   u4 redirect[table_length];
  65  *   u4 offsets[table_length];
  66  *   u1 location_attributes[location_attributes_size];
  67  *   u1 strings[strings_size];
  68  *   u1 content[if !EOF];
  69  * }
  70  * }</pre>
  71  */
  72 public final class ImageFileCreator {
  73     private final Map<String, List<Entry>> entriesForModule = new HashMap<>();
  74     private final ImagePluginStack plugins;
  75     private ImageFileCreator(ImagePluginStack plugins) {
  76         this.plugins = plugins;
  77     }
  78 
  79     public static ExecutableImage create(Set<Archive> archives,
  80             ImagePluginStack plugins)
  81             throws IOException {
  82         return ImageFileCreator.create(archives, ByteOrder.nativeOrder(),
  83                 plugins);
  84     }
  85 
  86     public static ExecutableImage create(Set<Archive> archives,
  87             ByteOrder byteOrder)
  88             throws IOException {
  89         return ImageFileCreator.create(archives, byteOrder,
  90                 new ImagePluginStack(null));
  91     }
  92 
  93     public static ExecutableImage create(Set<Archive> archives,
  94             ByteOrder byteOrder,
  95             ImagePluginStack plugins)
  96             throws IOException
  97     {
  98         ImageFileCreator image = new ImageFileCreator(plugins);
  99         try {
 100             image.readAllEntries(archives);
 101             // write to modular image
 102             image.writeImage(archives, byteOrder);
 103         } finally {
 104             //Close all archives
 105             for (Archive a : archives) {
 106                 a.close();
 107             }
 108         }
 109 
 110         return plugins.getExecutableImage();
 111     }
 112 
 113     private void readAllEntries(Set<Archive> archives) {
 114         archives.stream().forEach((archive) -> {
 115             Map<Boolean, List<Entry>> es;
 116             try (Stream<Entry> entries = archive.entries()) {
 117                 es = entries.collect(Collectors.partitioningBy(n -> n.type()
 118                         == EntryType.CLASS_OR_RESOURCE));
 119             }
 120             String mn = archive.moduleName();
 121             List<Entry> all = new ArrayList<>();
 122             all.addAll(es.get(false));
 123             all.addAll(es.get(true));
 124             entriesForModule.put(mn, all);
 125         });
 126     }
 127 
 128     public static boolean isClassPackage(String path) {
 129         return path.endsWith(".class") && !path.endsWith("module-info.class");
 130     }
 131 
 132     public static void recreateJimage(Path jimageFile,
 133             Set<Archive> archives,
 134             ImagePluginStack pluginSupport)
 135             throws IOException {
 136         try {
 137             Map<String, List<Entry>> entriesForModule
 138                     = archives.stream().collect(Collectors.toMap(
 139                                     Archive::moduleName,
 140                                     a -> {
 141                                         try (Stream<Entry> entries = a.entries()) {
 142                                             return entries.collect(Collectors.toList());
 143                                         }
 144                                     }));
 145             ByteOrder order = ByteOrder.nativeOrder();
 146             BasicImageWriter writer = new BasicImageWriter(order);
 147             PoolImpl pool = createPools(archives, entriesForModule, order, writer);
 148             try (OutputStream fos = Files.newOutputStream(jimageFile);
 149                     BufferedOutputStream bos = new BufferedOutputStream(fos);
 150                     DataOutputStream out = new DataOutputStream(bos)) {
 151                 generateJImage(pool, writer, pluginSupport, out);
 152             }
 153         } finally {
 154             //Close all archives
 155             for (Archive a : archives) {
 156                 a.close();
 157             }
 158         }
 159     }
 160 
 161     private void writeImage(Set<Archive> archives,
 162             ByteOrder byteOrder)
 163             throws IOException {
 164         BasicImageWriter writer = new BasicImageWriter(byteOrder);
 165         PoolImpl allContent = createPools(archives,
 166                 entriesForModule, byteOrder, writer);
 167         PoolImpl result = generateJImage(allContent,
 168              writer, plugins, plugins.getJImageFileOutputStream());
 169 
 170         //Handle files.
 171         try {
 172             plugins.storeFiles(allContent, result, writer);
 173         } catch (Exception ex) {
 174             throw new IOException(ex);
 175         }
 176     }
 177 
 178     private static PoolImpl generateJImage(PoolImpl allContent,
 179             BasicImageWriter writer,
 180             ImagePluginStack pluginSupport,
 181             DataOutputStream out
 182     ) throws IOException {
 183         PoolImpl resultResources;
 184         try {
 185             resultResources = pluginSupport.visitResources(allContent);
 186         } catch (Exception ex) {
 187             throw new IOException(ex);
 188         }
 189         Set<String> duplicates = new HashSet<>();
 190         long offset = 0;
 191 
 192         List<ModuleData> content = new ArrayList<>();
 193         List<String> paths = new ArrayList<>();
 194                  // the order of traversing the resources and the order of
 195         // the module content being written must be the same
 196         for (ModuleData res : resultResources.getContent()) {
 197             if (res.getType().equals(ModuleDataType.CLASS_OR_RESOURCE)) {
 198                 String path = res.getPath();
 199                 content.add(res);
 200                 long uncompressedSize = res.getLength();
 201                 long compressedSize = 0;
 202                 if (res instanceof CompressedModuleData) {
 203                     CompressedModuleData comp
 204                             = (CompressedModuleData) res;
 205                     compressedSize = res.getLength();
 206                     uncompressedSize = comp.getUncompressedSize();
 207                 }
 208                 long onFileSize = res.getLength();
 209 
 210                 if (duplicates.contains(path)) {
 211                     System.err.format("duplicate resource \"%s\", skipping%n",
 212                             path);
 213                     // TODO Need to hang bytes on resource and write
 214                     // from resource not zip.
 215                     // Skipping resource throws off writing from zip.
 216                     offset += onFileSize;
 217                     continue;
 218                 }
 219                 duplicates.add(path);
 220                 writer.addLocation(path, offset, compressedSize, uncompressedSize);
 221                 paths.add(path);
 222                 offset += onFileSize;
 223             }
 224         }
 225 
 226         ImageResourcesTree tree = new ImageResourcesTree(offset, writer, paths);
 227 
 228         // write header and indices
 229         byte[] bytes = writer.getBytes();
 230         out.write(bytes, 0, bytes.length);
 231 
 232         // write module content
 233         for (ModuleData res : content) {
 234             byte[] buf = res.getBytes();
 235             out.write(buf, 0, buf.length);
 236         }
 237 
 238         tree.addContent(out);
 239 
 240         out.close();
 241 
 242         return resultResources;
 243     }
 244 
 245     private static Pool.ModuleDataType mapImageFileType(EntryType type) {
 246         switch(type) {
 247             case CONFIG: {
 248                 return Pool.ModuleDataType.CONFIG;
 249             }
 250             case NATIVE_CMD: {
 251                 return Pool.ModuleDataType.NATIVE_CMD;
 252             }
 253             case NATIVE_LIB: {
 254                 return Pool.ModuleDataType.NATIVE_LIB;
 255             }
 256         }
 257         return null;
 258     }
 259 
 260     private static PoolImpl createPools(Set<Archive> archives,
 261             Map<String, List<Entry>> entriesForModule,
 262             ByteOrder byteOrder,
 263             BasicImageWriter writer) throws IOException {
 264         PoolImpl resources = new PoolImpl(byteOrder, new StringTable() {
 265 
 266             @Override
 267             public int addString(String str) {
 268                 return writer.addString(str);
 269             }
 270 
 271             @Override
 272             public String getString(int id) {
 273                 return writer.getString(id);
 274             }
 275         });
 276         for (Archive archive : archives) {
 277             String mn = archive.moduleName();
 278             for (Entry entry : entriesForModule.get(mn)) {
 279 
 280                 if (entry.type() == EntryType.CLASS_OR_RESOURCE) {
 281                     // Removal of "classes/" radical.
 282                     String path = entry.name();
 283                     try (InputStream stream = entry.stream()) {
 284                         byte[] bytes = readAllBytes(stream);
 285                         if (path.endsWith("module-info.class")) {
 286                             path = "/" + path;
 287                         } else {
 288                             path = "/" + mn + "/" + path;
 289                         }
 290                         try {
 291                             resources.add(Pool.newResource(path, bytes));
 292                         } catch (Exception ex) {
 293                             throw new IOException(ex);
 294                         }
 295                     }
 296                 } else {
 297                     try {
 298                         // Entry.path() contains the kind of file native, conf, bin, ...
 299                         // Keep it to avoid naming conflict (eg: native/jvm.cfg and config/jvm.cfg
 300                         resources.add(Pool.newImageFile(mn,
 301                                 "/" + mn + "/" + entry.path(), mapImageFileType(entry.type()),
 302                                 entry.stream(), entry.size()));
 303                     } catch (Exception ex) {
 304                         throw new IOException(ex);
 305                     }
 306                 }
 307             }
 308         }
 309         return resources;
 310     }
 311 
 312     private static final int BUF_SIZE = 8192;
 313 
 314     private static byte[] readAllBytes(InputStream is) throws IOException {
 315         ByteArrayOutputStream baos = new ByteArrayOutputStream();
 316         byte[] buf = new byte[BUF_SIZE];
 317         while (true) {
 318             int n = is.read(buf);
 319             if (n < 0) {
 320                 break;
 321             }
 322             baos.write(buf, 0, n);
 323         }
 324         return baos.toByteArray();
 325     }
 326 
 327     /**
 328      * Helper method that splits a Resource path onto 3 items: module, parent
 329      * and resource name.
 330      *
 331      * @param path
 332      * @return An array containing module, parent and name.
 333      */
 334     public static String[] splitPath(String path) {
 335         Objects.requireNonNull(path);
 336         String noRoot = path.substring(1);
 337         int pkgStart = noRoot.indexOf("/");
 338         String module = noRoot.substring(0, pkgStart);
 339         List<String> result = new ArrayList<>();
 340         result.add(module);
 341         String pkg = noRoot.substring(pkgStart + 1);
 342         String resName;
 343         int pkgEnd = pkg.lastIndexOf("/");
 344         if (pkgEnd == -1) { // No package.
 345             resName = pkg;
 346         } else {
 347             resName = pkg.substring(pkgEnd + 1);
 348         }
 349 
 350         pkg = toPackage(pkg, false);
 351         result.add(pkg);
 352         result.add(resName);
 353 
 354         String[] array = new String[result.size()];
 355         return result.toArray(array);
 356     }
 357 
 358     private static String toPackage(String name, boolean log) {
 359         int index = name.lastIndexOf('/');
 360         if (index > 0) {
 361             return name.substring(0, index).replace('/', '.');
 362         } else {
 363             // ## unnamed package
 364             if (log) {
 365                 System.err.format("Warning: %s in unnamed package%n", name);
 366             }
 367             return "";
 368         }
 369     }
 370 }