Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 135 additions & 66 deletions src/java.base/share/classes/jdk/internal/jrtfs/ExplodedImage.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,24 @@
*/
package jdk.internal.jrtfs;

import jdk.internal.jimage.ImageReader.Node;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystemException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import jdk.internal.jimage.ImageReader.Node;

/**
* A jrt file system built on $JAVA_HOME/modules directory ('exploded modules
* build')
Expand All @@ -54,14 +56,18 @@ class ExplodedImage extends SystemImage {

private static final String MODULES = "/modules/";
private static final String PACKAGES = "/packages/";
private static final Path META_INF_DIR = Paths.get("META-INF");
private static final Path PREVIEW_DIR = META_INF_DIR.resolve("preview");

private final Path modulesDir;
private final boolean isPreviewMode;
private final String separator;
private final Map<String, PathNode> nodes = new HashMap<>();
private final BasicFileAttributes modulesDirAttrs;

ExplodedImage(Path modulesDir) throws IOException {
ExplodedImage(Path modulesDir, boolean isPreviewMode) throws IOException {
this.modulesDir = modulesDir;
this.isPreviewMode = isPreviewMode;
String str = modulesDir.getFileSystem().getSeparator();
separator = str.equals("/") ? null : str;
modulesDirAttrs = Files.readAttributes(modulesDir, BasicFileAttributes.class);
Expand All @@ -70,23 +76,26 @@ class ExplodedImage extends SystemImage {

// A Node that is backed by actual default file system Path
private final class PathNode extends Node {

// Path in underlying default file system
private Path path;
// Path in underlying default file system relative to modulesDir.
// In preview mode this need not correspond to the node's name.
private Path relPath;
private PathNode link;
private List<Node> children;
private List<PathNode> children;

private PathNode(String name, Path path, BasicFileAttributes attrs) { // path
super(name, attrs);
this.path = path;
this.relPath = modulesDir.relativize(path);
if (relPath.isAbsolute() || relPath.getNameCount() == 0) {
throw new IllegalArgumentException("Invalid node path (must be relative): " + path);
}
}

private PathNode(String name, Node link) { // link
super(name, link.getFileAttributes());
this.link = (PathNode)link;
}

private PathNode(String name, List<Node> children) { // dir
private PathNode(String name, List<PathNode> children) { // dir
super(name, modulesDirAttrs);
this.children = children;
}
Expand Down Expand Up @@ -117,36 +126,55 @@ public PathNode resolveLink(boolean recursive) {
private byte[] getContent() throws IOException {
if (!getFileAttributes().isRegularFile())
throw new FileSystemException(getName() + " is not file");
return Files.readAllBytes(path);
return Files.readAllBytes(modulesDir.resolve(relPath));
}

@Override
public Stream<String> getChildNames() {
if (!isDirectory())
throw new IllegalArgumentException("not a directory: " + getName());
if (children == null) {
List<Node> list = new ArrayList<>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) {
for (Path p : stream) {
p = modulesDir.relativize(p);
String pName = MODULES + nativeSlashToFrontSlash(p.toString());
Node node = findNode(pName);
if (node != null) { // findNode may choose to hide certain files!
list.add(node);
}
throw new IllegalStateException("not a directory: " + getName());
List<PathNode> childNodes = children;
if (childNodes == null) {
childNodes = completeDirectory();
}
return childNodes.stream().map(Node::getName);
}

private synchronized List<PathNode> completeDirectory() {
if (children != null) {
return children;
}
List<PathNode> list = new ArrayList<>();
if (relPath.getNameCount() > 1 && !relPath.getName(1).equals(META_INF_DIR)) {
Path absPreviewDir = modulesDir
.resolve(relPath.getName(0))
.resolve(PREVIEW_DIR)
.resolve(relPath.subpath(1, relPath.getNameCount()));
if (Files.exists(absPreviewDir)) {
collectChildNodes(absPreviewDir, list);
}
}
collectChildNodes(modulesDir.resolve(relPath), list);
return children = list;
}

private void collectChildNodes(Path absPath, List<PathNode> list) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(absPath)) {
for (Path p : stream) {
PathNode node = (PathNode) findNode(getName() + "/" + p.getFileName().toString());
if (node != null) { // findNode may choose to hide certain files!
list.add(node);
}
} catch (IOException x) {
return null;
}
children = list;
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
return children.stream().map(Node::getName);
}

@Override
public long size() {
try {
return isDirectory() ? 0 : Files.size(path);
return isDirectory() ? 0 : Files.size(modulesDir.resolve(relPath));
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
Expand Down Expand Up @@ -180,6 +208,42 @@ public synchronized Node findNode(String name) {
return createModulesNode(name, path);
}

/**
* 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 (!isNonEmptyModulesName(name)) {
return null;
}
String relName = name.substring(MODULES.length());
Path relPath = Paths.get(frontSlashToNativeSlash(relName));
// The first path segment must exist due to check above.
Path modDir = relPath.getName(0);
Path previewDir = modDir.resolve(PREVIEW_DIR);
if (relPath.startsWith(previewDir)) {
return null;
}
Path path = modulesDir.resolve(relPath);
// Non-preview directories take precedence.
if (Files.isDirectory(path)) {
return path;
}
// Otherwise prefer preview resources over non-preview ones.
if (isPreviewMode
&& relPath.getNameCount() > 1
&& !modDir.equals(META_INF_DIR)) {
Path previewPath = modulesDir
.resolve(previewDir)
.resolve(relPath.subpath(1, relPath.getNameCount()));
if (Files.exists(previewPath)) {
return previewPath;
}
}
return Files.exists(path) ? path : null;
}

/**
* Lazily creates and caches a {@code Node} for the given "/modules/..." name
* and corresponding path to a file or directory.
Expand All @@ -191,7 +255,7 @@ public synchronized Node findNode(String name) {
*/
private Node createModulesNode(String name, Path path) {
assert !nodes.containsKey(name) : "Node must not already exist: " + name;
assert isNonEmptyModulesPath(name) : "Invalid modules name: " + name;
assert isNonEmptyModulesName(name) : "Invalid modules name: " + name;

try {
// We only know if we're creating a resource of directory when we
Expand All @@ -216,35 +280,24 @@ private Node createModulesNode(String name, Path path) {
}
}

/**
* 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;
}
private static final Pattern NON_EMPTY_MODULES_NAME =
Pattern.compile("/modules(/[^/]+)+");

private static boolean isNonEmptyModulesPath(String name) {
private static boolean isNonEmptyModulesName(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();
// Also make sure we can't be tricked by "/modules//absolute/path" or
// "/modules/../../escaped/path"
return NON_EMPTY_MODULES_NAME.matcher(name).matches()
&& !name.contains("/../")
&& !name.contains("/./");
}

// convert "/" to platform path separator
private String frontSlashToNativeSlash(String str) {
return separator == null ? str : str.replace("/", separator);
}

// convert platform path separator to "/"
private String nativeSlashToFrontSlash(String str) {
return separator == null ? str : str.replace(separator, "/");
}

// convert "/"s to "."s
private String slashesToDots(String str) {
return str.replace(separator != null ? separator : "/", ".");
Expand All @@ -256,23 +309,9 @@ private void initNodes() throws IOException {
// is filled by walking "jdk modules" directory recursively!
Map<String, List<String>> packageToModules = new HashMap<>();
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
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")) {
packageToModules
.computeIfAbsent(pkgName, k -> new ArrayList<>())
.add(moduleName);
}
});
}
for (Path moduleDir : stream) {
if (Files.isDirectory(moduleDir)) {
processModuleDirectory(moduleDir, packageToModules);
}
}
}
Expand All @@ -282,11 +321,11 @@ private void initNodes() throws IOException {
nodes.put(modulesRootNode.getName(), modulesRootNode);

// create children under "/packages"
List<Node> packagesChildren = new ArrayList<>(packageToModules.size());
List<PathNode> packagesChildren = new ArrayList<>(packageToModules.size());
for (Map.Entry<String, List<String>> entry : packageToModules.entrySet()) {
String pkgName = entry.getKey();
List<String> moduleNameList = entry.getValue();
List<Node> moduleLinkNodes = new ArrayList<>(moduleNameList.size());
List<PathNode> moduleLinkNodes = new ArrayList<>(moduleNameList.size());
for (String moduleName : moduleNameList) {
Node moduleNode = Objects.requireNonNull(nodes.get(MODULES + moduleName));
PathNode linkNode = new PathNode(PACKAGES + pkgName + "/" + moduleName, moduleNode);
Expand All @@ -302,10 +341,40 @@ private void initNodes() throws IOException {
nodes.put(packagesRootNode.getName(), packagesRootNode);

// finally "/" dir!
List<Node> rootChildren = new ArrayList<>();
List<PathNode> rootChildren = new ArrayList<>();
rootChildren.add(packagesRootNode);
rootChildren.add(modulesRootNode);
PathNode root = new PathNode("/", rootChildren);
nodes.put(root.getName(), root);
}

private void processModuleDirectory(Path moduleDir, Map<String, List<String>> packageToModules)
throws IOException {
String moduleName = moduleDir.getFileName().toString();
// Make sure "/modules/<moduleName>" is created
Objects.requireNonNull(createModulesNode(MODULES + moduleName, moduleDir));
// Skip the first path (it's always the given root directory).
try (Stream<Path> contentsStream = Files.walk(moduleDir).skip(1)) {
contentsStream
// Non-empty relative directory paths inside each module.
.filter(Files::isDirectory)
.map(moduleDir::relativize)
// Map paths inside preview directory to non-preview versions.
.filter(p -> isPreviewMode || !p.startsWith(PREVIEW_DIR))
.map(p -> isPreviewSubpath(p) ? PREVIEW_DIR.relativize(p) : p)
// Ignore special META-INF directory (including preview directory itself).
.filter(p -> !p.startsWith(META_INF_DIR))
// Extract unique package names.
.map(p -> slashesToDots(p.toString()))
.distinct()
.forEach(pkgName ->
packageToModules
.computeIfAbsent(pkgName, k -> new ArrayList<>())
.add(moduleName));
}
}

private static boolean isPreviewSubpath(Path p) {
return p.startsWith(PREVIEW_DIR) && p.getNameCount() > PREVIEW_DIR.getNameCount();
}
}
Loading