1 /*
  2  * Copyright (c) 2014, 2024, 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 static jdk.tools.jlink.internal.LinkableRuntimeImage.DIFF_PATTERN;
 28 import static jdk.tools.jlink.internal.LinkableRuntimeImage.RESPATH_PATTERN;
 29 
 30 import java.io.BufferedOutputStream;
 31 import java.io.ByteArrayOutputStream;
 32 import java.io.DataOutputStream;
 33 import java.io.IOException;
 34 import java.io.OutputStream;
 35 import java.io.UncheckedIOException;
 36 import java.nio.ByteOrder;
 37 import java.nio.charset.StandardCharsets;
 38 import java.nio.file.Files;
 39 import java.nio.file.Path;
 40 import java.util.ArrayList;
 41 import java.util.Collections;
 42 import java.util.HashMap;
 43 import java.util.HashSet;
 44 import java.util.List;
 45 import java.util.Map;
 46 import java.util.Objects;
 47 import java.util.Optional;
 48 import java.util.Set;
 49 import java.util.stream.Collectors;
 50 import java.util.stream.Stream;
 51 
 52 import jdk.tools.jlink.internal.Archive.Entry;
 53 import jdk.tools.jlink.internal.Archive.Entry.EntryType;
 54 import jdk.tools.jlink.internal.JRTArchive.ResourceFileEntry;
 55 import jdk.tools.jlink.internal.ResourcePoolManager.CompressedModuleData;
 56 import jdk.tools.jlink.internal.runtimelink.JimageDiffGenerator;
 57 import jdk.tools.jlink.internal.runtimelink.JimageDiffGenerator.ImageResource;
 58 import jdk.tools.jlink.internal.runtimelink.ResourceDiff;
 59 import jdk.tools.jlink.internal.runtimelink.ResourcePoolReader;
 60 import jdk.tools.jlink.plugin.PluginException;
 61 import jdk.tools.jlink.plugin.ResourcePool;
 62 import jdk.tools.jlink.plugin.ResourcePoolBuilder;
 63 import jdk.tools.jlink.plugin.ResourcePoolEntry;
 64 import jdk.tools.jlink.plugin.ResourcePoolModule;
 65 
 66 /**
 67  * An image (native endian.)
 68  * <pre>{@code
 69  * {
 70  *   u4 magic;
 71  *   u2 major_version;
 72  *   u2 minor_version;
 73  *   u4 resource_count;
 74  *   u4 table_length;
 75  *   u4 location_attributes_size;
 76  *   u4 strings_size;
 77  *   u4 redirect[table_length];
 78  *   u4 offsets[table_length];
 79  *   u1 location_attributes[location_attributes_size];
 80  *   u1 strings[strings_size];
 81  *   u1 content[if !EOF];
 82  * }
 83  * }</pre>
 84  */
 85 public final class ImageFileCreator {
 86     private static final byte[] EMPTY_RESOURCE_BYTES = new byte[] {};
 87 
 88     private static final String JLINK_MOD_NAME = "jdk.jlink";
 89     private static final String RESPATH = "/" + JLINK_MOD_NAME + "/" + RESPATH_PATTERN;
 90     private static final String DIFF_PATH = "/" + JLINK_MOD_NAME + "/" + DIFF_PATTERN;
 91     private final Map<String, List<Entry>> entriesForModule = new HashMap<>();
 92     private final ImagePluginStack plugins;
 93     private final boolean generateRuntimeImage;
 94 
 95     private ImageFileCreator(ImagePluginStack plugins,
 96                              boolean generateRuntimeImage) {
 97         this.plugins = Objects.requireNonNull(plugins);
 98         this.generateRuntimeImage = generateRuntimeImage;
 99     }
100 
101     /**
102      * Create an executable image based on a set of input archives and a given
103      * plugin stack for a given byte order. It optionally generates a runtime
104      * that can be used for linking from the run-time image if
105      * {@code generateRuntimeImage} is set to {@code true}.
106      *
107      * @param archives The set of input archives
108      * @param byteOrder The desired byte order of the result
109      * @param plugins The plugin stack to apply to the input
110      * @param generateRuntimeImage if a runtime suitable for linking from the
111      *        run-time image should get created.
112      * @return The executable image.
113      * @throws IOException
114      */
115     public static ExecutableImage create(Set<Archive> archives,
116             ByteOrder byteOrder,
117             ImagePluginStack plugins,
118             boolean generateRuntimeImage)
119             throws IOException
120     {
121         ImageFileCreator image = new ImageFileCreator(plugins,
122                                                       generateRuntimeImage);
123         try {
124             image.readAllEntries(archives);
125             // write to modular image
126             image.writeImage(archives, byteOrder);
127         } catch (UncheckedIOException e) {
128             // When linking from the run-time image, readAllEntries() might
129             // throw this exception for a modified runtime. Unpack and
130             // re-throw as IOException.
131             throw e.getCause();
132         } finally {
133             // Close all archives
134             for (Archive a : archives) {
135                 a.close();
136             }
137         }
138 
139         return plugins.getExecutableImage();
140     }
141 
142     private void readAllEntries(Set<Archive> archives) {
143         archives.forEach((archive) -> {
144             Map<Boolean, List<Entry>> es;
145             try (Stream<Entry> entries = archive.entries()) {
146                 es = entries.collect(Collectors.partitioningBy(n -> n.type()
147                         == EntryType.CLASS_OR_RESOURCE));
148             }
149             String mn = archive.moduleName();
150             List<Entry> all = new ArrayList<>();
151             all.addAll(es.get(false));
152             all.addAll(es.get(true));
153             entriesForModule.put(mn, all);
154         });
155     }
156 
157     public static void recreateJimage(Path jimageFile,
158             Set<Archive> archives,
159             ImagePluginStack pluginSupport,
160             boolean generateRuntimeImage)
161             throws IOException {
162         try {
163             Map<String, List<Entry>> entriesForModule
164                     = archives.stream().collect(Collectors.toMap(
165                                     Archive::moduleName,
166                                     a -> {
167                                         try (Stream<Entry> entries = a.entries()) {
168                                             return entries.toList();
169                                         }
170                                     }));
171             ByteOrder order = ByteOrder.nativeOrder();
172             BasicImageWriter writer = new BasicImageWriter(order);
173             ResourcePoolManager pool = createPoolManager(archives, entriesForModule, order, writer);
174             try (OutputStream fos = Files.newOutputStream(jimageFile);
175                     BufferedOutputStream bos = new BufferedOutputStream(fos);
176                     DataOutputStream out = new DataOutputStream(bos)) {
177                 generateJImage(pool, writer, pluginSupport, out, generateRuntimeImage);
178             }
179         } finally {
180             //Close all archives
181             for (Archive a : archives) {
182                 a.close();
183             }
184         }
185     }
186 
187     private void writeImage(Set<Archive> archives,
188             ByteOrder byteOrder)
189             throws IOException {
190         BasicImageWriter writer = new BasicImageWriter(byteOrder);
191         ResourcePoolManager allContent = createPoolManager(archives,
192                 entriesForModule, byteOrder, writer);
193         ResourcePool result = null;
194         try (DataOutputStream out = plugins.getJImageFileOutputStream()) {
195             result = generateJImage(allContent, writer, plugins, out, generateRuntimeImage);
196         }
197 
198         //Handle files.
199         try {
200             plugins.storeFiles(allContent.resourcePool(), result, writer);
201         } catch (Exception ex) {
202             if (JlinkTask.DEBUG) {
203                 ex.printStackTrace();
204             }
205             throw new IOException(ex);
206         }
207     }
208 
209     /**
210      * Create a jimage based on content of the given ResourcePoolManager,
211      * optionally creating a runtime that can be used for linking from the
212      * run-time image
213      *
214      * @param allContent The content that needs to get added to the resulting
215      *                   lib/modules (jimage) file.
216      * @param writer The writer for the jimage file.
217      * @param pluginSupport The stack of all plugins to apply.
218      * @param out The output stream to write the jimage to.
219      * @param generateRuntimeImage if a runtime suitable for linking from the
220      *        run-time image should get created.
221      * @return A pool of the actual result resources.
222      * @throws IOException
223      */
224     private static ResourcePool generateJImage(ResourcePoolManager allContent,
225             BasicImageWriter writer,
226             ImagePluginStack pluginSupport,
227             DataOutputStream out,
228             boolean generateRuntimeImage
229     ) throws IOException {
230         ResourcePool resultResources;
231         try {
232             resultResources = pluginSupport.visitResources(allContent);
233             if (generateRuntimeImage) {
234                 // Keep track of non-modules resources for linking from a run-time image
235                 resultResources = addNonClassResourcesTrackFiles(resultResources,
236                                                                  writer);
237                 // Generate the diff between the input resources from packaged
238                 // modules in 'allContent' to the plugin- or otherwise
239                 // generated-content in 'resultResources'
240                 resultResources = addResourceDiffFiles(allContent.resourcePool(),
241                                                        resultResources,
242                                                        writer);
243             }
244         } catch (PluginException pe) {
245             if (JlinkTask.DEBUG) {
246                 pe.printStackTrace();
247             }
248             throw pe;
249         } catch (Exception ex) {
250             if (JlinkTask.DEBUG) {
251                 ex.printStackTrace();
252             }
253             throw new IOException(ex);
254         }
255         Set<String> duplicates = new HashSet<>();
256         long[] offset = new long[1];
257 
258         List<ResourcePoolEntry> content = new ArrayList<>();
259         List<String> paths = new ArrayList<>();
260         // the order of traversing the resources and the order of
261         // the module content being written must be the same
262         resultResources.entries().forEach(res -> {
263             if (res.type().equals(ResourcePoolEntry.Type.CLASS_OR_RESOURCE)) {
264                 String path = res.path();
265                 content.add(res);
266                 long uncompressedSize = res.contentLength();
267                 long compressedSize = 0;
268                 if (res instanceof CompressedModuleData) {
269                     CompressedModuleData comp
270                             = (CompressedModuleData) res;
271                     compressedSize = res.contentLength();
272                     uncompressedSize = comp.getUncompressedSize();
273                 }
274                 long onFileSize = res.contentLength();
275 
276                 if (duplicates.contains(path)) {
277                     System.err.format("duplicate resource \"%s\", skipping%n",
278                             path);
279                     // TODO Need to hang bytes on resource and write
280                     // from resource not zip.
281                     // Skipping resource throws off writing from zip.
282                     offset[0] += onFileSize;
283                     return;
284                 }
285                 duplicates.add(path);
286                 writer.addLocation(path, offset[0], compressedSize, uncompressedSize);
287                 paths.add(path);
288                 offset[0] += onFileSize;
289             }
290         });
291 
292         ImageResourcesTree tree = new ImageResourcesTree(offset[0], writer, paths);
293 
294         // write header and indices
295         byte[] bytes = writer.getBytes();
296         out.write(bytes, 0, bytes.length);
297 
298         // write module content
299         content.forEach((res) -> {
300             res.write(out);
301         });
302 
303         tree.addContent(out);
304 
305         out.close();
306 
307         return resultResources;
308     }
309 
310     /**
311      * Support for creating a runtime suitable for linking from the run-time
312      * image.
313      *
314      * Generates differences between the packaged modules "view" in
315      * {@code jmodContent} to the optimized image in {@code resultContent} and
316      * adds the result to the returned resource pool.
317      *
318      * @param jmodContent The resource pool view of packaged modules
319      * @param resultContent The optimized result generated from the jmodContent
320      *                      input by applying the plugin stack.
321      * @param writer The image writer.
322      * @return The resource pool with the difference file resources added to
323      *         the {@code resultContent}
324      */
325     @SuppressWarnings("try")
326     private static ResourcePool addResourceDiffFiles(ResourcePool jmodContent,
327                                                      ResourcePool resultContent,
328                                                      BasicImageWriter writer) {
329         JimageDiffGenerator generator = new JimageDiffGenerator();
330         List<ResourceDiff> diff;
331         try (ImageResource jmods = new ResourcePoolReader(jmodContent);
332              ImageResource jimage = new ResourcePoolReader(resultContent)) {
333             diff = generator.generateDiff(jmods, jimage);
334         } catch (Exception e) {
335             throw new AssertionError("Failed to generate the runtime image diff", e);
336         }
337         Set<String> modules = resultContent.moduleView().modules()
338                                                         .map(a -> a.name())
339                                                         .collect(Collectors.toSet());
340         // Add resource diffs for the resource files we are about to add
341         modules.stream().forEach(m -> {
342             String resourceName = String.format(DIFF_PATH, m);
343             ResourceDiff.Builder builder = new ResourceDiff.Builder();
344             ResourceDiff d = builder.setKind(ResourceDiff.Kind.ADDED)
345                                     .setName(resourceName)
346                                     .build();
347             diff.add(d);
348         });
349         Map<String, List<ResourceDiff>> perModDiffs = preparePerModuleDiffs(diff,
350                                                                             modules);
351         return addDiffResourcesFiles(modules, perModDiffs, resultContent, writer);
352     }
353 
354     private static Map<String, List<ResourceDiff>> preparePerModuleDiffs(List<ResourceDiff> resDiffs,
355                                                                          Set<String> modules) {
356         Map<String, List<ResourceDiff>> modToDiff = new HashMap<>();
357         resDiffs.forEach(d -> {
358             int secondSlash = d.getName().indexOf("/", 1);
359             if (secondSlash == -1) {
360                 throw new AssertionError("Module name not present");
361             }
362             String module = d.getName().substring(1, secondSlash);
363             List<ResourceDiff> perModDiff = modToDiff.computeIfAbsent(module,
364                                                                       a -> new ArrayList<>());
365             perModDiff.add(d);
366         });
367         Map<String, List<ResourceDiff>> allModsToDiff = new HashMap<>();
368         modules.stream().forEach(m -> {
369             List<ResourceDiff> d = modToDiff.get(m);
370             if (d == null) {
371                 // Not all modules will have a diff
372                 allModsToDiff.put(m, Collections.emptyList());
373             } else {
374                 allModsToDiff.put(m, d);
375             }
376         });
377         return allModsToDiff;
378     }
379 
380     private static ResourcePool addDiffResourcesFiles(Set<String> modules,
381                                                       Map<String, List<ResourceDiff>> perModDiffs,
382                                                       ResourcePool resultResources,
383                                                       BasicImageWriter writer) {
384         ResourcePoolManager mgr = createPoolManager(resultResources, writer);
385         ResourcePoolBuilder out = mgr.resourcePoolBuilder();
386         modules.stream().sorted().forEach(module -> {
387             String mResource = String.format(DIFF_PATH, module);
388             List<ResourceDiff> diff = perModDiffs.get(module);
389             // Note that for modules without diff to the packaged modules view
390             // we create resource diff files with just the header and no content.
391             ByteArrayOutputStream bout = new ByteArrayOutputStream();
392             try {
393                 ResourceDiff.write(diff, bout);
394             } catch (IOException e) {
395                 throw new AssertionError("Failed to write resource diff file" +
396                                          " for module " + module, e);
397             }
398             out.add(ResourcePoolEntry.create(mResource, bout.toByteArray()));
399         });
400         return out.build();
401     }
402 
403     /**
404      * Support for creating runtimes that can be used for linking from the
405      * run-time image. Adds meta-data files for resources not in the lib/modules
406      * file of the JDK. That is, mapping files for which on-disk files belong to
407      * which module.
408      *
409      * @param resultResources
410      *            The original resources which serve as the basis for generating
411      *            the meta-data files.
412      * @param writer
413      *            The image writer.
414      *
415      * @return An amended resource pool which includes meta-data files.
416      */
417     private static ResourcePool addNonClassResourcesTrackFiles(ResourcePool resultResources,
418                                                                BasicImageWriter writer) {
419         // Only add resources if jdk.jlink module is present in the target image
420         Optional<ResourcePoolModule> jdkJlink = resultResources.moduleView()
421                                                                .findModule(JLINK_MOD_NAME);
422         if (jdkJlink.isPresent()) {
423             Map<String, List<String>> nonClassResources = recordAndFilterEntries(resultResources);
424             return addModuleResourceEntries(resultResources, nonClassResources, writer);
425         } else {
426             return resultResources; // No-op
427         }
428     }
429 
430     /**
431      * Support for creating runtimes that can be used for linking from the
432      * run-time image. Adds the given mapping of files as a meta-data file to
433      * the given resource pool.
434      *
435      * @param resultResources
436      *            The resource pool to add files to.
437      * @param nonClassResEntries
438      *            The per module mapping for which to create the meta-data files
439      *            for.
440      * @param writer
441      *            The image writer.
442      *
443      * @return A resource pool with meta-data files added.
444      */
445     private static ResourcePool addModuleResourceEntries(ResourcePool resultResources,
446                                                          Map<String, List<String>> nonClassResEntries,
447                                                          BasicImageWriter writer) {
448         Set<String> inputModules = resultResources.moduleView().modules()
449                                                   .map(rm -> rm.name())
450                                                   .collect(Collectors.toSet());
451         ResourcePoolManager mgr = createPoolManager(resultResources, writer);
452         ResourcePoolBuilder out = mgr.resourcePoolBuilder();
453         inputModules.stream().sorted().forEach(module -> {
454             String mResource = String.format(RESPATH, module);
455             List<String> mResources = nonClassResEntries.get(module);
456             if (mResources == null) {
457                 // We create empty resource files for modules in the resource
458                 // pool view that don't themselves contain native resources
459                 // or config files.
460                 out.add(ResourcePoolEntry.create(mResource, EMPTY_RESOURCE_BYTES));
461             } else {
462                 String mResContent = mResources.stream().sorted()
463                                                .collect(Collectors.joining("\n"));
464                 out.add(ResourcePoolEntry.create(mResource,
465                                                  mResContent.getBytes(StandardCharsets.UTF_8)));
466             }
467         });
468         return out.build();
469     }
470 
471     /**
472      * Support for creating runtimes that can be used for linking from the
473      * run-time image. Generates a per module mapping of files not part of the
474      * modules image (jimage). This mapping is needed so as to know which files
475      * of the installed JDK belong to which module.
476      *
477      * @param resultResources
478      *            The resources from which the mapping gets generated
479      * @return A mapping with the module names as keys and the list of files not
480      *         part of the modules image (jimage) as values.
481      */
482     private static Map<String, List<String>> recordAndFilterEntries(ResourcePool resultResources) {
483         Map<String, List<String>> nonClassResEntries = new HashMap<>();
484         Platform platform = getTargetPlatform(resultResources);
485         resultResources.entries().forEach(entry -> {
486             // Note that the fs_$module_files file is a resource file itself, so
487             // we cannot add fs_$module_files themselves due to the
488             // not(class_or_resources) condition. However, we also don't want
489             // to track 'release' file entries (not(top) condition) as those are
490             // handled by the release info plugin.
491             if (entry.type() != ResourcePoolEntry.Type.CLASS_OR_RESOURCE &&
492                     entry.type() != ResourcePoolEntry.Type.TOP) {
493                 List<String> mRes = nonClassResEntries.computeIfAbsent(entry.moduleName(),
494                                                                        a -> new ArrayList<>());
495                 ResourceFileEntry rfEntry = ResourceFileEntry.toResourceFileEntry(entry,
496                                                                                   platform);
497                 mRes.add(rfEntry.encodeToString());
498             }
499         });
500         return nonClassResEntries;
501     }
502 
503     private static Platform getTargetPlatform(ResourcePool in) {
504         String platform = in.moduleView().findModule("java.base")
505                 .map(ResourcePoolModule::targetPlatform)
506                 .orElseThrow(() -> new AssertionError("java.base not found"));
507         return Platform.parsePlatform(platform);
508     }
509 
510     private static ResourcePoolManager createPoolManager(Set<Archive> archives,
511             Map<String, List<Entry>> entriesForModule,
512             ByteOrder byteOrder,
513             BasicImageWriter writer) throws IOException {
514         ResourcePoolManager resources = createBasicResourcePoolManager(byteOrder, writer);
515         archives.stream()
516                 .map(Archive::moduleName)
517                 .sorted()
518                 .flatMap(mn ->
519                     entriesForModule.get(mn).stream()
520                             .map(e -> new ArchiveEntryResourcePoolEntry(mn,
521                                     e.getResourcePoolEntryName(), e)))
522                 .forEach(resources::add);
523         return resources;
524     }
525 
526     private static ResourcePoolManager createBasicResourcePoolManager(ByteOrder byteOrder,
527                                                                       BasicImageWriter writer) {
528         return new ResourcePoolManager(byteOrder, new StringTable() {
529 
530             @Override
531             public int addString(String str) {
532                 return writer.addString(str);
533             }
534 
535             @Override
536             public String getString(int id) {
537                 return writer.getString(id);
538             }
539         });
540     }
541 
542     /**
543      * Creates a ResourcePoolManager from existing resources so that more
544      * resources can be appended.
545      *
546      * @param resultResources The existing resources to initially add.
547      * @param writer The basic image writer.
548      * @return An appendable ResourcePoolManager.
549      */
550     private static ResourcePoolManager createPoolManager(ResourcePool resultResources,
551                                                          BasicImageWriter writer) {
552         ResourcePoolManager resources = createBasicResourcePoolManager(resultResources.byteOrder(),
553                                                                        writer);
554         // Note that resources are already sorted in the correct order.
555         // The underlying ResourcePoolManager keeps track of entries via
556         // LinkedHashMap, which keeps values in insertion order. Therefore
557         // adding resources here, preserving that same order is OK.
558         resultResources.entries().forEach(resources::add);
559         return resources;
560     }
561 
562     /**
563      * Helper method that splits a Resource path onto 3 items: module, parent
564      * and resource name.
565      *
566      * @param path
567      * @return An array containing module, parent and name.
568      */
569     public static String[] splitPath(String path) {
570         Objects.requireNonNull(path);
571         String noRoot = path.substring(1);
572         int pkgStart = noRoot.indexOf("/");
573         String module = noRoot.substring(0, pkgStart);
574         List<String> result = new ArrayList<>();
575         result.add(module);
576         String pkg = noRoot.substring(pkgStart + 1);
577         String resName;
578         int pkgEnd = pkg.lastIndexOf("/");
579         if (pkgEnd == -1) { // No package.
580             resName = pkg;
581         } else {
582             resName = pkg.substring(pkgEnd + 1);
583         }
584 
585         pkg = toPackage(pkg, false);
586         result.add(pkg);
587         result.add(resName);
588 
589         String[] array = new String[result.size()];
590         return result.toArray(array);
591     }
592 
593     /**
594      * Returns the path of the resource.
595      */
596     public static String resourceName(String path) {
597         Objects.requireNonNull(path);
598         String s = path.substring(1);
599         int index = s.indexOf("/");
600         return s.substring(index + 1);
601     }
602 
603     public static String toPackage(String name) {
604         return toPackage(name, false);
605     }
606 
607     private static String toPackage(String name, boolean log) {
608         int index = name.lastIndexOf('/');
609         if (index > 0) {
610             return name.substring(0, index).replace('/', '.');
611         } else {
612             // ## unnamed package
613             if (log) {
614                 System.err.format("Warning: %s in unnamed package%n", name);
615             }
616             return "";
617         }
618     }
619 }