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