1 /*
   2  * Copyright (c) 2014, 2017, Oracle and/or its affiliates. All rights reserved.
   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.internal.module;
  27 
  28 import java.io.BufferedInputStream;
  29 import java.io.BufferedReader;
  30 import java.io.File;
  31 import java.io.IOException;
  32 import java.io.InputStream;
  33 import java.io.InputStreamReader;
  34 import java.io.UncheckedIOException;
  35 import java.lang.module.FindException;
  36 import java.lang.module.InvalidModuleDescriptorException;
  37 import java.lang.module.ModuleDescriptor;
  38 import java.lang.module.ModuleFinder;
  39 import java.lang.module.ModuleReference;
  40 import java.net.URI;
  41 import java.nio.file.DirectoryStream;
  42 import java.nio.file.Files;
  43 import java.nio.file.NoSuchFileException;
  44 import java.nio.file.Path;
  45 import java.nio.file.Paths;
  46 import java.nio.file.attribute.BasicFileAttributes;
  47 import java.util.ArrayList;
  48 import java.util.Collections;
  49 import java.util.HashMap;
  50 import java.util.List;
  51 import java.util.Map;
  52 import java.util.Objects;
  53 import java.util.Optional;
  54 import java.util.Set;
  55 import java.util.jar.Attributes;
  56 import java.util.jar.JarEntry;
  57 import java.util.jar.JarFile;
  58 import java.util.jar.Manifest;
  59 import java.util.regex.Matcher;
  60 import java.util.regex.Pattern;
  61 import java.util.stream.Collectors;
  62 import java.util.zip.ZipFile;
  63 
  64 import jdk.internal.jmod.JmodFile;
  65 import jdk.internal.jmod.JmodFile.Section;
  66 import jdk.internal.perf.PerfCounter;
  67 import jdk.internal.util.jar.VersionedStream;
  68 
  69 
  70 /**
  71  * A {@code ModuleFinder} that locates modules on the file system by searching
  72  * a sequence of directories or packaged modules. The ModuleFinder can be
  73  * created to work in either the run-time or link-time phases. In both cases it
  74  * locates modular JAR and exploded modules. When created for link-time then it
  75  * additionally locates modules in JMOD files. The ModuleFinder can also
  76  * optionally patch any modules that it locates with a ModulePatcher.
  77  */
  78 
  79 public class ModulePath implements ModuleFinder {
  80     private static final String MODULE_INFO = "module-info.class";
  81 
  82     // the version to use for multi-release modular JARs
  83     private final Runtime.Version releaseVersion;
  84 
  85     // true for the link phase (supports modules packaged in JMOD format)
  86     private final boolean isLinkPhase;
  87 
  88     // for patching modules, can be null
  89     private final ModulePatcher patcher;
  90 
  91     // the entries on this module path
  92     private final Path[] entries;
  93     private int next;
  94 
  95     // map of module name to module reference map for modules already located
  96     private final Map<String, ModuleReference> cachedModules = new HashMap<>();
  97 
  98 
  99     private ModulePath(Runtime.Version version,
 100                        boolean isLinkPhase,
 101                        ModulePatcher patcher,
 102                        Path... entries) {
 103         this.releaseVersion = version;
 104         this.isLinkPhase = isLinkPhase;
 105         this.patcher = patcher;
 106         this.entries = entries.clone();
 107         for (Path entry : this.entries) {
 108             Objects.requireNonNull(entry);
 109         }
 110     }
 111 
 112     /**
 113      * Returns a ModuleFinder that that locates modules on the file system by
 114      * searching a sequence of directories and/or packaged modules. The modules
 115      * may be patched by the given ModulePatcher.
 116      */
 117     public static ModuleFinder of(ModulePatcher patcher, Path... entries) {
 118         return new ModulePath(JarFile.runtimeVersion(), false, patcher, entries);
 119     }
 120 
 121     /**
 122      * Returns a ModuleFinder that that locates modules on the file system by
 123      * searching a sequence of directories and/or packaged modules.
 124      */
 125     public static ModuleFinder of(Path... entries) {
 126         return of((ModulePatcher)null, entries);
 127     }
 128 
 129     /**
 130      * Returns a ModuleFinder that that locates modules on the file system by
 131      * searching a sequence of directories and/or packaged modules.
 132      *
 133      * @param version The release version to use for multi-release JAR files
 134      * @param isLinkPhase {@code true} if the link phase to locate JMOD files
 135      */
 136     public static ModuleFinder of(Runtime.Version version,
 137                                   boolean isLinkPhase,
 138                                   Path... entries) {
 139         return new ModulePath(version, isLinkPhase, null, entries);
 140     }
 141 
 142 
 143     @Override
 144     public Optional<ModuleReference> find(String name) {
 145         Objects.requireNonNull(name);
 146 
 147         // try cached modules
 148         ModuleReference m = cachedModules.get(name);
 149         if (m != null)
 150             return Optional.of(m);
 151 
 152         // the module may not have been encountered yet
 153         while (hasNextEntry()) {
 154             scanNextEntry();
 155             m = cachedModules.get(name);
 156             if (m != null)
 157                 return Optional.of(m);
 158         }
 159         return Optional.empty();
 160     }
 161 
 162     @Override
 163     public Set<ModuleReference> findAll() {
 164         // need to ensure that all entries have been scanned
 165         while (hasNextEntry()) {
 166             scanNextEntry();
 167         }
 168         return cachedModules.values().stream().collect(Collectors.toSet());
 169     }
 170 
 171     /**
 172      * Returns {@code true} if there are additional entries to scan
 173      */
 174     private boolean hasNextEntry() {
 175         return next < entries.length;
 176     }
 177 
 178     /**
 179      * Scans the next entry on the module path. A no-op if all entries have
 180      * already been scanned.
 181      *
 182      * @throws FindException if an error occurs scanning the next entry
 183      */
 184     private void scanNextEntry() {
 185         if (hasNextEntry()) {
 186 
 187             long t0 = System.nanoTime();
 188 
 189             Path entry = entries[next];
 190             Map<String, ModuleReference> modules = scan(entry);
 191             next++;
 192 
 193             // update cache, ignoring duplicates
 194             int initialSize = cachedModules.size();
 195             for (Map.Entry<String, ModuleReference> e : modules.entrySet()) {
 196                 cachedModules.putIfAbsent(e.getKey(), e.getValue());
 197             }
 198 
 199             // update counters
 200             int added = cachedModules.size() - initialSize;
 201             moduleCount.add(added);
 202 
 203             scanTime.addElapsedTimeFrom(t0);
 204         }
 205     }
 206 
 207 
 208     /**
 209      * Scan the given module path entry. If the entry is a directory then it is
 210      * a directory of modules or an exploded module. If the entry is a regular
 211      * file then it is assumed to be a packaged module.
 212      *
 213      * @throws FindException if an error occurs scanning the entry
 214      */
 215     private Map<String, ModuleReference> scan(Path entry) {
 216 
 217         BasicFileAttributes attrs;
 218         try {
 219             attrs = Files.readAttributes(entry, BasicFileAttributes.class);
 220         } catch (NoSuchFileException e) {
 221             return Collections.emptyMap();
 222         } catch (IOException ioe) {
 223             throw new FindException(ioe);
 224         }
 225 
 226         try {
 227 
 228             if (attrs.isDirectory()) {
 229                 Path mi = entry.resolve(MODULE_INFO);
 230                 if (!Files.exists(mi)) {
 231                     // assume a directory of modules
 232                     return scanDirectory(entry);
 233                 }
 234             }
 235 
 236             // packaged or exploded module
 237             ModuleReference mref = readModule(entry, attrs);
 238             if (mref != null) {
 239                 String name = mref.descriptor().name();
 240                 return Collections.singletonMap(name, mref);
 241             }
 242 
 243             // not recognized
 244             String msg;
 245             if (!isLinkPhase && entry.toString().endsWith(".jmod")) {
 246                 msg = "JMOD format not supported at execution time";
 247             } else {
 248                 msg = "Module format not recognized";
 249             }
 250             throw new FindException(msg + ": " + entry);
 251 
 252         } catch (IOException ioe) {
 253             throw new FindException(ioe);
 254         }
 255     }
 256 
 257 
 258     /**
 259      * Scans the given directory for packaged or exploded modules.
 260      *
 261      * @return a map of module name to ModuleReference for the modules found
 262      *         in the directory
 263      *
 264      * @throws IOException if an I/O error occurs
 265      * @throws FindException if an error occurs scanning the entry or the
 266      *         directory contains two or more modules with the same name
 267      */
 268     private Map<String, ModuleReference> scanDirectory(Path dir)
 269         throws IOException
 270     {
 271         // The map of name -> mref of modules found in this directory.
 272         Map<String, ModuleReference> nameToReference = new HashMap<>();
 273 
 274         try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
 275             for (Path entry : stream) {
 276                 BasicFileAttributes attrs;
 277                 try {
 278                     attrs = Files.readAttributes(entry, BasicFileAttributes.class);
 279                 } catch (NoSuchFileException ignore) {
 280                     // file has been removed or moved, ignore for now
 281                     continue;
 282                 }
 283 
 284                 ModuleReference mref = readModule(entry, attrs);
 285 
 286                 // module found
 287                 if (mref != null) {
 288                     // can have at most one version of a module in the directory
 289                     String name = mref.descriptor().name();
 290                     ModuleReference previous = nameToReference.put(name, mref);
 291                     if (previous != null) {
 292                         String fn1 = fileName(mref);
 293                         String fn2 = fileName(previous);
 294                         throw new FindException("Two versions of module "
 295                                                  + name + " found in " + dir
 296                                                  + " (" + fn1 + " and " + fn2 + ")");
 297                     }
 298                 }
 299             }
 300         }
 301 
 302         return nameToReference;
 303     }
 304 
 305 
 306     /**
 307      * Reads a packaged or exploded module, returning a {@code ModuleReference}
 308      * to the module. Returns {@code null} if the entry is not recognized.
 309      *
 310      * @throws IOException if an I/O error occurs
 311      * @throws FindException if an error occurs parsing its module descriptor
 312      */
 313     private ModuleReference readModule(Path entry, BasicFileAttributes attrs)
 314         throws IOException
 315     {
 316         try {
 317 
 318             if (attrs.isDirectory()) {
 319                 return readExplodedModule(entry); // may return null
 320             } else {
 321                 String fn = entry.getFileName().toString();
 322                 if (attrs.isRegularFile()) {
 323                     if (fn.endsWith(".jar")) {
 324                         return readJar(entry);
 325                     } else if (isLinkPhase && fn.endsWith(".jmod")) {
 326                         return readJMod(entry);
 327                     }
 328                 }
 329                 return null;
 330             }
 331 
 332         } catch (InvalidModuleDescriptorException e) {
 333             throw new FindException("Error reading module: " + entry, e);
 334         }
 335     }
 336 
 337 
 338     /**
 339      * Returns a string with the file name of the module if possible.
 340      * If the module location is not a file URI then return the URI
 341      * as a string.
 342      */
 343     private String fileName(ModuleReference mref) {
 344         URI uri = mref.location().orElse(null);
 345         if (uri != null) {
 346             if (uri.getScheme().equalsIgnoreCase("file")) {
 347                 Path file = Paths.get(uri);
 348                 return file.getFileName().toString();
 349             } else {
 350                 return uri.toString();
 351             }
 352         } else {
 353             return "<unknown>";
 354         }
 355     }
 356 
 357     // -- JMOD files --
 358 
 359     private Set<String> jmodPackages(JmodFile jf) {
 360         return jf.stream()
 361             .filter(e -> e.section() == Section.CLASSES)
 362             .map(JmodFile.Entry::name)
 363             .map(this::toPackageName)
 364             .flatMap(Optional::stream)
 365             .collect(Collectors.toSet());
 366     }
 367 
 368     /**
 369      * Returns a {@code ModuleReference} to a module in JMOD file on the
 370      * file system.
 371      *
 372      * @throws IOException
 373      * @throws InvalidModuleDescriptorException
 374      */
 375     private ModuleReference readJMod(Path file) throws IOException {
 376         try (JmodFile jf = new JmodFile(file)) {
 377             ModuleInfo.Attributes attrs;
 378             try (InputStream in = jf.getInputStream(Section.CLASSES, MODULE_INFO)) {
 379                 attrs  = ModuleInfo.read(in, () -> jmodPackages(jf));
 380             }
 381             return ModuleReferences.newJModModule(attrs, file);
 382         }
 383     }
 384 
 385 
 386     // -- JAR files --
 387 
 388     private static final String SERVICES_PREFIX = "META-INF/services/";
 389 
 390     /**
 391      * Returns the service type corresponding to the name of a services
 392      * configuration file if it is a legal type name.
 393      *
 394      * For example, if called with "META-INF/services/p.S" then this method
 395      * returns a container with the value "p.S".
 396      */
 397     private Optional<String> toServiceName(String cf) {
 398         assert cf.startsWith(SERVICES_PREFIX);
 399         int index = cf.lastIndexOf("/") + 1;
 400         if (index < cf.length()) {
 401             String prefix = cf.substring(0, index);
 402             if (prefix.equals(SERVICES_PREFIX)) {
 403                 String sn = cf.substring(index);
 404                 if (Checks.isClassName(sn))
 405                     return Optional.of(sn);
 406             }
 407         }
 408         return Optional.empty();
 409     }
 410 
 411     /**
 412      * Reads the next line from the given reader and trims it of comments and
 413      * leading/trailing white space.
 414      *
 415      * Returns null if the reader is at EOF.
 416      */
 417     private String nextLine(BufferedReader reader) throws IOException {
 418         String ln = reader.readLine();
 419         if (ln != null) {
 420             int ci = ln.indexOf('#');
 421             if (ci >= 0)
 422                 ln = ln.substring(0, ci);
 423             ln = ln.trim();
 424         }
 425         return ln;
 426     }
 427 
 428     /**
 429      * Treat the given JAR file as a module as follows:
 430      *
 431      * 1. The module name (and optionally the version) is derived from the file
 432      *    name of the JAR file
 433      * 2. All packages are derived from the .class files in the JAR file
 434      * 3. The contents of any META-INF/services configuration files are mapped
 435      *    to "provides" declarations
 436      * 4. The Main-Class attribute in the main attributes of the JAR manifest
 437      *    is mapped to the module descriptor mainClass
 438      */
 439     private ModuleDescriptor deriveModuleDescriptor(JarFile jf)
 440         throws IOException
 441     {
 442         // Derive module name and version from JAR file name
 443 
 444         String fn = jf.getName();
 445         int i = fn.lastIndexOf(File.separator);
 446         if (i != -1)
 447             fn = fn.substring(i+1);
 448 
 449         // drop .jar
 450         String mn = fn.substring(0, fn.length()-4);
 451         String vs = null;
 452 
 453         // find first occurrence of -${NUMBER}. or -${NUMBER}$
 454         Matcher matcher = Patterns.DASH_VERSION.matcher(mn);
 455         if (matcher.find()) {
 456             int start = matcher.start();
 457 
 458             // attempt to parse the tail as a version string
 459             try {
 460                 String tail = mn.substring(start+1);
 461                 ModuleDescriptor.Version.parse(tail);
 462                 vs = tail;
 463             } catch (IllegalArgumentException ignore) { }
 464 
 465             mn = mn.substring(0, start);
 466         }
 467 
 468         // finally clean up the module name
 469         mn = cleanModuleName(mn);
 470 
 471         // Builder throws IAE if module name is empty or invalid
 472         ModuleDescriptor.Builder builder = ModuleDescriptor.newAutomaticModule(mn);
 473         if (vs != null)
 474             builder.version(vs);
 475 
 476         // scan the names of the entries in the JAR file
 477         Map<Boolean, Set<String>> map = VersionedStream.stream(jf)
 478                 .filter(e -> !e.isDirectory())
 479                 .map(JarEntry::getName)
 480                 .filter(e -> (e.endsWith(".class") ^ e.startsWith(SERVICES_PREFIX)))
 481                 .collect(Collectors.partitioningBy(e -> e.startsWith(SERVICES_PREFIX),
 482                                                    Collectors.toSet()));
 483 
 484         Set<String> classFiles = map.get(Boolean.FALSE);
 485         Set<String> configFiles = map.get(Boolean.TRUE);
 486 
 487         // the packages containing class files
 488         Set<String> packages = classFiles.stream()
 489                 .map(this::toPackageName)
 490                 .flatMap(Optional::stream)
 491                 .distinct()
 492                 .collect(Collectors.toSet());
 493 
 494         // all packages are exported and open
 495         builder.packages(packages);
 496 
 497         // map names of service configuration files to service names
 498         Set<String> serviceNames = configFiles.stream()
 499                 .map(this::toServiceName)
 500                 .flatMap(Optional::stream)
 501                 .collect(Collectors.toSet());
 502 
 503         // parse each service configuration file
 504         for (String sn : serviceNames) {
 505             JarEntry entry = jf.getJarEntry(SERVICES_PREFIX + sn);
 506             List<String> providerClasses = new ArrayList<>();
 507             try (InputStream in = jf.getInputStream(entry)) {
 508                 BufferedReader reader
 509                     = new BufferedReader(new InputStreamReader(in, "UTF-8"));
 510                 String cn;
 511                 while ((cn = nextLine(reader)) != null) {
 512                     if (cn.length() > 0) {
 513                         String pn = packageName(cn);
 514                         if (!packages.contains(pn)) {
 515                             String msg = "Provider class " + cn + " not in module";
 516                             throw new InvalidModuleDescriptorException(msg);
 517                         }
 518                         providerClasses.add(cn);
 519                     }
 520                 }
 521             }
 522             if (!providerClasses.isEmpty())
 523                 builder.provides(sn, providerClasses);
 524         }
 525 
 526         // Main-Class attribute if it exists
 527         Manifest man = jf.getManifest();
 528         if (man != null) {
 529             Attributes attrs = man.getMainAttributes();
 530             String mainClass = attrs.getValue(Attributes.Name.MAIN_CLASS);
 531             if (mainClass != null) {
 532                 mainClass = mainClass.replace("/", ".");
 533                 String pn = packageName(mainClass);
 534                 if (!packages.contains(pn)) {
 535                     String msg = "Main-Class " + mainClass + " not in module";
 536                     throw new InvalidModuleDescriptorException(msg);
 537                 }
 538                 builder.mainClass(mainClass);
 539             }
 540         }
 541 
 542         return builder.build();
 543     }
 544 
 545     /**
 546      * Patterns used to derive the module name from a JAR file name.
 547      */
 548     private static class Patterns {
 549         static final Pattern DASH_VERSION = Pattern.compile("-(\\d+(\\.|$))");
 550         static final Pattern TRAILING_VERSION = Pattern.compile("(\\.|\\d)*$");
 551         static final Pattern NON_ALPHANUM = Pattern.compile("[^A-Za-z0-9]");
 552         static final Pattern REPEATING_DOTS = Pattern.compile("(\\.)(\\1)+");
 553         static final Pattern LEADING_DOTS = Pattern.compile("^\\.");
 554         static final Pattern TRAILING_DOTS = Pattern.compile("\\.$");
 555     }
 556 
 557     /**
 558      * Clean up candidate module name derived from a JAR file name.
 559      */
 560     private static String cleanModuleName(String mn) {
 561         // drop trailing version from name
 562         mn = Patterns.TRAILING_VERSION.matcher(mn).replaceAll("");
 563 
 564         // replace non-alphanumeric
 565         mn = Patterns.NON_ALPHANUM.matcher(mn).replaceAll(".");
 566 
 567         // collapse repeating dots
 568         mn = Patterns.REPEATING_DOTS.matcher(mn).replaceAll(".");
 569 
 570         // drop leading dots
 571         if (mn.length() > 0 && mn.charAt(0) == '.')
 572             mn = Patterns.LEADING_DOTS.matcher(mn).replaceAll("");
 573 
 574         // drop trailing dots
 575         int len = mn.length();
 576         if (len > 0 && mn.charAt(len-1) == '.')
 577             mn = Patterns.TRAILING_DOTS.matcher(mn).replaceAll("");
 578 
 579         return mn;
 580     }
 581 
 582     private Set<String> jarPackages(JarFile jf) {
 583         return VersionedStream.stream(jf)
 584                 .filter(e -> !e.isDirectory())
 585                 .map(JarEntry::getName)
 586                 .map(this::toPackageName)
 587                 .flatMap(Optional::stream)
 588                 .collect(Collectors.toSet());
 589     }
 590 
 591     /**
 592      * Returns a {@code ModuleReference} to a module in modular JAR file on
 593      * the file system.
 594      *
 595      * @throws IOException
 596      * @throws FindException
 597      * @throws InvalidModuleDescriptorException
 598      */
 599     private ModuleReference readJar(Path file) throws IOException {
 600         try (JarFile jf = new JarFile(file.toFile(),
 601                                       true,               // verify
 602                                       ZipFile.OPEN_READ,
 603                                       releaseVersion))
 604         {
 605             ModuleInfo.Attributes attrs;
 606             JarEntry entry = jf.getJarEntry(MODULE_INFO);
 607             if (entry == null) {
 608 
 609                 // no module-info.class so treat it as automatic module
 610                 try {
 611                     ModuleDescriptor md = deriveModuleDescriptor(jf);
 612                     attrs = new ModuleInfo.Attributes(md, null, null, null);
 613                 } catch (RuntimeException e) {
 614                     throw new FindException("Unable to derive module descriptor for "
 615                                             + jf.getName(), e);
 616                 }
 617 
 618             } else {
 619                 attrs = ModuleInfo.read(jf.getInputStream(entry),
 620                                         () -> jarPackages(jf));
 621             }
 622 
 623             return ModuleReferences.newJarModule(attrs, patcher, file);
 624         }
 625     }
 626 
 627 
 628     // -- exploded directories --
 629 
 630     private Set<String> explodedPackages(Path dir) {
 631         try {
 632             return Files.find(dir, Integer.MAX_VALUE,
 633                               ((path, attrs) -> attrs.isRegularFile()))
 634                     .map(path -> dir.relativize(path))
 635                     .map(this::toPackageName)
 636                     .flatMap(Optional::stream)
 637                     .collect(Collectors.toSet());
 638         } catch (IOException x) {
 639             throw new UncheckedIOException(x);
 640         }
 641     }
 642 
 643     /**
 644      * Returns a {@code ModuleReference} to an exploded module on the file
 645      * system or {@code null} if {@code module-info.class} not found.
 646      *
 647      * @throws IOException
 648      * @throws InvalidModuleDescriptorException
 649      */
 650     private ModuleReference readExplodedModule(Path dir) throws IOException {
 651         Path mi = dir.resolve(MODULE_INFO);
 652         ModuleInfo.Attributes attrs;
 653         try (InputStream in = Files.newInputStream(mi)) {
 654             attrs = ModuleInfo.read(new BufferedInputStream(in),
 655                                     () -> explodedPackages(dir));
 656         } catch (NoSuchFileException e) {
 657             // for now
 658             return null;
 659         }
 660         return ModuleReferences.newExplodedModule(attrs, patcher, dir);
 661     }
 662 
 663     /**
 664      * Maps a type name to its package name.
 665      */
 666     private static String packageName(String cn) {
 667         int index = cn.lastIndexOf('.');
 668         return (index == -1) ? "" : cn.substring(0, index);
 669     }
 670 
 671     /**
 672      * Maps the name of an entry in a JAR or ZIP file to a package name.
 673      *
 674      * @throws InvalidModuleDescriptorException if the name is a class file in
 675      *         the top-level directory of the JAR/ZIP file (and it's not
 676      *         module-info.class)
 677      */
 678     private Optional<String> toPackageName(String name) {
 679         assert !name.endsWith("/");
 680         int index = name.lastIndexOf("/");
 681         if (index == -1) {
 682             if (name.endsWith(".class") && !name.equals(MODULE_INFO)) {
 683                 String msg = name + " found in top-level directory"
 684                              + " (unnamed package not allowed in module)";
 685                 throw new InvalidModuleDescriptorException(msg);
 686             }
 687             return Optional.empty();
 688         }
 689 
 690         String pn = name.substring(0, index).replace('/', '.');
 691         if (Checks.isPackageName(pn)) {
 692             return Optional.of(pn);
 693         } else {
 694             // not a valid package name
 695             return Optional.empty();
 696         }
 697     }
 698 
 699     /**
 700      * Maps the relative path of an entry in an exploded module to a package
 701      * name.
 702      *
 703      * @throws InvalidModuleDescriptorException if the name is a class file in
 704      *         the top-level directory (and it's not module-info.class)
 705      */
 706     private Optional<String> toPackageName(Path file) {
 707         assert file.getRoot() == null;
 708 
 709         Path parent = file.getParent();
 710         if (parent == null) {
 711             String name = file.toString();
 712             if (name.endsWith(".class") && !name.equals(MODULE_INFO)) {
 713                 String msg = name + " found in top-level directory"
 714                              + " (unnamed package not allowed in module)";
 715                 throw new InvalidModuleDescriptorException(msg);
 716             }
 717             return Optional.empty();
 718         }
 719 
 720         String pn = parent.toString().replace(File.separatorChar, '.');
 721         if (Checks.isPackageName(pn)) {
 722             return Optional.of(pn);
 723         } else {
 724             // not a valid package name
 725             return Optional.empty();
 726         }
 727     }
 728 
 729     private static final PerfCounter scanTime
 730         = PerfCounter.newPerfCounter("jdk.module.finder.modulepath.scanTime");
 731     private static final PerfCounter moduleCount
 732         = PerfCounter.newPerfCounter("jdk.module.finder.modulepath.modules");
 733 }