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