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