View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   https://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.commons.compress.harmony.unpack200;
20  
21  import java.io.BufferedInputStream;
22  import java.io.BufferedOutputStream;
23  import java.io.FileInputStream;
24  import java.io.FileNotFoundException;
25  import java.io.FileOutputStream;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.nio.file.Files;
29  import java.nio.file.Path;
30  import java.nio.file.Paths;
31  import java.util.jar.JarEntry;
32  import java.util.jar.JarInputStream;
33  import java.util.jar.JarOutputStream;
34  import java.util.zip.GZIPInputStream;
35  
36  import org.apache.commons.compress.harmony.pack200.Pack200Exception;
37  import org.apache.commons.io.IOUtils;
38  import org.apache.commons.io.input.BoundedInputStream;
39  
40  /**
41   * 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
42   * and an output streams. Then {@code unpack()} is called, to unpack the pack200 archive.
43   */
44  public class Archive {
45  
46      private static final int[] MAGIC = { 0xCA, 0xFE, 0xD0, 0x0D };
47  
48      private BoundedInputStream inputStream;
49  
50      private final JarOutputStream outputStream;
51  
52      private boolean removePackFile;
53  
54      private int logLevel = Segment.LOG_LEVEL_STANDARD;
55  
56      private FileOutputStream logFile;
57  
58      private boolean overrideDeflateHint;
59  
60      private boolean deflateHint;
61  
62      private final Path inputPath;
63  
64      private final long inputSize;
65  
66      private final String outputFileName;
67  
68      private final boolean closeStreams;
69  
70      /**
71       * Creates an Archive with streams for the input and output files. Note: If you use this method then calling {@link #setRemovePackFile(boolean)} will have
72       * no effect.
73       *
74       * @param inputStream  the input stream, preferably a {@link BoundedInputStream}. The bound can the the file size.
75       * @param outputStream the JAR output stream.
76       * @throws IOException if an I/O error occurs
77       */
78      public Archive(final InputStream inputStream, final JarOutputStream outputStream) throws IOException {
79          this.inputStream = Pack200UnpackerAdapter.newBoundedInputStream(inputStream);
80          this.outputStream = outputStream;
81          if (inputStream instanceof FileInputStream) {
82              inputPath = Paths.get(Pack200UnpackerAdapter.readPathString((FileInputStream) inputStream));
83          } else {
84              inputPath = null;
85          }
86          this.outputFileName = null;
87          this.inputSize = -1;
88          this.closeStreams = false;
89      }
90  
91      /**
92       * Creates an Archive with the given input and output file names.
93       *
94       * @param inputFileName  the input file name.
95       * @param outputFileName the output file name
96       * @throws FileNotFoundException if the input file does not exist
97       * @throws IOException           if an I/O error occurs
98       */
99      @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 }