jdk/src/share/classes/com/sun/java/util/jar/pack/Package.java
changeset 2 90ce3da70b43
child 5506 202f599c92aa
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/src/share/classes/com/sun/java/util/jar/pack/Package.java	Sat Dec 01 00:00:00 2007 +0000
@@ -0,0 +1,1304 @@
+/*
+ * Copyright 2001-2005 Sun Microsystems, Inc.  All Rights Reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Sun designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Sun in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.com if you need additional information or
+ * have any questions.
+ */
+
+package com.sun.java.util.jar.pack;
+
+import java.lang.reflect.Modifier;
+import java.util.*;
+import java.util.zip.*;
+import java.util.jar.*;
+import java.io.*;
+import com.sun.java.util.jar.pack.ConstantPool.*;
+
+/**
+ * Define the main data structure transmitted by pack/unpack.
+ * @author John Rose
+ */
+class Package implements Constants {
+    int verbose;
+    {
+        PropMap pmap = Utils.currentPropMap();
+        if (pmap != null)
+            verbose = pmap.getInteger(Utils.DEBUG_VERBOSE);
+    }
+
+    int magic;
+    int package_minver;
+    int package_majver;
+
+    int default_modtime = NO_MODTIME;
+    int default_options = 0;  // FO_DEFLATE_HINT
+
+    short default_class_majver = -1; // fill in later
+    short default_class_minver = 0;  // fill in later
+
+    // These fields can be adjusted by driver properties.
+    short min_class_majver = JAVA_MIN_CLASS_MAJOR_VERSION;
+    short min_class_minver = JAVA_MIN_CLASS_MINOR_VERSION;
+    short max_class_majver = JAVA6_MAX_CLASS_MAJOR_VERSION;
+    short max_class_minver = JAVA6_MAX_CLASS_MINOR_VERSION;
+
+    short observed_max_class_majver = min_class_majver;
+    short observed_max_class_minver = min_class_minver;
+
+    // What constants are used in this unit?
+    ConstantPool.IndexGroup cp = new ConstantPool.IndexGroup();
+
+    Package() {
+        magic          = JAVA_PACKAGE_MAGIC;
+        package_minver = -1;  // fill in later
+        package_majver = 0;   // fill in later
+    }
+
+    public
+    void reset() {
+        cp = new ConstantPool.IndexGroup();
+        classes.clear();
+        files.clear();
+    }
+
+    int getPackageVersion() {
+        return (package_majver << 16) + (int)package_minver;
+    }
+
+    // Special empty versions of Code and InnerClasses, used for markers.
+    public static final Attribute.Layout attrCodeEmpty;
+    public static final Attribute.Layout attrInnerClassesEmpty;
+    public static final Attribute.Layout attrSourceFileSpecial;
+    public static final Map attrDefs;
+    static {
+        HashMap ad = new HashMap(2);
+        attrCodeEmpty = Attribute.define(ad, ATTR_CONTEXT_METHOD,
+                                         "Code", "").layout();
+        attrInnerClassesEmpty = Attribute.define(ad, ATTR_CONTEXT_CLASS,
+                                                 "InnerClasses", "").layout();
+        attrSourceFileSpecial = Attribute.define(ad, ATTR_CONTEXT_CLASS,
+                                                 "SourceFile", "RUNH").layout();
+        attrDefs = Collections.unmodifiableMap(ad);
+    }
+
+    int getDefaultClassVersion() {
+        return (default_class_majver << 16) + (char)default_class_minver;
+    }
+
+    /** Return the highest version number of all classes,
+     *  or 0 if there are no classes.
+     */
+    int getHighestClassVersion() {
+        int res = 0;  // initial low value
+        for (Iterator i = classes.iterator(); i.hasNext(); ) {
+            Class cls = (Class) i.next();
+            int ver = cls.getVersion();
+            if (res < ver)  res = ver;
+        }
+        return res;
+    }
+
+    /** Convenience function to choose an archive version based
+     *  on the class file versions observed within the archive.
+     */
+    void choosePackageVersion() {
+        assert(package_majver <= 0);  // do not call this twice
+        int classver = getHighestClassVersion();
+        if (classver != 0 &&
+            (classver >>> 16) < JAVA6_MAX_CLASS_MAJOR_VERSION) {
+            // There are only old classfiles in this segment.
+            package_majver = JAVA5_PACKAGE_MAJOR_VERSION;
+            package_minver = JAVA5_PACKAGE_MINOR_VERSION;
+        } else {
+            // Normal case.  Use the newest archive format.
+            package_majver = JAVA6_PACKAGE_MAJOR_VERSION;
+            package_minver = JAVA6_PACKAGE_MINOR_VERSION;
+        }
+    }
+
+    // What Java classes are in this unit?
+
+    // Fixed 6211177, converted to throw IOException
+    void checkVersion() throws IOException {
+        if (magic != JAVA_PACKAGE_MAGIC) {
+            String gotMag = Integer.toHexString(magic);
+            String expMag = Integer.toHexString(JAVA_PACKAGE_MAGIC);
+            throw new IOException("Unexpected package magic number: got "+gotMag+"; expected "+expMag);
+        }
+        if ((package_majver != JAVA6_PACKAGE_MAJOR_VERSION  &&
+             package_majver != JAVA5_PACKAGE_MAJOR_VERSION) ||
+             (package_minver != JAVA6_PACKAGE_MINOR_VERSION &&
+             package_minver != JAVA5_PACKAGE_MINOR_VERSION)) {
+
+            String gotVer = package_majver+"."+package_minver;
+            String expVer = JAVA6_PACKAGE_MAJOR_VERSION+"."+JAVA6_PACKAGE_MINOR_VERSION+
+                            " OR "+
+                            JAVA5_PACKAGE_MAJOR_VERSION+"."+JAVA5_PACKAGE_MINOR_VERSION;
+            throw new IOException("Unexpected package minor version: got "+gotVer+"; expected "+expVer);
+        }
+    }
+
+    ArrayList classes = new ArrayList();
+
+    public List getClasses() {
+        return classes;
+    }
+
+    public
+    class Class extends Attribute.Holder implements Comparable {
+        public Package getPackage() { return Package.this; }
+
+        // Optional file characteristics and data source (a "class stub")
+        File file;
+
+        // File header
+        int magic;
+        short minver, majver;
+
+        // Local constant pool (one-way mapping of index => package cp).
+        Entry[] cpMap;
+
+        // Class header
+        //int flags;  // in Attribute.Holder.this.flags
+        ClassEntry thisClass;
+        ClassEntry superClass;
+        ClassEntry[] interfaces;
+
+        // Class parts
+        ArrayList fields;
+        ArrayList methods;
+        //ArrayList attributes;  // in Attribute.Holder.this.attributes
+        // Note that InnerClasses may be collected at the package level.
+        ArrayList innerClasses;
+
+        Class(int flags, ClassEntry thisClass, ClassEntry superClass, ClassEntry[] interfaces) {
+            this.magic      = JAVA_MAGIC;
+            this.minver     = default_class_minver;
+            this.majver     = default_class_majver;
+            this.flags      = flags;
+            this.thisClass  = thisClass;
+            this.superClass = superClass;
+            this.interfaces = interfaces;
+
+            boolean added = classes.add(this);
+            assert(added);
+        }
+
+        Class(String classFile) {
+            // A blank class; must be read with a ClassReader, etc.
+            initFile(newStub(classFile));
+        }
+
+        List getFields() { return fields == null ? noFields : fields; }
+        List getMethods() { return methods == null ? noMethods : methods; }
+
+        public String getName() {
+            return thisClass.stringValue();
+        }
+
+        int getVersion() {
+            return (majver << 16) + (char)minver;
+        }
+        String getVersionString() {
+            return versionStringOf(majver, minver);
+        }
+
+        // Note:  equals and hashCode are identity-based.
+        public int compareTo(Object o) {
+            Class that = (Class)o;
+            String n0 = this.getName();
+            String n1 = that.getName();
+            return n0.compareTo(n1);
+        }
+
+        String getObviousSourceFile() {
+            return Package.getObviousSourceFile(getName());
+        }
+
+        private void transformSourceFile(boolean minimize) {
+            // Replace "obvious" SourceFile by null.
+            Attribute olda = getAttribute(attrSourceFileSpecial);
+            if (olda == null)
+                return;  // no SourceFile attr.
+            String obvious = getObviousSourceFile();
+            ArrayList ref = new ArrayList(1);
+            olda.visitRefs(this, VRM_PACKAGE, ref);
+            Utf8Entry sfName = (Utf8Entry) ref.get(0);
+            Attribute a = olda;
+            if (sfName == null) {
+                if (minimize) {
+                    // A pair of zero bytes.  Cannot use predef. layout.
+                    a = Attribute.find(ATTR_CONTEXT_CLASS, "SourceFile", "H");
+                    a = a.addContent(new byte[2]);
+                } else {
+                    // Expand null attribute to the obvious string.
+                    byte[] bytes = new byte[2];
+                    sfName = getRefString(obvious);
+                    Object f = null;
+                    f = Fixups.add(f, bytes, 0, Fixups.U2_FORMAT, sfName);
+                    a = attrSourceFileSpecial.addContent(bytes, f);
+                }
+            } else if (obvious.equals(sfName.stringValue())) {
+                if (minimize) {
+                    // Replace by an all-zero attribute.
+                    a = attrSourceFileSpecial.addContent(new byte[2]);
+                } else {
+                    assert(false);
+                }
+            }
+            if (a != olda) {
+                if (verbose > 2)
+                    Utils.log.fine("recoding obvious SourceFile="+obvious);
+                List newAttrs = new ArrayList(getAttributes());
+                int where = newAttrs.indexOf(olda);
+                newAttrs.set(where, a);
+                setAttributes(newAttrs);
+            }
+        }
+
+        void minimizeSourceFile() {
+            transformSourceFile(true);
+        }
+        void expandSourceFile() {
+            transformSourceFile(false);
+        }
+
+        protected Entry[] getCPMap() {
+            return cpMap;
+        }
+
+        protected void setCPMap(Entry[] cpMap) {
+            this.cpMap = cpMap;
+        }
+
+        boolean hasInnerClasses() {
+            return innerClasses != null;
+        }
+        List getInnerClasses() {
+            return innerClasses;
+        }
+
+        public void setInnerClasses(Collection ics) {
+            innerClasses = (ics == null) ? null : new ArrayList(ics);
+            // Edit the attribute list, if necessary.
+            Attribute a = getAttribute(attrInnerClassesEmpty);
+            if (innerClasses != null && a == null)
+                addAttribute(attrInnerClassesEmpty.canonicalInstance());
+            else if (innerClasses == null && a != null)
+                removeAttribute(a);
+        }
+
+        /** Given a global map of ICs (keyed by thisClass),
+         *  compute the subset of its Map.values which are
+         *  required to be present in the local InnerClasses
+         *  attribute.  Perform this calculation without
+         *  reference to any actual InnerClasses attribute.
+         *  <p>
+         *  The order of the resulting list is consistent
+         *  with that of Package.this.allInnerClasses.
+         */
+        public List computeGloballyImpliedICs() {
+            HashSet cpRefs = new HashSet();
+            {   // This block temporarily displaces this.innerClasses.
+                ArrayList innerClassesSaved = innerClasses;
+                innerClasses = null;  // ignore for the moment
+                visitRefs(VRM_CLASSIC, cpRefs);
+                innerClasses = innerClassesSaved;
+            }
+            ConstantPool.completeReferencesIn(cpRefs, true);
+
+            HashSet icRefs = new HashSet();
+            for (Iterator i = cpRefs.iterator(); i.hasNext(); ) {
+                Entry e = (Entry) i.next();
+                // Restrict cpRefs to InnerClasses entries only.
+                if (!(e instanceof ClassEntry))  continue;
+                // For every IC reference, add its outers also.
+                while (e != null) {
+                    InnerClass ic = getGlobalInnerClass(e);
+                    if (ic == null)  break;
+                    if (!icRefs.add(e))  break;
+                    e = ic.outerClass;
+                    // If we add A$B$C to the mix, we must also add A$B.
+                }
+            }
+            // This loop is structured this way so as to accumulate
+            // entries into impliedICs in an order which reflects
+            // the order of allInnerClasses.
+            ArrayList impliedICs = new ArrayList();
+            for (Iterator i = allInnerClasses.iterator(); i.hasNext(); ) {
+                InnerClass ic = (InnerClass) i.next();
+                // This one is locally relevant if it describes
+                // a member of the current class, or if the current
+                // class uses it somehow.  In the particular case
+                // where thisClass is an inner class, it will already
+                // be a member of icRefs.
+                if (icRefs.contains(ic.thisClass)
+                    || ic.outerClass == this.thisClass) {
+                    // Add every relevant class to the IC attribute:
+                    if (verbose > 1)
+                        Utils.log.fine("Relevant IC: "+ic);
+                    impliedICs.add(ic);
+                }
+            }
+            return impliedICs;
+        }
+
+        // Helper for both minimizing and expanding.
+        // Computes a symmetric difference.
+        private List computeICdiff() {
+            List impliedICs = computeGloballyImpliedICs();
+            List actualICs  = getInnerClasses();
+            if (actualICs == null)  actualICs = Collections.EMPTY_LIST;
+
+            // Symmetric difference is calculated from I, A like this:
+            //  diff = (I+A) - (I*A)
+            // Note that the center C is unordered, but the result
+            // preserves the original ordering of I and A.
+            //
+            // Class file rules require that outers precede inners.
+            // So, add I before A, in case A$B$Z is local, but A$B
+            // is implicit.  The reverse is never the case.
+            if (actualICs.isEmpty()) {
+                return impliedICs;
+                // Diff is I since A is empty.
+            }
+            if (impliedICs.isEmpty()) {
+                return actualICs;
+                // Diff is A since I is empty.
+            }
+            // (I*A) is non-trivial
+            HashSet center = new HashSet(actualICs);
+            center.retainAll(new HashSet(impliedICs));
+            impliedICs.addAll(actualICs);
+            impliedICs.removeAll(center);
+            // Diff is now I^A = (I+A)-(I*A).
+            return impliedICs;
+        }
+
+        /** When packing, anticipate the effect of expandLocalICs.
+         *  Replace the local ICs by their symmetric difference
+         *  with the globally implied ICs for this class; if this
+         *  difference is empty, remove the local ICs altogether.
+         *  <p>
+         *  An empty local IC attribute is reserved to signal
+         *  the unpacker to delete the attribute altogether,
+         *  so a missing local IC attribute signals the unpacker
+         *  to use the globally implied ICs changed.
+         */
+        void minimizeLocalICs() {
+            List diff = computeICdiff();
+            List actualICs = innerClasses;
+            List localICs;  // will be the diff, modulo edge cases
+            if (diff.isEmpty()) {
+                // No diff, so transmit no attribute.
+                localICs = null;
+                if (actualICs != null && actualICs.isEmpty()) {
+                    // Odd case:  No implied ICs, and a zero length attr.
+                    // Do not support it directly.
+                    if (verbose > 0)
+                        Utils.log.info("Warning: Dropping empty InnerClasses attribute from "+this);
+                }
+            } else if (actualICs == null) {
+                // No local IC attribute, even though some are implied.
+                // Signal with trivial attribute.
+                localICs = Collections.EMPTY_LIST;
+            } else {
+                // Transmit a non-empty diff, which will create
+                // a local ICs attribute.
+                localICs = diff;
+            }
+            // Reduce the set to the symmetric difference.
+            setInnerClasses(localICs);
+            if (verbose > 1 && localICs != null)
+                Utils.log.fine("keeping local ICs in "+this+": "+localICs);
+        }
+
+        /** When unpacking, undo the effect of minimizeLocalICs.
+         *  Must return negative if any IC tuples may have been deleted.
+         *  Otherwise, return positive if any IC tuples were added.
+         */
+        int expandLocalICs() {
+            List localICs = innerClasses;
+            List actualICs;
+            int changed;
+            if (localICs == null) {
+                // Diff was empty.  (Common case.)
+                List impliedICs = computeGloballyImpliedICs();
+                if (impliedICs.isEmpty()) {
+                    actualICs = null;
+                    changed = 0;
+                } else {
+                    actualICs = impliedICs;
+                    changed = 1;  // added more tuples
+                }
+            } else if (localICs.isEmpty()) {
+                // It was a non-empty diff, but the local ICs were absent.
+                actualICs = null;
+                changed = 0;  // [] => null, no tuple change
+            } else {
+                // Non-trivial diff was transmitted.
+                actualICs = computeICdiff();
+                // If we only added more ICs, return +1.
+                changed = actualICs.containsAll(localICs)? +1: -1;
+            }
+            setInnerClasses(actualICs);
+            return changed;
+        }
+
+        public abstract
+        class Member extends Attribute.Holder implements Comparable {
+            DescriptorEntry descriptor;
+
+            protected Member(int flags, DescriptorEntry descriptor) {
+                this.flags = flags;
+                this.descriptor = descriptor;
+            }
+
+            public Class thisClass() { return Class.this; }
+
+            public DescriptorEntry getDescriptor() {
+                return descriptor;
+            }
+            public String getName() {
+                return descriptor.nameRef.stringValue();
+            }
+            public String getType() {
+                return descriptor.typeRef.stringValue();
+            }
+
+            protected Entry[] getCPMap() {
+                return cpMap;
+            }
+            protected void visitRefs(int mode, Collection refs) {
+                if (verbose > 2)  Utils.log.fine("visitRefs "+this);
+                // Careful:  The descriptor is used by the package,
+                // but the classfile breaks it into component refs.
+                if (mode == VRM_CLASSIC) {
+                    refs.add(descriptor.nameRef);
+                    refs.add(descriptor.typeRef);
+                } else {
+                    refs.add(descriptor);
+                }
+                // Handle attribute list:
+                super.visitRefs(mode, refs);
+            }
+
+            public String toString() {
+                return Class.this + "." + descriptor.prettyString();
+            }
+        }
+
+        public
+        class Field extends Member {
+            // Order is significant for fields:  It is visible to reflection.
+            int order;
+
+            public Field(int flags, DescriptorEntry descriptor) {
+                super(flags, descriptor);
+                assert(!descriptor.isMethod());
+                if (fields == null)
+                    fields = new ArrayList();
+                boolean added = fields.add(this);
+                assert(added);
+                order = fields.size();
+            }
+
+            public byte getLiteralTag() {
+                return descriptor.getLiteralTag();
+            }
+
+            public int compareTo(Object o) {
+                Field that = (Field)o;
+                return this.order - that.order;
+            }
+        }
+
+        public
+        class Method extends Member {
+            // Code attribute is specially hardwired.
+            Code code;
+
+            public Method(int flags, DescriptorEntry descriptor) {
+                super(flags, descriptor);
+                assert(descriptor.isMethod());
+                if (methods == null)
+                    methods = new ArrayList();
+                boolean added = methods.add(this);
+                assert(added);
+            }
+
+            public void trimToSize() {
+                super.trimToSize();
+                if (code != null)
+                    code.trimToSize();
+            }
+
+            public int getArgumentSize() {
+                int argSize  = descriptor.typeRef.computeSize(true);
+                int thisSize = Modifier.isStatic(flags) ? 0 : 1;
+                return thisSize + argSize;
+            }
+
+            // Sort methods in a canonical order (by type, then by name).
+            public int compareTo(Object o) {
+                Method that = (Method)o;
+                return this.getDescriptor().compareTo(that.getDescriptor());
+            }
+
+            public void strip(String attrName) {
+                if (attrName == "Code")
+                    code = null;
+                if (code != null)
+                    code.strip(attrName);
+                super.strip(attrName);
+            }
+            protected void visitRefs(int mode, Collection refs) {
+                super.visitRefs(mode, refs);
+                if (code != null) {
+                    if (mode == VRM_CLASSIC) {
+                        refs.add(getRefString("Code"));
+                    }
+                    code.visitRefs(mode, refs);
+                }
+            }
+        }
+
+        public void trimToSize() {
+            super.trimToSize();
+            for (int isM = 0; isM <= 1; isM++) {
+                ArrayList members = (isM == 0) ? fields : methods;
+                if (members == null)  continue;
+                members.trimToSize();
+                for (Iterator i = members.iterator(); i.hasNext(); ) {
+                    Member m = (Member)i.next();
+                    m.trimToSize();
+                }
+            }
+            if (innerClasses != null) {
+                innerClasses.trimToSize();
+            }
+        }
+
+        public void strip(String attrName) {
+            if (attrName == "InnerClass")
+                innerClasses = null;
+            for (int isM = 0; isM <= 1; isM++) {
+                ArrayList members = (isM == 0) ? fields : methods;
+                if (members == null)  continue;
+                for (Iterator i = members.iterator(); i.hasNext(); ) {
+                    Member m = (Member)i.next();
+                    m.strip(attrName);
+                }
+            }
+            super.strip(attrName);
+        }
+
+        protected void visitRefs(int mode, Collection refs) {
+            if (verbose > 2)  Utils.log.fine("visitRefs "+this);
+            refs.add(thisClass);
+            refs.add(superClass);
+            for (int i = 0; i < interfaces.length; i++) {
+                refs.add(interfaces[i]);
+            }
+            for (int isM = 0; isM <= 1; isM++) {
+                ArrayList members = (isM == 0) ? fields : methods;
+                if (members == null)  continue;
+                for (Iterator i = members.iterator(); i.hasNext(); ) {
+                    Member m = (Member)i.next();
+                    boolean ok = false;
+                    try {
+                        m.visitRefs(mode, refs);
+                        ok = true;
+                    } finally {
+                        if (!ok)
+                            Utils.log.warning("Error scanning "+m);
+                    }
+                }
+            }
+            visitInnerClassRefs(mode, refs);
+            // Handle attribute list:
+            super.visitRefs(mode, refs);
+        }
+
+        protected void visitInnerClassRefs(int mode, Collection refs) {
+            Package.visitInnerClassRefs(innerClasses, mode, refs);
+        }
+
+        // Hook called by ClassReader when it's done.
+        void finishReading() {
+            trimToSize();
+            maybeChooseFileName();
+        }
+
+        public void initFile(File file) {
+            assert(this.file == null);  // set-once
+            if (file == null) {
+                // Build a trivial stub.
+                file = newStub(canonicalFileName());
+            }
+            this.file = file;
+            assert(file.isClassStub());
+            file.stubClass = this;
+            maybeChooseFileName();
+        }
+
+        public void maybeChooseFileName() {
+            if (thisClass == null) {
+                return;  // do not choose yet
+            }
+            String canonName = canonicalFileName();
+            if (file.nameString.equals("")) {
+                file.nameString = canonName;
+            }
+            if (file.nameString.equals(canonName)) {
+                // The file name is predictable.  Transmit "".
+                file.name = getRefString("");
+                return;
+            }
+            // If name has not yet been looked up, find it now.
+            if (file.name == null) {
+                file.name = getRefString(file.nameString);
+            }
+        }
+
+        public String canonicalFileName() {
+            if (thisClass == null)  return null;
+            return thisClass.stringValue() + ".class";
+        }
+
+        public java.io.File getFileName(java.io.File parent) {
+            String name = file.name.stringValue();
+            if (name.equals(""))
+                name = canonicalFileName();
+            String fname = name.replace('/', java.io.File.separatorChar);
+            return new java.io.File(parent, fname);
+        }
+        public java.io.File getFileName() {
+            return getFileName(null);
+        }
+
+        public String toString() {
+            return thisClass.stringValue();
+        }
+    }
+
+    void addClass(Class c) {
+        assert(c.getPackage() == this);
+        boolean added = classes.add(c);
+        assert(added);
+        // Make sure the class is represented in the total file order:
+        if (c.file == null)  c.initFile(null);
+        addFile(c.file);
+    }
+
+    // What non-class files are in this unit?
+    ArrayList files = new ArrayList();
+
+    public List getFiles() {
+        return files;
+    }
+
+    public List getClassStubs() {
+        ArrayList classStubs = new ArrayList(classes.size());
+        for (Iterator i = classes.iterator(); i.hasNext(); ) {
+            Class cls = (Class) i.next();
+            assert(cls.file.isClassStub());
+            classStubs.add(cls.file);
+        }
+        return classStubs;
+    }
+
+    public
+    class File implements Comparable {
+        String nameString;  // true name of this file
+        Utf8Entry name;
+        int modtime = NO_MODTIME;
+        int options = 0;  // random flag bits, such as deflate_hint
+        Class stubClass;  // if this is a stub, here's the class
+        ArrayList prepend = new ArrayList();  // list of byte[]
+        java.io.ByteArrayOutputStream append = new ByteArrayOutputStream();
+
+        File(Utf8Entry name) {
+            this.name = name;
+            this.nameString = name.stringValue();
+            // caller must fill in contents
+        }
+        File(String nameString) {
+            nameString = fixupFileName(nameString);
+            this.name = getRefString(nameString);
+            this.nameString = name.stringValue();
+        }
+
+        public boolean isDirectory() {
+            // JAR directory.  Useless.
+            return nameString.endsWith("/");
+        }
+        public boolean isClassStub() {
+            return (options & FO_IS_CLASS_STUB) != 0;
+        }
+        public Class getStubClass() {
+            assert(isClassStub());
+            assert(stubClass != null);
+            return stubClass;
+        }
+        public boolean isTrivialClassStub() {
+            return isClassStub()
+                && name.stringValue().equals("")
+                && (modtime == NO_MODTIME || modtime == default_modtime)
+                && (options &~ FO_IS_CLASS_STUB) == 0;
+        }
+
+        // The nameString is the key.  Ignore other things.
+        // (Note:  The name might be "", in the case of a trivial class stub.)
+        public boolean equals(Object o) {
+            File that = (File)o;
+            return that.nameString == this.nameString;
+        }
+        public int hashCode() {
+            return nameString.hashCode();
+        }
+        // Simple alphabetic sort.  PackageWriter uses a better comparator.
+        public int compareTo(Object o) {
+            File that = (File)o;
+            return this.nameString.compareTo(that.nameString);
+        }
+        public String toString() {
+            return nameString+"{"
+                +(isClassStub()?"*":"")
+                +(BandStructure.testBit(options,FO_DEFLATE_HINT)?"@":"")
+                +(modtime==NO_MODTIME?"":"M"+modtime)
+                +(getFileLength()==0?"":"["+getFileLength()+"]")
+                +"}";
+        }
+
+        public java.io.File getFileName() {
+            return getFileName(null);
+        }
+        public java.io.File getFileName(java.io.File parent) {
+            String name = this.nameString;
+            //if (name.startsWith("./"))  name = name.substring(2);
+            String fname = name.replace('/', java.io.File.separatorChar);
+            return new java.io.File(parent, fname);
+        }
+
+        public void addBytes(byte[] bytes) {
+            addBytes(bytes, 0, bytes.length);
+        }
+        public void addBytes(byte[] bytes, int off, int len) {
+            if (((append.size() | len) << 2) < 0) {
+                prepend.add(append.toByteArray());
+                append.reset();
+            }
+            append.write(bytes, off, len);
+        }
+        public long getFileLength() {
+            long len = 0;
+            if (prepend == null && append == null)  return 0;
+            for (Iterator i = prepend.iterator(); i.hasNext(); ) {
+                byte[] block = (byte[]) i.next();
+                len += block.length;
+            }
+            len += append.size();
+            return len;
+        }
+        public void writeTo(OutputStream out) throws IOException {
+            if (prepend == null && append == null)  return;
+            for (Iterator i = prepend.iterator(); i.hasNext(); ) {
+                byte[] block = (byte[]) i.next();
+                out.write(block);
+            }
+            append.writeTo(out);
+        }
+        public void readFrom(InputStream in) throws IOException {
+            byte[] buf = new byte[1 << 16];
+            int nr;
+            while ((nr = in.read(buf)) > 0) {
+                addBytes(buf, 0, nr);
+            }
+        }
+        public InputStream getInputStream() {
+            InputStream in = new ByteArrayInputStream(append.toByteArray());
+            if (prepend.size() == 0)  return in;
+            ArrayList isa = new ArrayList(prepend.size()+1);
+            for (Iterator i = prepend.iterator(); i.hasNext(); ) {
+                byte[] bytes = (byte[]) i.next();
+                isa.add(new ByteArrayInputStream(bytes));
+            }
+            isa.add(in);
+            return new SequenceInputStream(Collections.enumeration(isa));
+        }
+
+        protected void visitRefs(int mode, Collection refs) {
+            assert(name != null);
+            refs.add(name);
+        }
+    }
+
+    File newStub(String classFileNameString) {
+        File stub = new File(classFileNameString);
+        stub.options |= FO_IS_CLASS_STUB;
+        stub.prepend = null;
+        stub.append = null;  // do not collect data
+        return stub;
+    }
+
+    private static String fixupFileName(String name) {
+        String fname = name.replace(java.io.File.separatorChar, '/');
+        if (fname.startsWith("/")) {
+            throw new IllegalArgumentException("absolute file name "+fname);
+        }
+        return fname;
+    }
+
+    void addFile(File file) {
+        boolean added = files.add(file);
+        assert(added);
+    }
+
+    // Is there a globally declared table of inner classes?
+    ArrayList allInnerClasses = new ArrayList();
+    HashMap   allInnerClassesByThis;
+
+    public
+    List getAllInnerClasses() {
+        return allInnerClasses;
+    }
+
+    public
+    void setAllInnerClasses(Collection ics) {
+        assert(ics != allInnerClasses);
+        allInnerClasses.clear();
+        allInnerClasses.addAll(ics);
+
+        // Make an index:
+        allInnerClassesByThis = new HashMap(allInnerClasses.size());
+        for (Iterator i = allInnerClasses.iterator(); i.hasNext(); ) {
+            InnerClass ic = (InnerClass) i.next();
+            Object pic = allInnerClassesByThis.put(ic.thisClass, ic);
+            assert(pic == null);  // caller must ensure key uniqueness!
+        }
+    }
+
+    /** Return a global inner class record for the given thisClass. */
+    public
+    InnerClass getGlobalInnerClass(Entry thisClass) {
+        assert(thisClass instanceof ClassEntry);
+        return (InnerClass) allInnerClassesByThis.get(thisClass);
+    }
+
+    static
+    class InnerClass implements Comparable {
+        final ClassEntry thisClass;
+        final ClassEntry outerClass;
+        final Utf8Entry name;
+        final int flags;
+
+        // Can name and outerClass be derived from thisClass?
+        final boolean predictable;
+
+        // About 30% of inner classes are anonymous (in rt.jar).
+        // About 60% are class members; the rest are named locals.
+        // Nearly all have predictable outers and names.
+
+        InnerClass(ClassEntry thisClass, ClassEntry outerClass,
+                   Utf8Entry name, int flags) {
+            this.thisClass = thisClass;
+            this.outerClass = outerClass;
+            this.name = name;
+            this.flags = flags;
+            this.predictable = computePredictable();
+        }
+
+        private boolean computePredictable() {
+            //System.out.println("computePredictable "+outerClass+" "+this.name);
+            String[] parse = parseInnerClassName(thisClass.stringValue());
+            if (parse == null)  return false;
+            String pkgOuter = parse[0];
+            //String number = parse[1];
+            String name     = parse[2];
+            String haveName  = (this.name == null)  ? null : this.name.stringValue();
+            String haveOuter = (outerClass == null) ? null : outerClass.stringValue();
+            boolean predictable = (name == haveName && pkgOuter == haveOuter);
+            //System.out.println("computePredictable => "+predictable);
+            return predictable;
+        }
+
+        public boolean equals(Object o) {
+            if (o == null)  return false;
+            InnerClass that = (InnerClass)o;
+            return eq(this.thisClass, that.thisClass)
+                && eq(this.outerClass, that.outerClass)
+                && eq(this.name, that.name)
+                && this.flags == that.flags;
+        }
+        private static boolean eq(Object x, Object y) {
+            return (x == null)? y == null: x.equals(y);
+        }
+        public int hashCode() {
+            return thisClass.hashCode();
+        }
+        public int compareTo(Object o) {
+            InnerClass that = (InnerClass)o;
+            return this.thisClass.compareTo(that.thisClass);
+        }
+
+        protected void visitRefs(int mode, Collection refs) {
+            refs.add(thisClass);
+            if (mode == VRM_CLASSIC || !predictable) {
+                // If the name can be demangled, the package omits
+                // the products of demangling.  Otherwise, include them.
+                refs.add(outerClass);
+                refs.add(name);
+            }
+        }
+
+        public String toString() {
+            return thisClass.stringValue();
+        }
+    }
+
+    // Helper for building InnerClasses attributes.
+    static private
+    void visitInnerClassRefs(Collection innerClasses, int mode, Collection refs) {
+        if (innerClasses == null) {
+            return;  // no attribute; nothing to do
+        }
+        if (mode == VRM_CLASSIC) {
+            refs.add(getRefString("InnerClasses"));
+        }
+        if (innerClasses.size() > 0) {
+            // Count the entries themselves:
+            for (Iterator i = innerClasses.iterator(); i.hasNext(); ) {
+                InnerClass c = (InnerClass) i.next();
+                c.visitRefs(mode, refs);
+            }
+        }
+    }
+
+    static String[] parseInnerClassName(String n) {
+        //System.out.println("parseInnerClassName "+n);
+        String pkgOuter, number, name;
+        int dollar1, dollar2;  // pointers to $ in the pattern
+        // parse n = (<pkg>/)*<outer>($<number>)?($<name>)?
+        int nlen = n.length();
+        int pkglen = lastIndexOf(SLASH_MIN,  SLASH_MAX,  n, n.length()) + 1;
+        dollar2    = lastIndexOf(DOLLAR_MIN, DOLLAR_MAX, n, n.length());
+        if (dollar2 < pkglen)  return null;
+        if (isDigitString(n, dollar2+1, nlen)) {
+            // n = (<pkg>/)*<outer>$<number>
+            number = n.substring(dollar2+1, nlen);
+            name = null;
+            dollar1 = dollar2;
+        } else if ((dollar1
+                    = lastIndexOf(DOLLAR_MIN, DOLLAR_MAX, n, dollar2-1))
+                   > pkglen
+                   && isDigitString(n, dollar1+1, dollar2)) {
+            // n = (<pkg>/)*<outer>$<number>$<name>
+            number = n.substring(dollar1+1, dollar2);
+            name = n.substring(dollar2+1, nlen).intern();
+        } else {
+            // n = (<pkg>/)*<outer>$<name>
+            dollar1 = dollar2;
+            number = null;
+            name = n.substring(dollar2+1, nlen).intern();
+        }
+        if (number == null)
+            pkgOuter = n.substring(0, dollar1).intern();
+        else
+            pkgOuter = null;
+        //System.out.println("parseInnerClassName parses "+pkgOuter+" "+number+" "+name);
+        return new String[] { pkgOuter, number, name };
+    }
+
+    private static final int SLASH_MIN = '.';
+    private static final int SLASH_MAX = '/';
+    private static final int DOLLAR_MIN = 0;
+    private static final int DOLLAR_MAX = '-';
+    static {
+        assert(lastIndexOf(DOLLAR_MIN, DOLLAR_MAX, "x$$y$", 4) == 2);
+        assert(lastIndexOf(SLASH_MIN,  SLASH_MAX,  "x//y/", 4) == 2);
+    }
+
+    private static int lastIndexOf(int chMin, int chMax, String str, int pos) {
+        for (int i = pos; --i >= 0; ) {
+            int ch = str.charAt(i);
+            if (ch >= chMin && ch <= chMax) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    private static boolean isDigitString(String x, int beg, int end) {
+        if (beg == end)  return false;  // null string
+        for (int i = beg; i < end; i++) {
+            char ch = x.charAt(i);
+            if (!(ch >= '0' && ch <= '9'))  return false;
+        }
+        return true;
+    }
+
+    static String getObviousSourceFile(String className) {
+        String n = className;
+        int pkglen = lastIndexOf(SLASH_MIN,  SLASH_MAX,  n, n.length()) + 1;
+        n = n.substring(pkglen);
+        int cutoff = n.length();
+        for (;;) {
+            // Work backwards, finding all '$', '#', etc.
+            int dollar2 = lastIndexOf(DOLLAR_MIN, DOLLAR_MAX, n, cutoff-1);
+            if (dollar2 < 0)
+                break;
+            cutoff = dollar2;
+            if (cutoff == 0)
+                break;
+        }
+        String obvious = n.substring(0, cutoff)+".java";
+        return obvious;
+    }
+/*
+    static {
+        assert(getObviousSourceFile("foo").equals("foo.java"));
+        assert(getObviousSourceFile("foo/bar").equals("bar.java"));
+        assert(getObviousSourceFile("foo/bar$baz").equals("bar.java"));
+        assert(getObviousSourceFile("foo/bar#baz#1").equals("bar.java"));
+        assert(getObviousSourceFile("foo.bar.baz#1").equals("baz.java"));
+    }
+*/
+
+    static Utf8Entry getRefString(String s) {
+        return ConstantPool.getUtf8Entry(s);
+    }
+
+    static LiteralEntry getRefLiteral(Comparable s) {
+        return ConstantPool.getLiteralEntry(s);
+    }
+
+    void stripAttributeKind(String what) {
+        // what is one of { Debug, Compile, Constant, Exceptions, InnerClasses }
+        if (verbose > 0)
+            Utils.log.info("Stripping "+what.toLowerCase()+" data and attributes...");
+        if (what == "Debug") {
+            strip("SourceFile");
+            strip("LineNumberTable");
+            strip("LocalVariableTable");
+            strip("LocalVariableTypeTable");
+        }
+        if (what == "Compile") {
+            // Keep the inner classes normally.
+            // Although they have no effect on execution,
+            // the Reflection API exposes them, and JCK checks them.
+            // NO: // strip("InnerClasses");
+            strip("Deprecated");
+            strip("Synthetic");
+        }
+        if (what == "Exceptions") {
+            // Keep the exceptions normally.
+            // Although they have no effect on execution,
+            // the Reflection API exposes them, and JCK checks them.
+            strip("Exceptions");
+        }
+        if (what == "Constant") {
+            stripConstantFields();
+        }
+    }
+
+    public void trimToSize() {
+        classes.trimToSize();
+        for (Iterator i = classes.iterator(); i.hasNext(); ) {
+            Class c = (Class)i.next();
+            c.trimToSize();
+        }
+        files.trimToSize();
+    }
+
+    public void strip(String attrName) {
+        for (Iterator i = classes.iterator(); i.hasNext(); ) {
+            Class c = (Class)i.next();
+            c.strip(attrName);
+        }
+    }
+
+    public static String versionStringOf(int majver, int minver) {
+        return majver+"."+minver;
+    }
+    public static String versionStringOf(int version) {
+        return versionStringOf(version >>> 16, (char)version);
+    }
+
+    public void stripConstantFields() {
+        for (Iterator i = classes.iterator(); i.hasNext(); ) {
+            Class c = (Class) i.next();
+            for (Iterator j = c.fields.iterator(); j.hasNext(); ) {
+                Class.Field f = (Class.Field) j.next();
+                if (Modifier.isFinal(f.flags)
+                    // do not strip non-static finals:
+                    && Modifier.isStatic(f.flags)
+                    && f.getAttribute("ConstantValue") != null
+                    && !f.getName().startsWith("serial")) {
+                    if (verbose > 2) {
+                        Utils.log.fine(">> Strip "+this+" ConstantValue");
+                        j.remove();
+                    }
+                }
+            }
+        }
+    }
+
+    protected void visitRefs(int mode, Collection refs) {
+        for (Iterator i = classes.iterator(); i.hasNext(); ) {
+            Class c = (Class)i.next();
+            c.visitRefs(mode, refs);
+        }
+        if (mode != VRM_CLASSIC) {
+            for (Iterator i = files.iterator(); i.hasNext(); ) {
+                File f = (File)i.next();
+                f.visitRefs(mode, refs);
+            }
+            visitInnerClassRefs(allInnerClasses, mode, refs);
+        }
+    }
+
+    // Use this before writing the package file.
+    // It sorts files into a new order which seems likely to
+    // compress better.  It also moves classes to the end of the
+    // file order.  It also removes JAR directory entries, which
+    // are useless.
+    void reorderFiles(boolean keepClassOrder, boolean stripDirectories) {
+        // First reorder the classes, if that is allowed.
+        if (!keepClassOrder) {
+            // In one test with rt.jar, this trick gained 0.7%
+            Collections.sort(classes);
+        }
+
+        // Remove stubs from resources; maybe we'll add them on at the end,
+        // if there are some non-trivial ones.  The best case is that
+        // modtimes and options are not transmitted, and the stub files
+        // for class files do not need to be transmitted at all.
+        // Also
+        List stubs = getClassStubs();
+        for (Iterator i = files.iterator(); i.hasNext(); ) {
+            File file = (File) i.next();
+            if (file.isClassStub() ||
+                (stripDirectories && file.isDirectory())) {
+                i.remove();
+            }
+        }
+
+        // Sort the remaining non-class files.
+        // We sort them by file type.
+        // This keeps files of similar format near each other.
+        // Put class files at the end, keeping their fixed order.
+        // Be sure the JAR file's required manifest stays at the front. (4893051)
+        Collections.sort(files, new Comparator() {
+                public int compare(Object o0, Object o1) {
+                    File r0 = (File) o0;
+                    File r1 = (File) o1;
+                    // Get the file name.
+                    String f0 = r0.nameString;
+                    String f1 = r1.nameString;
+                    if (f0.equals(f1)) return 0;
+                    if (JarFile.MANIFEST_NAME.equals(f0))  return 0-1;
+                    if (JarFile.MANIFEST_NAME.equals(f1))  return 1-0;
+                    // Extract file basename.
+                    String n0 = f0.substring(1+f0.lastIndexOf('/'));
+                    String n1 = f1.substring(1+f1.lastIndexOf('/'));
+                    // Extract basename extension.
+                    String x0 = n0.substring(1+n0.lastIndexOf('.'));
+                    String x1 = n1.substring(1+n1.lastIndexOf('.'));
+                    int r;
+                    // Primary sort key is file extension.
+                    r = x0.compareTo(x1);
+                    if (r != 0)  return r;
+                    r = f0.compareTo(f1);
+                    return r;
+                }
+            });
+
+        // Add back the class stubs after sorting, before trimStubs.
+        files.addAll(stubs);
+    }
+
+    void trimStubs() {
+        // Restore enough non-trivial stubs to carry the needed class modtimes.
+        for (ListIterator i = files.listIterator(files.size()); i.hasPrevious(); ) {
+            File file = (File) i.previous();
+            if (!file.isTrivialClassStub()) {
+                if (verbose > 1)
+                    Utils.log.fine("Keeping last non-trivial "+file);
+                break;
+            }
+            if (verbose > 2)
+                Utils.log.fine("Removing trivial "+file);
+            i.remove();
+        }
+
+        if (verbose > 0) {
+            Utils.log.info("Transmitting "+files.size()+" files, including per-file data for "+getClassStubs().size()+" classes out of "+classes.size());
+        }
+    }
+
+    // Use this before writing the package file.
+    void buildGlobalConstantPool(Set requiredEntries) {
+        if (verbose > 1)
+            Utils.log.fine("Checking for unused CP entries");
+        requiredEntries.add(getRefString(""));  // uconditionally present
+        visitRefs(VRM_PACKAGE, requiredEntries);
+        ConstantPool.completeReferencesIn(requiredEntries, false);
+        if (verbose > 1)
+            Utils.log.fine("Sorting CP entries");
+        Index   cpAllU = ConstantPool.makeIndex("unsorted", requiredEntries);
+        Index[] byTagU = ConstantPool.partitionByTag(cpAllU);
+        for (int i = 0; i < ConstantPool.TAGS_IN_ORDER.length; i++) {
+            byte tag = ConstantPool.TAGS_IN_ORDER[i];
+            // Work on all entries of a given kind.
+            Index ix = byTagU[tag];
+            if (ix == null)  continue;
+            ConstantPool.sort(ix);
+            cp.initIndexByTag(tag, ix);
+            byTagU[tag] = null;  // done with it
+        }
+        for (int i = 0; i < byTagU.length; i++) {
+            assert(byTagU[i] == null);  // all consumed
+        }
+        for (int i = 0; i < ConstantPool.TAGS_IN_ORDER.length; i++) {
+            byte tag = ConstantPool.TAGS_IN_ORDER[i];
+            Index ix = cp.getIndexByTag(tag);
+            assert(ix.assertIsSorted());
+            if (verbose > 2)  Utils.log.fine(ix.dumpString());
+        }
+    }
+
+    // Use this before writing the class files.
+    void ensureAllClassFiles() {
+        HashSet fileSet = new HashSet(files);
+        for (Iterator i = classes.iterator(); i.hasNext(); ) {
+            Class cls = (Class) i.next();
+            // Add to the end of ths list:
+            if (!fileSet.contains(cls.file))
+                files.add(cls.file);
+        }
+    }
+
+    static final List noObjects = Arrays.asList(new Object[0]);
+    static final List noFields = Arrays.asList(new Class.Field[0]);
+    static final List noMethods = Arrays.asList(new Class.Method[0]);
+    static final List noInnerClasses = Arrays.asList(new InnerClass[0]);
+}