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