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.internal.jimage;
  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.ByteBuffer;
  34 import java.nio.ByteOrder;
  35 import java.nio.file.Files;
  36 import java.nio.file.Path;
  37 import java.util.ArrayList;
  38 import java.util.HashMap;
  39 import java.util.HashSet;
  40 import java.util.List;
  41 import java.util.Map;
  42 import java.util.Objects;
  43 import java.util.Set;
  44 import java.util.function.Consumer;
  45 import java.util.function.Function;
  46 import java.util.stream.Collectors;
  47 import java.util.stream.Stream;
  48 import jdk.internal.jimage.Archive.Entry;
  49 import jdk.internal.jimage.Archive.Entry.EntryType;
  50 import static jdk.internal.jimage.BasicImageWriter.BOOT_NAME;
  51 import static jdk.internal.jimage.BasicImageWriter.IMAGE_EXT;
  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 Path root;
  74     private final Path mdir;
  75     private final Map<String, List<Entry>> entriesForModule = new HashMap<>();
  76     private ImageFileCreator(Path path) {
  77         this.root = path;
  78         this.mdir = root.resolve(path.getFileSystem().getPath("lib", "modules"));
  79     }
  80 
  81     public static ImageFileCreator create(Path output,
  82             Set<Archive> archives)
  83             throws IOException {
  84         return create(output, BOOT_NAME, archives, ByteOrder.nativeOrder());
  85     }
  86 
  87     public static ImageFileCreator create(Path output,
  88             Set<Archive> archives,
  89             ByteOrder byteOrder)
  90             throws IOException {
  91         return create(output, BOOT_NAME, archives, byteOrder);
  92     }
  93 
  94     public static ImageFileCreator create(Path output,
  95                                    String fileName,
  96                                    Set<Archive> archives,
  97                                    ByteOrder byteOrder)
  98         throws IOException
  99     {
 100         ImageFileCreator image = new ImageFileCreator(output);
 101         // get all entries
 102         Map<String, Set<String>> modulePackagesMap = new HashMap<>();
 103         image.readAllEntries(modulePackagesMap, archives);
 104         // write to modular image
 105         image.writeImage(fileName, modulePackagesMap, archives, byteOrder);
 106         return image;
 107     }
 108 
 109     private void readAllEntries(Map<String, Set<String>> modulePackagesMap,
 110                                   Set<Archive> archives) {
 111         archives.stream().forEach((archive) -> {
 112             Map<Boolean, List<Entry>> es;
 113             try(Stream<Entry> entries = archive.entries()) {
 114                 es = entries.collect(Collectors.partitioningBy(n -> n.type()
 115                         == EntryType.CLASS_OR_RESOURCE));
 116             }
 117             String mn = archive.moduleName();
 118             List<Entry> all = new ArrayList<>();
 119             all.addAll(es.get(false));
 120             all.addAll(es.get(true));
 121             entriesForModule.put(mn, all);
 122             // Extract package names
 123             Set<String> pkgs = es.get(true).stream().map(Entry::name)
 124                     .filter(n -> isClassPackage(n))
 125                     .map(ImageFileCreator::toPackage)
 126                     .collect(Collectors.toSet());
 127             modulePackagesMap.put(mn, pkgs);
 128         });
 129     }
 130 
 131     public static boolean isClassPackage(String path) {
 132         return path.endsWith(".class");
 133     }
 134 
 135     public static boolean isResourcePackage(String path) {
 136         path = path.substring(1);
 137         path = path.substring(path.indexOf("/")+1);
 138         return !path.startsWith("META-INF/");
 139     }
 140 
 141     public static void recreateJimage(Path jimageFile,
 142             Set<Archive> archives,
 143             Map<String, Set<String>> modulePackages)
 144             throws IOException {
 145         Map<String, List<Entry>> entriesForModule
 146                 = archives.stream().collect(Collectors.toMap(
 147                                 Archive::moduleName,
 148                                 a -> {
 149                                     try(Stream<Entry> entries = a.entries()) {
 150                                         return entries.collect(Collectors.toList());
 151                                     }
 152                                 }));
 153         Map<String, Archive> nameToArchive
 154                 = archives.stream()
 155                 .collect(Collectors.toMap(Archive::moduleName, Function.identity()));
 156         ByteOrder order = ByteOrder.nativeOrder();
 157         ResourcePoolImpl resources = createResources(modulePackages, nameToArchive,
 158                 (Entry t) -> {
 159             throw new UnsupportedOperationException("Not supported, no external file "
 160                     + "in a jimage file");
 161         }, entriesForModule, order);
 162         String fileName = jimageFile.getRoot().toString();
 163         generateJImage(jimageFile, fileName, resources, order);
 164     }
 165 
 166     private void writeImage(String fileName,
 167             Map<String, Set<String>> modulePackagesMap,
 168             Set<Archive> archives,
 169             ByteOrder byteOrder)
 170             throws IOException {
 171         Files.createDirectories(mdir);
 172         ExternalFilesWriter filesWriter = new ExternalFilesWriter(root);
 173         // name to Archive file
 174         Map<String, Archive> nameToArchive
 175                 = archives.stream()
 176                 .collect(Collectors.toMap(Archive::moduleName, Function.identity()));
 177         ResourcePoolImpl resources = createResources(modulePackagesMap,
 178                 nameToArchive, filesWriter,
 179                 entriesForModule, byteOrder);
 180         generateJImage(mdir.resolve(fileName + IMAGE_EXT), fileName, resources,
 181                 byteOrder);
 182     }
 183 
 184     private static void generateJImage(Path img,
 185             String fileName,
 186             ResourcePoolImpl resources,
 187             ByteOrder byteOrder
 188     ) throws IOException {
 189         BasicImageWriter writer = new BasicImageWriter(byteOrder);
 190 
 191         Map<String, Set<String>> modulePackagesMap = resources.getModulePackages();
 192 
 193         try (OutputStream fos = Files.newOutputStream(img);
 194                 BufferedOutputStream bos = new BufferedOutputStream(fos);
 195                 DataOutputStream out = new DataOutputStream(bos)) {
 196             Set<String> duplicates = new HashSet<>();
 197             ImageModuleDataWriter moduleData =
 198             ImageModuleDataWriter.buildModuleData(writer, modulePackagesMap);
 199             moduleData.addLocation(fileName, writer);
 200             long offset = moduleData.size();
 201 
 202             List<ResourcePool.Resource> content = new ArrayList<>();
 203             List<String> paths = new ArrayList<>();
 204                  // the order of traversing the resources and the order of
 205             // the module content being written must be the same
 206             for (ResourcePool.Resource res : resources.getResources()) {
 207                 String path = res.getPath();
 208                 int index = path.indexOf("/META-INF/");
 209                 if (index != -1) {
 210                     path = path.substring(index + 1);
 211                 }
 212 
 213                 content.add(res);
 214                 long uncompressedSize = res.getLength();
 215                 long compressedSize = 0;
 216                 if (res instanceof ResourcePool.CompressedResource) {
 217                     ResourcePool.CompressedResource comp =
 218                             (ResourcePool.CompressedResource) res;
 219                     compressedSize = res.getLength();
 220                     uncompressedSize = comp.getUncompressedSize();
 221                 }
 222                 long onFileSize = res.getLength();
 223 
 224                 if (duplicates.contains(path)) {
 225                     System.err.format("duplicate resource \"%s\", skipping%n",
 226                             path);
 227                      // TODO Need to hang bytes on resource and write
 228                     // from resource not zip.
 229                     // Skipping resource throws off writing from zip.
 230                     offset += onFileSize;
 231                     continue;
 232                 }
 233                 duplicates.add(path);
 234                 writer.addLocation(path, offset, compressedSize, uncompressedSize);
 235                 paths.add(path);
 236                 offset += onFileSize;
 237             }
 238 
 239             ImageResourcesTree tree = new ImageResourcesTree(offset, writer, paths);
 240 
 241             // write header and indices
 242             byte[] bytes = writer.getBytes();
 243             out.write(bytes, 0, bytes.length);
 244 
 245             // write module meta data
 246             moduleData.writeTo(out);
 247 
 248             // write module content
 249             for(ResourcePool.Resource res : content) {
 250                 byte[] buf = res.getByteArray();
 251                 out.write(buf, 0, buf.length);
 252             }
 253 
 254             tree.addContent(out);
 255         }
 256     }
 257 
 258     private static ResourcePoolImpl createResources(Map<String, Set<String>> modulePackagesMap,
 259             Map<String, Archive> nameToArchive,
 260             Consumer<Entry> externalFileHandler,
 261             Map<String, List<Entry>> entriesForModule,
 262             ByteOrder byteOrder) throws IOException {
 263         ResourcePoolImpl resources = new ResourcePoolImpl(byteOrder);
 264         Set<String> mods = modulePackagesMap.keySet();
 265         for (String mn : mods) {
 266             for (Entry entry : entriesForModule.get(mn)) {
 267                 String path = entry.name();
 268                 if (entry.type() == EntryType.CLASS_OR_RESOURCE) {
 269                     if (!entry.path().endsWith(BOOT_NAME)) {
 270                         try (InputStream stream = entry.stream()) {
 271                             byte[] bytes = readAllBytes(stream);
 272                             path = "/" + mn + "/" + path;
 273                             try {
 274                                 resources.addResource(new ResourcePool.Resource(path,
 275                                         ByteBuffer.wrap(bytes)));
 276                             } catch (Exception ex) {
 277                                 throw new IOException(ex);
 278                             }
 279                         }
 280                     }
 281                 } else {
 282                     externalFileHandler.accept(entry);
 283                 }
 284             }
 285             // Done with this archive, close it.
 286             Archive archive = nameToArchive.get(mn);
 287             archive.close();
 288         }
 289         return resources;
 290     }
 291 
 292     private static final int BUF_SIZE = 8192;
 293 
 294     private static byte[] readAllBytes(InputStream is) throws IOException {
 295         ByteArrayOutputStream baos = new ByteArrayOutputStream();
 296         byte[] buf = new byte[BUF_SIZE];
 297         while (true) {
 298             int n = is.read(buf);
 299             if (n < 0) {
 300                 break;
 301             }
 302             baos.write(buf, 0, n);
 303         }
 304         return baos.toByteArray();
 305     }
 306 
 307     /**
 308      * Helper method that splits a Resource path onto 3 items: module, parent
 309      * and resource name.
 310      *
 311      * @param path
 312      * @return An array containing module, parent and name.
 313      */
 314     public static String[] splitPath(String path) {
 315         Objects.requireNonNull(path);
 316         String noRoot = path.substring(1);
 317         int pkgStart = noRoot.indexOf("/");
 318         String module = noRoot.substring(0, pkgStart);
 319         List<String> result = new ArrayList<>();
 320         result.add(module);
 321         String pkg = noRoot.substring(pkgStart + 1);
 322         String resName;
 323         int pkgEnd = pkg.lastIndexOf("/");
 324         if (pkgEnd == -1) { // No package.
 325             resName = pkg;
 326         } else {
 327             resName = pkg.substring(pkgEnd + 1);
 328         }
 329 
 330         pkg = toPackage(pkg, false);
 331         result.add(pkg);
 332         result.add(resName);
 333 
 334         String[] array = new String[result.size()];
 335         return result.toArray(array);
 336     }
 337 
 338     private static String toPackage(String name) {
 339         String pkg = toPackage(name, true);
 340         return pkg;
 341     }
 342 
 343     private static String toPackage(String name, boolean log) {
 344         int index = name.lastIndexOf('/');
 345         if (index > 0) {
 346             return name.substring(0, index).replace('/', '.');
 347         } else {
 348             // ## unnamed package
 349             if (log) {
 350                 System.err.format("Warning: %s in unnamed package%n", name);
 351             }
 352             return "";
 353         }
 354     }
 355 }