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 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 (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 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 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 } else { 118 // intentionally using the default encoding as that's the contract for a null charsetName 119 return new String(buffer.toByteArray()); 120 } 121 } 122 123 private void readFully(final DataInputStream dataIn, byte[] b) 124 throws IOException { 125 dataIn.readFully(b); 126 count(b.length); 127 } 128 129 private byte[] readHeader() throws IOException { 130 boolean found = false; 131 byte[] basicHeaderBytes = null; 132 do { 133 int first = 0; 134 int second = read8(in); 135 do { 136 first = second; 137 second = read8(in); 138 } while (first != ARJ_MAGIC_1 && second != ARJ_MAGIC_2); 139 final int basicHeaderSize = read16(in); 140 if (basicHeaderSize == 0) { 141 // end of archive 142 return null; 143 } 144 if (basicHeaderSize <= 2600) { 145 basicHeaderBytes = new byte[basicHeaderSize]; 146 readFully(in, basicHeaderBytes); 147 final long basicHeaderCrc32 = read32(in) & 0xFFFFFFFFL; 148 final CRC32 crc32 = new CRC32(); 149 crc32.update(basicHeaderBytes); 150 if (basicHeaderCrc32 == crc32.getValue()) { 151 found = true; 152 } 153 } 154 } while (!found); 155 return basicHeaderBytes; 156 } 157 158 private MainHeader readMainHeader() throws IOException { 159 final byte[] basicHeaderBytes = readHeader(); 160 if (basicHeaderBytes == null) { 161 throw new IOException("Archive ends without any headers"); 162 } 163 final DataInputStream basicHeader = new DataInputStream( 164 new ByteArrayInputStream(basicHeaderBytes)); 165 166 final int firstHeaderSize = basicHeader.readUnsignedByte(); 167 final byte[] firstHeaderBytes = new byte[firstHeaderSize - 1]; 168 basicHeader.readFully(firstHeaderBytes); 169 final DataInputStream firstHeader = new DataInputStream( 170 new ByteArrayInputStream(firstHeaderBytes)); 171 172 final MainHeader hdr = new MainHeader(); 173 hdr.archiverVersionNumber = firstHeader.readUnsignedByte(); 174 hdr.minVersionToExtract = firstHeader.readUnsignedByte(); 175 hdr.hostOS = firstHeader.readUnsignedByte(); 176 hdr.arjFlags = firstHeader.readUnsignedByte(); 177 hdr.securityVersion = firstHeader.readUnsignedByte(); 178 hdr.fileType = firstHeader.readUnsignedByte(); 179 hdr.reserved = firstHeader.readUnsignedByte(); 180 hdr.dateTimeCreated = read32(firstHeader); 181 hdr.dateTimeModified = read32(firstHeader); 182 hdr.archiveSize = 0xffffFFFFL & read32(firstHeader); 183 hdr.securityEnvelopeFilePosition = read32(firstHeader); 184 hdr.fileSpecPosition = read16(firstHeader); 185 hdr.securityEnvelopeLength = read16(firstHeader); 186 pushedBackBytes(20); // count has already counted them via readFully 187 hdr.encryptionVersion = firstHeader.readUnsignedByte(); 188 hdr.lastChapter = firstHeader.readUnsignedByte(); 189 190 if (firstHeaderSize >= 33) { 191 hdr.arjProtectionFactor = firstHeader.readUnsignedByte(); 192 hdr.arjFlags2 = firstHeader.readUnsignedByte(); 193 firstHeader.readUnsignedByte(); 194 firstHeader.readUnsignedByte(); 195 } 196 197 hdr.name = readString(basicHeader); 198 hdr.comment = readString(basicHeader); 199 200 final int extendedHeaderSize = read16(in); 201 if (extendedHeaderSize > 0) { 202 hdr.extendedHeaderBytes = new byte[extendedHeaderSize]; 203 readFully(in, hdr.extendedHeaderBytes); 204 final long extendedHeaderCrc32 = 0xffffFFFFL & read32(in); 205 final CRC32 crc32 = new CRC32(); 206 crc32.update(hdr.extendedHeaderBytes); 207 if (extendedHeaderCrc32 != crc32.getValue()) { 208 throw new IOException("Extended header CRC32 verification failure"); 209 } 210 } 211 212 return hdr; 213 } 214 215 private LocalFileHeader readLocalFileHeader() throws IOException { 216 final byte[] basicHeaderBytes = readHeader(); 217 if (basicHeaderBytes == null) { 218 return null; 219 } 220 final DataInputStream basicHeader = new DataInputStream( 221 new ByteArrayInputStream(basicHeaderBytes)); 222 223 final int firstHeaderSize = basicHeader.readUnsignedByte(); 224 final byte[] firstHeaderBytes = new byte[firstHeaderSize - 1]; 225 basicHeader.readFully(firstHeaderBytes); 226 final DataInputStream firstHeader = new DataInputStream( 227 new ByteArrayInputStream(firstHeaderBytes)); 228 229 final LocalFileHeader localFileHeader = new LocalFileHeader(); 230 localFileHeader.archiverVersionNumber = firstHeader.readUnsignedByte(); 231 localFileHeader.minVersionToExtract = firstHeader.readUnsignedByte(); 232 localFileHeader.hostOS = firstHeader.readUnsignedByte(); 233 localFileHeader.arjFlags = firstHeader.readUnsignedByte(); 234 localFileHeader.method = firstHeader.readUnsignedByte(); 235 localFileHeader.fileType = firstHeader.readUnsignedByte(); 236 localFileHeader.reserved = firstHeader.readUnsignedByte(); 237 localFileHeader.dateTimeModified = read32(firstHeader); 238 localFileHeader.compressedSize = 0xffffFFFFL & read32(firstHeader); 239 localFileHeader.originalSize = 0xffffFFFFL & read32(firstHeader); 240 localFileHeader.originalCrc32 = 0xffffFFFFL & read32(firstHeader); 241 localFileHeader.fileSpecPosition = read16(firstHeader); 242 localFileHeader.fileAccessMode = read16(firstHeader); 243 pushedBackBytes(20); 244 localFileHeader.firstChapter = firstHeader.readUnsignedByte(); 245 localFileHeader.lastChapter = firstHeader.readUnsignedByte(); 246 247 readExtraData(firstHeaderSize, firstHeader, localFileHeader); 248 249 localFileHeader.name = readString(basicHeader); 250 localFileHeader.comment = readString(basicHeader); 251 252 ArrayList<byte[]> extendedHeaders = new ArrayList<byte[]>(); 253 int extendedHeaderSize; 254 while ((extendedHeaderSize = read16(in)) > 0) { 255 final byte[] extendedHeaderBytes = new byte[extendedHeaderSize]; 256 readFully(in, extendedHeaderBytes); 257 final long extendedHeaderCrc32 = 0xffffFFFFL & read32(in); 258 final CRC32 crc32 = new CRC32(); 259 crc32.update(extendedHeaderBytes); 260 if (extendedHeaderCrc32 != crc32.getValue()) { 261 throw new IOException("Extended header CRC32 verification failure"); 262 } 263 extendedHeaders.add(extendedHeaderBytes); 264 } 265 localFileHeader.extendedHeaders = extendedHeaders.toArray(new byte[extendedHeaders.size()][]); 266 267 return localFileHeader; 268 } 269 270 private void readExtraData(int firstHeaderSize, DataInputStream firstHeader, 271 LocalFileHeader localFileHeader) throws IOException { 272 if (firstHeaderSize >= 33) { 273 localFileHeader.extendedFilePosition = read32(firstHeader); 274 if (firstHeaderSize >= 45) { 275 localFileHeader.dateTimeAccessed = read32(firstHeader); 276 localFileHeader.dateTimeCreated = read32(firstHeader); 277 localFileHeader.originalSizeEvenForVolumes = read32(firstHeader); 278 pushedBackBytes(12); 279 } 280 pushedBackBytes(4); 281 } 282 } 283 284 /** 285 * Checks if the signature matches what is expected for an arj file. 286 * 287 * @param signature 288 * the bytes to check 289 * @param length 290 * the number of bytes to check 291 * @return true, if this stream is an arj archive stream, false otherwise 292 */ 293 public static boolean matches(final byte[] signature, final int length) { 294 return length >= 2 && 295 (0xff & signature[0]) == ARJ_MAGIC_1 && 296 (0xff & signature[1]) == ARJ_MAGIC_2; 297 } 298 299 /** 300 * Gets the archive's recorded name. 301 */ 302 public String getArchiveName() { 303 return mainHeader.name; 304 } 305 306 /** 307 * Gets the archive's comment. 308 */ 309 public String getArchiveComment() { 310 return mainHeader.comment; 311 } 312 313 @Override 314 public ArjArchiveEntry getNextEntry() throws IOException { 315 if (currentInputStream != null) { 316 // return value ignored as IOUtils.skip ensures the stream is drained completely 317 IOUtils.skip(currentInputStream, Long.MAX_VALUE); 318 currentInputStream.close(); 319 currentLocalFileHeader = null; 320 currentInputStream = null; 321 } 322 323 currentLocalFileHeader = readLocalFileHeader(); 324 if (currentLocalFileHeader != null) { 325 currentInputStream = new BoundedInputStream(in, currentLocalFileHeader.compressedSize); 326 if (currentLocalFileHeader.method == LocalFileHeader.Methods.STORED) { 327 currentInputStream = new CRC32VerifyingInputStream(currentInputStream, 328 currentLocalFileHeader.originalSize, currentLocalFileHeader.originalCrc32); 329 } 330 return new ArjArchiveEntry(currentLocalFileHeader); 331 } else { 332 currentInputStream = null; 333 return null; 334 } 335 } 336 337 @Override 338 public boolean canReadEntryData(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}