1 /*
  2  * Copyright (c) 2008, 2021, 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 sun.nio.fs;
 27 
 28 import java.nio.file.*;
 29 import java.nio.charset.*;
 30 import java.io.*;
 31 import java.net.URI;
 32 import java.util.*;
 33 
 34 import jdk.internal.access.JavaLangAccess;
 35 import jdk.internal.access.SharedSecrets;
 36 
 37 import static sun.nio.fs.UnixNativeDispatcher.*;
 38 import static sun.nio.fs.UnixConstants.*;
 39 
 40 /**
 41  * Linux/Mac implementation of java.nio.file.Path
 42  */
 43 class UnixPath implements Path {
 44 
 45     private static final JavaLangAccess JLA = SharedSecrets.getJavaLangAccess();
 46 
 47     private final UnixFileSystem fs;
 48 
 49     // internal representation
 50     private final byte[] path;
 51 
 52     // String representation (created lazily)
 53     private volatile String stringValue;
 54 
 55     // cached hashcode (created lazily, no need to be volatile)
 56     private int hash;
 57 
 58     // array of offsets of elements in path (created lazily)
 59     private volatile int[] offsets;
 60 
 61     UnixPath(UnixFileSystem fs, byte[] path) {
 62         this.fs = fs;
 63         this.path = path;
 64     }
 65 
 66     UnixPath(UnixFileSystem fs, String input) {
 67         // removes redundant slashes and checks for invalid characters
 68         this(fs, encode(fs, normalizeAndCheck(input)));
 69     }
 70 
 71     // package-private
 72     // removes redundant slashes and check input for invalid characters
 73     static String normalizeAndCheck(String input) {
 74         int n = input.length();
 75         char prevChar = 0;
 76         for (int i=0; i < n; i++) {
 77             char c = input.charAt(i);
 78             if ((c == '/') && (prevChar == '/'))
 79                 return normalize(input, n, i - 1);
 80             checkNotNul(input, c);
 81             prevChar = c;
 82         }
 83         if (prevChar == '/')
 84             return normalize(input, n, n - 1);
 85         return input;
 86     }
 87 
 88     private static void checkNotNul(String input, char c) {
 89         if (c == '\u0000')
 90             throw new InvalidPathException(input, "Nul character not allowed");
 91     }
 92 
 93     private static String normalize(String input, int len, int off) {
 94         if (len == 0)
 95             return input;
 96         int n = len;
 97         while ((n > 0) && (input.charAt(n - 1) == '/')) n--;
 98         if (n == 0)
 99             return "/";
100         StringBuilder sb = new StringBuilder(input.length());
101         if (off > 0)
102             sb.append(input.substring(0, off));
103         char prevChar = 0;
104         for (int i=off; i < n; i++) {
105             char c = input.charAt(i);
106             if ((c == '/') && (prevChar == '/'))
107                 continue;
108             checkNotNul(input, c);
109             sb.append(c);
110             prevChar = c;
111         }
112         return sb.toString();
113     }
114 
115     // encodes the given path-string into a sequence of bytes
116     private static byte[] encode(UnixFileSystem fs, String input) {
117         input = fs.normalizeNativePath(input);
118         try {
119             return JLA.getBytesNoRepl(input, Util.jnuEncoding());
120         } catch (CharacterCodingException cce) {
121             throw new InvalidPathException(input,
122                 "Malformed input or input contains unmappable characters");
123         }
124     }
125 
126     // package-private
127     byte[] asByteArray() {
128         return path;
129     }
130 
131     // use this path when making system/library calls
132     byte[] getByteArrayForSysCalls() {
133         // resolve against default directory if required (chdir allowed or
134         // file system default directory is not working directory)
135         if (getFileSystem().needToResolveAgainstDefaultDirectory()) {
136             return resolve(getFileSystem().defaultDirectory(), path);
137         } else {
138             if (!isEmpty()) {
139                 return path;
140             } else {
141                 // empty path case will access current directory
142                 byte[] here = { '.' };
143                 return here;
144             }
145         }
146     }
147 
148     // use this message when throwing exceptions
149     String getPathForExceptionMessage() {
150         return toString();
151     }
152 
153     // use this path for permission checks
154     String getPathForPermissionCheck() {
155         if (getFileSystem().needToResolveAgainstDefaultDirectory()) {
156             return Util.toString(getByteArrayForSysCalls());
157         } else {
158             return toString();
159         }
160     }
161 
162     // Checks that the given file is a UnixPath
163     static UnixPath toUnixPath(Path obj) {
164         if (obj == null)
165             throw new NullPointerException();
166         if (!(obj instanceof UnixPath))
167             throw new ProviderMismatchException();
168         return (UnixPath)obj;
169     }
170 
171     // create offset list if not already created
172     private void initOffsets() {
173         if (offsets == null) {
174             int count, index;
175 
176             // count names
177             count = 0;
178             index = 0;
179             if (isEmpty()) {
180                 // empty path has one name
181                 count = 1;
182             } else {
183                 while (index < path.length) {
184                     byte c = path[index++];
185                     if (c != '/') {
186                         count++;
187                         while (index < path.length && path[index] != '/')
188                             index++;
189                     }
190                 }
191             }
192 
193             // populate offsets
194             int[] result = new int[count];
195             count = 0;
196             index = 0;
197             while (index < path.length) {
198                 byte c = path[index];
199                 if (c == '/') {
200                     index++;
201                 } else {
202                     result[count++] = index++;
203                     while (index < path.length && path[index] != '/')
204                         index++;
205                 }
206             }
207             synchronized (this) {
208                 if (offsets == null)
209                     offsets = result;
210             }
211         }
212     }
213 
214     // returns {@code true} if this path is an empty path
215     boolean isEmpty() {
216         return path.length == 0;
217     }
218 
219     // returns an empty path
220     private UnixPath emptyPath() {
221         return new UnixPath(getFileSystem(), new byte[0]);
222     }
223 
224 
225     // return true if this path has "." or ".."
226     private boolean hasDotOrDotDot() {
227         int n = getNameCount();
228         for (int i=0; i<n; i++) {
229             byte[] bytes = getName(i).path;
230             if ((bytes.length == 1 && bytes[0] == '.'))
231                 return true;
232             if ((bytes.length == 2 && bytes[0] == '.') && bytes[1] == '.') {
233                 return true;
234             }
235         }
236         return false;
237     }
238 
239     @Override
240     public UnixFileSystem getFileSystem() {
241         return fs;
242     }
243 
244     @Override
245     public UnixPath getRoot() {
246         if (path.length > 0 && path[0] == '/') {
247             return getFileSystem().rootDirectory();
248         } else {
249             return null;
250         }
251     }
252 
253     @Override
254     public UnixPath getFileName() {
255         initOffsets();
256 
257         int count = offsets.length;
258 
259         // no elements so no name
260         if (count == 0)
261             return null;
262 
263         // one name element and no root component
264         if (count == 1 && path.length > 0 && path[0] != '/')
265             return this;
266 
267         int lastOffset = offsets[count-1];
268         int len = path.length - lastOffset;
269         byte[] result = new byte[len];
270         System.arraycopy(path, lastOffset, result, 0, len);
271         return new UnixPath(getFileSystem(), result);
272     }
273 
274     @Override
275     public UnixPath getParent() {
276         initOffsets();
277 
278         int count = offsets.length;
279         if (count == 0) {
280             // no elements so no parent
281             return null;
282         }
283         int len = offsets[count-1] - 1;
284         if (len <= 0) {
285             // parent is root only (may be null)
286             return getRoot();
287         }
288         byte[] result = new byte[len];
289         System.arraycopy(path, 0, result, 0, len);
290         return new UnixPath(getFileSystem(), result);
291     }
292 
293     @Override
294     public int getNameCount() {
295         initOffsets();
296         return offsets.length;
297     }
298 
299     @Override
300     public UnixPath getName(int index) {
301         initOffsets();
302         if (index < 0)
303             throw new IllegalArgumentException();
304         if (index >= offsets.length)
305             throw new IllegalArgumentException();
306 
307         int begin = offsets[index];
308         int len;
309         if (index == (offsets.length-1)) {
310             len = path.length - begin;
311         } else {
312             len = offsets[index+1] - begin - 1;
313         }
314 
315         // construct result
316         byte[] result = new byte[len];
317         System.arraycopy(path, begin, result, 0, len);
318         return new UnixPath(getFileSystem(), result);
319     }
320 
321     @Override
322     public UnixPath subpath(int beginIndex, int endIndex) {
323         initOffsets();
324 
325         if (beginIndex < 0)
326             throw new IllegalArgumentException();
327         if (beginIndex >= offsets.length)
328             throw new IllegalArgumentException();
329         if (endIndex > offsets.length)
330             throw new IllegalArgumentException();
331         if (beginIndex >= endIndex) {
332             throw new IllegalArgumentException();
333         }
334 
335         // starting offset and length
336         int begin = offsets[beginIndex];
337         int len;
338         if (endIndex == offsets.length) {
339             len = path.length - begin;
340         } else {
341             len = offsets[endIndex] - begin - 1;
342         }
343 
344         // construct result
345         byte[] result = new byte[len];
346         System.arraycopy(path, begin, result, 0, len);
347         return new UnixPath(getFileSystem(), result);
348     }
349 
350     @Override
351     public boolean isAbsolute() {
352         return (path.length > 0 && path[0] == '/');
353     }
354 
355     // Resolve child against given base
356     private static byte[] resolve(byte[] base, byte[] child) {
357         int baseLength = base.length;
358         int childLength = child.length;
359         if (childLength == 0)
360             return base;
361         if (baseLength == 0 || child[0] == '/')
362             return child;
363         byte[] result;
364         if (baseLength == 1 && base[0] == '/') {
365             result = new byte[childLength + 1];
366             result[0] = '/';
367             System.arraycopy(child, 0, result, 1, childLength);
368         } else {
369             result = new byte[baseLength + 1 + childLength];
370             System.arraycopy(base, 0, result, 0, baseLength);
371             result[base.length] = '/';
372             System.arraycopy(child, 0, result, baseLength+1, childLength);
373         }
374         return result;
375     }
376 
377     @Override
378     public UnixPath resolve(Path obj) {
379         byte[] other = toUnixPath(obj).path;
380         if (other.length > 0 && other[0] == '/')
381             return ((UnixPath)obj);
382         byte[] result = resolve(path, other);
383         return new UnixPath(getFileSystem(), result);
384     }
385 
386     UnixPath resolve(byte[] other) {
387         return resolve(new UnixPath(getFileSystem(), other));
388     }
389 
390     @Override
391     public UnixPath relativize(Path obj) {
392         UnixPath child = toUnixPath(obj);
393         if (child.equals(this))
394             return emptyPath();
395 
396         // can only relativize paths of the same type
397         if (this.isAbsolute() != child.isAbsolute())
398             throw new IllegalArgumentException("'other' is different type of Path");
399 
400         // this path is the empty path
401         if (this.isEmpty())
402             return child;
403 
404         UnixPath base = this;
405         if (base.hasDotOrDotDot() || child.hasDotOrDotDot()) {
406             base = base.normalize();
407             child = child.normalize();
408         }
409 
410         int baseCount = base.getNameCount();
411         int childCount = child.getNameCount();
412 
413         // skip matching names
414         int n = Math.min(baseCount, childCount);
415         int i = 0;
416         while (i < n) {
417             if (!base.getName(i).equals(child.getName(i)))
418                 break;
419             i++;
420         }
421 
422         // remaining elements in child
423         UnixPath childRemaining;
424         boolean isChildEmpty;
425         if (i == childCount) {
426             childRemaining = emptyPath();
427             isChildEmpty = true;
428         } else {
429             childRemaining = child.subpath(i, childCount);
430             isChildEmpty = childRemaining.isEmpty();
431         }
432 
433         // matched all of base
434         if (i == baseCount) {
435             return childRemaining;
436         }
437 
438         // the remainder of base cannot contain ".."
439         UnixPath baseRemaining = base.subpath(i, baseCount);
440         if (baseRemaining.hasDotOrDotDot()) {
441             throw new IllegalArgumentException("Unable to compute relative "
442                     + " path from " + this + " to " + obj);
443         }
444         if (baseRemaining.isEmpty())
445             return childRemaining;
446 
447         // number of ".." needed
448         int dotdots = baseRemaining.getNameCount();
449         if (dotdots == 0) {
450             return childRemaining;
451         }
452 
453         // result is a  "../" for each remaining name in base followed by the
454         // remaining names in child. If the remainder is the empty path
455         // then we don't add the final trailing slash.
456         int len = dotdots*3 + childRemaining.path.length;
457         if (isChildEmpty) {
458             assert childRemaining.isEmpty();
459             len--;
460         }
461         byte[] result = new byte[len];
462         int pos = 0;
463         while (dotdots > 0) {
464             result[pos++] = (byte)'.';
465             result[pos++] = (byte)'.';
466             if (isChildEmpty) {
467                 if (dotdots > 1) result[pos++] = (byte)'/';
468             } else {
469                 result[pos++] = (byte)'/';
470             }
471             dotdots--;
472         }
473         System.arraycopy(childRemaining.path,0, result, pos,
474                              childRemaining.path.length);
475         return new UnixPath(getFileSystem(), result);
476     }
477 
478     @Override
479     public UnixPath normalize() {
480         final int count = getNameCount();
481         if (count == 0 || isEmpty())
482             return this;
483 
484         boolean[] ignore = new boolean[count];      // true => ignore name
485         int[] size = new int[count];                // length of name
486         int remaining = count;                      // number of names remaining
487         boolean hasDotDot = false;                  // has at least one ..
488         boolean isAbsolute = isAbsolute();
489 
490         // first pass:
491         //   1. compute length of names
492         //   2. mark all occurrences of "." to ignore
493         //   3. and look for any occurrences of ".."
494         for (int i=0; i<count; i++) {
495             int begin = offsets[i];
496             int len;
497             if (i == (offsets.length-1)) {
498                 len = path.length - begin;
499             } else {
500                 len = offsets[i+1] - begin - 1;
501             }
502             size[i] = len;
503 
504             if (path[begin] == '.') {
505                 if (len == 1) {
506                     ignore[i] = true;  // ignore  "."
507                     remaining--;
508                 }
509                 else {
510                     if (path[begin+1] == '.')   // ".." found
511                         hasDotDot = true;
512                 }
513             }
514         }
515 
516         // multiple passes to eliminate all occurrences of name/..
517         if (hasDotDot) {
518             int prevRemaining;
519             do {
520                 prevRemaining = remaining;
521                 int prevName = -1;
522                 for (int i=0; i<count; i++) {
523                     if (ignore[i])
524                         continue;
525 
526                     // not a ".."
527                     if (size[i] != 2) {
528                         prevName = i;
529                         continue;
530                     }
531 
532                     int begin = offsets[i];
533                     if (path[begin] != '.' || path[begin+1] != '.') {
534                         prevName = i;
535                         continue;
536                     }
537 
538                     // ".." found
539                     if (prevName >= 0) {
540                         // name/<ignored>/.. found so mark name and ".." to be
541                         // ignored
542                         ignore[prevName] = true;
543                         ignore[i] = true;
544                         remaining = remaining - 2;
545                         prevName = -1;
546                     } else {
547                         // Case: /<ignored>/.. so mark ".." as ignored
548                         if (isAbsolute) {
549                             boolean hasPrevious = false;
550                             for (int j=0; j<i; j++) {
551                                 if (!ignore[j]) {
552                                     hasPrevious = true;
553                                     break;
554                                 }
555                             }
556                             if (!hasPrevious) {
557                                 // all proceeding names are ignored
558                                 ignore[i] = true;
559                                 remaining--;
560                             }
561                         }
562                     }
563                 }
564             } while (prevRemaining > remaining);
565         }
566 
567         // no redundant names
568         if (remaining == count)
569             return this;
570 
571         // corner case - all names removed
572         if (remaining == 0) {
573             return isAbsolute ? getFileSystem().rootDirectory() : emptyPath();
574         }
575 
576         // compute length of result
577         int len = remaining - 1;
578         if (isAbsolute)
579             len++;
580 
581         for (int i=0; i<count; i++) {
582             if (!ignore[i])
583                 len += size[i];
584         }
585         byte[] result = new byte[len];
586 
587         // copy names into result
588         int pos = 0;
589         if (isAbsolute)
590             result[pos++] = '/';
591         for (int i=0; i<count; i++) {
592             if (!ignore[i]) {
593                 System.arraycopy(path, offsets[i], result, pos, size[i]);
594                 pos += size[i];
595                 if (--remaining > 0) {
596                     result[pos++] = '/';
597                 }
598             }
599         }
600         return new UnixPath(getFileSystem(), result);
601     }
602 
603     @Override
604     public boolean startsWith(Path other) {
605         if (!(Objects.requireNonNull(other) instanceof UnixPath))
606             return false;
607         UnixPath that = (UnixPath)other;
608 
609         // other path is longer
610         if (that.path.length > path.length)
611             return false;
612 
613         int thisOffsetCount = getNameCount();
614         int thatOffsetCount = that.getNameCount();
615 
616         // other path has no name elements
617         if (thatOffsetCount == 0 && this.isAbsolute()) {
618             return that.isEmpty() ? false : true;
619         }
620 
621         // given path has more elements that this path
622         if (thatOffsetCount > thisOffsetCount)
623             return false;
624 
625         // same number of elements so must be exact match
626         if ((thatOffsetCount == thisOffsetCount) &&
627             (path.length != that.path.length)) {
628             return false;
629         }
630 
631         // check offsets of elements match
632         for (int i=0; i<thatOffsetCount; i++) {
633             Integer o1 = offsets[i];
634             Integer o2 = that.offsets[i];
635             if (!o1.equals(o2))
636                 return false;
637         }
638 
639         // offsets match so need to compare bytes
640         int i=0;
641         while (i < that.path.length) {
642             if (this.path[i] != that.path[i])
643                 return false;
644             i++;
645         }
646 
647         // final check that match is on name boundary
648         if (i < path.length && this.path[i] != '/')
649             return false;
650 
651         return true;
652     }
653 
654     @Override
655     public boolean endsWith(Path other) {
656         if (!(Objects.requireNonNull(other) instanceof UnixPath))
657             return false;
658         UnixPath that = (UnixPath)other;
659 
660         int thisLen = path.length;
661         int thatLen = that.path.length;
662 
663         // other path is longer
664         if (thatLen > thisLen)
665             return false;
666 
667         // other path is the empty path
668         if (thisLen > 0 && thatLen == 0)
669             return false;
670 
671         // other path is absolute so this path must be absolute
672         if (that.isAbsolute() && !this.isAbsolute())
673             return false;
674 
675         int thisOffsetCount = getNameCount();
676         int thatOffsetCount = that.getNameCount();
677 
678         // given path has more elements that this path
679         if (thatOffsetCount > thisOffsetCount) {
680             return false;
681         } else {
682             // same number of elements
683             if (thatOffsetCount == thisOffsetCount) {
684                 if (thisOffsetCount == 0)
685                     return true;
686                 int expectedLen = thisLen;
687                 if (this.isAbsolute() && !that.isAbsolute())
688                     expectedLen--;
689                 if (thatLen != expectedLen)
690                     return false;
691             } else {
692                 // this path has more elements so given path must be relative
693                 if (that.isAbsolute())
694                     return false;
695             }
696         }
697 
698         // compare bytes
699         int thisPos = offsets[thisOffsetCount - thatOffsetCount];
700         int thatPos = that.offsets[0];
701         if ((thatLen - thatPos) != (thisLen - thisPos))
702             return false;
703         while (thatPos < thatLen) {
704             if (this.path[thisPos++] != that.path[thatPos++])
705                 return false;
706         }
707 
708         return true;
709     }
710 
711     @Override
712     public int compareTo(Path other) {
713         int len1 = path.length;
714         int len2 = ((UnixPath) other).path.length;
715 
716         int n = Math.min(len1, len2);
717         byte v1[] = path;
718         byte v2[] = ((UnixPath) other).path;
719 
720         int k = 0;
721         while (k < n) {
722             int c1 = v1[k] & 0xff;
723             int c2 = v2[k] & 0xff;
724             if (c1 != c2) {
725                 return c1 - c2;
726             }
727            k++;
728         }
729         return len1 - len2;
730     }
731 
732     @Override
733     public boolean equals(Object ob) {
734         if (ob instanceof UnixPath path) {
735             return compareTo(path) == 0;
736         }
737         return false;
738     }
739 
740     @Override
741     public int hashCode() {
742         // OK if two or more threads compute hash
743         int h = hash;
744         if (h == 0) {
745             for (int i = 0; i< path.length; i++) {
746                 h = 31*h + (path[i] & 0xff);
747             }
748             hash = h;
749         }
750         return h;
751     }
752 
753     @Override
754     public String toString() {
755         // OK if two or more threads create a String
756         if (stringValue == null) {
757             stringValue = fs.normalizeJavaPath(Util.toString(path));     // platform encoding
758         }
759         return stringValue;
760     }
761 
762     // -- file operations --
763 
764     // package-private
765     int openForAttributeAccess(boolean followLinks) throws UnixException {
766         int flags = O_RDONLY;
767         if (!followLinks) {
768             if (O_NOFOLLOW == 0)
769                 throw new UnixException
770                     ("NOFOLLOW_LINKS is not supported on this platform");
771             flags |= O_NOFOLLOW;
772         }
773         return open(this, flags, 0);
774     }
775 
776     void checkRead() {
777         @SuppressWarnings("removal")
778         SecurityManager sm = System.getSecurityManager();
779         if (sm != null)
780             sm.checkRead(getPathForPermissionCheck());
781     }
782 
783     void checkWrite() {
784         @SuppressWarnings("removal")
785         SecurityManager sm = System.getSecurityManager();
786         if (sm != null)
787             sm.checkWrite(getPathForPermissionCheck());
788     }
789 
790     void checkDelete() {
791         @SuppressWarnings("removal")
792         SecurityManager sm = System.getSecurityManager();
793         if (sm != null)
794             sm.checkDelete(getPathForPermissionCheck());
795     }
796 
797     @Override
798     public UnixPath toAbsolutePath() {
799         if (isAbsolute()) {
800             return this;
801         }
802         // The path is relative so need to resolve against default directory,
803         // taking care not to reveal the user.dir
804         @SuppressWarnings("removal")
805         SecurityManager sm = System.getSecurityManager();
806         if (sm != null) {
807             sm.checkPropertyAccess("user.dir");
808         }
809         return new UnixPath(getFileSystem(),
810             resolve(getFileSystem().defaultDirectory(), path));
811     }
812 
813     @Override
814     public Path toRealPath(LinkOption... options) throws IOException {
815         checkRead();
816 
817         UnixPath absolute = toAbsolutePath();
818 
819         // if resolving links then use realpath
820         if (Util.followLinks(options)) {
821             try {
822                 byte[] rp = realpath(absolute);
823                 return new UnixPath(getFileSystem(), rp);
824             } catch (UnixException x) {
825                 x.rethrowAsIOException(this);
826             }
827         }
828 
829         // if not resolving links then eliminate "." and also ".."
830         // where the previous element is not a link.
831         UnixPath result = fs.rootDirectory();
832         for (int i=0; i<absolute.getNameCount(); i++) {
833             UnixPath element = absolute.getName(i);
834 
835             // eliminate "."
836             if ((element.asByteArray().length == 1) && (element.asByteArray()[0] == '.'))
837                 continue;
838 
839             // cannot eliminate ".." if previous element is a link
840             if ((element.asByteArray().length == 2) && (element.asByteArray()[0] == '.') &&
841                 (element.asByteArray()[1] == '.'))
842             {
843                 UnixFileAttributes attrs = null;
844                 try {
845                     attrs = UnixFileAttributes.get(result, false);
846                 } catch (UnixException x) {
847                     x.rethrowAsIOException(result);
848                 }
849                 if (!attrs.isSymbolicLink()) {
850                     result = result.getParent();
851                     if (result == null) {
852                         result = fs.rootDirectory();
853                     }
854                     continue;
855                 }
856             }
857             result = result.resolve(element);
858         }
859 
860         // check file exists (without following links)
861         try {
862             UnixFileAttributes.get(result, false);
863         } catch (UnixException x) {
864             x.rethrowAsIOException(result);
865         }
866         return result;
867     }
868 
869     @Override
870     public URI toUri() {
871         return UnixUriUtils.toUri(this);
872     }
873 
874     @Override
875     public WatchKey register(WatchService watcher,
876                              WatchEvent.Kind<?>[] events,
877                              WatchEvent.Modifier... modifiers)
878         throws IOException
879     {
880         if (watcher == null)
881             throw new NullPointerException();
882         if (!(watcher instanceof AbstractWatchService))
883             throw new ProviderMismatchException();
884         checkRead();
885         return ((AbstractWatchService)watcher).register(this, events, modifiers);
886     }
887 }