1 /*
  2  * Copyright (c) 2024, Red Hat, Inc.
  3  * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
  4  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  5  *
  6  * This code is free software; you can redistribute it and/or modify it
  7  * under the terms of the GNU General Public License version 2 only, as
  8  * published by the Free Software Foundation.  Oracle designates this
  9  * particular file as subject to the "Classpath" exception as provided
 10  * by Oracle in the LICENSE file that accompanied this code.
 11  *
 12  * This code is distributed in the hope that it will be useful, but WITHOUT
 13  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 14  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 15  * version 2 for more details (a copy is included in the LICENSE file that
 16  * accompanied this code).
 17  *
 18  * You should have received a copy of the GNU General Public License version
 19  * 2 along with this work; if not, write to the Free Software Foundation,
 20  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 21  *
 22  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 23  * or visit www.oracle.com if you need additional information or have any
 24  * questions.
 25  */
 26 
 27 package jdk.tools.jlink.internal;
 28 
 29 import static jdk.tools.jlink.internal.LinkableRuntimeImage.RESPATH_PATTERN;
 30 
 31 import java.io.ByteArrayInputStream;
 32 import java.io.IOException;
 33 import java.io.InputStream;
 34 import java.io.UncheckedIOException;
 35 import java.lang.module.ModuleFinder;
 36 import java.nio.charset.StandardCharsets;
 37 import java.nio.file.Files;
 38 import java.nio.file.Path;
 39 import java.nio.file.Paths;
 40 import java.security.MessageDigest;
 41 import java.util.ArrayList;
 42 import java.util.Arrays;
 43 import java.util.Collections;
 44 import java.util.HexFormat;
 45 import java.util.List;
 46 import java.util.Map;
 47 import java.util.Objects;
 48 import java.util.Set;
 49 import java.util.function.Function;
 50 import java.util.function.Predicate;
 51 import java.util.stream.Collectors;
 52 import java.util.stream.Stream;
 53 
 54 import jdk.internal.jimage.ResourceEntries;
 55 import jdk.internal.jimage.SystemImageReader;
 56 import jdk.internal.util.OperatingSystem;
 57 import jdk.tools.jlink.internal.Archive.Entry.EntryType;
 58 import jdk.tools.jlink.internal.runtimelink.ResourceDiff;
 59 import jdk.tools.jlink.plugin.ResourcePoolEntry;
 60 import jdk.tools.jlink.plugin.ResourcePoolEntry.Type;
 61 
 62 /**
 63  * An archive implementation based on the JDK's run-time image. That is, classes
 64  * and resources from the modules image (lib/modules, or jimage) and other
 65  * associated files from the filesystem of the JDK installation.
 66  */
 67 public class JRTArchive implements Archive {
 68     private final String module;
 69     private final Path path;
 70     private final ResourceEntries imageResources;
 71     // The collection of files of this module
 72     private final List<JRTFile> files = new ArrayList<>();
 73     // Files not part of the lib/modules image of the JDK install.
 74     // Thus, native libraries, binaries, legal files, etc.
 75     private final List<String> otherRes;
 76     // Maps a module resource path to the corresponding diff to packaged
 77     // modules for that resource (if any)
 78     private final Map<String, ResourceDiff> resDiff;
 79     private final boolean errorOnModifiedFile;
 80     private final TaskHelper taskHelper;
 81     private final Set<String> upgradeableFiles;
 82 
 83     /**
 84      * JRTArchive constructor
 85      *
 86      * @param module The module name this archive refers to
 87      * @param path The JRT filesystem path.
 88      * @param errorOnModifiedFile Whether or not modified files of the JDK
 89      *        install aborts the link.
 90      * @param perModDiff The lib/modules (a.k.a jimage) diff for this module,
 91      *                   possibly an empty list if there are no differences.
 92      * @param taskHelper The task helper instance.
 93      * @param upgradeableFiles The set of files that are allowed for upgrades.
 94      */
 95     JRTArchive(String module,
 96                Path path,
 97                boolean errorOnModifiedFile,
 98                List<ResourceDiff> perModDiff,
 99                TaskHelper taskHelper,
100                Set<String> upgradeableFiles) {
101         this.module = module;
102         this.path = path;
103         ModuleFinder.ofSystem()
104                 .find(module)
105                 .orElseThrow(() -> new IllegalArgumentException(
106                         "Module " + module + " not part of the JDK install"));
107         this.imageResources = SystemImageReader.getResourceEntries();
108         this.errorOnModifiedFile = errorOnModifiedFile;
109         this.otherRes = readModuleResourceFile(module);
110         this.resDiff = Objects.requireNonNull(perModDiff).stream()
111                             .collect(Collectors.toMap(ResourceDiff::getName, Function.identity()));
112         this.taskHelper = taskHelper;
113         this.upgradeableFiles = upgradeableFiles;
114     }
115 
116     @Override
117     public String moduleName() {
118         return module;
119     }
120 
121     @Override
122     public Path getPath() {
123         return path;
124     }
125 
126     @Override
127     public Stream<Entry> entries() {
128         try {
129             collectFiles();
130         } catch (IOException e) {
131             throw new UncheckedIOException(e);
132         }
133         return files.stream().map(JRTFile::toEntry);
134     }
135 
136     @Override
137     public void open() throws IOException {
138         if (files.isEmpty()) {
139             collectFiles();
140         }
141     }
142 
143     @Override
144     public void close() throws IOException {
145         if (!files.isEmpty()) {
146             files.clear();
147         }
148     }
149 
150     @Override
151     public int hashCode() {
152         return Objects.hash(module, path);
153     }
154 
155     @Override
156     public boolean equals(Object obj) {
157         return (obj instanceof JRTArchive other &&
158                    Objects.equals(module, other.module) &&
159                    Objects.equals(path, other.path));
160     }
161 
162     private boolean isNormalOrModifiedDiff(String name) {
163         ResourceDiff rd = resDiff.get(name);
164         // Filter all resources with a resource diff of kind MODIFIED.
165         // Note that REMOVED won't happen since in that case the module listing
166         // won't have the resource anyway.
167         // Note as well that filter removes files of kind ADDED. Those files are
168         // not in the packaged modules, so ought not to get returned from the
169         // pipeline.
170         return (rd == null || rd.getKind() == ResourceDiff.Kind.MODIFIED);
171     }
172 
173     private void collectFiles() throws IOException {
174         if (files.isEmpty()) {
175             addNonClassResources();
176 
177             // Add classes/resources from the run-time image,
178             // patched with the run-time image diff
179             imageResources.getEntryNames(module)
180                     .filter(this::isNormalOrModifiedDiff)
181                     .sorted()
182                     .map(name -> new JrtModuleFile(this, name, resDiff.get(name)))
183                     .forEach(files::add);
184 
185             // Finally add all files only present in the resource diff
186             // That is, removed items in the run-time image.
187             files.addAll(resDiff.values().stream()
188                     .filter(rd -> rd.getKind() == ResourceDiff.Kind.REMOVED)
189                     .map(rd -> new JrtModuleFile(this, rd.getName(), rd))
190                     .toList());
191         }
192     }
193 
194     /*
195      * no need to keep track of the warning produced since this is eagerly
196      * checked once.
197      */
198     private void addNonClassResources() {
199         // Not all modules will have other resources like bin, lib, legal etc.
200         // files. In that case the list will be empty.
201         if (!otherRes.isEmpty()) {
202             files.addAll(otherRes.stream()
203                  .filter(Predicate.not(String::isEmpty))
204                  .map(s -> {
205                         ResourceFileEntry m = ResourceFileEntry.decodeFromString(s);
206 
207                         // Read from the base JDK image.
208                         Path path = BASE.resolve(m.resPath);
209                         if (!isUpgradeableFile(m.resPath) &&
210                                 shaSumMismatch(path, m.hashOrTarget, m.symlink)) {
211                             if (errorOnModifiedFile) {
212                                 String msg = taskHelper.getMessage("err.runtime.link.modified.file", path.toString());
213                                 IOException cause = new IOException(msg);
214                                 throw new UncheckedIOException(cause);
215                             } else {
216                                 taskHelper.warning("err.runtime.link.modified.file", path.toString());
217                             }
218                         }
219 
220                         return new JrtOtherFile(
221                                 this, m.resPath, toEntryType(m.resType), m.hashOrTarget, m.symlink);
222                     })
223                     .toList());
224         }
225     }
226 
227     /**
228      * Certain files in a module are considered upgradeable. That is,
229      * their hash sums aren't checked.
230      *
231      * @param resPath The resource path of the file to check for upgradeability.
232      * @return {@code true} if the file is upgradeable. {@code false} otherwise.
233      */
234     private boolean isUpgradeableFile(String resPath) {
235         return upgradeableFiles.contains(resPath);
236     }
237 
238     static boolean shaSumMismatch(Path res, String expectedSha, boolean isSymlink) {
239         if (isSymlink) {
240             return false;
241         }
242         // handle non-symlink resources
243         try {
244             HexFormat format = HexFormat.of();
245             byte[] expected = format.parseHex(expectedSha);
246             MessageDigest digest = MessageDigest.getInstance("SHA-512");
247             try (InputStream is = Files.newInputStream(res)) {
248                 byte[] buf = new byte[1024];
249                 int readBytes = -1;
250                 while ((readBytes = is.read(buf)) != -1) {
251                     digest.update(buf, 0, readBytes);
252                 }
253             }
254             byte[] actual = digest.digest();
255             return !MessageDigest.isEqual(expected, actual);
256         } catch (Exception e) {
257             throw new AssertionError("SHA-512 sum check failed!", e);
258         }
259     }
260 
261     private static EntryType toEntryType(Type input) {
262         return switch(input) {
263             case CLASS_OR_RESOURCE -> EntryType.CLASS_OR_RESOURCE;
264             case CONFIG -> EntryType.CONFIG;
265             case HEADER_FILE -> EntryType.HEADER_FILE;
266             case LEGAL_NOTICE -> EntryType.LEGAL_NOTICE;
267             case MAN_PAGE -> EntryType.MAN_PAGE;
268             case NATIVE_CMD -> EntryType.NATIVE_CMD;
269             case NATIVE_LIB -> EntryType.NATIVE_LIB;
270             case TOP -> throw new IllegalArgumentException(
271                            "TOP files should be handled by ReleaseInfoPlugin!");
272             default -> throw new IllegalArgumentException("Unknown type: " + input);
273         };
274     }
275 
276     public record ResourceFileEntry(Type resType,
277                                     boolean symlink,
278                                     String hashOrTarget,
279                                     String resPath) {
280         // Type file format:
281         // '<type>|{0,1}|<sha-sum>|<file-path>'
282         //   (1)    (2)      (3)      (4)
283         //
284         // Where fields are:
285         //
286         // (1) The resource type as specified by ResourcePoolEntry.type()
287         // (2) Symlink designator. 0 => regular resource, 1 => symlinked resource
288         // (3) The SHA-512 sum of the resources' content. The link to the target
289         //     for symlinked resources.
290         // (4) The relative file path of the resource
291         private static final String TYPE_FILE_FORMAT = "%d|%d|%s|%s";
292 
293         private static final Map<Integer, Type> typeMap = Arrays.stream(Type.values())
294                 .collect(Collectors.toMap(Type::ordinal, Function.identity()));
295 
296         public String encodeToString() {
297             return String.format(TYPE_FILE_FORMAT,
298                                  resType.ordinal(),
299                                  symlink ? 1 : 0,
300                                  hashOrTarget,
301                                  resPath);
302         }
303 
304         /*
305          *  line: <int>|<int>|<hashOrTarget>|<path>
306          *
307          *  Take the integer before '|' convert it to a Type. The second token
308          *  is an integer representing symlinks (or not). The third token is
309          *  a hash sum (sha512) of the file denoted by the fourth token (path).
310          */
311         static ResourceFileEntry decodeFromString(String line) {
312             assert !line.isEmpty();
313 
314             String[] tokens = line.split("\\|", 4);
315             Type type = null;
316             int symlinkNum = -1;
317             try {
318                 Integer typeInt = Integer.valueOf(tokens[0]);
319                 type = typeMap.get(typeInt);
320                 if (type == null) {
321                     throw new AssertionError("Illegal type ordinal: " + typeInt);
322                 }
323                 symlinkNum = Integer.valueOf(tokens[1]);
324             } catch (NumberFormatException e) {
325                 throw new AssertionError(e); // must not happen
326             }
327             if (symlinkNum < 0 || symlinkNum > 1) {
328                 throw new AssertionError(
329                         "Symlink designator out of range [0,1] got: " +
330                         symlinkNum);
331             }
332             return new ResourceFileEntry(type,
333                                          symlinkNum == 1,
334                                          tokens[2] /* hash or target */,
335                                          tokens[3] /* resource path */);
336         }
337 
338         public static ResourceFileEntry toResourceFileEntry(ResourcePoolEntry entry,
339                                                             Platform platform) {
340             String resPathWithoutMod = dropModuleFromPath(entry, platform);
341             // Symlinks don't have a hash sum, but a link to the target instead
342             String hashOrTarget = entry.linkedTarget() == null
343                                         ? computeSha512(entry)
344                                         : dropModuleFromPath(entry.linkedTarget(),
345                                                              platform);
346             return new ResourceFileEntry(entry.type(),
347                                          entry.linkedTarget() != null,
348                                          hashOrTarget,
349                                          resPathWithoutMod);
350         }
351 
352         private static String computeSha512(ResourcePoolEntry entry) {
353             try {
354                 assert entry.linkedTarget() == null;
355                 MessageDigest digest = MessageDigest.getInstance("SHA-512");
356                 try (InputStream is = entry.content()) {
357                     byte[] buf = new byte[1024];
358                     int bytesRead = -1;
359                     while ((bytesRead = is.read(buf)) != -1) {
360                         digest.update(buf, 0, bytesRead);
361                     }
362                 }
363                 byte[] db = digest.digest();
364                 HexFormat format = HexFormat.of();
365                 return format.formatHex(db);
366             } catch (Exception e) {
367                 throw new AssertionError("Failed to generate hash sum for " +
368                                          entry.path());
369             }
370         }
371 
372         private static String dropModuleFromPath(ResourcePoolEntry entry,
373                                                  Platform platform) {
374             String resPath = entry.path()
375                                   .substring(
376                                       // + 2 => prefixed and suffixed '/'
377                                       // For example: '/java.base/'
378                                       entry.moduleName().length() + 2);
379             if (!isWindows(platform)) {
380                 return resPath;
381             }
382             // For Windows the libraries live in the 'bin' folder rather than
383             // the 'lib' folder in the final image. Note that going by the
384             // NATIVE_LIB type only is insufficient since only files with suffix
385             // .dll/diz/map/pdb are transplanted to 'bin'.
386             // See: DefaultImageBuilder.nativeDir()
387             return nativeDir(entry, resPath);
388         }
389 
390         private static boolean isWindows(Platform platform) {
391             return platform.os() == OperatingSystem.WINDOWS;
392         }
393 
394         private static String nativeDir(ResourcePoolEntry entry, String resPath) {
395             if (entry.type() != ResourcePoolEntry.Type.NATIVE_LIB) {
396                 return resPath;
397             }
398             // precondition: Native lib, windows platform
399             if (resPath.endsWith(".dll") || resPath.endsWith(".diz")
400                     || resPath.endsWith(".pdb") || resPath.endsWith(".map")) {
401                 if (resPath.startsWith(LIB_DIRNAME + "/")) {
402                     return BIN_DIRNAME + "/" +
403                                resPath.substring((LIB_DIRNAME + "/").length());
404                 }
405             }
406             return resPath;
407         }
408         private static final String BIN_DIRNAME = "bin";
409         private static final String LIB_DIRNAME = "lib";
410     }
411 
412     private static final Path BASE = Paths.get(System.getProperty("java.home"));
413 
414     interface JRTFile {
415         Entry toEntry();
416     }
417 
418     record JrtModuleFile(
419             JRTArchive archive,
420             String resPath,
421             ResourceDiff diff) implements JRTFile {
422         @Override
423         public Entry toEntry() {
424             assert resPath.startsWith("/" + archive.moduleName() + "/");
425             String resName = resPath.substring(archive.moduleName().length() + 2);
426 
427             // If the resource has a diff to the packaged modules, use the diff.
428             // Diffs of kind ADDED have been filtered out in collectFiles();
429             if (diff != null) {
430                 assert diff.getKind() != ResourceDiff.Kind.ADDED;
431                 assert diff.getName().equals(resPath);
432 
433                 return new Entry(archive, resPath, resName, EntryType.CLASS_OR_RESOURCE) {
434                     @Override
435                     public long size() {
436                         return diff.getResourceBytes().length;
437                     }
438                     @Override
439                     public InputStream stream() {
440                         return new ByteArrayInputStream(diff.getResourceBytes());
441                     }
442                 };
443             } else {
444                 return new Entry(archive, resPath, resName, EntryType.CLASS_OR_RESOURCE) {
445                     @Override
446                     public long size() {
447                         return archive.imageResources.getSize(resPath);
448                     }
449 
450                     @Override
451                     public InputStream stream() {
452                         // Byte content could be cached in the entry if needed.
453                         return new ByteArrayInputStream(archive.imageResources.getBytes(resPath));
454                     }
455                 };
456             }
457         }
458     }
459 
460     record JrtOtherFile(
461             JRTArchive archive,
462             String resPath,
463             EntryType resType,
464             String sha,
465             boolean symlink) implements JRTFile {
466 
467         // Read from the base JDK image, special casing
468         // symlinks, which have the link target in the
469         // hashOrTarget field.
470         Path targetPath() {
471             return BASE.resolve(symlink ? sha : resPath);
472         }
473 
474         public Entry toEntry() {
475             assert resType != EntryType.CLASS_OR_RESOURCE;
476 
477             return new Entry(
478                     archive,
479                     String.format("/%s/%s", archive.moduleName(), resPath),
480                     resPath,
481                     resType) {
482 
483                 @Override
484                 public long size() {
485                     try {
486                         return Files.size(targetPath());
487                     } catch (IOException e) {
488                         throw new UncheckedIOException(e);
489                     }
490                 }
491 
492                 @Override
493                 public InputStream stream() throws IOException {
494                     return Files.newInputStream(targetPath());
495                 }
496             };
497         }
498     }
499 
500     private static List<String> readModuleResourceFile(String modName) {
501         String resName = String.format(RESPATH_PATTERN, modName);
502         try {
503             try (InputStream inStream = JRTArchive.class.getModule()
504                                                   .getResourceAsStream(resName)) {
505                 String input = new String(inStream.readAllBytes(), StandardCharsets.UTF_8);
506                 if (input.isEmpty()) {
507                     // Not all modules have non-class resources
508                     return Collections.emptyList();
509                 } else {
510                     return Arrays.asList(input.split("\n"));
511                 }
512             }
513         } catch (IOException e) {
514             throw new UncheckedIOException("Failed to process resources from the " +
515                                            "run-time image for module " + modName, e);
516         }
517     }
518 }