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