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