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.unpack200; 020 021import java.io.BufferedInputStream; 022import java.io.BufferedOutputStream; 023import java.io.FileInputStream; 024import java.io.FileNotFoundException; 025import java.io.FileOutputStream; 026import java.io.IOException; 027import java.io.InputStream; 028import java.nio.file.Files; 029import java.nio.file.Path; 030import java.nio.file.Paths; 031import java.util.jar.JarEntry; 032import java.util.jar.JarInputStream; 033import java.util.jar.JarOutputStream; 034import java.util.zip.GZIPInputStream; 035 036import org.apache.commons.compress.harmony.pack200.Pack200Exception; 037import org.apache.commons.io.IOUtils; 038import org.apache.commons.io.input.BoundedInputStream; 039 040/** 041 * Archive is the main entry point to unpack200. An archive is constructed with either two file names, a pack file and an output file name or an input stream 042 * and an output streams. Then {@code unpack()} is called, to unpack the pack200 archive. 043 */ 044public class Archive { 045 046 private static final int[] MAGIC = { 0xCA, 0xFE, 0xD0, 0x0D }; 047 048 private BoundedInputStream inputStream; 049 050 private final JarOutputStream outputStream; 051 052 private boolean removePackFile; 053 054 private int logLevel = Segment.LOG_LEVEL_STANDARD; 055 056 private FileOutputStream logFile; 057 058 private boolean overrideDeflateHint; 059 060 private boolean deflateHint; 061 062 private final Path inputPath; 063 064 private final long inputSize; 065 066 private final String outputFileName; 067 068 private final boolean closeStreams; 069 070 /** 071 * Creates an Archive with streams for the input and output files. Note: If you use this method then calling {@link #setRemovePackFile(boolean)} will have 072 * no effect. 073 * 074 * @param inputStream the input stream, preferably a {@link BoundedInputStream}. The bound can the the file size. 075 * @param outputStream the JAR output stream. 076 * @throws IOException if an I/O error occurs 077 */ 078 public Archive(final InputStream inputStream, final JarOutputStream outputStream) throws IOException { 079 this.inputStream = Pack200UnpackerAdapter.newBoundedInputStream(inputStream); 080 this.outputStream = outputStream; 081 if (inputStream instanceof FileInputStream) { 082 inputPath = Paths.get(Pack200UnpackerAdapter.readPathString((FileInputStream) inputStream)); 083 } else { 084 inputPath = null; 085 } 086 this.outputFileName = null; 087 this.inputSize = -1; 088 this.closeStreams = false; 089 } 090 091 /** 092 * Creates an Archive with the given input and output file names. 093 * 094 * @param inputFileName the input file name. 095 * @param outputFileName the output file name 096 * @throws FileNotFoundException if the input file does not exist 097 * @throws IOException if an I/O error occurs 098 */ 099 @SuppressWarnings("resource") 100 public Archive(final String inputFileName, final String outputFileName) throws FileNotFoundException, IOException { 101 this.inputPath = Paths.get(inputFileName); 102 this.inputSize = Files.size(this.inputPath); 103 this.inputStream = BoundedInputStream.builder().setPath(inputPath).setMaxCount(inputSize).get(); 104 this.outputStream = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(outputFileName))); 105 this.outputFileName = outputFileName; 106 this.closeStreams = true; 107 } 108 109 private boolean available(final InputStream inputStream) throws IOException { 110 inputStream.mark(1); 111 final int check = inputStream.read(); 112 inputStream.reset(); 113 return check != -1; 114 } 115 116 /** 117 * Sets the default hint. 118 * 119 * @param deflateHint the hint value. 120 */ 121 public void setDeflateHint(final boolean deflateHint) { 122 overrideDeflateHint = true; 123 this.deflateHint = deflateHint; 124 } 125 126 /** 127 * Sets the lgg file. 128 * 129 * @param logFileName the log file name. 130 * @throws FileNotFoundException if the file exists but is a directory rather than a regular file, does not exist but cannot be created, or cannot be opened 131 * for any other reason 132 */ 133 public void setLogFile(final String logFileName) throws FileNotFoundException { 134 logFile = new FileOutputStream(logFileName); 135 } 136 137 /** 138 * Sets the lgg file. 139 * 140 * @param logFileName the log file name. 141 * @param append if {@code true}, then bytes will be written to the end of the file rather than the beginning 142 * @throws FileNotFoundException if the file exists but is a directory rather than a regular file, does not exist but cannot be created, or cannot be opened 143 * for any other reason 144 */ 145 public void setLogFile(final String logFileName, final boolean append) throws FileNotFoundException { 146 logFile = new FileOutputStream(logFileName, append); 147 } 148 149 /** 150 * Sets whether to set the log level to quiet. 151 * 152 * @param quiet whether to set the log level to quiet. 153 */ 154 public void setQuiet(final boolean quiet) { 155 if (quiet || logLevel == Segment.LOG_LEVEL_QUIET) { 156 logLevel = Segment.LOG_LEVEL_QUIET; 157 } 158 } 159 160 /** 161 * If removePackFile is set to true, the input file is deleted after unpacking. 162 * 163 * @param removePackFile If true, the input file is deleted after unpacking. 164 */ 165 public void setRemovePackFile(final boolean removePackFile) { 166 this.removePackFile = removePackFile; 167 } 168 169 /** 170 * Sets whether to set the log level to verbose. 171 * 172 * @param verbose whether to set the log level to verbose. 173 */ 174 public void setVerbose(final boolean verbose) { 175 if (verbose) { 176 logLevel = Segment.LOG_LEVEL_VERBOSE; 177 } else if (logLevel == Segment.LOG_LEVEL_VERBOSE) { 178 logLevel = Segment.LOG_LEVEL_STANDARD; 179 } 180 } 181 182 /** 183 * Unpacks the Archive from the input file to the output file. 184 * 185 * @throws Pack200Exception Never thrown. 186 * @throws IOException if an I/O error has occurred. 187 */ 188 public void unpack() throws Pack200Exception, IOException { 189 outputStream.setComment("PACK200"); 190 try { 191 if (!inputStream.markSupported()) { 192 inputStream = new BoundedInputStream(new BufferedInputStream(inputStream)); 193 if (!inputStream.markSupported()) { 194 throw new IllegalStateException(); 195 } 196 } 197 inputStream.mark(2); 198 if ((inputStream.read() & 0xFF | (inputStream.read() & 0xFF) << 8) == GZIPInputStream.GZIP_MAGIC) { 199 inputStream.reset(); 200 inputStream = new BoundedInputStream(new BufferedInputStream(new GZIPInputStream(inputStream))); 201 } else { 202 inputStream.reset(); 203 } 204 inputStream.mark(MAGIC.length); 205 // pack200 206 final int[] word = new int[MAGIC.length]; 207 for (int i = 0; i < word.length; i++) { 208 word[i] = inputStream.read(); 209 } 210 boolean compressedWithE0 = false; 211 for (int m = 0; m < MAGIC.length; m++) { 212 if (word[m] != MAGIC[m]) { 213 compressedWithE0 = true; 214 break; 215 } 216 } 217 inputStream.reset(); 218 if (compressedWithE0) { // The original Jar was not packed, so just copy it across. 219 final JarInputStream jarInputStream = new JarInputStream(inputStream); 220 JarEntry jarEntry; 221 while ((jarEntry = jarInputStream.getNextJarEntry()) != null) { 222 outputStream.putNextEntry(jarEntry); 223 IOUtils.copy(jarInputStream, outputStream, 16_384); 224 outputStream.closeEntry(); 225 } 226 } else { 227 int i = 0; 228 while (available(inputStream)) { 229 i++; 230 final Segment segment = new Segment(); 231 segment.setLogLevel(logLevel); 232 segment.setLogStream(logFile); 233 segment.setPreRead(false); 234 if (i == 1) { 235 segment.log(Segment.LOG_LEVEL_VERBOSE, "Unpacking from " + inputPath + " to " + outputFileName); 236 } 237 segment.log(Segment.LOG_LEVEL_VERBOSE, "Reading segment " + i); 238 if (overrideDeflateHint) { 239 segment.overrideDeflateHint(deflateHint); 240 } 241 segment.unpack(inputStream, outputStream); 242 outputStream.flush(); 243 } 244 } 245 } finally { 246 if (closeStreams) { 247 IOUtils.closeQuietly(inputStream); 248 IOUtils.closeQuietly(outputStream); 249 } 250 IOUtils.closeQuietly(logFile); 251 } 252 if (removePackFile && inputPath != null) { 253 Files.delete(inputPath); 254 } 255 } 256 257}