Home | History | Annotate | Download | only in arj
      1 /*
      2  *  Licensed to the Apache Software Foundation (ASF) under one or more
      3  *  contributor license agreements.  See the NOTICE file distributed with
      4  *  this work for additional information regarding copyright ownership.
      5  *  The ASF licenses this file to You under the Apache License, Version 2.0
      6  *  (the "License"); you may not use this file except in compliance with
      7  *  the License.  You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  *  Unless required by applicable law or agreed to in writing, software
     12  *  distributed under the License is distributed on an "AS IS" BASIS,
     13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  *  See the License for the specific language governing permissions and
     15  *  limitations under the License.
     16  *
     17  */
     18 package org.apache.commons.compress.archivers.arj;
     19 
     20 import java.io.ByteArrayInputStream;
     21 import java.io.ByteArrayOutputStream;
     22 import java.io.DataInputStream;
     23 import java.io.IOException;
     24 import java.io.InputStream;
     25 import java.util.ArrayList;
     26 import java.util.zip.CRC32;
     27 
     28 import org.apache.commons.compress.archivers.ArchiveEntry;
     29 import org.apache.commons.compress.archivers.ArchiveException;
     30 import org.apache.commons.compress.archivers.ArchiveInputStream;
     31 import org.apache.commons.compress.utils.BoundedInputStream;
     32 import org.apache.commons.compress.utils.CRC32VerifyingInputStream;
     33 import org.apache.commons.compress.utils.IOUtils;
     34 
     35 /**
     36  * Implements the "arj" archive format as an InputStream.
     37  * <p>
     38  * <a href="http://farmanager.com/svn/trunk/plugins/multiarc/arc.doc/arj.txt">Reference</a>
     39  * @NotThreadSafe
     40  * @since 1.6
     41  */
     42 public class ArjArchiveInputStream extends ArchiveInputStream {
     43     private static final int ARJ_MAGIC_1 = 0x60;
     44     private static final int ARJ_MAGIC_2 = 0xEA;
     45     private final DataInputStream in;
     46     private final String charsetName;
     47     private final MainHeader mainHeader;
     48     private LocalFileHeader currentLocalFileHeader = null;
     49     private InputStream currentInputStream = null;
     50 
     51     /**
     52      * Constructs the ArjInputStream, taking ownership of the inputStream that is passed in.
     53      * @param inputStream the underlying stream, whose ownership is taken
     54      * @param charsetName the charset used for file names and comments
     55      *   in the archive. May be {@code null} to use the platform default.
     56      * @throws ArchiveException if an exception occurs while reading
     57      */
     58     public ArjArchiveInputStream(final InputStream inputStream,
     59             final String charsetName) throws ArchiveException {
     60         in = new DataInputStream(inputStream);
     61         this.charsetName = charsetName;
     62         try {
     63             mainHeader = readMainHeader();
     64             if ((mainHeader.arjFlags & MainHeader.Flags.GARBLED) != 0) {
     65                 throw new ArchiveException("Encrypted ARJ files are unsupported");
     66             }
     67             if ((mainHeader.arjFlags & MainHeader.Flags.VOLUME) != 0) {
     68                 throw new ArchiveException("Multi-volume ARJ files are unsupported");
     69             }
     70         } catch (final IOException ioException) {
     71             throw new ArchiveException(ioException.getMessage(), ioException);
     72         }
     73     }
     74 
     75     /**
     76      * Constructs the ArjInputStream, taking ownership of the inputStream that is passed in,
     77      * and using the CP437 character encoding.
     78      * @param inputStream the underlying stream, whose ownership is taken
     79      * @throws ArchiveException if an exception occurs while reading
     80      */
     81     public ArjArchiveInputStream(final InputStream inputStream)
     82             throws ArchiveException {
     83         this(inputStream, "CP437");
     84     }
     85 
     86     @Override
     87     public void close() throws IOException {
     88         in.close();
     89     }
     90 
     91     private int read8(final DataInputStream dataIn) throws IOException {
     92         final int value = dataIn.readUnsignedByte();
     93         count(1);
     94         return value;
     95     }
     96 
     97     private int read16(final DataInputStream dataIn) throws IOException {
     98         final int value = dataIn.readUnsignedShort();
     99         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 }
    354