package org.apache.commons.compress.archivers.arj;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.zip.CRC32;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.utils.BoundedInputStream;
import org.apache.commons.compress.utils.CRC32VerifyingInputStream;
import org.apache.commons.compress.utils.IOUtils;
public class ArjArchiveInputStream extends ArchiveInputStream {
private static final int ARJ_MAGIC_1 = 0x60;
private static final int ARJ_MAGIC_2 = 0xEA;
private final DataInputStream in;
private final String charsetName;
private final MainHeader mainHeader;
private LocalFileHeader = null;
private InputStream currentInputStream = null;
public ArjArchiveInputStream(final InputStream inputStream,
final String charsetName) throws ArchiveException {
in = new DataInputStream(inputStream);
this.charsetName = charsetName;
try {
mainHeader = readMainHeader();
if ((mainHeader.arjFlags & MainHeader.Flags.GARBLED) != 0) {
throw new ArchiveException("Encrypted ARJ files are unsupported");
}
if ((mainHeader.arjFlags & MainHeader.Flags.VOLUME) != 0) {
throw new ArchiveException("Multi-volume ARJ files are unsupported");
}
} catch (final IOException ioException) {
throw new ArchiveException(ioException.getMessage(), ioException);
}
}
public ArjArchiveInputStream(final InputStream inputStream)
throws ArchiveException {
this(inputStream, "CP437");
}
@Override
public void close() throws IOException {
in.close();
}
private int read8(final DataInputStream dataIn) throws IOException {
final int value = dataIn.readUnsignedByte();
count(1);
return value;
}
private int read16(final DataInputStream dataIn) throws IOException {
final int value = dataIn.readUnsignedShort();
count(2);
return Integer.reverseBytes(value) >>> 16;
}
private int read32(final DataInputStream dataIn) throws IOException {
final int value = dataIn.readInt();
count(4);
return Integer.reverseBytes(value);
}
private String readString(final DataInputStream dataIn) throws IOException {
try (final ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
int nextByte;
while ((nextByte = dataIn.readUnsignedByte()) != 0) {
buffer.write(nextByte);
}
if (charsetName != null) {
return new String(buffer.toByteArray(), charsetName);
}
return new String(buffer.toByteArray());
}
}
private void readFully(final DataInputStream dataIn, final byte[] b)
throws IOException {
dataIn.readFully(b);
count(b.length);
}
private byte[] () throws IOException {
boolean found = false;
byte[] basicHeaderBytes = null;
do {
int first = 0;
int second = read8(in);
do {
first = second;
second = read8(in);
} while (first != ARJ_MAGIC_1 && second != ARJ_MAGIC_2);
final int basicHeaderSize = read16(in);
if (basicHeaderSize == 0) {
return null;
}
if (basicHeaderSize <= 2600) {
basicHeaderBytes = new byte[basicHeaderSize];
readFully(in, basicHeaderBytes);
final long basicHeaderCrc32 = read32(in) & 0xFFFFFFFFL;
final CRC32 crc32 = new CRC32();
crc32.update(basicHeaderBytes);
if (basicHeaderCrc32 == crc32.getValue()) {
found = true;
}
}
} while (!found);
return basicHeaderBytes;
}
private MainHeader readMainHeader() throws IOException {
final byte[] basicHeaderBytes = readHeader();
if (basicHeaderBytes == null) {
throw new IOException("Archive ends without any headers");
}
final DataInputStream basicHeader = new DataInputStream(
new ByteArrayInputStream(basicHeaderBytes));
final int firstHeaderSize = basicHeader.readUnsignedByte();
final byte[] firstHeaderBytes = new byte[firstHeaderSize - 1];
basicHeader.readFully(firstHeaderBytes);
final DataInputStream firstHeader = new DataInputStream(
new ByteArrayInputStream(firstHeaderBytes));
final MainHeader hdr = new MainHeader();
hdr.archiverVersionNumber = firstHeader.readUnsignedByte();
hdr.minVersionToExtract = firstHeader.readUnsignedByte();
hdr.hostOS = firstHeader.readUnsignedByte();
hdr.arjFlags = firstHeader.readUnsignedByte();
hdr.securityVersion = firstHeader.readUnsignedByte();
hdr.fileType = firstHeader.readUnsignedByte();
hdr.reserved = firstHeader.readUnsignedByte();
hdr.dateTimeCreated = read32(firstHeader);
hdr.dateTimeModified = read32(firstHeader);
hdr.archiveSize = 0xffffFFFFL & read32(firstHeader);
hdr.securityEnvelopeFilePosition = read32(firstHeader);
hdr.fileSpecPosition = read16(firstHeader);
hdr.securityEnvelopeLength = read16(firstHeader);
pushedBackBytes(20);
hdr.encryptionVersion = firstHeader.readUnsignedByte();
hdr.lastChapter = firstHeader.readUnsignedByte();
if (firstHeaderSize >= 33) {
hdr.arjProtectionFactor = firstHeader.readUnsignedByte();
hdr.arjFlags2 = firstHeader.readUnsignedByte();
firstHeader.readUnsignedByte();
firstHeader.readUnsignedByte();
}
hdr.name = readString(basicHeader);
hdr.comment = readString(basicHeader);
final int extendedHeaderSize = read16(in);
if (extendedHeaderSize > 0) {
hdr.extendedHeaderBytes = new byte[extendedHeaderSize];
readFully(in, hdr.extendedHeaderBytes);
final long extendedHeaderCrc32 = 0xffffFFFFL & read32(in);
final CRC32 crc32 = new CRC32();
crc32.update(hdr.extendedHeaderBytes);
if (extendedHeaderCrc32 != crc32.getValue()) {
throw new IOException("Extended header CRC32 verification failure");
}
}
return hdr;
}
private LocalFileHeader () throws IOException {
final byte[] basicHeaderBytes = readHeader();
if (basicHeaderBytes == null) {
return null;
}
try (final DataInputStream basicHeader = new DataInputStream(new ByteArrayInputStream(basicHeaderBytes))) {
final int firstHeaderSize = basicHeader.readUnsignedByte();
final byte[] firstHeaderBytes = new byte[firstHeaderSize - 1];
basicHeader.readFully(firstHeaderBytes);
try (final DataInputStream firstHeader = new DataInputStream(new ByteArrayInputStream(firstHeaderBytes))) {
final LocalFileHeader localFileHeader = new LocalFileHeader();
localFileHeader.archiverVersionNumber = firstHeader.readUnsignedByte();
localFileHeader.minVersionToExtract = firstHeader.readUnsignedByte();
localFileHeader.hostOS = firstHeader.readUnsignedByte();
localFileHeader.arjFlags = firstHeader.readUnsignedByte();
localFileHeader.method = firstHeader.readUnsignedByte();
localFileHeader.fileType = firstHeader.readUnsignedByte();
localFileHeader.reserved = firstHeader.readUnsignedByte();
localFileHeader.dateTimeModified = read32(firstHeader);
localFileHeader.compressedSize = 0xffffFFFFL & read32(firstHeader);
localFileHeader.originalSize = 0xffffFFFFL & read32(firstHeader);
localFileHeader.originalCrc32 = 0xffffFFFFL & read32(firstHeader);
localFileHeader.fileSpecPosition = read16(firstHeader);
localFileHeader.fileAccessMode = read16(firstHeader);
pushedBackBytes(20);
localFileHeader.firstChapter = firstHeader.readUnsignedByte();
localFileHeader.lastChapter = firstHeader.readUnsignedByte();
readExtraData(firstHeaderSize, firstHeader, localFileHeader);
localFileHeader.name = readString(basicHeader);
localFileHeader.comment = readString(basicHeader);
final ArrayList<byte[]> extendedHeaders = new ArrayList<>();
int extendedHeaderSize;
while ((extendedHeaderSize = read16(in)) > 0) {
final byte[] extendedHeaderBytes = new byte[extendedHeaderSize];
readFully(in, extendedHeaderBytes);
final long extendedHeaderCrc32 = 0xffffFFFFL & read32(in);
final CRC32 crc32 = new CRC32();
crc32.update(extendedHeaderBytes);
if (extendedHeaderCrc32 != crc32.getValue()) {
throw new IOException("Extended header CRC32 verification failure");
}
extendedHeaders.add(extendedHeaderBytes);
}
localFileHeader.extendedHeaders = extendedHeaders.toArray(new byte[0][]);
return localFileHeader;
}
}
}
private void (final int firstHeaderSize, final DataInputStream firstHeader,
final LocalFileHeader localFileHeader) throws IOException {
if (firstHeaderSize >= 33) {
localFileHeader.extendedFilePosition = read32(firstHeader);
if (firstHeaderSize >= 45) {
localFileHeader.dateTimeAccessed = read32(firstHeader);
localFileHeader.dateTimeCreated = read32(firstHeader);
localFileHeader.originalSizeEvenForVolumes = read32(firstHeader);
pushedBackBytes(12);
}
pushedBackBytes(4);
}
}
public static boolean matches(final byte[] signature, final int length) {
return length >= 2 &&
(0xff & signature[0]) == ARJ_MAGIC_1 &&
(0xff & signature[1]) == ARJ_MAGIC_2;
}
public String getArchiveName() {
return mainHeader.name;
}
public String () {
return mainHeader.comment;
}
@Override
public ArjArchiveEntry getNextEntry() throws IOException {
if (currentInputStream != null) {
IOUtils.skip(currentInputStream, Long.MAX_VALUE);
currentInputStream.close();
currentLocalFileHeader = null;
currentInputStream = null;
}
currentLocalFileHeader = readLocalFileHeader();
if (currentLocalFileHeader != null) {
currentInputStream = new BoundedInputStream(in, currentLocalFileHeader.compressedSize);
if (currentLocalFileHeader.method == LocalFileHeader.Methods.STORED) {
currentInputStream = new CRC32VerifyingInputStream(currentInputStream,
currentLocalFileHeader.originalSize, currentLocalFileHeader.originalCrc32);
}
return new ArjArchiveEntry(currentLocalFileHeader);
}
currentInputStream = null;
return null;
}
@Override
public boolean canReadEntryData(final ArchiveEntry ae) {
return ae instanceof ArjArchiveEntry
&& ((ArjArchiveEntry) ae).getMethod() == LocalFileHeader.Methods.STORED;
}
@Override
public int read(final byte[] b, final int off, final int len) throws IOException {
if (len == 0) {
return 0;
}
if (currentLocalFileHeader == null) {
throw new IllegalStateException("No current arj entry");
}
if (currentLocalFileHeader.method != LocalFileHeader.Methods.STORED) {
throw new IOException("Unsupported compression method " + currentLocalFileHeader.method);
}
return currentInputStream.read(b, off, len);
}
}