1 /* 2 * Copyright (c) 1997, 2013, 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 java.util.jar; 27 28 import java.io.*; 29 import java.lang.ref.SoftReference; 30 import java.net.URL; 31 import java.util.*; 32 import java.util.stream.Stream; 33 import java.util.stream.StreamSupport; 34 import java.util.zip.*; 35 import java.security.CodeSigner; 36 import java.security.cert.Certificate; 37 import java.security.AccessController; 38 import java.security.CodeSource; 39 import sun.misc.IOUtils; 40 import sun.security.action.GetPropertyAction; 41 import sun.security.util.ManifestEntryVerifier; 42 import sun.misc.SharedSecrets; 43 import sun.security.util.SignatureFileVerifier; 44 45 /** 46 * The <code>JarFile</code> class is used to read the contents of a jar file 47 * from any file that can be opened with <code>java.io.RandomAccessFile</code>. 48 * It extends the class <code>java.util.zip.ZipFile</code> with support 49 * for reading an optional <code>Manifest</code> entry. The 50 * <code>Manifest</code> can be used to specify meta-information about the 51 * jar file and its entries. 52 * 53 * <p> Unless otherwise noted, passing a <tt>null</tt> argument to a constructor 54 * or method in this class will cause a {@link NullPointerException} to be 55 * thrown. 56 * 57 * If the verify flag is on when opening a signed jar file, the content of the 58 * file is verified against its signature embedded inside the file. Please note 59 * that the verification process does not include validating the signer's 60 * certificate. A caller should inspect the return value of 61 * {@link JarEntry#getCodeSigners()} to further determine if the signature 62 * can be trusted. 63 * 64 * @author David Connelly 65 * @see Manifest 66 * @see java.util.zip.ZipFile 67 * @see java.util.jar.JarEntry 68 * @since 1.2 69 */ 70 public 71 class JarFile extends ZipFile { 72 private SoftReference<Manifest> manRef; 73 private JarEntry manEntry; 74 private JarVerifier jv; 75 private boolean jvInitialized; 76 private boolean verify; 77 78 // indicates if Class-Path attribute present (only valid if hasCheckedSpecialAttributes true) 79 private boolean hasClassPathAttribute; 80 // true if manifest checked for special attributes 81 private volatile boolean hasCheckedSpecialAttributes; 82 83 // Set up JavaUtilJarAccess in SharedSecrets 84 static { 85 SharedSecrets.setJavaUtilJarAccess(new JavaUtilJarAccessImpl()); 86 } 87 88 /** 89 * The JAR manifest file name. 90 */ 91 public static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; 92 93 /** 94 * Creates a new <code>JarFile</code> to read from the specified 95 * file <code>name</code>. The <code>JarFile</code> will be verified if 96 * it is signed. 97 * @param name the name of the jar file to be opened for reading 98 * @throws IOException if an I/O error has occurred 99 * @throws SecurityException if access to the file is denied 100 * by the SecurityManager 101 */ 102 public JarFile(String name) throws IOException { 103 this(new File(name), true, ZipFile.OPEN_READ); 104 } 105 106 /** 107 * Creates a new <code>JarFile</code> to read from the specified 108 * file <code>name</code>. 109 * @param name the name of the jar file to be opened for reading 110 * @param verify whether or not to verify the jar file if 111 * it is signed. 112 * @throws IOException if an I/O error has occurred 113 * @throws SecurityException if access to the file is denied 114 * by the SecurityManager 115 */ 116 public JarFile(String name, boolean verify) throws IOException { 117 this(new File(name), verify, ZipFile.OPEN_READ); 118 } 119 120 /** 121 * Creates a new <code>JarFile</code> to read from the specified 122 * <code>File</code> object. The <code>JarFile</code> will be verified if 123 * it is signed. 124 * @param file the jar file to be opened for reading 125 * @throws IOException if an I/O error has occurred 126 * @throws SecurityException if access to the file is denied 127 * by the SecurityManager 128 */ 129 public JarFile(File file) throws IOException { 130 this(file, true, ZipFile.OPEN_READ); 131 } 132 133 134 /** 135 * Creates a new <code>JarFile</code> to read from the specified 136 * <code>File</code> object. 137 * @param file the jar file to be opened for reading 138 * @param verify whether or not to verify the jar file if 139 * it is signed. 140 * @throws IOException if an I/O error has occurred 141 * @throws SecurityException if access to the file is denied 142 * by the SecurityManager. 143 */ 144 public JarFile(File file, boolean verify) throws IOException { 145 this(file, verify, ZipFile.OPEN_READ); 146 } 147 148 149 /** 150 * Creates a new <code>JarFile</code> to read from the specified 151 * <code>File</code> object in the specified mode. The mode argument 152 * must be either <tt>OPEN_READ</tt> or <tt>OPEN_READ | OPEN_DELETE</tt>. 153 * 154 * @param file the jar file to be opened for reading 155 * @param verify whether or not to verify the jar file if 156 * it is signed. 157 * @param mode the mode in which the file is to be opened 158 * @throws IOException if an I/O error has occurred 159 * @throws IllegalArgumentException 160 * if the <tt>mode</tt> argument is invalid 161 * @throws SecurityException if access to the file is denied 162 * by the SecurityManager 163 * @since 1.3 164 */ 165 public JarFile(File file, boolean verify, int mode) throws IOException { 166 super(file, mode); 167 this.verify = verify; 168 } 169 170 /** 171 * Returns the jar file manifest, or <code>null</code> if none. 172 * 173 * @return the jar file manifest, or <code>null</code> if none 174 * 175 * @throws IllegalStateException 176 * may be thrown if the jar file has been closed 177 * @throws IOException if an I/O error has occurred 178 */ 179 public Manifest getManifest() throws IOException { 180 return getManifestFromReference(); 181 } 182 183 private Manifest getManifestFromReference() throws IOException { 184 Manifest man = manRef != null ? manRef.get() : null; 185 186 if (man == null) { 187 188 JarEntry manEntry = getManEntry(); 189 190 // If found then load the manifest 191 if (manEntry != null) { 192 if (verify) { 193 byte[] b = getBytes(manEntry); 194 man = new Manifest(new ByteArrayInputStream(b)); 195 if (!jvInitialized) { 196 jv = new JarVerifier(b); 197 } 198 } else { 199 man = new Manifest(super.getInputStream(manEntry)); 200 } 201 manRef = new SoftReference<>(man); 202 } 203 } 204 return man; 205 } 206 207 private native String[] getMetaInfEntryNames(); 208 209 /** 210 * Returns the <code>JarEntry</code> for the given entry name or 211 * <code>null</code> if not found. 212 * 213 * @param name the jar file entry name 214 * @return the <code>JarEntry</code> for the given entry name or 215 * <code>null</code> if not found. 216 * 217 * @throws IllegalStateException 218 * may be thrown if the jar file has been closed 219 * 220 * @see java.util.jar.JarEntry 221 */ 222 public JarEntry getJarEntry(String name) { 223 return (JarEntry)getEntry(name); 224 } 225 226 /** 227 * Returns the <code>ZipEntry</code> for the given entry name or 228 * <code>null</code> if not found. 229 * 230 * @param name the jar file entry name 231 * @return the <code>ZipEntry</code> for the given entry name or 232 * <code>null</code> if not found 233 * 234 * @throws IllegalStateException 235 * may be thrown if the jar file has been closed 236 * 237 * @see java.util.zip.ZipEntry 238 */ 239 public ZipEntry getEntry(String name) { 240 ZipEntry ze = super.getEntry(name); 241 if (ze != null) { 242 return new JarFileEntry(ze); 243 } 244 return null; 245 } 246 247 private class JarEntryIterator implements Enumeration<JarEntry>, 248 Iterator<JarEntry> 249 { 250 final Enumeration<? extends ZipEntry> e = JarFile.super.entries(); 251 252 public boolean hasNext() { 253 return e.hasMoreElements(); 254 } 255 256 public JarEntry next() { 257 ZipEntry ze = e.nextElement(); 258 return new JarFileEntry(ze); 259 } 260 261 public boolean hasMoreElements() { 262 return hasNext(); 263 } 264 265 public JarEntry nextElement() { 266 return next(); 267 } 268 } 269 270 /** 271 * Returns an enumeration of the zip file entries. 272 */ 273 public Enumeration<JarEntry> entries() { 274 return new JarEntryIterator(); 275 } 276 277 @Override 278 public Stream<JarEntry> stream() { 279 return StreamSupport.stream(Spliterators.spliterator( 280 new JarEntryIterator(), size(), 281 Spliterator.ORDERED | Spliterator.DISTINCT | 282 Spliterator.IMMUTABLE | Spliterator.NONNULL), false); 283 } 284 285 private class JarFileEntry extends JarEntry { 286 JarFileEntry(ZipEntry ze) { 287 super(ze); 288 } 289 public Attributes getAttributes() throws IOException { 290 Manifest man = JarFile.this.getManifest(); 291 if (man != null) { 292 return man.getAttributes(getName()); 293 } else { 294 return null; 295 } 296 } 297 public Certificate[] getCertificates() { 298 try { 299 maybeInstantiateVerifier(); 300 } catch (IOException e) { 301 throw new RuntimeException(e); 302 } 303 if (certs == null && jv != null) { 304 certs = jv.getCerts(JarFile.this, this); 305 } 306 return certs == null ? null : certs.clone(); 307 } 308 public CodeSigner[] getCodeSigners() { 309 try { 310 maybeInstantiateVerifier(); 311 } catch (IOException e) { 312 throw new RuntimeException(e); 313 } 314 if (signers == null && jv != null) { 315 signers = jv.getCodeSigners(JarFile.this, this); 316 } 317 return signers == null ? null : signers.clone(); 318 } 319 } 320 321 /* 322 * Ensures that the JarVerifier has been created if one is 323 * necessary (i.e., the jar appears to be signed.) This is done as 324 * a quick check to avoid processing of the manifest for unsigned 325 * jars. 326 */ 327 private void maybeInstantiateVerifier() throws IOException { 328 if (jv != null) { 329 return; 330 } 331 332 if (verify) { 333 String[] names = getMetaInfEntryNames(); 334 if (names != null) { 335 for (String nameLower : names) { 336 String name = nameLower.toUpperCase(Locale.ENGLISH); 337 if (name.endsWith(".DSA") || 338 name.endsWith(".RSA") || 339 name.endsWith(".EC") || 340 name.endsWith(".SF")) { 341 // Assume since we found a signature-related file 342 // that the jar is signed and that we therefore 343 // need a JarVerifier and Manifest 344 getManifest(); 345 return; 346 } 347 } 348 } 349 // No signature-related files; don't instantiate a 350 // verifier 351 verify = false; 352 } 353 } 354 355 356 /* 357 * Initializes the verifier object by reading all the manifest 358 * entries and passing them to the verifier. 359 */ 360 private void initializeVerifier() { 361 ManifestEntryVerifier mev = null; 362 363 // Verify "META-INF/" entries... 364 try { 365 String[] names = getMetaInfEntryNames(); 366 if (names != null) { 367 for (String name : names) { 368 String uname = name.toUpperCase(Locale.ENGLISH); 369 if (MANIFEST_NAME.equals(uname) 370 || SignatureFileVerifier.isBlockOrSF(uname)) { 371 JarEntry e = getJarEntry(name); 372 if (e == null) { 373 throw new JarException("corrupted jar file"); 374 } 375 if (mev == null) { 376 mev = new ManifestEntryVerifier 377 (getManifestFromReference()); 378 } 379 byte[] b = getBytes(e); 380 if (b != null && b.length > 0) { 381 jv.beginEntry(e, mev); 382 jv.update(b.length, b, 0, b.length, mev); 383 jv.update(-1, null, 0, 0, mev); 384 } 385 } 386 } 387 } 388 } catch (IOException ex) { 389 // if we had an error parsing any blocks, just 390 // treat the jar file as being unsigned 391 jv = null; 392 verify = false; 393 if (JarVerifier.debug != null) { 394 JarVerifier.debug.println("jarfile parsing error!"); 395 ex.printStackTrace(); 396 } 397 } 398 399 // if after initializing the verifier we have nothing 400 // signed, we null it out. 401 402 if (jv != null) { 403 404 jv.doneWithMeta(); 405 if (JarVerifier.debug != null) { 406 JarVerifier.debug.println("done with meta!"); 407 } 408 409 if (jv.nothingToVerify()) { 410 if (JarVerifier.debug != null) { 411 JarVerifier.debug.println("nothing to verify!"); 412 } 413 jv = null; 414 verify = false; 415 } 416 } 417 } 418 419 /* 420 * Reads all the bytes for a given entry. Used to process the 421 * META-INF files. 422 */ 423 private byte[] getBytes(ZipEntry ze) throws IOException { 424 try (InputStream is = super.getInputStream(ze)) { 425 return IOUtils.readFully(is, (int)ze.getSize(), true); 426 } 427 } 428 429 /** 430 * Returns an input stream for reading the contents of the specified 431 * zip file entry. 432 * @param ze the zip file entry 433 * @return an input stream for reading the contents of the specified 434 * zip file entry 435 * @throws ZipException if a zip file format error has occurred 436 * @throws IOException if an I/O error has occurred 437 * @throws SecurityException if any of the jar file entries 438 * are incorrectly signed. 439 * @throws IllegalStateException 440 * may be thrown if the jar file has been closed 441 */ 442 public synchronized InputStream getInputStream(ZipEntry ze) 443 throws IOException 444 { 445 maybeInstantiateVerifier(); 446 if (jv == null) { 447 return super.getInputStream(ze); 448 } 449 if (!jvInitialized) { 450 initializeVerifier(); 451 jvInitialized = true; 452 // could be set to null after a call to 453 // initializeVerifier if we have nothing to 454 // verify 455 if (jv == null) 456 return super.getInputStream(ze); 457 } 458 459 // wrap a verifier stream around the real stream 460 return new JarVerifier.VerifierStream( 461 getManifestFromReference(), 462 ze instanceof JarFileEntry ? 463 (JarEntry) ze : getJarEntry(ze.getName()), 464 super.getInputStream(ze), 465 jv); 466 } 467 468 // Statics for hand-coded Boyer-Moore search 469 private static final char[] CLASSPATH_CHARS = {'c','l','a','s','s','-','p','a','t','h'}; 470 // The bad character shift for "class-path" 471 private static final int[] CLASSPATH_LASTOCC; 472 // The good suffix shift for "class-path" 473 private static final int[] CLASSPATH_OPTOSFT; 474 475 static { 476 CLASSPATH_LASTOCC = new int[128]; 477 CLASSPATH_OPTOSFT = new int[10]; 478 CLASSPATH_LASTOCC[(int)'c'] = 1; 479 CLASSPATH_LASTOCC[(int)'l'] = 2; 480 CLASSPATH_LASTOCC[(int)'s'] = 5; 481 CLASSPATH_LASTOCC[(int)'-'] = 6; 482 CLASSPATH_LASTOCC[(int)'p'] = 7; 483 CLASSPATH_LASTOCC[(int)'a'] = 8; 484 CLASSPATH_LASTOCC[(int)'t'] = 9; 485 CLASSPATH_LASTOCC[(int)'h'] = 10; 486 for (int i=0; i<9; i++) 487 CLASSPATH_OPTOSFT[i] = 10; 488 CLASSPATH_OPTOSFT[9]=1; 489 } 490 491 private JarEntry getManEntry() { 492 if (manEntry == null) { 493 // First look up manifest entry using standard name 494 manEntry = getJarEntry(MANIFEST_NAME); 495 if (manEntry == null) { 496 // If not found, then iterate through all the "META-INF/" 497 // entries to find a match. 498 String[] names = getMetaInfEntryNames(); 499 if (names != null) { 500 for (String name : names) { 501 if (MANIFEST_NAME.equals(name.toUpperCase(Locale.ENGLISH))) { 502 manEntry = getJarEntry(name); 503 break; 504 } 505 } 506 } 507 } 508 } 509 return manEntry; 510 } 511 512 /** 513 * Returns {@code true} iff this JAR file has a manifest with the 514 * Class-Path attribute 515 */ 516 boolean hasClassPathAttribute() throws IOException { 517 checkForSpecialAttributes(); 518 return hasClassPathAttribute; 519 } 520 521 /** 522 * Returns true if the pattern {@code src} is found in {@code b}. 523 * The {@code lastOcc} and {@code optoSft} arrays are the precomputed 524 * bad character and good suffix shifts. 525 */ 526 private boolean match(char[] src, byte[] b, int[] lastOcc, int[] optoSft) { 527 int len = src.length; 528 int last = b.length - len; 529 int i = 0; 530 next: 531 while (i<=last) { 532 for (int j=(len-1); j>=0; j--) { 533 char c = (char) b[i+j]; 534 c = (((c-'A')|('Z'-c)) >= 0) ? (char)(c + 32) : c; 535 if (c != src[j]) { 536 i += Math.max(j + 1 - lastOcc[c&0x7F], optoSft[j]); 537 continue next; 538 } 539 } 540 return true; 541 } 542 return false; 543 } 544 545 /** 546 * On first invocation, check if the JAR file has the Class-Path 547 * attribute. A no-op on subsequent calls. 548 */ 549 private void checkForSpecialAttributes() throws IOException { 550 if (hasCheckedSpecialAttributes) return; 551 if (!isKnownNotToHaveSpecialAttributes()) { 552 JarEntry manEntry = getManEntry(); 553 if (manEntry != null) { 554 byte[] b = getBytes(manEntry); 555 if (match(CLASSPATH_CHARS, b, CLASSPATH_LASTOCC, CLASSPATH_OPTOSFT)) 556 hasClassPathAttribute = true; 557 } 558 } 559 hasCheckedSpecialAttributes = true; 560 } 561 562 private static String javaHome; 563 private static volatile String[] jarNames; 564 private boolean isKnownNotToHaveSpecialAttributes() { 565 // Optimize away even scanning of manifest for jar files we 566 // deliver which don't have a class-path attribute. If one of 567 // these jars is changed to include such an attribute this code 568 // must be changed. 569 if (javaHome == null) { 570 javaHome = AccessController.doPrivileged( 571 new GetPropertyAction("java.home")); 572 } 573 if (jarNames == null) { 574 String[] names = new String[11]; 575 String fileSep = File.separator; 576 int i = 0; 577 names[i++] = fileSep + "rt.jar"; 578 names[i++] = fileSep + "jsse.jar"; 579 names[i++] = fileSep + "jce.jar"; 580 names[i++] = fileSep + "charsets.jar"; 581 names[i++] = fileSep + "dnsns.jar"; 582 names[i++] = fileSep + "zipfs.jar"; 583 names[i++] = fileSep + "localedata.jar"; 584 names[i++] = fileSep = "cldrdata.jar"; 585 names[i++] = fileSep + "sunjce_provider.jar"; 586 names[i++] = fileSep + "sunpkcs11.jar"; 587 names[i++] = fileSep + "sunec.jar"; 588 jarNames = names; 589 } 590 591 String name = getName(); 592 if (name.startsWith(javaHome)) { 593 String[] names = jarNames; 594 for (String jarName : names) { 595 if (name.endsWith(jarName)) { 596 return true; 597 } 598 } 599 } 600 return false; 601 } 602 603 private synchronized void ensureInitialization() { 604 try { 605 maybeInstantiateVerifier(); 606 } catch (IOException e) { 607 throw new RuntimeException(e); 608 } 609 if (jv != null && !jvInitialized) { 610 initializeVerifier(); 611 jvInitialized = true; 612 } 613 } 614 615 JarEntry newEntry(ZipEntry ze) { 616 return new JarFileEntry(ze); 617 } 618 619 Enumeration<String> entryNames(CodeSource[] cs) { 620 ensureInitialization(); 621 if (jv != null) { 622 return jv.entryNames(this, cs); 623 } 624 625 /* 626 * JAR file has no signed content. Is there a non-signing 627 * code source? 628 */ 629 boolean includeUnsigned = false; 630 for (CodeSource c : cs) { 631 if (c.getCodeSigners() == null) { 632 includeUnsigned = true; 633 break; 634 } 635 } 636 if (includeUnsigned) { 637 return unsignedEntryNames(); 638 } else { 639 return new Enumeration<String>() { 640 641 public boolean hasMoreElements() { 642 return false; 643 } 644 645 public String nextElement() { 646 throw new NoSuchElementException(); 647 } 648 }; 649 } 650 } 651 652 /** 653 * Returns an enumeration of the zip file entries 654 * excluding internal JAR mechanism entries and including 655 * signed entries missing from the ZIP directory. 656 */ 657 Enumeration<JarEntry> entries2() { 658 ensureInitialization(); 659 if (jv != null) { 660 return jv.entries2(this, super.entries()); 661 } 662 663 // screen out entries which are never signed 664 final Enumeration<? extends ZipEntry> enum_ = super.entries(); 665 return new Enumeration<JarEntry>() { 666 667 ZipEntry entry; 668 669 public boolean hasMoreElements() { 670 if (entry != null) { 671 return true; 672 } 673 while (enum_.hasMoreElements()) { 674 ZipEntry ze = enum_.nextElement(); 675 if (JarVerifier.isSigningRelated(ze.getName())) { 676 continue; 677 } 678 entry = ze; 679 return true; 680 } 681 return false; 682 } 683 684 public JarFileEntry nextElement() { 685 if (hasMoreElements()) { 686 ZipEntry ze = entry; 687 entry = null; 688 return new JarFileEntry(ze); 689 } 690 throw new NoSuchElementException(); 691 } 692 }; 693 } 694 695 CodeSource[] getCodeSources(URL url) { 696 ensureInitialization(); 697 if (jv != null) { 698 return jv.getCodeSources(this, url); 699 } 700 701 /* 702 * JAR file has no signed content. Is there a non-signing 703 * code source? 704 */ 705 Enumeration<String> unsigned = unsignedEntryNames(); 706 if (unsigned.hasMoreElements()) { 707 return new CodeSource[]{JarVerifier.getUnsignedCS(url)}; 708 } else { 709 return null; 710 } 711 } 712 713 private Enumeration<String> unsignedEntryNames() { 714 final Enumeration<JarEntry> entries = entries(); 715 return new Enumeration<String>() { 716 717 String name; 718 719 /* 720 * Grab entries from ZIP directory but screen out 721 * metadata. 722 */ 723 public boolean hasMoreElements() { 724 if (name != null) { 725 return true; 726 } 727 while (entries.hasMoreElements()) { 728 String value; 729 ZipEntry e = entries.nextElement(); 730 value = e.getName(); 731 if (e.isDirectory() || JarVerifier.isSigningRelated(value)) { 732 continue; 733 } 734 name = value; 735 return true; 736 } 737 return false; 738 } 739 740 public String nextElement() { 741 if (hasMoreElements()) { 742 String value = name; 743 name = null; 744 return value; 745 } 746 throw new NoSuchElementException(); 747 } 748 }; 749 } 750 751 CodeSource getCodeSource(URL url, String name) { 752 ensureInitialization(); 753 if (jv != null) { 754 if (jv.eagerValidation) { 755 CodeSource cs = null; 756 JarEntry je = getJarEntry(name); 757 if (je != null) { 758 cs = jv.getCodeSource(url, this, je); 759 } else { 760 cs = jv.getCodeSource(url, name); 761 } 762 return cs; 763 } else { 764 return jv.getCodeSource(url, name); 765 } 766 } 767 768 return JarVerifier.getUnsignedCS(url); 769 } 770 771 void setEagerValidation(boolean eager) { 772 try { 773 maybeInstantiateVerifier(); 774 } catch (IOException e) { 775 throw new RuntimeException(e); 776 } 777 if (jv != null) { 778 jv.setEagerValidation(eager); 779 } 780 } 781 782 List<Object> getManifestDigests() { 783 ensureInitialization(); 784 if (jv != null) { 785 return jv.getManifestDigests(); 786 } 787 return new ArrayList<>(); 788 } 789 }