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 
  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 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 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 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             // exploded module
 319             if (attrs.isDirectory()) {
 320                 return readExplodedModule(entry); // may return null
 321             }
 322 
 323             // JAR or JMOD file
 324             if (attrs.isRegularFile()) {
 325                 String fn = entry.getFileName().toString();
 326                 boolean isDefaultFileSystem = isDefaultFileSystem(entry);
 327 
 328                 // JAR file
 329                 if (fn.endsWith(".jar")) {
 330                     if (isDefaultFileSystem) {
 331                         return readJar(entry);
 332                     } else {
 333                         // the JAR file is in a custom file system so
 334                         // need to copy it to the local file system
 335                         Path tmpdir = Files.createTempDirectory("mlib");
 336                         Path target = Files.copy(entry, tmpdir.resolve(fn));
 337                         return readJar(target);
 338                     }
 339                 }
 340 
 341                 // JMOD file
 342                 if (isDefaultFileSystem && isLinkPhase && fn.endsWith(".jmod")) {
 343                     return readJMod(entry);
 344                 }
 345             }
 346 
 347             return null;
 348 
 349         } catch (InvalidModuleDescriptorException e) {
 350             throw new FindException("Error reading module: " + entry, e);
 351         }
 352     }
 353 
 354     /**
 355      * Returns a string with the file name of the module if possible.
 356      * If the module location is not a file URI then return the URI
 357      * as a string.
 358      */
 359     private String fileName(ModuleReference mref) {
 360         URI uri = mref.location().orElse(null);
 361         if (uri != null) {
 362             if (uri.getScheme().equalsIgnoreCase("file")) {
 363                 Path file = Paths.get(uri);
 364                 return file.getFileName().toString();
 365             } else {
 366                 return uri.toString();
 367             }
 368         } else {
 369             return "<unknown>";
 370         }
 371     }
 372 
 373     // -- JMOD files --
 374 
 375     private Set<String> jmodPackages(JmodFile jf) {
 376         return jf.stream()
 377             .filter(e -> e.section() == Section.CLASSES)
 378             .map(JmodFile.Entry::name)
 379             .map(this::toPackageName)
 380             .flatMap(Optional::stream)
 381             .collect(Collectors.toSet());
 382     }
 383 
 384     /**
 385      * Returns a {@code ModuleReference} to a module in JMOD file on the
 386      * file system.
 387      *
 388      * @throws IOException
 389      * @throws InvalidModuleDescriptorException
 390      */
 391     private ModuleReference readJMod(Path file) throws IOException {
 392         try (JmodFile jf = new JmodFile(file)) {
 393             ModuleInfo.Attributes attrs;
 394             try (InputStream in = jf.getInputStream(Section.CLASSES, MODULE_INFO)) {
 395                 attrs  = ModuleInfo.read(in, () -> jmodPackages(jf));
 396             }
 397             return ModuleReferences.newJModModule(attrs, file);
 398         }
 399     }
 400 
 401 
 402     // -- JAR files --
 403 
 404     private static final String SERVICES_PREFIX = "META-INF/services/";
 405 
 406     private static final Attributes.Name AUTOMATIC_MODULE_NAME
 407         = new Attributes.Name("Automatic-Module-Name");
 408 
 409     /**
 410      * Returns the service type corresponding to the name of a services
 411      * configuration file if it is a legal type name.
 412      *
 413      * For example, if called with "META-INF/services/p.S" then this method
 414      * returns a container with the value "p.S".
 415      */
 416     private Optional<String> toServiceName(String cf) {
 417         assert cf.startsWith(SERVICES_PREFIX);
 418         int index = cf.lastIndexOf("/") + 1;
 419         if (index < cf.length()) {
 420             String prefix = cf.substring(0, index);
 421             if (prefix.equals(SERVICES_PREFIX)) {
 422                 String sn = cf.substring(index);
 423                 if (Checks.isClassName(sn))
 424                     return Optional.of(sn);
 425             }
 426         }
 427         return Optional.empty();
 428     }
 429 
 430     /**
 431      * Reads the next line from the given reader and trims it of comments and
 432      * leading/trailing white space.
 433      *
 434      * Returns null if the reader is at EOF.
 435      */
 436     private String nextLine(BufferedReader reader) throws IOException {
 437         String ln = reader.readLine();
 438         if (ln != null) {
 439             int ci = ln.indexOf('#');
 440             if (ci >= 0)
 441                 ln = ln.substring(0, ci);
 442             ln = ln.trim();
 443         }
 444         return ln;
 445     }
 446 
 447     /**
 448      * Treat the given JAR file as a module as follows:
 449      *
 450      * 1. The value of the Automatic-Module-Name attribute is the module name
 451      * 2. The version, and the module name when the  Automatic-Module-Name
 452      *    attribute is not present, is derived from the file ame of the JAR file
 453      * 3. All packages are derived from the .class files in the JAR file
 454      * 4. The contents of any META-INF/services configuration files are mapped
 455      *    to "provides" declarations
 456      * 5. The Main-Class attribute in the main attributes of the JAR manifest
 457      *    is mapped to the module descriptor mainClass if possible
 458      */
 459     private ModuleDescriptor deriveModuleDescriptor(JarFile jf)
 460         throws IOException
 461     {
 462         // Read Automatic-Module-Name attribute if present
 463         Manifest man = jf.getManifest();
 464         Attributes attrs = null;
 465         String moduleName = null;
 466         if (man != null) {
 467             attrs = man.getMainAttributes();
 468             if (attrs != null) {
 469                 moduleName = attrs.getValue(AUTOMATIC_MODULE_NAME);
 470             }
 471         }
 472 
 473         // Derive the version, and the module name if needed, from JAR file name
 474         String fn = jf.getName();
 475         int i = fn.lastIndexOf(File.separator);
 476         if (i != -1)
 477             fn = fn.substring(i + 1);
 478 
 479         // drop ".jar"
 480         String name = fn.substring(0, fn.length() - 4);
 481         String vs = null;
 482 
 483         // find first occurrence of -${NUMBER}. or -${NUMBER}$
 484         Matcher matcher = Patterns.DASH_VERSION.matcher(name);
 485         if (matcher.find()) {
 486             int start = matcher.start();
 487 
 488             // attempt to parse the tail as a version string
 489             try {
 490                 String tail = name.substring(start + 1);
 491                 ModuleDescriptor.Version.parse(tail);
 492                 vs = tail;
 493             } catch (IllegalArgumentException ignore) { }
 494 
 495             name = name.substring(0, start);
 496         }
 497 
 498         // Create builder, using the name derived from file name when
 499         // Automatic-Module-Name not present
 500         Builder builder;
 501         if (moduleName != null) {
 502             try {
 503                 builder = ModuleDescriptor.newAutomaticModule(moduleName);
 504             } catch (IllegalArgumentException e) {
 505                 throw new FindException(AUTOMATIC_MODULE_NAME + ": " + e.getMessage());
 506             }
 507         } else {
 508             builder = ModuleDescriptor.newAutomaticModule(cleanModuleName(name));
 509         }
 510 
 511         // module version if present
 512         if (vs != null)
 513             builder.version(vs);
 514 
 515         // scan the names of the entries in the JAR file
 516         Map<Boolean, Set<String>> map = jf.versionedStream()
 517                 .filter(e -> !e.isDirectory())
 518                 .map(JarEntry::getName)
 519                 .filter(e -> (e.endsWith(".class") ^ e.startsWith(SERVICES_PREFIX)))
 520                 .collect(Collectors.partitioningBy(e -> e.startsWith(SERVICES_PREFIX),
 521                                                    Collectors.toSet()));
 522 
 523         Set<String> classFiles = map.get(Boolean.FALSE);
 524         Set<String> configFiles = map.get(Boolean.TRUE);
 525 
 526         // the packages containing class files
 527         Set<String> packages = classFiles.stream()
 528                 .map(this::toPackageName)
 529                 .flatMap(Optional::stream)
 530                 .distinct()
 531                 .collect(Collectors.toSet());
 532 
 533         // all packages are exported and open
 534         builder.packages(packages);
 535 
 536         // map names of service configuration files to service names
 537         Set<String> serviceNames = configFiles.stream()
 538                 .map(this::toServiceName)
 539                 .flatMap(Optional::stream)
 540                 .collect(Collectors.toSet());
 541 
 542         // parse each service configuration file
 543         for (String sn : serviceNames) {
 544             JarEntry entry = jf.getJarEntry(SERVICES_PREFIX + sn);
 545             List<String> providerClasses = new ArrayList<>();
 546             try (InputStream in = jf.getInputStream(entry)) {
 547                 BufferedReader reader
 548                     = new BufferedReader(new InputStreamReader(in, "UTF-8"));
 549                 String cn;
 550                 while ((cn = nextLine(reader)) != null) {
 551                     if (cn.length() > 0) {
 552                         String pn = packageName(cn);
 553                         if (!packages.contains(pn)) {
 554                             String msg = "Provider class " + cn + " not in module";
 555                             throw new InvalidModuleDescriptorException(msg);
 556                         }
 557                         providerClasses.add(cn);
 558                     }
 559                 }
 560             }
 561             if (!providerClasses.isEmpty())
 562                 builder.provides(sn, providerClasses);
 563         }
 564 
 565         // Main-Class attribute if it exists
 566         if (attrs != null) {
 567             String mainClass = attrs.getValue(Attributes.Name.MAIN_CLASS);
 568             if (mainClass != null) {
 569                 mainClass = mainClass.replace("/", ".");
 570                 if (Checks.isClassName(mainClass)) {
 571                     String pn = packageName(mainClass);
 572                     if (packages.contains(pn)) {
 573                         builder.mainClass(mainClass);
 574                     }
 575                 }
 576             }
 577         }
 578 
 579         return builder.build();
 580     }
 581 
 582     /**
 583      * Patterns used to derive the module name from a JAR file name.
 584      */
 585     private static class Patterns {
 586         static final Pattern DASH_VERSION = Pattern.compile("-(\\d+(\\.|$))");
 587         static final Pattern NON_ALPHANUM = Pattern.compile("[^A-Za-z0-9]");
 588         static final Pattern REPEATING_DOTS = Pattern.compile("(\\.)(\\1)+");
 589         static final Pattern LEADING_DOTS = Pattern.compile("^\\.");
 590         static final Pattern TRAILING_DOTS = Pattern.compile("\\.$");
 591     }
 592 
 593     /**
 594      * Clean up candidate module name derived from a JAR file name.
 595      */
 596     private static String cleanModuleName(String mn) {
 597         // replace non-alphanumeric
 598         mn = Patterns.NON_ALPHANUM.matcher(mn).replaceAll(".");
 599 
 600         // collapse repeating dots
 601         mn = Patterns.REPEATING_DOTS.matcher(mn).replaceAll(".");
 602 
 603         // drop leading dots
 604         if (mn.length() > 0 && mn.charAt(0) == '.')
 605             mn = Patterns.LEADING_DOTS.matcher(mn).replaceAll("");
 606 
 607         // drop trailing dots
 608         int len = mn.length();
 609         if (len > 0 && mn.charAt(len-1) == '.')
 610             mn = Patterns.TRAILING_DOTS.matcher(mn).replaceAll("");
 611 
 612         return mn;
 613     }
 614 
 615     private Set<String> jarPackages(JarFile jf) {
 616         return jf.versionedStream()
 617                 .filter(e -> !e.isDirectory())
 618                 .map(JarEntry::getName)
 619                 .map(this::toPackageName)
 620                 .flatMap(Optional::stream)
 621                 .collect(Collectors.toSet());
 622     }
 623 
 624     /**
 625      * Returns a {@code ModuleReference} to a module in modular JAR file on
 626      * the file system.
 627      *
 628      * @throws IOException
 629      * @throws FindException
 630      * @throws InvalidModuleDescriptorException
 631      */
 632     private ModuleReference readJar(Path file) throws IOException {
 633         try (JarFile jf = new JarFile(file.toFile(),
 634                                       true,               // verify
 635                                       ZipFile.OPEN_READ,
 636                                       releaseVersion))
 637         {
 638             ModuleInfo.Attributes attrs;
 639             JarEntry entry = jf.getJarEntry(MODULE_INFO);
 640             if (entry == null) {
 641 
 642                 // no module-info.class so treat it as automatic module
 643                 try {
 644                     ModuleDescriptor md = deriveModuleDescriptor(jf);
 645                     attrs = new ModuleInfo.Attributes(md, null, null, null);
 646                 } catch (RuntimeException e) {
 647                     throw new FindException("Unable to derive module descriptor for "
 648                                             + jf.getName(), e);
 649                 }
 650 
 651             } else {
 652                 attrs = ModuleInfo.read(jf.getInputStream(entry),
 653                                         () -> jarPackages(jf));
 654             }
 655 
 656             return ModuleReferences.newJarModule(attrs, patcher, file);
 657         } catch (ZipException e) {
 658             throw new FindException("Error reading " + file, e);
 659         }
 660     }
 661 
 662 
 663     // -- exploded directories --
 664 
 665     private Set<String> explodedPackages(Path dir) {
 666         try {
 667             return Files.find(dir, Integer.MAX_VALUE,
 668                     ((path, attrs) -> attrs.isRegularFile() && !isHidden(path)))
 669                     .map(path -> dir.relativize(path))
 670                     .map(this::toPackageName)
 671                     .flatMap(Optional::stream)
 672                     .collect(Collectors.toSet());
 673         } catch (IOException x) {
 674             throw new UncheckedIOException(x);
 675         }
 676     }
 677 
 678     /**
 679      * Returns a {@code ModuleReference} to an exploded module on the file
 680      * system or {@code null} if {@code module-info.class} not found.
 681      *
 682      * @throws IOException
 683      * @throws InvalidModuleDescriptorException
 684      */
 685     private ModuleReference readExplodedModule(Path dir) throws IOException {
 686         Path mi = dir.resolve(MODULE_INFO);
 687         ModuleInfo.Attributes attrs;
 688         try (InputStream in = Files.newInputStream(mi)) {
 689             attrs = ModuleInfo.read(new BufferedInputStream(in),
 690                                     () -> explodedPackages(dir));
 691         } catch (NoSuchFileException e) {
 692             // for now
 693             return null;
 694         }
 695         return ModuleReferences.newExplodedModule(attrs, patcher, dir);
 696     }
 697 
 698     /**
 699      * Maps a type name to its package name.
 700      */
 701     private static String packageName(String cn) {
 702         int index = cn.lastIndexOf('.');
 703         return (index == -1) ? "" : cn.substring(0, index);
 704     }
 705 
 706     /**
 707      * Maps the name of an entry in a JAR or ZIP file to a package name.
 708      *
 709      * @throws InvalidModuleDescriptorException if the name is a class file in
 710      *         the top-level directory of the JAR/ZIP file (and it's not
 711      *         module-info.class)
 712      */
 713     private Optional<String> toPackageName(String name) {
 714         assert !name.endsWith("/");
 715         int index = name.lastIndexOf("/");
 716         if (index == -1) {
 717             if (name.endsWith(".class") && !name.equals(MODULE_INFO)) {
 718                 String msg = name + " found in top-level directory"
 719                              + " (unnamed package not allowed in module)";
 720                 throw new InvalidModuleDescriptorException(msg);
 721             }
 722             return Optional.empty();
 723         }
 724 
 725         String pn = name.substring(0, index).replace('/', '.');
 726         if (Checks.isPackageName(pn)) {
 727             return Optional.of(pn);
 728         } else {
 729             // not a valid package name
 730             return Optional.empty();
 731         }
 732     }
 733 
 734     /**
 735      * Maps the relative path of an entry in an exploded module to a package
 736      * name.
 737      *
 738      * @throws InvalidModuleDescriptorException if the name is a class file in
 739      *         the top-level directory (and it's not module-info.class)
 740      */
 741     private Optional<String> toPackageName(Path file) {
 742         assert file.getRoot() == null;
 743 
 744         Path parent = file.getParent();
 745         if (parent == null) {
 746             String name = file.toString();
 747             if (name.endsWith(".class") && !name.equals(MODULE_INFO)) {
 748                 String msg = name + " found in top-level directory"
 749                              + " (unnamed package not allowed in module)";
 750                 throw new InvalidModuleDescriptorException(msg);
 751             }
 752             return Optional.empty();
 753         }
 754 
 755         String pn = parent.toString().replace(File.separatorChar, '.');
 756         if (Checks.isPackageName(pn)) {
 757             return Optional.of(pn);
 758         } else {
 759             // not a valid package name
 760             return Optional.empty();
 761         }
 762     }
 763 
 764     /**
 765      * Returns true if the given file exists and is a hidden file
 766      */
 767     private boolean isHidden(Path file) {
 768         try {
 769             return Files.isHidden(file);
 770         } catch (IOException ioe) {
 771             return false;
 772         }
 773     }
 774 
 775 
 776     /**
 777      * Return true if a path locates a path in the default file system
 778      */
 779     private boolean isDefaultFileSystem(Path path) {
 780         return path.getFileSystem().provider()
 781                 .getScheme().equalsIgnoreCase("file");
 782     }
 783 
 784 
 785     private static final PerfCounter scanTime
 786         = PerfCounter.newPerfCounter("jdk.module.finder.modulepath.scanTime");
 787     private static final PerfCounter moduleCount
 788         = PerfCounter.newPerfCounter("jdk.module.finder.modulepath.modules");
 789 }