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 }