1 /* 2 * Copyright (c) 2014, 2015, 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.lang.module; 27 28 import java.io.DataInput; 29 import java.io.DataInputStream; 30 import java.io.EOFException; 31 import java.io.IOException; 32 import java.io.InputStream; 33 import java.io.UncheckedIOException; 34 import java.lang.module.ModuleDescriptor.Requires.Modifier; 35 import java.nio.ByteBuffer; 36 import java.nio.BufferUnderflowException; 37 import java.util.Collections; 38 import java.util.HashMap; 39 import java.util.HashSet; 40 import java.util.Map; 41 import java.util.Set; 42 import java.util.function.Supplier; 43 import java.util.stream.Collectors; 44 import java.util.stream.Stream; 45 46 import jdk.internal.module.Hasher.DependencyHashes; 47 48 import static jdk.internal.module.ClassFileConstants.*; 49 50 51 /** 52 * Read module information from a {@code module-info} class file. 53 * 54 * @implNote The rationale for the hand-coded reader is startup performance 55 * and fine control over the throwing of InvalidModuleDescriptorException. 56 */ 57 58 final class ModuleInfo { 59 60 // supplies the set of packages when ConcealedPackages not present 61 private final Supplier<Set<String>> packageFinder; 62 63 // indicates if the Hashes attribute should be parsed 64 private final boolean parseHashes; 65 66 // the builder, created when parsing 67 private ModuleDescriptor.Builder builder; 68 69 private ModuleInfo(Supplier<Set<String>> pf, boolean ph) { 70 packageFinder = pf; 71 parseHashes = ph; 72 } 73 74 private ModuleInfo(Supplier<Set<String>> pf) { 75 this(pf, true); 76 } 77 78 /** 79 * Reads a {@code module-info.class} from the given input stream. 80 * 81 * @throws InvalidModuleDescriptorException 82 * @throws IOException 83 */ 84 public static ModuleDescriptor read(InputStream in, 85 Supplier<Set<String>> pf) 86 throws IOException 87 { 88 try { 89 return new ModuleInfo(pf).doRead(new DataInputStream(in)); 90 } catch (IllegalArgumentException iae) { 91 // IllegalArgumentException means a malformed class 92 throw invalidModuleDescriptor(iae.getMessage()); 93 } catch (EOFException x) { 94 throw truncatedModuleDescriptor(); 95 } 96 } 97 98 /** 99 * Reads a {@code module-info.class} from the given byte buffer. 100 * 101 * @throws InvalidModuleDescriptorException 102 * @throws UncheckedIOException 103 */ 104 public static ModuleDescriptor read(ByteBuffer bb, 105 Supplier<Set<String>> pf) 106 { 107 try { 108 return new ModuleInfo(pf).doRead(new DataInputWrapper(bb)); 109 } catch (IllegalArgumentException iae) { 110 // IllegalArgumentException means a malformed class 111 throw invalidModuleDescriptor(iae.getMessage()); 112 } catch (EOFException x) { 113 throw truncatedModuleDescriptor(); 114 } catch (IOException ioe) { 115 throw new UncheckedIOException(ioe); 116 } 117 } 118 119 /** 120 * Reads a {@code module-info.class} from the given byte buffer 121 * but ignore the {@code Hashes} attribute. 122 * 123 * @throws InvalidModuleDescriptorException 124 * @throws UncheckedIOException 125 */ 126 static ModuleDescriptor readIgnoringHashes(ByteBuffer bb, 127 Supplier<Set<String>> pf) 128 { 129 try { 130 return new ModuleInfo(pf, false).doRead(new DataInputWrapper(bb)); 131 } catch (IllegalArgumentException iae) { 132 throw invalidModuleDescriptor(iae.getMessage()); 133 } catch (EOFException x) { 134 throw truncatedModuleDescriptor(); 135 } catch (IOException ioe) { 136 throw new UncheckedIOException(ioe); 137 } 138 } 139 140 /** 141 * Reads the input as a module-info class file. 142 * 143 * @throws IOException 144 * @throws InvalidModuleDescriptorException 145 * @throws IllegalArgumentException if thrown by the ModuleDescriptor.Builder 146 * because an identifier is not a legal Java identifier, duplicate 147 * exports, and many other reasons 148 */ 149 private ModuleDescriptor doRead(DataInput in) throws IOException { 150 151 int magic = in.readInt(); 152 if (magic != 0xCAFEBABE) 153 throw invalidModuleDescriptor("Bad magic number"); 154 155 int minor_version = in.readUnsignedShort(); 156 int major_version = in.readUnsignedShort(); 157 if (major_version < 53) { 158 // throw invalidModuleDescriptor"Must be >= 53.0"); 159 } 160 161 ConstantPool cpool = new ConstantPool(in); 162 163 int access_flags = in.readUnsignedShort(); 164 if (access_flags != ACC_MODULE) 165 throw invalidModuleDescriptor("access_flags should be ACC_MODULE"); 166 167 int this_class = in.readUnsignedShort(); 168 String mn = cpool.getClassName(this_class); 169 int suffix = mn.indexOf("/module-info"); 170 if (suffix < 1) 171 throw invalidModuleDescriptor("this_class not of form name/module-info"); 172 mn = mn.substring(0, suffix).replace('/', '.'); 173 builder = new ModuleDescriptor.Builder(mn); 174 175 int super_class = in.readUnsignedShort(); 176 if (super_class > 0) 177 throw invalidModuleDescriptor("bad #super_class"); 178 179 int interfaces_count = in.readUnsignedShort(); 180 if (interfaces_count > 0) 181 throw invalidModuleDescriptor("Bad #interfaces"); 182 183 int fields_count = in.readUnsignedShort(); 184 if (fields_count > 0) 185 throw invalidModuleDescriptor("Bad #fields"); 186 187 int methods_count = in.readUnsignedShort(); 188 if (methods_count > 0) 189 throw invalidModuleDescriptor("Bad #methods"); 190 191 int attributes_count = in.readUnsignedShort(); 192 193 // the names of the attributes found in the class file 194 Set<String> attributes = new HashSet<>(); 195 196 for (int i = 0; i < attributes_count ; i++) { 197 int name_index = in.readUnsignedShort(); 198 String attribute_name = cpool.getUtf8(name_index); 199 int length = in.readInt(); 200 201 boolean added = attributes.add(attribute_name); 202 if (!added && isAttributeAtMostOnce(attribute_name)) { 203 throw invalidModuleDescriptor("More than one " 204 + attribute_name + " attribute"); 205 } 206 207 switch (attribute_name) { 208 209 case MODULE : 210 readModuleAttribute(mn, in, cpool); 211 break; 212 213 case CONCEALED_PACKAGES : 214 readConcealedPackagesAttribute(in, cpool); 215 break; 216 217 case VERSION : 218 readVersionAttribute(in, cpool); 219 break; 220 221 case MAIN_CLASS : 222 readMainClassAttribute(in, cpool); 223 break; 224 225 case TARGET_PLATFORM : 226 readTargetPlatformAttribute(in, cpool); 227 break; 228 229 case HASHES : 230 if (parseHashes) { 231 readHashesAttribute(in, cpool); 232 } else { 233 in.skipBytes(length); 234 } 235 break; 236 237 default: 238 if (isAttributeDisallowed(attribute_name)) { 239 throw invalidModuleDescriptor(attribute_name 240 + " attribute not allowed"); 241 } else { 242 in.skipBytes(length); 243 } 244 245 } 246 } 247 248 // the Module attribute is required 249 if (!attributes.contains(MODULE)) { 250 throw invalidModuleDescriptor(MODULE + " attribute not found"); 251 } 252 253 // If the ConcealedPackages attribute is not present then the 254 // packageFinder is used to to find any non-exported packages. 255 if (!attributes.contains(CONCEALED_PACKAGES) && packageFinder != null) { 256 Set<String> pkgs; 257 try { 258 pkgs = new HashSet<>(packageFinder.get()); 259 } catch (UncheckedIOException x) { 260 throw x.getCause(); 261 } 262 pkgs.removeAll(builder.exportedPackages()); 263 builder.conceals(pkgs); 264 } 265 266 // Was the Synthetic attribute present? 267 if (attributes.contains(SYNTHETIC)) 268 builder.synthetic(true); 269 270 return builder.build(); 271 } 272 273 /** 274 * Reads the Module attribute. 275 */ 276 private void readModuleAttribute(String mn, DataInput in, ConstantPool cpool) 277 throws IOException 278 { 279 int requires_count = in.readUnsignedShort(); 280 if (requires_count == 0 && !mn.equals("java.base")) { 281 throw invalidModuleDescriptor("The requires table must have" 282 + " at least one entry"); 283 } 284 for (int i=0; i<requires_count; i++) { 285 int index = in.readUnsignedShort(); 286 int flags = in.readUnsignedShort(); 287 String dn = cpool.getUtf8(index); 288 Set<Modifier> mods; 289 if (flags == 0) { 290 mods = Collections.emptySet(); 291 } else { 292 mods = new HashSet<>(); 293 if ((flags & ACC_PUBLIC) != 0) 294 mods.add(Modifier.PUBLIC); 295 if ((flags & ACC_SYNTHETIC) != 0) 296 mods.add(Modifier.SYNTHETIC); 297 if ((flags & ACC_MANDATED) != 0) 298 mods.add(Modifier.MANDATED); 299 } 300 builder.requires(mods, dn); 301 } 302 303 int exports_count = in.readUnsignedShort(); 304 if (exports_count > 0) { 305 for (int i=0; i<exports_count; i++) { 306 int index = in.readUnsignedShort(); 307 String pkg = cpool.getUtf8(index).replace('/', '.'); 308 int exports_to_count = in.readUnsignedShort(); 309 if (exports_to_count > 0) { 310 Set<String> targets = new HashSet<>(exports_to_count); 311 for (int j=0; j<exports_to_count; j++) { 312 int exports_to_index = in.readUnsignedShort(); 313 targets.add(cpool.getUtf8(exports_to_index)); 314 } 315 builder.exports(pkg, targets); 316 } else { 317 builder.exports(pkg); 318 } 319 } 320 } 321 322 int uses_count = in.readUnsignedShort(); 323 if (uses_count > 0) { 324 for (int i=0; i<uses_count; i++) { 325 int index = in.readUnsignedShort(); 326 String sn = cpool.getClassName(index).replace('/', '.'); 327 builder.uses(sn); 328 } 329 } 330 331 int provides_count = in.readUnsignedShort(); 332 if (provides_count > 0) { 333 Map<String, Set<String>> pm = new HashMap<>(); 334 for (int i=0; i<provides_count; i++) { 335 int index = in.readUnsignedShort(); 336 int with_index = in.readUnsignedShort(); 337 String sn = cpool.getClassName(index).replace('/', '.'); 338 String cn = cpool.getClassName(with_index).replace('/', '.'); 339 // computeIfAbsent 340 Set<String> providers = pm.get(sn); 341 if (providers == null) { 342 providers = new HashSet<>(); 343 pm.put(sn, providers); 344 } 345 providers.add(cn); 346 } 347 for (Map.Entry<String, Set<String>> e : pm.entrySet()) { 348 builder.provides(e.getKey(), e.getValue()); 349 } 350 } 351 } 352 353 /** 354 * Reads the ConcealedPackages attribute 355 */ 356 private void readConcealedPackagesAttribute(DataInput in, ConstantPool cpool) 357 throws IOException 358 { 359 int package_count = in.readUnsignedShort(); 360 for (int i=0; i<package_count; i++) { 361 int index = in.readUnsignedShort(); 362 String pn = cpool.getUtf8(index).replace('/', '.'); 363 builder.conceals(pn); 364 } 365 } 366 367 /** 368 * Reads the Version attribute 369 */ 370 private void readVersionAttribute(DataInput in, ConstantPool cpool) 371 throws IOException 372 { 373 int index = in.readUnsignedShort(); 374 builder.version(cpool.getUtf8(index)); 375 } 376 377 /** 378 * Reads the MainClass attribute 379 */ 380 private void readMainClassAttribute(DataInput in, ConstantPool cpool) 381 throws IOException 382 { 383 int index = in.readUnsignedShort(); 384 builder.mainClass(cpool.getClassName(index).replace('/', '.')); 385 } 386 387 /** 388 * Reads the TargetPlatform attribute 389 */ 390 private void readTargetPlatformAttribute(DataInput in, ConstantPool cpool) 391 throws IOException 392 { 393 int name_index = in.readUnsignedShort(); 394 if (name_index != 0) 395 builder.osName(cpool.getUtf8(name_index)); 396 397 int arch_index = in.readUnsignedShort(); 398 if (arch_index != 0) 399 builder.osArch(cpool.getUtf8(arch_index)); 400 401 int version_index = in.readUnsignedShort(); 402 if (version_index != 0) 403 builder.osVersion(cpool.getUtf8(version_index)); 404 } 405 406 407 /** 408 * Reads the Hashes attribute 409 * 410 * @apiNote For now the hash is stored in base64 as a UTF-8 string, this 411 * should be changed to be an array of u1. 412 */ 413 private void readHashesAttribute(DataInput in, ConstantPool cpool) 414 throws IOException 415 { 416 int index = in.readUnsignedShort(); 417 String algorithm = cpool.getUtf8(index); 418 419 int hash_count = in.readUnsignedShort(); 420 421 Map<String, String> map = new HashMap<>(hash_count); 422 for (int i=0; i<hash_count; i++) { 423 index = in.readUnsignedShort(); 424 String dn = cpool.getUtf8(index); 425 index = in.readUnsignedShort(); 426 String hash = cpool.getUtf8(index); 427 map.put(dn, hash); 428 } 429 430 builder.hashes(new DependencyHashes(algorithm, map)); 431 } 432 433 434 /** 435 * Returns true if the given attribute can be present at most once 436 * in the class file. Returns false otherwise. 437 */ 438 private static boolean isAttributeAtMostOnce(String name) { 439 440 if (name.equals(MODULE) || 441 name.equals(SOURCE_FILE) || 442 name.equals(SDE) || 443 name.equals(CONCEALED_PACKAGES) || 444 name.equals(VERSION) || 445 name.equals(MAIN_CLASS) || 446 name.equals(TARGET_PLATFORM) || 447 name.equals(HASHES)) 448 return true; 449 450 return false; 451 } 452 453 /** 454 * Return true if the given attribute name is the name of a pre-defined 455 * attribute that is not allowed in the class file. 456 * 457 * Except for Module, InnerClasses, Synthetic, SourceFile, SourceDebugExtension, 458 * and Deprecated, none of the pre-defined attributes in JVMS 4.7 may appear. 459 */ 460 private static boolean isAttributeDisallowed(String name) { 461 Set<String> notAllowed = predefinedNotAllowed; 462 if (notAllowed == null) { 463 notAllowed = Stream.of( 464 "ConstantValue", 465 "Code", 466 "StackMapTable", 467 "Exceptions", 468 "EnclosingMethod", 469 "Signature", 470 "LineNumberTable", 471 "LocalVariableTable", 472 "LocalVariableTypeTable", 473 "RuntimeVisibleAnnotations", 474 "RuntimeInvisibleAnnotations", 475 "RuntimeVisibleParameterAnnotations", 476 "RuntimeInvisibleParameterAnnotations", 477 "RuntimeVisibleTypeAnnotations", 478 "RuntimeInvisibleTypeAnnotations", 479 "AnnotationDefault", 480 "BootstrapMethods", 481 "MethodParameters") 482 .collect(Collectors.toSet()); 483 predefinedNotAllowed = notAllowed; 484 } 485 486 if (notAllowed.contains(name)) { 487 return true; 488 } else { 489 return false; 490 } 491 } 492 493 // lazily created set the pre-defined attributes that are not allowed 494 private static volatile Set<String> predefinedNotAllowed; 495 496 497 498 /** 499 * The constant pool in a class file. 500 */ 501 private static class ConstantPool { 502 static final int CONSTANT_Utf8 = 1; 503 static final int CONSTANT_Integer = 3; 504 static final int CONSTANT_Float = 4; 505 static final int CONSTANT_Long = 5; 506 static final int CONSTANT_Double = 6; 507 static final int CONSTANT_Class = 7; 508 static final int CONSTANT_String = 8; 509 static final int CONSTANT_Fieldref = 9; 510 static final int CONSTANT_Methodref = 10; 511 static final int CONSTANT_InterfaceMethodref = 11; 512 static final int CONSTANT_NameAndType = 12; 513 static final int CONSTANT_MethodHandle = 15; 514 static final int CONSTANT_MethodType = 16; 515 static final int CONSTANT_InvokeDynamic = 18; 516 517 private static class Entry { 518 protected Entry(int tag) { 519 this.tag = tag; 520 } 521 final int tag; 522 } 523 524 private static class IndexEntry extends Entry { 525 IndexEntry(int tag, int index) { 526 super(tag); 527 this.index = index; 528 } 529 final int index; 530 } 531 532 private static class Index2Entry extends Entry { 533 Index2Entry(int tag, int index1, int index2) { 534 super(tag); 535 this.index1 = index1; 536 this.index2 = index2; 537 } 538 final int index1, index2; 539 } 540 541 private static class ValueEntry extends Entry { 542 ValueEntry(int tag, Object value) { 543 super(tag); 544 this.value = value; 545 } 546 final Object value; 547 } 548 549 final Entry[] pool; 550 551 ConstantPool(DataInput in) throws IOException { 552 int count = in.readUnsignedShort(); 553 pool = new Entry[count]; 554 555 for (int i = 1; i < count; i++) { 556 int tag = in.readUnsignedByte(); 557 switch (tag) { 558 559 case CONSTANT_Utf8: 560 String svalue = in.readUTF(); 561 pool[i] = new ValueEntry(tag, svalue); 562 break; 563 564 case CONSTANT_Class: 565 case CONSTANT_String: 566 int index = in.readUnsignedShort(); 567 pool[i] = new IndexEntry(tag, index); 568 break; 569 570 case CONSTANT_Double: 571 double dvalue = in.readDouble(); 572 pool[i] = new ValueEntry(tag, dvalue); 573 i++; 574 break; 575 576 case CONSTANT_Fieldref: 577 case CONSTANT_InterfaceMethodref: 578 case CONSTANT_Methodref: 579 case CONSTANT_InvokeDynamic: 580 case CONSTANT_NameAndType: 581 int index1 = in.readUnsignedShort(); 582 int index2 = in.readUnsignedShort(); 583 pool[i] = new Index2Entry(tag, index1, index2); 584 break; 585 586 case CONSTANT_MethodHandle: 587 int refKind = in.readUnsignedByte(); 588 index = in.readUnsignedShort(); 589 pool[i] = new Index2Entry(tag, refKind, index); 590 break; 591 592 case CONSTANT_MethodType: 593 index = in.readUnsignedShort(); 594 pool[i] = new IndexEntry(tag, index); 595 break; 596 597 case CONSTANT_Float: 598 float fvalue = in.readFloat(); 599 pool[i] = new ValueEntry(tag, fvalue); 600 break; 601 602 case CONSTANT_Integer: 603 int ivalue = in.readInt(); 604 pool[i] = new ValueEntry(tag, ivalue); 605 break; 606 607 case CONSTANT_Long: 608 long lvalue = in.readLong(); 609 pool[i] = new ValueEntry(tag, lvalue); 610 i++; 611 break; 612 613 default: 614 throw invalidModuleDescriptor("Bad constant pool entry: " 615 + i); 616 } 617 } 618 } 619 620 String getClassName(int index) { 621 checkIndex(index); 622 Entry e = pool[index]; 623 if (e.tag != CONSTANT_Class) { 624 throw invalidModuleDescriptor("CONSTANT_Class expected at entry: " 625 + index); 626 } 627 return getUtf8(((IndexEntry) e).index); 628 } 629 630 String getUtf8(int index) { 631 checkIndex(index); 632 Entry e = pool[index]; 633 if (e.tag != CONSTANT_Utf8) { 634 throw invalidModuleDescriptor("CONSTANT_Utf8 expected at entry: " 635 + index); 636 } 637 return (String) (((ValueEntry) e).value); 638 } 639 640 void checkIndex(int index) { 641 if (index < 1 || index >= pool.length) 642 throw invalidModuleDescriptor("Index into constant pool out of range"); 643 } 644 } 645 646 /** 647 * A DataInput implementation that reads from a ByteBuffer. 648 */ 649 private static class DataInputWrapper implements DataInput { 650 private final ByteBuffer bb; 651 652 DataInputWrapper(ByteBuffer bb) { 653 this.bb = bb; 654 } 655 656 @Override 657 public void readFully(byte b[]) throws IOException { 658 readFully(b, 0, b.length); 659 } 660 661 @Override 662 public void readFully(byte b[], int off, int len) throws IOException { 663 try { 664 bb.get(b, off, len); 665 } catch (BufferUnderflowException e) { 666 throw new EOFException(e.getMessage()); 667 } 668 } 669 670 @Override 671 public int skipBytes(int n) { 672 int skip = Math.min(n, bb.remaining()); 673 bb.position(bb.position() + skip); 674 return skip; 675 } 676 677 @Override 678 public boolean readBoolean() throws IOException { 679 try { 680 int ch = bb.get(); 681 return (ch != 0); 682 } catch (BufferUnderflowException e) { 683 throw new EOFException(e.getMessage()); 684 } 685 } 686 687 @Override 688 public byte readByte() throws IOException { 689 try { 690 return bb.get(); 691 } catch (BufferUnderflowException e) { 692 throw new EOFException(e.getMessage()); 693 } 694 } 695 696 @Override 697 public int readUnsignedByte() throws IOException { 698 try { 699 return ((int) bb.get()) & 0xff; 700 } catch (BufferUnderflowException e) { 701 throw new EOFException(e.getMessage()); 702 } 703 } 704 705 @Override 706 public short readShort() throws IOException { 707 try { 708 return bb.getShort(); 709 } catch (BufferUnderflowException e) { 710 throw new EOFException(e.getMessage()); 711 } 712 } 713 714 @Override 715 public int readUnsignedShort() throws IOException { 716 try { 717 return ((int) bb.getShort()) & 0xffff; 718 } catch (BufferUnderflowException e) { 719 throw new EOFException(e.getMessage()); 720 } 721 } 722 723 @Override 724 public char readChar() throws IOException { 725 try { 726 return bb.getChar(); 727 } catch (BufferUnderflowException e) { 728 throw new EOFException(e.getMessage()); 729 } 730 } 731 732 @Override 733 public int readInt() throws IOException { 734 try { 735 return bb.getInt(); 736 } catch (BufferUnderflowException e) { 737 throw new EOFException(e.getMessage()); 738 } 739 } 740 741 @Override 742 public long readLong() throws IOException { 743 try { 744 return bb.getLong(); 745 } catch (BufferUnderflowException e) { 746 throw new EOFException(e.getMessage()); 747 } 748 } 749 750 @Override 751 public float readFloat() throws IOException { 752 try { 753 return bb.getFloat(); 754 } catch (BufferUnderflowException e) { 755 throw new EOFException(e.getMessage()); 756 } 757 } 758 759 @Override 760 public double readDouble() throws IOException { 761 try { 762 return bb.getDouble(); 763 } catch (BufferUnderflowException e) { 764 throw new EOFException(e.getMessage()); 765 } 766 } 767 768 @Override 769 public String readLine() { 770 throw new RuntimeException("readLine() not implemented"); 771 } 772 773 @Override 774 public String readUTF() throws IOException { 775 // ### Need to measure the performance and feasibility of using 776 // the UTF-8 decoder instead. 777 return DataInputStream.readUTF(this); 778 } 779 } 780 781 /** 782 * Returns an InvalidModuleDescriptorException with the given detail 783 * message 784 */ 785 private static InvalidModuleDescriptorException 786 invalidModuleDescriptor(String msg) { 787 return new InvalidModuleDescriptorException(msg); 788 } 789 790 /** 791 * Returns an InvalidModuleDescriptorException with a detail message to 792 * indicate that the class file is truncated. 793 */ 794 private static InvalidModuleDescriptorException truncatedModuleDescriptor() { 795 return invalidModuleDescriptor("Truncated module-info.class"); 796 } 797 798 }