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