< prev index next >

src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageResourcesTree.java

Print this page
@@ -22,37 +22,38 @@
   * or visit www.oracle.com if you need additional information or have any
   * questions.
   */
  package jdk.tools.jlink.internal;
  
+ import jdk.internal.jimage.ImageLocation;
+ import jdk.internal.jimage.ModuleReference;
+ 
  import java.io.DataOutputStream;
  import java.io.IOException;
  import java.nio.ByteBuffer;
  import java.util.ArrayList;
  import java.util.Collections;
+ import java.util.Comparator;
  import java.util.HashMap;
+ import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Objects;
  import java.util.Set;
  import java.util.TreeMap;
- import java.util.TreeSet;
+ import java.util.stream.Collectors;
  
  /**
   * A class to build a sorted tree of Resource paths as a tree of ImageLocation.
-  *
   */
  // XXX Public only due to the JImageTask / JImageTask code duplication
  public final class ImageResourcesTree {
-     public static boolean isTreeInfoResource(String path) {
-         return path.startsWith("/packages") || path.startsWith("/modules");
-     }
- 
      /**
       * Path item tree node.
       */
-     private static class Node {
+     // Visible for testing only.
+     static class Node {
  
          private final String name;
          private final Map<String, Node> children = new TreeMap<>();
          private final Node parent;
          private ImageLocationWriter loc;

@@ -64,10 +65,18 @@
              if (parent != null) {
                  parent.children.put(name, this);
              }
          }
  
+         private void setLocation(ImageLocationWriter loc) {
+             // This *can* be called more than once, but only with the same instance.
+             if (this.loc != null && loc != this.loc) {
+                 throw new IllegalStateException("Cannot add different locations: " + name);
+             }
+             this.loc = Objects.requireNonNull(loc);
+         }
+ 
          public String getPath() {
              if (parent == null) {
                  return "/";
              }
              return buildPath(this);

@@ -93,219 +102,210 @@
                  return path + "/" + item.getName();
              }
          }
      }
  
