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 (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        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        } else {
118            // intentionally using the default encoding as that's the contract for a null charsetName
119            return new String(buffer.toByteArray());
120        }
121    }
122    
123    private void readFully(final DataInputStream dataIn, byte[] b)
124        throws IOException {
125        dataIn.readFully(b);
126        count(b.length);
127    }
128    
129    private byte[] readHeader() throws IOException {
130        boolean found = false;
131        byte[] basicHeaderBytes = null;
132        do {
133            int first = 0;
134            int second = read8(in);
135            do {
136                first = second;
137                second = read8(in);
138            } while (first != ARJ_MAGIC_1 && second != ARJ_MAGIC_2);
139            final int basicHeaderSize = read16(in);
140            if (basicHeaderSize == 0) {
141                // end of archive
142                return null;
143            }
144            if (basicHeaderSize <= 2600) {
145                basicHeaderBytes = new byte[basicHeaderSize];
146                readFully(in, basicHeaderBytes);
147                final long basicHeaderCrc32 = read32(in) & 0xFFFFFFFFL;
148                final CRC32 crc32 = new CRC32();
149                crc32.update(basicHeaderBytes);
150                if (basicHeaderCrc32 == crc32.getValue()) {
151                    found = true;
152                }
153            }
154        } while (!found);
155        return basicHeaderBytes;
156    }
157    
158    private MainHeader readMainHeader() throws IOException {
159        final byte[] basicHeaderBytes = readHeader();
160        if (basicHeaderBytes == null) {
161            throw new IOException("Archive ends without any headers");
162        }
163        final DataInputStream basicHeader = new DataInputStream(
164                new ByteArrayInputStream(basicHeaderBytes));
165        
166        final int firstHeaderSize = basicHeader.readUnsignedByte();
167        final byte[] firstHeaderBytes = new byte[firstHeaderSize - 1];
168        basicHeader.readFully(firstHeaderBytes);
169        final DataInputStream firstHeader = new DataInputStream(
170                new ByteArrayInputStream(firstHeaderBytes));
171        
172        final MainHeader hdr = new MainHeader();
173        hdr.archiverVersionNumber = firstHeader.readUnsignedByte();
174        hdr.minVersionToExtract = firstHeader.readUnsignedByte();
175        hdr.hostOS = firstHeader.readUnsignedByte();
176        hdr.arjFlags = firstHeader.readUnsignedByte();
177        hdr.securityVersion = firstHeader.readUnsignedByte();
178        hdr.fileType = firstHeader.readUnsignedByte();
179        hdr.reserved = firstHeader.readUnsignedByte();
180        hdr.dateTimeCreated = read32(firstHeader);
181        hdr.dateTimeModified = read32(firstHeader);
182        hdr.archiveSize = 0xffffFFFFL & read32(firstHeader);
183        hdr.securityEnvelopeFilePosition = read32(firstHeader);
184        hdr.fileSpecPosition = read16(firstHeader);
185        hdr.securityEnvelopeLength = read16(firstHeader);
186        pushedBackBytes(20); // count has already counted them via readFully
187        hdr.encryptionVersion = firstHeader.readUnsignedByte();
188        hdr.lastChapter = firstHeader.readUnsignedByte();
189        
190        if (firstHeaderSize >= 33) {
191            hdr.arjProtectionFactor = firstHeader.readUnsignedByte();
192            hdr.arjFlags2 = firstHeader.readUnsignedByte();
193            firstHeader.readUnsignedByte();
194            firstHeader.readUnsignedByte();
195        }
196
197        hdr.name = readString(basicHeader);
198        hdr.comment = readString(basicHeader);
199        
200        final  int extendedHeaderSize = read16(in);
201        if (extendedHeaderSize > 0) {
202            hdr.extendedHeaderBytes = new byte[extendedHeaderSize];
203            readFully(in, hdr.extendedHeaderBytes);
204            final long extendedHeaderCrc32 = 0xffffFFFFL & read32(in);
205            final CRC32 crc32 = new CRC32();
206            crc32.update(hdr.extendedHeaderBytes);
207            if (extendedHeaderCrc32 != crc32.getValue()) {
208                throw new IOException("Extended header CRC32 verification failure");
209            }
210        }
211        
212        return hdr;
213    }
214    
215    private LocalFileHeader readLocalFileHeader() throws IOException {
216        final byte[] basicHeaderBytes = readHeader();
217        if (basicHeaderBytes == null) {
218            return null;
219        }
220        final DataInputStream basicHeader = new DataInputStream(
221                new ByteArrayInputStream(basicHeaderBytes));
222        
223        final int firstHeaderSize = basicHeader.readUnsignedByte();
224        final byte[] firstHeaderBytes = new byte[firstHeaderSize - 1];
225        basicHeader.readFully(firstHeaderBytes);
226        final DataInputStream firstHeader = new DataInputStream(
227                new ByteArrayInputStream(firstHeaderBytes));
228
229        final LocalFileHeader localFileHeader = new LocalFileHeader();
230        localFileHeader.archiverVersionNumber = firstHeader.readUnsignedByte();
231        localFileHeader.minVersionToExtract = firstHeader.readUnsignedByte();
232        localFileHeader.hostOS = firstHeader.readUnsignedByte();
233        localFileHeader.arjFlags = firstHeader.readUnsignedByte();
234        localFileHeader.method = firstHeader.readUnsignedByte();
235        localFileHeader.fileType = firstHeader.readUnsignedByte();
236        localFileHeader.reserved = firstHeader.readUnsignedByte();
237        localFileHeader.dateTimeModified = read32(firstHeader);
238        localFileHeader.compressedSize = 0xffffFFFFL & read32(firstHeader);
239        localFileHeader.originalSize = 0xffffFFFFL & read32(firstHeader);
240        localFileHeader.originalCrc32 = 0xffffFFFFL & read32(firstHeader);
241        localFileHeader.fileSpecPosition = read16(firstHeader);
242        localFileHeader.fileAccessMode = read16(firstHeader);
243        pushedBackBytes(20);
244        localFileHeader.firstChapter = firstHeader.readUnsignedByte();
245        localFileHeader.lastChapter = firstHeader.readUnsignedByte();
246        
247        readExtraData(firstHeaderSize, firstHeader, localFileHeader);
248
249        localFileHeader.name = readString(basicHeader);
250        localFileHeader.comment = readString(basicHeader);
251
252        ArrayList<byte[]> extendedHeaders = new ArrayList<byte[]>();
253        int extendedHeaderSize;
254        while ((extendedHeaderSize = read16(in)) > 0) {
255            final byte[] extendedHeaderBytes = new byte[extendedHeaderSize];
256            readFully(in, extendedHeaderBytes);
257            final long extendedHeaderCrc32 = 0xffffFFFFL & read32(in);
258            final CRC32 crc32 = new CRC32();
259            crc32.update(extendedHeaderBytes);
260            if (extendedHeaderCrc32 != crc32.getValue()) {
261                throw new IOException("Extended header CRC32 verification failure");
262            }
263            extendedHeaders.add(extendedHeaderBytes);
264        }
265        localFileHeader.extendedHeaders = extendedHeaders.toArray(new byte[extendedHeaders.size()][]);
266        
267        return localFileHeader;
268    }
269    
270    private void readExtraData(int firstHeaderSize, DataInputStream firstHeader,
271                               LocalFileHeader localFileHeader) throws IOException {
272        if (firstHeaderSize >= 33) {
273            localFileHeader.extendedFilePosition = read32(firstHeader);
274            if (firstHeaderSize >= 45) {
275                localFileHeader.dateTimeAccessed = read32(firstHeader);
276                localFileHeader.dateTimeCreated = read32(firstHeader);
277                localFileHeader.originalSizeEvenForVolumes = read32(firstHeader);
278                pushedBackBytes(12);
279            }
280            pushedBackBytes(4);
281        }
282    }
283
284    /**
285     * Checks if the signature matches what is expected for an arj file.
286     *
287     * @param signature
288     *            the bytes to check
289     * @param length
290     *            the number of bytes to check
291     * @return true, if this stream is an arj archive stream, false otherwise
292     */
293    public static boolean matches(final byte[] signature, final int length) {
294        return length >= 2 &&
295                (0xff & signature[0]) == ARJ_MAGIC_1 &&
296                (0xff & signature[1]) == ARJ_MAGIC_2;
297    }
298    
299    /**
300     * Gets the archive's recorded name.
301     * @return the archive's name
302     */
303    public String getArchiveName() {
304        return mainHeader.name;
305    }
306    
307    /**
308     * Gets the archive's comment.
309     * @return the archive's comment
310     */
311    public String getArchiveComment() {
312        return mainHeader.comment;
313    }
314    
315    @Override
316    public ArjArchiveEntry getNextEntry() throws IOException {
317        if (currentInputStream != null) {
318            // return value ignored as IOUtils.skip ensures the stream is drained completely
319            IOUtils.skip(currentInputStream, Long.MAX_VALUE);
320            currentInputStream.close();
321            currentLocalFileHeader = null;
322            currentInputStream = null;
323        }
324        
325        currentLocalFileHeader = readLocalFileHeader();
326        if (currentLocalFileHeader != null) {
327            currentInputStream = new BoundedInputStream(in, currentLocalFileHeader.compressedSize);
328            if (currentLocalFileHeader.method == LocalFileHeader.Methods.STORED) {
329                currentInputStream = new CRC32VerifyingInputStream(currentInputStream,
330                        currentLocalFileHeader.originalSize, currentLocalFileHeader.originalCrc32);
331            }
332            return new ArjArchiveEntry(currentLocalFileHeader);
333        } else {
334            currentInputStream = null;
335            return null;
336        }
337    }
338
339    @Override
340    public boolean canReadEntryData(ArchiveEntry ae) {
341        return ae instanceof ArjArchiveEntry
342            && ((ArjArchiveEntry) ae).getMethod() == LocalFileHeader.Methods.STORED;
343    }
344
345    @Override
346    public int read(final byte[] b, final int off, final int len) throws IOException {
347        if (currentLocalFileHeader == null) {
348            throw new IllegalStateException("No current arj entry");
349        }
350        if (currentLocalFileHeader.method != LocalFileHeader.Methods.STORED) {
351            throw new IOException("Unsupported compression method " + currentLocalFileHeader.method);
352        }
353        return currentInputStream.read(b, off, len);
354    }
355}