1 /*
  2  * Copyright (c) 2014, 2025, 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 package jdk.internal.jrtfs;
 26 
 27 import java.io.ByteArrayInputStream;
 28 import java.io.IOException;
 29 import java.io.InputStream;
 30 import java.io.OutputStream;
 31 import java.nio.ByteBuffer;
 32 import java.nio.channels.Channels;
 33 import java.nio.channels.FileChannel;
 34 import java.nio.channels.NonWritableChannelException;
 35 import java.nio.channels.ReadableByteChannel;
 36 import java.nio.channels.SeekableByteChannel;
 37 import java.nio.file.ClosedFileSystemException;
 38 import java.nio.file.CopyOption;
 39 import java.nio.file.DirectoryStream;
 40 import java.nio.file.FileStore;
 41 import java.nio.file.FileSystem;
 42 import java.nio.file.FileSystemException;
 43 import java.nio.file.InvalidPathException;
 44 import java.nio.file.LinkOption;
 45 import java.nio.file.NoSuchFileException;
 46 import java.nio.file.NotDirectoryException;
 47 import java.nio.file.OpenOption;
 48 import java.nio.file.Path;
 49 import java.nio.file.PathMatcher;
 50 import java.nio.file.ReadOnlyFileSystemException;
 51 import java.nio.file.StandardOpenOption;
 52 import java.nio.file.WatchService;
 53 import java.nio.file.attribute.FileAttribute;
 54 import java.nio.file.attribute.FileTime;
 55 import java.nio.file.attribute.UserPrincipalLookupService;
 56 import java.nio.file.spi.FileSystemProvider;
 57 import java.util.Arrays;
 58 import java.util.Collections;
 59 import java.util.HashSet;
 60 import java.util.Iterator;
 61 import java.util.Map;
 62 import java.util.Objects;
 63 import java.util.Set;
 64 import java.util.regex.Pattern;
 65 import jdk.internal.jimage.ImageReader.Node;
 66 
 67 /**
 68  * jrt file system implementation built on System jimage files.
 69  *
 70  * @implNote This class needs to maintain JDK 8 source compatibility.
 71  *
 72  * It is used internally in the JDK to implement jimage/jrtfs access,
 73  * but also compiled and delivered as part of the jrtfs.jar to support access
 74  * to the jimage file provided by the shipped JDK by tools running on JDK 8.
 75  */
 76 class JrtFileSystem extends FileSystem {
 77 
 78     private final JrtFileSystemProvider provider;
 79     private final JrtPath rootPath = new JrtPath(this, "/");
 80     private volatile boolean isOpen;
 81     private volatile boolean isClosable;
 82     private SystemImage image;
 83 
 84     JrtFileSystem(JrtFileSystemProvider provider, Map<String, ?> env)
 85             throws IOException
 86     {
 87         this.provider = provider;
 88         this.image = SystemImage.open();  // open image file
 89         this.isOpen = true;
 90         this.isClosable = env != null;
 91     }
 92 
 93     // FileSystem method implementations
 94     @Override
 95     public boolean isOpen() {
 96         return isOpen;
 97     }
 98 
 99     @Override
100     public void close() throws IOException {
101         if (!isClosable)
102             throw new UnsupportedOperationException();
103         cleanup();
104     }
105 
106     @Override
107     public FileSystemProvider provider() {
108         return provider;
109     }
110 
111     @Override
112     public Iterable<Path> getRootDirectories() {
113         return Collections.singleton(getRootPath());
114     }
115 
116     @Override
117     public JrtPath getPath(String first, String... more) {
118         if (more.length == 0) {
119             return new JrtPath(this, first);
120         }
121         StringBuilder sb = new StringBuilder();
122         sb.append(first);
123         for (String path : more) {
124             if (!path.isEmpty()) {
125                 if (sb.length() > 0) {
126                     sb.append('/');
127                 }
128                 sb.append(path);
129             }
130         }
131         return new JrtPath(this, sb.toString());
132     }
133 
134     @Override
135     public final boolean isReadOnly() {
136         return true;
137     }
138 
139     @Override
140     public final UserPrincipalLookupService getUserPrincipalLookupService() {
141         throw new UnsupportedOperationException();
142     }
143 
144     @Override
145     public final WatchService newWatchService() {
146         throw new UnsupportedOperationException();
147     }
148 
149     @Override
150     public final Iterable<FileStore> getFileStores() {
151         return Collections.singleton(getFileStore(getRootPath()));
152     }
153 
154     private static final Set<String> supportedFileAttributeViews
155             = Collections.unmodifiableSet(
156                     new HashSet<String>(Arrays.asList("basic", "jrt")));
157 
158     @Override
159     public final Set<String> supportedFileAttributeViews() {
160         return supportedFileAttributeViews;
161     }
162 
163     @Override
164     public final String toString() {
165         return "jrt:/";
166     }
167 
168     @Override
169     public final String getSeparator() {
170         return "/";
171     }
172 
173     @Override
174     public PathMatcher getPathMatcher(String syntaxAndInput) {
175         int pos = syntaxAndInput.indexOf(':');
176         if (pos <= 0) {
177             throw new IllegalArgumentException();
178         }
179         String syntax = syntaxAndInput.substring(0, pos);
180         String input = syntaxAndInput.substring(pos + 1);
181         String expr;
182         if (syntax.equalsIgnoreCase("glob")) {
183             expr = JrtUtils.toRegexPattern(input);
184         } else if (syntax.equalsIgnoreCase("regex")) {
185             expr = input;
186         } else {
187                 throw new UnsupportedOperationException("Syntax '" + syntax
188                         + "' not recognized");
189         }
190         // return matcher
191         final Pattern pattern = Pattern.compile(expr);
192         return (Path path) -> pattern.matcher(path.toString()).matches();
193     }
194 
195     JrtPath resolveLink(JrtPath path) throws IOException {
196         Node node = checkNode(path);
197         if (node.isLink()) {
198             node = node.resolveLink();
199             return new JrtPath(this, node.getName());  // TBD, normalized?
200         }
201         return path;
202     }
203 
204     JrtFileAttributes getFileAttributes(JrtPath path, LinkOption... options)
205             throws IOException {
206         Node node = checkNode(path);
207         if (node.isLink() && followLinks(options)) {
208             return new JrtFileAttributes(node.resolveLink(true));
209         }
210         return new JrtFileAttributes(node);
211     }
212 
213     /**
214      * returns the list of child paths of the given directory "path"
215      *
216      * @param path name of the directory whose content is listed
217      * @return iterator for child paths of the given directory path
218      */
219     Iterator<Path> iteratorOf(JrtPath path, DirectoryStream.Filter<? super Path> filter)
220             throws IOException {
221         Node node = checkNode(path).resolveLink(true);
222         if (!node.isDirectory()) {
223             throw new NotDirectoryException(path.getName());
224         }
225         if (filter == null) {
226             return node.getChildNames()
227                     .map(child -> (Path) (path.resolve(new JrtPath(this, child).getFileName())))
228                     .iterator();
229         }
230         return node.getChildNames()
231                 .map(child -> (Path) (path.resolve(new JrtPath(this, child).getFileName())))
232                 .filter(p -> {
233                     try {
234                         return filter.accept(p);
235                     } catch (IOException x) {}
236                     return false;
237                 })
238                 .iterator();
239     }
240 
241     // returns the content of the file resource specified by the path
242     byte[] getFileContent(JrtPath path) throws IOException {
243         Node node = checkNode(path);
244         if (node.isDirectory()) {
245             throw new FileSystemException(path + " is a directory");
246         }
247         //assert node.isResource() : "resource node expected here";
248         return image.getResource(node);
249     }
250 
251     /////////////// Implementation details below this point //////////
252 
253     // static utility methods
254     static ReadOnlyFileSystemException readOnly() {
255         return new ReadOnlyFileSystemException();
256     }
257 
258     // do the supplied options imply that we have to chase symlinks?
259     static boolean followLinks(LinkOption... options) {
260         if (options != null) {
261             for (LinkOption lo : options) {
262                 Objects.requireNonNull(lo);
263                 if (lo == LinkOption.NOFOLLOW_LINKS) {
264                     return false;
265                 } else {
266                     throw new AssertionError("should not reach here");
267                 }
268             }
269         }
270         return true;
271     }
272 
273     // check that the options passed are supported by (read-only) jrt file system
274     static void checkOptions(Set<? extends OpenOption> options) {
275         // check for options of null type and option is an instance of StandardOpenOption
276         for (OpenOption option : options) {
277             Objects.requireNonNull(option);
278             if (!(option instanceof StandardOpenOption)) {
279                 throw new IllegalArgumentException(
280                     "option class: " + option.getClass());
281             }
282         }
283         if (options.contains(StandardOpenOption.WRITE) ||
284             options.contains(StandardOpenOption.APPEND)) {
285             throw readOnly();
286         }
287     }
288 
289     // clean up this file system - called from finalize and close
290     synchronized void cleanup() throws IOException {
291         if (isOpen) {
292             isOpen = false;
293             image.close();
294             image = null;
295         }
296     }
297 
298     // These methods throw read only file system exception
299     final void setTimes(JrtPath jrtPath, FileTime mtime, FileTime atime, FileTime ctime)
300             throws IOException {
301         throw readOnly();
302     }
303 
304     // These methods throw read only file system exception
305     final void createDirectory(JrtPath jrtPath, FileAttribute<?>... attrs) throws IOException {
306         throw readOnly();
307     }
308 
309     final void deleteFile(JrtPath jrtPath, boolean failIfNotExists)
310             throws IOException {
311         throw readOnly();
312     }
313 
314     final OutputStream newOutputStream(JrtPath jrtPath, OpenOption... options)
315             throws IOException {
316         throw readOnly();
317     }
318 
319     final void copyFile(boolean deletesrc, JrtPath srcPath, JrtPath dstPath, CopyOption... options)
320             throws IOException {
321         throw readOnly();
322     }
323 
324     final FileChannel newFileChannel(JrtPath path,
325             Set<? extends OpenOption> options,
326             FileAttribute<?>... attrs)
327             throws IOException {
328         throw new UnsupportedOperationException("newFileChannel");
329     }
330 
331     final InputStream newInputStream(JrtPath path) throws IOException {
332         return new ByteArrayInputStream(getFileContent(path));
333     }
334 
335     final SeekableByteChannel newByteChannel(JrtPath path,
336             Set<? extends OpenOption> options,
337             FileAttribute<?>... attrs)
338             throws IOException {
339         checkOptions(options);
340 
341         byte[] buf = getFileContent(path);
342         final ReadableByteChannel rbc
343                 = Channels.newChannel(new ByteArrayInputStream(buf));
344         final long size = buf.length;
345         return new SeekableByteChannel() {
346             long read = 0;
347 
348             @Override
349             public boolean isOpen() {
350                 return rbc.isOpen();
351             }
352 
353             @Override
354             public long position() throws IOException {
355                 return read;
356             }
357 
358             @Override
359             public SeekableByteChannel position(long pos)
360                     throws IOException {
361                 throw new UnsupportedOperationException();
362             }
363 
364             @Override
365             public int read(ByteBuffer dst) throws IOException {
366                 int n = rbc.read(dst);
367                 if (n > 0) {
368                     read += n;
369                 }
370                 return n;
371             }
372 
373             @Override
374             public SeekableByteChannel truncate(long size)
375                     throws IOException {
376                 throw new NonWritableChannelException();
377             }
378 
379             @Override
380             public int write(ByteBuffer src) throws IOException {
381                 throw new NonWritableChannelException();
382             }
383 
384             @Override
385             public long size() throws IOException {
386                 return size;
387             }
388 
389             @Override
390             public void close() throws IOException {
391                 rbc.close();
392             }
393         };
394     }
395 
396     final JrtFileStore getFileStore(JrtPath path) {
397         return new JrtFileStore(path);
398     }
399 
400     final void ensureOpen() throws IOException {
401         if (!isOpen()) {
402             throw new ClosedFileSystemException();
403         }
404     }
405 
406     final JrtPath getRootPath() {
407         return rootPath;
408     }
409 
410     boolean isSameFile(JrtPath path1, JrtPath path2) throws IOException {
411         return checkNode(path1) == checkNode(path2);
412     }
413 
414     boolean isLink(JrtPath path) throws IOException {
415         return checkNode(path).isLink();
416     }
417 
418     boolean exists(JrtPath path) throws IOException {
419         try {
420             checkNode(path);
421         } catch (NoSuchFileException exp) {
422             return false;
423         }
424         return true;
425     }
426 
427     boolean isDirectory(JrtPath path, boolean resolveLinks)
428             throws IOException {
429         Node node = checkNode(path);
430         return resolveLinks && node.isLink()
431                 ? node.resolveLink(true).isDirectory()
432                 : node.isDirectory();
433     }
434 
435     JrtPath toRealPath(JrtPath path, LinkOption... options)
436             throws IOException {
437         Node node = checkNode(path);
438         if (followLinks(options) && node.isLink()) {
439             node = node.resolveLink();
440         }
441         // image node holds the real/absolute path name
442         return new JrtPath(this, node.getName(), true);
443     }
444 
445     private Node lookup(String path) {
446         try {
447             return image.findNode(path);
448         } catch (RuntimeException | IOException ex) {
449             throw new InvalidPathException(path, ex.toString());
450         }
451     }
452 
453     private Node lookupSymbolic(String path) {
454         int i = 1;
455         while (i < path.length()) {
456             i = path.indexOf('/', i);
457             if (i == -1) {
458                 break;
459             }
460             String prefix = path.substring(0, i);
461             Node node = lookup(prefix);
462             if (node == null) {
463                 break;
464             }
465             if (node.isLink()) {
466                 Node link = node.resolveLink(true);
467                 // resolved symbolic path concatenated to the rest of the path
468                 String resPath = link.getName() + path.substring(i);
469                 node = lookup(resPath);
470                 return node != null ? node : lookupSymbolic(resPath);
471             }
472             i++;
473         }
474         return null;
475     }
476 
477     Node checkNode(JrtPath path) throws IOException {
478         ensureOpen();
479         String p = path.getResolvedPath();
480         Node node = lookup(p);
481         if (node == null) {
482             node = lookupSymbolic(p);
483             if (node == null) {
484                 throw new NoSuchFileException(p);
485             }
486         }
487         return node;
488     }
489 }