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