rev 57619 : 7143743: (zipfs) Potential memory leak with zip provider
Summary: ZipFileSystem.close() now clears the "inodes" Map, to avoid holding on to potentially large amounts of data
Reviewed-by:

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