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