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