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