    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}