aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStream.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStream.java')
-rw-r--r--src/main/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStream.java353
1 files changed, 353 insertions, 0 deletions
diff --git a/src/main/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStream.java b/src/main/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStream.java
new file mode 100644
index 000000000..c22d4c047
--- /dev/null
+++ b/src/main/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStream.java
@@ -0,0 +1,353 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+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;
+
+/**
+ * Implements the "arj" archive format as an InputStream.
+ * <p>
+ * <a href="http://farmanager.com/svn/trunk/plugins/multiarc/arc.doc/arj.txt">Reference</a>
+ * @NotThreadSafe
+ * @since 1.6
+ */
+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 currentLocalFileHeader = null;
+ private InputStream currentInputStream = null;
+
+ /**
+ * Constructs the ArjInputStream, taking ownership of the inputStream that is passed in.
+ * @param inputStream the underlying stream, whose ownership is taken
+ * @param charsetName the charset used for file names and comments
+ * in the archive. May be {@code null} to use the platform default.
+ * @throws ArchiveException if an exception occurs while reading
+ */
+ 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);
+ }
+ }
+
+ /**
+ * Constructs the ArjInputStream, taking ownership of the inputStream that is passed in,
+ * and using the CP437 character encoding.
+ * @param inputStream the underlying stream, whose ownership is taken
+ * @throws ArchiveException if an exception occurs while reading
+ */
+ 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 {
+ final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ int nextByte;
+ while ((nextByte = dataIn.readUnsignedByte()) != 0) {
+ buffer.write(nextByte);
+ }
+ if (charsetName != null) {
+ return new String(buffer.toByteArray(), charsetName);
+ }
+ // intentionally using the default encoding as that's the contract for a null 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[] readHeader() 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) {
+ // end of archive
+ 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); // count has already counted them via readFully
+ 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 readLocalFileHeader() 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[extendedHeaders.size()][]);
+
+ return localFileHeader;
+ }
+ }
+ }
+
+ private void readExtraData(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);
+ }
+ }
+
+ /**
+ * Checks if the signature matches what is expected for an arj file.
+ *
+ * @param signature
+ * the bytes to check
+ * @param length
+ * the number of bytes to check
+ * @return true, if this stream is an arj archive stream, false otherwise
+ */
+ 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;
+ }
+
+ /**
+ * Gets the archive's recorded name.
+ * @return the archive's name
+ */
+ public String getArchiveName() {
+ return mainHeader.name;
+ }
+
+ /**
+ * Gets the archive's comment.
+ * @return the archive's comment
+ */
+ public String getArchiveComment() {
+ return mainHeader.comment;
+ }
+
+ @Override
+ public ArjArchiveEntry getNextEntry() throws IOException {
+ if (currentInputStream != null) {
+ // return value ignored as IOUtils.skip ensures the stream is drained completely
+ 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 (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);
+ }
+}