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