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