1/*
2 * Copyright (c) 2003, 2015, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package com.sun.java.util.jar.pack;
27
28import com.sun.java.util.jar.pack.Attribute.Layout;
29import java.io.BufferedInputStream;
30import java.io.ByteArrayInputStream;
31import java.io.ByteArrayOutputStream;
32import java.io.File;
33import java.io.FileInputStream;
34import java.io.IOException;
35import java.io.InputStream;
36import java.io.OutputStream;
37import java.time.ZoneOffset;
38import java.util.ArrayList;
39import java.util.Collections;
40import java.util.HashMap;
41import java.util.List;
42import java.util.ListIterator;
43import java.util.Map;
44import java.util.SortedMap;
45import java.util.jar.JarEntry;
46import java.util.jar.JarFile;
47import java.util.jar.JarInputStream;
48import java.util.jar.Pack200;
49
50
51/*
52 * Implementation of the Pack provider.
53 * </pre></blockquote>
54 * @author John Rose
55 * @author Kumar Srinivasan
56 */
57
58
59public class PackerImpl  extends TLGlobals implements Pack200.Packer {
60
61    /**
62     * Constructs a Packer object and sets the initial state of
63     * the packer engines.
64     */
65    public PackerImpl() {}
66
67    /**
68     * Get the set of options for the pack and unpack engines.
69     * @return A sorted association of option key strings to option values.
70     */
71    public SortedMap<String, String> properties() {
72        return props;
73    }
74
75    //Driver routines
76
77    /**
78     * Takes a JarFile and converts into a pack-stream.
79     * <p>
80     * Closes its input but not its output.  (Pack200 archives are appendable.)
81     * @param in a JarFile
82     * @param out an OutputStream
83     * @exception IOException if an error is encountered.
84     */
85    public synchronized void pack(JarFile in, OutputStream out) throws IOException {
86        assert(Utils.currentInstance.get() == null);
87        try {
88            Utils.currentInstance.set(this);
89            if ("0".equals(props.getProperty(Pack200.Packer.EFFORT))) {
90                Utils.copyJarFile(in, out);
91            } else {
92                (new DoPack()).run(in, out);
93            }
94        } finally {
95            Utils.currentInstance.set(null);
96            in.close();
97        }
98    }
99
100    /**
101     * Takes a JarInputStream and converts into a pack-stream.
102     * <p>
103     * Closes its input but not its output.  (Pack200 archives are appendable.)
104     * <p>
105     * The modification time and deflation hint attributes are not available,
106     * for the jar-manifest file and the directory containing the file.
107     *
108     * @see #MODIFICATION_TIME
109     * @see #DEFLATION_HINT
110     * @param in a JarInputStream
111     * @param out an OutputStream
112     * @exception IOException if an error is encountered.
113     */
114    public synchronized void pack(JarInputStream in, OutputStream out) throws IOException {
115        assert(Utils.currentInstance.get() == null);
116        try {
117            Utils.currentInstance.set(this);
118            if ("0".equals(props.getProperty(Pack200.Packer.EFFORT))) {
119                Utils.copyJarFile(in, out);
120            } else {
121                (new DoPack()).run(in, out);
122            }
123        } finally {
124            Utils.currentInstance.set(null);
125            in.close();
126        }
127    }
128
129    // All the worker bees.....
130    // The packer worker.
131    private class DoPack {
132        final int verbose = props.getInteger(Utils.DEBUG_VERBOSE);
133
134        {
135            props.setInteger(Pack200.Packer.PROGRESS, 0);
136            if (verbose > 0) Utils.log.info(props.toString());
137        }
138
139        // Here's where the bits are collected before getting packed, we also
140        // initialize the version numbers now.
141        final Package pkg = new Package(Package.Version.makeVersion(props, "min.class"),
142                                        Package.Version.makeVersion(props, "max.class"),
143                                        Package.Version.makeVersion(props, "package"));
144
145        final String unknownAttrCommand;
146        {
147            String uaMode = props.getProperty(Pack200.Packer.UNKNOWN_ATTRIBUTE, Pack200.Packer.PASS);
148            if (!(Pack200.Packer.STRIP.equals(uaMode) ||
149                  Pack200.Packer.PASS.equals(uaMode) ||
150                  Pack200.Packer.ERROR.equals(uaMode))) {
151                throw new RuntimeException("Bad option: " + Pack200.Packer.UNKNOWN_ATTRIBUTE + " = " + uaMode);
152            }
153            unknownAttrCommand = uaMode.intern();
154        }
155        final String classFormatCommand;
156        {
157            String fmtMode = props.getProperty(Utils.CLASS_FORMAT_ERROR, Pack200.Packer.PASS);
158            if (!(Pack200.Packer.PASS.equals(fmtMode) ||
159                  Pack200.Packer.ERROR.equals(fmtMode))) {
160                throw new RuntimeException("Bad option: " + Utils.CLASS_FORMAT_ERROR + " = " + fmtMode);
161            }
162            classFormatCommand = fmtMode.intern();
163        }
164
165        final Map<Attribute.Layout, Attribute> attrDefs;
166        final Map<Attribute.Layout, String> attrCommands;
167        {
168            Map<Attribute.Layout, Attribute> lattrDefs   = new HashMap<>();
169            Map<Attribute.Layout, String>  lattrCommands = new HashMap<>();
170            String[] keys = {
171                Pack200.Packer.CLASS_ATTRIBUTE_PFX,
172                Pack200.Packer.FIELD_ATTRIBUTE_PFX,
173                Pack200.Packer.METHOD_ATTRIBUTE_PFX,
174                Pack200.Packer.CODE_ATTRIBUTE_PFX
175            };
176            int[] ctypes = {
177                Constants.ATTR_CONTEXT_CLASS,
178                Constants.ATTR_CONTEXT_FIELD,
179                Constants.ATTR_CONTEXT_METHOD,
180                Constants.ATTR_CONTEXT_CODE
181            };
182            for (int i = 0; i < ctypes.length; i++) {
183                String pfx = keys[i];
184                Map<String, String> map = props.prefixMap(pfx);
185                for (String key : map.keySet()) {
186                    assert(key.startsWith(pfx));
187                    String name = key.substring(pfx.length());
188                    String layout = props.getProperty(key);
189                    Layout lkey = Attribute.keyForLookup(ctypes[i], name);
190                    if (Pack200.Packer.STRIP.equals(layout) ||
191                        Pack200.Packer.PASS.equals(layout) ||
192                        Pack200.Packer.ERROR.equals(layout)) {
193                        lattrCommands.put(lkey, layout.intern());
194                    } else {
195                        Attribute.define(lattrDefs, ctypes[i], name, layout);
196                        if (verbose > 1) {
197                            Utils.log.fine("Added layout for "+Constants.ATTR_CONTEXT_NAME[i]+" attribute "+name+" = "+layout);
198                        }
199                        assert(lattrDefs.containsKey(lkey));
200                    }
201                }
202            }
203            this.attrDefs = (lattrDefs.isEmpty()) ? null : lattrDefs;
204            this.attrCommands = (lattrCommands.isEmpty()) ? null : lattrCommands;
205        }
206
207        final boolean keepFileOrder
208            = props.getBoolean(Pack200.Packer.KEEP_FILE_ORDER);
209        final boolean keepClassOrder
210            = props.getBoolean(Utils.PACK_KEEP_CLASS_ORDER);
211
212        final boolean keepModtime
213            = Pack200.Packer.KEEP.equals(props.getProperty(Pack200.Packer.MODIFICATION_TIME));
214        final boolean latestModtime
215            = Pack200.Packer.LATEST.equals(props.getProperty(Pack200.Packer.MODIFICATION_TIME));
216        final boolean keepDeflateHint
217            = Pack200.Packer.KEEP.equals(props.getProperty(Pack200.Packer.DEFLATE_HINT));
218        {
219            if (!keepModtime && !latestModtime) {
220                int modtime = props.getTime(Pack200.Packer.MODIFICATION_TIME);
221                if (modtime != Constants.NO_MODTIME) {
222                    pkg.default_modtime = modtime;
223                }
224            }
225            if (!keepDeflateHint) {
226                boolean deflate_hint = props.getBoolean(Pack200.Packer.DEFLATE_HINT);
227                if (deflate_hint) {
228                    pkg.default_options |= Constants.AO_DEFLATE_HINT;
229                }
230            }
231        }
232
233        long totalOutputSize = 0;
234        int  segmentCount = 0;
235        long segmentTotalSize = 0;
236        long segmentSize = 0;  // running counter
237        final long segmentLimit;
238        {
239            long limit;
240            if (props.getProperty(Pack200.Packer.SEGMENT_LIMIT, "").equals(""))
241                limit = -1;
242            else
243                limit = props.getLong(Pack200.Packer.SEGMENT_LIMIT);
244            limit = Math.min(Integer.MAX_VALUE, limit);
245            limit = Math.max(-1, limit);
246            if (limit == -1)
247                limit = Long.MAX_VALUE;
248            segmentLimit = limit;
249        }
250
251        final List<String> passFiles;  // parsed pack.pass.file options
252        {
253            // Which class files will be passed through?
254            passFiles = props.getProperties(Pack200.Packer.PASS_FILE_PFX);
255            for (ListIterator<String> i = passFiles.listIterator(); i.hasNext(); ) {
256                String file = i.next();
257                if (file == null) { i.remove(); continue; }
258                file = Utils.getJarEntryName(file);  // normalize '\\' to '/'
259                if (file.endsWith("/"))
260                    file = file.substring(0, file.length()-1);
261                i.set(file);
262            }
263            if (verbose > 0) Utils.log.info("passFiles = " + passFiles);
264        }
265
266        {
267            // Hook for testing:  Forces use of special archive modes.
268            int opt = props.getInteger(Utils.COM_PREFIX+"archive.options");
269            if (opt != 0)
270                pkg.default_options |= opt;
271        }
272
273        // (Done collecting options from props.)
274
275        // Get a new package, based on the old one.
276        private void makeNextPackage() {
277            pkg.reset();
278        }
279
280        final class InFile {
281            final String name;
282            final JarFile jf;
283            final JarEntry je;
284            final File f;
285            int modtime = Constants.NO_MODTIME;
286            int options;
287            InFile(String name) {
288                this.name = Utils.getJarEntryName(name);
289                this.f = new File(name);
290                this.jf = null;
291                this.je = null;
292                int timeSecs = getModtime(f.lastModified());
293                if (keepModtime && timeSecs != Constants.NO_MODTIME) {
294                    this.modtime = timeSecs;
295                } else if (latestModtime && timeSecs > pkg.default_modtime) {
296                    pkg.default_modtime = timeSecs;
297                }
298            }
299            InFile(JarFile jf, JarEntry je) {
300                this.name = Utils.getJarEntryName(je.getName());
301                this.f = null;
302                this.jf = jf;
303                this.je = je;
304                int timeSecs = (int) je.getTimeLocal()
305                        .atOffset(ZoneOffset.UTC)
306                        .toEpochSecond();
307                if (keepModtime && timeSecs != Constants.NO_MODTIME) {
308                     this.modtime = timeSecs;
309                } else if (latestModtime && timeSecs > pkg.default_modtime) {
310                    pkg.default_modtime = timeSecs;
311                }
312                if (keepDeflateHint && je.getMethod() == JarEntry.DEFLATED) {
313                    options |= Constants.FO_DEFLATE_HINT;
314                }
315            }
316            InFile(JarEntry je) {
317                this(null, je);
318            }
319            boolean isClassFile() {
320                if (!name.endsWith(".class") || name.endsWith("module-info.class")) {
321                    return false;
322                }
323                for (String prefix = name;;) {
324                    if (passFiles.contains(prefix)) {
325                        return false;
326                    }
327                    int chop = prefix.lastIndexOf('/');
328                    if (chop < 0) {
329                        break;
330                    }
331                    prefix = prefix.substring(0, chop);
332                }
333                return true;
334            }
335            boolean isMetaInfFile() {
336                return name.startsWith("/" + Utils.METAINF)
337                        || name.startsWith(Utils.METAINF);
338            }
339            boolean mustProcess() {
340                return !isMetaInfFile() && isClassFile();
341            }
342            long getInputLength() {
343                long len = (je != null)? je.getSize(): f.length();
344                assert(len >= 0) : this+".len="+len;
345                // Bump size by pathname length and modtime/def-hint bytes.
346                return Math.max(0, len) + name.length() + 5;
347            }
348            int getModtime(long timeMillis) {
349                // Convert milliseconds to seconds.
350                long seconds = (timeMillis+500) / 1000;
351                if ((int)seconds == seconds) {
352                    return (int)seconds;
353                } else {
354                    Utils.log.warning("overflow in modtime for "+f);
355                    return Constants.NO_MODTIME;
356                }
357            }
358            void copyTo(Package.File file) {
359                if (modtime != Constants.NO_MODTIME)
360                    file.modtime = modtime;
361                file.options |= options;
362            }
363            InputStream getInputStream() throws IOException {
364                if (jf != null)
365                    return jf.getInputStream(je);
366                else
367                    return new FileInputStream(f);
368            }
369
370            public String toString() {
371                return name;
372            }
373        }
374
375        private int nread = 0;  // used only if (verbose > 0)
376        private void noteRead(InFile f) {
377            nread++;
378            if (verbose > 2)
379                Utils.log.fine("...read "+f.name);
380            if (verbose > 0 && (nread % 1000) == 0)
381                Utils.log.info("Have read "+nread+" files...");
382        }
383
384        void run(JarInputStream in, OutputStream out) throws IOException {
385            // First thing we do is get the manifest, as JIS does
386            // not provide the Manifest as an entry.
387            if (in.getManifest() != null) {
388                ByteArrayOutputStream tmp = new ByteArrayOutputStream();
389                in.getManifest().write(tmp);
390                InputStream tmpIn = new ByteArrayInputStream(tmp.toByteArray());
391                pkg.addFile(readFile(JarFile.MANIFEST_NAME, tmpIn));
392            }
393            for (JarEntry je; (je = in.getNextJarEntry()) != null; ) {
394                InFile inFile = new InFile(je);
395
396                String name = inFile.name;
397                Package.File bits = readFile(name, in);
398                Package.File file = null;
399                // (5078608) : discount the resource files in META-INF
400                // from segment computation.
401                long inflen = (inFile.isMetaInfFile())
402                              ? 0L
403                              : inFile.getInputLength();
404
405                if ((segmentSize += inflen) > segmentLimit) {
406                    segmentSize -= inflen;
407                    int nextCount = -1;  // don't know; it's a stream
408                    flushPartial(out, nextCount);
409                }
410                if (verbose > 1) {
411                    Utils.log.fine("Reading " + name);
412                }
413
414                assert(je.isDirectory() == name.endsWith("/"));
415
416                if (inFile.mustProcess()) {
417                    file = readClass(name, bits.getInputStream());
418                }
419                if (file == null) {
420                    file = bits;
421                    pkg.addFile(file);
422                }
423                inFile.copyTo(file);
424                noteRead(inFile);
425            }
426            flushAll(out);
427        }
428
429        void run(JarFile in, OutputStream out) throws IOException {
430            List<InFile> inFiles = scanJar(in);
431
432            if (verbose > 0)
433                Utils.log.info("Reading " + inFiles.size() + " files...");
434
435            int numDone = 0;
436            for (InFile inFile : inFiles) {
437                String name      = inFile.name;
438                // (5078608) : discount the resource files completely from segmenting
439                long inflen = (inFile.isMetaInfFile())
440                               ? 0L
441                               : inFile.getInputLength() ;
442                if ((segmentSize += inflen) > segmentLimit) {
443                    segmentSize -= inflen;
444                    // Estimate number of remaining segments:
445                    float filesDone = numDone+1;
446                    float segsDone  = segmentCount+1;
447                    float filesToDo = inFiles.size() - filesDone;
448                    float segsToDo  = filesToDo * (segsDone/filesDone);
449                    if (verbose > 1)
450                        Utils.log.fine("Estimated segments to do: "+segsToDo);
451                    flushPartial(out, (int) Math.ceil(segsToDo));
452                }
453                InputStream strm = inFile.getInputStream();
454                if (verbose > 1)
455                    Utils.log.fine("Reading " + name);
456                Package.File file = null;
457                if (inFile.mustProcess()) {
458                    file = readClass(name, strm);
459                    if (file == null) {
460                        strm.close();
461                        strm = inFile.getInputStream();
462                    }
463                }
464                if (file == null) {
465                    file = readFile(name, strm);
466                    pkg.addFile(file);
467                }
468                inFile.copyTo(file);
469                strm.close();  // tidy up
470                noteRead(inFile);
471                numDone += 1;
472            }
473            flushAll(out);
474        }
475
476        Package.File readClass(String fname, InputStream in) throws IOException {
477            Package.Class cls = pkg.new Class(fname);
478            in = new BufferedInputStream(in);
479            ClassReader reader = new ClassReader(cls, in);
480            reader.setAttrDefs(attrDefs);
481            reader.setAttrCommands(attrCommands);
482            reader.unknownAttrCommand = unknownAttrCommand;
483            try {
484                reader.read();
485            } catch (IOException ioe) {
486                String message = "Passing class file uncompressed due to";
487                if (ioe instanceof Attribute.FormatException) {
488                    Attribute.FormatException ee = (Attribute.FormatException) ioe;
489                    // He passed up the category to us in layout.
490                    if (ee.layout.equals(Pack200.Packer.PASS)) {
491                        Utils.log.info(ee.toString());
492                        Utils.log.warning(message + " unrecognized attribute: " +
493                                fname);
494                        return null;
495                    }
496                } else if (ioe instanceof ClassReader.ClassFormatException) {
497                    ClassReader.ClassFormatException ce = (ClassReader.ClassFormatException) ioe;
498                    if (classFormatCommand.equals(Pack200.Packer.PASS)) {
499                        Utils.log.info(ce.toString());
500                        Utils.log.warning(message + " unknown class format: " +
501                                fname);
502                        return null;
503                    }
504                }
505                // Otherwise, it must be an error.
506                throw ioe;
507            }
508            pkg.addClass(cls);
509            return cls.file;
510        }
511
512        // Read raw data.
513        Package.File readFile(String fname, InputStream in) throws IOException {
514
515            Package.File file = pkg.new File(fname);
516            file.readFrom(in);
517            if (file.isDirectory() && file.getFileLength() != 0)
518                throw new IllegalArgumentException("Non-empty directory: "+file.getFileName());
519            return file;
520        }
521
522        void flushPartial(OutputStream out, int nextCount) throws IOException {
523            if (pkg.files.isEmpty() && pkg.classes.isEmpty()) {
524                return;  // do not flush an empty segment
525            }
526            flushPackage(out, Math.max(1, nextCount));
527            props.setInteger(Pack200.Packer.PROGRESS, 25);
528            // In case there will be another segment:
529            makeNextPackage();
530            segmentCount += 1;
531            segmentTotalSize += segmentSize;
532            segmentSize = 0;
533        }
534
535        void flushAll(OutputStream out) throws IOException {
536            props.setInteger(Pack200.Packer.PROGRESS, 50);
537            flushPackage(out, 0);
538            out.flush();
539            props.setInteger(Pack200.Packer.PROGRESS, 100);
540            segmentCount += 1;
541            segmentTotalSize += segmentSize;
542            segmentSize = 0;
543            if (verbose > 0 && segmentCount > 1) {
544                Utils.log.info("Transmitted "
545                                 +segmentTotalSize+" input bytes in "
546                                 +segmentCount+" segments totaling "
547                                 +totalOutputSize+" bytes");
548            }
549        }
550
551
552        /** Write all information in the current package segment
553         *  to the output stream.
554         */
555        void flushPackage(OutputStream out, int nextCount) throws IOException {
556            int nfiles = pkg.files.size();
557            if (!keepFileOrder) {
558                // Keeping the order of classes costs about 1%
559                // Keeping the order of all files costs something more.
560                if (verbose > 1)  Utils.log.fine("Reordering files.");
561                boolean stripDirectories = true;
562                pkg.reorderFiles(keepClassOrder, stripDirectories);
563            } else {
564                // Package builder must have created a stub for each class.
565                assert(pkg.files.containsAll(pkg.getClassStubs()));
566                // Order of stubs in file list must agree with classes.
567                List<Package.File> res = pkg.files;
568                assert((res = new ArrayList<>(pkg.files))
569                       .retainAll(pkg.getClassStubs()) || true);
570                assert(res.equals(pkg.getClassStubs()));
571            }
572            pkg.trimStubs();
573
574            // Do some stripping, maybe.
575            if (props.getBoolean(Utils.COM_PREFIX+"strip.debug"))        pkg.stripAttributeKind("Debug");
576            if (props.getBoolean(Utils.COM_PREFIX+"strip.compile"))      pkg.stripAttributeKind("Compile");
577            if (props.getBoolean(Utils.COM_PREFIX+"strip.constants"))    pkg.stripAttributeKind("Constant");
578            if (props.getBoolean(Utils.COM_PREFIX+"strip.exceptions"))   pkg.stripAttributeKind("Exceptions");
579            if (props.getBoolean(Utils.COM_PREFIX+"strip.innerclasses")) pkg.stripAttributeKind("InnerClasses");
580
581            PackageWriter pw = new PackageWriter(pkg, out);
582            pw.archiveNextCount = nextCount;
583            pw.write();
584            out.flush();
585            if (verbose > 0) {
586                long outSize = pw.archiveSize0+pw.archiveSize1;
587                totalOutputSize += outSize;
588                long inSize = segmentSize;
589                Utils.log.info("Transmitted "
590                                 +nfiles+" files of "
591                                 +inSize+" input bytes in a segment of "
592                                 +outSize+" bytes");
593            }
594        }
595
596        List<InFile> scanJar(JarFile jf) throws IOException {
597            // Collect jar entries, preserving order.
598            List<InFile> inFiles = new ArrayList<>();
599            try {
600                for (JarEntry je : Collections.list(jf.entries())) {
601                    InFile inFile = new InFile(jf, je);
602                    assert(je.isDirectory() == inFile.name.endsWith("/"));
603                    inFiles.add(inFile);
604                }
605            } catch (IllegalStateException ise) {
606                throw new IOException(ise.getLocalizedMessage(), ise);
607            }
608            return inFiles;
609        }
610    }
611}
612