001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.compress.archivers.arj; 018 019import java.io.ByteArrayInputStream; 020import java.io.ByteArrayOutputStream; 021import java.io.DataInputStream; 022import java.io.EOFException; 023import java.io.IOException; 024import java.io.InputStream; 025import java.util.ArrayList; 026import java.util.zip.CRC32; 027 028import org.apache.commons.compress.archivers.ArchiveEntry; 029import org.apache.commons.compress.archivers.ArchiveException; 030import org.apache.commons.compress.archivers.ArchiveInputStream; 031import org.apache.commons.compress.utils.IOUtils; 032import org.apache.commons.io.input.BoundedInputStream; 033import org.apache.commons.io.input.ChecksumInputStream; 034 035/** 036 * Implements the "arj" archive format as an InputStream. 037 * <ul> 038 * <li><a href="https://github.com/FarGroup/FarManager/blob/master/plugins/multiarc/arc.doc/arj.txt">Reference 1</a></li> 039 * <li><a href="http://www.fileformat.info/format/arj/corion.htm">Reference 2</a></li> 040 * </ul> 041 * 042 * @NotThreadSafe 043 * @since 1.6 044 */ 045public class ArjArchiveInputStream extends ArchiveInputStream<ArjArchiveEntry> { 046 047 private static final String ENCODING_NAME = "CP437"; 048 private static final int ARJ_MAGIC_1 = 0x60; 049 private static final int ARJ_MAGIC_2 = 0xEA; 050 051 /** 052 * Checks if the signature matches what is expected for an arj file. 053 * 054 * @param signature the bytes to check 055 * @param length the number of bytes to check 056 * @return true, if this stream is an arj archive stream, false otherwise 057 */ 058 public static boolean matches(final byte[] signature, final int length) { 059 return length >= 2 && (0xff & signature[0]) == ARJ_MAGIC_1 && (0xff & signature[1]) == ARJ_MAGIC_2; 060 } 061 062 private final DataInputStream dis; 063 private final MainHeader mainHeader; 064 private LocalFileHeader currentLocalFileHeader; 065 private InputStream currentInputStream; 066 067 /** 068 * Constructs the ArjInputStream, taking ownership of the inputStream that is passed in, and using the CP437 character encoding. 069 * 070 * @param inputStream the underlying stream, whose ownership is taken 071 * @throws ArchiveException if an exception occurs while reading 072 */ 073 public ArjArchiveInputStream(final InputStream inputStream) throws ArchiveException { 074 this(inputStream, ENCODING_NAME); 075 } 076 077 /** 078 * Constructs the ArjInputStream, taking ownership of the inputStream that is passed in. 079 * 080 * @param inputStream the underlying stream, whose ownership is taken 081 * @param charsetName the charset used for file names and comments in the archive. May be {@code null} to use the platform default. 082 * @throws ArchiveException if an exception occurs while reading 083 */ 084 public ArjArchiveInputStream(final InputStream inputStream, final String charsetName) throws ArchiveException { 085 super(inputStream, charsetName); 086 in = dis = new DataInputStream(inputStream); 087 try { 088 mainHeader = readMainHeader(); 089 if ((mainHeader.arjFlags & MainHeader.Flags.GARBLED) != 0) { 090 throw new ArchiveException("Encrypted ARJ files are unsupported"); 091 } 092 if ((mainHeader.arjFlags & MainHeader.Flags.VOLUME) != 0) { 093 throw new ArchiveException("Multi-volume ARJ files are unsupported"); 094 } 095 } catch (final IOException ioException) { 096 throw new ArchiveException(ioException.getMessage(), ioException); 097 } 098 } 099 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}