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