1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 package org.apache.commons.compress.archivers.arj; 19 20 import java.io.ByteArrayInputStream; 21 import java.io.ByteArrayOutputStream; 22 import java.io.DataInputStream; 23 import java.io.IOException; 24 import java.io.InputStream; 25 import java.util.ArrayList; 26 import java.util.zip.CRC32; 27 28 import org.apache.commons.compress.archivers.ArchiveEntry; 29 import org.apache.commons.compress.archivers.ArchiveException; 30 import org.apache.commons.compress.archivers.ArchiveInputStream; 31 import org.apache.commons.compress.utils.BoundedInputStream; 32 import org.apache.commons.compress.utils.CRC32VerifyingInputStream; 33 import org.apache.commons.compress.utils.IOUtils; 34 35 /** 36 * Implements the "arj" archive format as an InputStream. 37 * <p> 38 * <a href="http://farmanager.com/svn/trunk/plugins/multiarc/arc.doc/arj.txt">Reference</a> 39 * @NotThreadSafe 40 * @since 1.6 41 */ 42 public class ArjArchiveInputStream extends ArchiveInputStream { 43 private static final int ARJ_MAGIC_1 = 0x60; 44 private static final int ARJ_MAGIC_2 = 0xEA; 45 private final DataInputStream in; 46 private final String charsetName; 47 private final MainHeader mainHeader; 48 private LocalFileHeader currentLocalFileHeader = null; 49 private InputStream currentInputStream = null; 50 51 /** 52 * Constructs the ArjInputStream, taking ownership of the inputStream that is passed in. 53 * @param inputStream the underlying stream, whose ownership is taken 54 * @param charsetName the charset used for file names and comments 55 * in the archive. May be {@code null} to use the platform default. 56 * @throws ArchiveException if an exception occurs while reading 57 */ 58 public ArjArchiveInputStream(final InputStream inputStream, 59 final String charsetName) throws ArchiveException { 60 in = new DataInputStream(inputStream); 61 this.charsetName = charsetName; 62 try { 63 mainHeader = readMainHeader(); 64 if ((mainHeader.arjFlags & MainHeader.Flags.GARBLED) != 0) { 65 throw new ArchiveException("Encrypted ARJ files are unsupported"); 66 } 67 if ((mainHeader.arjFlags & MainHeader.Flags.VOLUME) != 0) { 68 throw new ArchiveException("Multi-volume ARJ files are unsupported"); 69 } 70 } catch (final IOException ioException) { 71 throw new ArchiveException(ioException.getMessage(), ioException); 72 } 73 } 74 75 /** 76 * Constructs the ArjInputStream, taking ownership of the inputStream that is passed in, 77 * and using the CP437 character encoding. 78 * @param inputStream the underlying stream, whose ownership is taken 79 * @throws ArchiveException if an exception occurs while reading 80 */ 81 public ArjArchiveInputStream(final InputStream inputStream) 82 throws ArchiveException { 83 this(inputStream, "CP437"); 84 } 85 86 @Override 87 public void close() throws IOException { 88 in.close(); 89 } 90 91 private int read8(final DataInputStream dataIn) throws IOException { 92 final int value = dataIn.readUnsignedByte(); 93 count(1); 94 return value; 95 } 96 97 private int read16(final DataInputStream dataIn) throws IOException { 98 final int value = dataIn.readUnsignedShort(); 99 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 } 354