-     private static final class ResourceNode extends Node {
+     // Visible for testing only.
+     static final class ResourceNode extends Node {
  
          public ResourceNode(String name, Node parent) {
              super(name, parent);
          }
      }
  
-     private static class PackageNode extends Node {
-         /**
-          * A reference to a package. Empty packages can be located inside one or
-          * more modules. A package with classes exist in only one module.
-          */
-         static final class PackageReference {
- 
-             private final String name;
-             private final boolean isEmpty;
+     /**
+      * A 2nd level package directory, {@code "/packages/<package-name>"}.
+      *
+      * <p>While package paths can exist within many modules, for each package
+      * there is at most one module in which that package has resources.
+      *
+      * <p>For example, the package path {@code java/util} exists in both the
+      * {@code java.base} and {@code java.logging} modules. This means both
+      * {@code "/packages/java.util/java.base"} and
+      * {@code "/packages/java.util/java.logging"} will exist, but only
+      * {@code "java.base"} entry will be marked as having content.
+      *
+      * <p>When processing module references in non-preview mode, entries marked
+      * as {@link ModuleReference#isPreviewOnly() preview-only} must be ignored.
+      *
+      * <p>If all references in a package are preview-only, then the entire
+      * package is marked as preview-only, and must be ignored.
+      */
+     // Visible for testing only.
+     static final class PackageNode extends Node {
+         private final List<ModuleReference> moduleReferences;
  
-             PackageReference(String name, boolean isEmpty) {
-                 this.name = Objects.requireNonNull(name);
-                 this.isEmpty = isEmpty;
+         PackageNode(String name, List<ModuleReference> moduleReferences, Node parent) {
+             super(name, parent);
+             if (moduleReferences.isEmpty()) {
+                 throw new IllegalStateException("Package must be associated with modules: " + name);
              }
- 
-             @Override
-             public String toString() {
-                 return name + "[empty:" + isEmpty + "]";
+             if (moduleReferences.stream().filter(ModuleReference::hasResources).count() > 1) {
+                 throw new IllegalStateException("Multiple modules contain non-empty package: " + name);
              }
+             this.moduleReferences = Collections.unmodifiableList(moduleReferences);
          }
  
-         private final Map<String, PackageReference> references = new TreeMap<>();
- 
-         PackageNode(String name, Node parent) {
-             super(name, parent);
-         }
- 
-         private void addReference(String name, boolean isEmpty) {
-             PackageReference ref = references.get(name);
-             if (ref == null || ref.isEmpty) {
-                 references.put(name, new PackageReference(name, isEmpty));
-             }
+         List<ModuleReference> getModuleReferences() {
+             return moduleReferences;
          }
+     }
  
-         private void validate() {
-             boolean exists = false;
-             for (PackageReference ref : references.values()) {
-                 if (!ref.isEmpty) {
-                     if (exists) {
-                         throw new RuntimeException("Multiple modules to contain package "
-                                 + getName());
-                     } else {
-                         exists = true;
-                     }
-                 }
-             }
+     // Not serialized, and never stored in any field of any class that is.
+     @SuppressWarnings("serial")
+     private static final class InvalidTreeException extends Exception {
+         public InvalidTreeException(Node badNode) {
+             super("Resources tree, invalid data structure, skipping: " + badNode.getPath());
          }
+         // Exception only used for program flow, not debugging.
+         @Override
+         public Throwable fillInStackTrace() {return this;}
      }
  
      /**
       * Tree of nodes.
       */
-     private static final class Tree {
+     // Visible for testing only.
+     static final class Tree {
+         private static final String PREVIEW_PREFIX = "META-INF/preview/";
  
          private final Map<String, Node> directAccess = new HashMap<>();
          private final List<String> paths;
          private final Node root;
-         private Node modules;
-         private Node packages;
+         private Node packagesRoot;
  
-         private Tree(List<String> paths) {
-             this.paths = paths;
+         // Visible for testing only.
+         Tree(List<String> paths) {
+             this.paths = paths.stream().sorted(Comparator.reverseOrder()).toList();
+             // Root node is not added to the directAccess map.
              root = new Node("", null);
              buildTree();
          }
  
          private void buildTree() {
-             modules = new Node("modules", root);
-             directAccess.put(modules.getPath(), modules);
- 
-             Map<String, Set<String>> moduleToPackage = new TreeMap<>();
-             Map<String, Set<String>> packageToModule = new TreeMap<>();
- 
-             for (String p : paths) {
-                 if (!p.startsWith("/")) {
-                     continue;
-                 }
-                 String[] split = p.split("/");
-                 // minimum length is 3 items: /<mod>/<pkg>
-                 if (split.length < 3) {
-                     System.err.println("Resources tree, invalid data structure, "
-                             + "skipping " + p);
-                     continue;
-                 }
-                 Node current = modules;
-                 String module = null;
-                 for (int i = 0; i < split.length; i++) {
-                     // When a non terminal node is marked as being a resource, something is wrong.
+             Node modulesRoot = new Node("modules", root);
+             directAccess.put(modulesRoot.getPath(), modulesRoot);
+             packagesRoot = new Node("packages", root);
+             directAccess.put(packagesRoot.getPath(), packagesRoot);
+ 
+             // Map of dot-separated package names to module references (those
+             // in which the package appear). References are merged after to
+             // ensure each module name appears only once, but temporarily a
+             // module may have several entries per package (e.g. with-content,
+             // without-content, normal, preview-only etc..).
+             Map<String, Set<ModuleReference>> packageToModules = new TreeMap<>();
+             for (String fullPath : paths) {
+                 try {
+                     processPath(fullPath, modulesRoot, packageToModules);
+                 } catch (InvalidTreeException ex) {
                      // It has been observed some badly created jar file to contain
-                     // invalid directory entry marled as not directory (see 8131762)
-                     if (current instanceof ResourceNode) {
-                         System.err.println("Resources tree, invalid data structure, "
-                                 + "skipping " + p);
-                         continue;
-                     }
-                     String s = split[i];
-                     if (!s.isEmpty()) {
-                         // First item, this is the module, simply add a new node to the
-                         // tree.
-                         if (module == null) {
-                             module = s;
-                         }
-                         Node n = current.children.get(s);
-                         if (n == null) {
-                             if (i == split.length - 1) { // Leaf
-                                 n = new ResourceNode(s, current);
-                                 String pkg = toPackageName(n.parent);
-                                 //System.err.println("Adding a resource node. pkg " + pkg + ", name " + s);
-                                 if (pkg != null && !pkg.startsWith("META-INF")) {
-                                     Set<String> pkgs = moduleToPackage.get(module);
-                                     if (pkgs == null) {
-                                         pkgs = new TreeSet<>();
-                                         moduleToPackage.put(module, pkgs);
-                                     }
-                                     pkgs.add(pkg);
-                                 }
-                             } else { // put only sub trees, no leaf
-                                 n = new Node(s, current);
-                                 directAccess.put(n.getPath(), n);
-                                 String pkg = toPackageName(n);
-                                 if (pkg != null && !pkg.startsWith("META-INF")) {
-                                     Set<String> mods = packageToModule.get(pkg);
-                                     if (mods == null) {
-                                         mods = new TreeSet<>();
-                                         packageToModule.put(pkg, mods);
-                                     }
-                                     mods.add(module);
-                                 }
-                             }
-                         }
-                         current = n;
-                     }
-                 }
-             }
-             packages = new Node("packages", root);
-             directAccess.put(packages.getPath(), packages);
-             // The subset of package nodes that have some content.
-             // These packages exist only in a single module.
-             for (Map.Entry<String, Set<String>> entry : moduleToPackage.entrySet()) {
-                 for (String pkg : entry.getValue()) {
-                     PackageNode pkgNode = new PackageNode(pkg, packages);
-                     pkgNode.addReference(entry.getKey(), false);
-                     directAccess.put(pkgNode.getPath(), pkgNode);
+                     // invalid directory entry marked as not directory (see 8131762).
+                     System.err.println(ex.getMessage());
                  }
              }
  
-             // All packages
-             for (Map.Entry<String, Set<String>> entry : packageToModule.entrySet()) {
-                 // Do we already have a package node?
-                 PackageNode pkgNode = (PackageNode) packages.getChildren(entry.getKey());
-                 if (pkgNode == null) {
-                     pkgNode = new PackageNode(entry.getKey(), packages);
-                 }
-                 for (String module : entry.getValue()) {
-                     pkgNode.addReference(module, true);
-                 }
+             // We've collected information for all "packages", including the root
+             // (empty) package and anything under "META-INF". However, these should
+             // not have entries in the "/packages" directory.
+             packageToModules.keySet().removeIf(p -> p.isEmpty() || p.equals("META-INF") || p.startsWith("META-INF."));
+             packageToModules.forEach((pkgName, modRefs) -> {
+                 // Merge multiple refs for the same module.
+                 List<ModuleReference> pkgModules = modRefs.stream()
+                         .collect(Collectors.groupingBy(ModuleReference::name))
+                         .values().stream()
+                         .map(refs -> refs.stream().reduce(ModuleReference::merge).orElseThrow())
+                         .sorted()
+                         .toList();
+                 PackageNode pkgNode = new PackageNode(pkgName, pkgModules, packagesRoot);
                  directAccess.put(pkgNode.getPath(), pkgNode);
+             });
+         }
+ 
+         private void processPath(
+                 String fullPath,
+                 Node modulesRoot,
+                 Map<String, Set<ModuleReference>> packageToModules)
+                 throws InvalidTreeException {
+             // Paths are untrusted, so be careful about checking expected format.
+             if (!fullPath.startsWith("/") || fullPath.endsWith("/") || fullPath.contains("//")) {
+                 return;
+             }
+             int modEnd = fullPath.indexOf('/', 1);
+             // Ensure non-empty module name with non-empty suffix.
+             if (modEnd <= 1) {
+                 return;
              }
-             // Validate that the packages are well formed.
-             for (Node n : packages.children.values()) {
-                 ((PackageNode)n).validate();
+             String modName = fullPath.substring(1, modEnd);
+             String pkgPath = fullPath.substring(modEnd + 1);
+ 
+             Node parentNode = getDirectoryNode(modName, modulesRoot);
+             boolean isPreviewPath = false;
+             if (pkgPath.startsWith(PREVIEW_PREFIX)) {
+                 // For preview paths, process nodes relative to the preview directory.
+                 pkgPath = pkgPath.substring(PREVIEW_PREFIX.length());
+                 Node metaInf = getDirectoryNode("META-INF", parentNode);
+                 parentNode = getDirectoryNode("preview", metaInf);
+                 isPreviewPath = true;
              }
  
+             int pathEnd = pkgPath.lastIndexOf('/');
+             // From invariants tested above, this must now be well-formed.
+             String fullPkgName = (pathEnd == -1) ? "" : pkgPath.substring(0, pathEnd).replace('/', '.');
+             String resourceName = pkgPath.substring(pathEnd + 1);
+             // Intermediate packages are marked "empty" (no resources). This might
+             // later be merged with a non-empty reference for the same package.
+             ModuleReference emptyRef = ModuleReference.forEmptyPackage(modName, isPreviewPath);
+ 
+             // Work down through empty packages to final resource.
+             for (int i = pkgEndIndex(fullPkgName, 0); i != -1; i = pkgEndIndex(fullPkgName, i)) {
+                 // Due to invariants already checked, pkgName is non-empty.
+                 String pkgName = fullPkgName.substring(0, i);
+                 packageToModules.computeIfAbsent(pkgName, p -> new HashSet<>()).add(emptyRef);
+                 String childNodeName = pkgName.substring(pkgName.lastIndexOf('.') + 1);
+                 parentNode = getDirectoryNode(childNodeName, parentNode);
+             }
+             // Reached non-empty (leaf) package (could still be a duplicate).
+             Node resourceNode = parentNode.getChildren(resourceName);
+             if (resourceNode == null) {
+                 ModuleReference resourceRef = ModuleReference.forPackage(modName, isPreviewPath);
+                 packageToModules.computeIfAbsent(fullPkgName, p -> new HashSet<>()).add(resourceRef);
+                 // Init adds new node to parent (don't add resources to directAccess).
+                 new ResourceNode(resourceName, parentNode);
+             } else if (!(resourceNode instanceof ResourceNode)) {
+                 throw new InvalidTreeException(resourceNode);
+             }
          }
  
-         public String toResourceName(Node node) {
-             if (!node.children.isEmpty()) {
-                 throw new RuntimeException("Node is not a resource");
+         private Node getDirectoryNode(String name, Node parent) throws InvalidTreeException {
+             Node child = parent.getChildren(name);
+             if (child == null) {
+                 // Adds child to parent during init.
+                 child = new Node(name, parent);
+                 directAccess.put(child.getPath(), child);
+             } else if (child instanceof ResourceNode) {
+                 throw new InvalidTreeException(child);
              }
-             return removeRadical(node);
+             return child;
          }
  
-         public String getModule(Node node) {
-             if (node.parent == null || node.getName().equals("modules")
-                     || node.getName().startsWith("packages")) {
-                 return null;
-             }
-             String path = removeRadical(node);
-             // "/xxx/...";
-             path = path.substring(1);
-             int i = path.indexOf("/");
-             if (i == -1) {
-                 return path;
-             } else {
-                 return path.substring(0, i);
+         // Helper to iterate package names up to, and including, the complete name.
+         private int pkgEndIndex(String s, int i) {
+             if (i >= 0 && i < s.length()) {
+                 i = s.indexOf('.', i + 1);
+                 return i != -1 ? i : s.length();
              }
+             return -1;
          }
  
-         public String toPackageName(Node node) {
-             if (node.parent == null) {
-                 return null;
-             }
-             String path = removeRadical(node.getPath(), "/modules/");
-             String module = getModule(node);
-             if (path.equals(module)) {
-                 return null;
+         private String toResourceName(Node node) {
+             if (!node.children.isEmpty()) {
+                 throw new RuntimeException("Node is not a resource");
              }
-             String pkg = removeRadical(path, module + "/");
-             return pkg.replace('/', '.');
+             return removeRadical(node);
          }
  
-         public String removeRadical(Node node) {
+         private String removeRadical(Node node) {
              return removeRadical(node.getPath(), "/modules");
          }
  
          private String removeRadical(String path, String str) {
              if (!(path.length() < str.length())) {

@@ -337,24 +337,27 @@
              addLocations(tree.getRoot());
          }
  
          private int addLocations(Node current) {
              if (current instanceof PackageNode) {
-                 PackageNode pkgNode = (PackageNode) current;
-                 int size = pkgNode.references.size() * 8;
-                 writer.addLocation(current.getPath(), offset, 0, size);
+                 List<ModuleReference> refs = ((PackageNode) current).getModuleReferences();
+                 // "/packages/<pkg name>" entries have 8-byte entries (flags+offset).
+                 int size = refs.size() * 8;
+                 writer.addLocation(current.getPath(), offset, 0, size, ImageLocation.getPackageFlags(refs));
                  offset += size;
              } else {
                  int[] ret = new int[current.children.size()];
                  int i = 0;
                  for (java.util.Map.Entry<String, Node> entry : current.children.entrySet()) {
                      ret[i] = addLocations(entry.getValue());
                      i += 1;
                  }
                  if (current != tree.getRoot() && !(current instanceof ResourceNode)) {
+                     int locFlags = ImageLocation.getFlags(current.getPath(), tree.directAccess::containsKey);
+                     // Normal directory entries have 4-byte entries (offset only).
                      int size = ret.length * 4;
-                     writer.addLocation(current.getPath(), offset, 0, size);
+                     writer.addLocation(current.getPath(), offset, 0, size, locFlags);
                      offset += size;
                  }
              }
              return 0;
          }

@@ -367,31 +370,26 @@
              }
              // Attach location to node
              for (Map.Entry<String, ImageLocationWriter> entry : outLocations.entrySet()) {
                  Node item = tree.getMap().get(entry.getKey());
                  if (item != null) {
-                     item.loc = entry.getValue();
+                     item.setLocation(entry.getValue());
                  }
              }
              computeContent(tree.getRoot(), outLocations);
              return content;
          }
  
          private int computeContent(Node current, Map<String, ImageLocationWriter> outLocations) {
              if (current instanceof PackageNode) {
-                 // /packages/<pkg name>
-                 PackageNode pkgNode = (PackageNode) current;
-                 int size = pkgNode.references.size() * 8;
-                 ByteBuffer buff = ByteBuffer.allocate(size);
-                 buff.order(writer.getByteOrder());
-                 for (PackageNode.PackageReference mod : pkgNode.references.values()) {
-                     buff.putInt(mod.isEmpty ? 1 : 0);
-                     buff.putInt(writer.addString(mod.name));
-                 }
-                 byte[] arr = buff.array();
-                 content.add(arr);
-                 current.loc = outLocations.get(current.getPath());
+                 // "/packages/<pkg name>" entries have 8-byte entries (flags+offset).
+                 List<ModuleReference> refs = ((PackageNode) current).getModuleReferences();
+                 ByteBuffer byteBuffer = ByteBuffer.allocate(8 * refs.size());
+                 byteBuffer.order(writer.getByteOrder());
+                 ModuleReference.write(refs, byteBuffer.asIntBuffer(), writer::addString);
+                 content.add(byteBuffer.array());
+                 current.setLocation(outLocations.get(current.getPath()));
              } else {
                  int[] ret = new int[current.children.size()];
                  int i = 0;
                  for (java.util.Map.Entry<String, Node> entry : current.children.entrySet()) {
                      ret[i] = computeContent(entry.getValue(), outLocations);

@@ -408,14 +406,14 @@
                      content.add(arr);
                  } else {
                      if (current instanceof ResourceNode) {
                          // A resource location, remove "/modules"
                          String s = tree.toResourceName(current);
-                         current.loc = outLocations.get(s);
+                         current.setLocation(outLocations.get(s));
                      } else {
                          // empty "/packages" or empty "/modules" paths
-                         current.loc = outLocations.get(current.getPath());
+                         current.setLocation(outLocations.get(current.getPath()));
                      }
                  }
                  if (current.loc == null && current != tree.getRoot()) {
                      System.err.println("Invalid path in metadata, skipping " + current.getPath());
                  }
< prev index next >