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