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