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.BoundedInputStream; 032import org.apache.commons.compress.utils.CRC32VerifyingInputStream; 033import org.apache.commons.compress.utils.Charsets; 034import org.apache.commons.compress.utils.IOUtils; 035 036/** 037 * Implements the "arj" archive format as an InputStream. 038 * <p> 039 * <a href="https://github.com/FarGroup/FarManager/blob/master/plugins/multiarc/arc.doc/arj.txt">Reference 1</a> 040 * <br> 041 * <a href="http://www.fileformat.info/format/arj/corion.htm">Reference 2</a> 042 * @NotThreadSafe 043 * @since 1.6 044 */ 045public class ArjArchiveInputStream extends ArchiveInputStream { 046 private static final int ARJ_MAGIC_1 = 0x60; 047 private static final int ARJ_MAGIC_2 = 0xEA; 048 /** 049 * Checks if the signature matches what is expected for an arj file. 050 * 051 * @param signature 052 * the bytes to check 053 * @param length 054 * the number of bytes to check 055 * @return true, if this stream is an arj archive stream, false otherwise 056 */ 057 public static boolean matches(final byte[] signature, final int length) { 058 return length >= 2 && 059 (0xff & signature[0]) == ARJ_MAGIC_1 && 060 (0xff & signature[1]) == ARJ_MAGIC_2; 061 } 062 private final DataInputStream in; 063 private final String charsetName; 064 private final MainHeader mainHeader; 065 private LocalFileHeader currentLocalFileHeader; 066 067 private InputStream currentInputStream; 068 069 /** 070 * Constructs the ArjInputStream, taking ownership of the inputStream that is passed in, 071 * and using the CP437 character encoding. 072 * @param inputStream the underlying stream, whose ownership is taken 073 * @throws ArchiveException if an exception occurs while reading 074 */ 075 public ArjArchiveInputStream(final InputStream inputStream) 076 throws ArchiveException { 077 this(inputStream, "CP437"); 078 } 079 080 /** 081 * Constructs the ArjInputStream, taking ownership of the inputStream that is passed in. 082 * @param inputStream the underlying stream, whose ownership is taken 083 * @param charsetName the charset used for file names and comments 084 * in the archive. May be {@code null} to use the platform default. 085 * @throws ArchiveException if an exception occurs while reading 086 */ 087 public ArjArchiveInputStream(final InputStream inputStream, 088 final String charsetName) throws ArchiveException { 089 in = new DataInputStream(inputStream); 090 this.charsetName = charsetName; 091 try { 092 mainHeader = readMainHeader(); 093 if ((mainHeader.arjFlags & MainHeader.Flags.GARBLED) != 0) { 094 throw new ArchiveException("Encrypted ARJ files are unsupported"); 095 } 096 if ((mainHeader.arjFlags & MainHeader.Flags.VOLUME) != 0) { 097 throw new ArchiveException("Multi-volume ARJ files are unsupported"); 098 } 099 } catch (final IOException ioException) { 100 throw new ArchiveException(ioException.getMessage(), ioException); 101 } 102 } 103 104 @Override 105 public boolean canReadEntryData(final ArchiveEntry ae) { 106 return ae instanceof ArjArchiveEntry 107 && ((ArjArchiveEntry) ae).getMethod() == LocalFileHeader.Methods.STORED; 108 } 109 110 @Override 111 public void close() throws IOException { 112 in.close(); 113 } 114 115 /** 116 * Gets the archive's comment. 117 * @return the archive's comment 118 */ 119 public String getArchiveComment() { 120 return mainHeader.comment; 121 } 122 123 /** 124 * Gets the archive's recorded name. 125 * @return the archive's name 126 */ 127 public String getArchiveName() { 128 return mainHeader.name; 129 } 130 131 @Override 132 public ArjArchiveEntry getNextEntry() throws IOException { 133 if (currentInputStream != null) { 134 // return value ignored as IOUtils.skip ensures the stream is drained completely 135 IOUtils.skip(currentInputStream, Long.MAX_VALUE); 136 currentInputStream.close(); 137 currentLocalFileHeader = null; 138 currentInputStream = null; 139 } 140 141 currentLocalFileHeader = readLocalFileHeader(); 142 if (currentLocalFileHeader != null) { 143 currentInputStream = new BoundedInputStream(in, currentLocalFileHeader.compressedSize); 144 if (currentLocalFileHeader.method == LocalFileHeader.Methods.STORED) { 145 currentInputStream = new CRC32VerifyingInputStream(currentInputStream, 146 currentLocalFileHeader.originalSize, currentLocalFileHeader.originalCrc32); 147 } 148 return new ArjArchiveEntry(currentLocalFileHeader); 149 } 150 currentInputStream = null; 151 return null; 152 } 153 154 @Override 155 public int read(final byte[] b, final int off, final int len) throws IOException { 156 if (len == 0) { 157 return 0; 158 } 159 if (currentLocalFileHeader == null) { 160 throw new IllegalStateException("No current arj entry"); 161 } 162 if (currentLocalFileHeader.method != LocalFileHeader.Methods.STORED) { 163 throw new IOException("Unsupported compression method " + currentLocalFileHeader.method); 164 } 165 return currentInputStream.read(b, off, len); 166 } 167 168 private int read16(final DataInputStream dataIn) throws IOException { 169 final int value = dataIn.readUnsignedShort(); 170 count(2); 171 return Integer.reverseBytes(value) >>> 16; 172 } 173 174 private int read32(final DataInputStream dataIn) throws IOException { 175 final int value = dataIn.readInt(); 176 count(4); 177 return Integer.reverseBytes(value); 178 } 179 180 private int read8(final DataInputStream dataIn) throws IOException { 181 final int value = dataIn.readUnsignedByte(); 182 count(1); 183 return value; 184 } 185 186 private void readExtraData(final int firstHeaderSize, final DataInputStream firstHeader, 187 final LocalFileHeader localFileHeader) throws IOException { 188 if (firstHeaderSize >= 33) { 189 localFileHeader.extendedFilePosition = read32(firstHeader); 190 if (firstHeaderSize >= 45) { 191 localFileHeader.dateTimeAccessed = read32(firstHeader); 192 localFileHeader.dateTimeCreated = read32(firstHeader); 193 localFileHeader.originalSizeEvenForVolumes = read32(firstHeader); 194 pushedBackBytes(12); 195 } 196 pushedBackBytes(4); 197 } 198 } 199 200 private byte[] readHeader() throws IOException { 201 boolean found = false; 202 byte[] basicHeaderBytes = null; 203 do { 204 int first; 205 int second = read8(in); 206 do { 207 first = second; 208 second = read8(in); 209 } while (first != ARJ_MAGIC_1 && second != ARJ_MAGIC_2); 210 final int basicHeaderSize = read16(in); 211 if (basicHeaderSize == 0) { 212 // end of archive 213 return null; 214 } 215 if (basicHeaderSize <= 2600) { 216 basicHeaderBytes = readRange(in, basicHeaderSize); 217 final long basicHeaderCrc32 = read32(in) & 0xFFFFFFFFL; 218 final CRC32 crc32 = new CRC32(); 219 crc32.update(basicHeaderBytes); 220 if (basicHeaderCrc32 == crc32.getValue()) { 221 found = true; 222 } 223 } 224 } while (!found); 225 return basicHeaderBytes; 226 } 227 228 private LocalFileHeader readLocalFileHeader() throws IOException { 229 final byte[] basicHeaderBytes = readHeader(); 230 if (basicHeaderBytes == null) { 231 return null; 232 } 233 try (final DataInputStream basicHeader = new DataInputStream(new ByteArrayInputStream(basicHeaderBytes))) { 234 235 final int firstHeaderSize = basicHeader.readUnsignedByte(); 236 final byte[] firstHeaderBytes = readRange(basicHeader, firstHeaderSize - 1); 237 pushedBackBytes(firstHeaderBytes.length); 238 try (final DataInputStream firstHeader = new DataInputStream(new ByteArrayInputStream(firstHeaderBytes))) { 239 240 final LocalFileHeader localFileHeader = new LocalFileHeader(); 241 localFileHeader.archiverVersionNumber = firstHeader.readUnsignedByte(); 242 localFileHeader.minVersionToExtract = firstHeader.readUnsignedByte(); 243 localFileHeader.hostOS = firstHeader.readUnsignedByte(); 244 localFileHeader.arjFlags = firstHeader.readUnsignedByte(); 245 localFileHeader.method = firstHeader.readUnsignedByte(); 246 localFileHeader.fileType = firstHeader.readUnsignedByte(); 247 localFileHeader.reserved = firstHeader.readUnsignedByte(); 248 localFileHeader.dateTimeModified = read32(firstHeader); 249 localFileHeader.compressedSize = 0xffffFFFFL & read32(firstHeader); 250 localFileHeader.originalSize = 0xffffFFFFL & read32(firstHeader); 251 localFileHeader.originalCrc32 = 0xffffFFFFL & read32(firstHeader); 252 localFileHeader.fileSpecPosition = read16(firstHeader); 253 localFileHeader.fileAccessMode = read16(firstHeader); 254 pushedBackBytes(20); 255 localFileHeader.firstChapter = firstHeader.readUnsignedByte(); 256 localFileHeader.lastChapter = firstHeader.readUnsignedByte(); 257 258 readExtraData(firstHeaderSize, firstHeader, localFileHeader); 259 260 localFileHeader.name = readString(basicHeader); 261 localFileHeader.comment = readString(basicHeader); 262 263 final ArrayList<byte[]> extendedHeaders = new ArrayList<>(); 264 int extendedHeaderSize; 265 while ((extendedHeaderSize = read16(in)) > 0) { 266 final byte[] extendedHeaderBytes = readRange(in, extendedHeaderSize); 267 final long extendedHeaderCrc32 = 0xffffFFFFL & read32(in); 268 final CRC32 crc32 = new CRC32(); 269 crc32.update(extendedHeaderBytes); 270 if (extendedHeaderCrc32 != crc32.getValue()) { 271 throw new IOException("Extended header CRC32 verification failure"); 272 } 273 extendedHeaders.add(extendedHeaderBytes); 274 } 275 localFileHeader.extendedHeaders = extendedHeaders.toArray(new byte[0][]); 276 277 return localFileHeader; 278 } 279 } 280 } 281 282 private MainHeader readMainHeader() throws IOException { 283 final byte[] basicHeaderBytes = readHeader(); 284 if (basicHeaderBytes == null) { 285 throw new IOException("Archive ends without any headers"); 286 } 287 final DataInputStream basicHeader = new DataInputStream( 288 new ByteArrayInputStream(basicHeaderBytes)); 289 290 final int firstHeaderSize = basicHeader.readUnsignedByte(); 291 final byte[] firstHeaderBytes = readRange(basicHeader, firstHeaderSize - 1); 292 pushedBackBytes(firstHeaderBytes.length); 293 294 final DataInputStream firstHeader = new DataInputStream( 295 new ByteArrayInputStream(firstHeaderBytes)); 296 297 final MainHeader hdr = new MainHeader(); 298 hdr.archiverVersionNumber = firstHeader.readUnsignedByte(); 299 hdr.minVersionToExtract = firstHeader.readUnsignedByte(); 300 hdr.hostOS = firstHeader.readUnsignedByte(); 301 hdr.arjFlags = firstHeader.readUnsignedByte(); 302 hdr.securityVersion = firstHeader.readUnsignedByte(); 303 hdr.fileType = firstHeader.readUnsignedByte(); 304 hdr.reserved = firstHeader.readUnsignedByte(); 305 hdr.dateTimeCreated = read32(firstHeader); 306 hdr.dateTimeModified = read32(firstHeader); 307 hdr.archiveSize = 0xffffFFFFL & read32(firstHeader); 308 hdr.securityEnvelopeFilePosition = read32(firstHeader); 309 hdr.fileSpecPosition = read16(firstHeader); 310 hdr.securityEnvelopeLength = read16(firstHeader); 311 pushedBackBytes(20); // count has already counted them via readRange 312 hdr.encryptionVersion = firstHeader.readUnsignedByte(); 313 hdr.lastChapter = firstHeader.readUnsignedByte(); 314 315 if (firstHeaderSize >= 33) { 316 hdr.arjProtectionFactor = firstHeader.readUnsignedByte(); 317 hdr.arjFlags2 = firstHeader.readUnsignedByte(); 318 firstHeader.readUnsignedByte(); 319 firstHeader.readUnsignedByte(); 320 } 321 322 hdr.name = readString(basicHeader); 323 hdr.comment = readString(basicHeader); 324 325 final int extendedHeaderSize = read16(in); 326 if (extendedHeaderSize > 0) { 327 hdr.extendedHeaderBytes = readRange(in, extendedHeaderSize); 328 final long extendedHeaderCrc32 = 0xffffFFFFL & read32(in); 329 final CRC32 crc32 = new CRC32(); 330 crc32.update(hdr.extendedHeaderBytes); 331 if (extendedHeaderCrc32 != crc32.getValue()) { 332 throw new IOException("Extended header CRC32 verification failure"); 333 } 334 } 335 336 return hdr; 337 } 338 339 private byte[] readRange(final InputStream in, final int len) 340 throws IOException { 341 final byte[] b = IOUtils.readRange(in, len); 342 count(b.length); 343 if (b.length < len) { 344 throw new EOFException(); 345 } 346 return b; 347 } 348 349 private String readString(final DataInputStream dataIn) throws IOException { 350 try (final ByteArrayOutputStream buffer = new ByteArrayOutputStream()) { 351 int nextByte; 352 while ((nextByte = dataIn.readUnsignedByte()) != 0) { 353 buffer.write(nextByte); 354 } 355 return buffer.toString(Charsets.toCharset(charsetName).name()); 356 } 357 } 358}