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