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