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