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 */
019package org.apache.commons.compress.compressors.xz;
020
021import java.io.IOException;
022import java.io.InputStream;
023
024import org.apache.commons.compress.MemoryLimitException;
025import org.tukaani.xz.XZ;
026import org.tukaani.xz.SingleXZInputStream;
027import org.tukaani.xz.XZInputStream;
028
029import org.apache.commons.compress.compressors.CompressorInputStream;
030
031/**
032 * XZ decompressor.
033 * @since 1.4
034 */
035public class XZCompressorInputStream extends CompressorInputStream {
036    private final InputStream in;
037
038    /**
039     * Checks if the signature matches what is expected for a .xz file.
040     *
041     * @param   signature     the bytes to check
042     * @param   length        the number of bytes to check
043     * @return  true if signature matches the .xz magic bytes, false otherwise
044     */
045    public static boolean matches(final byte[] signature, final int length) {
046        if (length < XZ.HEADER_MAGIC.length) {
047            return false;
048        }
049
050        for (int i = 0; i < XZ.HEADER_MAGIC.length; ++i) {
051            if (signature[i] != XZ.HEADER_MAGIC[i]) {
052                return false;
053            }
054        }
055
056        return true;
057    }
058
059    /**
060     * Creates a new input stream that decompresses XZ-compressed data
061     * from the specified input stream. This doesn't support
062     * concatenated .xz files.
063     *
064     * @param       inputStream where to read the compressed data
065     *
066     * @throws      IOException if the input is not in the .xz format,
067     *                          the input is corrupt or truncated, the .xz
068     *                          headers specify options that are not supported
069     *                          by this implementation, or the underlying
070     *                          <code>inputStream</code> throws an exception
071     */
072    public XZCompressorInputStream(final InputStream inputStream)
073            throws IOException {
074        this(inputStream, false);
075    }
076
077    /**
078     * Creates a new input stream that decompresses XZ-compressed data
079     * from the specified input stream.
080     *
081     * @param       inputStream where to read the compressed data
082     * @param       decompressConcatenated
083     *                          if true, decompress until the end of the
084     *                          input; if false, stop after the first .xz
085     *                          stream and leave the input position to point
086     *                          to the next byte after the .xz stream
087     *
088     * @throws      IOException if the input is not in the .xz format,
089     *                          the input is corrupt or truncated, the .xz
090     *                          headers specify options that are not supported
091     *                          by this implementation, or the underlying
092     *                          <code>inputStream</code> throws an exception
093     */
094    public XZCompressorInputStream(final InputStream inputStream,
095                                   final boolean decompressConcatenated)
096            throws IOException {
097        this(inputStream, decompressConcatenated, -1);
098    }
099
100    /**
101     * Creates a new input stream that decompresses XZ-compressed data
102     * from the specified input stream.
103     *
104     * @param       inputStream where to read the compressed data
105     * @param       decompressConcatenated
106     *                          if true, decompress until the end of the
107     *                          input; if false, stop after the first .xz
108     *                          stream and leave the input position to point
109     *                          to the next byte after the .xz stream
110     * @param       memoryLimitInKb memory limit used when reading blocks.  If
111     *                          the estimated memory limit is exceeded on {@link #read()},
112     *                          a {@link MemoryLimitException} is thrown.
113     *
114     * @throws      IOException if the input is not in the .xz format,
115     *                          the input is corrupt or truncated, the .xz
116     *                          headers specify options that are not supported
117     *                          by this implementation,
118     *                          or the underlying <code>inputStream</code> throws an exception
119     *
120     * @since 1.14
121     */
122    public XZCompressorInputStream(InputStream inputStream,
123                                   boolean decompressConcatenated, final int memoryLimitInKb)
124            throws IOException {
125        if (decompressConcatenated) {
126            in = new XZInputStream(inputStream, memoryLimitInKb);
127        } else {
128            in = new SingleXZInputStream(inputStream, memoryLimitInKb);
129        }
130    }
131
132    @Override
133    public int read() throws IOException {
134        try {
135            final int ret = in.read();
136            count(ret == -1 ? -1 : 1);
137            return ret;
138        } catch (org.tukaani.xz.MemoryLimitException e) {
139            throw new MemoryLimitException(e.getMemoryNeeded(), e.getMemoryLimit(), e);
140        }
141    }
142
143    @Override
144    public int read(final byte[] buf, final int off, final int len) throws IOException {
145        try {
146            final int ret = in.read(buf, off, len);
147            count(ret);
148            return ret;
149        } catch (org.tukaani.xz.MemoryLimitException e) {
150            //convert to commons-compress MemoryLimtException
151            throw new MemoryLimitException(e.getMemoryNeeded(), e.getMemoryLimit(), e);
152        }
153    }
154
155    @Override
156    public long skip(final long n) throws IOException {
157        try {
158            return in.skip(n);
159        } catch (org.tukaani.xz.MemoryLimitException e) {
160            //convert to commons-compress MemoryLimtException
161            throw new MemoryLimitException(e.getMemoryNeeded(), e.getMemoryLimit(), e);
162        }
163    }
164
165    @Override
166    public int available() throws IOException {
167        return in.available();
168    }
169
170    @Override
171    public void close() throws IOException {
172        in.close();
173    }
174}