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