1 /*
   2  * Copyright (c) 2001, 2016, 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 com.sun.java.util.jar.pack;
  27 
  28 import java.util.jar.Pack200;
  29 import com.sun.java.util.jar.pack.Attribute.Layout;
  30 import com.sun.java.util.jar.pack.ConstantPool.ClassEntry;
  31 import com.sun.java.util.jar.pack.ConstantPool.DescriptorEntry;
  32 import com.sun.java.util.jar.pack.ConstantPool.BootstrapMethodEntry;
  33 import com.sun.java.util.jar.pack.ConstantPool.Index;
  34 import com.sun.java.util.jar.pack.ConstantPool.LiteralEntry;
  35 import com.sun.java.util.jar.pack.ConstantPool.Utf8Entry;
  36 import com.sun.java.util.jar.pack.ConstantPool.Entry;
  37 import java.io.ByteArrayInputStream;
  38 import java.io.ByteArrayOutputStream;
  39 import java.io.IOException;
  40 import java.io.InputStream;
  41 import java.io.OutputStream;
  42 import java.io.SequenceInputStream;
  43 import java.lang.reflect.Modifier;
  44 import java.util.ArrayList;
  45 import java.util.Arrays;
  46 import java.util.Collection;
  47 import java.util.Collections;
  48 import java.util.Comparator;
  49 import java.util.HashMap;
  50 import java.util.HashSet;
  51 import java.util.Iterator;
  52 import java.util.List;
  53 import java.util.ListIterator;
  54 import java.util.Map;
  55 import java.util.Set;
  56 import java.util.jar.JarFile;
  57 import static com.sun.java.util.jar.pack.Constants.*;
  58 
  59 /**
  60  * Define the main data structure transmitted by pack/unpack.
  61  * @author John Rose
  62  */
  63 class Package {
  64     int verbose;
  65     {
  66         PropMap pmap = Utils.currentPropMap();
  67         if (pmap != null)
  68             verbose = pmap.getInteger(Utils.DEBUG_VERBOSE);
  69     }
  70 
  71     final int magic = JAVA_PACKAGE_MAGIC;
  72 
  73     int default_modtime = NO_MODTIME;
  74     int default_options = 0;  // FO_DEFLATE_HINT
  75 
  76     Version defaultClassVersion = null;
  77 
  78     // These fields can be adjusted by driver properties.
  79     final Version minClassVersion;
  80     final Version maxClassVersion;
  81     // null, indicates that consensus rules during package write
  82     final Version packageVersion;
  83 
  84     Version observedHighestClassVersion = null;
  85 
  86 
  87     // What constants are used in this unit?
  88     ConstantPool.IndexGroup cp = new ConstantPool.IndexGroup();
  89 
  90     /*
  91      * typically used by the PackageReader to set the defaults, in which
  92      * case we take the defaults.
  93      */
  94     public Package() {
  95         minClassVersion = JAVA_MIN_CLASS_VERSION;
  96         maxClassVersion = JAVA_MAX_CLASS_VERSION;
  97         packageVersion = null;
  98     }
  99 
 100 
 101     /*
 102      * Typically used by the PackerImpl during before packing, the defaults are
 103      * overridden by the users preferences.
 104      */
 105     public Package(Version minClassVersion, Version maxClassVersion, Version packageVersion) {
 106         // Fill in permitted range of major/minor version numbers.
 107         this.minClassVersion = minClassVersion == null
 108                 ? JAVA_MIN_CLASS_VERSION
 109                 : minClassVersion;
 110         this.maxClassVersion = maxClassVersion == null
 111                 ? JAVA_MAX_CLASS_VERSION
 112                 : maxClassVersion;
 113         this.packageVersion  = packageVersion;
 114     }
 115 
 116 
 117     public void reset() {
 118         cp = new ConstantPool.IndexGroup();
 119         classes.clear();
 120         files.clear();
 121         BandStructure.nextSeqForDebug = 0;
 122         observedHighestClassVersion = null;
 123     }
 124 
 125     // Special empty versions of Code and InnerClasses, used for markers.
 126     public static final Attribute.Layout attrCodeEmpty;
 127     public static final Attribute.Layout attrBootstrapMethodsEmpty;
 128     public static final Attribute.Layout attrInnerClassesEmpty;
 129     public static final Attribute.Layout attrSourceFileSpecial;
 130     public static final Map<Attribute.Layout, Attribute> attrDefs;
 131     static {
 132         Map<Layout, Attribute> ad = new HashMap<>(3);
 133         attrCodeEmpty = Attribute.define(ad, ATTR_CONTEXT_METHOD,
 134                                          "Code", "").layout();
 135         attrBootstrapMethodsEmpty = Attribute.define(ad, ATTR_CONTEXT_CLASS,
 136                                                      "BootstrapMethods", "").layout();
 137         attrInnerClassesEmpty = Attribute.define(ad, ATTR_CONTEXT_CLASS,
 138                                                  "InnerClasses", "").layout();
 139         attrSourceFileSpecial = Attribute.define(ad, ATTR_CONTEXT_CLASS,
 140                                                  "SourceFile", "RUNH").layout();
 141         attrDefs = Collections.unmodifiableMap(ad);
 142     }
 143 
 144     Version getDefaultClassVersion() {
 145         return defaultClassVersion;
 146     }
 147 
 148     /** Return the highest version number of all classes,
 149      *  or 0 if there are no classes.
 150      */
 151     private void setHighestClassVersion() {
 152         if (observedHighestClassVersion != null)
 153             return;
 154         Version res = JAVA_MIN_CLASS_VERSION;  // initial low value
 155         for (Class cls : classes) {
 156             Version ver = cls.getVersion();
 157             if (res.lessThan(ver))  res = ver;
 158         }
 159         observedHighestClassVersion = res;
 160     }
 161 
 162     Version getHighestClassVersion() {
 163         setHighestClassVersion();
 164         return observedHighestClassVersion;
 165     }
 166 
 167     // What Java classes are in this unit?
 168 
 169     ArrayList<Package.Class> classes = new ArrayList<>();
 170 
 171     public List<Package.Class> getClasses() {
 172         return classes;
 173     }
 174 
 175     public final
 176     class Class extends Attribute.Holder implements Comparable<Class> {
 177         public Package getPackage() { return Package.this; }
 178 
 179         // Optional file characteristics and data source (a "class stub")
 180         File file;
 181 
 182         // File header
 183         int magic;
 184         Version version;
 185 
 186         // Local constant pool (one-way mapping of index => package cp).
 187         Entry[] cpMap;
 188 
 189         // Class header
 190         //int flags;  // in Attribute.Holder.this.flags
 191         ClassEntry thisClass;
 192         ClassEntry superClass;
 193         ClassEntry[] interfaces;
 194 
 195         // Class parts
 196         ArrayList<Field> fields;
 197         ArrayList<Method> methods;
 198         //ArrayList attributes;  // in Attribute.Holder.this.attributes
 199         // Note that InnerClasses may be collected at the package level.
 200         ArrayList<InnerClass> innerClasses;
 201         ArrayList<BootstrapMethodEntry> bootstrapMethods;
 202 
 203         Class(int flags, ClassEntry thisClass, ClassEntry superClass, ClassEntry[] interfaces) {
 204             this.magic      = JAVA_MAGIC;
 205             this.version    = defaultClassVersion;
 206             this.flags      = flags;
 207             this.thisClass  = thisClass;
 208             this.superClass = superClass;
 209             this.interfaces = interfaces;
 210 
 211             boolean added = classes.add(this);
 212             assert(added);
 213         }
 214 
 215         Class(String classFile) {
 216             // A blank class; must be read with a ClassReader, etc.
 217             initFile(newStub(classFile));
 218         }
 219 
 220         List<Field> getFields() { return fields == null ? noFields : fields; }
 221         List<Method> getMethods() { return methods == null ? noMethods : methods; }
 222 
 223         public String getName() {
 224             return thisClass.stringValue();
 225         }
 226 
 227         Version getVersion() {
 228             return this.version;
 229         }
 230 
 231         // Note:  equals and hashCode are identity-based.
 232         public int compareTo(Class that) {
 233             String n0 = this.getName();
 234             String n1 = that.getName();
 235             return n0.compareTo(n1);
 236         }
 237 
 238         String getObviousSourceFile() {
 239             return Package.getObviousSourceFile(getName());
 240         }
 241 
 242         private void transformSourceFile(boolean minimize) {
 243             // Replace "obvious" SourceFile by null.
 244             Attribute olda = getAttribute(attrSourceFileSpecial);
 245             if (olda == null)
 246                 return;  // no SourceFile attr.
 247             String obvious = getObviousSourceFile();
 248             List<Entry> ref = new ArrayList<>(1);
 249             olda.visitRefs(this, VRM_PACKAGE, ref);
 250             Utf8Entry sfName = (Utf8Entry) ref.get(0);
 251             Attribute a = olda;
 252             if (sfName == null) {
 253                 if (minimize) {
 254                     // A pair of zero bytes.  Cannot use predef. layout.
 255                     a = Attribute.find(ATTR_CONTEXT_CLASS, "SourceFile", "H");
 256                     a = a.addContent(new byte[2]);
 257                 } else {
 258                     // Expand null attribute to the obvious string.
 259                     byte[] bytes = new byte[2];
 260                     sfName = getRefString(obvious);
 261                     Object f = null;
 262                     f = Fixups.addRefWithBytes(f, bytes, sfName);
 263                     a = attrSourceFileSpecial.addContent(bytes, f);
 264                 }
 265             } else if (obvious.equals(sfName.stringValue())) {
 266                 if (minimize) {
 267                     // Replace by an all-zero attribute.
 268                     a = attrSourceFileSpecial.addContent(new byte[2]);
 269                 } else {
 270                     assert(false);
 271                 }
 272             }
 273             if (a != olda) {
 274                 if (verbose > 2)
 275                     Utils.log.fine("recoding obvious SourceFile="+obvious);
 276                 List<Attribute> newAttrs = new ArrayList<>(getAttributes());
 277                 int where = newAttrs.indexOf(olda);
 278                 newAttrs.set(where, a);
 279                 setAttributes(newAttrs);
 280             }
 281         }
 282 
 283         void minimizeSourceFile() {
 284             transformSourceFile(true);
 285         }
 286         void expandSourceFile() {
 287             transformSourceFile(false);
 288         }
 289 
 290         protected Entry[] getCPMap() {
 291             return cpMap;
 292         }
 293 
 294         protected void setCPMap(Entry[] cpMap) {
 295             this.cpMap = cpMap;
 296         }
 297 
 298         boolean hasBootstrapMethods() {
 299             return bootstrapMethods != null && !bootstrapMethods.isEmpty();
 300         }
 301 
 302         List<BootstrapMethodEntry> getBootstrapMethods() {
 303             return bootstrapMethods;
 304         }
 305 
 306         BootstrapMethodEntry[] getBootstrapMethodMap() {
 307             return (hasBootstrapMethods())
 308                     ? bootstrapMethods.toArray(new BootstrapMethodEntry[bootstrapMethods.size()])
 309                     : null;
 310         }
 311 
 312         void setBootstrapMethods(Collection<BootstrapMethodEntry> bsms) {
 313             assert(bootstrapMethods == null);  // do not do this twice
 314             bootstrapMethods = new ArrayList<>(bsms);
 315         }
 316 
 317         boolean hasInnerClasses() {
 318             return innerClasses != null;
 319         }
 320         List<InnerClass> getInnerClasses() {
 321             return innerClasses;
 322         }
 323 
 324         public void setInnerClasses(Collection<InnerClass> ics) {
 325             innerClasses = (ics == null) ? null : new ArrayList<>(ics);
 326             // Edit the attribute list, if necessary.
 327             Attribute a = getAttribute(attrInnerClassesEmpty);
 328             if (innerClasses != null && a == null)
 329                 addAttribute(attrInnerClassesEmpty.canonicalInstance());
 330             else if (innerClasses == null && a != null)
 331                 removeAttribute(a);
 332         }
 333 
 334         /** Given a global map of ICs (keyed by thisClass),
 335          *  compute the subset of its Map.values which are
 336          *  required to be present in the local InnerClasses
 337          *  attribute.  Perform this calculation without
 338          *  reference to any actual InnerClasses attribute.
 339          *  <p>
 340          *  The order of the resulting list is consistent
 341          *  with that of Package.this.allInnerClasses.
 342          */
 343         public List<InnerClass> computeGloballyImpliedICs() {
 344             Set<Entry> cpRefs = new HashSet<>();
 345             {   // This block temporarily displaces this.innerClasses.
 346                 ArrayList<InnerClass> innerClassesSaved = innerClasses;
 347                 innerClasses = null;  // ignore for the moment
 348                 visitRefs(VRM_CLASSIC, cpRefs);
 349                 innerClasses = innerClassesSaved;
 350             }
 351             ConstantPool.completeReferencesIn(cpRefs, true);
 352 
 353             Set<Entry> icRefs = new HashSet<>();
 354             for (Entry e : cpRefs) {
 355                 // Restrict cpRefs to InnerClasses entries only.
 356                 if (!(e instanceof ClassEntry))  continue;
 357                 // For every IC reference, add its outers also.
 358                 while (e != null) {
 359                     InnerClass ic = getGlobalInnerClass(e);
 360                     if (ic == null)  break;
 361                     if (!icRefs.add(e))  break;
 362                     e = ic.outerClass;
 363                     // If we add A$B$C to the mix, we must also add A$B.
 364                 }
 365             }
 366             // This loop is structured this way so as to accumulate
 367             // entries into impliedICs in an order which reflects
 368             // the order of allInnerClasses.
 369             ArrayList<InnerClass> impliedICs = new ArrayList<>();
 370             for (InnerClass ic : allInnerClasses) {
 371                 // This one is locally relevant if it describes
 372                 // a member of the current class, or if the current
 373                 // class uses it somehow.  In the particular case
 374                 // where thisClass is an inner class, it will already
 375                 // be a member of icRefs.
 376                 if (icRefs.contains(ic.thisClass)
 377                     || ic.outerClass == this.thisClass) {
 378                     // Add every relevant class to the IC attribute:
 379                     if (verbose > 1)
 380                         Utils.log.fine("Relevant IC: "+ic);
 381                     impliedICs.add(ic);
 382                 }
 383             }
 384             return impliedICs;
 385         }
 386 
 387         // Helper for both minimizing and expanding.
 388         // Computes a symmetric difference.
 389         private List<InnerClass> computeICdiff() {
 390             List<InnerClass> impliedICs = computeGloballyImpliedICs();
 391             List<InnerClass> actualICs  = getInnerClasses();
 392             if (actualICs == null)
 393                 actualICs = Collections.emptyList();
 394 
 395             // Symmetric difference is calculated from I, A like this:
 396             //  diff = (I+A) - (I*A)
 397             // Note that the center C is unordered, but the result
 398             // preserves the original ordering of I and A.
 399             //
 400             // Class file rules require that outers precede inners.
 401             // So, add I before A, in case A$B$Z is local, but A$B
 402             // is implicit.  The reverse is never the case.
 403             if (actualICs.isEmpty()) {
 404                 return impliedICs;
 405                 // Diff is I since A is empty.
 406             }
 407             if (impliedICs.isEmpty()) {
 408                 return actualICs;
 409                 // Diff is A since I is empty.
 410             }
 411             // (I*A) is non-trivial
 412             Set<InnerClass> center = new HashSet<>(actualICs);
 413             center.retainAll(new HashSet<>(impliedICs));
 414             impliedICs.addAll(actualICs);
 415             impliedICs.removeAll(center);
 416             // Diff is now I^A = (I+A)-(I*A).
 417             return impliedICs;
 418         }
 419 
 420         /** When packing, anticipate the effect of expandLocalICs.
 421          *  Replace the local ICs by their symmetric difference
 422          *  with the globally implied ICs for this class; if this
 423          *  difference is empty, remove the local ICs altogether.
 424          *  <p>
 425          *  An empty local IC attribute is reserved to signal
 426          *  the unpacker to delete the attribute altogether,
 427          *  so a missing local IC attribute signals the unpacker
 428          *  to use the globally implied ICs changed.
 429          */
 430         void minimizeLocalICs() {
 431             List<InnerClass> diff = computeICdiff();
 432             List<InnerClass> actualICs = innerClasses;
 433             List<InnerClass> localICs;  // will be the diff, modulo edge cases
 434             if (diff.isEmpty()) {
 435                 // No diff, so transmit no attribute.
 436                 localICs = null;
 437                 if (actualICs != null && actualICs.isEmpty()) {
 438                     // Odd case:  No implied ICs, and a zero length attr.
 439                     // Do not support it directly.
 440                     if (verbose > 0)
 441                         Utils.log.info("Warning: Dropping empty InnerClasses attribute from "+this);
 442                 }
 443             } else if (actualICs == null) {
 444                 // No local IC attribute, even though some are implied.
 445                 // Signal with trivial attribute.
 446                 localICs = Collections.emptyList();
 447             } else {
 448                 // Transmit a non-empty diff, which will create
 449                 // a local ICs attribute.
 450                 localICs = diff;
 451             }
 452             // Reduce the set to the symmetric difference.
 453             setInnerClasses(localICs);
 454             if (verbose > 1 && localICs != null)
 455                 Utils.log.fine("keeping local ICs in "+this+": "+localICs);
 456         }
 457 
 458         /** When unpacking, undo the effect of minimizeLocalICs.
 459          *  Must return negative if any IC tuples may have been deleted.
 460          *  Otherwise, return positive if any IC tuples were added.
 461          */
 462         int expandLocalICs() {
 463             List<InnerClass> localICs = innerClasses;
 464             List<InnerClass> actualICs;
 465             int changed;
 466             if (localICs == null) {
 467                 // Diff was empty.  (Common case.)
 468                 List<InnerClass> impliedICs = computeGloballyImpliedICs();
 469                 if (impliedICs.isEmpty()) {
 470                     actualICs = null;
 471                     changed = 0;
 472                 } else {
 473                     actualICs = impliedICs;
 474                     changed = 1;  // added more tuples
 475                 }
 476             } else if (localICs.isEmpty()) {
 477                 // It was a non-empty diff, but the local ICs were absent.
 478                 actualICs = null;
 479                 // [] => null, no tuple change, but attribute deletion.
 480                 changed = -1;
 481             } else {
 482                 // Non-trivial diff was transmitted.
 483                 actualICs = computeICdiff();
 484                 // If we only added more ICs, return +1.
 485                 changed = actualICs.containsAll(localICs)? +1: -1;
 486             }
 487             setInnerClasses(actualICs);
 488             return changed;
 489         }
 490 
 491         public abstract
 492         class Member extends Attribute.Holder implements Comparable<Member> {
 493             DescriptorEntry descriptor;
 494 
 495             protected Member(int flags, DescriptorEntry descriptor) {
 496                 this.flags = flags;
 497                 this.descriptor = descriptor;
 498             }
 499 
 500             public Class thisClass() { return Class.this; }
 501 
 502             public DescriptorEntry getDescriptor() {
 503                 return descriptor;
 504             }
 505             public String getName() {
 506                 return descriptor.nameRef.stringValue();
 507             }
 508             public String getType() {
 509                 return descriptor.typeRef.stringValue();
 510             }
 511 
 512             protected Entry[] getCPMap() {
 513                 return cpMap;
 514             }
 515             protected void visitRefs(int mode, Collection<Entry> refs) {
 516                 if (verbose > 2)  Utils.log.fine("visitRefs "+this);
 517                 // Careful:  The descriptor is used by the package,
 518                 // but the classfile breaks it into component refs.
 519                 if (mode == VRM_CLASSIC) {
 520                     refs.add(descriptor.nameRef);
 521                     refs.add(descriptor.typeRef);
 522                 } else {
 523                     refs.add(descriptor);
 524                 }
 525                 // Handle attribute list:
 526                 super.visitRefs(mode, refs);
 527             }
 528 
 529             public String toString() {
 530                 return Class.this + "." + descriptor.prettyString();
 531             }
 532         }
 533 
 534         public
 535         class Field extends Member {
 536             // Order is significant for fields:  It is visible to reflection.
 537             int order;
 538 
 539             public Field(int flags, DescriptorEntry descriptor) {
 540                 super(flags, descriptor);
 541                 assert(!descriptor.isMethod());
 542                 if (fields == null)
 543                     fields = new ArrayList<>();
 544                 boolean added = fields.add(this);
 545                 assert(added);
 546                 order = fields.size();
 547             }
 548 
 549             public byte getLiteralTag() {
 550                 return descriptor.getLiteralTag();
 551             }
 552 
 553             public int compareTo(Member o) {
 554                 Field that = (Field)o;
 555                 return this.order - that.order;
 556             }
 557         }
 558 
 559         public
 560         class Method extends Member {
 561             // Code attribute is specially hardwired.
 562             Code code;
 563 
 564             public Method(int flags, DescriptorEntry descriptor) {
 565                 super(flags, descriptor);
 566                 assert(descriptor.isMethod());
 567                 if (methods == null)
 568                     methods = new ArrayList<>();
 569                 boolean added = methods.add(this);
 570                 assert(added);
 571             }
 572 
 573             public void trimToSize() {
 574                 super.trimToSize();
 575                 if (code != null)
 576                     code.trimToSize();
 577             }
 578 
 579             public int getArgumentSize() {
 580                 int argSize  = descriptor.typeRef.computeSize(true);
 581                 int thisSize = Modifier.isStatic(flags) ? 0 : 1;
 582                 return thisSize + argSize;
 583             }
 584 
 585             // Sort methods in a canonical order (by type, then by name).
 586             public int compareTo(Member o) {
 587                 Method that = (Method)o;
 588                 return this.getDescriptor().compareTo(that.getDescriptor());
 589             }
 590 
 591             public void strip(String attrName) {
 592                 if ("Code".equals(attrName))
 593                     code = null;
 594                 if (code != null)
 595                     code.strip(attrName);
 596                 super.strip(attrName);
 597             }
 598             protected void visitRefs(int mode, Collection<Entry> refs) {
 599                 super.visitRefs(mode, refs);
 600                 if (code != null) {
 601                     if (mode == VRM_CLASSIC) {
 602                         refs.add(getRefString("Code"));
 603                     }
 604                     code.visitRefs(mode, refs);
 605                 }
 606             }
 607         }
 608 
 609         public void trimToSize() {
 610             super.trimToSize();
 611             for (int isM = 0; isM <= 1; isM++) {
 612                 ArrayList<? extends Member> members = (isM == 0) ? fields : methods;
 613                 if (members == null)  continue;
 614                 members.trimToSize();
 615                 for (Member m : members) {
 616                     m.trimToSize();
 617                 }
 618             }
 619             if (innerClasses != null) {
 620                 innerClasses.trimToSize();
 621             }
 622         }
 623 
 624         public void strip(String attrName) {
 625             if ("InnerClass".equals(attrName))
 626                 innerClasses = null;
 627             for (int isM = 0; isM <= 1; isM++) {
 628                 ArrayList<? extends Member> members = (isM == 0) ? fields : methods;
 629                 if (members == null)  continue;
 630                 for (Member m : members) {
 631                     m.strip(attrName);
 632                 }
 633             }
 634             super.strip(attrName);
 635         }
 636 
 637         protected void visitRefs(int mode, Collection<Entry> refs) {
 638             if (verbose > 2)  Utils.log.fine("visitRefs "+this);
 639             refs.add(thisClass);
 640             refs.add(superClass);
 641             refs.addAll(Arrays.asList(interfaces));
 642             for (int isM = 0; isM <= 1; isM++) {
 643                 ArrayList<? extends Member> members = (isM == 0) ? fields : methods;
 644                 if (members == null)  continue;
 645                 for (Member m : members) {
 646                     boolean ok = false;
 647                     try {
 648                         m.visitRefs(mode, refs);
 649                         ok = true;
 650                     } finally {
 651                         if (!ok)
 652                             Utils.log.warning("Error scanning "+m);
 653                     }
 654                 }
 655             }
 656             visitInnerClassRefs(mode, refs);
 657             // Handle attribute list:
 658             super.visitRefs(mode, refs);
 659         }
 660 
 661         protected void visitInnerClassRefs(int mode, Collection<Entry> refs) {
 662             Package.visitInnerClassRefs(innerClasses, mode, refs);
 663         }
 664 
 665         // Hook called by ClassReader when it's done.
 666         void finishReading() {
 667             trimToSize();
 668             maybeChooseFileName();
 669         }
 670 
 671         public void initFile(File file) {
 672             assert(this.file == null);  // set-once
 673             if (file == null) {
 674                 // Build a trivial stub.
 675                 file = newStub(canonicalFileName());
 676             }
 677             this.file = file;
 678             assert(file.isClassStub());
 679             file.stubClass = this;
 680             maybeChooseFileName();
 681         }
 682 
 683         public void maybeChooseFileName() {
 684             if (thisClass == null) {
 685                 return;  // do not choose yet
 686             }
 687             String canonName = canonicalFileName();
 688             if (file.nameString.equals("")) {
 689                 file.nameString = canonName;
 690             }
 691             if (file.nameString.equals(canonName)) {
 692                 // The file name is predictable.  Transmit "".
 693                 file.name = getRefString("");
 694                 return;
 695             }
 696             // If name has not yet been looked up, find it now.
 697             if (file.name == null) {
 698                 file.name = getRefString(file.nameString);
 699             }
 700         }
 701 
 702         public String canonicalFileName() {
 703             if (thisClass == null)  return null;
 704             return thisClass.stringValue() + ".class";
 705         }
 706 
 707         public java.io.File getFileName(java.io.File parent) {
 708             String name = file.name.stringValue();
 709             if (name.equals(""))
 710                 name = canonicalFileName();
 711             String fname = name.replace('/', java.io.File.separatorChar);
 712             return new java.io.File(parent, fname);
 713         }
 714         public java.io.File getFileName() {
 715             return getFileName(null);
 716         }
 717 
 718         public String toString() {
 719             return thisClass.stringValue();
 720         }
 721     }
 722 
 723     void addClass(Class c) {
 724         assert(c.getPackage() == this);
 725         boolean added = classes.add(c);
 726         assert(added);
 727         // Make sure the class is represented in the total file order:
 728         if (c.file == null)  c.initFile(null);
 729         addFile(c.file);
 730     }
 731 
 732     // What non-class files are in this unit?
 733     ArrayList<File> files = new ArrayList<>();
 734 
 735     public List<File> getFiles() {
 736         return files;
 737     }
 738 
 739     public List<File> getClassStubs() {
 740         List<File> classStubs = new ArrayList<>(classes.size());
 741         for (Class cls : classes) {
 742             assert(cls.file.isClassStub());
 743             classStubs.add(cls.file);
 744         }
 745         return classStubs;
 746     }
 747 
 748     public final class File implements Comparable<File> {
 749         String nameString;  // true name of this file
 750         Utf8Entry name;
 751         int modtime = NO_MODTIME;
 752         int options = 0;  // random flag bits, such as deflate_hint
 753         Class stubClass;  // if this is a stub, here's the class
 754         ArrayList<byte[]> prepend = new ArrayList<>();  // list of byte[]
 755         java.io.ByteArrayOutputStream append = new ByteArrayOutputStream();
 756 
 757         File(Utf8Entry name) {
 758             this.name = name;
 759             this.nameString = name.stringValue();
 760             // caller must fill in contents
 761         }
 762         File(String nameString) {
 763             nameString = fixupFileName(nameString);
 764             this.name = getRefString(nameString);
 765             this.nameString = name.stringValue();
 766         }
 767 
 768         public boolean isDirectory() {
 769             // JAR directory.  Useless.
 770             return nameString.endsWith("/");
 771         }
 772         public boolean isClassStub() {
 773             return (options & FO_IS_CLASS_STUB) != 0;
 774         }
 775         public Class getStubClass() {
 776             assert(isClassStub());
 777             assert(stubClass != null);
 778             return stubClass;
 779         }
 780         public boolean isTrivialClassStub() {
 781             return isClassStub()
 782                 && name.stringValue().equals("")
 783                 && (modtime == NO_MODTIME || modtime == default_modtime)
 784                 && (options &~ FO_IS_CLASS_STUB) == 0;
 785         }
 786 
 787         // The nameString is the key.  Ignore other things.
 788         // (Note:  The name might be "", in the case of a trivial class stub.)
 789         public boolean equals(Object o) {
 790             if (o == null || (o.getClass() != File.class))
 791                 return false;
 792             File that = (File)o;
 793             return that.nameString.equals(this.nameString);
 794         }
 795         public int hashCode() {
 796             return nameString.hashCode();
 797         }
 798         // Simple alphabetic sort.  PackageWriter uses a better comparator.
 799         public int compareTo(File that) {
 800             return this.nameString.compareTo(that.nameString);
 801         }
 802         public String toString() {
 803             return nameString+"{"
 804                 +(isClassStub()?"*":"")
 805                 +(BandStructure.testBit(options,FO_DEFLATE_HINT)?"@":"")
 806                 +(modtime==NO_MODTIME?"":"M"+modtime)
 807                 +(getFileLength()==0?"":"["+getFileLength()+"]")
 808                 +"}";
 809         }
 810 
 811         public java.io.File getFileName() {
 812             return getFileName(null);
 813         }
 814         public java.io.File getFileName(java.io.File parent) {
 815             String lname = this.nameString;
 816             //if (name.startsWith("./"))  name = name.substring(2);
 817             String fname = lname.replace('/', java.io.File.separatorChar);
 818             return new java.io.File(parent, fname);
 819         }
 820 
 821         public void addBytes(byte[] bytes) {
 822             addBytes(bytes, 0, bytes.length);
 823         }
 824         public void addBytes(byte[] bytes, int off, int len) {
 825             if (((append.size() | len) << 2) < 0) {
 826                 prepend.add(append.toByteArray());
 827                 append.reset();
 828             }
 829             append.write(bytes, off, len);
 830         }
 831         public long getFileLength() {
 832             long len = 0;
 833             if (prepend == null || append == null)  return 0;
 834             for (byte[] block : prepend) {
 835                 len += block.length;
 836             }
 837             len += append.size();
 838             return len;
 839         }
 840         public void writeTo(OutputStream out) throws IOException {
 841             if (prepend == null || append == null)  return;
 842             for (byte[] block : prepend) {
 843                 out.write(block);
 844             }
 845             append.writeTo(out);
 846         }
 847         public void readFrom(InputStream in) throws IOException {
 848             byte[] buf = new byte[1 << 16];
 849             int nr;
 850             while ((nr = in.read(buf)) > 0) {
 851                 addBytes(buf, 0, nr);
 852             }
 853         }
 854         public InputStream getInputStream() {
 855             InputStream in = new ByteArrayInputStream(append.toByteArray());
 856             if (prepend.isEmpty())  return in;
 857             List<InputStream> isa = new ArrayList<>(prepend.size()+1);
 858             for (byte[] bytes : prepend) {
 859                 isa.add(new ByteArrayInputStream(bytes));
 860             }
 861             isa.add(in);
 862             return new SequenceInputStream(Collections.enumeration(isa));
 863         }
 864 
 865         protected void visitRefs(int mode, Collection<Entry> refs) {
 866             assert(name != null);
 867             refs.add(name);
 868         }
 869     }
 870 
 871     File newStub(String classFileNameString) {
 872         File stub = new File(classFileNameString);
 873         stub.options |= FO_IS_CLASS_STUB;
 874         stub.prepend = null;
 875         stub.append = null;  // do not collect data
 876         return stub;
 877     }
 878 
 879     private static String fixupFileName(String name) {
 880         String fname = name.replace(java.io.File.separatorChar, '/');
 881         if (fname.startsWith("/")) {
 882             throw new IllegalArgumentException("absolute file name "+fname);
 883         }
 884         return fname;
 885     }
 886 
 887     void addFile(File file) {
 888         boolean added = files.add(file);
 889         assert(added);
 890     }
 891 
 892     // Is there a globally declared table of inner classes?
 893     List<InnerClass> allInnerClasses = new ArrayList<>();
 894     Map<ClassEntry, InnerClass>   allInnerClassesByThis;
 895 
 896     public
 897     List<InnerClass> getAllInnerClasses() {
 898         return allInnerClasses;
 899     }
 900 
 901     public
 902     void setAllInnerClasses(Collection<InnerClass> ics) {
 903         assert(ics != allInnerClasses);
 904         allInnerClasses.clear();
 905         allInnerClasses.addAll(ics);
 906 
 907         // Make an index:
 908         allInnerClassesByThis = new HashMap<>(allInnerClasses.size());
 909         for (InnerClass ic : allInnerClasses) {
 910             Object pic = allInnerClassesByThis.put(ic.thisClass, ic);
 911             assert(pic == null);  // caller must ensure key uniqueness!
 912         }
 913     }
 914 
 915     /** Return a global inner class record for the given thisClass. */
 916     public
 917     InnerClass getGlobalInnerClass(Entry thisClass) {
 918         assert(thisClass instanceof ClassEntry);
 919         return allInnerClassesByThis.get(thisClass);
 920     }
 921 
 922     static
 923     class InnerClass implements Comparable<InnerClass> {
 924         final ClassEntry thisClass;
 925         final ClassEntry outerClass;
 926         final Utf8Entry name;
 927         final int flags;
 928 
 929         // Can name and outerClass be derived from thisClass?
 930         final boolean predictable;
 931 
 932         // About 30% of inner classes are anonymous (in rt.jar).
 933         // About 60% are class members; the rest are named locals.
 934         // Nearly all have predictable outers and names.
 935 
 936         InnerClass(ClassEntry thisClass, ClassEntry outerClass,
 937                    Utf8Entry name, int flags) {
 938             this.thisClass = thisClass;
 939             this.outerClass = outerClass;
 940             this.name = name;
 941             this.flags = flags;
 942             this.predictable = computePredictable();
 943         }
 944 
 945         private boolean computePredictable() {
 946             //System.out.println("computePredictable "+outerClass+" "+this.name);
 947             String[] parse = parseInnerClassName(thisClass.stringValue());
 948             if (parse == null)  return false;
 949             String pkgOuter = parse[0];
 950             //String number = parse[1];
 951             String lname     = parse[2];
 952             String haveName  = (this.name == null)  ? null : this.name.stringValue();
 953             String haveOuter = (outerClass == null) ? null : outerClass.stringValue();
 954             boolean lpredictable = (lname == haveName && pkgOuter == haveOuter);
 955             //System.out.println("computePredictable => "+predictable);
 956             return lpredictable;
 957         }
 958 
 959         public boolean equals(Object o) {
 960             if (o == null || o.getClass() != InnerClass.class)
 961                 return false;
 962             InnerClass that = (InnerClass)o;
 963             return eq(this.thisClass, that.thisClass)
 964                 && eq(this.outerClass, that.outerClass)
 965                 && eq(this.name, that.name)
 966                 && this.flags == that.flags;
 967         }
 968         private static boolean eq(Object x, Object y) {
 969             return (x == null)? y == null: x.equals(y);
 970         }
 971         public int hashCode() {
 972             return thisClass.hashCode();
 973         }
 974         public int compareTo(InnerClass that) {
 975             return this.thisClass.compareTo(that.thisClass);
 976         }
 977 
 978         protected void visitRefs(int mode, Collection<Entry> refs) {
 979             refs.add(thisClass);
 980             if (mode == VRM_CLASSIC || !predictable) {
 981                 // If the name can be demangled, the package omits
 982                 // the products of demangling.  Otherwise, include them.
 983                 refs.add(outerClass);
 984                 refs.add(name);
 985             }
 986         }
 987 
 988         public String toString() {
 989             return thisClass.stringValue();
 990         }
 991     }
 992 
 993     // Helper for building InnerClasses attributes.
 994     private static
 995     void visitInnerClassRefs(Collection<InnerClass> innerClasses, int mode, Collection<Entry> refs) {
 996         if (innerClasses == null) {
 997             return;  // no attribute; nothing to do
 998         }
 999         if (mode == VRM_CLASSIC) {
1000             refs.add(getRefString("InnerClasses"));
1001         }
1002         if (innerClasses.size() > 0) {
1003             // Count the entries themselves:
1004             for (InnerClass c : innerClasses) {
1005                 c.visitRefs(mode, refs);
1006             }
1007         }
1008     }
1009 
1010     static String[] parseInnerClassName(String n) {
1011         //System.out.println("parseInnerClassName "+n);
1012         String pkgOuter, number, name;
1013         int dollar1, dollar2;  // pointers to $ in the pattern
1014         // parse n = (<pkg>/)*<outer>($<number>)?($<name>)?
1015         int nlen = n.length();
1016         int pkglen = lastIndexOf(SLASH_MIN,  SLASH_MAX,  n, n.length()) + 1;
1017         dollar2    = lastIndexOf(DOLLAR_MIN, DOLLAR_MAX, n, n.length());
1018         if (dollar2 < pkglen)  return null;
1019         if (isDigitString(n, dollar2+1, nlen)) {
1020             // n = (<pkg>/)*<outer>$<number>
1021             number = n.substring(dollar2+1, nlen);
1022             name = null;
1023             dollar1 = dollar2;
1024         } else if ((dollar1
1025                     = lastIndexOf(DOLLAR_MIN, DOLLAR_MAX, n, dollar2-1))
1026                    > pkglen
1027                    && isDigitString(n, dollar1+1, dollar2)) {
1028             // n = (<pkg>/)*<outer>$<number>$<name>
1029             number = n.substring(dollar1+1, dollar2);
1030             name = n.substring(dollar2+1, nlen).intern();
1031         } else {
1032             // n = (<pkg>/)*<outer>$<name>
1033             dollar1 = dollar2;
1034             number = null;
1035             name = n.substring(dollar2+1, nlen).intern();
1036         }
1037         if (number == null)
1038             pkgOuter = n.substring(0, dollar1).intern();
1039         else
1040             pkgOuter = null;
1041         //System.out.println("parseInnerClassName parses "+pkgOuter+" "+number+" "+name);
1042         return new String[] { pkgOuter, number, name };
1043     }
1044 
1045     private static final int SLASH_MIN = '.';
1046     private static final int SLASH_MAX = '/';
1047     private static final int DOLLAR_MIN = 0;
1048     private static final int DOLLAR_MAX = '-';
1049     static {
1050         assert(lastIndexOf(DOLLAR_MIN, DOLLAR_MAX, "x$$y$", 4) == 2);
1051         assert(lastIndexOf(SLASH_MIN,  SLASH_MAX,  "x//y/", 4) == 2);
1052     }
1053 
1054     private static int lastIndexOf(int chMin, int chMax, String str, int pos) {
1055         for (int i = pos; --i >= 0; ) {
1056             int ch = str.charAt(i);
1057             if (ch >= chMin && ch <= chMax) {
1058                 return i;
1059             }
1060         }
1061         return -1;
1062     }
1063 
1064     private static boolean isDigitString(String x, int beg, int end) {
1065         if (beg == end)  return false;  // null string
1066         for (int i = beg; i < end; i++) {
1067             char ch = x.charAt(i);
1068             if (!(ch >= '0' && ch <= '9'))  return false;
1069         }
1070         return true;
1071     }
1072 
1073     static String getObviousSourceFile(String className) {
1074         String n = className;
1075         int pkglen = lastIndexOf(SLASH_MIN,  SLASH_MAX,  n, n.length()) + 1;
1076         n = n.substring(pkglen);
1077         int cutoff = n.length();
1078         for (;;) {
1079             // Work backwards, finding all '$', '#', etc.
1080             int dollar2 = lastIndexOf(DOLLAR_MIN, DOLLAR_MAX, n, cutoff-1);
1081             if (dollar2 < 0)
1082                 break;
1083             cutoff = dollar2;
1084             if (cutoff == 0)
1085                 break;
1086         }
1087         String obvious = n.substring(0, cutoff)+".java";
1088         return obvious;
1089     }
1090 /*
1091     static {
1092         assert(getObviousSourceFile("foo").equals("foo.java"));
1093         assert(getObviousSourceFile("foo/bar").equals("bar.java"));
1094         assert(getObviousSourceFile("foo/bar$baz").equals("bar.java"));
1095         assert(getObviousSourceFile("foo/bar#baz#1").equals("bar.java"));
1096         assert(getObviousSourceFile("foo.bar.baz#1").equals("baz.java"));
1097     }
1098 */
1099 
1100     static Utf8Entry getRefString(String s) {
1101         return ConstantPool.getUtf8Entry(s);
1102     }
1103 
1104     static LiteralEntry getRefLiteral(Comparable<?> s) {
1105         return ConstantPool.getLiteralEntry(s);
1106     }
1107 
1108     void stripAttributeKind(String what) {
1109         // what is one of { Debug, Compile, Constant, Exceptions, InnerClasses }
1110         if (verbose > 0)
1111             Utils.log.info("Stripping "+what.toLowerCase()+" data and attributes...");
1112         switch (what) {
1113             case "Debug":
1114                 strip("SourceFile");
1115                 strip("LineNumberTable");
1116                 strip("LocalVariableTable");
1117                 strip("LocalVariableTypeTable");
1118                 break;
1119             case "Compile":
1120                 // Keep the inner classes normally.
1121                 // Although they have no effect on execution,
1122                 // the Reflection API exposes them, and JCK checks them.
1123                 // NO: // strip("InnerClasses");
1124                 strip("Deprecated");
1125                 strip("Synthetic");
1126                 break;
1127             case "Exceptions":
1128                 // Keep the exceptions normally.
1129                 // Although they have no effect on execution,
1130                 // the Reflection API exposes them, and JCK checks them.
1131                 strip("Exceptions");
1132                 break;
1133             case "Constant":
1134                 stripConstantFields();
1135                 break;
1136         }
1137     }
1138 
1139     public void trimToSize() {
1140         classes.trimToSize();
1141         for (Class c : classes) {
1142             c.trimToSize();
1143         }
1144         files.trimToSize();
1145     }
1146 
1147     public void strip(String attrName) {
1148         for (Class c : classes) {
1149             c.strip(attrName);
1150         }
1151     }
1152 
1153     public void stripConstantFields() {
1154         for (Class c : classes) {
1155             for (Iterator<Class.Field> j = c.fields.iterator(); j.hasNext(); ) {
1156                 Class.Field f = j.next();
1157                 if (Modifier.isFinal(f.flags)
1158                     // do not strip non-static finals:
1159                     && Modifier.isStatic(f.flags)
1160                     && f.getAttribute("ConstantValue") != null
1161                     && !f.getName().startsWith("serial")) {
1162                     if (verbose > 2) {
1163                         Utils.log.fine(">> Strip "+this+" ConstantValue");
1164                         j.remove();
1165                     }
1166                 }
1167             }
1168         }
1169     }
1170 
1171     protected void visitRefs(int mode, Collection<Entry> refs) {
1172         for ( Class c : classes) {
1173             c.visitRefs(mode, refs);
1174         }
1175         if (mode != VRM_CLASSIC) {
1176             for (File f : files) {
1177                 f.visitRefs(mode, refs);
1178             }
1179             visitInnerClassRefs(allInnerClasses, mode, refs);
1180         }
1181     }
1182 
1183     // Use this before writing the package file.
1184     // It sorts files into a new order which seems likely to
1185     // compress better.  It also moves classes to the end of the
1186     // file order.  It also removes JAR directory entries, which
1187     // are useless.
1188     void reorderFiles(boolean keepClassOrder, boolean stripDirectories) {
1189         // First reorder the classes, if that is allowed.
1190         if (!keepClassOrder) {
1191             // In one test with rt.jar, this trick gained 0.7%
1192             Collections.sort(classes);
1193         }
1194 
1195         // Remove stubs from resources; maybe we'll add them on at the end,
1196         // if there are some non-trivial ones.  The best case is that
1197         // modtimes and options are not transmitted, and the stub files
1198         // for class files do not need to be transmitted at all.
1199         // Also
1200         List<File> stubs = getClassStubs();
1201         for (Iterator<File> i = files.iterator(); i.hasNext(); ) {
1202             File file = i.next();
1203             if (file.isClassStub() ||
1204                 (stripDirectories && file.isDirectory())) {
1205                 i.remove();
1206             }
1207         }
1208 
1209         // Sort the remaining non-class files.
1210         // We sort them by file type.
1211         // This keeps files of similar format near each other.
1212         // Put class files at the end, keeping their fixed order.
1213         // Be sure the JAR file's required manifest stays at the front. (4893051)
1214         Collections.sort(files, new Comparator<>() {
1215                 public int compare(File r0, File r1) {
1216                     // Get the file name.
1217                     String f0 = r0.nameString;
1218                     String f1 = r1.nameString;
1219                     if (f0.equals(f1)) return 0;
1220                     if (JarFile.MANIFEST_NAME.equals(f0))  return 0-1;
1221                     if (JarFile.MANIFEST_NAME.equals(f1))  return 1-0;
1222                     // Extract file basename.
1223                     String n0 = f0.substring(1+f0.lastIndexOf('/'));
1224                     String n1 = f1.substring(1+f1.lastIndexOf('/'));
1225                     // Extract basename extension.
1226                     String x0 = n0.substring(1+n0.lastIndexOf('.'));
1227                     String x1 = n1.substring(1+n1.lastIndexOf('.'));
1228                     int r;
1229                     // Primary sort key is file extension.
1230                     r = x0.compareTo(x1);
1231                     if (r != 0)  return r;
1232                     r = f0.compareTo(f1);
1233                     return r;
1234                 }
1235             });
1236 
1237         // Add back the class stubs after sorting, before trimStubs.
1238         files.addAll(stubs);
1239     }
1240 
1241     void trimStubs() {
1242         // Restore enough non-trivial stubs to carry the needed class modtimes.
1243         for (ListIterator<File> i = files.listIterator(files.size()); i.hasPrevious(); ) {
1244             File file = i.previous();
1245             if (!file.isTrivialClassStub()) {
1246                 if (verbose > 1)
1247                     Utils.log.fine("Keeping last non-trivial "+file);
1248                 break;
1249             }
1250             if (verbose > 2)
1251                 Utils.log.fine("Removing trivial "+file);
1252             i.remove();
1253         }
1254 
1255         if (verbose > 0) {
1256             Utils.log.info("Transmitting "+files.size()+" files, including per-file data for "+getClassStubs().size()+" classes out of "+classes.size());
1257         }
1258     }
1259 
1260     // Use this before writing the package file.
1261     void buildGlobalConstantPool(Set<Entry> requiredEntries) {
1262         if (verbose > 1)
1263             Utils.log.fine("Checking for unused CP entries");
1264         requiredEntries.add(getRefString(""));  // uconditionally present
1265         visitRefs(VRM_PACKAGE, requiredEntries);
1266         ConstantPool.completeReferencesIn(requiredEntries, false);
1267         if (verbose > 1)
1268             Utils.log.fine("Sorting CP entries");
1269         Index   cpAllU = ConstantPool.makeIndex("unsorted", requiredEntries);
1270         Index[] byTagU = ConstantPool.partitionByTag(cpAllU);
1271         for (int i = 0; i < ConstantPool.TAGS_IN_ORDER.length; i++) {
1272             byte tag = ConstantPool.TAGS_IN_ORDER[i];
1273             // Work on all entries of a given kind.
1274             Index ix = byTagU[tag];
1275             if (ix == null)  continue;
1276             ConstantPool.sort(ix);
1277             cp.initIndexByTag(tag, ix);
1278             byTagU[tag] = null;  // done with it
1279         }
1280         for (int i = 0; i < byTagU.length; i++) {
1281             Index ix = byTagU[i];
1282             assert(ix == null);  // all consumed
1283         }
1284         for (int i = 0; i < ConstantPool.TAGS_IN_ORDER.length; i++) {
1285             byte tag = ConstantPool.TAGS_IN_ORDER[i];
1286             Index ix = cp.getIndexByTag(tag);
1287             assert(ix.assertIsSorted());
1288             if (verbose > 2)  Utils.log.fine(ix.dumpString());
1289         }
1290     }
1291 
1292     // Use this before writing the class files.
1293     void ensureAllClassFiles() {
1294         Set<File> fileSet = new HashSet<>(files);
1295         for (Class cls : classes) {
1296             // Add to the end of ths list:
1297             if (!fileSet.contains(cls.file))
1298                 files.add(cls.file);
1299         }
1300     }
1301 
1302     static final List<Object> noObjects = Arrays.asList(new Object[0]);
1303     static final List<Class.Field> noFields = Arrays.asList(new Class.Field[0]);
1304     static final List<Class.Method> noMethods = Arrays.asList(new Class.Method[0]);
1305     static final List<InnerClass> noInnerClasses = Arrays.asList(new InnerClass[0]);
1306 
1307     protected static final class Version {
1308 
1309         public final short major;
1310         public final short minor;
1311 
1312         private Version(short major, short minor) {
1313             this.major = major;
1314             this.minor = minor;
1315         }
1316 
1317         public String toString() {
1318             return major + "." + minor;
1319         }
1320 
1321         public boolean equals(Object that) {
1322             return that instanceof Version
1323                     && major == ((Version)that).major
1324                     && minor == ((Version)that).minor;
1325         }
1326 
1327         public int intValue() {
1328             return (major << 16) + minor;
1329         }
1330 
1331         public int hashCode() {
1332             return (major << 16) + 7 + minor;
1333         }
1334 
1335         public static Version of(int major, int minor) {
1336             return new Version((short)major, (short)minor);
1337         }
1338 
1339         public static Version of(byte[] bytes) {
1340            int minor = ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF);
1341            int major = ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF);
1342            return new Version((short)major, (short)minor);
1343         }
1344 
1345         public static Version of(int major_minor) {
1346             short minor = (short)major_minor;
1347             short major = (short)(major_minor >>> 16);
1348             return new Version(major, minor);
1349         }
1350 
1351         public static Version makeVersion(PropMap props, String partialKey) {
1352             int min = props.getInteger(Utils.COM_PREFIX
1353                     + partialKey + ".minver", -1);
1354             int maj = props.getInteger(Utils.COM_PREFIX
1355                     + partialKey + ".majver", -1);
1356             return min >= 0 && maj >= 0 ? Version.of(maj, min) : null;
1357         }
1358         public byte[] asBytes() {
1359             byte[] bytes = {
1360                 (byte) (minor >> 8), (byte) minor,
1361                 (byte) (major >> 8), (byte) major
1362             };
1363             return bytes;
1364         }
1365         public int compareTo(Version that) {
1366             return this.intValue() - that.intValue();
1367         }
1368 
1369         public boolean lessThan(Version that) {
1370             return compareTo(that) < 0 ;
1371         }
1372 
1373         public boolean greaterThan(Version that) {
1374             return compareTo(that) > 0 ;
1375         }
1376     }
1377 }