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.archivers.arj;
018
019import java.io.ByteArrayInputStream;
020import java.io.ByteArrayOutputStream;
021import java.io.DataInputStream;
022import java.io.EOFException;
023import java.io.IOException;
024import java.io.InputStream;
025import java.util.ArrayList;
026import java.util.zip.CRC32;
027
028import org.apache.commons.compress.archivers.ArchiveEntry;
029import org.apache.commons.compress.archivers.ArchiveException;
030import org.apache.commons.compress.archivers.ArchiveInputStream;
031import org.apache.commons.compress.utils.BoundedInputStream;
032import org.apache.commons.compress.utils.CRC32VerifyingInputStream;
033import org.apache.commons.compress.utils.Charsets;
034import org.apache.commons.compress.utils.IOUtils;
035
036/**
037 * Implements the "arj" archive format as an InputStream.
038 * <p>
039 * <a href="https://github.com/FarGroup/FarManager/blob/master/plugins/multiarc/arc.doc/arj.txt">Reference 1</a>
040 * <br>
041 * <a href="http://www.fileformat.info/format/arj/corion.htm">Reference 2</a>
042 * @NotThreadSafe
043 * @since 1.6
044 */
045public class ArjArchiveInputStream extends ArchiveInputStream {
046    private static final int ARJ_MAGIC_1 = 0x60;
047    private static final int ARJ_MAGIC_2 = 0xEA;
048    /**
049     * Checks if the signature matches what is expected for an arj file.
050     *
051     * @param signature
052     *            the bytes to check
053     * @param length
054     *            the number of bytes to check
055     * @return true, if this stream is an arj archive stream, false otherwise
056     */
057    public static boolean matches(final byte[] signature, final int length) {
058        return length >= 2 &&
059                (0xff & signature[0]) == ARJ_MAGIC_1 &&
060                (0xff & signature[1]) == ARJ_MAGIC_2;
061    }
062    private final DataInputStream in;
063    private final String charsetName;
064    private final MainHeader mainHeader;
065    private LocalFileHeader currentLocalFileHeader;
066
067    private InputStream currentInputStream;
068
069    /**
070     * Constructs the ArjInputStream, taking ownership of the inputStream that is passed in,
071     * and using the CP437 character encoding.
072     * @param inputStream the underlying stream, whose ownership is taken
073     * @throws ArchiveException if an exception occurs while reading
074     */
075    public ArjArchiveInputStream(final InputStream inputStream)
076            throws ArchiveException {
077        this(inputStream, "CP437");
078    }
079
080    /**
081     * Constructs the ArjInputStream, taking ownership of the inputStream that is passed in.
082     * @param inputStream the underlying stream, whose ownership is taken
083     * @param charsetName the charset used for file names and comments
084     *   in the archive. May be {@code null} to use the platform default.
085     * @throws ArchiveException if an exception occurs while reading
086     */
087    public ArjArchiveInputStream(final InputStream inputStream,
088            final String charsetName) throws ArchiveException {
089        in = new DataInputStream(inputStream);
090        this.charsetName = charsetName;
091        try {
092            mainHeader = readMainHeader();
093            if ((mainHeader.arjFlags & MainHeader.Flags.GARBLED) != 0) {
094                throw new ArchiveException("Encrypted ARJ files are unsupported");
095            }
096            if ((mainHeader.arjFlags & MainHeader.Flags.VOLUME) != 0) {
097                throw new ArchiveException("Multi-volume ARJ files are unsupported");
098            }
099        } catch (final IOException ioException) {
100            throw new ArchiveException(ioException.getMessage(), ioException);
101        }
102    }
103
104    @Override
105    public boolean canReadEntryData(final ArchiveEntry ae) {
106        return ae instanceof ArjArchiveEntry
107            && ((ArjArchiveEntry) ae).getMethod() == LocalFileHeader.Methods.STORED;
108    }
109
110    @Override
111    public void close() throws IOException {
112        in.close();
113    }
114
115    /**
116     * Gets the archive's comment.
117     * @return the archive's comment
118     */
119    public String getArchiveComment() {
120        return mainHeader.comment;
121    }
122
123    /**
124     * Gets the archive's recorded name.
125     * @return the archive's name
126     */
127    public String getArchiveName() {
128        return mainHeader.name;
129    }
130
131    @Override
132    public ArjArchiveEntry getNextEntry() throws IOException {
133        if (currentInputStream != null) {
134            // return value ignored as IOUtils.skip ensures the stream is drained completely
135            IOUtils.skip(currentInputStream, Long.MAX_VALUE);
136            currentInputStream.close();
137            currentLocalFileHeader = null;
138            currentInputStream = null;
139        }
140
141        currentLocalFileHeader = readLocalFileHeader();
142        if (currentLocalFileHeader != null) {
143            currentInputStream = new BoundedInputStream(in, currentLocalFileHeader.compressedSize);
144            if (currentLocalFileHeader.method == LocalFileHeader.Methods.STORED) {
145                currentInputStream = new CRC32VerifyingInputStream(currentInputStream,
146                        currentLocalFileHeader.originalSize, currentLocalFileHeader.originalCrc32);
147            }
148            return new ArjArchiveEntry(currentLocalFileHeader);
149        }
150        currentInputStream = null;
151        return null;
152    }
153
154    @Override
155    public int read(final byte[] b, final int off, final int len) throws IOException {
156        if (len == 0) {
157            return 0;
158        }
159        if (currentLocalFileHeader == null) {
160            throw new IllegalStateException("No current arj entry");
161        }
162        if (currentLocalFileHeader.method != LocalFileHeader.Methods.STORED) {
163            throw new IOException("Unsupported compression method " + currentLocalFileHeader.method);
164        }
165        return currentInputStream.read(b, off, len);
166    }
167
168    private int read16(final DataInputStream dataIn) throws IOException {
169        final int value = dataIn.readUnsignedShort();
170        count(2);
171        return Integer.reverseBytes(value) >>> 16;
172    }
173
174    private int read32(final DataInputStream dataIn) throws IOException {
175        final int value = dataIn.readInt();
176        count(4);
177        return Integer.reverseBytes(value);
178    }
179
180    private int read8(final DataInputStream dataIn) throws IOException {
181        final int value = dataIn.readUnsignedByte();
182        count(1);
183        return value;
184    }
185
186    private void readExtraData(final int firstHeaderSize, final DataInputStream firstHeader,
187                               final LocalFileHeader localFileHeader) throws IOException {
188        if (firstHeaderSize >= 33) {
189            localFileHeader.extendedFilePosition = read32(firstHeader);
190            if (firstHeaderSize >= 45) {
191                localFileHeader.dateTimeAccessed = read32(firstHeader);
192                localFileHeader.dateTimeCreated = read32(firstHeader);
193                localFileHeader.originalSizeEvenForVolumes = read32(firstHeader);
194                pushedBackBytes(12);
195            }
196            pushedBackBytes(4);
197        }
198    }
199
200    private byte[] readHeader() throws IOException {
201        boolean found = false;
202        byte[] basicHeaderBytes = null;
203        do {
204            int first;
205            int second = read8(in);
206            do {
207                first = second;
208                second = read8(in);
209            } while (first != ARJ_MAGIC_1 && second != ARJ_MAGIC_2);
210            final int basicHeaderSize = read16(in);
211            if (basicHeaderSize == 0) {
212                // end of archive
213                return null;
214            }
215            if (basicHeaderSize <= 2600) {
216                basicHeaderBytes = readRange(in, basicHeaderSize);
217                final long basicHeaderCrc32 = read32(in) & 0xFFFFFFFFL;
218                final CRC32 crc32 = new CRC32();
219                crc32.update(basicHeaderBytes);
220                if (basicHeaderCrc32 == crc32.getValue()) {
221                    found = true;
222                }
223            }
224        } while (!found);
225        return basicHeaderBytes;
226    }
227
228    private LocalFileHeader readLocalFileHeader() throws IOException {
229        final byte[] basicHeaderBytes = readHeader();
230        if (basicHeaderBytes == null) {
231            return null;
232        }
233        try (final DataInputStream basicHeader = new DataInputStream(new ByteArrayInputStream(basicHeaderBytes))) {
234
235            final int firstHeaderSize = basicHeader.readUnsignedByte();
236            final byte[] firstHeaderBytes = readRange(basicHeader, firstHeaderSize - 1);
237            pushedBackBytes(firstHeaderBytes.length);
238            try (final DataInputStream firstHeader = new DataInputStream(new ByteArrayInputStream(firstHeaderBytes))) {
239
240                final LocalFileHeader localFileHeader = new LocalFileHeader();
241                localFileHeader.archiverVersionNumber = firstHeader.readUnsignedByte();
242                localFileHeader.minVersionToExtract = firstHeader.readUnsignedByte();
243                localFileHeader.hostOS = firstHeader.readUnsignedByte();
244                localFileHeader.arjFlags = firstHeader.readUnsignedByte();
245                localFileHeader.method = firstHeader.readUnsignedByte();
246                localFileHeader.fileType = firstHeader.readUnsignedByte();
247                localFileHeader.reserved = firstHeader.readUnsignedByte();
248                localFileHeader.dateTimeModified = read32(firstHeader);
249                localFileHeader.compressedSize = 0xffffFFFFL & read32(firstHeader);
250                localFileHeader.originalSize = 0xffffFFFFL & read32(firstHeader);
251                localFileHeader.originalCrc32 = 0xffffFFFFL & read32(firstHeader);
252                localFileHeader.fileSpecPosition = read16(firstHeader);
253                localFileHeader.fileAccessMode = read16(firstHeader);
254                pushedBackBytes(20);
255                localFileHeader.firstChapter = firstHeader.readUnsignedByte();
256                localFileHeader.lastChapter = firstHeader.readUnsignedByte();
257
258                readExtraData(firstHeaderSize, firstHeader, localFileHeader);
259
260                localFileHeader.name = readString(basicHeader);
261                localFileHeader.comment = readString(basicHeader);
262
263                final ArrayList<byte[]> extendedHeaders = new ArrayList<>();
264                int extendedHeaderSize;
265                while ((extendedHeaderSize = read16(in)) > 0) {
266                    final byte[] extendedHeaderBytes = readRange(in, extendedHeaderSize);
267                    final long extendedHeaderCrc32 = 0xffffFFFFL & read32(in);
268                    final CRC32 crc32 = new CRC32();
269                    crc32.update(extendedHeaderBytes);
270                    if (extendedHeaderCrc32 != crc32.getValue()) {
271                        throw new IOException("Extended header CRC32 verification failure");
272                    }
273                    extendedHeaders.add(extendedHeaderBytes);
274                }
275                localFileHeader.extendedHeaders = extendedHeaders.toArray(new byte[0][]);
276
277                return localFileHeader;
278            }
279        }
280    }
281
282    private MainHeader readMainHeader() throws IOException {
283        final byte[] basicHeaderBytes = readHeader();
284        if (basicHeaderBytes == null) {
285            throw new IOException("Archive ends without any headers");
286        }
287        final DataInputStream basicHeader = new DataInputStream(
288                new ByteArrayInputStream(basicHeaderBytes));
289
290        final int firstHeaderSize = basicHeader.readUnsignedByte();
291        final byte[] firstHeaderBytes = readRange(basicHeader, firstHeaderSize - 1);
292        pushedBackBytes(firstHeaderBytes.length);
293
294        final DataInputStream firstHeader = new DataInputStream(
295                new ByteArrayInputStream(firstHeaderBytes));
296
297        final MainHeader hdr = new MainHeader();
298        hdr.archiverVersionNumber = firstHeader.readUnsignedByte();
299        hdr.minVersionToExtract = firstHeader.readUnsignedByte();
300        hdr.hostOS = firstHeader.readUnsignedByte();
301        hdr.arjFlags = firstHeader.readUnsignedByte();
302        hdr.securityVersion = firstHeader.readUnsignedByte();
303        hdr.fileType = firstHeader.readUnsignedByte();
304        hdr.reserved = firstHeader.readUnsignedByte();
305        hdr.dateTimeCreated = read32(firstHeader);
306        hdr.dateTimeModified = read32(firstHeader);
307        hdr.archiveSize = 0xffffFFFFL & read32(firstHeader);
308        hdr.securityEnvelopeFilePosition = read32(firstHeader);
309        hdr.fileSpecPosition = read16(firstHeader);
310        hdr.securityEnvelopeLength = read16(firstHeader);
311        pushedBackBytes(20); // count has already counted them via readRange
312        hdr.encryptionVersion = firstHeader.readUnsignedByte();
313        hdr.lastChapter = firstHeader.readUnsignedByte();
314
315        if (firstHeaderSize >= 33) {
316            hdr.arjProtectionFactor = firstHeader.readUnsignedByte();
317            hdr.arjFlags2 = firstHeader.readUnsignedByte();
318            firstHeader.readUnsignedByte();
319            firstHeader.readUnsignedByte();
320        }
321
322        hdr.name = readString(basicHeader);
323        hdr.comment = readString(basicHeader);
324
325        final  int extendedHeaderSize = read16(in);
326        if (extendedHeaderSize > 0) {
327            hdr.extendedHeaderBytes = readRange(in, extendedHeaderSize);
328            final long extendedHeaderCrc32 = 0xffffFFFFL & read32(in);
329            final CRC32 crc32 = new CRC32();
330            crc32.update(hdr.extendedHeaderBytes);
331            if (extendedHeaderCrc32 != crc32.getValue()) {
332                throw new IOException("Extended header CRC32 verification failure");
333            }
334        }
335
336        return hdr;
337    }
338
339    private byte[] readRange(final InputStream in, final int len)
340        throws IOException {
341        final byte[] b = IOUtils.readRange(in, len);
342        count(b.length);
343        if (b.length < len) {
344            throw new EOFException();
345        }
346        return b;
347    }
348
349    private String readString(final DataInputStream dataIn) throws IOException {
350        try (final ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
351            int nextByte;
352            while ((nextByte = dataIn.readUnsignedByte()) != 0) {
353                buffer.write(nextByte);
354            }
355            return buffer.toString(Charsets.toCharset(charsetName).name());
356        }
357    }
358}