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