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