1 /*
   2  * Copyright (c) 2009, 2020, 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.BufferedOutputStream;
  29 import java.io.ByteArrayInputStream;
  30 import java.io.ByteArrayOutputStream;
  31 import java.io.EOFException;
  32 import java.io.FilterOutputStream;
  33 import java.io.IOException;
  34 import java.io.InputStream;
  35 import java.io.OutputStream;
  36 import java.lang.Runtime.Version;
  37 import java.nio.ByteBuffer;
  38 import java.nio.MappedByteBuffer;
  39 import java.nio.channels.FileChannel;
  40 import java.nio.channels.FileLock;
  41 import java.nio.channels.ReadableByteChannel;
  42 import java.nio.channels.SeekableByteChannel;
  43 import java.nio.channels.WritableByteChannel;
  44 import java.nio.file.*;
  45 import java.nio.file.attribute.*;
  46 import java.nio.file.spi.FileSystemProvider;
  47 import java.security.AccessController;
  48 import java.security.PrivilegedAction;
  49 import java.security.PrivilegedActionException;
  50 import java.security.PrivilegedExceptionAction;
  51 import java.util.*;
  52 import java.util.concurrent.locks.ReadWriteLock;
  53 import java.util.concurrent.locks.ReentrantReadWriteLock;
  54 import java.util.function.Consumer;
  55 import java.util.function.Function;
  56 import java.util.jar.Attributes;
  57 import java.util.jar.Manifest;
  58 import java.util.regex.Pattern;
  59 import java.util.zip.CRC32;
  60 import java.util.zip.Deflater;
  61 import java.util.zip.DeflaterOutputStream;
  62 import java.util.zip.Inflater;
  63 import java.util.zip.InflaterInputStream;
  64 import java.util.zip.ZipException;
  65 
  66 import static java.lang.Boolean.TRUE;
  67 import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
  68 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
  69 import static java.nio.file.StandardOpenOption.APPEND;
  70 import static java.nio.file.StandardOpenOption.CREATE;
  71 import static java.nio.file.StandardOpenOption.CREATE_NEW;
  72 import static java.nio.file.StandardOpenOption.READ;
  73 import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
  74 import static java.nio.file.StandardOpenOption.WRITE;
  75 import static jdk.nio.zipfs.ZipConstants.*;
  76 import static jdk.nio.zipfs.ZipUtils.*;
  77 
  78 /**
  79  * A FileSystem built on a zip file
  80  *
  81  * @author Xueming Shen
  82  */
  83 class ZipFileSystem extends FileSystem {
  84     // statics
  85     private static final boolean isWindows = AccessController.doPrivileged(
  86         (PrivilegedAction<Boolean>)()->System.getProperty("os.name")
  87                                              .startsWith("Windows"));
  88     private static final byte[] ROOTPATH = new byte[] { '/' };
  89     private static final String PROPERTY_POSIX = "enablePosixFileAttributes";
  90     private static final String PROPERTY_DEFAULT_OWNER = "defaultOwner";
  91     private static final String PROPERTY_DEFAULT_GROUP = "defaultGroup";
  92     private static final String PROPERTY_DEFAULT_PERMISSIONS = "defaultPermissions";
  93     // Property used to specify the entry version to use for a multi-release JAR
  94     private static final String PROPERTY_RELEASE_VERSION = "releaseVersion";
  95     // Original property used to specify the entry version to use for a
  96     // multi-release JAR which is kept for backwards compatibility.
  97     private static final String PROPERTY_MULTI_RELEASE = "multi-release";
  98 
  99     private static final Set<PosixFilePermission> DEFAULT_PERMISSIONS =
 100         PosixFilePermissions.fromString("rwxrwxrwx");
 101     // Property used to specify the compression mode to use
 102     private static final String PROPERTY_COMPRESSION_METHOD = "compressionMethod";
 103     // Value specified for compressionMethod property to compress Zip entries
 104     private static final String COMPRESSION_METHOD_DEFLATED = "DEFLATED";
 105     // Value specified for compressionMethod property to not compress Zip entries
 106     private static final String COMPRESSION_METHOD_STORED = "STORED";
 107 
 108     private final ZipFileSystemProvider provider;
 109     private final Path zfpath;
 110     final ZipCoder zc;
 111     private final ZipPath rootdir;
 112     private boolean readOnly; // readonly file system, false by default
 113 
 114     // default time stamp for pseudo entries
 115     private final long zfsDefaultTimeStamp = System.currentTimeMillis();
 116 
 117     // configurable by env map
 118     private final boolean noExtt;        // see readExtra()
 119     private final boolean useTempFile;   // use a temp file for newOS, default
 120                                          // is to use BAOS for better performance
 121     private final boolean forceEnd64;
 122     private final int defaultCompressionMethod; // METHOD_STORED if "noCompression=true"
 123                                                 // METHOD_DEFLATED otherwise
 124 
 125     // entryLookup is identity by default, will be overridden for multi-release jars
 126     private Function<byte[], byte[]> entryLookup = Function.identity();
 127 
 128     // POSIX support
 129     final boolean supportPosix;
 130     private final UserPrincipal defaultOwner;
 131     private final GroupPrincipal defaultGroup;
 132     private final Set<PosixFilePermission> defaultPermissions;
 133 
 134     private final Set<String> supportedFileAttributeViews;
 135 
 136     ZipFileSystem(ZipFileSystemProvider provider,
 137                   Path zfpath,
 138                   Map<String, ?> env) throws IOException
 139     {
 140         // default encoding for name/comment
 141         String nameEncoding = env.containsKey("encoding") ?
 142             (String)env.get("encoding") : "UTF-8";
 143         this.noExtt = "false".equals(env.get("zipinfo-time"));
 144         this.useTempFile  = isTrue(env, "useTempFile");
 145         this.forceEnd64 = isTrue(env, "forceZIP64End");
 146         this.defaultCompressionMethod = getDefaultCompressionMethod(env);
 147         this.supportPosix = isTrue(env, PROPERTY_POSIX);
 148         this.defaultOwner = initOwner(zfpath, env);
 149         this.defaultGroup = initGroup(zfpath, env);
 150         this.defaultPermissions = initPermissions(env);
 151         this.supportedFileAttributeViews = supportPosix ?
 152             Set.of("basic", "posix", "zip") : Set.of("basic", "zip");
 153         if (Files.notExists(zfpath)) {
 154             // create a new zip if it doesn't exist
 155             if (isTrue(env, "create")) {
 156                 try (OutputStream os = Files.newOutputStream(zfpath, CREATE_NEW, WRITE)) {
 157                     new END().write(os, 0, forceEnd64);
 158                 }
 159             } else {
 160                 throw new NoSuchFileException(zfpath.toString());
 161             }
 162         }
 163         // sm and existence check
 164         zfpath.getFileSystem().provider().checkAccess(zfpath, AccessMode.READ);
 165         boolean writeable = AccessController.doPrivileged(
 166             (PrivilegedAction<Boolean>)()->Files.isWritable(zfpath));
 167         this.readOnly = !writeable;
 168         this.zc = ZipCoder.get(nameEncoding);
 169         this.rootdir = new ZipPath(this, new byte[]{'/'});
 170         this.ch = Files.newByteChannel(zfpath, READ);
 171         try {
 172             this.cen = initCEN();
 173         } catch (IOException x) {
 174             try {
 175                 this.ch.close();
 176             } catch (IOException xx) {
 177                 x.addSuppressed(xx);
 178             }
 179             throw x;
 180         }
 181         this.provider = provider;
 182         this.zfpath = zfpath;
 183 
 184         initializeReleaseVersion(env);
 185     }
 186 
 187     /**
 188      * Return the compression method to use (STORED or DEFLATED).  If the
 189      * property {@code commpressionMethod} is set use its value to determine
 190      * the compression method to use.  If the property is not set, then the
 191      * default compression is DEFLATED unless the property {@code noCompression}
 192      * is set which is supported for backwards compatibility.
 193      * @param env Zip FS map of properties
 194      * @return The Compression method to use
 195      */
 196     private int getDefaultCompressionMethod(Map<String, ?> env) {
 197         int result =
 198                 isTrue(env, "noCompression") ? METHOD_STORED : METHOD_DEFLATED;
 199         if (env.containsKey(PROPERTY_COMPRESSION_METHOD)) {
 200             Object compressionMethod =  env.get(PROPERTY_COMPRESSION_METHOD);
 201             if (compressionMethod != null) {
 202                 if (compressionMethod instanceof String) {
 203                     switch (((String) compressionMethod).toUpperCase()) {
 204                         case COMPRESSION_METHOD_STORED:
 205                             result = METHOD_STORED;
 206                             break;
 207                         case COMPRESSION_METHOD_DEFLATED:
 208                             result = METHOD_DEFLATED;
 209                             break;
 210                         default:
 211                             throw new IllegalArgumentException(String.format(
 212                                     "The value for the %s property must be %s or %s",
 213                                     PROPERTY_COMPRESSION_METHOD, COMPRESSION_METHOD_STORED,
 214                                     COMPRESSION_METHOD_DEFLATED));
 215                     }
 216                 } else {
 217                     throw new IllegalArgumentException(String.format(
 218                             "The Object type for the %s property must be a String",
 219                             PROPERTY_COMPRESSION_METHOD));
 220                 }
 221             } else {
 222                 throw new IllegalArgumentException(String.format(
 223                         "The value for the %s property must be %s or %s",
 224                         PROPERTY_COMPRESSION_METHOD, COMPRESSION_METHOD_STORED,
 225                         COMPRESSION_METHOD_DEFLATED));
 226             }
 227         }
 228         return result;
 229     }
 230 
 231     // returns true if there is a name=true/"true" setting in env
 232     private static boolean isTrue(Map<String, ?> env, String name) {
 233         return "true".equals(env.get(name)) || TRUE.equals(env.get(name));
 234     }
 235 
 236     // Initialize the default owner for files inside the zip archive.
 237     // If not specified in env, it is the owner of the archive. If no owner can
 238     // be determined, we try to go with system property "user.name". If that's not
 239     // accessible, we return "<zipfs_default>".
 240     private UserPrincipal initOwner(Path zfpath, Map<String, ?> env) throws IOException {
 241         Object o = env.get(PROPERTY_DEFAULT_OWNER);
 242         if (o == null) {
 243             try {
 244                 PrivilegedExceptionAction<UserPrincipal> pa = ()->Files.getOwner(zfpath);
 245                 return AccessController.doPrivileged(pa);
 246             } catch (UnsupportedOperationException | PrivilegedActionException e) {
 247                 if (e instanceof UnsupportedOperationException ||
 248                     e.getCause() instanceof NoSuchFileException)
 249                 {
 250                     PrivilegedAction<String> pa = ()->System.getProperty("user.name");
 251                     String userName = AccessController.doPrivileged(pa);
 252                     return ()->userName;
 253                 } else {
 254                     throw new IOException(e);
 255                 }
 256             }
 257         }
 258         if (o instanceof String) {
 259             if (((String)o).isEmpty()) {
 260                 throw new IllegalArgumentException("Value for property " +
 261                         PROPERTY_DEFAULT_OWNER + " must not be empty.");
 262             }
 263             return ()->(String)o;
 264         }
 265         if (o instanceof UserPrincipal) {
 266             return (UserPrincipal)o;
 267         }
 268         throw new IllegalArgumentException("Value for property " +
 269                 PROPERTY_DEFAULT_OWNER + " must be of type " + String.class +
 270             " or " + UserPrincipal.class);
 271     }
 272 
 273     // Initialize the default group for files inside the zip archive.
 274     // If not specified in env, we try to determine the group of the zip archive itself.
 275     // If this is not possible/unsupported, we will return a group principal going by
 276     // the same name as the default owner.
 277     private GroupPrincipal initGroup(Path zfpath, Map<String, ?> env) throws IOException {
 278         Object o = env.get(PROPERTY_DEFAULT_GROUP);
 279         if (o == null) {
 280             try {
 281                 PosixFileAttributeView zfpv = Files.getFileAttributeView(zfpath, PosixFileAttributeView.class);
 282                 if (zfpv == null) {
 283                     return defaultOwner::getName;
 284                 }
 285                 PrivilegedExceptionAction<GroupPrincipal> pa = ()->zfpv.readAttributes().group();
 286                 return AccessController.doPrivileged(pa);
 287             } catch (UnsupportedOperationException | PrivilegedActionException e) {
 288                 if (e instanceof UnsupportedOperationException ||
 289                     e.getCause() instanceof NoSuchFileException)
 290                 {
 291                     return defaultOwner::getName;
 292                 } else {
 293                     throw new IOException(e);
 294                 }
 295             }
 296         }
 297         if (o instanceof String) {
 298             if (((String)o).isEmpty()) {
 299                 throw new IllegalArgumentException("Value for property " +
 300                         PROPERTY_DEFAULT_GROUP + " must not be empty.");
 301             }
 302             return ()->(String)o;
 303         }
 304         if (o instanceof GroupPrincipal) {
 305             return (GroupPrincipal)o;
 306         }
 307         throw new IllegalArgumentException("Value for property " +
 308                 PROPERTY_DEFAULT_GROUP + " must be of type " + String.class +
 309             " or " + GroupPrincipal.class);
 310     }
 311 
 312     // Initialize the default permissions for files inside the zip archive.
 313     // If not specified in env, it will return 777.
 314     private Set<PosixFilePermission> initPermissions(Map<String, ?> env) {
 315         Object o = env.get(PROPERTY_DEFAULT_PERMISSIONS);
 316         if (o == null) {
 317             return DEFAULT_PERMISSIONS;
 318         }
 319         if (o instanceof String) {
 320             return PosixFilePermissions.fromString((String)o);
 321         }
 322         if (!(o instanceof Set)) {
 323             throw new IllegalArgumentException("Value for property " +
 324                 PROPERTY_DEFAULT_PERMISSIONS + " must be of type " + String.class +
 325                 " or " + Set.class);
 326         }
 327         Set<PosixFilePermission> perms = new HashSet<>();
 328         for (Object o2 : (Set<?>)o) {
 329             if (o2 instanceof PosixFilePermission) {
 330                 perms.add((PosixFilePermission)o2);
 331             } else {
 332                 throw new IllegalArgumentException(PROPERTY_DEFAULT_PERMISSIONS +
 333                     " must only contain objects of type " + PosixFilePermission.class);
 334             }
 335         }
 336         return perms;
 337     }
 338 
 339     @Override
 340     public FileSystemProvider provider() {
 341         return provider;
 342     }
 343 
 344     @Override
 345     public String getSeparator() {
 346         return "/";
 347     }
 348 
 349     @Override
 350     public boolean isOpen() {
 351         return isOpen;
 352     }
 353 
 354     @Override
 355     public boolean isReadOnly() {
 356         return readOnly;
 357     }
 358 
 359     private void checkWritable() {
 360         if (readOnly) {
 361             throw new ReadOnlyFileSystemException();
 362         }
 363     }
 364 
 365     void setReadOnly() {
 366         this.readOnly = true;
 367     }
 368 
 369     @Override
 370     public Iterable<Path> getRootDirectories() {
 371         return List.of(rootdir);
 372     }
 373 
 374     ZipPath getRootDir() {
 375         return rootdir;
 376     }
 377 
 378     @Override
 379     public ZipPath getPath(String first, String... more) {
 380         if (more.length == 0) {
 381             return new ZipPath(this, first);
 382         }
 383         StringBuilder sb = new StringBuilder();
 384         sb.append(first);
 385         for (String path : more) {
 386             if (path.length() > 0) {
 387                 if (sb.length() > 0) {
 388                     sb.append('/');
 389                 }
 390                 sb.append(path);
 391             }
 392         }
 393         return new ZipPath(this, sb.toString());
 394     }
 395 
 396     @Override
 397     public UserPrincipalLookupService getUserPrincipalLookupService() {
 398         throw new UnsupportedOperationException();
 399     }
 400 
 401     @Override
 402     public WatchService newWatchService() {
 403         throw new UnsupportedOperationException();
 404     }
 405 
 406     FileStore getFileStore(ZipPath path) {
 407         return new ZipFileStore(path);
 408     }
 409 
 410     @Override
 411     public Iterable<FileStore> getFileStores() {
 412         return List.of(new ZipFileStore(rootdir));
 413     }
 414 
 415     @Override
 416     public Set<String> supportedFileAttributeViews() {
 417         return supportedFileAttributeViews;
 418     }
 419 
 420     @Override
 421     public String toString() {
 422         return zfpath.toString();
 423     }
 424 
 425     Path getZipFile() {
 426         return zfpath;
 427     }
 428 
 429     private static final String GLOB_SYNTAX = "glob";
 430     private static final String REGEX_SYNTAX = "regex";
 431 
 432     @Override
 433     public PathMatcher getPathMatcher(String syntaxAndInput) {
 434         int pos = syntaxAndInput.indexOf(':');
 435         if (pos <= 0 || pos == syntaxAndInput.length()) {
 436             throw new IllegalArgumentException();
 437         }
 438         String syntax = syntaxAndInput.substring(0, pos);
 439         String input = syntaxAndInput.substring(pos + 1);
 440         String expr;
 441         if (syntax.equalsIgnoreCase(GLOB_SYNTAX)) {
 442             expr = toRegexPattern(input);
 443         } else {
 444             if (syntax.equalsIgnoreCase(REGEX_SYNTAX)) {
 445                 expr = input;
 446             } else {
 447                 throw new UnsupportedOperationException("Syntax '" + syntax +
 448                     "' not recognized");
 449             }
 450         }
 451         // return matcher
 452         final Pattern pattern = Pattern.compile(expr);
 453         return (path)->pattern.matcher(path.toString()).matches();
 454     }
 455 
 456     @Override
 457     public void close() throws IOException {
 458         beginWrite();
 459         try {
 460             if (!isOpen)
 461                 return;
 462             isOpen = false;          // set closed
 463         } finally {
 464             endWrite();
 465         }
 466         if (!streams.isEmpty()) {    // unlock and close all remaining streams
 467             Set<InputStream> copy = new HashSet<>(streams);
 468             for (InputStream is : copy)
 469                 is.close();
 470         }
 471         beginWrite();                // lock and sync
 472         try {
 473             AccessController.doPrivileged((PrivilegedExceptionAction<Void>)() -> {
 474                 sync(); return null;
 475             });
 476             ch.close();              // close the ch just in case no update
 477                                      // and sync didn't close the ch
 478         } catch (PrivilegedActionException e) {
 479             throw (IOException)e.getException();
 480         } finally {
 481             endWrite();
 482         }
 483 
 484         synchronized (inflaters) {
 485             for (Inflater inf : inflaters)
 486                 inf.end();
 487         }
 488         synchronized (deflaters) {
 489             for (Deflater def : deflaters)
 490                 def.end();
 491         }
 492 
 493         beginWrite();                // lock and sync
 494         try {
 495             // Clear the map so that its keys & values can be garbage collected
 496             inodes = null;
 497         } finally {
 498             endWrite();
 499         }
 500 
 501         IOException ioe = null;
 502         synchronized (tmppaths) {
 503             for (Path p : tmppaths) {
 504                 try {
 505                     AccessController.doPrivileged(
 506                         (PrivilegedExceptionAction<Boolean>)() -> Files.deleteIfExists(p));
 507                 } catch (PrivilegedActionException e) {
 508                     IOException x = (IOException)e.getException();
 509                     if (ioe == null)
 510                         ioe = x;
 511                     else
 512                         ioe.addSuppressed(x);
 513                 }
 514             }
 515         }
 516         provider.removeFileSystem(zfpath, this);
 517         if (ioe != null)
 518            throw ioe;
 519     }
 520 
 521     ZipFileAttributes getFileAttributes(byte[] path)
 522         throws IOException
 523     {
 524         beginRead();
 525         try {
 526             ensureOpen();
 527             IndexNode inode = getInode(path);
 528             if (inode == null) {
 529                 return null;
 530             } else if (inode instanceof Entry) {
 531                 return (Entry)inode;
 532             } else if (inode.pos == -1) {
 533                 // pseudo directory, uses METHOD_STORED
 534                 Entry e = supportPosix ?
 535                     new PosixEntry(inode.name, inode.isdir, METHOD_STORED) :
 536                     new Entry(inode.name, inode.isdir, METHOD_STORED);
 537                 e.mtime = e.atime = e.ctime = zfsDefaultTimeStamp;
 538                 return e;
 539             } else {
 540                 return supportPosix ? new PosixEntry(this, inode) : new Entry(this, inode);
 541             }
 542         } finally {
 543             endRead();
 544         }
 545     }
 546 
 547     void checkAccess(byte[] path) throws IOException {
 548         beginRead();
 549         try {
 550             ensureOpen();
 551             // is it necessary to readCEN as a sanity check?
 552             if (getInode(path) == null) {
 553                 throw new NoSuchFileException(toString());
 554             }
 555 
 556         } finally {
 557             endRead();
 558         }
 559     }
 560 
 561     void setTimes(byte[] path, FileTime mtime, FileTime atime, FileTime ctime)
 562         throws IOException
 563     {
 564         checkWritable();
 565         beginWrite();
 566         try {
 567             ensureOpen();
 568             Entry e = getEntry(path);    // ensureOpen checked
 569             if (e == null)
 570                 throw new NoSuchFileException(getString(path));
 571             if (e.type == Entry.CEN)
 572                 e.type = Entry.COPY;     // copy e
 573             if (mtime != null)
 574                 e.mtime = mtime.toMillis();
 575             if (atime != null)
 576                 e.atime = atime.toMillis();
 577             if (ctime != null)
 578                 e.ctime = ctime.toMillis();
 579             update(e);
 580         } finally {
 581             endWrite();
 582         }
 583     }
 584 
 585     void setOwner(byte[] path, UserPrincipal owner) throws IOException {
 586         checkWritable();
 587         beginWrite();
 588         try {
 589             ensureOpen();
 590             Entry e = getEntry(path);    // ensureOpen checked
 591             if (e == null) {
 592                 throw new NoSuchFileException(getString(path));
 593             }
 594             // as the owner information is not persistent, we don't need to
 595             // change e.type to Entry.COPY
 596             if (e instanceof PosixEntry) {
 597                 ((PosixEntry)e).owner = owner;
 598                 update(e);
 599             }
 600         } finally {
 601             endWrite();
 602         }
 603     }
 604 
 605     void setGroup(byte[] path, GroupPrincipal group) throws IOException {
 606         checkWritable();
 607         beginWrite();
 608         try {
 609             ensureOpen();
 610             Entry e = getEntry(path);    // ensureOpen checked
 611             if (e == null) {
 612                 throw new NoSuchFileException(getString(path));
 613             }
 614             // as the group information is not persistent, we don't need to
 615             // change e.type to Entry.COPY
 616             if (e instanceof PosixEntry) {
 617                 ((PosixEntry)e).group = group;
 618                 update(e);
 619             }
 620         } finally {
 621             endWrite();
 622         }
 623     }
 624 
 625     void setPermissions(byte[] path, Set<PosixFilePermission> perms) throws IOException {
 626         checkWritable();
 627         beginWrite();
 628         try {
 629             ensureOpen();
 630             Entry e = getEntry(path);    // ensureOpen checked
 631             if (e == null) {
 632                 throw new NoSuchFileException(getString(path));
 633             }
 634             if (e.type == Entry.CEN) {
 635                 e.type = Entry.COPY;     // copy e
 636             }
 637             e.posixPerms = perms == null ? -1 : ZipUtils.permsToFlags(perms);
 638             update(e);
 639         } finally {
 640             endWrite();
 641         }
 642     }
 643 
 644     boolean exists(byte[] path) {
 645         beginRead();
 646         try {
 647             ensureOpen();
 648             return getInode(path) != null;
 649         } finally {
 650             endRead();
 651         }
 652     }
 653 
 654     boolean isDirectory(byte[] path) {
 655         beginRead();
 656         try {
 657             IndexNode n = getInode(path);
 658             return n != null && n.isDir();
 659         } finally {
 660             endRead();
 661         }
 662     }
 663 
 664     // returns the list of child paths of "path"
 665     Iterator<Path> iteratorOf(ZipPath dir,
 666                               DirectoryStream.Filter<? super Path> filter)
 667         throws IOException
 668     {
 669         beginWrite();    // iteration of inodes needs exclusive lock
 670         try {
 671             ensureOpen();
 672             byte[] path = dir.getResolvedPath();
 673             IndexNode inode = getInode(path);
 674             if (inode == null)
 675                 throw new NotDirectoryException(getString(path));
 676             List<Path> list = new ArrayList<>();
 677             IndexNode child = inode.child;
 678             while (child != null) {
 679                 // (1) Assume each path from the zip file itself is "normalized"
 680                 // (2) IndexNode.name is absolute. see IndexNode(byte[],int,int)
 681                 // (3) If parent "dir" is relative when ZipDirectoryStream
 682                 //     is created, the returned child path needs to be relative
 683                 //     as well.
 684                 ZipPath childPath = new ZipPath(this, child.name, true);
 685                 ZipPath childFileName = childPath.getFileName();
 686                 ZipPath zpath = dir.resolve(childFileName);
 687                 if (filter == null || filter.accept(zpath))
 688                     list.add(zpath);
 689                 child = child.sibling;
 690             }
 691             return list.iterator();
 692         } finally {
 693             endWrite();
 694         }
 695     }
 696 
 697     void createDirectory(byte[] dir, FileAttribute<?>... attrs) throws IOException {
 698         checkWritable();
 699         beginWrite();
 700         try {
 701             ensureOpen();
 702             if (dir.length == 0 || exists(dir))  // root dir, or existing dir
 703                 throw new FileAlreadyExistsException(getString(dir));
 704             checkParents(dir);
 705             Entry e = supportPosix ?
 706                 new PosixEntry(dir, Entry.NEW, true, METHOD_STORED, attrs) :
 707                 new Entry(dir, Entry.NEW, true, METHOD_STORED, attrs);
 708             update(e);
 709         } finally {
 710             endWrite();
 711         }
 712     }
 713 
 714     void copyFile(boolean deletesrc, byte[]src, byte[] dst, CopyOption... options)
 715         throws IOException
 716     {
 717         checkWritable();
 718         if (Arrays.equals(src, dst))
 719             return;    // do nothing, src and dst are the same
 720 
 721         beginWrite();
 722         try {
 723             ensureOpen();
 724             Entry eSrc = getEntry(src);  // ensureOpen checked
 725 
 726             if (eSrc == null)
 727                 throw new NoSuchFileException(getString(src));
 728             if (eSrc.isDir()) {    // spec says to create dst dir
 729                 createDirectory(dst);
 730                 return;
 731             }
 732             boolean hasReplace = false;
 733             boolean hasCopyAttrs = false;
 734             for (CopyOption opt : options) {
 735                 if (opt == REPLACE_EXISTING)
 736                     hasReplace = true;
 737                 else if (opt == COPY_ATTRIBUTES)
 738                     hasCopyAttrs = true;
 739             }
 740             Entry eDst = getEntry(dst);
 741             if (eDst != null) {
 742                 if (!hasReplace)
 743                     throw new FileAlreadyExistsException(getString(dst));
 744             } else {
 745                 checkParents(dst);
 746             }
 747             // copy eSrc entry and change name
 748             Entry u = supportPosix ?
 749                 new PosixEntry((PosixEntry)eSrc, Entry.COPY) :
 750                 new Entry(eSrc, Entry.COPY);
 751             u.name(dst);
 752             if (eSrc.type == Entry.NEW || eSrc.type == Entry.FILECH) {
 753                 u.type = eSrc.type;    // make it the same type
 754                 if (deletesrc) {       // if it's a "rename", take the data
 755                     u.bytes = eSrc.bytes;
 756                     u.file = eSrc.file;
 757                 } else {               // if it's not "rename", copy the data
 758                     if (eSrc.bytes != null)
 759                         u.bytes = Arrays.copyOf(eSrc.bytes, eSrc.bytes.length);
 760                     else if (eSrc.file != null) {
 761                         u.file = getTempPathForEntry(null);
 762                         Files.copy(eSrc.file, u.file, REPLACE_EXISTING);
 763                     }
 764                 }
 765             } else if (eSrc.type == Entry.CEN && eSrc.method != defaultCompressionMethod) {
 766 
 767                 /**
 768                  * We are copying a file within the same Zip file using a
 769                  * different compression method.
 770                  */
 771                 try (InputStream in = newInputStream(src);
 772                      OutputStream out = newOutputStream(dst,
 773                              CREATE, TRUNCATE_EXISTING, WRITE)) {
 774                     in.transferTo(out);
 775                 }
 776                 u = getEntry(dst);
 777             }
 778 
 779             if (!hasCopyAttrs)
 780                 u.mtime = u.atime= u.ctime = System.currentTimeMillis();
 781             update(u);
 782             if (deletesrc)
 783                 updateDelete(eSrc);
 784         } finally {
 785             endWrite();
 786         }
 787     }
 788 
 789     // Returns an output stream for writing the contents into the specified
 790     // entry.
 791     OutputStream newOutputStream(byte[] path, OpenOption... options)
 792         throws IOException
 793     {
 794         checkWritable();
 795         boolean hasCreateNew = false;
 796         boolean hasCreate = false;
 797         boolean hasAppend = false;
 798         boolean hasTruncate = false;
 799         for (OpenOption opt : options) {
 800             if (opt == READ)
 801                 throw new IllegalArgumentException("READ not allowed");
 802             if (opt == CREATE_NEW)
 803                 hasCreateNew = true;
 804             if (opt == CREATE)
 805                 hasCreate = true;
 806             if (opt == APPEND)
 807                 hasAppend = true;
 808             if (opt == TRUNCATE_EXISTING)
 809                 hasTruncate = true;
 810         }
 811         if (hasAppend && hasTruncate)
 812             throw new IllegalArgumentException("APPEND + TRUNCATE_EXISTING not allowed");
 813         beginRead();                 // only need a readlock, the "update()" will
 814         try {                        // try to obtain a writelock when the os is
 815             ensureOpen();            // being closed.
 816             Entry e = getEntry(path);
 817             if (e != null) {
 818                 if (e.isDir() || hasCreateNew)
 819                     throw new FileAlreadyExistsException(getString(path));
 820                 if (hasAppend) {
 821                     OutputStream os = getOutputStream(new Entry(e, Entry.NEW));
 822                     try (InputStream is = getInputStream(e)) {
 823                         is.transferTo(os);
 824                     }
 825                     return os;
 826                 }
 827                 return getOutputStream(supportPosix ?
 828                     new PosixEntry((PosixEntry)e, Entry.NEW, defaultCompressionMethod)
 829                         : new Entry(e, Entry.NEW, defaultCompressionMethod));
 830             } else {
 831                 if (!hasCreate && !hasCreateNew)
 832                     throw new NoSuchFileException(getString(path));
 833                 checkParents(path);
 834                 return getOutputStream(supportPosix ?
 835                     new PosixEntry(path, Entry.NEW, false, defaultCompressionMethod) :
 836                     new Entry(path, Entry.NEW, false, defaultCompressionMethod));
 837             }
 838         } finally {
 839             endRead();
 840         }
 841     }
 842 
 843     // Returns an input stream for reading the contents of the specified
 844     // file entry.
 845     InputStream newInputStream(byte[] path) throws IOException {
 846         beginRead();
 847         try {
 848             ensureOpen();
 849             Entry e = getEntry(path);
 850             if (e == null)
 851                 throw new NoSuchFileException(getString(path));
 852             if (e.isDir())
 853                 throw new FileSystemException(getString(path), "is a directory", null);
 854             return getInputStream(e);
 855         } finally {
 856             endRead();
 857         }
 858     }
 859 
 860     private void checkOptions(Set<? extends OpenOption> options) {
 861         // check for options of null type and option is an intance of StandardOpenOption
 862         for (OpenOption option : options) {
 863             if (option == null)
 864                 throw new NullPointerException();
 865             if (!(option instanceof StandardOpenOption))
 866                 throw new IllegalArgumentException();
 867         }
 868         if (options.contains(APPEND) && options.contains(TRUNCATE_EXISTING))
 869             throw new IllegalArgumentException("APPEND + TRUNCATE_EXISTING not allowed");
 870     }
 871 
 872     // Returns an output SeekableByteChannel for either
 873     // (1) writing the contents of a new entry, if the entry doesn't exist, or
 874     // (2) updating/replacing the contents of an existing entry.
 875     // Note: The content of the channel is not compressed until the
 876     // channel is closed
 877     private class EntryOutputChannel extends ByteArrayChannel {
 878         final Entry e;
 879 
 880         EntryOutputChannel(Entry e) {
 881             super(e.size > 0? (int)e.size : 8192, false);
 882             this.e = e;
 883             if (e.mtime == -1)
 884                 e.mtime = System.currentTimeMillis();
 885             if (e.method == -1)
 886                 e.method = defaultCompressionMethod;
 887             // store size, compressed size, and crc-32 in datadescriptor
 888             e.flag = FLAG_DATADESCR;
 889             if (zc.isUTF8())
 890                 e.flag |= FLAG_USE_UTF8;
 891         }
 892 
 893         @Override
 894         public void close() throws IOException {
 895             // will update the entry
 896             try (OutputStream os = getOutputStream(e)) {
 897                 os.write(toByteArray());
 898             }
 899             super.close();
 900         }
 901     }
 902 
 903     // Returns a Writable/ReadByteChannel for now. Might consider to use
 904     // newFileChannel() instead, which dump the entry data into a regular
 905     // file on the default file system and create a FileChannel on top of it.
 906     SeekableByteChannel newByteChannel(byte[] path,
 907                                        Set<? extends OpenOption> options,
 908                                        FileAttribute<?>... attrs)
 909         throws IOException
 910     {
 911         checkOptions(options);
 912         if (options.contains(StandardOpenOption.WRITE) ||
 913             options.contains(StandardOpenOption.APPEND)) {
 914             checkWritable();
 915             beginRead();    // only need a read lock, the "update()" will obtain
 916                             // the write lock when the channel is closed
 917             try {
 918                 Entry e = getEntry(path);
 919                 if (e != null) {
 920                     if (e.isDir() || options.contains(CREATE_NEW))
 921                         throw new FileAlreadyExistsException(getString(path));
 922                     SeekableByteChannel sbc =
 923                             new EntryOutputChannel(supportPosix ?
 924                                 new PosixEntry((PosixEntry)e, Entry.NEW) :
 925                                 new Entry(e, Entry.NEW));
 926                     if (options.contains(APPEND)) {
 927                         try (InputStream is = getInputStream(e)) {  // copyover
 928                             byte[] buf = new byte[8192];
 929                             ByteBuffer bb = ByteBuffer.wrap(buf);
 930                             int n;
 931                             while ((n = is.read(buf)) != -1) {
 932                                 bb.position(0);
 933                                 bb.limit(n);
 934                                 sbc.write(bb);
 935                             }
 936                         }
 937                     }
 938                     return sbc;
 939                 }
 940                 if (!options.contains(CREATE) && !options.contains(CREATE_NEW))
 941                     throw new NoSuchFileException(getString(path));
 942                 checkParents(path);
 943                 return new EntryOutputChannel(
 944                     supportPosix ?
 945                         new PosixEntry(path, Entry.NEW, false, defaultCompressionMethod, attrs) :
 946                         new Entry(path, Entry.NEW, false, defaultCompressionMethod, attrs));
 947             } finally {
 948                 endRead();
 949             }
 950         } else {
 951             beginRead();
 952             try {
 953                 ensureOpen();
 954                 Entry e = getEntry(path);
 955                 if (e == null || e.isDir())
 956                     throw new NoSuchFileException(getString(path));
 957                 try (InputStream is = getInputStream(e)) {
 958                     // TBD: if (e.size < NNNNN);
 959                     return new ByteArrayChannel(is.readAllBytes(), true);
 960                 }
 961             } finally {
 962                 endRead();
 963             }
 964         }
 965     }
 966 
 967     // Returns a FileChannel of the specified entry.
 968     //
 969     // This implementation creates a temporary file on the default file system,
 970     // copy the entry data into it if the entry exists, and then create a
 971     // FileChannel on top of it.
 972     FileChannel newFileChannel(byte[] path,
 973                                Set<? extends OpenOption> options,
 974                                FileAttribute<?>... attrs)
 975         throws IOException
 976     {
 977         checkOptions(options);
 978         final  boolean forWrite = (options.contains(StandardOpenOption.WRITE) ||
 979                                    options.contains(StandardOpenOption.APPEND));
 980         beginRead();
 981         try {
 982             ensureOpen();
 983             Entry e = getEntry(path);
 984             if (forWrite) {
 985                 checkWritable();
 986                 if (e == null) {
 987                     if (!options.contains(StandardOpenOption.CREATE) &&
 988                         !options.contains(StandardOpenOption.CREATE_NEW)) {
 989                         throw new NoSuchFileException(getString(path));
 990                     }
 991                 } else {
 992                     if (options.contains(StandardOpenOption.CREATE_NEW)) {
 993                         throw new FileAlreadyExistsException(getString(path));
 994                     }
 995                     if (e.isDir())
 996                         throw new FileAlreadyExistsException("directory <"
 997                             + getString(path) + "> exists");
 998                 }
 999                 options = new HashSet<>(options);
1000                 options.remove(StandardOpenOption.CREATE_NEW); // for tmpfile
1001             } else if (e == null || e.isDir()) {
1002                 throw new NoSuchFileException(getString(path));
1003             }
1004 
1005             final boolean isFCH = (e != null && e.type == Entry.FILECH);
1006             final Path tmpfile = isFCH ? e.file : getTempPathForEntry(path);
1007             final FileChannel fch = tmpfile.getFileSystem()
1008                                            .provider()
1009                                            .newFileChannel(tmpfile, options, attrs);
1010             final Entry u = isFCH ? e : (
1011                 supportPosix ?
1012                 new PosixEntry(path, tmpfile, Entry.FILECH, attrs) :
1013                 new Entry(path, tmpfile, Entry.FILECH, attrs));
1014             if (forWrite) {
1015                 u.flag = FLAG_DATADESCR;
1016                 u.method = defaultCompressionMethod;
1017             }
1018             // is there a better way to hook into the FileChannel's close method?
1019             return new FileChannel() {
1020                 public int write(ByteBuffer src) throws IOException {
1021                     return fch.write(src);
1022                 }
1023                 public long write(ByteBuffer[] srcs, int offset, int length)
1024                     throws IOException
1025                 {
1026                     return fch.write(srcs, offset, length);
1027                 }
1028                 public long position() throws IOException {
1029                     return fch.position();
1030                 }
1031                 public FileChannel position(long newPosition)
1032                     throws IOException
1033                 {
1034                     fch.position(newPosition);
1035                     return this;
1036                 }
1037                 public long size() throws IOException {
1038                     return fch.size();
1039                 }
1040                 public FileChannel truncate(long size)
1041                     throws IOException
1042                 {
1043                     fch.truncate(size);
1044                     return this;
1045                 }
1046                 public void force(boolean metaData)
1047                     throws IOException
1048                 {
1049                     fch.force(metaData);
1050                 }
1051                 public long transferTo(long position, long count,
1052                                        WritableByteChannel target)
1053                     throws IOException
1054                 {
1055                     return fch.transferTo(position, count, target);
1056                 }
1057                 public long transferFrom(ReadableByteChannel src,
1058                                          long position, long count)
1059                     throws IOException
1060                 {
1061                     return fch.transferFrom(src, position, count);
1062                 }
1063                 public int read(ByteBuffer dst) throws IOException {
1064                     return fch.read(dst);
1065                 }
1066                 public int read(ByteBuffer dst, long position)
1067                     throws IOException
1068                 {
1069                     return fch.read(dst, position);
1070                 }
1071                 public long read(ByteBuffer[] dsts, int offset, int length)
1072                     throws IOException
1073                 {
1074                     return fch.read(dsts, offset, length);
1075                 }
1076                 public int write(ByteBuffer src, long position)
1077                     throws IOException
1078                     {
1079                    return fch.write(src, position);
1080                 }
1081                 public MappedByteBuffer map(MapMode mode,
1082                                             long position, long size)
1083                 {
1084                     throw new UnsupportedOperationException();
1085                 }
1086                 public FileLock lock(long position, long size, boolean shared)
1087                     throws IOException
1088                 {
1089                     return fch.lock(position, size, shared);
1090                 }
1091                 public FileLock tryLock(long position, long size, boolean shared)
1092                     throws IOException
1093                 {
1094                     return fch.tryLock(position, size, shared);
1095                 }
1096                 protected void implCloseChannel() throws IOException {
1097                     fch.close();
1098                     if (forWrite) {
1099                         u.mtime = System.currentTimeMillis();
1100                         u.size = Files.size(u.file);
1101                         update(u);
1102                     } else {
1103                         if (!isFCH)    // if this is a new fch for reading
1104                             removeTempPathForEntry(tmpfile);
1105                     }
1106                }
1107             };
1108         } finally {
1109             endRead();
1110         }
1111     }
1112 
1113     // the outstanding input streams that need to be closed
1114     private Set<InputStream> streams =
1115         Collections.synchronizedSet(new HashSet<>());
1116 
1117     // the ex-channel and ex-path that need to close when their outstanding
1118     // input streams are all closed by the obtainers.
1119     private final Set<ExistingChannelCloser> exChClosers = new HashSet<>();
1120 
1121     private final Set<Path> tmppaths = Collections.synchronizedSet(new HashSet<>());
1122     private Path getTempPathForEntry(byte[] path) throws IOException {
1123         Path tmpPath = createTempFileInSameDirectoryAs(zfpath);
1124         if (path != null) {
1125             Entry e = getEntry(path);
1126             if (e != null) {
1127                 try (InputStream is = newInputStream(path)) {
1128                     Files.copy(is, tmpPath, REPLACE_EXISTING);
1129                 }
1130             }
1131         }
1132         return tmpPath;
1133     }
1134 
1135     private void removeTempPathForEntry(Path path) throws IOException {
1136         Files.delete(path);
1137         tmppaths.remove(path);
1138     }
1139 
1140     // check if all parents really exist. ZIP spec does not require
1141     // the existence of any "parent directory".
1142     private void checkParents(byte[] path) throws IOException {
1143         beginRead();
1144         try {
1145             while ((path = getParent(path)) != null &&
1146                     path != ROOTPATH) {
1147                 if (!inodes.containsKey(IndexNode.keyOf(path))) {
1148                     throw new NoSuchFileException(getString(path));
1149                 }
1150             }
1151         } finally {
1152             endRead();
1153         }
1154     }
1155 
1156     private static byte[] getParent(byte[] path) {
1157         int off = getParentOff(path);
1158         if (off <= 1)
1159             return ROOTPATH;
1160         return Arrays.copyOf(path, off);
1161     }
1162 
1163     private static int getParentOff(byte[] path) {
1164         int off = path.length - 1;
1165         if (off > 0 && path[off] == '/')  // isDirectory
1166             off--;
1167         while (off > 0 && path[off] != '/') { off--; }
1168         return off;
1169     }
1170 
1171     private void beginWrite() {
1172         rwlock.writeLock().lock();
1173     }
1174 
1175     private void endWrite() {
1176         rwlock.writeLock().unlock();
1177     }
1178 
1179     private void beginRead() {
1180         rwlock.readLock().lock();
1181     }
1182 
1183     private void endRead() {
1184         rwlock.readLock().unlock();
1185     }
1186 
1187     ///////////////////////////////////////////////////////////////////
1188 
1189     private volatile boolean isOpen = true;
1190     private final SeekableByteChannel ch; // channel to the zipfile
1191     final byte[]  cen;     // CEN & ENDHDR
1192     private END  end;
1193     private long locpos;   // position of first LOC header (usually 0)
1194 
1195     private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
1196 
1197     // name -> pos (in cen), IndexNode itself can be used as a "key"
1198     private LinkedHashMap<IndexNode, IndexNode> inodes;
1199 
1200     final byte[] getBytes(String name) {
1201         return zc.getBytes(name);
1202     }
1203 
1204     final String getString(byte[] name) {
1205         return zc.toString(name);
1206     }
1207 
1208     @SuppressWarnings("deprecation")
1209     protected void finalize() throws IOException {
1210         close();
1211     }
1212 
1213     // Reads len bytes of data from the specified offset into buf.
1214     // Returns the total number of bytes read.
1215     // Each/every byte read from here (except the cen, which is mapped).
1216     final long readFullyAt(byte[] buf, int off, long len, long pos)
1217         throws IOException
1218     {
1219         ByteBuffer bb = ByteBuffer.wrap(buf);
1220         bb.position(off);
1221         bb.limit((int)(off + len));
1222         return readFullyAt(bb, pos);
1223     }
1224 
1225     private long readFullyAt(ByteBuffer bb, long pos) throws IOException {
1226         synchronized(ch) {
1227             return ch.position(pos).read(bb);
1228         }
1229     }
1230 
1231     // Searches for end of central directory (END) header. The contents of
1232     // the END header will be read and placed in endbuf. Returns the file
1233     // position of the END header, otherwise returns -1 if the END header
1234     // was not found or an error occurred.
1235     private END findEND() throws IOException {
1236         byte[] buf = new byte[READBLOCKSZ];
1237         long ziplen = ch.size();
1238         long minHDR = (ziplen - END_MAXLEN) > 0 ? ziplen - END_MAXLEN : 0;
1239         long minPos = minHDR - (buf.length - ENDHDR);
1240 
1241         for (long pos = ziplen - buf.length; pos >= minPos; pos -= (buf.length - ENDHDR)) {
1242             int off = 0;
1243             if (pos < 0) {
1244                 // Pretend there are some NUL bytes before start of file
1245                 off = (int)-pos;
1246                 Arrays.fill(buf, 0, off, (byte)0);
1247             }
1248             int len = buf.length - off;
1249             if (readFullyAt(buf, off, len, pos + off) != len)
1250                 throw new ZipException("zip END header not found");
1251 
1252             // Now scan the block backwards for END header signature
1253             for (int i = buf.length - ENDHDR; i >= 0; i--) {
1254                 if (buf[i]   == (byte)'P'    &&
1255                     buf[i+1] == (byte)'K'    &&
1256                     buf[i+2] == (byte)'\005' &&
1257                     buf[i+3] == (byte)'\006' &&
1258                     (pos + i + ENDHDR + ENDCOM(buf, i) == ziplen)) {
1259                     // Found END header
1260                     buf = Arrays.copyOfRange(buf, i, i + ENDHDR);
1261                     END end = new END();
1262                     // end.endsub = ENDSUB(buf); // not used
1263                     end.centot = ENDTOT(buf);
1264                     end.cenlen = ENDSIZ(buf);
1265                     end.cenoff = ENDOFF(buf);
1266                     // end.comlen = ENDCOM(buf); // not used
1267                     end.endpos = pos + i;
1268                     // try if there is zip64 end;
1269                     byte[] loc64 = new byte[ZIP64_LOCHDR];
1270                     if (end.endpos < ZIP64_LOCHDR ||
1271                         readFullyAt(loc64, 0, loc64.length, end.endpos - ZIP64_LOCHDR)
1272                         != loc64.length ||
1273                         !locator64SigAt(loc64, 0)) {
1274                         return end;
1275                     }
1276                     long end64pos = ZIP64_LOCOFF(loc64);
1277                     byte[] end64buf = new byte[ZIP64_ENDHDR];
1278                     if (readFullyAt(end64buf, 0, end64buf.length, end64pos)
1279                         != end64buf.length ||
1280                         !end64SigAt(end64buf, 0)) {
1281                         return end;
1282                     }
1283                     // end64 found,
1284                     long cenlen64 = ZIP64_ENDSIZ(end64buf);
1285                     long cenoff64 = ZIP64_ENDOFF(end64buf);
1286                     long centot64 = ZIP64_ENDTOT(end64buf);
1287                     // double-check
1288                     if (cenlen64 != end.cenlen && end.cenlen != ZIP64_MINVAL ||
1289                         cenoff64 != end.cenoff && end.cenoff != ZIP64_MINVAL ||
1290                         centot64 != end.centot && end.centot != ZIP64_MINVAL32) {
1291                         return end;
1292                     }
1293                     // to use the end64 values
1294                     end.cenlen = cenlen64;
1295                     end.cenoff = cenoff64;
1296                     end.centot = (int)centot64; // assume total < 2g
1297                     end.endpos = end64pos;
1298                     return end;
1299                 }
1300             }
1301         }
1302         throw new ZipException("zip END header not found");
1303     }
1304 
1305     private void makeParentDirs(IndexNode node, IndexNode root) {
1306         IndexNode parent;
1307         ParentLookup lookup = new ParentLookup();
1308         while (true) {
1309             int off = getParentOff(node.name);
1310             // parent is root
1311             if (off <= 1) {
1312                 node.sibling = root.child;
1313                 root.child = node;
1314                 break;
1315             }
1316             // parent exists
1317             lookup = lookup.as(node.name, off);
1318             if (inodes.containsKey(lookup)) {
1319                 parent = inodes.get(lookup);
1320                 node.sibling = parent.child;
1321                 parent.child = node;
1322                 break;
1323             }
1324             // parent does not exist, add new pseudo directory entry
1325             parent = new IndexNode(Arrays.copyOf(node.name, off), true);
1326             inodes.put(parent, parent);
1327             node.sibling = parent.child;
1328             parent.child = node;
1329             node = parent;
1330         }
1331     }
1332 
1333     // ZIP directory has two issues:
1334     // (1) ZIP spec does not require the ZIP file to include
1335     //     directory entry
1336     // (2) all entries are not stored/organized in a "tree"
1337     //     structure.
1338     // A possible solution is to build the node tree ourself as
1339     // implemented below.
1340     private void buildNodeTree() {
1341         beginWrite();
1342         try {
1343             IndexNode root = inodes.remove(LOOKUPKEY.as(ROOTPATH));
1344             if (root == null) {
1345                 root = new IndexNode(ROOTPATH, true);
1346             }
1347             IndexNode[] nodes = inodes.values().toArray(new IndexNode[0]);
1348             inodes.put(root, root);
1349             for (IndexNode node : nodes) {
1350                 makeParentDirs(node, root);
1351             }
1352         } finally {
1353             endWrite();
1354         }
1355     }
1356 
1357     private void removeFromTree(IndexNode inode) {
1358         IndexNode parent = inodes.get(LOOKUPKEY.as(getParent(inode.name)));
1359         IndexNode child = parent.child;
1360         if (child.equals(inode)) {
1361             parent.child = child.sibling;
1362         } else {
1363             IndexNode last = child;
1364             while ((child = child.sibling) != null) {
1365                 if (child.equals(inode)) {
1366                     last.sibling = child.sibling;
1367                     break;
1368                 } else {
1369                     last = child;
1370                 }
1371             }
1372         }
1373     }
1374 
1375     /**
1376      * If a version property has been specified and the file represents a multi-release JAR,
1377      * determine the requested runtime version and initialize the ZipFileSystem instance accordingly.
1378      *
1379      * Checks if the Zip File System property "releaseVersion" has been specified. If it has,
1380      * use its value to determine the requested version. If not use the value of the "multi-release" property.
1381      */
1382     private void initializeReleaseVersion(Map<String, ?> env) throws IOException {
1383         Object o = env.containsKey(PROPERTY_RELEASE_VERSION) ?
1384             env.get(PROPERTY_RELEASE_VERSION) :
1385             env.get(PROPERTY_MULTI_RELEASE);
1386 
1387         if (o != null && isMultiReleaseJar()) {
1388             int version;
1389             if (o instanceof String) {
1390                 String s = (String)o;
1391                 if (s.equals("runtime")) {
1392                     version = Runtime.version().feature();
1393                 } else if (s.matches("^[1-9][0-9]*$")) {
1394                     version = Version.parse(s).feature();
1395                 } else {
1396                     throw new IllegalArgumentException("Invalid runtime version");
1397                 }
1398             } else if (o instanceof Integer) {
1399                 version = Version.parse(((Integer)o).toString()).feature();
1400             } else if (o instanceof Version) {
1401                 version = ((Version)o).feature();
1402             } else {
1403                 throw new IllegalArgumentException("env parameter must be String, " +
1404                     "Integer, or Version");
1405             }
1406             createVersionedLinks(version < 0 ? 0 : version);
1407             setReadOnly();
1408         }
1409     }
1410 
1411     /**
1412      * Returns true if the Manifest main attribute "Multi-Release" is set to true; false otherwise.
1413      */
1414     private boolean isMultiReleaseJar() throws IOException {
1415         try (InputStream is = newInputStream(getBytes("/META-INF/MANIFEST.MF"))) {
1416             String multiRelease = new Manifest(is).getMainAttributes()
1417                 .getValue(Attributes.Name.MULTI_RELEASE);
1418             return "true".equalsIgnoreCase(multiRelease);
1419         } catch (NoSuchFileException x) {
1420             return false;
1421         }
1422     }
1423 
1424     /**
1425      * Create a map of aliases for versioned entries, for example:
1426      *   version/PackagePrivate.class -> META-INF/versions/9/version/PackagePrivate.class
1427      *   version/PackagePrivate.java -> META-INF/versions/9/version/PackagePrivate.java
1428      *   version/Version.class -> META-INF/versions/10/version/Version.class
1429      *   version/Version.java -> META-INF/versions/10/version/Version.java
1430      *
1431      * Then wrap the map in a function that getEntry can use to override root
1432      * entry lookup for entries that have corresponding versioned entries.
1433      */
1434     private void createVersionedLinks(int version) {
1435         IndexNode verdir = getInode(getBytes("/META-INF/versions"));
1436         // nothing to do, if no /META-INF/versions
1437         if (verdir == null) {
1438             return;
1439         }
1440         // otherwise, create a map and for each META-INF/versions/{n} directory
1441         // put all the leaf inodes, i.e. entries, into the alias map
1442         // possibly shadowing lower versioned entries
1443         HashMap<IndexNode, byte[]> aliasMap = new HashMap<>();
1444         getVersionMap(version, verdir).values().forEach(versionNode ->
1445             walk(versionNode.child, entryNode ->
1446                 aliasMap.put(
1447                     getOrCreateInode(getRootName(entryNode, versionNode), entryNode.isdir),
1448                     entryNode.name))
1449         );
1450         entryLookup = path -> {
1451             byte[] entry = aliasMap.get(IndexNode.keyOf(path));
1452             return entry == null ? path : entry;
1453         };
1454     }
1455 
1456     /**
1457      * Create a sorted version map of version -> inode, for inodes <= max version.
1458      *   9 -> META-INF/versions/9
1459      *  10 -> META-INF/versions/10
1460      */
1461     private TreeMap<Integer, IndexNode> getVersionMap(int version, IndexNode metaInfVersions) {
1462         TreeMap<Integer,IndexNode> map = new TreeMap<>();
1463         IndexNode child = metaInfVersions.child;
1464         while (child != null) {
1465             Integer key = getVersion(child, metaInfVersions);
1466             if (key != null && key <= version) {
1467                 map.put(key, child);
1468             }
1469             child = child.sibling;
1470         }
1471         return map;
1472     }
1473 
1474     /**
1475      * Extract the integer version number -- META-INF/versions/9 returns 9.
1476      */
1477     private Integer getVersion(IndexNode inode, IndexNode metaInfVersions) {
1478         try {
1479             byte[] fullName = inode.name;
1480             return Integer.parseInt(getString(Arrays
1481                 .copyOfRange(fullName, metaInfVersions.name.length + 1, fullName.length)));
1482         } catch (NumberFormatException x) {
1483             // ignore this even though it might indicate issues with the JAR structure
1484             return null;
1485         }
1486     }
1487 
1488     /**
1489      * Walk the IndexNode tree processing all leaf nodes.
1490      */
1491     private void walk(IndexNode inode, Consumer<IndexNode> consumer) {
1492         if (inode == null) return;
1493         if (inode.isDir()) {
1494             walk(inode.child, consumer);
1495         } else {
1496             consumer.accept(inode);
1497         }
1498         walk(inode.sibling, consumer);
1499     }
1500 
1501     /**
1502      * Extract the root name from a versioned entry name.
1503      * E.g. given inode 'META-INF/versions/9/foo/bar.class'
1504      * and prefix 'META-INF/versions/9/' returns 'foo/bar.class'.
1505      */
1506     private byte[] getRootName(IndexNode inode, IndexNode prefix) {
1507         byte[] fullName = inode.name;
1508         return Arrays.copyOfRange(fullName, prefix.name.length, fullName.length);
1509     }
1510 
1511     // Reads zip file central directory. Returns the file position of first
1512     // CEN header, otherwise returns -1 if an error occurred. If zip->msg != NULL
1513     // then the error was a zip format error and zip->msg has the error text.
1514     // Always pass in -1 for knownTotal; it's used for a recursive call.
1515     private byte[] initCEN() throws IOException {
1516         end = findEND();
1517         if (end.endpos == 0) {
1518             inodes = new LinkedHashMap<>(10);
1519             locpos = 0;
1520             buildNodeTree();
1521             return null;         // only END header present
1522         }
1523         if (end.cenlen > end.endpos)
1524             throw new ZipException("invalid END header (bad central directory size)");
1525         long cenpos = end.endpos - end.cenlen;     // position of CEN table
1526 
1527         // Get position of first local file (LOC) header, taking into
1528         // account that there may be a stub prefixed to the zip file.
1529         locpos = cenpos - end.cenoff;
1530         if (locpos < 0)
1531             throw new ZipException("invalid END header (bad central directory offset)");
1532 
1533         // read in the CEN and END
1534         byte[] cen = new byte[(int)(end.cenlen + ENDHDR)];
1535         if (readFullyAt(cen, 0, cen.length, cenpos) != end.cenlen + ENDHDR) {
1536             throw new ZipException("read CEN tables failed");
1537         }
1538         // Iterate through the entries in the central directory
1539         inodes = new LinkedHashMap<>(end.centot + 1);
1540         int pos = 0;
1541         int limit = cen.length - ENDHDR;
1542         while (pos < limit) {
1543             if (!cenSigAt(cen, pos))
1544                 throw new ZipException("invalid CEN header (bad signature)");
1545             int method = CENHOW(cen, pos);
1546             int nlen   = CENNAM(cen, pos);
1547             int elen   = CENEXT(cen, pos);
1548             int clen   = CENCOM(cen, pos);
1549             if ((CENFLG(cen, pos) & 1) != 0) {
1550                 throw new ZipException("invalid CEN header (encrypted entry)");
1551             }
1552             if (method != METHOD_STORED && method != METHOD_DEFLATED) {
1553                 throw new ZipException("invalid CEN header (unsupported compression method: " + method + ")");
1554             }
1555             if (pos + CENHDR + nlen > limit) {
1556                 throw new ZipException("invalid CEN header (bad header size)");
1557             }
1558             IndexNode inode = new IndexNode(cen, pos, nlen);
1559             inodes.put(inode, inode);
1560 
1561             // skip ext and comment
1562             pos += (CENHDR + nlen + elen + clen);
1563         }
1564         if (pos + ENDHDR != cen.length) {
1565             throw new ZipException("invalid CEN header (bad header size)");
1566         }
1567         buildNodeTree();
1568         return cen;
1569     }
1570 
1571     private void ensureOpen() {
1572         if (!isOpen)
1573             throw new ClosedFileSystemException();
1574     }
1575 
1576     // Creates a new empty temporary file in the same directory as the
1577     // specified file.  A variant of Files.createTempFile.
1578     private Path createTempFileInSameDirectoryAs(Path path) throws IOException {
1579         Path parent = path.toAbsolutePath().getParent();
1580         Path dir = (parent == null) ? path.getFileSystem().getPath(".") : parent;
1581         Path tmpPath = Files.createTempFile(dir, "zipfstmp", null);
1582         tmppaths.add(tmpPath);
1583         return tmpPath;
1584     }
1585 
1586     ////////////////////update & sync //////////////////////////////////////
1587 
1588     private boolean hasUpdate = false;
1589 
1590     // shared key. consumer guarantees the "writeLock" before use it.
1591     private final IndexNode LOOKUPKEY = new IndexNode(null, -1);
1592 
1593     private void updateDelete(IndexNode inode) {
1594         beginWrite();
1595         try {
1596             removeFromTree(inode);
1597             inodes.remove(inode);
1598             hasUpdate = true;
1599         } finally {
1600              endWrite();
1601         }
1602     }
1603 
1604     private void update(Entry e) {
1605         beginWrite();
1606         try {
1607             IndexNode old = inodes.put(e, e);
1608             if (old != null) {
1609                 removeFromTree(old);
1610             }
1611             if (e.type == Entry.NEW || e.type == Entry.FILECH || e.type == Entry.COPY) {
1612                 IndexNode parent = inodes.get(LOOKUPKEY.as(getParent(e.name)));
1613                 e.sibling = parent.child;
1614                 parent.child = e;
1615             }
1616             hasUpdate = true;
1617         } finally {
1618             endWrite();
1619         }
1620     }
1621 
1622     // copy over the whole LOC entry (header if necessary, data and ext) from
1623     // old zip to the new one.
1624     private long copyLOCEntry(Entry e, boolean updateHeader,
1625                               OutputStream os,
1626                               long written, byte[] buf)
1627         throws IOException
1628     {
1629         long locoff = e.locoff;  // where to read
1630         e.locoff = written;      // update the e.locoff with new value
1631 
1632         // calculate the size need to write out
1633         long size = 0;
1634         //  if there is A ext
1635         if ((e.flag & FLAG_DATADESCR) != 0) {
1636             if (e.size >= ZIP64_MINVAL || e.csize >= ZIP64_MINVAL)
1637                 size = 24;
1638             else
1639                 size = 16;
1640         }
1641         // read loc, use the original loc.elen/nlen
1642         //
1643         // an extra byte after loc is read, which should be the first byte of the
1644         // 'name' field of the loc. if this byte is '/', which means the original
1645         // entry has an absolute path in original zip/jar file, the e.writeLOC()
1646         // is used to output the loc, in which the leading "/" will be removed
1647         if (readFullyAt(buf, 0, LOCHDR + 1 , locoff) != LOCHDR + 1)
1648             throw new ZipException("loc: reading failed");
1649 
1650         if (updateHeader || LOCNAM(buf) > 0 && buf[LOCHDR] == '/') {
1651             locoff += LOCHDR + LOCNAM(buf) + LOCEXT(buf);  // skip header
1652             size += e.csize;
1653             written = e.writeLOC(os) + size;
1654         } else {
1655             os.write(buf, 0, LOCHDR);    // write out the loc header
1656             locoff += LOCHDR;
1657             // use e.csize,  LOCSIZ(buf) is zero if FLAG_DATADESCR is on
1658             // size += LOCNAM(buf) + LOCEXT(buf) + LOCSIZ(buf);
1659             size += LOCNAM(buf) + LOCEXT(buf) + e.csize;
1660             written = LOCHDR + size;
1661         }
1662         int n;
1663         while (size > 0 &&
1664             (n = (int)readFullyAt(buf, 0, buf.length, locoff)) != -1)
1665         {
1666             if (size < n)
1667                 n = (int)size;
1668             os.write(buf, 0, n);
1669             size -= n;
1670             locoff += n;
1671         }
1672         return written;
1673     }
1674 
1675     private long writeEntry(Entry e, OutputStream os)
1676         throws IOException {
1677 
1678         if (e.bytes == null && e.file == null)    // dir, 0-length data
1679             return 0;
1680 
1681         long written = 0;
1682         if (e.method != METHOD_STORED && e.csize > 0 && (e.crc != 0 || e.size == 0)) {
1683             // pre-compressed entry, write directly to output stream
1684             writeTo(e, os);
1685         } else {
1686             try (OutputStream os2 = (e.method == METHOD_STORED) ?
1687                     new EntryOutputStreamCRC32(e, os) : new EntryOutputStreamDef(e, os)) {
1688                 writeTo(e, os2);
1689             }
1690         }
1691         written += e.csize;
1692         if ((e.flag & FLAG_DATADESCR) != 0) {
1693             written += e.writeEXT(os);
1694         }
1695         return written;
1696     }
1697 
1698     private void writeTo(Entry e, OutputStream os) throws IOException {
1699         if (e.bytes != null) {
1700             os.write(e.bytes, 0, e.bytes.length);
1701         } else if (e.file != null) {
1702             if (e.type == Entry.NEW || e.type == Entry.FILECH) {
1703                 try (InputStream is = Files.newInputStream(e.file)) {
1704                     is.transferTo(os);
1705                 }
1706             }
1707             Files.delete(e.file);
1708             tmppaths.remove(e.file);
1709         }
1710     }
1711 
1712     // sync the zip file system, if there is any update
1713     private void sync() throws IOException {
1714         // check ex-closer
1715         if (!exChClosers.isEmpty()) {
1716             for (ExistingChannelCloser ecc : exChClosers) {
1717                 if (ecc.closeAndDeleteIfDone()) {
1718                     exChClosers.remove(ecc);
1719                 }
1720             }
1721         }
1722         if (!hasUpdate)
1723             return;
1724         PosixFileAttributes attrs = getPosixAttributes(zfpath);
1725         Path tmpFile = createTempFileInSameDirectoryAs(zfpath);
1726         try (OutputStream os = new BufferedOutputStream(Files.newOutputStream(tmpFile, WRITE))) {
1727             ArrayList<Entry> elist = new ArrayList<>(inodes.size());
1728             long written = 0;
1729             byte[] buf = null;
1730             Entry e;
1731 
1732             final IndexNode manifestInode = inodes.get(IndexNode.keyOf(getBytes("/META-INF/MANIFEST.MF")));
1733             final Iterator<IndexNode> inodeIterator = inodes.values().iterator();
1734             boolean manifestProcessed = false;
1735 
1736             // write loc
1737             while (inodeIterator.hasNext()) {
1738                 final IndexNode inode;
1739 
1740                 // write the manifest inode (if any) first so that
1741                 // java.util.jar.JarInputStream can find it
1742                 if (manifestInode == null) {
1743                     inode = inodeIterator.next();
1744                 } else {
1745                     if (manifestProcessed) {
1746                         // advance to next node, filtering out the manifest
1747                         // which was already written
1748                         inode = inodeIterator.next();
1749                         if (inode == manifestInode) {
1750                             continue;
1751                         }
1752                     } else {
1753                         inode = manifestInode;
1754                         manifestProcessed = true;
1755                     }
1756                 }
1757 
1758                 if (inode instanceof Entry) {    // an updated inode
1759                     e = (Entry)inode;
1760                     try {
1761                         if (e.type == Entry.COPY) {
1762                             // entry copy: the only thing changed is the "name"
1763                             // and "nlen" in LOC header, so we update/rewrite the
1764                             // LOC in new file and simply copy the rest (data and
1765                             // ext) without enflating/deflating from the old zip
1766                             // file LOC entry.
1767                             if (buf == null)
1768                                 buf = new byte[8192];
1769                             written += copyLOCEntry(e, true, os, written, buf);
1770                         } else {                          // NEW, FILECH or CEN
1771                             e.locoff = written;
1772                             written += e.writeLOC(os);    // write loc header
1773                             written += writeEntry(e, os);
1774                         }
1775                         elist.add(e);
1776                     } catch (IOException x) {
1777                         x.printStackTrace();    // skip any in-accurate entry
1778                     }
1779                 } else {                        // unchanged inode
1780                     if (inode.pos == -1) {
1781                         continue;               // pseudo directory node
1782                     }
1783                     if (inode.name.length == 1 && inode.name[0] == '/') {
1784                         continue;               // no root '/' directory even if it
1785                                                 // exists in original zip/jar file.
1786                     }
1787                     e = supportPosix ? new PosixEntry(this, inode) : new Entry(this, inode);
1788                     try {
1789                         if (buf == null)
1790                             buf = new byte[8192];
1791                         written += copyLOCEntry(e, false, os, written, buf);
1792                         elist.add(e);
1793                     } catch (IOException x) {
1794                         x.printStackTrace();    // skip any wrong entry
1795                     }
1796                 }
1797             }
1798 
1799             // now write back the cen and end table
1800             end.cenoff = written;
1801             for (Entry entry : elist) {
1802                 written += entry.writeCEN(os);
1803             }
1804             end.centot = elist.size();
1805             end.cenlen = written - end.cenoff;
1806             end.write(os, written, forceEnd64);
1807         }
1808         if (!streams.isEmpty()) {
1809             //
1810             // There are outstanding input streams open on existing "ch",
1811             // so, don't close the "cha" and delete the "file for now, let
1812             // the "ex-channel-closer" to handle them
1813             Path path = createTempFileInSameDirectoryAs(zfpath);
1814             ExistingChannelCloser ecc = new ExistingChannelCloser(path,
1815                                                                   ch,
1816                                                                   streams);
1817             Files.move(zfpath, path, REPLACE_EXISTING);
1818             exChClosers.add(ecc);
1819             streams = Collections.synchronizedSet(new HashSet<>());
1820         } else {
1821             ch.close();
1822             Files.delete(zfpath);
1823         }
1824 
1825         // Set the POSIX permissions of the original Zip File if available
1826         // before moving the temp file
1827         if (attrs != null) {
1828             Files.setPosixFilePermissions(tmpFile, attrs.permissions());
1829         }
1830         Files.move(tmpFile, zfpath, REPLACE_EXISTING);
1831         hasUpdate = false;    // clear
1832     }
1833 
1834     /**
1835      * Returns a file's POSIX file attributes.
1836      * @param path The path to the file
1837      * @return The POSIX file attributes for the specified file or
1838      *         null if the POSIX attribute view is not available
1839      * @throws IOException If an error occurs obtaining the POSIX attributes for
1840      *                    the specified file
1841      */
1842     private PosixFileAttributes getPosixAttributes(Path path) throws IOException {
1843         try {
1844             PosixFileAttributeView view =
1845                     Files.getFileAttributeView(path, PosixFileAttributeView.class);
1846             // Return if the attribute view is not supported
1847             if (view == null) {
1848                 return null;
1849             }
1850             return view.readAttributes();
1851         } catch (UnsupportedOperationException e) {
1852             // PosixFileAttributes not available
1853             return null;
1854         }
1855     }
1856 
1857     private IndexNode getInode(byte[] path) {
1858         return inodes.get(IndexNode.keyOf(Objects.requireNonNull(entryLookup.apply(path), "path")));
1859     }
1860 
1861     /**
1862      * Return the IndexNode from the root tree. If it doesn't exist,
1863      * it gets created along with all parent directory IndexNodes.
1864      */
1865     private IndexNode getOrCreateInode(byte[] path, boolean isdir) {
1866         IndexNode node = getInode(path);
1867         // if node exists, return it
1868         if (node != null) {
1869             return node;
1870         }
1871 
1872         // otherwise create new pseudo node and parent directory hierarchy
1873         node = new IndexNode(path, isdir);
1874         beginWrite();
1875         try {
1876             makeParentDirs(node, Objects.requireNonNull(inodes.get(IndexNode.keyOf(ROOTPATH)), "no root node found"));
1877             return node;
1878         } finally {
1879             endWrite();
1880         }
1881     }
1882 
1883     private Entry getEntry(byte[] path) throws IOException {
1884         IndexNode inode = getInode(path);
1885         if (inode instanceof Entry)
1886             return (Entry)inode;
1887         if (inode == null || inode.pos == -1)
1888             return null;
1889         return supportPosix ? new PosixEntry(this, inode): new Entry(this, inode);
1890     }
1891 
1892     public void deleteFile(byte[] path, boolean failIfNotExists)
1893         throws IOException
1894     {
1895         checkWritable();
1896         IndexNode inode = getInode(path);
1897         if (inode == null) {
1898             if (path != null && path.length == 0)
1899                 throw new ZipException("root directory </> can't not be delete");
1900             if (failIfNotExists)
1901                 throw new NoSuchFileException(getString(path));
1902         } else {
1903             if (inode.isDir() && inode.child != null)
1904                 throw new DirectoryNotEmptyException(getString(path));
1905             updateDelete(inode);
1906         }
1907     }
1908 
1909     // Returns an out stream for either
1910     // (1) writing the contents of a new entry, if the entry exists, or
1911     // (2) updating/replacing the contents of the specified existing entry.
1912     private OutputStream getOutputStream(Entry e) throws IOException {
1913         if (e.mtime == -1)
1914             e.mtime = System.currentTimeMillis();
1915         if (e.method == -1)
1916             e.method = defaultCompressionMethod;
1917         // store size, compressed size, and crc-32 in datadescr
1918         e.flag = FLAG_DATADESCR;
1919         if (zc.isUTF8())
1920             e.flag |= FLAG_USE_UTF8;
1921         OutputStream os;
1922         if (useTempFile) {
1923             e.file = getTempPathForEntry(null);
1924             os = Files.newOutputStream(e.file, WRITE);
1925         } else {
1926             os = new ByteArrayOutputStream((e.size > 0)? (int)e.size : 8192);
1927         }
1928         if (e.method == METHOD_DEFLATED) {
1929             return new DeflatingEntryOutputStream(e, os);
1930         } else {
1931             return new EntryOutputStream(e, os);
1932         }
1933     }
1934 
1935     private class EntryOutputStream extends FilterOutputStream {
1936         private final Entry e;
1937         private long written;
1938         private boolean isClosed;
1939 
1940         EntryOutputStream(Entry e, OutputStream os) {
1941             super(os);
1942             this.e =  Objects.requireNonNull(e, "Zip entry is null");
1943             // this.written = 0;
1944         }
1945 
1946         @Override
1947         public synchronized void write(int b) throws IOException {
1948             out.write(b);
1949             written += 1;
1950         }
1951 
1952         @Override
1953         public synchronized void write(byte[] b, int off, int len)
1954                 throws IOException {
1955             out.write(b, off, len);
1956             written += len;
1957         }
1958 
1959         @Override
1960         public synchronized void close() throws IOException {
1961             if (isClosed) {
1962                 return;
1963             }
1964             isClosed = true;
1965             e.size = written;
1966             if (out instanceof ByteArrayOutputStream)
1967                 e.bytes = ((ByteArrayOutputStream)out).toByteArray();
1968             super.close();
1969             update(e);
1970         }
1971     }
1972 
1973     // Output stream returned when writing "deflated" entries into memory,
1974     // to enable eager (possibly parallel) deflation and reduce memory required.
1975     private class DeflatingEntryOutputStream extends DeflaterOutputStream {
1976         private final CRC32 crc;
1977         private final Entry e;
1978         private boolean isClosed;
1979 
1980         DeflatingEntryOutputStream(Entry e, OutputStream os) {
1981             super(os, getDeflater());
1982             this.e = Objects.requireNonNull(e, "Zip entry is null");
1983             this.crc = new CRC32();
1984         }
1985 
1986         @Override
1987         public synchronized void write(byte[] b, int off, int len)
1988                 throws IOException {
1989             super.write(b, off, len);
1990             crc.update(b, off, len);
1991         }
1992 
1993         @Override
1994         public synchronized void close() throws IOException {
1995             if (isClosed)
1996                 return;
1997             isClosed = true;
1998             finish();
1999             e.size  = def.getBytesRead();
2000             e.csize = def.getBytesWritten();
2001             e.crc = crc.getValue();
2002             if (out instanceof ByteArrayOutputStream)
2003                 e.bytes = ((ByteArrayOutputStream)out).toByteArray();
2004             super.close();
2005             update(e);
2006             releaseDeflater(def);
2007         }
2008     }
2009 
2010     // Wrapper output stream class to write out a "stored" entry.
2011     // (1) this class does not close the underlying out stream when
2012     //     being closed.
2013     // (2) no need to be "synchronized", only used by sync()
2014     private class EntryOutputStreamCRC32 extends FilterOutputStream {
2015         private final CRC32 crc;
2016         private final Entry e;
2017         private long written;
2018         private boolean isClosed;
2019 
2020         EntryOutputStreamCRC32(Entry e, OutputStream os) {
2021             super(os);
2022             this.e =  Objects.requireNonNull(e, "Zip entry is null");
2023             this.crc = new CRC32();
2024         }
2025 
2026         @Override
2027         public void write(int b) throws IOException {
2028             out.write(b);
2029             crc.update(b);
2030             written += 1;
2031         }
2032 
2033         @Override
2034         public void write(byte[] b, int off, int len)
2035                 throws IOException {
2036             out.write(b, off, len);
2037             crc.update(b, off, len);
2038             written += len;
2039         }
2040 
2041         @Override
2042         public void close() {
2043             if (isClosed)
2044                 return;
2045             isClosed = true;
2046             e.size = e.csize = written;
2047             e.crc = crc.getValue();
2048         }
2049     }
2050 
2051     // Wrapper output stream class to write out a "deflated" entry.
2052     // (1) this class does not close the underlying out stream when
2053     //     being closed.
2054     // (2) no need to be "synchronized", only used by sync()
2055     private class EntryOutputStreamDef extends DeflaterOutputStream {
2056         private final CRC32 crc;
2057         private final Entry e;
2058         private boolean isClosed;
2059 
2060         EntryOutputStreamDef(Entry e, OutputStream os) {
2061             super(os, getDeflater());
2062             this.e = Objects.requireNonNull(e, "Zip entry is null");
2063             this.crc = new CRC32();
2064         }
2065 
2066         @Override
2067         public void write(byte[] b, int off, int len) throws IOException {
2068             super.write(b, off, len);
2069             crc.update(b, off, len);
2070         }
2071 
2072         @Override
2073         public void close() throws IOException {
2074             if (isClosed)
2075                 return;
2076             isClosed = true;
2077             finish();
2078             e.size = def.getBytesRead();
2079             e.csize = def.getBytesWritten();
2080             e.crc = crc.getValue();
2081             releaseDeflater(def);
2082         }
2083     }
2084 
2085     private InputStream getInputStream(Entry e)
2086         throws IOException
2087     {
2088         InputStream eis;
2089         if (e.type == Entry.NEW) {
2090             if (e.bytes != null)
2091                 eis = new ByteArrayInputStream(e.bytes);
2092             else if (e.file != null)
2093                 eis = Files.newInputStream(e.file);
2094             else
2095                 throw new ZipException("update entry data is missing");
2096         } else if (e.type == Entry.FILECH) {
2097             // FILECH result is un-compressed.
2098             eis = Files.newInputStream(e.file);
2099             // TBD: wrap to hook close()
2100             // streams.add(eis);
2101             return eis;
2102         } else {  // untouched CEN or COPY
2103             eis = new EntryInputStream(e, ch);
2104         }
2105         if (e.method == METHOD_DEFLATED) {
2106             // MORE: Compute good size for inflater stream:
2107             long bufSize = e.size + 2; // Inflater likes a bit of slack
2108             if (bufSize > 65536)
2109                 bufSize = 8192;
2110             final long size = e.size;
2111             eis = new InflaterInputStream(eis, getInflater(), (int)bufSize) {
2112                 private boolean isClosed = false;
2113                 public void close() throws IOException {
2114                     if (!isClosed) {
2115                         releaseInflater(inf);
2116                         this.in.close();
2117                         isClosed = true;
2118                         streams.remove(this);
2119                     }
2120                 }
2121                 // Override fill() method to provide an extra "dummy" byte
2122                 // at the end of the input stream. This is required when
2123                 // using the "nowrap" Inflater option. (it appears the new
2124                 // zlib in 7 does not need it, but keep it for now)
2125                 protected void fill() throws IOException {
2126                     if (eof) {
2127                         throw new EOFException(
2128                             "Unexpected end of ZLIB input stream");
2129                     }
2130                     len = this.in.read(buf, 0, buf.length);
2131                     if (len == -1) {
2132                         buf[0] = 0;
2133                         len = 1;
2134                         eof = true;
2135                     }
2136                     inf.setInput(buf, 0, len);
2137                 }
2138                 private boolean eof;
2139 
2140                 public int available() {
2141                     if (isClosed)
2142                         return 0;
2143                     long avail = size - inf.getBytesWritten();
2144                     return avail > (long) Integer.MAX_VALUE ?
2145                         Integer.MAX_VALUE : (int) avail;
2146                 }
2147             };
2148         } else if (e.method == METHOD_STORED) {
2149             // TBD: wrap/ it does not seem necessary
2150         } else {
2151             throw new ZipException("invalid compression method");
2152         }
2153         streams.add(eis);
2154         return eis;
2155     }
2156 
2157     // Inner class implementing the input stream used to read
2158     // a (possibly compressed) zip file entry.
2159     private class EntryInputStream extends InputStream {
2160         private final SeekableByteChannel zfch; // local ref to zipfs's "ch". zipfs.ch might
2161                                                 // point to a new channel after sync()
2162         private long pos;                       // current position within entry data
2163         private long rem;                       // number of remaining bytes within entry
2164 
2165         EntryInputStream(Entry e, SeekableByteChannel zfch)
2166             throws IOException
2167         {
2168             this.zfch = zfch;
2169             rem = e.csize;
2170             pos = e.locoff;
2171             if (pos == -1) {
2172                 Entry e2 = getEntry(e.name);
2173                 if (e2 == null) {
2174                     throw new ZipException("invalid loc for entry <" + getString(e.name) + ">");
2175                 }
2176                 pos = e2.locoff;
2177             }
2178             pos = -pos;  // lazy initialize the real data offset
2179         }
2180 
2181         public int read(byte[] b, int off, int len) throws IOException {
2182             ensureOpen();
2183             initDataPos();
2184             if (rem == 0) {
2185                 return -1;
2186             }
2187             if (len <= 0) {
2188                 return 0;
2189             }
2190             if (len > rem) {
2191                 len = (int) rem;
2192             }
2193             // readFullyAt()
2194             long n;
2195             ByteBuffer bb = ByteBuffer.wrap(b);
2196             bb.position(off);
2197             bb.limit(off + len);
2198             synchronized(zfch) {
2199                 n = zfch.position(pos).read(bb);
2200             }
2201             if (n > 0) {
2202                 pos += n;
2203                 rem -= n;
2204             }
2205             if (rem == 0) {
2206                 close();
2207             }
2208             return (int)n;
2209         }
2210 
2211         public int read() throws IOException {
2212             byte[] b = new byte[1];
2213             if (read(b, 0, 1) == 1) {
2214                 return b[0] & 0xff;
2215             } else {
2216                 return -1;
2217             }
2218         }
2219 
2220         public long skip(long n) {
2221             ensureOpen();
2222             if (n > rem)
2223                 n = rem;
2224             pos += n;
2225             rem -= n;
2226             if (rem == 0) {
2227                 close();
2228             }
2229             return n;
2230         }
2231 
2232         public int available() {
2233             return rem > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) rem;
2234         }
2235 
2236         public void close() {
2237             rem = 0;
2238             streams.remove(this);
2239         }
2240 
2241         private void initDataPos() throws IOException {
2242             if (pos <= 0) {
2243                 pos = -pos + locpos;
2244                 byte[] buf = new byte[LOCHDR];
2245                 if (readFullyAt(buf, 0, buf.length, pos) != LOCHDR) {
2246                     throw new ZipException("invalid loc " + pos + " for entry reading");
2247                 }
2248                 pos += LOCHDR + LOCNAM(buf) + LOCEXT(buf);
2249             }
2250         }
2251     }
2252 
2253     // Maxmum number of de/inflater we cache
2254     private final int MAX_FLATER = 20;
2255     // List of available Inflater objects for decompression
2256     private final List<Inflater> inflaters = new ArrayList<>();
2257 
2258     // Gets an inflater from the list of available inflaters or allocates
2259     // a new one.
2260     private Inflater getInflater() {
2261         synchronized (inflaters) {
2262             int size = inflaters.size();
2263             if (size > 0) {
2264                 return inflaters.remove(size - 1);
2265             } else {
2266                 return new Inflater(true);
2267             }
2268         }
2269     }
2270 
2271     // Releases the specified inflater to the list of available inflaters.
2272     private void releaseInflater(Inflater inf) {
2273         synchronized (inflaters) {
2274             if (inflaters.size() < MAX_FLATER) {
2275                 inf.reset();
2276                 inflaters.add(inf);
2277             } else {
2278                 inf.end();
2279             }
2280         }
2281     }
2282 
2283     // List of available Deflater objects for compression
2284     private final List<Deflater> deflaters = new ArrayList<>();
2285 
2286     // Gets a deflater from the list of available deflaters or allocates
2287     // a new one.
2288     private Deflater getDeflater() {
2289         synchronized (deflaters) {
2290             int size = deflaters.size();
2291             if (size > 0) {
2292                 return deflaters.remove(size - 1);
2293             } else {
2294                 return new Deflater(Deflater.DEFAULT_COMPRESSION, true);
2295             }
2296         }
2297     }
2298 
2299     // Releases the specified inflater to the list of available inflaters.
2300     private void releaseDeflater(Deflater def) {
2301         synchronized (deflaters) {
2302             if (deflaters.size() < MAX_FLATER) {
2303                def.reset();
2304                deflaters.add(def);
2305             } else {
2306                def.end();
2307             }
2308         }
2309     }
2310 
2311     // End of central directory record
2312     static class END {
2313         // The fields that are commented out below are not used by anyone and write() uses "0"
2314         // int  disknum;
2315         // int  sdisknum;
2316         // int  endsub;
2317         int  centot;        // 4 bytes
2318         long cenlen;        // 4 bytes
2319         long cenoff;        // 4 bytes
2320         // int  comlen;     // comment length
2321         // byte[] comment;
2322 
2323         // members of Zip64 end of central directory locator
2324         // int diskNum;
2325         long endpos;
2326         // int disktot;
2327 
2328         void write(OutputStream os, long offset, boolean forceEnd64) throws IOException {
2329             boolean hasZip64 = forceEnd64; // false;
2330             long xlen = cenlen;
2331             long xoff = cenoff;
2332             if (xlen >= ZIP64_MINVAL) {
2333                 xlen = ZIP64_MINVAL;
2334                 hasZip64 = true;
2335             }
2336             if (xoff >= ZIP64_MINVAL) {
2337                 xoff = ZIP64_MINVAL;
2338                 hasZip64 = true;
2339             }
2340             int count = centot;
2341             if (count >= ZIP64_MINVAL32) {
2342                 count = ZIP64_MINVAL32;
2343                 hasZip64 = true;
2344             }
2345             if (hasZip64) {
2346                 //zip64 end of central directory record
2347                 writeInt(os, ZIP64_ENDSIG);       // zip64 END record signature
2348                 writeLong(os, ZIP64_ENDHDR - 12); // size of zip64 end
2349                 writeShort(os, 45);               // version made by
2350                 writeShort(os, 45);               // version needed to extract
2351                 writeInt(os, 0);                  // number of this disk
2352                 writeInt(os, 0);                  // central directory start disk
2353                 writeLong(os, centot);            // number of directory entries on disk
2354                 writeLong(os, centot);            // number of directory entries
2355                 writeLong(os, cenlen);            // length of central directory
2356                 writeLong(os, cenoff);            // offset of central directory
2357 
2358                 //zip64 end of central directory locator
2359                 writeInt(os, ZIP64_LOCSIG);       // zip64 END locator signature
2360                 writeInt(os, 0);                  // zip64 END start disk
2361                 writeLong(os, offset);            // offset of zip64 END
2362                 writeInt(os, 1);                  // total number of disks (?)
2363             }
2364             writeInt(os, ENDSIG);                 // END record signature
2365             writeShort(os, 0);                    // number of this disk
2366             writeShort(os, 0);                    // central directory start disk
2367             writeShort(os, count);                // number of directory entries on disk
2368             writeShort(os, count);                // total number of directory entries
2369             writeInt(os, xlen);                   // length of central directory
2370             writeInt(os, xoff);                   // offset of central directory
2371             writeShort(os, 0);                    // zip file comment, not used
2372         }
2373     }
2374 
2375     // Internal node that links a "name" to its pos in cen table.
2376     // The node itself can be used as a "key" to lookup itself in
2377     // the HashMap inodes.
2378     static class IndexNode {
2379         byte[]  name;
2380         int     hashcode;    // node is hashable/hashed by its name
2381         boolean isdir;
2382         int     pos = -1;    // position in cen table, -1 means the
2383                              // entry does not exist in zip file
2384         IndexNode child;     // first child
2385         IndexNode sibling;   // next sibling
2386 
2387         IndexNode() {}
2388 
2389         IndexNode(byte[] name, boolean isdir) {
2390             name(name);
2391             this.isdir = isdir;
2392             this.pos = -1;
2393         }
2394 
2395         IndexNode(byte[] name, int pos) {
2396             name(name);
2397             this.pos = pos;
2398         }
2399 
2400         // constructor for initCEN() (1) remove trailing '/' (2) pad leading '/'
2401         IndexNode(byte[] cen, int pos, int nlen) {
2402             int noff = pos + CENHDR;
2403             if (cen[noff + nlen - 1] == '/') {
2404                 isdir = true;
2405                 nlen--;
2406             }
2407             if (nlen > 0 && cen[noff] == '/') {
2408                 name = Arrays.copyOfRange(cen, noff, noff + nlen);
2409             } else {
2410                 name = new byte[nlen + 1];
2411                 System.arraycopy(cen, noff, name, 1, nlen);
2412                 name[0] = '/';
2413             }
2414             name(normalize(name));
2415             this.pos = pos;
2416         }
2417 
2418         // Normalize the IndexNode.name field.
2419         private byte[] normalize(byte[] path) {
2420             int len = path.length;
2421             if (len == 0)
2422                 return path;
2423             byte prevC = 0;
2424             for (int pathPos = 0; pathPos < len; pathPos++) {
2425                 byte c = path[pathPos];
2426                 if (c == '/' && prevC == '/')
2427                     return normalize(path, pathPos - 1);
2428                 prevC = c;
2429             }
2430             if (len > 1 && prevC == '/') {
2431                 return Arrays.copyOf(path, len - 1);
2432             }
2433             return path;
2434         }
2435 
2436         private byte[] normalize(byte[] path, int off) {
2437             // As we know we have at least one / to trim, we can reduce
2438             // the size of the resulting array
2439             byte[] to = new byte[path.length - 1];
2440             int pathPos = 0;
2441             while (pathPos < off) {
2442                 to[pathPos] = path[pathPos];
2443                 pathPos++;
2444             }
2445             int toPos = pathPos;
2446             byte prevC = 0;
2447             while (pathPos < path.length) {
2448                 byte c = path[pathPos++];
2449                 if (c == '/' && prevC == '/')
2450                     continue;
2451                 to[toPos++] = c;
2452                 prevC = c;
2453             }
2454             if (toPos > 1 && to[toPos - 1] == '/')
2455                 toPos--;
2456             return (toPos == to.length) ? to : Arrays.copyOf(to, toPos);
2457         }
2458 
2459         private static final ThreadLocal<IndexNode> cachedKey = new ThreadLocal<>();
2460 
2461         static final IndexNode keyOf(byte[] name) { // get a lookup key;
2462             IndexNode key = cachedKey.get();
2463             if (key == null) {
2464                 key = new IndexNode(name, -1);
2465                 cachedKey.set(key);
2466             }
2467             return key.as(name);
2468         }
2469 
2470         final void name(byte[] name) {
2471             this.name = name;
2472             this.hashcode = Arrays.hashCode(name);
2473         }
2474 
2475         final IndexNode as(byte[] name) {           // reuse the node, mostly
2476             name(name);                             // as a lookup "key"
2477             return this;
2478         }
2479 
2480         boolean isDir() {
2481             return isdir;
2482         }
2483 
2484         @Override
2485         public boolean equals(Object other) {
2486             if (!(other instanceof IndexNode)) {
2487                 return false;
2488             }
2489             if (other instanceof ParentLookup) {
2490                 return ((ParentLookup)other).equals(this);
2491             }
2492             return Arrays.equals(name, ((IndexNode)other).name);
2493         }
2494 
2495         @Override
2496         public int hashCode() {
2497             return hashcode;
2498         }
2499 
2500         @Override
2501         public String toString() {
2502             return new String(name) + (isdir ? " (dir)" : " ") + ", index: " + pos;
2503         }
2504     }
2505 
2506     static class Entry extends IndexNode implements ZipFileAttributes {
2507         static final int CEN    = 1;  // entry read from cen
2508         static final int NEW    = 2;  // updated contents in bytes or file
2509         static final int FILECH = 3;  // fch update in "file"
2510         static final int COPY   = 4;  // copy of a CEN entry
2511 
2512         byte[] bytes;                 // updated content bytes
2513         Path   file;                  // use tmp file to store bytes;
2514         int    type = CEN;            // default is the entry read from cen
2515 
2516         // entry attributes
2517         int    version;
2518         int    flag;
2519         int    posixPerms = -1; // posix permissions
2520         int    method = -1;    // compression method
2521         long   mtime  = -1;    // last modification time (in DOS time)
2522         long   atime  = -1;    // last access time
2523         long   ctime  = -1;    // create time
2524         long   crc    = -1;    // crc-32 of entry data
2525         long   csize  = -1;    // compressed size of entry data
2526         long   size   = -1;    // uncompressed size of entry data
2527         byte[] extra;
2528 
2529         // CEN
2530         // The fields that are commented out below are not used by anyone and write() uses "0"
2531         // int    versionMade;
2532         // int    disk;
2533         // int    attrs;
2534         // long   attrsEx;
2535         long   locoff;
2536         byte[] comment;
2537 
2538         Entry(byte[] name, boolean isdir, int method) {
2539             name(name);
2540             this.isdir = isdir;
2541             this.mtime  = this.ctime = this.atime = System.currentTimeMillis();
2542             this.crc    = 0;
2543             this.size   = 0;
2544             this.csize  = 0;
2545             this.method = method;
2546         }
2547 
2548         @SuppressWarnings("unchecked")
2549         Entry(byte[] name, int type, boolean isdir, int method, FileAttribute<?>... attrs) {
2550             this(name, isdir, method);
2551             this.type = type;
2552             for (FileAttribute<?> attr : attrs) {
2553                 String attrName = attr.name();
2554                 if (attrName.equals("posix:permissions")) {
2555                     posixPerms = ZipUtils.permsToFlags((Set<PosixFilePermission>)attr.value());
2556                 }
2557             }
2558         }
2559 
2560         Entry(byte[] name, Path file, int type, FileAttribute<?>... attrs) {
2561             this(name, type, false, METHOD_STORED, attrs);
2562             this.file = file;
2563         }
2564 
2565         Entry(Entry e, int type, int compressionMethod) {
2566             this(e, type);
2567             this.method = compressionMethod;
2568         }
2569 
2570         Entry(Entry e, int type) {
2571             name(e.name);
2572             this.isdir     = e.isdir;
2573             this.version   = e.version;
2574             this.ctime     = e.ctime;
2575             this.atime     = e.atime;
2576             this.mtime     = e.mtime;
2577             this.crc       = e.crc;
2578             this.size      = e.size;
2579             this.csize     = e.csize;
2580             this.method    = e.method;
2581             this.extra     = e.extra;
2582             /*
2583             this.versionMade = e.versionMade;
2584             this.disk      = e.disk;
2585             this.attrs     = e.attrs;
2586             this.attrsEx   = e.attrsEx;
2587             */
2588             this.locoff    = e.locoff;
2589             this.comment   = e.comment;
2590             this.posixPerms = e.posixPerms;
2591             this.type      = type;
2592         }
2593 
2594         Entry(ZipFileSystem zipfs, IndexNode inode) throws IOException {
2595             readCEN(zipfs, inode);
2596         }
2597 
2598         // Calculates a suitable base for the version number to
2599         // be used for fields version made by/version needed to extract.
2600         // The lower bytes of these 2 byte fields hold the version number
2601         // (value/10 = major; value%10 = minor)
2602         // For different features certain minimum versions apply:
2603         // stored = 10 (1.0), deflated = 20 (2.0), zip64 = 45 (4.5)
2604         private int version(boolean zip64) throws ZipException {
2605             if (zip64) {
2606                 return 45;
2607             }
2608             if (method == METHOD_DEFLATED)
2609                 return 20;
2610             else if (method == METHOD_STORED)
2611                 return 10;
2612             throw new ZipException("unsupported compression method");
2613         }
2614 
2615         /**
2616          * Adds information about compatibility of file attribute information
2617          * to a version value.
2618          */
2619         private int versionMadeBy(int version) {
2620             return (posixPerms < 0) ? version :
2621                 VERSION_MADE_BY_BASE_UNIX | (version & 0xff);
2622         }
2623 
2624         ///////////////////// CEN //////////////////////
2625         private void readCEN(ZipFileSystem zipfs, IndexNode inode) throws IOException {
2626             byte[] cen = zipfs.cen;
2627             int pos = inode.pos;
2628             if (!cenSigAt(cen, pos))
2629                 throw new ZipException("invalid CEN header (bad signature)");
2630             version     = CENVER(cen, pos);
2631             flag        = CENFLG(cen, pos);
2632             method      = CENHOW(cen, pos);
2633             mtime       = dosToJavaTime(CENTIM(cen, pos));
2634             crc         = CENCRC(cen, pos);
2635             csize       = CENSIZ(cen, pos);
2636             size        = CENLEN(cen, pos);
2637             int nlen    = CENNAM(cen, pos);
2638             int elen    = CENEXT(cen, pos);
2639             int clen    = CENCOM(cen, pos);
2640             /*
2641             versionMade = CENVEM(cen, pos);
2642             disk        = CENDSK(cen, pos);
2643             attrs       = CENATT(cen, pos);
2644             attrsEx     = CENATX(cen, pos);
2645             */
2646             if (CENVEM_FA(cen, pos) == FILE_ATTRIBUTES_UNIX) {
2647                 posixPerms = CENATX_PERMS(cen, pos) & 0xFFF; // 12 bits for setuid, setgid, sticky + perms
2648             }
2649             locoff      = CENOFF(cen, pos);
2650             pos += CENHDR;
2651             this.name = inode.name;
2652             this.isdir = inode.isdir;
2653             this.hashcode = inode.hashcode;
2654 
2655             pos += nlen;
2656             if (elen > 0) {
2657                 extra = Arrays.copyOfRange(cen, pos, pos + elen);
2658                 pos += elen;
2659                 readExtra(zipfs);
2660             }
2661             if (clen > 0) {
2662                 comment = Arrays.copyOfRange(cen, pos, pos + clen);
2663             }
2664         }
2665 
2666         private int writeCEN(OutputStream os) throws IOException {
2667             long csize0  = csize;
2668             long size0   = size;
2669             long locoff0 = locoff;
2670             int elen64   = 0;                // extra for ZIP64
2671             int elenNTFS = 0;                // extra for NTFS (a/c/mtime)
2672             int elenEXTT = 0;                // extra for Extended Timestamp
2673             boolean foundExtraTime = false;  // if time stamp NTFS, EXTT present
2674 
2675             byte[] zname = isdir ? toDirectoryPath(name) : name;
2676 
2677             // confirm size/length
2678             int nlen = (zname != null) ? zname.length - 1 : 0;  // name has [0] as "slash"
2679             int elen = (extra != null) ? extra.length : 0;
2680             int eoff = 0;
2681             int clen = (comment != null) ? comment.length : 0;
2682             if (csize >= ZIP64_MINVAL) {
2683                 csize0 = ZIP64_MINVAL;
2684                 elen64 += 8;                 // csize(8)
2685             }
2686             if (size >= ZIP64_MINVAL) {
2687                 size0 = ZIP64_MINVAL;        // size(8)
2688                 elen64 += 8;
2689             }
2690             if (locoff >= ZIP64_MINVAL) {
2691                 locoff0 = ZIP64_MINVAL;
2692                 elen64 += 8;                 // offset(8)
2693             }
2694             if (elen64 != 0) {
2695                 elen64 += 4;                 // header and data sz 4 bytes
2696             }
2697             boolean zip64 = (elen64 != 0);
2698             int version0 = version(zip64);
2699             while (eoff + 4 < elen) {
2700                 int tag = SH(extra, eoff);
2701                 int sz = SH(extra, eoff + 2);
2702                 if (tag == EXTID_EXTT || tag == EXTID_NTFS) {
2703                     foundExtraTime = true;
2704                 }
2705                 eoff += (4 + sz);
2706             }
2707             if (!foundExtraTime) {
2708                 if (isWindows) {             // use NTFS
2709                     elenNTFS = 36;           // total 36 bytes
2710                 } else {                     // Extended Timestamp otherwise
2711                     elenEXTT = 9;            // only mtime in cen
2712                 }
2713             }
2714             writeInt(os, CENSIG);            // CEN header signature
2715             writeShort(os, versionMadeBy(version0)); // version made by
2716             writeShort(os, version0);        // version needed to extract
2717             writeShort(os, flag);            // general purpose bit flag
2718             writeShort(os, method);          // compression method
2719                                              // last modification time
2720             writeInt(os, (int)javaToDosTime(mtime));
2721             writeInt(os, crc);               // crc-32
2722             writeInt(os, csize0);            // compressed size
2723             writeInt(os, size0);             // uncompressed size
2724             writeShort(os, nlen);
2725             writeShort(os, elen + elen64 + elenNTFS + elenEXTT);
2726 
2727             if (comment != null) {
2728                 writeShort(os, Math.min(clen, 0xffff));
2729             } else {
2730                 writeShort(os, 0);
2731             }
2732             writeShort(os, 0);              // starting disk number
2733             writeShort(os, 0);              // internal file attributes (unused)
2734             writeInt(os, posixPerms > 0 ? posixPerms << 16 : 0); // external file
2735                                             // attributes, used for storing posix
2736                                             // permissions
2737             writeInt(os, locoff0);          // relative offset of local header
2738             writeBytes(os, zname, 1, nlen);
2739             if (zip64) {
2740                 writeShort(os, EXTID_ZIP64);// Zip64 extra
2741                 writeShort(os, elen64 - 4); // size of "this" extra block
2742                 if (size0 == ZIP64_MINVAL)
2743                     writeLong(os, size);
2744                 if (csize0 == ZIP64_MINVAL)
2745                     writeLong(os, csize);
2746                 if (locoff0 == ZIP64_MINVAL)
2747                     writeLong(os, locoff);
2748             }
2749             if (elenNTFS != 0) {
2750                 writeShort(os, EXTID_NTFS);
2751                 writeShort(os, elenNTFS - 4);
2752                 writeInt(os, 0);            // reserved
2753                 writeShort(os, 0x0001);     // NTFS attr tag
2754                 writeShort(os, 24);
2755                 writeLong(os, javaToWinTime(mtime));
2756                 writeLong(os, javaToWinTime(atime));
2757                 writeLong(os, javaToWinTime(ctime));
2758             }
2759             if (elenEXTT != 0) {
2760                 writeShort(os, EXTID_EXTT);
2761                 writeShort(os, elenEXTT - 4);
2762                 if (ctime == -1)
2763                     os.write(0x3);          // mtime and atime
2764                 else
2765                     os.write(0x7);          // mtime, atime and ctime
2766                 writeInt(os, javaToUnixTime(mtime));
2767             }
2768             if (extra != null)              // whatever not recognized
2769                 writeBytes(os, extra);
2770             if (comment != null)            //TBD: 0, Math.min(commentBytes.length, 0xffff));
2771                 writeBytes(os, comment);
2772             return CENHDR + nlen + elen + clen + elen64 + elenNTFS + elenEXTT;
2773         }
2774 
2775         ///////////////////// LOC //////////////////////
2776 
2777         private int writeLOC(OutputStream os) throws IOException {
2778             byte[] zname = isdir ? toDirectoryPath(name) : name;
2779             int nlen = (zname != null) ? zname.length - 1 : 0; // [0] is slash
2780             int elen = (extra != null) ? extra.length : 0;
2781             boolean foundExtraTime = false;     // if extra timestamp present
2782             int eoff = 0;
2783             int elen64 = 0;
2784             boolean zip64 = false;
2785             int elenEXTT = 0;
2786             int elenNTFS = 0;
2787             writeInt(os, LOCSIG);               // LOC header signature
2788             if ((flag & FLAG_DATADESCR) != 0) {
2789                 writeShort(os, version(false)); // version needed to extract
2790                 writeShort(os, flag);           // general purpose bit flag
2791                 writeShort(os, method);         // compression method
2792                 // last modification time
2793                 writeInt(os, (int)javaToDosTime(mtime));
2794                 // store size, uncompressed size, and crc-32 in data descriptor
2795                 // immediately following compressed entry data
2796                 writeInt(os, 0);
2797                 writeInt(os, 0);
2798                 writeInt(os, 0);
2799             } else {
2800                 if (csize >= ZIP64_MINVAL || size >= ZIP64_MINVAL) {
2801                     elen64 = 20;    //headid(2) + size(2) + size(8) + csize(8)
2802                     zip64 = true;
2803                 }
2804                 writeShort(os, version(zip64)); // version needed to extract
2805                 writeShort(os, flag);           // general purpose bit flag
2806                 writeShort(os, method);         // compression method
2807                                                 // last modification time
2808                 writeInt(os, (int)javaToDosTime(mtime));
2809                 writeInt(os, crc);              // crc-32
2810                 if (zip64) {
2811                     writeInt(os, ZIP64_MINVAL);
2812                     writeInt(os, ZIP64_MINVAL);
2813                 } else {
2814                     writeInt(os, csize);        // compressed size
2815                     writeInt(os, size);         // uncompressed size
2816                 }
2817             }
2818             while (eoff + 4 < elen) {
2819                 int tag = SH(extra, eoff);
2820                 int sz = SH(extra, eoff + 2);
2821                 if (tag == EXTID_EXTT || tag == EXTID_NTFS) {
2822                     foundExtraTime = true;
2823                 }
2824                 eoff += (4 + sz);
2825             }
2826             if (!foundExtraTime) {
2827                 if (isWindows) {
2828                     elenNTFS = 36;              // NTFS, total 36 bytes
2829                 } else {                        // on unix use "ext time"
2830                     elenEXTT = 9;
2831                     if (atime != -1)
2832                         elenEXTT += 4;
2833                     if (ctime != -1)
2834                         elenEXTT += 4;
2835                 }
2836             }
2837             writeShort(os, nlen);
2838             writeShort(os, elen + elen64 + elenNTFS + elenEXTT);
2839             writeBytes(os, zname, 1, nlen);
2840             if (zip64) {
2841                 writeShort(os, EXTID_ZIP64);
2842                 writeShort(os, 16);
2843                 writeLong(os, size);
2844                 writeLong(os, csize);
2845             }
2846             if (elenNTFS != 0) {
2847                 writeShort(os, EXTID_NTFS);
2848                 writeShort(os, elenNTFS - 4);
2849                 writeInt(os, 0);            // reserved
2850                 writeShort(os, 0x0001);     // NTFS attr tag
2851                 writeShort(os, 24);
2852                 writeLong(os, javaToWinTime(mtime));
2853                 writeLong(os, javaToWinTime(atime));
2854                 writeLong(os, javaToWinTime(ctime));
2855             }
2856             if (elenEXTT != 0) {
2857                 writeShort(os, EXTID_EXTT);
2858                 writeShort(os, elenEXTT - 4);// size for the folowing data block
2859                 int fbyte = 0x1;
2860                 if (atime != -1)           // mtime and atime
2861                     fbyte |= 0x2;
2862                 if (ctime != -1)           // mtime, atime and ctime
2863                     fbyte |= 0x4;
2864                 os.write(fbyte);           // flags byte
2865                 writeInt(os, javaToUnixTime(mtime));
2866                 if (atime != -1)
2867                     writeInt(os, javaToUnixTime(atime));
2868                 if (ctime != -1)
2869                     writeInt(os, javaToUnixTime(ctime));
2870             }
2871             if (extra != null) {
2872                 writeBytes(os, extra);
2873             }
2874             return LOCHDR + nlen + elen + elen64 + elenNTFS + elenEXTT;
2875         }
2876 
2877         // Data Descriptor
2878         private int writeEXT(OutputStream os) throws IOException {
2879             writeInt(os, EXTSIG);           // EXT header signature
2880             writeInt(os, crc);              // crc-32
2881             if (csize >= ZIP64_MINVAL || size >= ZIP64_MINVAL) {
2882                 writeLong(os, csize);
2883                 writeLong(os, size);
2884                 return 24;
2885             } else {
2886                 writeInt(os, csize);        // compressed size
2887                 writeInt(os, size);         // uncompressed size
2888                 return 16;
2889             }
2890         }
2891 
2892         // read NTFS, UNIX and ZIP64 data from cen.extra
2893         private void readExtra(ZipFileSystem zipfs) throws IOException {
2894             if (extra == null)
2895                 return;
2896             int elen = extra.length;
2897             int off = 0;
2898             int newOff = 0;
2899             while (off + 4 < elen) {
2900                 // extra spec: HeaderID+DataSize+Data
2901                 int pos = off;
2902                 int tag = SH(extra, pos);
2903                 int sz = SH(extra, pos + 2);
2904                 pos += 4;
2905                 if (pos + sz > elen)         // invalid data
2906                     break;
2907                 switch (tag) {
2908                 case EXTID_ZIP64 :
2909                     if (size == ZIP64_MINVAL) {
2910                         if (pos + 8 > elen)  // invalid zip64 extra
2911                             break;           // fields, just skip
2912                         size = LL(extra, pos);
2913                         pos += 8;
2914                     }
2915                     if (csize == ZIP64_MINVAL) {
2916                         if (pos + 8 > elen)
2917                             break;
2918                         csize = LL(extra, pos);
2919                         pos += 8;
2920                     }
2921                     if (locoff == ZIP64_MINVAL) {
2922                         if (pos + 8 > elen)
2923                             break;
2924                         locoff = LL(extra, pos);
2925                     }
2926                     break;
2927                 case EXTID_NTFS:
2928                     if (sz < 32)
2929                         break;
2930                     pos += 4;    // reserved 4 bytes
2931                     if (SH(extra, pos) !=  0x0001)
2932                         break;
2933                     if (SH(extra, pos + 2) != 24)
2934                         break;
2935                     // override the loc field, datatime here is
2936                     // more "accurate"
2937                     mtime  = winToJavaTime(LL(extra, pos + 4));
2938                     atime  = winToJavaTime(LL(extra, pos + 12));
2939                     ctime  = winToJavaTime(LL(extra, pos + 20));
2940                     break;
2941                 case EXTID_EXTT:
2942                     // spec says the Extened timestamp in cen only has mtime
2943                     // need to read the loc to get the extra a/ctime, if flag
2944                     // "zipinfo-time" is not specified to false;
2945                     // there is performance cost (move up to loc and read) to
2946                     // access the loc table foreach entry;
2947                     if (zipfs.noExtt) {
2948                         if (sz == 5)
2949                             mtime = unixToJavaTime(LG(extra, pos + 1));
2950                          break;
2951                     }
2952                     byte[] buf = new byte[LOCHDR];
2953                     if (zipfs.readFullyAt(buf, 0, buf.length , locoff)
2954                         != buf.length)
2955                         throw new ZipException("loc: reading failed");
2956                     if (!locSigAt(buf, 0))
2957                         throw new ZipException("loc: wrong sig ->"
2958                                            + Long.toString(getSig(buf, 0), 16));
2959                     int locElen = LOCEXT(buf);
2960                     if (locElen < 9)    // EXTT is at least 9 bytes
2961                         break;
2962                     int locNlen = LOCNAM(buf);
2963                     buf = new byte[locElen];
2964                     if (zipfs.readFullyAt(buf, 0, buf.length , locoff + LOCHDR + locNlen)
2965                         != buf.length)
2966                         throw new ZipException("loc extra: reading failed");
2967                     int locPos = 0;
2968                     while (locPos + 4 < buf.length) {
2969                         int locTag = SH(buf, locPos);
2970                         int locSZ  = SH(buf, locPos + 2);
2971                         locPos += 4;
2972                         if (locTag  != EXTID_EXTT) {
2973                             locPos += locSZ;
2974                              continue;
2975                         }
2976                         int end = locPos + locSZ - 4;
2977                         int flag = CH(buf, locPos++);
2978                         if ((flag & 0x1) != 0 && locPos <= end) {
2979                             mtime = unixToJavaTime(LG(buf, locPos));
2980                             locPos += 4;
2981                         }
2982                         if ((flag & 0x2) != 0 && locPos <= end) {
2983                             atime = unixToJavaTime(LG(buf, locPos));
2984                             locPos += 4;
2985                         }
2986                         if ((flag & 0x4) != 0 && locPos <= end) {
2987                             ctime = unixToJavaTime(LG(buf, locPos));
2988                         }
2989                         break;
2990                     }
2991                     break;
2992                 default:    // unknown tag
2993                     System.arraycopy(extra, off, extra, newOff, sz + 4);
2994                     newOff += (sz + 4);
2995                 }
2996                 off += (sz + 4);
2997             }
2998             if (newOff != 0 && newOff != extra.length)
2999                 extra = Arrays.copyOf(extra, newOff);
3000             else
3001                 extra = null;
3002         }
3003 
3004         @Override
3005         public String toString() {
3006             StringBuilder sb = new StringBuilder(1024);
3007             Formatter fm = new Formatter(sb);
3008             fm.format("    name            : %s%n", new String(name));
3009             fm.format("    creationTime    : %tc%n", creationTime().toMillis());
3010             fm.format("    lastAccessTime  : %tc%n", lastAccessTime().toMillis());
3011             fm.format("    lastModifiedTime: %tc%n", lastModifiedTime().toMillis());
3012             fm.format("    isRegularFile   : %b%n", isRegularFile());
3013             fm.format("    isDirectory     : %b%n", isDirectory());
3014             fm.format("    isSymbolicLink  : %b%n", isSymbolicLink());
3015             fm.format("    isOther         : %b%n", isOther());
3016             fm.format("    fileKey         : %s%n", fileKey());
3017             fm.format("    size            : %d%n", size());
3018             fm.format("    compressedSize  : %d%n", compressedSize());
3019             fm.format("    crc             : %x%n", crc());
3020             fm.format("    method          : %d%n", method());
3021             Set<PosixFilePermission> permissions = storedPermissions().orElse(null);
3022             if (permissions != null) {
3023                 fm.format("    permissions     : %s%n", permissions);
3024             }
3025             fm.close();
3026             return sb.toString();
3027         }
3028 
3029         ///////// basic file attributes ///////////
3030         @Override
3031         public FileTime creationTime() {
3032             return FileTime.fromMillis(ctime == -1 ? mtime : ctime);
3033         }
3034 
3035         @Override
3036         public boolean isDirectory() {
3037             return isDir();
3038         }
3039 
3040         @Override
3041         public boolean isOther() {
3042             return false;
3043         }
3044 
3045         @Override
3046         public boolean isRegularFile() {
3047             return !isDir();
3048         }
3049 
3050         @Override
3051         public FileTime lastAccessTime() {
3052             return FileTime.fromMillis(atime == -1 ? mtime : atime);
3053         }
3054 
3055         @Override
3056         public FileTime lastModifiedTime() {
3057             return FileTime.fromMillis(mtime);
3058         }
3059 
3060         @Override
3061         public long size() {
3062             return size;
3063         }
3064 
3065         @Override
3066         public boolean isSymbolicLink() {
3067             return false;
3068         }
3069 
3070         @Override
3071         public Object fileKey() {
3072             return null;
3073         }
3074 
3075         ///////// zip file attributes ///////////
3076 
3077         @Override
3078         public long compressedSize() {
3079             return csize;
3080         }
3081 
3082         @Override
3083         public long crc() {
3084             return crc;
3085         }
3086 
3087         @Override
3088         public int method() {
3089             return method;
3090         }
3091 
3092         @Override
3093         public byte[] extra() {
3094             if (extra != null)
3095                 return Arrays.copyOf(extra, extra.length);
3096             return null;
3097         }
3098 
3099         @Override
3100         public byte[] comment() {
3101             if (comment != null)
3102                 return Arrays.copyOf(comment, comment.length);
3103             return null;
3104         }
3105 
3106         @Override
3107         public Optional<Set<PosixFilePermission>> storedPermissions() {
3108             Set<PosixFilePermission> perms = null;
3109             if (posixPerms != -1) {
3110                 perms = new HashSet<>(PosixFilePermission.values().length);
3111                 for (PosixFilePermission perm : PosixFilePermission.values()) {
3112                     if ((posixPerms & ZipUtils.permToFlag(perm)) != 0) {
3113                         perms.add(perm);
3114                     }
3115                 }
3116             }
3117             return Optional.ofNullable(perms);
3118         }
3119     }
3120 
3121     final class PosixEntry extends Entry implements PosixFileAttributes {
3122         private UserPrincipal owner = defaultOwner;
3123         private GroupPrincipal group = defaultGroup;
3124 
3125         PosixEntry(byte[] name, boolean isdir, int method) {
3126             super(name, isdir, method);
3127         }
3128 
3129         PosixEntry(byte[] name, int type, boolean isdir, int method, FileAttribute<?>... attrs) {
3130             super(name, type, isdir, method, attrs);
3131         }
3132 
3133         PosixEntry(byte[] name, Path file, int type, FileAttribute<?>... attrs) {
3134             super(name, file, type, attrs);
3135         }
3136 
3137         PosixEntry(PosixEntry e, int type, int compressionMethod) {
3138             super(e, type);
3139             this.method = compressionMethod;
3140         }
3141 
3142         PosixEntry(PosixEntry e, int type) {
3143             super(e, type);
3144             this.owner = e.owner;
3145             this.group = e.group;
3146         }
3147 
3148         PosixEntry(ZipFileSystem zipfs, IndexNode inode) throws IOException {
3149             super(zipfs, inode);
3150         }
3151 
3152         @Override
3153         public UserPrincipal owner() {
3154             return owner;
3155         }
3156 
3157         @Override
3158         public GroupPrincipal group() {
3159             return group;
3160         }
3161 
3162         @Override
3163         public Set<PosixFilePermission> permissions() {
3164             return storedPermissions().orElse(Set.copyOf(defaultPermissions));
3165         }
3166     }
3167 
3168     private static class ExistingChannelCloser {
3169         private final Path path;
3170         private final SeekableByteChannel ch;
3171         private final Set<InputStream> streams;
3172         ExistingChannelCloser(Path path,
3173                               SeekableByteChannel ch,
3174                               Set<InputStream> streams) {
3175             this.path = path;
3176             this.ch = ch;
3177             this.streams = streams;
3178         }
3179 
3180         /**
3181          * If there are no more outstanding streams, close the channel and
3182          * delete the backing file
3183          *
3184          * @return true if we're done and closed the backing file,
3185          *         otherwise false
3186          * @throws IOException
3187          */
3188         private boolean closeAndDeleteIfDone() throws IOException {
3189             if (streams.isEmpty()) {
3190                 ch.close();
3191                 Files.delete(path);
3192                 return true;
3193             }
3194             return false;
3195         }
3196     }
3197 
3198     // purely for parent lookup, so we don't have to copy the parent
3199     // name every time
3200     static class ParentLookup extends IndexNode {
3201         int len;
3202         ParentLookup() {}
3203 
3204         final ParentLookup as(byte[] name, int len) { // as a lookup "key"
3205             name(name, len);
3206             return this;
3207         }
3208 
3209         void name(byte[] name, int len) {
3210             this.name = name;
3211             this.len = len;
3212             // calculate the hashcode the same way as Arrays.hashCode() does
3213             int result = 1;
3214             for (int i = 0; i < len; i++)
3215                 result = 31 * result + name[i];
3216             this.hashcode = result;
3217         }
3218 
3219         @Override
3220         public boolean equals(Object other) {
3221             if (!(other instanceof IndexNode)) {
3222                 return false;
3223             }
3224             byte[] oname = ((IndexNode)other).name;
3225             return Arrays.equals(name, 0, len,
3226                                  oname, 0, oname.length);
3227         }
3228     }
3229 }