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