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