1 /*
2 * Copyright (c) 2024, Red Hat, Inc.
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
26 package jdk.tools.jlink.internal;
27
28 import static jdk.tools.jlink.internal.LinkableRuntimeImage.RESPATH_PATTERN;
29
30 import java.io.ByteArrayInputStream;
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.io.UncheckedIOException;
34 import java.lang.module.ModuleFinder;
35 import java.lang.module.ModuleReference;
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.util.OperatingSystem;
55 import jdk.tools.jlink.internal.Archive.Entry.EntryType;
56 import jdk.tools.jlink.internal.runtimelink.ResourceDiff;
57 import jdk.tools.jlink.plugin.ResourcePoolEntry;
58 import jdk.tools.jlink.plugin.ResourcePoolEntry.Type;
59
60 /**
61 * An archive implementation based on the JDK's run-time image. That is, classes
62 * and resources from the modules image (lib/modules, or jimage) and other
63 * associated files from the filesystem of the JDK installation.
64 */
65 public class JRTArchive implements Archive {
66
67 private final String module;
68 private final Path path;
69 private final ModuleReference ref;
70 // The collection of files of this module
71 private final List<JRTFile> files = new ArrayList<>();
72 // Files not part of the lib/modules image of the JDK install.
73 // Thus, native libraries, binaries, legal files, etc.
74 private final List<String> otherRes;
75 // Maps a module resource path to the corresponding diff to packaged
76 // modules for that resource (if any)
77 private final Map<String, ResourceDiff> resDiff;
78 private final boolean errorOnModifiedFile;
79 private final TaskHelper taskHelper;
80 private final Set<String> upgradeableFiles;
81
82 /**
83 * JRTArchive constructor
84 *
85 * @param module The module name this archive refers to
86 * @param path The JRT filesystem path.
87 * @param errorOnModifiedFile Whether or not modified files of the JDK
88 * install aborts the link.
89 * @param perModDiff The lib/modules (a.k.a jimage) diff for this module,
90 * possibly an empty list if there are no differences.
91 * @param taskHelper The task helper instance.
92 * @param upgradeableFiles The set of files that are allowed for upgrades.
93 */
94 JRTArchive(String module,
95 Path path,
96 boolean errorOnModifiedFile,
97 List<ResourceDiff> perModDiff,
98 TaskHelper taskHelper,
99 Set<String> upgradeableFiles) {
100 this.module = module;
101 this.path = path;
102 this.ref = ModuleFinder.ofSystem()
103 .find(module)
104 .orElseThrow(() ->
105 new IllegalArgumentException(
106 "Module " + module +
107 " not part of the JDK install"));
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 void collectFiles() throws IOException {
163 if (files.isEmpty()) {
164 addNonClassResources();
165 // Add classes/resources from the run-time image,
166 // patched with the run-time image diff
167 files.addAll(ref.open().list()
168 .filter(i -> {
169 String lookupKey = String.format("/%s/%s", module, i);
170 ResourceDiff rd = resDiff.get(lookupKey);
171 // Filter all resources with a resource diff
172 // that are of kind MODIFIED.
173 // Note that REMOVED won't happen since in
174 // that case the module listing won't have
175 // the resource anyway.
176 // Note as well that filter removes files
177 // of kind ADDED. Those files are not in
178 // the packaged modules, so ought not to
179 // get returned from the pipeline.
180 return (rd == null ||
181 rd.getKind() == ResourceDiff.Kind.MODIFIED);
182 })
183 .map(s -> {
184 String lookupKey = String.format("/%s/%s", module, s);
185 return new JRTArchiveFile(JRTArchive.this, s,
186 EntryType.CLASS_OR_RESOURCE,
187 null /* hashOrTarget */,
188 false /* symlink */,
189 resDiff.get(lookupKey));
190 })
191 .toList());
192 // Finally add all files only present in the resource diff
193 // That is, removed items in the run-time image.
194 files.addAll(resDiff.values().stream()
195 .filter(rd -> rd.getKind() == ResourceDiff.Kind.REMOVED)
196 .map(s -> {
197 int secondSlash = s.getName().indexOf("/", 1);
198 assert secondSlash != -1;
199 String pathWithoutModule = s.getName().substring(secondSlash + 1);
200 return new JRTArchiveFile(JRTArchive.this,
201 pathWithoutModule,
202 EntryType.CLASS_OR_RESOURCE,
203 null /* hashOrTarget */,
204 false /* symlink */,
205 s);
206 })
207 .toList());
208 }
209 }
210
211 /*
212 * no need to keep track of the warning produced since this is eagerly
213 * checked once.
214 */
215 private void addNonClassResources() {
216 // Not all modules will have other resources like bin, lib, legal etc.
217 // files. In that case the list will be empty.
218 if (!otherRes.isEmpty()) {
219 files.addAll(otherRes.stream()
220 .filter(Predicate.not(String::isEmpty))
221 .map(s -> {
222 ResourceFileEntry m = ResourceFileEntry.decodeFromString(s);
223
224 // Read from the base JDK image.
225 Path path = BASE.resolve(m.resPath);
226 if (!isUpgradeableFile(m.resPath) &&
227 shaSumMismatch(path, m.hashOrTarget, m.symlink)) {
228 if (errorOnModifiedFile) {
229 String msg = taskHelper.getMessage("err.runtime.link.modified.file", path.toString());
230 IOException cause = new IOException(msg);
231 throw new UncheckedIOException(cause);
232 } else {
233 taskHelper.warning("err.runtime.link.modified.file", path.toString());
234 }
235 }
236
237 return new JRTArchiveFile(JRTArchive.this,
238 m.resPath,
239 toEntryType(m.resType),
240 m.hashOrTarget,
241 m.symlink,
242 /* diff only for resources */
243 null);
244 })
245 .toList());
246 }
247 }
248
249 /**
250 * Certain files in a module are considered upgradeable. That is,
251 * their hash sums aren't checked.
252 *
253 * @param resPath The resource path of the file to check for upgradeability.
254 * @return {@code true} if the file is upgradeable. {@code false} otherwise.
255 */
256 private boolean isUpgradeableFile(String resPath) {
257 return upgradeableFiles.contains(resPath);
258 }
259
260 static boolean shaSumMismatch(Path res, String expectedSha, boolean isSymlink) {
261 if (isSymlink) {
262 return false;
263 }
264 // handle non-symlink resources
265 try {
266 HexFormat format = HexFormat.of();
267 byte[] expected = format.parseHex(expectedSha);
268 MessageDigest digest = MessageDigest.getInstance("SHA-512");
269 try (InputStream is = Files.newInputStream(res)) {
270 byte[] buf = new byte[1024];
271 int readBytes = -1;
272 while ((readBytes = is.read(buf)) != -1) {
273 digest.update(buf, 0, readBytes);
274 }
275 }
276 byte[] actual = digest.digest();
277 return !MessageDigest.isEqual(expected, actual);
278 } catch (Exception e) {
279 throw new AssertionError("SHA-512 sum check failed!", e);
280 }
281 }
282
283 private static EntryType toEntryType(Type input) {
284 return switch(input) {
285 case CLASS_OR_RESOURCE -> EntryType.CLASS_OR_RESOURCE;
286 case CONFIG -> EntryType.CONFIG;
287 case HEADER_FILE -> EntryType.HEADER_FILE;
288 case LEGAL_NOTICE -> EntryType.LEGAL_NOTICE;
289 case MAN_PAGE -> EntryType.MAN_PAGE;
290 case NATIVE_CMD -> EntryType.NATIVE_CMD;
291 case NATIVE_LIB -> EntryType.NATIVE_LIB;
292 case TOP -> throw new IllegalArgumentException(
293 "TOP files should be handled by ReleaseInfoPlugin!");
294 default -> throw new IllegalArgumentException("Unknown type: " + input);
295 };
296 }
297
298 public record ResourceFileEntry(Type resType,
299 boolean symlink,
300 String hashOrTarget,
301 String resPath) {
302 // Type file format:
303 // '<type>|{0,1}|<sha-sum>|<file-path>'
304 // (1) (2) (3) (4)
305 //
306 // Where fields are:
307 //
308 // (1) The resource type as specified by ResourcePoolEntry.type()
309 // (2) Symlink designator. 0 => regular resource, 1 => symlinked resource
310 // (3) The SHA-512 sum of the resources' content. The link to the target
311 // for symlinked resources.
312 // (4) The relative file path of the resource
313 private static final String TYPE_FILE_FORMAT = "%d|%d|%s|%s";
314
315 private static final Map<Integer, Type> typeMap = Arrays.stream(Type.values())
316 .collect(Collectors.toMap(Type::ordinal, Function.identity()));
317
318 public String encodeToString() {
319 return String.format(TYPE_FILE_FORMAT,
320 resType.ordinal(),
321 symlink ? 1 : 0,
322 hashOrTarget,
323 resPath);
324 }
325
326 /**
327 * line: <int>|<int>|<hashOrTarget>|<path>
328 *
329 * Take the integer before '|' convert it to a Type. The second
330 * token is an integer representing symlinks (or not). The third token is
331 * a hash sum (sha512) of the file denoted by the fourth token (path).
332 */
333 static ResourceFileEntry decodeFromString(String line) {
334 assert !line.isEmpty();
335
336 String[] tokens = line.split("\\|", 4);
337 Type type = null;
338 int symlinkNum = -1;
339 try {
340 Integer typeInt = Integer.valueOf(tokens[0]);
341 type = typeMap.get(typeInt);
342 if (type == null) {
343 throw new AssertionError("Illegal type ordinal: " + typeInt);
344 }
345 symlinkNum = Integer.valueOf(tokens[1]);
346 } catch (NumberFormatException e) {
347 throw new AssertionError(e); // must not happen
348 }
349 if (symlinkNum < 0 || symlinkNum > 1) {
350 throw new AssertionError(
351 "Symlink designator out of range [0,1] got: " +
352 symlinkNum);
353 }
354 return new ResourceFileEntry(type,
355 symlinkNum == 1,
356 tokens[2] /* hash or target */,
357 tokens[3] /* resource path */);
358 }
359
360 public static ResourceFileEntry toResourceFileEntry(ResourcePoolEntry entry,
361 Platform platform) {
362 String resPathWithoutMod = dropModuleFromPath(entry, platform);
363 // Symlinks don't have a hash sum, but a link to the target instead
364 String hashOrTarget = entry.linkedTarget() == null
365 ? computeSha512(entry)
366 : dropModuleFromPath(entry.linkedTarget(),
367 platform);
368 return new ResourceFileEntry(entry.type(),
369 entry.linkedTarget() != null,
370 hashOrTarget,
371 resPathWithoutMod);
372 }
373
374 private static String computeSha512(ResourcePoolEntry entry) {
375 try {
376 assert entry.linkedTarget() == null;
377 MessageDigest digest = MessageDigest.getInstance("SHA-512");
378 try (InputStream is = entry.content()) {
379 byte[] buf = new byte[1024];
380 int bytesRead = -1;
381 while ((bytesRead = is.read(buf)) != -1) {
382 digest.update(buf, 0, bytesRead);
383 }
384 }
385 byte[] db = digest.digest();
386 HexFormat format = HexFormat.of();
387 return format.formatHex(db);
388 } catch (Exception e) {
389 throw new AssertionError("Failed to generate hash sum for " +
390 entry.path());
391 }
392 }
393
394 private static String dropModuleFromPath(ResourcePoolEntry entry,
395 Platform platform) {
396 String resPath = entry.path()
397 .substring(
398 // + 2 => prefixed and suffixed '/'
399 // For example: '/java.base/'
400 entry.moduleName().length() + 2);
401 if (!isWindows(platform)) {
402 return resPath;
403 }
404 // For Windows the libraries live in the 'bin' folder rather than
405 // the 'lib' folder in the final image. Note that going by the
406 // NATIVE_LIB type only is insufficient since only files with suffix
407 // .dll/diz/map/pdb are transplanted to 'bin'.
408 // See: DefaultImageBuilder.nativeDir()
409 return nativeDir(entry, resPath);
410 }
411
412 private static boolean isWindows(Platform platform) {
413 return platform.os() == OperatingSystem.WINDOWS;
414 }
415
416 private static String nativeDir(ResourcePoolEntry entry, String resPath) {
417 if (entry.type() != ResourcePoolEntry.Type.NATIVE_LIB) {
418 return resPath;
419 }
420 // precondition: Native lib, windows platform
421 if (resPath.endsWith(".dll") || resPath.endsWith(".diz")
422 || resPath.endsWith(".pdb") || resPath.endsWith(".map")) {
423 if (resPath.startsWith(LIB_DIRNAME + "/")) {
424 return BIN_DIRNAME + "/" +
425 resPath.substring((LIB_DIRNAME + "/").length());
426 }
427 }
428 return resPath;
429 }
430 private static final String BIN_DIRNAME = "bin";
431 private static final String LIB_DIRNAME = "lib";
432 }
433
434 private static final Path BASE = Paths.get(System.getProperty("java.home"));
435
436 interface JRTFile {
437 Entry toEntry();
438 }
439
440 record JRTArchiveFile(Archive archive,
441 String resPath,
442 EntryType resType,
443 String sha,
444 boolean symlink,
445 ResourceDiff diff) implements JRTFile {
446 public Entry toEntry() {
447 return new Entry(archive,
448 String.format("/%s/%s",
449 archive.moduleName(),
450 resPath),
451 resPath,
452 resType) {
453 @Override
454 public long size() {
455 try {
456 if (resType != EntryType.CLASS_OR_RESOURCE) {
457 // Read from the base JDK image, special casing
458 // symlinks, which have the link target in the
459 // hashOrTarget field
460 if (symlink) {
461 return Files.size(BASE.resolve(sha));
462 }
463 return Files.size(BASE.resolve(resPath));
464 } else {
465 if (diff != null) {
466 // If the resource has a diff to the
467 // packaged modules, use the diff. Diffs of kind
468 // ADDED have been filtered out in collectFiles();
469 assert diff.getKind() != ResourceDiff.Kind.ADDED;
470 assert diff.getName().equals(String.format("/%s/%s",
471 archive.moduleName(),
472 resPath));
473 return diff.getResourceBytes().length;
474 }
475 // Read from the module image. This works, because
476 // the underlying base path is a JrtPath with the
477 // JrtFileSystem underneath which is able to handle
478 // this size query.
479 return Files.size(archive.getPath().resolve(resPath));
480 }
481 } catch (IOException e) {
482 throw new UncheckedIOException(e);
483 }
484 }
485
486 @Override
487 public InputStream stream() throws IOException {
488 if (resType != EntryType.CLASS_OR_RESOURCE) {
489 // Read from the base JDK image.
490 Path path = symlink ? BASE.resolve(sha) : BASE.resolve(resPath);
491 return Files.newInputStream(path);
492 } else {
493 // Read from the module image. Use the diff to the
494 // packaged modules if we have one. Diffs of kind
495 // ADDED have been filtered out in collectFiles();
496 if (diff != null) {
497 assert diff.getKind() != ResourceDiff.Kind.ADDED;
498 assert diff.getName().equals(String.format("/%s/%s",
499 archive.moduleName(),
500 resPath));
501 return new ByteArrayInputStream(diff.getResourceBytes());
502 }
503 String module = archive.moduleName();
504 ModuleReference mRef = ModuleFinder.ofSystem()
505 .find(module).orElseThrow();
506 return mRef.open().open(resPath).orElseThrow();
507 }
508 }
509
510 };
511 }
512 }
513
514 private static List<String> readModuleResourceFile(String modName) {
515 String resName = String.format(RESPATH_PATTERN, modName);
516 try {
517 try (InputStream inStream = JRTArchive.class.getModule()
518 .getResourceAsStream(resName)) {
519 String input = new String(inStream.readAllBytes(), StandardCharsets.UTF_8);
520 if (input.isEmpty()) {
521 // Not all modules have non-class resources
522 return Collections.emptyList();
523 } else {
524 return Arrays.asList(input.split("\n"));
525 }
526 }
527 } catch (IOException e) {
528 throw new UncheckedIOException("Failed to process resources from the " +
529 "run-time image for module " + modName, e);
530 }
531 }
532 }