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