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