• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
ArjArchiveInputStream(final InputStream inputStream, final String charsetName)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      */
ArjArchiveInputStream(final InputStream inputStream)81     public ArjArchiveInputStream(final InputStream inputStream)
82             throws ArchiveException {
83         this(inputStream, "CP437");
84     }
85 
86     @Override
close()87     public void close() throws IOException {
88         in.close();
89     }
90 
read8(final DataInputStream dataIn)91     private int read8(final DataInputStream dataIn) throws IOException {
92         final int value = dataIn.readUnsignedByte();
93         count(1);
94         return value;
95     }
96 
read16(final DataInputStream dataIn)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 
read32(final DataInputStream dataIn)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 
readString(final DataInputStream dataIn)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 
readFully(final DataInputStream dataIn, final byte[] b)122     private void readFully(final DataInputStream dataIn, final byte[] b)
123         throws IOException {
124         dataIn.readFully(b);
125         count(b.length);
126     }
127 
readHeader()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 
readMainHeader()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 
readLocalFileHeader()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 
readExtraData(final int firstHeaderSize, final DataInputStream firstHeader, final LocalFileHeader localFileHeader)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      */
matches(final byte[] signature, final int length)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      */
getArchiveName()302     public String getArchiveName() {
303         return mainHeader.name;
304     }
305 
306     /**
307      * Gets the archive's comment.
308      * @return the archive's comment
309      */
getArchiveComment()310     public String getArchiveComment() {
311         return mainHeader.comment;
312     }
313 
314     @Override
getNextEntry()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
canReadEntryData(final ArchiveEntry ae)338     public boolean canReadEntryData(final ArchiveEntry ae) {
339         return ae instanceof ArjArchiveEntry
340             && ((ArjArchiveEntry) ae).getMethod() == LocalFileHeader.Methods.STORED;
341     }
342 
343     @Override
read(final byte[] b, final int off, final int len)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