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