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