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