View Javadoc
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  package org.apache.commons.compress.archivers.arj;
18  
19  import java.io.ByteArrayInputStream;
20  import java.io.ByteArrayOutputStream;
21  import java.io.DataInputStream;
22  import java.io.EOFException;
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   * <ul>
38   * <li><a href="https://github.com/FarGroup/FarManager/blob/master/plugins/multiarc/arc.doc/arj.txt">Reference 1</a></li>
39   * <li><a href="http://www.fileformat.info/format/arj/corion.htm">Reference 2</a></li>
40   * </ul>
41   *
42   * @NotThreadSafe
43   * @since 1.6
44   */
45  public class ArjArchiveInputStream extends ArchiveInputStream<ArjArchiveEntry> {
46  
47      private static final String ENCODING_NAME = "CP437";
48      private static final int ARJ_MAGIC_1 = 0x60;
49      private static final int ARJ_MAGIC_2 = 0xEA;
50  
51      /**
52       * Checks if the signature matches what is expected for an arj file.
53       *
54       * @param signature the bytes to check
55       * @param length    the number of bytes to check
56       * @return true, if this stream is an arj archive stream, false otherwise
57       */
58      public static boolean matches(final byte[] signature, final int length) {
59          return length >= 2 && (0xff & signature[0]) == ARJ_MAGIC_1 && (0xff & signature[1]) == ARJ_MAGIC_2;
60      }
61  
62      private final DataInputStream dis;
63      private final MainHeader mainHeader;
64      private LocalFileHeader currentLocalFileHeader;
65      private InputStream currentInputStream;
66  
67      /**
68       * Constructs the ArjInputStream, taking ownership of the inputStream that is passed in, and using the CP437 character encoding.
69       *
70       * @param inputStream the underlying stream, whose ownership is taken
71       * @throws ArchiveException if an exception occurs while reading
72       */
73      public ArjArchiveInputStream(final InputStream inputStream) throws ArchiveException {
74          this(inputStream, ENCODING_NAME);
75      }
76  
77      /**
78       * Constructs the ArjInputStream, taking ownership of the inputStream that is passed in.
79       *
80       * @param inputStream the underlying stream, whose ownership is taken
81       * @param charsetName the charset used for file names and comments in the archive. May be {@code null} to use the platform default.
82       * @throws ArchiveException if an exception occurs while reading
83       */
84      public ArjArchiveInputStream(final InputStream inputStream, final String charsetName) throws ArchiveException {
85          super(inputStream, charsetName);
86          in = dis = new DataInputStream(inputStream);
87          try {
88              mainHeader = readMainHeader();
89              if ((mainHeader.arjFlags & MainHeader.Flags.GARBLED) != 0) {
90                  throw new ArchiveException("Encrypted ARJ files are unsupported");
91              }
92              if ((mainHeader.arjFlags & MainHeader.Flags.VOLUME) != 0) {
93                  throw new ArchiveException("Multi-volume ARJ files are unsupported");
94              }
95          } catch (final IOException ioException) {
96              throw new ArchiveException(ioException.getMessage(), ioException);
97          }
98      }
99  
100     @Override
101     public boolean canReadEntryData(final ArchiveEntry ae) {
102         return ae instanceof ArjArchiveEntry && ((ArjArchiveEntry) ae).getMethod() == LocalFileHeader.Methods.STORED;
103     }
104 
105     @Override
106     public void close() throws IOException {
107         dis.close();
108     }
109 
110     /**
111      * Gets the archive's comment.
112      *
113      * @return the archive's comment
114      */
115     public String getArchiveComment() {
116         return mainHeader.comment;
117     }
118 
119     /**
120      * Gets the archive's recorded name.
121      *
122      * @return the archive's name
123      */
124     public String getArchiveName() {
125         return mainHeader.name;
126     }
127 
128     @Override
129     public ArjArchiveEntry getNextEntry() throws IOException {
130         if (currentInputStream != null) {
131             // return value ignored as IOUtils.skip ensures the stream is drained completely
132             final InputStream input = currentInputStream;
133             org.apache.commons.io.IOUtils.skip(input, Long.MAX_VALUE);
134             currentInputStream.close();
135             currentLocalFileHeader = null;
136             currentInputStream = null;
137         }
138 
139         currentLocalFileHeader = readLocalFileHeader();
140         if (currentLocalFileHeader != null) {
141             currentInputStream = new BoundedInputStream(dis, currentLocalFileHeader.compressedSize);
142             if (currentLocalFileHeader.method == LocalFileHeader.Methods.STORED) {
143                 currentInputStream = new CRC32VerifyingInputStream(currentInputStream, currentLocalFileHeader.originalSize,
144                         currentLocalFileHeader.originalCrc32);
145             }
146             return new ArjArchiveEntry(currentLocalFileHeader);
147         }
148         currentInputStream = null;
149         return null;
150     }
151 
152     @Override
153     public int read(final byte[] b, final int off, final int len) throws IOException {
154         if (len == 0) {
155             return 0;
156         }
157         if (currentLocalFileHeader == null) {
158             throw new IllegalStateException("No current arj entry");
159         }
160         if (currentLocalFileHeader.method != LocalFileHeader.Methods.STORED) {
161             throw new IOException("Unsupported compression method " + currentLocalFileHeader.method);
162         }
163         return currentInputStream.read(b, off, len);
164     }
165 
166     private int read16(final DataInputStream dataIn) throws IOException {
167         final int value = dataIn.readUnsignedShort();
168         count(2);
169         return Integer.reverseBytes(value) >>> 16;
170     }
171 
172     private int read32(final DataInputStream dataIn) throws IOException {
173         final int value = dataIn.readInt();
174         count(4);
175         return Integer.reverseBytes(value);
176     }
177 
178     private int read8(final DataInputStream dataIn) throws IOException {
179         final int value = dataIn.readUnsignedByte();
180         count(1);
181         return value;
182     }
183 
184     private void readExtraData(final int firstHeaderSize, final DataInputStream firstHeader, final LocalFileHeader localFileHeader) throws IOException {
185         if (firstHeaderSize >= 33) {
186             localFileHeader.extendedFilePosition = read32(firstHeader);
187             if (firstHeaderSize >= 45) {
188                 localFileHeader.dateTimeAccessed = read32(firstHeader);
189                 localFileHeader.dateTimeCreated = read32(firstHeader);
190                 localFileHeader.originalSizeEvenForVolumes = read32(firstHeader);
191                 pushedBackBytes(12);
192             }
193             pushedBackBytes(4);
194         }
195     }
196 
197     private byte[] readHeader() throws IOException {
198         boolean found = false;
199         byte[] basicHeaderBytes = null;
200         do {
201             int first;
202             int second = read8(dis);
203             do {
204                 first = second;
205                 second = read8(dis);
206             } while (first != ARJ_MAGIC_1 && second != ARJ_MAGIC_2);
207             final int basicHeaderSize = read16(dis);
208             if (basicHeaderSize == 0) {
209                 // end of archive
210                 return null;
211             }
212             if (basicHeaderSize <= 2600) {
213                 basicHeaderBytes = readRange(dis, basicHeaderSize);
214                 final long basicHeaderCrc32 = read32(dis) & 0xFFFFFFFFL;
215                 final CRC32 crc32 = new CRC32();
216                 crc32.update(basicHeaderBytes);
217                 if (basicHeaderCrc32 == crc32.getValue()) {
218                     found = true;
219                 }
220             }
221         } while (!found);
222         return basicHeaderBytes;
223     }
224 
225     private LocalFileHeader readLocalFileHeader() throws IOException {
226         final byte[] basicHeaderBytes = readHeader();
227         if (basicHeaderBytes == null) {
228             return null;
229         }
230         try (DataInputStream basicHeader = new DataInputStream(new ByteArrayInputStream(basicHeaderBytes))) {
231 
232             final int firstHeaderSize = basicHeader.readUnsignedByte();
233             final byte[] firstHeaderBytes = readRange(basicHeader, firstHeaderSize - 1);
234             pushedBackBytes(firstHeaderBytes.length);
235             try (DataInputStream firstHeader = new DataInputStream(new ByteArrayInputStream(firstHeaderBytes))) {
236 
237                 final LocalFileHeader localFileHeader = new LocalFileHeader();
238                 localFileHeader.archiverVersionNumber = firstHeader.readUnsignedByte();
239                 localFileHeader.minVersionToExtract = firstHeader.readUnsignedByte();
240                 localFileHeader.hostOS = firstHeader.readUnsignedByte();
241                 localFileHeader.arjFlags = firstHeader.readUnsignedByte();
242                 localFileHeader.method = firstHeader.readUnsignedByte();
243                 localFileHeader.fileType = firstHeader.readUnsignedByte();
244                 localFileHeader.reserved = firstHeader.readUnsignedByte();
245                 localFileHeader.dateTimeModified = read32(firstHeader);
246                 localFileHeader.compressedSize = 0xffffFFFFL & read32(firstHeader);
247                 localFileHeader.originalSize = 0xffffFFFFL & read32(firstHeader);
248                 localFileHeader.originalCrc32 = 0xffffFFFFL & read32(firstHeader);
249                 localFileHeader.fileSpecPosition = read16(firstHeader);
250                 localFileHeader.fileAccessMode = read16(firstHeader);
251                 pushedBackBytes(20);
252                 localFileHeader.firstChapter = firstHeader.readUnsignedByte();
253                 localFileHeader.lastChapter = firstHeader.readUnsignedByte();
254 
255                 readExtraData(firstHeaderSize, firstHeader, localFileHeader);
256 
257                 localFileHeader.name = readString(basicHeader);
258                 localFileHeader.comment = readString(basicHeader);
259 
260                 final ArrayList<byte[]> extendedHeaders = new ArrayList<>();
261                 int extendedHeaderSize;
262                 while ((extendedHeaderSize = read16(dis)) > 0) {
263                     final byte[] extendedHeaderBytes = readRange(dis, extendedHeaderSize);
264                     final long extendedHeaderCrc32 = 0xffffFFFFL & read32(dis);
265                     final CRC32 crc32 = new CRC32();
266                     crc32.update(extendedHeaderBytes);
267                     if (extendedHeaderCrc32 != crc32.getValue()) {
268                         throw new IOException("Extended header CRC32 verification failure");
269                     }
270                     extendedHeaders.add(extendedHeaderBytes);
271                 }
272                 localFileHeader.extendedHeaders = extendedHeaders.toArray(new byte[0][]);
273 
274                 return localFileHeader;
275             }
276         }
277     }
278 
279     private MainHeader readMainHeader() throws IOException {
280         final byte[] basicHeaderBytes = readHeader();
281         if (basicHeaderBytes == null) {
282             throw new IOException("Archive ends without any headers");
283         }
284         final DataInputStream basicHeader = new DataInputStream(new ByteArrayInputStream(basicHeaderBytes));
285 
286         final int firstHeaderSize = basicHeader.readUnsignedByte();
287         final byte[] firstHeaderBytes = readRange(basicHeader, firstHeaderSize - 1);
288         pushedBackBytes(firstHeaderBytes.length);
289 
290         final DataInputStream firstHeader = new DataInputStream(new ByteArrayInputStream(firstHeaderBytes));
291 
292         final MainHeader hdr = new MainHeader();
293         hdr.archiverVersionNumber = firstHeader.readUnsignedByte();
294         hdr.minVersionToExtract = firstHeader.readUnsignedByte();
295         hdr.hostOS = firstHeader.readUnsignedByte();
296         hdr.arjFlags = firstHeader.readUnsignedByte();
297         hdr.securityVersion = firstHeader.readUnsignedByte();
298         hdr.fileType = firstHeader.readUnsignedByte();
299         hdr.reserved = firstHeader.readUnsignedByte();
300         hdr.dateTimeCreated = read32(firstHeader);
301         hdr.dateTimeModified = read32(firstHeader);
302         hdr.archiveSize = 0xffffFFFFL & read32(firstHeader);
303         hdr.securityEnvelopeFilePosition = read32(firstHeader);
304         hdr.fileSpecPosition = read16(firstHeader);
305         hdr.securityEnvelopeLength = read16(firstHeader);
306         pushedBackBytes(20); // count has already counted them via readRange
307         hdr.encryptionVersion = firstHeader.readUnsignedByte();
308         hdr.lastChapter = firstHeader.readUnsignedByte();
309 
310         if (firstHeaderSize >= 33) {
311             hdr.arjProtectionFactor = firstHeader.readUnsignedByte();
312             hdr.arjFlags2 = firstHeader.readUnsignedByte();
313             firstHeader.readUnsignedByte();
314             firstHeader.readUnsignedByte();
315         }
316 
317         hdr.name = readString(basicHeader);
318         hdr.comment = readString(basicHeader);
319 
320         final int extendedHeaderSize = read16(dis);
321         if (extendedHeaderSize > 0) {
322             hdr.extendedHeaderBytes = readRange(dis, extendedHeaderSize);
323             final long extendedHeaderCrc32 = 0xffffFFFFL & read32(dis);
324             final CRC32 crc32 = new CRC32();
325             crc32.update(hdr.extendedHeaderBytes);
326             if (extendedHeaderCrc32 != crc32.getValue()) {
327                 throw new IOException("Extended header CRC32 verification failure");
328             }
329         }
330 
331         return hdr;
332     }
333 
334     private byte[] readRange(final InputStream in, final int len) throws IOException {
335         final byte[] b = IOUtils.readRange(in, len);
336         count(b.length);
337         if (b.length < len) {
338             throw new EOFException();
339         }
340         return b;
341     }
342 
343     private String readString(final DataInputStream dataIn) throws IOException {
344         try (ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
345             int nextByte;
346             while ((nextByte = dataIn.readUnsignedByte()) != 0) {
347                 buffer.write(nextByte);
348             }
349             return buffer.toString(getCharset().name());
350         }
351     }
352 }