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