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.IOUtils;
32  import org.apache.commons.io.input.BoundedInputStream;
33  import org.apache.commons.io.input.ChecksumInputStream;
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             // @formatter:off
142             currentInputStream = BoundedInputStream.builder()
143                     .setInputStream(dis)
144                     .setMaxCount(currentLocalFileHeader.compressedSize)
145                     .setPropagateClose(false)
146                     .get();
147             // @formatter:on
148             if (currentLocalFileHeader.method == LocalFileHeader.Methods.STORED) {
149                 // @formatter:off
150                 currentInputStream = ChecksumInputStream.builder()
151                         .setChecksum(new CRC32())
152                         .setInputStream(currentInputStream)
153                         .setCountThreshold(currentLocalFileHeader.originalSize)
154                         .setExpectedChecksumValue(currentLocalFileHeader.originalCrc32)
155                         .get();
156                 // @formatter:on
157             }
158             return new ArjArchiveEntry(currentLocalFileHeader);
159         }
160         currentInputStream = null;
161         return null;
162     }
163 
164     @Override
165     public int read(final byte[] b, final int off, final int len) throws IOException {
166         if (len == 0) {
167             return 0;
168         }
169         if (currentLocalFileHeader == null) {
170             throw new IllegalStateException("No current arj entry");
171         }
172         if (currentLocalFileHeader.method != LocalFileHeader.Methods.STORED) {
173             throw new IOException("Unsupported compression method " + currentLocalFileHeader.method);
174         }
175         return currentInputStream.read(b, off, len);
176     }
177 
178     private int read16(final DataInputStream dataIn) throws IOException {
179         final int value = dataIn.readUnsignedShort();
180         count(2);
181         return Integer.reverseBytes(value) >>> 16;
182     }
183 
184     private int read32(final DataInputStream dataIn) throws IOException {
185         final int value = dataIn.readInt();
186         count(4);
187         return Integer.reverseBytes(value);
188     }
189 
190     private int read8(final DataInputStream dataIn) throws IOException {
191         final int value = dataIn.readUnsignedByte();
192         count(1);
193         return value;
194     }
195 
196     private void readExtraData(final int firstHeaderSize, final DataInputStream firstHeader, final LocalFileHeader localFileHeader) throws IOException {
197         if (firstHeaderSize >= 33) {
198             localFileHeader.extendedFilePosition = read32(firstHeader);
199             if (firstHeaderSize >= 45) {
200                 localFileHeader.dateTimeAccessed = read32(firstHeader);
201                 localFileHeader.dateTimeCreated = read32(firstHeader);
202                 localFileHeader.originalSizeEvenForVolumes = read32(firstHeader);
203                 pushedBackBytes(12);
204             }
205             pushedBackBytes(4);
206         }
207     }
208 
209     private byte[] readHeader() throws IOException {
210         boolean found = false;
211         byte[] basicHeaderBytes = null;
212         do {
213             int first;
214             int second = read8(dis);
215             do {
216                 first = second;
217                 second = read8(dis);
218             } while (first != ARJ_MAGIC_1 && second != ARJ_MAGIC_2);
219             final int basicHeaderSize = read16(dis);
220             if (basicHeaderSize == 0) {
221                 // end of archive
222                 return null;
223             }
224             if (basicHeaderSize <= 2600) {
225                 basicHeaderBytes = readRange(dis, basicHeaderSize);
226                 final long basicHeaderCrc32 = read32(dis) & 0xFFFFFFFFL;
227                 final CRC32 crc32 = new CRC32();
228                 crc32.update(basicHeaderBytes);
229                 if (basicHeaderCrc32 == crc32.getValue()) {
230                     found = true;
231                 }
232             }
233         } while (!found);
234         return basicHeaderBytes;
235     }
236 
237     private LocalFileHeader readLocalFileHeader() throws IOException {
238         final byte[] basicHeaderBytes = readHeader();
239         if (basicHeaderBytes == null) {
240             return null;
241         }
242         try (DataInputStream basicHeader = new DataInputStream(new ByteArrayInputStream(basicHeaderBytes))) {
243 
244             final int firstHeaderSize = basicHeader.readUnsignedByte();
245             final byte[] firstHeaderBytes = readRange(basicHeader, firstHeaderSize - 1);
246             pushedBackBytes(firstHeaderBytes.length);
247             try (DataInputStream firstHeader = new DataInputStream(new ByteArrayInputStream(firstHeaderBytes))) {
248 
249                 final LocalFileHeader localFileHeader = new LocalFileHeader();
250                 localFileHeader.archiverVersionNumber = firstHeader.readUnsignedByte();
251                 localFileHeader.minVersionToExtract = firstHeader.readUnsignedByte();
252                 localFileHeader.hostOS = firstHeader.readUnsignedByte();
253                 localFileHeader.arjFlags = firstHeader.readUnsignedByte();
254                 localFileHeader.method = firstHeader.readUnsignedByte();
255                 localFileHeader.fileType = firstHeader.readUnsignedByte();
256                 localFileHeader.reserved = firstHeader.readUnsignedByte();
257                 localFileHeader.dateTimeModified = read32(firstHeader);
258                 localFileHeader.compressedSize = 0xffffFFFFL & read32(firstHeader);
259                 localFileHeader.originalSize = 0xffffFFFFL & read32(firstHeader);
260                 localFileHeader.originalCrc32 = 0xffffFFFFL & read32(firstHeader);
261                 localFileHeader.fileSpecPosition = read16(firstHeader);
262                 localFileHeader.fileAccessMode = read16(firstHeader);
263                 pushedBackBytes(20);
264                 localFileHeader.firstChapter = firstHeader.readUnsignedByte();
265                 localFileHeader.lastChapter = firstHeader.readUnsignedByte();
266 
267                 readExtraData(firstHeaderSize, firstHeader, localFileHeader);
268 
269                 localFileHeader.name = readString(basicHeader);
270                 localFileHeader.comment = readString(basicHeader);
271 
272                 final ArrayList<byte[]> extendedHeaders = new ArrayList<>();
273                 int extendedHeaderSize;
274                 while ((extendedHeaderSize = read16(dis)) > 0) {
275                     final byte[] extendedHeaderBytes = readRange(dis, extendedHeaderSize);
276                     final long extendedHeaderCrc32 = 0xffffFFFFL & read32(dis);
277                     final CRC32 crc32 = new CRC32();
278                     crc32.update(extendedHeaderBytes);
279                     if (extendedHeaderCrc32 != crc32.getValue()) {
280                         throw new IOException("Extended header CRC32 verification failure");
281                     }
282                     extendedHeaders.add(extendedHeaderBytes);
283                 }
284                 localFileHeader.extendedHeaders = extendedHeaders.toArray(new byte[0][]);
285 
286                 return localFileHeader;
287             }
288         }
289     }
290 
291     private MainHeader readMainHeader() throws IOException {
292         final byte[] basicHeaderBytes = readHeader();
293         if (basicHeaderBytes == null) {
294             throw new IOException("Archive ends without any headers");
295         }
296         final DataInputStream basicHeader = new DataInputStream(new ByteArrayInputStream(basicHeaderBytes));
297 
298         final int firstHeaderSize = basicHeader.readUnsignedByte();
299         final byte[] firstHeaderBytes = readRange(basicHeader, firstHeaderSize - 1);
300         pushedBackBytes(firstHeaderBytes.length);
301 
302         final DataInputStream firstHeader = new DataInputStream(new ByteArrayInputStream(firstHeaderBytes));
303 
304         final MainHeader hdr = new MainHeader();
305         hdr.archiverVersionNumber = firstHeader.readUnsignedByte();
306         hdr.minVersionToExtract = firstHeader.readUnsignedByte();
307         hdr.hostOS = firstHeader.readUnsignedByte();
308         hdr.arjFlags = firstHeader.readUnsignedByte();
309         hdr.securityVersion = firstHeader.readUnsignedByte();
310         hdr.fileType = firstHeader.readUnsignedByte();
311         hdr.reserved = firstHeader.readUnsignedByte();
312         hdr.dateTimeCreated = read32(firstHeader);
313         hdr.dateTimeModified = read32(firstHeader);
314         hdr.archiveSize = 0xffffFFFFL & read32(firstHeader);
315         hdr.securityEnvelopeFilePosition = read32(firstHeader);
316         hdr.fileSpecPosition = read16(firstHeader);
317         hdr.securityEnvelopeLength = read16(firstHeader);
318         pushedBackBytes(20); // count has already counted them via readRange
319         hdr.encryptionVersion = firstHeader.readUnsignedByte();
320         hdr.lastChapter = firstHeader.readUnsignedByte();
321 
322         if (firstHeaderSize >= 33) {
323             hdr.arjProtectionFactor = firstHeader.readUnsignedByte();
324             hdr.arjFlags2 = firstHeader.readUnsignedByte();
325             firstHeader.readUnsignedByte();
326             firstHeader.readUnsignedByte();
327         }
328 
329         hdr.name = readString(basicHeader);
330         hdr.comment = readString(basicHeader);
331 
332         final int extendedHeaderSize = read16(dis);
333         if (extendedHeaderSize > 0) {
334             hdr.extendedHeaderBytes = readRange(dis, extendedHeaderSize);
335             final long extendedHeaderCrc32 = 0xffffFFFFL & read32(dis);
336             final CRC32 crc32 = new CRC32();
337             crc32.update(hdr.extendedHeaderBytes);
338             if (extendedHeaderCrc32 != crc32.getValue()) {
339                 throw new IOException("Extended header CRC32 verification failure");
340             }
341         }
342 
343         return hdr;
344     }
345 
346     private byte[] readRange(final InputStream in, final int len) throws IOException {
347         final byte[] b = IOUtils.readRange(in, len);
348         count(b.length);
349         if (b.length < len) {
350             throw new EOFException();
351         }
352         return b;
353     }
354 
355     private String readString(final DataInputStream dataIn) throws IOException {
356         try (ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
357             int nextByte;
358             while ((nextByte = dataIn.readUnsignedByte()) != 0) {
359                 buffer.write(nextByte);
360             }
361             return buffer.toString(getCharset().name());
362         }
363     }
364 }