    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.IOUtils;
    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            currentInputStream = new BoundedInputStream(dis, currentLocalFileHeader.compressedSize);
    142            if (currentLocalFileHeader.method == LocalFileHeader.Methods.STORED) {
    143                currentInputStream = new CRC32VerifyingInputStream(currentInputStream, currentLocalFileHeader.originalSize,
    144                        currentLocalFileHeader.originalCrc32);
    145            }
    146            return new ArjArchiveEntry(currentLocalFileHeader);
    147        }
    148        currentInputStream = null;
    149        return null;
    150    }
    151
    152    @Override
    153    public int read(final byte[] b, final int off, final int len) throws IOException {
    154        if (len == 0) {
    155            return 0;
    156        }
    157        if (currentLocalFileHeader == null) {
    158            throw new IllegalStateException("No current arj entry");
    159        }
    160        if (currentLocalFileHeader.method != LocalFileHeader.Methods.STORED) {
    161            throw new IOException("Unsupported compression method " + currentLocalFileHeader.method);
    162        }
    163        return currentInputStream.read(b, off, len);
    164    }
    165
    166    private int read16(final DataInputStream dataIn) throws IOException {
    167        final int value = dataIn.readUnsignedShort();
    168        count(2);
    169        return Integer.reverseBytes(value) >>> 16;
    170    }
    171
    172    private int read32(final DataInputStream dataIn) throws IOException {
    173        final int value = dataIn.readInt();
    174        count(4);
    175        return Integer.reverseBytes(value);
    176    }
    177
    178    private int read8(final DataInputStream dataIn) throws IOException {
    179        final int value = dataIn.readUnsignedByte();
    180        count(1);
    181        return value;
    182    }
    183
    184    private void readExtraData(final int firstHeaderSize, final DataInputStream firstHeader, final LocalFileHeader localFileHeader) throws IOException {
    185        if (firstHeaderSize >= 33) {
    186            localFileHeader.extendedFilePosition = read32(firstHeader);
    187            if (firstHeaderSize >= 45) {
    188                localFileHeader.dateTimeAccessed = read32(firstHeader);
    189                localFileHeader.dateTimeCreated = read32(firstHeader);
    190                localFileHeader.originalSizeEvenForVolumes = read32(firstHeader);
    191                pushedBackBytes(12);
    192            }
    193            pushedBackBytes(4);
    194        }
    195    }
    196
    197    private byte[] readHeader() throws IOException {
    198        boolean found = false;
    199        byte[] basicHeaderBytes = null;
    200        do {
    201            int first;
    202            int second = read8(dis);
    203            do {
    204                first = second;
    205                second = read8(dis);
    206            } while (first != ARJ_MAGIC_1 && second != ARJ_MAGIC_2);
    207            final int basicHeaderSize = read16(dis);
    208            if (basicHeaderSize == 0) {
    209                // end of archive
    210                return null;
    211            }
    212            if (basicHeaderSize <= 2600) {
    213                basicHeaderBytes = readRange(dis, basicHeaderSize);
    214                final long basicHeaderCrc32 = read32(dis) & 0xFFFFFFFFL;
    215                final CRC32 crc32 = new CRC32();
    216                crc32.update(basicHeaderBytes);
    217                if (basicHeaderCrc32 == crc32.getValue()) {
    218                    found = true;
    219                }
    220            }
    221        } while (!found);
    222        return basicHeaderBytes;
    223    }
    224
    225    private LocalFileHeader readLocalFileHeader() throws IOException {
    226        final byte[] basicHeaderBytes = readHeader();
    227        if (basicHeaderBytes == null) {
    228            return null;
    229        }
    230        try (DataInputStream basicHeader = new DataInputStream(new ByteArrayInputStream(basicHeaderBytes))) {
    231
    232            final int firstHeaderSize = basicHeader.readUnsignedByte();
    233            final byte[] firstHeaderBytes = readRange(basicHeader, firstHeaderSize - 1);
    234            pushedBackBytes(firstHeaderBytes.length);
    235            try (DataInputStream firstHeader = new DataInputStream(new ByteArrayInputStream(firstHeaderBytes))) {
    236
    237                final LocalFileHeader localFileHeader = new LocalFileHeader();
    238                localFileHeader.archiverVersionNumber = firstHeader.readUnsignedByte();
    239                localFileHeader.minVersionToExtract = firstHeader.readUnsignedByte();
    240                localFileHeader.hostOS = firstHeader.readUnsignedByte();
    241                localFileHeader.arjFlags = firstHeader.readUnsignedByte();
    242                localFileHeader.method = firstHeader.readUnsignedByte();
    243                localFileHeader.fileType = firstHeader.readUnsignedByte();
    244                localFileHeader.reserved = firstHeader.readUnsignedByte();
    245                localFileHeader.dateTimeModified = read32(firstHeader);
    246                localFileHeader.compressedSize = 0xffffFFFFL & read32(firstHeader);
    247                localFileHeader.originalSize = 0xffffFFFFL & read32(firstHeader);
    248                localFileHeader.originalCrc32 = 0xffffFFFFL & read32(firstHeader);
    249                localFileHeader.fileSpecPosition = read16(firstHeader);
    250                localFileHeader.fileAccessMode = read16(firstHeader);
    251                pushedBackBytes(20);
    252                localFileHeader.firstChapter = firstHeader.readUnsignedByte();
    253                localFileHeader.lastChapter = firstHeader.readUnsignedByte();
    254
    255                readExtraData(firstHeaderSize, firstHeader, localFileHeader);
    256
    257                localFileHeader.name = readString(basicHeader);
    258                localFileHeader.comment = readString(basicHeader);
    259
    260                final ArrayList<byte[]> extendedHeaders = new ArrayList<>();
    261                int extendedHeaderSize;
    262                while ((extendedHeaderSize = read16(dis)) > 0) {
    263                    final byte[] extendedHeaderBytes = readRange(dis, extendedHeaderSize);
    264                    final long extendedHeaderCrc32 = 0xffffFFFFL & read32(dis);
    265                    final CRC32 crc32 = new CRC32();
    266                    crc32.update(extendedHeaderBytes);
    267                    if (extendedHeaderCrc32 != crc32.getValue()) {
    268                        throw new IOException("Extended header CRC32 verification failure");
    269                    }
    270                    extendedHeaders.add(extendedHeaderBytes);
    271                }
    272                localFileHeader.extendedHeaders = extendedHeaders.toArray(new byte[0][]);
    273
    274                return localFileHeader;
    275            }
    276        }
    277    }
    278
    279    private MainHeader readMainHeader() throws IOException {
    280        final byte[] basicHeaderBytes = readHeader();
    281        if (basicHeaderBytes == null) {
    282            throw new IOException("Archive ends without any headers");
    283        }
    284        final DataInputStream basicHeader = new DataInputStream(new ByteArrayInputStream(basicHeaderBytes));
    285
    286        final int firstHeaderSize = basicHeader.readUnsignedByte();
    287        final byte[] firstHeaderBytes = readRange(basicHeader, firstHeaderSize - 1);
    288        pushedBackBytes(firstHeaderBytes.length);
    289
    290        final DataInputStream firstHeader = new DataInputStream(new ByteArrayInputStream(firstHeaderBytes));
    291
    292        final MainHeader hdr = new MainHeader();
    293        hdr.archiverVersionNumber = firstHeader.readUnsignedByte();
    294        hdr.minVersionToExtract = firstHeader.readUnsignedByte();
    295        hdr.hostOS = firstHeader.readUnsignedByte();
    296        hdr.arjFlags = firstHeader.readUnsignedByte();
    297        hdr.securityVersion = firstHeader.readUnsignedByte();
    298        hdr.fileType = firstHeader.readUnsignedByte();
    299        hdr.reserved = firstHeader.readUnsignedByte();
    300        hdr.dateTimeCreated = read32(firstHeader);
    301        hdr.dateTimeModified = read32(firstHeader);
    302        hdr.archiveSize = 0xffffFFFFL & read32(firstHeader);
    303        hdr.securityEnvelopeFilePosition = read32(firstHeader);
    304        hdr.fileSpecPosition = read16(firstHeader);
    305        hdr.securityEnvelopeLength = read16(firstHeader);
    306        pushedBackBytes(20); // count has already counted them via readRange
    307        hdr.encryptionVersion = firstHeader.readUnsignedByte();
    308        hdr.lastChapter = firstHeader.readUnsignedByte();
    309
    310        if (firstHeaderSize >= 33) {
    311            hdr.arjProtectionFactor = firstHeader.readUnsignedByte();
    312            hdr.arjFlags2 = firstHeader.readUnsignedByte();
    313            firstHeader.readUnsignedByte();
    314            firstHeader.readUnsignedByte();
    315        }
    316
    317        hdr.name = readString(basicHeader);
    318        hdr.comment = readString(basicHeader);
    319
    320        final int extendedHeaderSize = read16(dis);
    321        if (extendedHeaderSize > 0) {
    322            hdr.extendedHeaderBytes = readRange(dis, extendedHeaderSize);
    323            final long extendedHeaderCrc32 = 0xffffFFFFL & read32(dis);
    324            final CRC32 crc32 = new CRC32();
    325            crc32.update(hdr.extendedHeaderBytes);
    326            if (extendedHeaderCrc32 != crc32.getValue()) {
    327                throw new IOException("Extended header CRC32 verification failure");
    328            }
    329        }
    330
    331        return hdr;
    332    }
    333
    334    private byte[] readRange(final InputStream in, final int len) throws IOException {
    335        final byte[] b = IOUtils.readRange(in, len);
    336        count(b.length);
    337        if (b.length < len) {
    338            throw new EOFException();
    339        }
    340        return b;
    341    }
    342
    343    private String readString(final DataInputStream dataIn) throws IOException {
    344        try (ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
    345            int nextByte;
    346            while ((nextByte = dataIn.readUnsignedByte()) != 0) {
    347                buffer.write(nextByte);
    348            }
    349            return buffer.toString(getCharset().name());
    350        }
    351    }
    352}