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.File;
021import java.io.FileInputStream;
022import java.io.FilterInputStream;
023import java.io.IOException;
024import java.io.InputStream;
025import java.net.URISyntaxException;
026import java.net.URL;
027import java.nio.file.Files;
028import java.nio.file.Path;
029import java.nio.file.Paths;
030import java.util.jar.JarOutputStream;
031
032import org.apache.commons.compress.harmony.pack200.Pack200Adapter;
033import org.apache.commons.compress.harmony.pack200.Pack200Exception;
034import org.apache.commons.compress.java.util.jar.Pack200.Unpacker;
035import org.apache.commons.io.input.BoundedInputStream;
036import org.apache.commons.io.input.CloseShieldInputStream;
037import org.apache.commons.lang3.reflect.FieldUtils;
038
039/**
040 * This class provides the binding between the standard Pack200 interface and the internal interface for (un)packing.
041 */
042public class Pack200UnpackerAdapter extends Pack200Adapter implements Unpacker {
043
044    /**
045     * Creates a new BoundedInputStream bound by the size of the given file.
046     * <p>
047     * The new BoundedInputStream wraps a new {@link BufferedInputStream}.
048     * </p>
049     *
050     * @param file The file.
051     * @return a new BoundedInputStream
052     * @throws IOException if an I/O error occurs
053     */
054    static BoundedInputStream newBoundedInputStream(final File file) throws IOException {
055        return newBoundedInputStream(file.toPath());
056    }
057
058    private static BoundedInputStream newBoundedInputStream(final FileInputStream fileInputStream) throws IOException {
059        return newBoundedInputStream(readPathString(fileInputStream));
060    }
061
062    @SuppressWarnings("resource") // Caller closes.
063    static BoundedInputStream newBoundedInputStream(final InputStream inputStream) throws IOException {
064        if (inputStream instanceof BoundedInputStream) {
065            // Already bound.
066            return (BoundedInputStream) inputStream;
067        }
068        if (inputStream instanceof CloseShieldInputStream) {
069            // Don't unwrap to keep close shield.
070            return newBoundedInputStream(BoundedInputStream.builder().setInputStream(inputStream).get());
071        }
072        if (inputStream instanceof FilterInputStream) {
073            return newBoundedInputStream(unwrap((FilterInputStream) inputStream));
074        }
075        if (inputStream instanceof FileInputStream) {
076            return newBoundedInputStream((FileInputStream) inputStream);
077        }
078        // No limit
079        return newBoundedInputStream(BoundedInputStream.builder().setInputStream(inputStream).get());
080    }
081
082    /**
083     * Creates a new BoundedInputStream bound by the size of the given path.
084     * <p>
085     * The new BoundedInputStream wraps a new {@link BufferedInputStream}.
086     * </p>
087     *
088     * @param path The path.
089     * @return a new BoundedInputStream
090     * @throws IOException if an I/O error occurs
091     */
092    @SuppressWarnings("resource") // Caller closes.
093    static BoundedInputStream newBoundedInputStream(final Path path) throws IOException {
094        // @formatter:off
095        return BoundedInputStream.builder()
096                .setInputStream(new BufferedInputStream(Files.newInputStream(path)))
097                .setMaxCount(Files.size(path))
098                .setPropagateClose(false)
099                .get();
100        // @formatter:on
101    }
102
103    /**
104     * Creates a new BoundedInputStream bound by the size of the given file.
105     * <p>
106     * The new BoundedInputStream wraps a new {@link BufferedInputStream}.
107     * </p>
108     *
109     * @param first the path string or initial part of the path string.
110     * @param more  additional strings to be joined to form the path string.
111     * @return a new BoundedInputStream
112     * @throws IOException if an I/O error occurs
113     */
114    static BoundedInputStream newBoundedInputStream(final String first, final String... more) throws IOException {
115        return newBoundedInputStream(Paths.get(first, more));
116    }
117
118    /**
119     * Creates a new BoundedInputStream bound by the size of the given URL to a file.
120     * <p>
121     * The new BoundedInputStream wraps a new {@link BufferedInputStream}.
122     * </p>
123     *
124     * @param url The URL.
125     * @return a new BoundedInputStream
126     * @throws IOException        if an I/O error occurs
127     * @throws URISyntaxException
128     */
129    static BoundedInputStream newBoundedInputStream(final URL url) throws IOException, URISyntaxException {
130        return newBoundedInputStream(Paths.get(url.toURI()));
131    }
132
133    @SuppressWarnings("unchecked")
134    private static <T> T readField(final Object object, final String fieldName) {
135        try {
136            return (T) FieldUtils.readField(object, fieldName, true);
137        } catch (final IllegalAccessException e) {
138            return null;
139        }
140    }
141
142    static String readPathString(final FileInputStream fis) {
143        return readField(fis, "path");
144    }
145
146    /**
147     * Unwraps the given FilterInputStream to return its wrapped InputStream.
148     *
149     * @param filterInputStream The FilterInputStream to unwrap.
150     * @return The wrapped InputStream
151     */
152    static InputStream unwrap(final FilterInputStream filterInputStream) {
153        return readField(filterInputStream, "in");
154    }
155
156    /**
157     * Unwraps the given InputStream if it is an FilterInputStream to return its wrapped InputStream.
158     *
159     * @param filterInputStream The FilterInputStream to unwrap.
160     * @return The wrapped InputStream
161     */
162    @SuppressWarnings("resource")
163    static InputStream unwrap(final InputStream inputStream) {
164        return inputStream instanceof FilterInputStream ? unwrap((FilterInputStream) inputStream) : inputStream;
165    }
166
167    @Override
168    public void unpack(final File file, final JarOutputStream out) throws IOException {
169        if (file == null || out == null) {
170            throw new IllegalArgumentException("Must specify both input and output streams");
171        }
172        final long size = file.length();
173        final int bufferSize = size > 0 && size < DEFAULT_BUFFER_SIZE ? (int) size : DEFAULT_BUFFER_SIZE;
174        try (InputStream in = new BufferedInputStream(Files.newInputStream(file.toPath()), bufferSize)) {
175            unpack(in, out);
176        }
177    }
178
179    @Override
180    public void unpack(final InputStream in, final JarOutputStream out) throws IOException {
181        if (in == null || out == null) {
182            throw new IllegalArgumentException("Must specify both input and output streams");
183        }
184        completed(0);
185        try {
186            new Archive(in, out).unpack();
187        } catch (final Pack200Exception e) {
188            throw new IOException("Failed to unpack Jar:" + e);
189        }
190        completed(1);
191    }
192}