1/*
2 * Copyright (c) 2015, 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.File;
28import java.io.IOException;
29import java.io.InputStream;
30import java.io.OutputStream;
31import java.net.URI;
32import java.net.URISyntaxException;
33import java.nio.channels.FileChannel;
34import java.nio.channels.SeekableByteChannel;
35import java.nio.file.*;
36import java.nio.file.DirectoryStream.Filter;;
37import java.nio.file.attribute.BasicFileAttributes;
38import java.nio.file.attribute.BasicFileAttributeView;
39import java.nio.file.attribute.FileAttribute;
40import java.nio.file.attribute.FileTime;
41import java.util.Iterator;
42import java.util.Map;
43import java.util.NoSuchElementException;
44import java.util.Objects;
45import java.util.Set;
46import static java.nio.file.StandardOpenOption.*;
47import static java.nio.file.StandardCopyOption.*;
48
49/**
50 * Base class for Path implementation of jrt file systems.
51 *
52 * @implNote This class needs to maintain JDK 8 source compatibility.
53 *
54 * It is used internally in the JDK to implement jimage/jrtfs access,
55 * but also compiled and delivered as part of the jrtfs.jar to support access
56 * to the jimage file provided by the shipped JDK by tools running on JDK 8.
57 */
58final class JrtPath implements Path {
59
60    final JrtFileSystem jrtfs;
61    private final String path;
62    private volatile int[] offsets;
63
64    JrtPath(JrtFileSystem jrtfs, String path) {
65        this.jrtfs = jrtfs;
66        this.path = normalize(path);
67        this.resolved = null;
68    }
69
70    JrtPath(JrtFileSystem jrtfs, String path, boolean normalized) {
71        this.jrtfs = jrtfs;
72        this.path = normalized ? path : normalize(path);
73        this.resolved = null;
74    }
75
76    final String getName() {
77        return path;
78    }
79
80    @Override
81    public final JrtPath getRoot() {
82        if (this.isAbsolute()) {
83            return jrtfs.getRootPath();
84        } else {
85            return null;
86        }
87    }
88
89    @Override
90    public final JrtPath getFileName() {
91        if (path.length() == 0)
92            return this;
93        if (path.length() == 1 && path.charAt(0) == '/')
94            return null;
95        int off = path.lastIndexOf('/');
96        if (off == -1)
97            return this;
98        return new JrtPath(jrtfs, path.substring(off + 1), true);
99    }
100
101    @Override
102    public final JrtPath getParent() {
103        initOffsets();
104        int count = offsets.length;
105        if (count == 0) {     // no elements so no parent
106            return null;
107        }
108        int off = offsets[count - 1] - 1;
109        if (off <= 0) {       // parent is root only (may be null)
110            return getRoot();
111        }
112        return new JrtPath(jrtfs, path.substring(0, off));
113    }
114
115    @Override
116    public final int getNameCount() {
117        initOffsets();
118        return offsets.length;
119    }
120
121    @Override
122    public final JrtPath getName(int index) {
123        initOffsets();
124        if (index < 0 || index >= offsets.length) {
125            throw new IllegalArgumentException("index: " +
126                index + ", offsets length: " + offsets.length);
127        }
128        int begin = offsets[index];
129        int end;
130        if (index == (offsets.length - 1)) {
131            end = path.length();
132        } else {
133            end = offsets[index + 1];
134        }
135        return new JrtPath(jrtfs, path.substring(begin, end));
136    }
137
138    @Override
139    public final JrtPath subpath(int beginIndex, int endIndex) {
140        initOffsets();
141        if (beginIndex < 0 || endIndex > offsets.length ||
142            beginIndex >= endIndex) {
143            throw new IllegalArgumentException(
144                "beginIndex: " + beginIndex + ", endIndex: " + endIndex +
145                ", offsets length: " + offsets.length);
146        }
147        // starting/ending offsets
148        int begin = offsets[beginIndex];
149        int end;
150        if (endIndex == offsets.length) {
151            end = path.length();
152        } else {
153            end = offsets[endIndex];
154        }
155        return new JrtPath(jrtfs, path.substring(begin, end));
156    }
157
158    @Override
159    public final JrtPath toRealPath(LinkOption... options) throws IOException {
160        return jrtfs.toRealPath(this, options);
161    }
162
163    @Override
164    public final JrtPath toAbsolutePath() {
165        if (isAbsolute())
166            return this;
167        return new JrtPath(jrtfs, "/" + path, true);
168    }
169
170    @Override
171    public final URI toUri() {
172        try {
173            return new URI("jrt", toAbsolutePath().path, null);
174        } catch (URISyntaxException ex) {
175            throw new AssertionError(ex);
176        }
177    }
178
179    private boolean equalsNameAt(JrtPath other, int index) {
180        int mbegin = offsets[index];
181        int mlen;
182        if (index == (offsets.length - 1)) {
183            mlen = path.length() - mbegin;
184        } else {
185            mlen = offsets[index + 1] - mbegin - 1;
186        }
187        int obegin = other.offsets[index];
188        int olen;
189        if (index == (other.offsets.length - 1)) {
190            olen = other.path.length() - obegin;
191        } else {
192            olen = other.offsets[index + 1] - obegin - 1;
193        }
194        if (mlen != olen) {
195            return false;
196        }
197        int n = 0;
198        while (n < mlen) {
199            if (path.charAt(mbegin + n) != other.path.charAt(obegin + n)) {
200                return false;
201            }
202            n++;
203        }
204        return true;
205    }
206
207    @Override
208    public final JrtPath relativize(Path other) {
209        final JrtPath o = checkPath(other);
210        if (o.equals(this)) {
211            return new JrtPath(jrtfs, "", true);
212        }
213        if (path.length() == 0) {
214            return o;
215        }
216        if (jrtfs != o.jrtfs || isAbsolute() != o.isAbsolute()) {
217            throw new IllegalArgumentException(
218                "Incorrect filesystem or path: " + other);
219        }
220        final String tp = this.path;
221        final String op = o.path;
222        if (op.startsWith(tp)) {    // fast path
223            int off = tp.length();
224            if (op.charAt(off - 1) == '/')
225                return new JrtPath(jrtfs, op.substring(off), true);
226            if (op.charAt(off) == '/')
227                return new JrtPath(jrtfs, op.substring(off + 1), true);
228        }
229        int mc = this.getNameCount();
230        int oc = o.getNameCount();
231        int n = Math.min(mc, oc);
232        int i = 0;
233        while (i < n) {
234            if (!equalsNameAt(o, i)) {
235                break;
236            }
237            i++;
238        }
239        int dotdots = mc - i;
240        int len = dotdots * 3 - 1;
241        if (i < oc) {
242            len += (o.path.length() - o.offsets[i] + 1);
243        }
244        StringBuilder sb  = new StringBuilder(len);
245        while (dotdots > 0) {
246            sb.append("..");
247            if (sb.length() < len) {  // no tailing slash at the end
248                sb.append('/');
249            }
250            dotdots--;
251        }
252        if (i < oc) {
253            sb.append(o.path, o.offsets[i], o.path.length());
254        }
255        return new JrtPath(jrtfs, sb.toString(), true);
256    }
257
258    @Override
259    public JrtFileSystem getFileSystem() {
260        return jrtfs;
261    }
262
263    @Override
264    public final boolean isAbsolute() {
265        return path.length() > 0 && path.charAt(0) == '/';
266    }
267
268    @Override
269    public final JrtPath resolve(Path other) {
270        final JrtPath o = checkPath(other);
271        if (this.path.length() == 0 || o.isAbsolute()) {
272            return o;
273        }
274        if (o.path.length() == 0) {
275            return this;
276        }
277        StringBuilder sb = new StringBuilder(path.length() + o.path.length());
278        sb.append(path);
279        if (path.charAt(path.length() - 1) != '/')
280            sb.append('/');
281        sb.append(o.path);
282        return new JrtPath(jrtfs, sb.toString(), true);
283    }
284
285    @Override
286    public final Path resolveSibling(Path other) {
287        Objects.requireNonNull(other, "other");
288        Path parent = getParent();
289        return (parent == null) ? other : parent.resolve(other);
290    }
291
292    @Override
293    public final boolean startsWith(Path other) {
294        if (!(Objects.requireNonNull(other) instanceof JrtPath))
295            return false;
296        final JrtPath o = (JrtPath)other;
297        final String tp = this.path;
298        final String op = o.path;
299        if (isAbsolute() != o.isAbsolute() || !tp.startsWith(op)) {
300            return false;
301        }
302        int off = op.length();
303        if (off == 0) {
304            return tp.length() == 0;
305        }
306        // check match is on name boundary
307        return tp.length() == off || tp.charAt(off) == '/' ||
308               off == 0 || op.charAt(off - 1) == '/';
309    }
310
311    @Override
312    public final boolean endsWith(Path other) {
313        if (!(Objects.requireNonNull(other) instanceof JrtPath))
314            return false;
315        final JrtPath o = (JrtPath)other;
316        final JrtPath t = this;
317        int olast = o.path.length() - 1;
318        if (olast > 0 && o.path.charAt(olast) == '/') {
319            olast--;
320        }
321        int last = t.path.length() - 1;
322        if (last > 0 && t.path.charAt(last) == '/') {
323            last--;
324        }
325        if (olast == -1) {  // o.path.length == 0
326            return last == -1;
327        }
328        if ((o.isAbsolute() && (!t.isAbsolute() || olast != last))
329            || last < olast) {
330            return false;
331        }
332        for (; olast >= 0; olast--, last--) {
333            if (o.path.charAt(olast) != t.path.charAt(last)) {
334                return false;
335            }
336        }
337        return o.path.charAt(olast + 1) == '/' ||
338               last == -1 || t.path.charAt(last) == '/';
339    }
340
341    @Override
342    public final JrtPath resolve(String other) {
343        return resolve(getFileSystem().getPath(other));
344    }
345
346    @Override
347    public final Path resolveSibling(String other) {
348        return resolveSibling(getFileSystem().getPath(other));
349    }
350
351    @Override
352    public final boolean startsWith(String other) {
353        return startsWith(getFileSystem().getPath(other));
354    }
355
356    @Override
357    public final boolean endsWith(String other) {
358        return endsWith(getFileSystem().getPath(other));
359    }
360
361    @Override
362    public final JrtPath normalize() {
363        String res = getResolved();
364        if (res == path) {  // no change
365            return this;
366        }
367        return new JrtPath(jrtfs, res, true);
368    }
369
370    private JrtPath checkPath(Path path) {
371        Objects.requireNonNull(path);
372        if (!(path instanceof JrtPath))
373            throw new ProviderMismatchException("path class: " +
374                path.getClass());
375        return (JrtPath) path;
376    }
377
378    // create offset list if not already created
379    private void initOffsets() {
380        if (this.offsets == null) {
381            int len = path.length();
382            // count names
383            int count = 0;
384            int off = 0;
385            while (off < len) {
386                char c = path.charAt(off++);
387                if (c != '/') {
388                    count++;
389                    off = path.indexOf('/', off);
390                    if (off == -1)
391                        break;
392                }
393            }
394            // populate offsets
395            int[] offsets = new int[count];
396            count = 0;
397            off = 0;
398            while (off < len) {
399                char c = path.charAt(off);
400                if (c == '/') {
401                    off++;
402                } else {
403                    offsets[count++] = off++;
404                    off = path.indexOf('/', off);
405                    if (off == -1)
406                        break;
407                }
408            }
409            this.offsets = offsets;
410        }
411    }
412
413    private volatile String resolved;
414
415    final String getResolvedPath() {
416        String r = resolved;
417        if (r == null) {
418            if (isAbsolute()) {
419                r = getResolved();
420            } else {
421                r = toAbsolutePath().getResolvedPath();
422            }
423            resolved = r;
424        }
425        return r;
426    }
427
428    // removes redundant slashs, replace "\" to separator "/"
429    // and check for invalid characters
430    private static String normalize(String path) {
431        int len = path.length();
432        if (len == 0) {
433            return path;
434        }
435        char prevC = 0;
436        for (int i = 0; i < len; i++) {
437            char c = path.charAt(i);
438            if (c == '\\' || c == '\u0000') {
439                return normalize(path, i);
440            }
441            if (c == '/' && prevC == '/') {
442                return normalize(path, i - 1);
443            }
444            prevC = c;
445        }
446        if (prevC == '/' && len > 1) {
447            return path.substring(0, len - 1);
448        }
449        return path;
450    }
451
452    private static String normalize(String path, int off) {
453        int len = path.length();
454        StringBuilder to = new StringBuilder(len);
455        to.append(path, 0, off);
456        char prevC = 0;
457        while (off < len) {
458            char c = path.charAt(off++);
459            if (c == '\\') {
460                c = '/';
461            }
462            if (c == '/' && prevC == '/') {
463                continue;
464            }
465            if (c == '\u0000') {
466                throw new InvalidPathException(path,
467                        "Path: NUL character not allowed");
468            }
469            to.append(c);
470            prevC = c;
471        }
472        len = to.length();
473        if (len > 1 && to.charAt(len - 1) == '/') {
474            to.deleteCharAt(len - 1);
475        }
476        return to.toString();
477    }
478
479    // Remove DotSlash(./) and resolve DotDot (..) components
480    private String getResolved() {
481        if (path.length() == 0) {
482            return path;
483        }
484        if (path.indexOf('.') == -1) {
485            return path;
486        }
487        int length = path.length();
488        char[] to = new char[length];
489        int nc = getNameCount();
490        int[] lastM = new int[nc];
491        int lastMOff = -1;
492        int m = 0;
493        for (int i = 0; i < nc; i++) {
494            int n = offsets[i];
495            int len = (i == offsets.length - 1) ? length - n
496                                                : offsets[i + 1] - n - 1;
497            if (len == 1 && path.charAt(n) == '.') {
498                if (m == 0 && path.charAt(0) == '/')   // absolute path
499                    to[m++] = '/';
500                continue;
501            }
502            if (len == 2 && path.charAt(n) == '.' && path.charAt(n + 1) == '.') {
503                if (lastMOff >= 0) {
504                    m = lastM[lastMOff--];    // retreat
505                    continue;
506                }
507                if (path.charAt(0) == '/') {  // "/../xyz" skip
508                    if (m == 0)
509                        to[m++] = '/';
510                } else {                      // "../xyz" -> "../xyz"
511                    if (m != 0 && to[m-1] != '/')
512                        to[m++] = '/';
513                    while (len-- > 0)
514                        to[m++] = path.charAt(n++);
515                }
516                continue;
517            }
518            if (m == 0 && path.charAt(0) == '/' ||   // absolute path
519                m != 0 && to[m-1] != '/') {   // not the first name
520                to[m++] = '/';
521            }
522            lastM[++lastMOff] = m;
523            while (len-- > 0)
524                to[m++] = path.charAt(n++);
525        }
526        if (m > 1 && to[m - 1] == '/')
527            m--;
528        return (m == to.length) ? new String(to) : new String(to, 0, m);
529    }
530
531    @Override
532    public final String toString() {
533        return path;
534    }
535
536    @Override
537    public final int hashCode() {
538        return path.hashCode();
539    }
540
541    @Override
542    public final boolean equals(Object obj) {
543        return obj instanceof JrtPath &&
544               this.path.equals(((JrtPath) obj).path);
545    }
546
547    @Override
548    public final int compareTo(Path other) {
549        final JrtPath o = checkPath(other);
550        return path.compareTo(o.path);
551    }
552
553    @Override
554    public final WatchKey register(
555            WatchService watcher,
556            WatchEvent.Kind<?>[] events,
557            WatchEvent.Modifier... modifiers) {
558        Objects.requireNonNull(watcher, "watcher");
559        Objects.requireNonNull(events, "events");
560        Objects.requireNonNull(modifiers, "modifiers");
561        throw new UnsupportedOperationException();
562    }
563
564    @Override
565    public final WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events) {
566        return register(watcher, events, new WatchEvent.Modifier[0]);
567    }
568
569    @Override
570    public final File toFile() {
571        throw new UnsupportedOperationException();
572    }
573
574    @Override
575    public final Iterator<Path> iterator() {
576        return new Iterator<Path>() {
577            private int i = 0;
578
579            @Override
580            public boolean hasNext() {
581                return (i < getNameCount());
582            }
583
584            @Override
585            public Path next() {
586                if (i < getNameCount()) {
587                    Path result = getName(i);
588                    i++;
589                    return result;
590                } else {
591                    throw new NoSuchElementException();
592                }
593            }
594
595            @Override
596            public void remove() {
597                throw new ReadOnlyFileSystemException();
598            }
599        };
600    }
601
602    // Helpers for JrtFileSystemProvider and JrtFileSystem
603
604    final JrtPath readSymbolicLink() throws IOException {
605        if (!jrtfs.isLink(this)) {
606            throw new IOException("not a symbolic link");
607        }
608        return jrtfs.resolveLink(this);
609    }
610
611    final boolean isHidden() {
612        return false;
613    }
614
615    final void createDirectory(FileAttribute<?>... attrs)
616            throws IOException {
617        jrtfs.createDirectory(this, attrs);
618    }
619
620    final InputStream newInputStream(OpenOption... options) throws IOException {
621        if (options.length > 0) {
622            for (OpenOption opt : options) {
623                if (opt != READ) {
624                    throw new UnsupportedOperationException("'" + opt + "' not allowed");
625                }
626            }
627        }
628        return jrtfs.newInputStream(this);
629    }
630
631    final DirectoryStream<Path> newDirectoryStream(Filter<? super Path> filter)
632            throws IOException {
633        return new JrtDirectoryStream(this, filter);
634    }
635
636    final void delete() throws IOException {
637        jrtfs.deleteFile(this, true);
638    }
639
640    final void deleteIfExists() throws IOException {
641        jrtfs.deleteFile(this, false);
642    }
643
644    final JrtFileAttributes getAttributes(LinkOption... options) throws IOException {
645        JrtFileAttributes zfas = jrtfs.getFileAttributes(this, options);
646        if (zfas == null) {
647            throw new NoSuchFileException(toString());
648        }
649        return zfas;
650    }
651
652    final void setAttribute(String attribute, Object value, LinkOption... options)
653            throws IOException {
654        JrtFileAttributeView.setAttribute(this, attribute, value);
655    }
656
657    final Map<String, Object> readAttributes(String attributes, LinkOption... options)
658            throws IOException {
659        return JrtFileAttributeView.readAttributes(this, attributes, options);
660    }
661
662    final void setTimes(FileTime mtime, FileTime atime, FileTime ctime)
663            throws IOException {
664        jrtfs.setTimes(this, mtime, atime, ctime);
665    }
666
667    final FileStore getFileStore() throws IOException {
668        // each JrtFileSystem only has one root (as requested for now)
669        if (exists()) {
670            return jrtfs.getFileStore(this);
671        }
672        throw new NoSuchFileException(path);
673    }
674
675    final boolean isSameFile(Path other) throws IOException {
676        if (this == other || this.equals(other)) {
677            return true;
678        }
679        if (other == null || this.getFileSystem() != other.getFileSystem()) {
680            return false;
681        }
682        this.checkAccess();
683        JrtPath o = (JrtPath) other;
684        o.checkAccess();
685        return this.getResolvedPath().equals(o.getResolvedPath()) ||
686               jrtfs.isSameFile(this, o);
687    }
688
689    final SeekableByteChannel newByteChannel(Set<? extends OpenOption> options,
690                                             FileAttribute<?>... attrs)
691            throws IOException
692    {
693        return jrtfs.newByteChannel(this, options, attrs);
694    }
695
696    final FileChannel newFileChannel(Set<? extends OpenOption> options,
697            FileAttribute<?>... attrs)
698            throws IOException {
699        return jrtfs.newFileChannel(this, options, attrs);
700    }
701
702    final void checkAccess(AccessMode... modes) throws IOException {
703        if (modes.length == 0) {    // check if the path exists
704            jrtfs.checkNode(this);  // no need to follow link. the "link" node
705                                    // is built from real node under "/module"
706        } else {
707            boolean w = false;
708            for (AccessMode mode : modes) {
709                switch (mode) {
710                    case READ:
711                        break;
712                    case WRITE:
713                        w = true;
714                        break;
715                    case EXECUTE:
716                        throw new AccessDeniedException(toString());
717                    default:
718                        throw new UnsupportedOperationException();
719                }
720            }
721            jrtfs.checkNode(this);
722            if (w && jrtfs.isReadOnly()) {
723                throw new AccessDeniedException(toString());
724            }
725        }
726    }
727
728    final boolean exists() {
729        try {
730            return jrtfs.exists(this);
731        } catch (IOException x) {}
732        return false;
733    }
734
735    final OutputStream newOutputStream(OpenOption... options) throws IOException {
736        if (options.length == 0) {
737            return jrtfs.newOutputStream(this, CREATE_NEW, WRITE);
738        }
739        return jrtfs.newOutputStream(this, options);
740    }
741
742    final void move(JrtPath target, CopyOption... options) throws IOException {
743        if (this.jrtfs == target.jrtfs) {
744            jrtfs.copyFile(true, this, target, options);
745        } else {
746            copyToTarget(target, options);
747            delete();
748        }
749    }
750
751    final void copy(JrtPath target, CopyOption... options) throws IOException {
752        if (this.jrtfs == target.jrtfs) {
753            jrtfs.copyFile(false, this, target, options);
754        } else {
755            copyToTarget(target, options);
756        }
757    }
758
759    private void copyToTarget(JrtPath target, CopyOption... options)
760            throws IOException {
761        boolean replaceExisting = false;
762        boolean copyAttrs = false;
763        for (CopyOption opt : options) {
764            if (opt == REPLACE_EXISTING) {
765                replaceExisting = true;
766            } else if (opt == COPY_ATTRIBUTES) {
767                copyAttrs = true;
768            }
769        }
770        // attributes of source file
771        BasicFileAttributes jrtfas = getAttributes();
772        // check if target exists
773        boolean exists;
774        if (replaceExisting) {
775            try {
776                target.deleteIfExists();
777                exists = false;
778            } catch (DirectoryNotEmptyException x) {
779                exists = true;
780            }
781        } else {
782            exists = target.exists();
783        }
784        if (exists) {
785            throw new FileAlreadyExistsException(target.toString());
786        }
787        if (jrtfas.isDirectory()) {
788            // create directory or file
789            target.createDirectory();
790        } else {
791            try (InputStream is = jrtfs.newInputStream(this);
792                 OutputStream os = target.newOutputStream()) {
793                byte[] buf = new byte[8192];
794                int n;
795                while ((n = is.read(buf)) != -1) {
796                    os.write(buf, 0, n);
797                }
798            }
799        }
800        if (copyAttrs) {
801            BasicFileAttributeView view =
802                Files.getFileAttributeView(target, BasicFileAttributeView.class);
803            try {
804                view.setTimes(jrtfas.lastModifiedTime(),
805                              jrtfas.lastAccessTime(),
806                              jrtfas.creationTime());
807            } catch (IOException x) {
808                try {
809                    target.delete();  // rollback?
810                } catch (IOException ignore) {}
811                throw x;
812            }
813        }
814    }
815}
816