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