001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one or more
003 *  contributor license agreements.  See the NOTICE file distributed with
004 *  this work for additional information regarding copyright ownership.
005 *  The ASF licenses this file to You under the Apache License, Version 2.0
006 *  (the "License"); you may not use this file except in compliance with
007 *  the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 *  Unless required by applicable law or agreed to in writing, software
012 *  distributed under the License is distributed on an "AS IS" BASIS,
013 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 *  See the License for the specific language governing permissions and
015 *  limitations under the License.
016 */
017package org.apache.commons.compress.harmony.pack200;
018
019import java.io.BufferedOutputStream;
020import java.io.IOException;
021import java.io.OutputStream;
022import java.util.ArrayList;
023import java.util.List;
024import java.util.jar.JarEntry;
025import java.util.jar.JarFile;
026import java.util.jar.JarInputStream;
027import java.util.zip.GZIPOutputStream;
028import java.util.zip.ZipEntry;
029
030/**
031 * Archive is the main entry point to pack200 and represents a packed archive. An archive is constructed with either a
032 * JarInputStream and an output stream or a JarFile as input and an OutputStream. Options can be set, then
033 * {@code pack()} is called, to pack the Jar file into a pack200 archive.
034 */
035public class Archive {
036
037    private final JarInputStream jarInputStream;
038    private final OutputStream outputStream;
039    private JarFile jarFile;
040    private long currentSegmentSize;
041    private final PackingOptions options;
042
043    /**
044     * Creates an Archive with streams for the input and output.
045     *
046     * @param inputStream TODO
047     * @param outputStream TODO
048     * @param options - packing options (if null then defaults are used)
049     * @throws IOException If an I/O error occurs.
050     */
051    public Archive(final JarInputStream inputStream, OutputStream outputStream, PackingOptions options)
052        throws IOException {
053        jarInputStream = inputStream;
054        if (options == null) {
055            // use all defaults
056            options = new PackingOptions();
057        }
058        this.options = options;
059        if (options.isGzip()) {
060            outputStream = new GZIPOutputStream(outputStream);
061        }
062        this.outputStream = new BufferedOutputStream(outputStream);
063        PackingUtils.config(options);
064    }
065
066    /**
067     * Creates an Archive with the given input file and a stream for the output
068     *
069     * @param jarFile - the input file
070     * @param outputStream TODO
071     * @param options - packing options (if null then defaults are used)
072     * @throws IOException If an I/O error occurs.
073     */
074    public Archive(final JarFile jarFile, OutputStream outputStream, PackingOptions options) throws IOException {
075        if (options == null) { // use all defaults
076            options = new PackingOptions();
077        }
078        this.options = options;
079        if (options.isGzip()) {
080            outputStream = new GZIPOutputStream(outputStream);
081        }
082        this.outputStream = new BufferedOutputStream(outputStream);
083        this.jarFile = jarFile;
084        jarInputStream = null;
085        PackingUtils.config(options);
086    }
087
088    /**
089     * Pack the archive
090     *
091     * @throws Pack200Exception TODO
092     * @throws IOException If an I/O error occurs.
093     */
094    public void pack() throws Pack200Exception, IOException {
095        if (0 == options.getEffort()) {
096            doZeroEffortPack();
097        } else {
098            doNormalPack();
099        }
100    }
101
102    private void doZeroEffortPack() throws IOException {
103        PackingUtils.log("Start to perform a zero-effort packing");
104        if (jarInputStream != null) {
105            PackingUtils.copyThroughJar(jarInputStream, outputStream);
106        } else {
107            PackingUtils.copyThroughJar(jarFile, outputStream);
108        }
109    }
110
111        private void doNormalPack() throws IOException, Pack200Exception {
112                PackingUtils.log("Start to perform a normal packing");
113                List<PackingFile> packingFileList;
114                if (jarInputStream != null) {
115                        packingFileList = PackingUtils.getPackingFileListFromJar(jarInputStream, options.isKeepFileOrder());
116                } else {
117                        packingFileList = PackingUtils.getPackingFileListFromJar(jarFile, options.isKeepFileOrder());
118                }
119
120                final List<SegmentUnit> segmentUnitList = splitIntoSegments(packingFileList);
121                int previousByteAmount = 0;
122                int packedByteAmount = 0;
123
124                final int segmentSize = segmentUnitList.size();
125                SegmentUnit segmentUnit;
126                for (int index = 0; index < segmentSize; index++) {
127                        segmentUnit = segmentUnitList.get(index);
128                        new Segment().pack(segmentUnit, outputStream, options);
129                        previousByteAmount += segmentUnit.getByteAmount();
130                        packedByteAmount += segmentUnit.getPackedByteAmount();
131                }
132
133                PackingUtils.log("Total: Packed " + previousByteAmount + " input bytes of " + packingFileList.size()
134                                + " files into " + packedByteAmount + " bytes in " + segmentSize + " segments");
135
136                outputStream.close();
137        }
138
139    private List<SegmentUnit> splitIntoSegments(final List<PackingFile> packingFileList) {
140        final List<SegmentUnit> segmentUnitList = new ArrayList<>();
141        List<Pack200ClassReader> classes = new ArrayList<>();
142        List<PackingFile> files = new ArrayList<>();
143        final long segmentLimit = options.getSegmentLimit();
144
145        final int size = packingFileList.size();
146        PackingFile packingFile;
147        for (int index = 0; index < size; index++) {
148            packingFile = packingFileList.get(index);
149            if (!addJarEntry(packingFile, classes, files)) {
150                // not added because segment has reached maximum size
151                segmentUnitList.add(new SegmentUnit(classes, files));
152                classes = new ArrayList<>();
153                files = new ArrayList<>();
154                currentSegmentSize = 0;
155                // add the jar to a new segment
156                addJarEntry(packingFile, classes, files);
157                // ignore the size of first entry for compatibility with RI
158                currentSegmentSize = 0;
159            } else if (segmentLimit == 0 && estimateSize(packingFile) > 0) {
160                // create a new segment for each class unless size is 0
161                segmentUnitList.add(new SegmentUnit(classes, files));
162                classes = new ArrayList<>();
163                files = new ArrayList<>();
164            }
165        }
166        // Change for Apache Commons Compress based on Apache Harmony.
167        // if (classes.size() > 0 && files.size() > 0) {
168        if (classes.size() > 0 || files.size() > 0) {
169            segmentUnitList.add(new SegmentUnit(classes, files));
170        }
171        return segmentUnitList;
172    }
173
174    private boolean addJarEntry(final PackingFile packingFile, final List<Pack200ClassReader> javaClasses, final List<PackingFile> files) {
175        final long segmentLimit = options.getSegmentLimit();
176        if (segmentLimit != -1 && segmentLimit != 0) {
177            // -1 is a special case where only one segment is created and
178            // 0 is a special case where one segment is created for each file
179            // except for files in "META-INF"
180            final long packedSize = estimateSize(packingFile);
181            if (packedSize + currentSegmentSize > segmentLimit && currentSegmentSize > 0) {
182                // don't add this JarEntry to the current segment
183                return false;
184            }
185            // do add this JarEntry
186            currentSegmentSize += packedSize;
187        }
188
189        final String name = packingFile.getName();
190        if (name.endsWith(".class") && !options.isPassFile(name)) {
191            final Pack200ClassReader classParser = new Pack200ClassReader(packingFile.contents);
192            classParser.setFileName(name);
193            javaClasses.add(classParser);
194            packingFile.contents = new byte[0];
195        }
196        files.add(packingFile);
197        return true;
198    }
199
200    private long estimateSize(final PackingFile packingFile) {
201        // The heuristic used here is for compatibility with the RI and should
202        // not be changed
203        final String name = packingFile.getName();
204        if (name.startsWith("META-INF") || name.startsWith("/META-INF")) {
205            return 0;
206        }
207        long fileSize = packingFile.contents.length;
208        if (fileSize < 0) {
209            fileSize = 0;
210        }
211        return name.length() + fileSize + 5;
212    }
213
214    static class SegmentUnit {
215
216        private final List<Pack200ClassReader> classList;
217
218        private final List<PackingFile> fileList;
219
220        private int byteAmount;
221
222        private int packedByteAmount;
223
224        public SegmentUnit(final List<Pack200ClassReader> classes, final List<PackingFile> files) {
225            classList = classes;
226            fileList = files;
227            byteAmount = 0;
228            // Calculate the amount of bytes in classes and files before packing
229            byteAmount += classList.stream().mapToInt(element -> element.b.length).sum();
230            byteAmount += fileList.stream().mapToInt(element -> element.contents.length).sum();
231        }
232
233        public List<Pack200ClassReader> getClassList() {
234            return classList;
235        }
236
237        public int classListSize() {
238            return classList.size();
239        }
240
241        public int fileListSize() {
242            return fileList.size();
243        }
244
245        public List<PackingFile> getFileList() {
246            return fileList;
247        }
248
249        public int getByteAmount() {
250            return byteAmount;
251        }
252
253        public int getPackedByteAmount() {
254            return packedByteAmount;
255        }
256
257        public void addPackedByteAmount(final int amount) {
258            packedByteAmount += amount;
259        }
260    }
261
262    static class PackingFile {
263
264        private final String name;
265        private byte[] contents;
266        private final long modtime;
267        private final boolean deflateHint;
268        private final boolean isDirectory;
269
270        public PackingFile(final String name, final byte[] contents, final long modtime) {
271            this.name = name;
272            this.contents = contents;
273            this.modtime = modtime;
274            deflateHint = false;
275            isDirectory = false;
276        }
277
278        public PackingFile(final byte[] bytes, final JarEntry jarEntry) {
279            name = jarEntry.getName();
280            contents = bytes;
281            modtime = jarEntry.getTime();
282            deflateHint = jarEntry.getMethod() == ZipEntry.DEFLATED;
283            isDirectory = jarEntry.isDirectory();
284        }
285
286        public byte[] getContents() {
287            return contents;
288        }
289
290        public String getName() {
291            return name;
292        }
293
294        public long getModtime() {
295            return modtime;
296        }
297
298        public void setContents(final byte[] contents) {
299            this.contents = contents;
300        }
301
302        public boolean isDefalteHint() {
303            return deflateHint;
304        }
305
306        public boolean isDirectory() {
307            return isDirectory;
308        }
309
310        @Override
311        public String toString() {
312            return name;
313        }
314    }
315
316}