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 * http://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 */
019
020package org.apache.commons.compress.compressors.pack200;
021
022import java.io.File;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.UncheckedIOException;
026import java.util.Map;
027import java.util.jar.JarOutputStream;
028
029import org.apache.commons.compress.compressors.CompressorInputStream;
030import org.apache.commons.compress.java.util.jar.Pack200;
031import org.apache.commons.compress.utils.CloseShieldFilterInputStream;
032import org.apache.commons.compress.utils.IOUtils;
033
034/**
035 * An input stream that decompresses from the Pack200 format to be read as any other stream.
036 *
037 * <p>
038 * The {@link CompressorInputStream#getCount getCount} and {@link CompressorInputStream#getBytesRead getBytesRead} methods always return 0.
039 * </p>
040 *
041 * @NotThreadSafe
042 * @since 1.3
043 */
044public class Pack200CompressorInputStream extends CompressorInputStream {
045
046    private static final byte[] CAFE_DOOD = { (byte) 0xCA, (byte) 0xFE, (byte) 0xD0, (byte) 0x0D };
047    private static final int SIG_LENGTH = CAFE_DOOD.length;
048
049    /**
050     * Checks if the signature matches what is expected for a pack200 file (0xCAFED00D).
051     *
052     * @param signature the bytes to check
053     * @param length    the number of bytes to check
054     * @return true, if this stream is a pack200 compressed stream, false otherwise
055     */
056    public static boolean matches(final byte[] signature, final int length) {
057        if (length < SIG_LENGTH) {
058            return false;
059        }
060
061        for (int i = 0; i < SIG_LENGTH; i++) {
062            if (signature[i] != CAFE_DOOD[i]) {
063                return false;
064            }
065        }
066
067        return true;
068    }
069
070    private final InputStream originalInputStream;
071
072    private final AbstractStreamBridge abstractStreamBridge;
073
074    /**
075     * Decompresses the given file, caching the decompressed data in memory.
076     *
077     * @param file the file to decompress
078     * @throws IOException if reading fails
079     */
080    public Pack200CompressorInputStream(final File file) throws IOException {
081        this(file, Pack200Strategy.IN_MEMORY);
082    }
083
084    /**
085     * Decompresses the given file, caching the decompressed data in memory and using the given properties.
086     *
087     * @param file     the file to decompress
088     * @param properties Pack200 properties to use
089     * @throws IOException if reading fails
090     */
091    public Pack200CompressorInputStream(final File file, final Map<String, String> properties) throws IOException {
092        this(file, Pack200Strategy.IN_MEMORY, properties);
093    }
094
095    /**
096     * Decompresses the given file using the given strategy to cache the results.
097     *
098     * @param file    the file to decompress
099     * @param mode the strategy to use
100     * @throws IOException if reading fails
101     */
102    public Pack200CompressorInputStream(final File file, final Pack200Strategy mode) throws IOException {
103        this(null, file, mode, null);
104    }
105
106    /**
107     * Decompresses the given file using the given strategy to cache the results and the given properties.
108     *
109     * @param file     the file to decompress
110     * @param mode  the strategy to use
111     * @param properties Pack200 properties to use
112     * @throws IOException if reading fails
113     */
114    public Pack200CompressorInputStream(final File file, final Pack200Strategy mode, final Map<String, String> properties) throws IOException {
115        this(null, file, mode, properties);
116    }
117
118    /**
119     * Decompresses the given stream, caching the decompressed data in memory.
120     *
121     * <p>
122     * When reading from a file the File-arg constructor may provide better performance.
123     * </p>
124     *
125     * @param inputStream the InputStream from which this object should be created
126     * @throws IOException if reading fails
127     */
128    public Pack200CompressorInputStream(final InputStream inputStream) throws IOException {
129        this(inputStream, Pack200Strategy.IN_MEMORY);
130    }
131
132    private Pack200CompressorInputStream(final InputStream inputStream, final File file, final Pack200Strategy mode, final Map<String, String> properties)
133            throws IOException {
134        this.originalInputStream = inputStream;
135        this.abstractStreamBridge = mode.newStreamBridge();
136        try (final JarOutputStream jarOut = new JarOutputStream(abstractStreamBridge)) {
137            final Pack200.Unpacker unpacker = Pack200.newUnpacker();
138            if (properties != null) {
139                unpacker.properties().putAll(properties);
140            }
141            if (file == null) {
142                // unpack would close this stream but we want to give the call site more control
143                // TODO unpack should not close its given stream.
144                try (final CloseShieldFilterInputStream closeShield = new CloseShieldFilterInputStream(inputStream)) {
145                    unpacker.unpack(closeShield, jarOut);
146                }
147            } else {
148                unpacker.unpack(file, jarOut);
149            }
150        }
151    }
152
153    /**
154     * Decompresses the given stream, caching the decompressed data in memory and using the given properties.
155     *
156     * <p>
157     * When reading from a file the File-arg constructor may provide better performance.
158     * </p>
159     *
160     * @param inputStream    the InputStream from which this object should be created
161     * @param properties Pack200 properties to use
162     * @throws IOException if reading fails
163     */
164    public Pack200CompressorInputStream(final InputStream inputStream, final Map<String, String> properties) throws IOException {
165        this(inputStream, Pack200Strategy.IN_MEMORY, properties);
166    }
167
168    /**
169     * Decompresses the given stream using the given strategy to cache the results.
170     *
171     * <p>
172     * When reading from a file the File-arg constructor may provide better performance.
173     * </p>
174     *
175     * @param inputStream   the InputStream from which this object should be created
176     * @param mode the strategy to use
177     * @throws IOException if reading fails
178     */
179    public Pack200CompressorInputStream(final InputStream inputStream, final Pack200Strategy mode) throws IOException {
180        this(inputStream, null, mode, null);
181    }
182
183    /**
184     * Decompresses the given stream using the given strategy to cache the results and the given properties.
185     *
186     * <p>
187     * When reading from a file the File-arg constructor may provide better performance.
188     * </p>
189     *
190     * @param inputStream    the InputStream from which this object should be created
191     * @param mode  the strategy to use
192     * @param properties Pack200 properties to use
193     * @throws IOException if reading fails
194     */
195    public Pack200CompressorInputStream(final InputStream inputStream, final Pack200Strategy mode, final Map<String, String> properties) throws IOException {
196        this(inputStream, null, mode, properties);
197    }
198
199    @SuppressWarnings("resource") // Does not allocate
200    @Override
201    public int available() throws IOException {
202        return getInputStream().available();
203    }
204
205    @Override
206    public void close() throws IOException {
207        try {
208            abstractStreamBridge.stop();
209        } finally {
210            if (originalInputStream != null) {
211                originalInputStream.close();
212            }
213        }
214    }
215
216    private InputStream getInputStream() throws IOException {
217        return abstractStreamBridge.getInputStream();
218    }
219
220    @SuppressWarnings("resource") // Does not allocate
221    @Override
222    public synchronized void mark(final int limit) {
223        try {
224            getInputStream().mark(limit);
225        } catch (final IOException ex) {
226            throw new UncheckedIOException(ex); // NOSONAR
227        }
228    }
229
230    @SuppressWarnings("resource") // Does not allocate
231    @Override
232    public boolean markSupported() {
233        try {
234            return getInputStream().markSupported();
235        } catch (final IOException ex) { // NOSONAR
236            return false;
237        }
238    }
239
240    @SuppressWarnings("resource") // Does not allocate
241    @Override
242    public int read() throws IOException {
243        return getInputStream().read();
244    }
245
246    @SuppressWarnings("resource") // Does not allocate
247    @Override
248    public int read(final byte[] b) throws IOException {
249        return getInputStream().read(b);
250    }
251
252    @SuppressWarnings("resource") // Does not allocate
253    @Override
254    public int read(final byte[] b, final int off, final int count) throws IOException {
255        return getInputStream().read(b, off, count);
256    }
257
258    @SuppressWarnings("resource") // Does not allocate
259    @Override
260    public synchronized void reset() throws IOException {
261        getInputStream().reset();
262    }
263
264    @SuppressWarnings("resource") // Does not allocate
265    @Override
266    public long skip(final long count) throws IOException {
267        return IOUtils.skip(getInputStream(), count);
268    }
269}