1 /*
  2  * Copyright (c) 2014, 2022, 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.stream.Stream;
 62 import java.util.zip.ZipException;
 63 import java.util.zip.ZipFile;
 64 
 65 import sun.nio.cs.UTF_8;
 66 
 67 import jdk.internal.jmod.JmodFile;
 68 import jdk.internal.jmod.JmodFile.Section;
 69 import jdk.internal.perf.PerfCounter;
 70 
 71 /**
 72  * A {@code ModuleFinder} that locates modules on the file system by searching
 73  * a sequence of directories or packaged modules. The ModuleFinder can be
 74  * created to work in either the run-time or link-time phases. In both cases it
 75  * locates modular JAR and exploded modules. When created for link-time then it
 76  * additionally locates modules in JMOD files. The ModuleFinder can also
 77  * optionally patch any modules that it locates with a ModulePatcher.
 78  */
 79 
 80 public class ModulePath implements ModuleFinder {
 81     private static final String MODULE_INFO = "module-info.class";
 82 
 83     // the version to use for multi-release modular JARs
 84     private final Runtime.Version releaseVersion;
 85 
 86     // true for the link phase (supports modules packaged in JMOD format)
 87     private final boolean isLinkPhase;
 88 
 89     // for patching modules, can be null
 90     private final ModulePatcher patcher;
 91 
 92     // the entries on this module path
 93     private final Path[] entries;
 94     private int next;
 95 
 96     // map of module name to module reference map for modules already located
 97     private final Map<String, ModuleReference> cachedModules = new HashMap<>();
 98 
 99 
100     private ModulePath(Runtime.Version version,
101                        boolean isLinkPhase,
102                        ModulePatcher patcher,
103                        Path... entries) {
104         this.releaseVersion = version;
105         this.isLinkPhase = isLinkPhase;
106         this.patcher = patcher;
107         this.entries = entries.clone();
108         for (Path entry : this.entries) {
109             Objects.requireNonNull(entry);
110         }
111     }
112 
113     /**
114      * Returns a ModuleFinder that locates modules on the file system by
115      * searching a sequence of directories and/or packaged modules. The modules
116      * may be patched by the given ModulePatcher.
117      */
118     public static ModuleFinder of(ModulePatcher patcher, Path... entries) {
119         return new ModulePath(JarFile.runtimeVersion(), false, patcher, entries);
120     }
121 
122     /**
123      * Returns a ModuleFinder that locates modules on the file system by
124      * searching a sequence of directories and/or packaged modules.
125      */
126     public static ModuleFinder of(Path... entries) {
127         return of((ModulePatcher)null, entries);
128     }
129 
130     /**
131      * Returns a ModuleFinder that locates modules on the file system by
132      * searching a sequence of directories and/or packaged modules.
133      *
134      * @param version The release version to use for multi-release JAR files
135      * @param isLinkPhase {@code true} if the link phase to locate JMOD files
136      */
137     public static ModuleFinder of(Runtime.Version version,
138                                   boolean isLinkPhase,
139                                   Path... entries) {
140         return new ModulePath(version, isLinkPhase, null, entries);
141     }
142 
143 
144     @Override
145     public Optional<ModuleReference> find(String name) {
146         Objects.requireNonNull(name);
147 
148         // try cached modules
149         ModuleReference m = cachedModules.get(name);
150         if (m != null)
151             return Optional.of(m);
152 
153         // the module may not have been encountered yet
154         while (hasNextEntry()) {
155             scanNextEntry();
156             m = cachedModules.get(name);
157             if (m != null)
158                 return Optional.of(m);
159         }
160         return Optional.empty();
161     }
162 
163     @Override
164     public Set<ModuleReference> findAll() {
165         // need to ensure that all entries have been scanned
166         while (hasNextEntry()) {
167             scanNextEntry();
168         }
169         return cachedModules.values().stream().collect(Collectors.toSet());
170     }
171 
172     /**
173      * Returns {@code true} if there are additional entries to scan
174      */
175     private boolean hasNextEntry() {
176         return next < entries.length;
177     }
178 
179     /**
180      * Scans the next entry on the module path. A no-op if all entries have
181      * already been scanned.
182      *
183      * @throws FindException if an error occurs scanning the next entry
184      */
185     private void scanNextEntry() {
186         if (hasNextEntry()) {
187 
188             long t0 = System.nanoTime();
189 
190             Path entry = entries[next];
191             Map<String, ModuleReference> modules = scan(entry);
192             next++;
193 
194             // update cache, ignoring duplicates
195             int initialSize = cachedModules.size();
196             for (Map.Entry<String, ModuleReference> e : modules.entrySet()) {
197                 cachedModules.putIfAbsent(e.getKey(), e.getValue());
198             }
199 
200             // update counters
201             int added = cachedModules.size() - initialSize;
202             moduleCount.add(added);
203 
204             scanTime.addElapsedTimeFrom(t0);
205         }
206     }
207 
208 
209     /**
210      * Scan the given module path entry. If the entry is a directory then it is
211      * a directory of modules or an exploded module. If the entry is a regular
212      * file then it is assumed to be a packaged module.
213      *
214      * @throws FindException if an error occurs scanning the entry
215      */
216     private Map<String, ModuleReference> scan(Path entry) {
217 
218         BasicFileAttributes attrs;
219         try {
220             attrs = Files.readAttributes(entry, BasicFileAttributes.class);
221         } catch (NoSuchFileException e) {
222             return Map.of();
223         } catch (IOException ioe) {
224             throw new FindException(ioe);
225         }
226 
227         try {
228 
229             if (attrs.isDirectory()) {
230                 Path mi = entry.resolve(MODULE_INFO);
231                 if (!Files.exists(mi)) {
232                     // assume a directory of modules
233                     return scanDirectory(entry);
234                 }
235             }
236 
237             // packaged or exploded module
238             ModuleReference mref = readModule(entry, attrs);
239             if (mref != null) {
240                 String name = mref.descriptor().name();
241                 return Map.of(name, mref);
242             }
243 
244             // not recognized
245             String msg;
246             if (!isLinkPhase && entry.toString().endsWith(".jmod")) {
247                 msg = "JMOD format not supported at execution time";
248             } else {
249                 msg = "Module format not recognized";
250             }
251             throw new FindException(msg + ": " + entry);
252 
253         } catch (IOException ioe) {
254             throw new FindException(ioe);
255         }
256     }
257 
258 
259     /**
260      * Scans the given directory for packaged or exploded modules.
261      *
262      * @return a map of module name to ModuleReference for the modules found
263      *         in the directory
264      *
265      * @throws IOException if an I/O error occurs
266      * @throws FindException if an error occurs scanning the entry or the
267      *         directory contains two or more modules with the same name
268      */
269     private Map<String, ModuleReference> scanDirectory(Path dir)
270         throws IOException
271     {
272         // The map of name -> mref of modules found in this directory.
273         Map<String, ModuleReference> nameToReference = new HashMap<>();
274 
275         try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
276             for (Path entry : stream) {
277                 BasicFileAttributes attrs;
278                 try {
279                     attrs = Files.readAttributes(entry, BasicFileAttributes.class);
280                 } catch (NoSuchFileException ignore) {
281                     // file has been removed or moved, ignore for now
282                     continue;
283                 }
284 
285                 ModuleReference mref = readModule(entry, attrs);
286 
287                 // module found
288                 if (mref != null) {
289                     // can have at most one version of a module in the directory
290                     String name = mref.descriptor().name();
291                     ModuleReference previous = nameToReference.put(name, mref);
292                     if (previous != null) {
293                         String fn1 = fileName(mref);
294                         String fn2 = fileName(previous);
295                         throw new FindException("Two versions of module "
296                                                  + name + " found in " + dir
297                                                  + " (" + fn1 + " and " + fn2 + ")");
298                     }
299                 }
300             }
301         }
302 
303         return nameToReference;
304     }
305 
306 
307     /**
308      * Reads a packaged or exploded module, returning a {@code ModuleReference}
309      * to the module. Returns {@code null} if the entry is not recognized.
310      *
311      * @throws IOException if an I/O error occurs
312      * @throws FindException if an error occurs parsing its module descriptor
313      */
314     private ModuleReference readModule(Path entry, BasicFileAttributes attrs)
315         throws IOException
316     {
317         try {
318 
319             // exploded module
320             if (attrs.isDirectory()) {
321                 return readExplodedModule(entry); // may return null
322             }
323 
324             // JAR or JMOD file
325             if (attrs.isRegularFile()) {
326                 String fn = entry.getFileName().toString();
327                 boolean isDefaultFileSystem = isDefaultFileSystem(entry);
328 
329                 // JAR file
330                 if (fn.endsWith(".jar")) {
331                     if (isDefaultFileSystem) {
332                         return readJar(entry);
333                     } else {
334                         // the JAR file is in a custom file system so
335                         // need to copy it to the local file system
336                         Path tmpdir = Files.createTempDirectory("mlib");
337                         Path target = Files.copy(entry, tmpdir.resolve(fn));
338                         return readJar(target);
339                     }
340                 }
341 
342                 // JMOD file
343                 if (isDefaultFileSystem && isLinkPhase && fn.endsWith(".jmod")) {
344                     return readJMod(entry);
345                 }
346             }
347 
348             return null;
349 
350         } catch (InvalidModuleDescriptorException e) {
351             throw new FindException("Error reading module: " + entry, e);
352         }
353     }
354 
355     /**
356      * Returns a string with the file name of the module if possible.
357      * If the module location is not a file URI then return the URI
358      * as a string.
359      */
360     private String fileName(ModuleReference mref) {
361         URI uri = mref.location().orElse(null);
362         if (uri != null) {
363             if (uri.getScheme().equalsIgnoreCase("file")) {
364                 Path file = Path.of(uri);
365                 return file.getFileName().toString();
366             } else {
367                 return uri.toString();
368             }
369         } else {
370             return "<unknown>";
371         }
372     }
373 
374     // -- JMOD files --
375 
376     private Set<String> jmodPackages(JmodFile jf) {
377         return jf.stream()
378             .filter(e -> e.section() == Section.CLASSES)
379             .map(JmodFile.Entry::name)
380             .map(this::toPackageName)
381             .flatMap(Optional::stream)
382             .collect(Collectors.toSet());
383     }
384 
385     /**
386      * Returns a {@code ModuleReference} to a module in JMOD file on the
387      * file system.
388      *
389      * @throws IOException
390      * @throws InvalidModuleDescriptorException
391      */
392     private ModuleReference readJMod(Path file) throws IOException {
393         try (JmodFile jf = new JmodFile(file)) {
394             ModuleInfo.Attributes attrs;
395             try (InputStream in = jf.getInputStream(Section.CLASSES, MODULE_INFO)) {
396                 attrs  = ModuleInfo.read(in, () -> jmodPackages(jf));
397             }
398             return ModuleReferences.newJModModule(attrs, file);
399         }
400     }
401 
402 
403     // -- JAR files --
404 
405     private static final String SERVICES_PREFIX = "META-INF/services/";
406 
407     private static final Attributes.Name AUTOMATIC_MODULE_NAME
408         = new Attributes.Name("Automatic-Module-Name");
409 
410     /**
411      * Returns the service type corresponding to the name of a services
412      * configuration file if it is a legal type name.
413      *
414      * For example, if called with "META-INF/services/p.S" then this method
415      * returns a container with the value "p.S".
416      */
417     private Optional<String> toServiceName(String cf) {
418         assert cf.startsWith(SERVICES_PREFIX);
419         int index = cf.lastIndexOf("/") + 1;
420         if (index < cf.length()) {
421             String prefix = cf.substring(0, index);
422             if (prefix.equals(SERVICES_PREFIX)) {
423                 String sn = cf.substring(index);
424                 if (Checks.isClassName(sn))
425                     return Optional.of(sn);
426             }
427         }
428         return Optional.empty();
429     }
430 
431     /**
432      * Reads the next line from the given reader and trims it of comments and
433      * leading/trailing white space.
434      *
435      * Returns null if the reader is at EOF.
436      */
437     private String nextLine(BufferedReader reader) throws IOException {
438         String ln = reader.readLine();
439         if (ln != null) {
440             int ci = ln.indexOf('#');
441             if (ci >= 0)
442                 ln = ln.substring(0, ci);
443             ln = ln.trim();
444         }
445         return ln;
446     }
447 
448     /**
449      * Treat the given JAR file as a module as follows:
450      *
451      * 1. The value of the Automatic-Module-Name attribute is the module name
452      * 2. The version, and the module name when the  Automatic-Module-Name
453      *    attribute is not present, is derived from the file ame of the JAR file
454      * 3. All packages are derived from the .class files in the JAR file
455      * 4. The contents of any META-INF/services configuration files are mapped
456      *    to "provides" declarations
457      * 5. The Main-Class attribute in the main attributes of the JAR manifest
458      *    is mapped to the module descriptor mainClass if possible
459      */
460     private ModuleDescriptor deriveModuleDescriptor(JarFile jf)
461         throws IOException
462     {
463         // Read Automatic-Module-Name attribute if present
464         Manifest man = jf.getManifest();
465         Attributes attrs = null;
466         String moduleName = null;
467         if (man != null) {
468             attrs = man.getMainAttributes();
469             if (attrs != null) {
470                 moduleName = attrs.getValue(AUTOMATIC_MODULE_NAME);
471             }
472         }
473 
474         // Derive the version, and the module name if needed, from JAR file name
475         String fn = jf.getName();
476         int i = fn.lastIndexOf(File.separator);
477         if (i != -1)
478             fn = fn.substring(i + 1);
479 
480         // drop ".jar"
481         String name = fn.substring(0, fn.length() - 4);
482         String vs = null;
483 
484         // find first occurrence of -${NUMBER}. or -${NUMBER}$
485         Matcher matcher = Patterns.DASH_VERSION.matcher(name);
486         if (matcher.find()) {
487             int start = matcher.start();
488 
489             // attempt to parse the tail as a version string
490             try {
491                 String tail = name.substring(start + 1);
492                 ModuleDescriptor.Version.parse(tail);
493                 vs = tail;
494             } catch (IllegalArgumentException ignore) { }
495 
496             name = name.substring(0, start);
497         }
498 
499         // Create builder, using the name derived from file name when
500         // Automatic-Module-Name not present
501         Builder builder;
502         if (moduleName != null) {
503             try {
504                 builder = ModuleDescriptor.newAutomaticModule(moduleName);
505             } catch (IllegalArgumentException e) {
506                 throw new FindException(AUTOMATIC_MODULE_NAME + ": " + e.getMessage());
507             }
508         } else {
509             builder = ModuleDescriptor.newAutomaticModule(cleanModuleName(name));
510         }
511 
512         // module version if present
513         if (vs != null)
514             builder.version(vs);
515 
516         // scan the names of the entries in the JAR file
517         Map<Boolean, Set<String>> map = jf.versionedStream()
518                 .filter(e -> !e.isDirectory())
519                 .map(JarEntry::getName)
520                 .filter(e -> (e.endsWith(".class") ^ e.startsWith(SERVICES_PREFIX)))
521                 .collect(Collectors.partitioningBy(e -> e.startsWith(SERVICES_PREFIX),
522                                                    Collectors.toSet()));
523 
524         Set<String> classFiles = map.get(Boolean.FALSE);
525         Set<String> configFiles = map.get(Boolean.TRUE);
526 
527         // the packages containing class files
528         Set<String> packages = classFiles.stream()
529                 .map(this::toPackageName)
530                 .flatMap(Optional::stream)
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.INSTANCE));
549                 String cn;
550                 while ((cn = nextLine(reader)) != null) {
551                     if (!cn.isEmpty()) {
552                         String pn = packageName(cn);
553                         if (!packages.contains(pn)) {
554                             String msg = "Provider class " + cn + " not in JAR file " + fn;
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.isEmpty() && 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         String separator = dir.getFileSystem().getSeparator();
667         try (Stream<Path> stream = Files.find(dir, Integer.MAX_VALUE,
668                 (path, attrs) -> attrs.isRegularFile() && !isHidden(path))) {
669             return stream.map(dir::relativize)
670                 .map(path -> toPackageName(path, separator))
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, String separator) {
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(separator, ".");
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 }