View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   https://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.commons.compress.archivers.arj;
20  
21  import java.io.ByteArrayInputStream;
22  import java.io.ByteArrayOutputStream;
23  import java.io.DataInputStream;
24  import java.io.EOFException;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.util.ArrayList;
28  import java.util.zip.CRC32;
29  
30  import org.apache.commons.compress.archivers.ArchiveEntry;
31  import org.apache.commons.compress.archivers.ArchiveException;
32  import org.apache.commons.compress.archivers.ArchiveInputStream;
33  import org.apache.commons.compress.utils.IOUtils;
34  import org.apache.commons.io.input.BoundedInputStream;
35  import org.apache.commons.io.input.ChecksumInputStream;
36  
37  /**
38   * Implements the "arj" archive format as an InputStream.
39   * <ul>
40   * <li><a href="https://github.com/FarGroup/FarManager/blob/master/plugins/multiarc/arc.doc/arj.txt">Reference 1</a></li>
41   * <li><a href="http://www.fileformat.info/format/arj/corion.htm">Reference 2</a></li>
42   * </ul>
43   *
44   * @NotThreadSafe
45   * @since 1.6
46   */
47  public class ArjArchiveInputStream extends ArchiveInputStream<ArjArchiveEntry> {
48  
49      private static final String ENCODING_NAME = "CP437";
50      private static final int ARJ_MAGIC_1 = 0x60;
51      private static final int ARJ_MAGIC_2 = 0xEA;
52  
53      /**
54       * Checks if the signature matches what is expected for an arj file.
55       *
56       * @param signature the bytes to check
57       * @param length    the number of bytes to check
58       * @return true, if this stream is an arj archive stream, false otherwise
59       */
60      public static boolean matches(final byte[] signature, final int length) {
61          return length >= 2 && (0xff & signature[0]) == ARJ_MAGIC_1 && (0xff & signature[1]) == ARJ_MAGIC_2;
62      }
63  
64      private final DataInputStream dis;
65      private final MainHeader mainHeader;
66      private LocalFileHeader currentLocalFileHeader;
67      private InputStream currentInputStream;
68  
69      /**
70       * Constructs the ArjInputStream, taking ownership of the inputStream that is passed in, and using the CP437 character encoding.
71       *
72       * @param inputStream the underlying stream, whose ownership is taken
73       * @throws ArchiveException if an exception occurs while reading
74       */
75      public ArjArchiveInputStream(final InputStream inputStream) throws ArchiveException {
76          this(inputStream, ENCODING_NAME);
77      }
78  
79      /**
80       * Constructs the ArjInputStream, taking ownership of the inputStream that is passed in.
81       *
82       * @param inputStream the underlying stream, whose ownership is taken
83       * @param charsetName the charset used for file names and comments in the archive. May be {@code null} to use the platform default.
84       * @throws ArchiveException if an exception occurs while reading
85       */
86      public ArjArchiveInputStream(final InputStream inputStream, final String charsetName) throws ArchiveException {
87          super(inputStream, charsetName);
88          in = dis = new DataInputStream(inputStream);
89          try {
90              mainHeader = readMainHeader();
91              if ((mainHeader.arjFlags & MainHeader.Flags.GARBLED) != 0) {
92                  throw new ArchiveException("Encrypted ARJ files are unsupported");
93              }
94              if ((mainHeader.arjFlags & MainHeader.Flags.VOLUME) != 0) {
95                  throw new ArchiveException("Multi-volume ARJ files are unsupported");
96              }
97          } catch (final IOException e) {
98              throw new ArchiveException(e.getMessage(), (Throwable) e);
99          }
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 }