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