1 /*
  2  * Copyright (c) 2015, 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.File;
 29 import java.io.IOError;
 30 import java.io.IOException;
 31 import java.io.InputStream;
 32 import java.io.UncheckedIOException;
 33 import java.lang.module.ModuleReader;
 34 import java.lang.module.ModuleReference;
 35 import java.net.URI;
 36 import java.nio.ByteBuffer;
 37 import java.nio.file.Files;
 38 import java.nio.file.Path;
 39 import java.util.LinkedHashSet;
 40 import java.util.List;
 41 import java.util.Objects;
 42 import java.util.Optional;
 43 import java.util.Set;
 44 import java.util.concurrent.locks.Lock;
 45 import java.util.concurrent.locks.ReadWriteLock;
 46 import java.util.concurrent.locks.ReentrantReadWriteLock;
 47 import java.util.function.Supplier;
 48 import java.util.jar.JarEntry;
 49 import java.util.jar.JarFile;
 50 import java.util.stream.Stream;
 51 import java.util.zip.ZipFile;
 52 
 53 import jdk.internal.jmod.JmodFile;
 54 import jdk.internal.module.ModuleHashes.HashSupplier;
 55 import sun.net.www.ParseUtil;
 56 
 57 /**
 58  * A factory for creating ModuleReference implementations where the modules are
 59  * packaged as modular JAR file, JMOD files or where the modules are exploded
 60  * on the file system.
 61  */
 62 
 63 class ModuleReferences {
 64     private ModuleReferences() { }
 65 
 66     /**
 67      * Creates a ModuleReference to a possibly-patched module
 68      */
 69     private static ModuleReference newModule(ModuleInfo.Attributes attrs,
 70                                              URI uri,
 71                                              Supplier<ModuleReader> supplier,
 72                                              ModulePatcher patcher,
 73                                              HashSupplier hasher) {
 74         ModuleReference mref = new ModuleReferenceImpl(attrs.descriptor(),
 75                                                        uri,
 76                                                        supplier,
 77                                                        null,
 78                                                        attrs.target(),
 79                                                        attrs.recordedHashes(),
 80                                                        hasher,
 81                                                        attrs.moduleResolution());
 82         if (patcher != null)
 83             mref = patcher.patchIfNeeded(mref);
 84 
 85         return mref;
 86     }
 87 
 88     /**
 89      * Creates a ModuleReference to a possibly-patched module in a modular JAR.
 90      */
 91     static ModuleReference newJarModule(ModuleInfo.Attributes attrs,
 92                                         ModulePatcher patcher,
 93                                         Path file) {
 94         URI uri = file.toUri();
 95         String fileString = file.toString();
 96         Supplier<ModuleReader> supplier = new Supplier<>() {
 97             @Override
 98             public ModuleReader get() {
 99                 return new JarModuleReader(fileString, uri);
100             }
101         };
102         HashSupplier hasher = new HashSupplier() {
103             @Override
104             public byte[] generate(String algorithm) {
105               return ModuleHashes.computeHash(supplier, algorithm);
106             }
107         };
108         return newModule(attrs, uri, supplier, patcher, hasher);
109     }
110 
111     /**
112      * Creates a ModuleReference to a module in a JMOD file.
113      */
114     static ModuleReference newJModModule(ModuleInfo.Attributes attrs, Path file) {
115         URI uri = file.toUri();
116         Supplier<ModuleReader> supplier = () -> new JModModuleReader(file, uri);
117         HashSupplier hasher = (a) -> ModuleHashes.computeHash(supplier, a);
118         return newModule(attrs, uri, supplier, null, hasher);
119     }
120 
121     /**
122      * Creates a ModuleReference to a possibly-patched exploded module.
123      */
124     static ModuleReference newExplodedModule(ModuleInfo.Attributes attrs,
125                                              ModulePatcher patcher,
126                                              boolean previewMode,
127                                              Path dir) {
128         Supplier<ModuleReader> supplier = () -> new ExplodedModuleReader(dir, previewMode);
129         return newModule(attrs, dir.toUri(), supplier, patcher, null);
130     }
131 
132 
133     /**
134      * A base module reader that encapsulates machinery required to close the
135      * module reader safely.
136      */
137     abstract static class SafeCloseModuleReader implements ModuleReader {
138 
139         // RW lock to support safe close
140         private final ReadWriteLock lock = new ReentrantReadWriteLock();
141         private final Lock readLock = lock.readLock();
142         private final Lock writeLock = lock.writeLock();
143         private boolean closed;
144 
145         SafeCloseModuleReader() { }
146 
147         /**
148          * Returns a URL to  resource. This method is invoked by the find
149          * method to do the actual work of finding the resource.
150          */
151         abstract Optional<URI> implFind(String name) throws IOException;
152 
153         /**
154          * Returns an input stream for reading a resource. This method is
155          * invoked by the open method to do the actual work of opening
156          * an input stream to the resource.
157          */
158         abstract Optional<InputStream> implOpen(String name) throws IOException;
159 
160         /**
161          * Returns a stream of the names of resources in the module. This
162          * method is invoked by the list method to do the actual work of
163          * creating the stream.
164          */
165         abstract Stream<String> implList() throws IOException;
166 
167         /**
168          * Closes the module reader. This method is invoked by close to do the
169          * actual work of closing the module reader.
170          */
171         abstract void implClose() throws IOException;
172 
173         @Override
174         public final Optional<URI> find(String name) throws IOException {
175             readLock.lock();
176             try {
177                 if (!closed) {
178                     return implFind(name);
179                 } else {
180                     throw new IOException("ModuleReader is closed");
181                 }
182             } finally {
183                 readLock.unlock();
184             }
185         }
186 
187 
188         @Override
189         public final Optional<InputStream> open(String name) throws IOException {
190             readLock.lock();
191             try {
192                 if (!closed) {
193                     return implOpen(name);
194                 } else {
195                     throw new IOException("ModuleReader is closed");
196                 }
197             } finally {
198                 readLock.unlock();
199             }
200         }
201 
202         @Override
203         public final Stream<String> list() throws IOException {
204             readLock.lock();
205             try {
206                 if (!closed) {
207                     return implList();
208                 } else {
209                     throw new IOException("ModuleReader is closed");
210                 }
211             } finally {
212                 readLock.unlock();
213             }
214         }
215 
216         @Override
217         public final void close() throws IOException {
218             writeLock.lock();
219             try {
220                 if (!closed) {
221                     closed = true;
222                     implClose();
223                 }
224             } finally {
225                 writeLock.unlock();
226             }
227         }
228     }
229 
230 
231     /**
232      * A ModuleReader for a modular JAR file.
233      */
234     static class JarModuleReader extends SafeCloseModuleReader {
235         private final JarFile jf;
236         private final URI uri;
237 
238         static JarFile newJarFile(String path) {
239             try {
240                 return new JarFile(new File(path),
241                                    true,                       // verify
242                                    ZipFile.OPEN_READ,
243                                    JarFile.runtimeVersion());
244             } catch (IOException ioe) {
245                 throw new UncheckedIOException(ioe);
246             }
247         }
248 
249         JarModuleReader(String path, URI uri) {
250             this.jf = newJarFile(path);
251             this.uri = uri;
252         }
253 
254         private JarEntry getEntry(String name) {
255             return jf.getJarEntry(Objects.requireNonNull(name));
256         }
257 
258         @Override
259         Optional<URI> implFind(String name) throws IOException {
260             JarEntry je = getEntry(name);
261             if (je != null) {
262                 if (jf.isMultiRelease())
263                     name = je.getRealName();
264                 if (je.isDirectory() && !name.endsWith("/"))
265                     name += "/";
266                 String encodedPath = ParseUtil.encodePath(name, false);
267                 String uris = "jar:" + uri + "!/" + encodedPath;
268                 return Optional.of(URI.create(uris));
269             } else {
270                 return Optional.empty();
271             }
272         }
273 
274         @Override
275         Optional<InputStream> implOpen(String name) throws IOException {
276             JarEntry je = getEntry(name);
277             if (je != null) {
278                 return Optional.of(jf.getInputStream(je));
279             } else {
280                 return Optional.empty();
281             }
282         }
283 
284         @Override
285         Stream<String> implList() throws IOException {
286             // take snapshot to avoid async close
287             List<String> names = jf.versionedStream()
288                     .map(JarEntry::getName)
289                     .toList();
290             return names.stream();
291         }
292 
293         @Override
294         void implClose() throws IOException {
295             jf.close();
296         }
297     }
298 
299 
300     /**
301      * A ModuleReader for a JMOD file.
302      */
303     static class JModModuleReader extends SafeCloseModuleReader {
304         private final JmodFile jf;
305         private final URI uri;
306 
307         static JmodFile newJmodFile(Path path) {
308             try {
309                 return new JmodFile(path);
310             } catch (IOException ioe) {
311                 throw new UncheckedIOException(ioe);
312             }
313         }
314 
315         JModModuleReader(Path path, URI uri) {
316             this.jf = newJmodFile(path);
317             this.uri = uri;
318         }
319 
320         private JmodFile.Entry getEntry(String name) {
321             Objects.requireNonNull(name);
322             return jf.getEntry(JmodFile.Section.CLASSES, name);
323         }
324 
325         @Override
326         Optional<URI> implFind(String name) {
327             JmodFile.Entry je = getEntry(name);
328             if (je != null) {
329                 if (je.isDirectory() && !name.endsWith("/"))
330                     name += "/";
331                 String encodedPath = ParseUtil.encodePath(name, false);
332                 String uris = "jmod:" + uri + "!/" + encodedPath;
333                 return Optional.of(URI.create(uris));
334             } else {
335                 return Optional.empty();
336             }
337         }
338 
339         @Override
340         Optional<InputStream> implOpen(String name) throws IOException {
341             JmodFile.Entry je = getEntry(name);
342             if (je != null) {
343                 return Optional.of(jf.getInputStream(je));
344             } else {
345                 return Optional.empty();
346             }
347         }
348 
349         @Override
350         Stream<String> implList() throws IOException {
351             // take snapshot to avoid async close
352             List<String> names = jf.stream()
353                     .filter(e -> e.section() == JmodFile.Section.CLASSES)
354                     .map(JmodFile.Entry::name)
355                     .toList();
356             return names.stream();
357         }
358 
359         @Override
360         void implClose() throws IOException {
361             jf.close();
362         }
363     }
364 
365 
366     /**
367      * A ModuleReader for an exploded module.
368      */
369     static class ExplodedModuleReader implements ModuleReader {
370         private final Path dir;
371         private final Path previewDir;
372         private volatile boolean closed;
373 
374         ExplodedModuleReader(Path dir, boolean previewMode) {
375             this.dir = dir;
376             Path path = dir.resolve("META-INF", "preview");
377             this.previewDir = (previewMode && Files.isDirectory(path)) ? path : null;
378         }
379 
380         /**
381          * Throws IOException if the module reader is closed;
382          */
383         private void ensureOpen() throws IOException {
384             if (closed) throw new IOException("ModuleReader is closed");
385         }
386 
387         private Path toFilePath(String name) throws IOException {
388             if (previewDir != null) {
389                 if (isPreviewEntry(name)) {
390                     return null;
391                 }
392                 Path previewPath = Resources.toFilePath(previewDir, name);
393                 if (previewPath != null && Files.exists(previewPath)) {
394                     return previewPath;
395                 }
396             }
397             return Resources.toFilePath(dir, name);
398         }
399 
400         @Override
401         public Optional<URI> find(String name) throws IOException {
402             ensureOpen();
403             Path path = toFilePath(name);
404             if (path != null) {
405                 try {
406                     return Optional.of(path.toUri());
407                 } catch (IOError e) {
408                     throw (IOException) e.getCause();
409                 }
410             } else {
411                 return Optional.empty();
412             }
413         }
414 
415         @Override
416         public Optional<InputStream> open(String name) throws IOException {
417             ensureOpen();
418             Path path = toFilePath(name);
419             if (path != null) {
420                 return Optional.of(Files.newInputStream(path));
421             } else {
422                 return Optional.empty();
423             }
424         }
425 
426         @Override
427         public Optional<ByteBuffer> read(String name) throws IOException {
428             ensureOpen();
429             Path path = toFilePath(name);
430             if (path != null) {
431                 return Optional.of(ByteBuffer.wrap(Files.readAllBytes(path)));
432             } else {
433                 return Optional.empty();
434             }
435         }
436 
437         @Override
438         public Stream<String> list() throws IOException {
439             ensureOpen();
440             LinkedHashSet<String> names = new LinkedHashSet<>();
441             collectNames(dir, names);
442             if (previewDir != null) {
443                 collectNames(previewDir, names);
444             }
445             return names.stream();
446         }
447 
448         private void collectNames(Path dir, Set<String> dest) throws IOException {
449             try (Stream<Path> files = Files.walk(dir, Integer.MAX_VALUE)) {
450                 files.map(f -> Resources.toResourceName(dir, f))
451                         .filter(s -> !s.isEmpty())
452                         .filter(s -> previewDir == null || !isPreviewEntry(s))
453                         .forEach(dest::add);
454             }
455         }
456 
457         @Override
458         public void close() {
459             closed = true;
460         }
461 
462         // Names do not have a leading '/'.
463         private static final String PREVIEW_PREFIX = "META-INF/preview";
464 
465         private static boolean isPreviewEntry(String name) {
466             return name.startsWith(PREVIEW_PREFIX) &&
467                     (name.length() == PREVIEW_PREFIX.length()
468                             || name.charAt(PREVIEW_PREFIX.length()) == '/');
469         }
470     }
471 }