8365467: Issues with jrtfs implementation for exploded run-time images

Reviewed-by: rriggs, sundar
This commit is contained in:
David Beaumont 2025-09-04 13:19:12 +00:00 committed by Roger Riggs
parent 80873a09bf
commit e190355777
5 changed files with 359 additions and 90 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -27,17 +27,15 @@ package jdk.internal.jrtfs;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
import jdk.internal.jimage.ImageReader.Node;
@ -56,16 +54,15 @@ class ExplodedImage extends SystemImage {
private static final String MODULES = "/modules/";
private static final String PACKAGES = "/packages/";
private static final int PACKAGES_LEN = PACKAGES.length();
private final FileSystem defaultFS;
private final Path modulesDir;
private final String separator;
private final Map<String, PathNode> nodes = Collections.synchronizedMap(new HashMap<>());
private final Map<String, PathNode> nodes = new HashMap<>();
private final BasicFileAttributes modulesDirAttrs;
ExplodedImage(Path modulesDir) throws IOException {
defaultFS = FileSystems.getDefault();
String str = defaultFS.getSeparator();
this.modulesDir = modulesDir;
String str = modulesDir.getFileSystem().getSeparator();
separator = str.equals("/") ? null : str;
modulesDirAttrs = Files.readAttributes(modulesDir, BasicFileAttributes.class);
initNodes();
@ -79,21 +76,26 @@ class ExplodedImage extends SystemImage {
private PathNode link;
private List<Node> children;
PathNode(String name, Path path, BasicFileAttributes attrs) { // path
private PathNode(String name, Path path, BasicFileAttributes attrs) { // path
super(name, attrs);
this.path = path;
}
PathNode(String name, Node link) { // link
private PathNode(String name, Node link) { // link
super(name, link.getFileAttributes());
this.link = (PathNode)link;
}
PathNode(String name, List<Node> children) { // dir
private PathNode(String name, List<Node> children) { // dir
super(name, modulesDirAttrs);
this.children = children;
}
@Override
public boolean isResource() {
return link == null && !getFileAttributes().isDirectory();
}
@Override
public boolean isDirectory() {
return children != null ||
@ -112,7 +114,7 @@ class ExplodedImage extends SystemImage {
return recursive && link.isLink() ? link.resolveLink(true) : link;
}
byte[] getContent() throws IOException {
private byte[] getContent() throws IOException {
if (!getFileAttributes().isRegularFile())
throw new FileSystemException(getName() + " is not file");
return Files.readAllBytes(path);
@ -126,7 +128,7 @@ class ExplodedImage extends SystemImage {
List<Node> list = new ArrayList<>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) {
for (Path p : stream) {
p = explodedModulesDir.relativize(p);
p = modulesDir.relativize(p);
String pName = MODULES + nativeSlashToFrontSlash(p.toString());
Node node = findNode(pName);
if (node != null) { // findNode may choose to hide certain files!
@ -152,7 +154,7 @@ class ExplodedImage extends SystemImage {
}
@Override
public void close() throws IOException {
public synchronized void close() throws IOException {
nodes.clear();
}
@ -161,72 +163,76 @@ class ExplodedImage extends SystemImage {
return ((PathNode)node).getContent();
}
// find Node for the given Path
@Override
public synchronized Node findNode(String str) {
Node node = findModulesNode(str);
public synchronized Node findNode(String name) {
PathNode node = nodes.get(name);
if (node != null) {
return node;
}
// lazily created for paths like /packages/<package>/<module>/xyz
// For example /packages/java.lang/java.base/java/lang/
if (str.startsWith(PACKAGES)) {
// pkgEndIdx marks end of <package> part
int pkgEndIdx = str.indexOf('/', PACKAGES_LEN);
if (pkgEndIdx != -1) {
// modEndIdx marks end of <module> part
int modEndIdx = str.indexOf('/', pkgEndIdx + 1);
if (modEndIdx != -1) {
// make sure we have such module link!
// ie., /packages/<package>/<module> is valid
Node linkNode = nodes.get(str.substring(0, modEndIdx));
if (linkNode == null || !linkNode.isLink()) {
return null;
}
// map to "/modules/zyz" path and return that node
// For example, "/modules/java.base/java/lang" for
// "/packages/java.lang/java.base/java/lang".
String mod = MODULES + str.substring(pkgEndIdx + 1);
return findModulesNode(mod);
// If null, this was not the name of "/modules/..." node, and since all
// "/packages/..." nodes were created and cached in advance, the name
// cannot reference a valid node.
Path path = underlyingModulesPath(name);
if (path == null) {
return null;
}
// This can still return null for hidden files.
return createModulesNode(name, path);
}
/**
* Lazily creates and caches a {@code Node} for the given "/modules/..." name
* and corresponding path to a file or directory.
*
* @param name a resource or directory node name, of the form "/modules/...".
* @param path the path of a file for a resource or directory.
* @return the newly created and cached node, or {@code null} if the given
* path references a file which must be hidden in the node hierarchy.
*/
private Node createModulesNode(String name, Path path) {
assert !nodes.containsKey(name) : "Node must not already exist: " + name;
assert isNonEmptyModulesPath(name) : "Invalid modules name: " + name;
try {
// We only know if we're creating a resource of directory when we
// look up file attributes, and we only do that once. Thus, we can
// only reject "marker files" here, rather than by inspecting the
// given name string, since it doesn't apply to directories.
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
if (attrs.isRegularFile()) {
Path f = path.getFileName();
if (f.toString().startsWith("_the.")) {
return null;
}
} else if (!attrs.isDirectory()) {
return null;
}
PathNode node = new PathNode(name, path, attrs);
nodes.put(name, node);
return node;
} catch (IOException x) {
// Since the path reference a file, any errors should not be ignored.
throw new UncheckedIOException(x);
}
}
/**
* Returns the expected file path for name in the "/modules/..." namespace,
* or {@code null} if the name is not in the "/modules/..." namespace or the
* path does not reference a file.
*/
private Path underlyingModulesPath(String name) {
if (isNonEmptyModulesPath(name)) {
Path path = modulesDir.resolve(frontSlashToNativeSlash(name.substring(MODULES.length())));
return Files.exists(path) ? path : null;
}
return null;
}
// find a Node for a path that starts like "/modules/..."
Node findModulesNode(String str) {
PathNode node = nodes.get(str);
if (node != null) {
return node;
}
// lazily created "/modules/xyz/abc/" Node
// This is mapped to default file system path "<JDK_MODULES_DIR>/xyz/abc"
Path p = underlyingPath(str);
if (p != null) {
try {
BasicFileAttributes attrs = Files.readAttributes(p, BasicFileAttributes.class);
if (attrs.isRegularFile()) {
Path f = p.getFileName();
if (f.toString().startsWith("_the."))
return null;
}
node = new PathNode(str, p, attrs);
nodes.put(str, node);
return node;
} catch (IOException x) {
// does not exists or unable to determine
}
}
return null;
}
Path underlyingPath(String str) {
if (str.startsWith(MODULES)) {
str = frontSlashToNativeSlash(str.substring("/modules".length()));
return defaultFS.getPath(explodedModulesDir.toString(), str);
}
return null;
private static boolean isNonEmptyModulesPath(String name) {
// Don't just check the prefix, there must be something after it too
// (otherwise you end up with an empty string after trimming).
return name.startsWith(MODULES) && name.length() > MODULES.length();
}
// convert "/" to platform path separator
@ -249,24 +255,21 @@ class ExplodedImage extends SystemImage {
// same package prefix may exist in multiple modules. This Map
// is filled by walking "jdk modules" directory recursively!
Map<String, List<String>> packageToModules = new HashMap<>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(explodedModulesDir)) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(modulesDir)) {
for (Path module : stream) {
if (Files.isDirectory(module)) {
String moduleName = module.getFileName().toString();
// make sure "/modules/<moduleName>" is created
findModulesNode(MODULES + moduleName);
Objects.requireNonNull(createModulesNode(MODULES + moduleName, module));
try (Stream<Path> contentsStream = Files.walk(module)) {
contentsStream.filter(Files::isDirectory).forEach((p) -> {
p = module.relativize(p);
String pkgName = slashesToDots(p.toString());
// skip META-INF and empty strings
if (!pkgName.isEmpty() && !pkgName.startsWith("META-INF")) {
List<String> moduleNames = packageToModules.get(pkgName);
if (moduleNames == null) {
moduleNames = new ArrayList<>();
packageToModules.put(pkgName, moduleNames);
}
moduleNames.add(moduleName);
packageToModules
.computeIfAbsent(pkgName, k -> new ArrayList<>())
.add(moduleName);
}
});
}
@ -275,8 +278,8 @@ class ExplodedImage extends SystemImage {
}
// create "/modules" directory
// "nodes" map contains only /modules/<foo> nodes only so far and so add all as children of /modules
PathNode modulesDir = new PathNode("/modules", new ArrayList<>(nodes.values()));
nodes.put(modulesDir.getName(), modulesDir);
PathNode modulesRootNode = new PathNode("/modules", new ArrayList<>(nodes.values()));
nodes.put(modulesRootNode.getName(), modulesRootNode);
// create children under "/packages"
List<Node> packagesChildren = new ArrayList<>(packageToModules.size());
@ -285,7 +288,7 @@ class ExplodedImage extends SystemImage {
List<String> moduleNameList = entry.getValue();
List<Node> moduleLinkNodes = new ArrayList<>(moduleNameList.size());
for (String moduleName : moduleNameList) {
Node moduleNode = findModulesNode(MODULES + moduleName);
Node moduleNode = Objects.requireNonNull(nodes.get(MODULES + moduleName));
PathNode linkNode = new PathNode(PACKAGES + pkgName + "/" + moduleName, moduleNode);
nodes.put(linkNode.getName(), linkNode);
moduleLinkNodes.add(linkNode);
@ -295,13 +298,13 @@ class ExplodedImage extends SystemImage {
packagesChildren.add(pkgDir);
}
// "/packages" dir
PathNode packagesDir = new PathNode("/packages", packagesChildren);
nodes.put(packagesDir.getName(), packagesDir);
PathNode packagesRootNode = new PathNode("/packages", packagesChildren);
nodes.put(packagesRootNode.getName(), packagesRootNode);
// finally "/" dir!
List<Node> rootChildren = new ArrayList<>();
rootChildren.add(packagesDir);
rootChildren.add(modulesDir);
rootChildren.add(packagesRootNode);
rootChildren.add(modulesRootNode);
PathNode root = new PathNode("/", rootChildren);
nodes.put(root.getName(), root);
}

View File

@ -78,13 +78,13 @@ abstract class SystemImage {
return new ExplodedImage(explodedModulesDir);
}
static final String RUNTIME_HOME;
private static final String RUNTIME_HOME;
// "modules" jimage file Path
static final Path moduleImageFile;
private static final Path moduleImageFile;
// "modules" jimage exists or not?
static final boolean modulesImageExists;
private static final boolean modulesImageExists;
// <JAVA_HOME>/modules directory Path
static final Path explodedModulesDir;
private static final Path explodedModulesDir;
static {
PrivilegedAction<String> pa = SystemImage::findHome;

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* @test
* @summary Whitebox tests for ExplodedImage to ensure compatibility with ImageReader.
* @modules java.base/jdk.internal.jrtfs java.base/jdk.internal.jimage
* @run junit/othervm java.base/jdk.internal.jrtfs.ExplodedImageTest
*/
public class ExplodedImageTestDriver {}

View File

@ -0,0 +1,4 @@
modules = \
java.base/jdk.internal.jimage \
java.base/jdk.internal.jrtfs
bootclasspath.dirs=.

View File

@ -0,0 +1,232 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.internal.jrtfs;
import jdk.internal.jimage.ImageReader;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.io.IOException;
import java.net.URI;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Tests an {@link ExplodedImage} view of a class-file hierarchy.
*
* <p>For simplicity and performance, only a subset of the JRT files are copied
* to disk for testing.
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class ExplodedImageTest {
private Path modulesRoot;
private SystemImage explodedImage;
private String pathSeparator;
@BeforeAll
public void createTestDirectory(@TempDir Path modulesRoot) throws IOException {
this.modulesRoot = modulesRoot;
this.pathSeparator = modulesRoot.getFileSystem().getSeparator();
// Copy only a useful subset of files for testing. Use at least two
// modules with "overlapping" packages to test /package links better.
unpackModulesDirectoriesFromJrtFileSystem(modulesRoot,
"java.base/java/util",
"java.base/java/util/zip",
"java.logging/java/util/logging");
this.explodedImage = new ExplodedImage(modulesRoot);
}
/** Unpacks a list of "/modules/..." directories non-recursively into the specified root directory. */
private static void unpackModulesDirectoriesFromJrtFileSystem(Path modulesRoot, String... dirNames)
throws IOException {
FileSystem jrtfs = FileSystems.getFileSystem(URI.create("jrt:/"));
List<Path> srcDirs = Arrays.stream(dirNames).map(s -> "/modules/" + s).map(jrtfs::getPath).toList();
for (Path srcDir : srcDirs) {
// Skip-1 to remove "modules" segment (not part of the file system path).
Path dstDir = StreamSupport.stream(srcDir.spliterator(), false)
.skip(1)
.reduce(modulesRoot, (path, segment) -> path.resolve(segment.toString()));
Files.createDirectories(dstDir);
try (DirectoryStream<Path> files = Files.newDirectoryStream(srcDir)) {
for (Path srcFile : files) {
Files.copy(srcFile, dstDir.resolve(srcFile.getFileName().toString()));
}
}
}
}
@Test
public void topLevelNodes() throws IOException {
ImageReader.Node root = explodedImage.findNode("/");
ImageReader.Node modules = explodedImage.findNode("/modules");
ImageReader.Node packages = explodedImage.findNode("/packages");
assertEquals(
Set.of(modules.getName(), packages.getName()),
root.getChildNames().collect(Collectors.toSet()));
}
@ParameterizedTest
@ValueSource(strings = {
"/modules/java.base/java/util/List.class",
"/modules/java.base/java/util/zip/ZipEntry.class",
"/modules/java.logging/java/util/logging/Logger.class"})
public void basicLookupResource(String expectedResourceName) throws IOException {
ImageReader.Node node = assertResourceNode(expectedResourceName);
Path fsRelPath = getRelativePath(expectedResourceName);
assertArrayEquals(
Files.readAllBytes(modulesRoot.resolve(fsRelPath)),
explodedImage.getResource(node));
}
@ParameterizedTest
@ValueSource(strings = {
"/modules/java.base",
"/modules/java.logging",
"/modules/java.base/java",
"/modules/java.base/java/util",
"/modules/java.logging/java/util",
})
public void basicLookupDirectory(String expectedDirectoryName) throws IOException {
ImageReader.Node node = assertDirectoryNode(expectedDirectoryName);
Path fsRelPath = getRelativePath(expectedDirectoryName);
List<String> fsChildBaseNames;
try (DirectoryStream<Path> paths = Files.newDirectoryStream(modulesRoot.resolve(fsRelPath))) {
fsChildBaseNames = StreamSupport.stream(paths.spliterator(), false)
.map(Path::getFileName)
.map(Path::toString)
.toList();
}
List<String> nodeChildBaseNames = node.getChildNames()
.map(s -> s.substring(node.getName().length() + 1))
.toList();
assertEquals(fsChildBaseNames, nodeChildBaseNames, "expected same child names");
}
@ParameterizedTest
@ValueSource(strings = {
"/packages/java/java.base",
"/packages/java/java.logging",
"/packages/java.util/java.base",
"/packages/java.util/java.logging",
"/packages/java.util.zip/java.base"})
public void basicLookupPackageLinks(String expectedLinkName) throws IOException {
ImageReader.Node node = assertLinkNode(expectedLinkName);
ImageReader.Node resolved = node.resolveLink();
assertSame(explodedImage.findNode(resolved.getName()), resolved);
String moduleName = expectedLinkName.substring(expectedLinkName.lastIndexOf('/') + 1);
assertEquals("/modules/" + moduleName, resolved.getName());
}
@ParameterizedTest
@ValueSource(strings = {
"/packages/java",
"/packages/java.util",
"/packages/java.util.zip"})
public void packageDirectories(String expectedDirectoryName) throws IOException {
ImageReader.Node node = assertDirectoryNode(expectedDirectoryName);
assertTrue(node.getChildNames().findFirst().isPresent(),
"Package directories should not be empty: " + node);
}
@ParameterizedTest
@ValueSource(strings = {
"",
".",
"/.",
"modules",
"packages",
"/modules/",
"/modules/xxxx",
"/modules/java.base/java/lang/Xxxx.class",
"/packages/",
"/packages/xxxx",
"/packages/java.xxxx",
"/packages/java.util.",
// Mismatched module.
"/packages/java.util.logging/java.base",
"/packages/java.util.zip/java.logging",
// Links are not resolved as they are fetched (old/broken behaviour).
"/packages/java.util/java.base/java/util/Vector.class",
})
public void invalidNames(String invalidName) throws IOException {
assertNull(explodedImage.findNode(invalidName), "No node expected for: " + invalidName);
}
private ImageReader.Node assertResourceNode(String name) throws IOException {
ImageReader.Node node = explodedImage.findNode(name);
assertNotNull(node);
assertEquals(name, node.getName(), "expected node name: " + name);
assertTrue(node.isResource(), "expected a resource: " + node);
assertFalse(node.isDirectory(), "resources are not directories: " + node);
assertFalse(node.isLink(), "resources are not links: " + node);
return node;
}
private ImageReader.Node assertDirectoryNode(String name) throws IOException {
ImageReader.Node node = explodedImage.findNode(name);
assertNotNull(node);
assertEquals(name, node.getName(), "expected node name: " + name);
assertTrue(node.isDirectory(), "expected a directory: " + node);
assertFalse(node.isResource(), "directories are not resources: " + node);
assertFalse(node.isLink(), "directories are not links: " + node);
return node;
}
private ImageReader.Node assertLinkNode(String name) throws IOException {
ImageReader.Node node = explodedImage.findNode(name);
assertNotNull(node);
assertEquals(name, node.getName(), "expected node name: " + name);
assertTrue(node.isLink(), "expected a link: " + node);
assertFalse(node.isResource(), "links are not resources: " + node);
assertFalse(node.isDirectory(), "links are not directories: " + node);
return node;
}
private Path getRelativePath(String name) {
return Path.of(name.substring("/modules/".length()).replace("/", pathSeparator));
}
}