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