Back to index...
/*
 * Copyright (c) 2003, 2015, Oracle and/or its affiliates. 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.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.sun.java.util.jar.pack;
import com.sun.java.util.jar.pack.Attribute.Layout;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.SortedMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.Pack200;
/*
 * Implementation of the Pack provider.
 * </pre></blockquote>
 * @author John Rose
 * @author Kumar Srinivasan
 */
@SuppressWarnings({"removal"})
public class PackerImpl  extends TLGlobals implements Pack200.Packer {
    /**
     * Constructs a Packer object and sets the initial state of
     * the packer engines.
     */
    public PackerImpl() {}
    /**
     * Get the set of options for the pack and unpack engines.
     * @return A sorted association of option key strings to option values.
     */
    public SortedMap<String, String> properties() {
        return props;
    }
    //Driver routines
    /**
     * Takes a JarFile and converts into a pack-stream.
     * <p>
     * Closes its input but not its output.  (Pack200 archives are appendable.)
     * @param in a JarFile
     * @param out an OutputStream
     * @exception IOException if an error is encountered.
     */
    public synchronized void pack(JarFile in, OutputStream out) throws IOException {
        assert(Utils.currentInstance.get() == null);
        try {
            Utils.currentInstance.set(this);
            if ("0".equals(props.getProperty(Pack200.Packer.EFFORT))) {
                Utils.copyJarFile(in, out);
            } else {
                (new DoPack()).run(in, out);
            }
        } finally {
            Utils.currentInstance.set(null);
            in.close();
        }
    }
    /**
     * Takes a JarInputStream and converts into a pack-stream.
     * <p>
     * Closes its input but not its output.  (Pack200 archives are appendable.)
     * <p>
     * The modification time and deflation hint attributes are not available,
     * for the jar-manifest file and the directory containing the file.
     *
     * @see #MODIFICATION_TIME
     * @see #DEFLATION_HINT
     * @param in a JarInputStream
     * @param out an OutputStream
     * @exception IOException if an error is encountered.
     */
    public synchronized void pack(JarInputStream in, OutputStream out) throws IOException {
        assert(Utils.currentInstance.get() == null);
        try {
            Utils.currentInstance.set(this);
            if ("0".equals(props.getProperty(Pack200.Packer.EFFORT))) {
                Utils.copyJarFile(in, out);
            } else {
                (new DoPack()).run(in, out);
            }
        } finally {
            Utils.currentInstance.set(null);
            in.close();
        }
    }
    // All the worker bees.....
    // The packer worker.
    private class DoPack {
        final int verbose = props.getInteger(Utils.DEBUG_VERBOSE);
        {
            props.setInteger(Pack200.Packer.PROGRESS, 0);
            if (verbose > 0) Utils.log.info(props.toString());
        }
        // Here's where the bits are collected before getting packed, we also
        // initialize the version numbers now.
        final Package pkg = new Package(Package.Version.makeVersion(props, "min.class"),
                                        Package.Version.makeVersion(props, "max.class"),
                                        Package.Version.makeVersion(props, "package"));
        final String unknownAttrCommand;
        {
            String uaMode = props.getProperty(Pack200.Packer.UNKNOWN_ATTRIBUTE, Pack200.Packer.PASS);
            if (!(Pack200.Packer.STRIP.equals(uaMode) ||
                  Pack200.Packer.PASS.equals(uaMode) ||
                  Pack200.Packer.ERROR.equals(uaMode))) {
                throw new RuntimeException("Bad option: " + Pack200.Packer.UNKNOWN_ATTRIBUTE + " = " + uaMode);
            }
            unknownAttrCommand = uaMode.intern();
        }
        final String classFormatCommand;
        {
            String fmtMode = props.getProperty(Utils.CLASS_FORMAT_ERROR, Pack200.Packer.PASS);
            if (!(Pack200.Packer.PASS.equals(fmtMode) ||
                  Pack200.Packer.ERROR.equals(fmtMode))) {
                throw new RuntimeException("Bad option: " + Utils.CLASS_FORMAT_ERROR + " = " + fmtMode);
            }
            classFormatCommand = fmtMode.intern();
        }
        final Map<Attribute.Layout, Attribute> attrDefs;
        final Map<Attribute.Layout, String> attrCommands;
        {
            Map<Attribute.Layout, Attribute> lattrDefs   = new HashMap<>();
            Map<Attribute.Layout, String>  lattrCommands = new HashMap<>();
            String[] keys = {
                Pack200.Packer.CLASS_ATTRIBUTE_PFX,
                Pack200.Packer.FIELD_ATTRIBUTE_PFX,
                Pack200.Packer.METHOD_ATTRIBUTE_PFX,
                Pack200.Packer.CODE_ATTRIBUTE_PFX
            };
            int[] ctypes = {
                Constants.ATTR_CONTEXT_CLASS,
                Constants.ATTR_CONTEXT_FIELD,
                Constants.ATTR_CONTEXT_METHOD,
                Constants.ATTR_CONTEXT_CODE
            };
            for (int i = 0; i < ctypes.length; i++) {
                String pfx = keys[i];
                Map<String, String> map = props.prefixMap(pfx);
                for (String key : map.keySet()) {
                    assert(key.startsWith(pfx));
                    String name = key.substring(pfx.length());
                    String layout = props.getProperty(key);
                    Layout lkey = Attribute.keyForLookup(ctypes[i], name);
                    if (Pack200.Packer.STRIP.equals(layout) ||
                        Pack200.Packer.PASS.equals(layout) ||
                        Pack200.Packer.ERROR.equals(layout)) {
                        lattrCommands.put(lkey, layout.intern());
                    } else {
                        Attribute.define(lattrDefs, ctypes[i], name, layout);
                        if (verbose > 1) {
                            Utils.log.fine("Added layout for "+Constants.ATTR_CONTEXT_NAME[i]+" attribute "+name+" = "+layout);
                        }
                        assert(lattrDefs.containsKey(lkey));
                    }
                }
            }
            this.attrDefs = (lattrDefs.isEmpty()) ? null : lattrDefs;
            this.attrCommands = (lattrCommands.isEmpty()) ? null : lattrCommands;
        }
        final boolean keepFileOrder
            = props.getBoolean(Pack200.Packer.KEEP_FILE_ORDER);
        final boolean keepClassOrder
            = props.getBoolean(Utils.PACK_KEEP_CLASS_ORDER);
        final boolean keepModtime
            = Pack200.Packer.KEEP.equals(props.getProperty(Pack200.Packer.MODIFICATION_TIME));
        final boolean latestModtime
            = Pack200.Packer.LATEST.equals(props.getProperty(Pack200.Packer.MODIFICATION_TIME));
        final boolean keepDeflateHint
            = Pack200.Packer.KEEP.equals(props.getProperty(Pack200.Packer.DEFLATE_HINT));
        {
            if (!keepModtime && !latestModtime) {
                int modtime = props.getTime(Pack200.Packer.MODIFICATION_TIME);
                if (modtime != Constants.NO_MODTIME) {
                    pkg.default_modtime = modtime;
                }
            }
            if (!keepDeflateHint) {
                boolean deflate_hint = props.getBoolean(Pack200.Packer.DEFLATE_HINT);
                if (deflate_hint) {
                    pkg.default_options |= Constants.AO_DEFLATE_HINT;
                }
            }
        }
        long totalOutputSize = 0;
        int  segmentCount = 0;
        long segmentTotalSize = 0;
        long segmentSize = 0;  // running counter
        final long segmentLimit;
        {
            long limit;
            if (props.getProperty(Pack200.Packer.SEGMENT_LIMIT, "").equals(""))
                limit = -1;
            else
                limit = props.getLong(Pack200.Packer.SEGMENT_LIMIT);
            limit = Math.min(Integer.MAX_VALUE, limit);
            limit = Math.max(-1, limit);
            if (limit == -1)
                limit = Long.MAX_VALUE;
            segmentLimit = limit;
        }
        final List<String> passFiles;  // parsed pack.pass.file options
        {
            // Which class files will be passed through?
            passFiles = props.getProperties(Pack200.Packer.PASS_FILE_PFX);
            for (ListIterator<String> i = passFiles.listIterator(); i.hasNext(); ) {
                String file = i.next();
                if (file == null) { i.remove(); continue; }
                file = Utils.getJarEntryName(file);  // normalize '\\' to '/'
                if (file.endsWith("/"))
                    file = file.substring(0, file.length()-1);
                i.set(file);
            }
            if (verbose > 0) Utils.log.info("passFiles = " + passFiles);
        }
        {
            // Hook for testing:  Forces use of special archive modes.
            int opt = props.getInteger(Utils.COM_PREFIX+"archive.options");
            if (opt != 0)
                pkg.default_options |= opt;
        }
        // (Done collecting options from props.)
        // Get a new package, based on the old one.
        private void makeNextPackage() {
            pkg.reset();
        }
        final class InFile {
            final String name;
            final JarFile jf;
            final JarEntry je;
            final File f;
            int modtime = Constants.NO_MODTIME;
            int options;
            InFile(String name) {
                this.name = Utils.getJarEntryName(name);
                this.f = new File(name);
                this.jf = null;
                this.je = null;
                int timeSecs = getModtime(f.lastModified());
                if (keepModtime && timeSecs != Constants.NO_MODTIME) {
                    this.modtime = timeSecs;
                } else if (latestModtime && timeSecs > pkg.default_modtime) {
                    pkg.default_modtime = timeSecs;
                }
            }
            InFile(JarFile jf, JarEntry je) {
                this.name = Utils.getJarEntryName(je.getName());
                this.f = null;
                this.jf = jf;
                this.je = je;
                int timeSecs = (int) je.getTimeLocal()
                        .atOffset(ZoneOffset.UTC)
                        .toEpochSecond();
                if (keepModtime && timeSecs != Constants.NO_MODTIME) {
                     this.modtime = timeSecs;
                } else if (latestModtime && timeSecs > pkg.default_modtime) {
                    pkg.default_modtime = timeSecs;
                }
                if (keepDeflateHint && je.getMethod() == JarEntry.DEFLATED) {
                    options |= Constants.FO_DEFLATE_HINT;
                }
            }
            InFile(JarEntry je) {
                this(null, je);
            }
            boolean isClassFile() {
                if (!name.endsWith(".class") || name.endsWith("module-info.class")) {
                    return false;
                }
                for (String prefix = name;;) {
                    if (passFiles.contains(prefix)) {
                        return false;
                    }
                    int chop = prefix.lastIndexOf('/');
                    if (chop < 0) {
                        break;
                    }
                    prefix = prefix.substring(0, chop);
                }
                return true;
            }
            boolean isMetaInfFile() {
                return name.startsWith("/" + Utils.METAINF)
                        || name.startsWith(Utils.METAINF);
            }
            boolean mustProcess() {
                return !isMetaInfFile() && isClassFile();
            }
            long getInputLength() {
                long len = (je != null)? je.getSize(): f.length();
                assert(len >= 0) : this+".len="+len;
                // Bump size by pathname length and modtime/def-hint bytes.
                return Math.max(0, len) + name.length() + 5;
            }
            int getModtime(long timeMillis) {
                // Convert milliseconds to seconds.
                long seconds = (timeMillis+500) / 1000;
                if ((int)seconds == seconds) {
                    return (int)seconds;
                } else {
                    Utils.log.warning("overflow in modtime for "+f);
                    return Constants.NO_MODTIME;
                }
            }
            void copyTo(Package.File file) {
                if (modtime != Constants.NO_MODTIME)
                    file.modtime = modtime;
                file.options |= options;
            }
            InputStream getInputStream() throws IOException {
                if (jf != null)
                    return jf.getInputStream(je);
                else
                    return new FileInputStream(f);
            }
            public String toString() {
                return name;
            }
        }
        private int nread = 0;  // used only if (verbose > 0)
        private void noteRead(InFile f) {
            nread++;
            if (verbose > 2)
                Utils.log.fine("...read "+f.name);
            if (verbose > 0 && (nread % 1000) == 0)
                Utils.log.info("Have read "+nread+" files...");
        }
        void run(JarInputStream in, OutputStream out) throws IOException {
            // First thing we do is get the manifest, as JIS does
            // not provide the Manifest as an entry.
            if (in.getManifest() != null) {
                ByteArrayOutputStream tmp = new ByteArrayOutputStream();
                in.getManifest().write(tmp);
                InputStream tmpIn = new ByteArrayInputStream(tmp.toByteArray());
                pkg.addFile(readFile(JarFile.MANIFEST_NAME, tmpIn));
            }
            for (JarEntry je; (je = in.getNextJarEntry()) != null; ) {
                InFile inFile = new InFile(je);
                String name = inFile.name;
                Package.File bits = readFile(name, in);
                Package.File file = null;
                // (5078608) : discount the resource files in META-INF
                // from segment computation.
                long inflen = (inFile.isMetaInfFile())
                              ? 0L
                              : inFile.getInputLength();
                if ((segmentSize += inflen) > segmentLimit) {
                    segmentSize -= inflen;
                    int nextCount = -1;  // don't know; it's a stream
                    flushPartial(out, nextCount);
                }
                if (verbose > 1) {
                    Utils.log.fine("Reading " + name);
                }
                assert(je.isDirectory() == name.endsWith("/"));
                if (inFile.mustProcess()) {
                    file = readClass(name, bits.getInputStream());
                }
                if (file == null) {
                    file = bits;
                    pkg.addFile(file);
                }
                inFile.copyTo(file);
                noteRead(inFile);
            }
            flushAll(out);
        }
        void run(JarFile in, OutputStream out) throws IOException {
            List<InFile> inFiles = scanJar(in);
            if (verbose > 0)
                Utils.log.info("Reading " + inFiles.size() + " files...");
            int numDone = 0;
            for (InFile inFile : inFiles) {
                String name      = inFile.name;
                // (5078608) : discount the resource files completely from segmenting
                long inflen = (inFile.isMetaInfFile())
                               ? 0L
                               : inFile.getInputLength() ;
                if ((segmentSize += inflen) > segmentLimit) {
                    segmentSize -= inflen;
                    // Estimate number of remaining segments:
                    float filesDone = numDone+1;
                    float segsDone  = segmentCount+1;
                    float filesToDo = inFiles.size() - filesDone;
                    float segsToDo  = filesToDo * (segsDone/filesDone);
                    if (verbose > 1)
                        Utils.log.fine("Estimated segments to do: "+segsToDo);
                    flushPartial(out, (int) Math.ceil(segsToDo));
                }
                InputStream strm = inFile.getInputStream();
                if (verbose > 1)
                    Utils.log.fine("Reading " + name);
                Package.File file = null;
                if (inFile.mustProcess()) {
                    file = readClass(name, strm);
                    if (file == null) {
                        strm.close();
                        strm = inFile.getInputStream();
                    }
                }
                if (file == null) {
                    file = readFile(name, strm);
                    pkg.addFile(file);
                }
                inFile.copyTo(file);
                strm.close();  // tidy up
                noteRead(inFile);
                numDone += 1;
            }
            flushAll(out);
        }
        Package.File readClass(String fname, InputStream in) throws IOException {
            Package.Class cls = pkg.new Class(fname);
            in = new BufferedInputStream(in);
            ClassReader reader = new ClassReader(cls, in);
            reader.setAttrDefs(attrDefs);
            reader.setAttrCommands(attrCommands);
            reader.unknownAttrCommand = unknownAttrCommand;
            try {
                reader.read();
            } catch (IOException ioe) {
                String message = "Passing class file uncompressed due to";
                if (ioe instanceof Attribute.FormatException) {
                    Attribute.FormatException ee = (Attribute.FormatException) ioe;
                    // He passed up the category to us in layout.
                    if (ee.layout.equals(Pack200.Packer.PASS)) {
                        Utils.log.info(ee.toString());
                        Utils.log.warning(message + " unrecognized attribute: " +
                                fname);
                        return null;
                    }
                } else if (ioe instanceof ClassReader.ClassFormatException) {
                    ClassReader.ClassFormatException ce = (ClassReader.ClassFormatException) ioe;
                    if (classFormatCommand.equals(Pack200.Packer.PASS)) {
                        Utils.log.info(ce.toString());
                        Utils.log.warning(message + " unknown class format: " +
                                fname);
                        return null;
                    }
                }
                // Otherwise, it must be an error.
                throw ioe;
            }
            pkg.addClass(cls);
            return cls.file;
        }
        // Read raw data.
        Package.File readFile(String fname, InputStream in) throws IOException {
            Package.File file = pkg.new File(fname);
            file.readFrom(in);
            if (file.isDirectory() && file.getFileLength() != 0)
                throw new IllegalArgumentException("Non-empty directory: "+file.getFileName());
            return file;
        }
        void flushPartial(OutputStream out, int nextCount) throws IOException {
            if (pkg.files.isEmpty() && pkg.classes.isEmpty()) {
                return;  // do not flush an empty segment
            }
            flushPackage(out, Math.max(1, nextCount));
            props.setInteger(Pack200.Packer.PROGRESS, 25);
            // In case there will be another segment:
            makeNextPackage();
            segmentCount += 1;
            segmentTotalSize += segmentSize;
            segmentSize = 0;
        }
        void flushAll(OutputStream out) throws IOException {
            props.setInteger(Pack200.Packer.PROGRESS, 50);
            flushPackage(out, 0);
            out.flush();
            props.setInteger(Pack200.Packer.PROGRESS, 100);
            segmentCount += 1;
            segmentTotalSize += segmentSize;
            segmentSize = 0;
            if (verbose > 0 && segmentCount > 1) {
                Utils.log.info("Transmitted "
                                 +segmentTotalSize+" input bytes in "
                                 +segmentCount+" segments totaling "
                                 +totalOutputSize+" bytes");
            }
        }
        /** Write all information in the current package segment
         *  to the output stream.
         */
        void flushPackage(OutputStream out, int nextCount) throws IOException {
            int nfiles = pkg.files.size();
            if (!keepFileOrder) {
                // Keeping the order of classes costs about 1%
                // Keeping the order of all files costs something more.
                if (verbose > 1)  Utils.log.fine("Reordering files.");
                boolean stripDirectories = true;
                pkg.reorderFiles(keepClassOrder, stripDirectories);
            } else {
                // Package builder must have created a stub for each class.
                assert(pkg.files.containsAll(pkg.getClassStubs()));
                // Order of stubs in file list must agree with classes.
                List<Package.File> res = pkg.files;
                assert((res = new ArrayList<>(pkg.files))
                       .retainAll(pkg.getClassStubs()) || true);
                assert(res.equals(pkg.getClassStubs()));
            }
            pkg.trimStubs();
            // Do some stripping, maybe.
            if (props.getBoolean(Utils.COM_PREFIX+"strip.debug"))        pkg.stripAttributeKind("Debug");
            if (props.getBoolean(Utils.COM_PREFIX+"strip.compile"))      pkg.stripAttributeKind("Compile");
            if (props.getBoolean(Utils.COM_PREFIX+"strip.constants"))    pkg.stripAttributeKind("Constant");
            if (props.getBoolean(Utils.COM_PREFIX+"strip.exceptions"))   pkg.stripAttributeKind("Exceptions");
            if (props.getBoolean(Utils.COM_PREFIX+"strip.innerclasses")) pkg.stripAttributeKind("InnerClasses");
            PackageWriter pw = new PackageWriter(pkg, out);
            pw.archiveNextCount = nextCount;
            pw.write();
            out.flush();
            if (verbose > 0) {
                long outSize = pw.archiveSize0+pw.archiveSize1;
                totalOutputSize += outSize;
                long inSize = segmentSize;
                Utils.log.info("Transmitted "
                                 +nfiles+" files of "
                                 +inSize+" input bytes in a segment of "
                                 +outSize+" bytes");
            }
        }
        List<InFile> scanJar(JarFile jf) throws IOException {
            // Collect jar entries, preserving order.
            List<InFile> inFiles = new ArrayList<>();
            try {
                for (JarEntry je : Collections.list(jf.entries())) {
                    InFile inFile = new InFile(jf, je);
                    assert(je.isDirectory() == inFile.name.endsWith("/"));
                    inFiles.add(inFile);
                }
            } catch (IllegalStateException ise) {
                throw new IOException(ise.getLocalizedMessage(), ise);
            }
            return inFiles;
        }
    }
}
Back to index...