ArjArchiveInputStream.java
- /*
- * 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.EOFException;
- 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.IOUtils;
- import org.apache.commons.io.input.BoundedInputStream;
- import org.apache.commons.io.input.ChecksumInputStream;
- /**
- * Implements the "arj" archive format as an InputStream.
- * <ul>
- * <li><a href="https://github.com/FarGroup/FarManager/blob/master/plugins/multiarc/arc.doc/arj.txt">Reference 1</a></li>
- * <li><a href="http://www.fileformat.info/format/arj/corion.htm">Reference 2</a></li>
- * </ul>
- *
- * @NotThreadSafe
- * @since 1.6
- */
- public class ArjArchiveInputStream extends ArchiveInputStream<ArjArchiveEntry> {
- private static final String ENCODING_NAME = "CP437";
- private static final int ARJ_MAGIC_1 = 0x60;
- private static final int ARJ_MAGIC_2 = 0xEA;
- /**
- * 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;
- }
- private final DataInputStream dis;
- private final MainHeader mainHeader;
- private LocalFileHeader currentLocalFileHeader;
- private InputStream currentInputStream;
- /**
- * 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, ENCODING_NAME);
- }
- /**
- * 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 {
- super(inputStream, charsetName);
- in = dis = new DataInputStream(inputStream);
- 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);
- }
- }
- @Override
- public boolean canReadEntryData(final ArchiveEntry ae) {
- return ae instanceof ArjArchiveEntry && ((ArjArchiveEntry) ae).getMethod() == LocalFileHeader.Methods.STORED;
- }
- @Override
- public void close() throws IOException {
- dis.close();
- }
- /**
- * Gets the archive's comment.
- *
- * @return the archive's comment
- */
- public String getArchiveComment() {
- return mainHeader.comment;
- }
- /**
- * Gets the archive's recorded name.
- *
- * @return the archive's name
- */
- public String getArchiveName() {
- return mainHeader.name;
- }
- @Override
- public ArjArchiveEntry getNextEntry() throws IOException {
- if (currentInputStream != null) {
- // return value ignored as IOUtils.skip ensures the stream is drained completely
- final InputStream input = currentInputStream;
- org.apache.commons.io.IOUtils.skip(input, Long.MAX_VALUE);
- currentInputStream.close();
- currentLocalFileHeader = null;
- currentInputStream = null;
- }
- currentLocalFileHeader = readLocalFileHeader();
- if (currentLocalFileHeader != null) {
- // @formatter:off
- currentInputStream = BoundedInputStream.builder()
- .setInputStream(dis)
- .setMaxCount(currentLocalFileHeader.compressedSize)
- .setPropagateClose(false)
- .get();
- // @formatter:on
- if (currentLocalFileHeader.method == LocalFileHeader.Methods.STORED) {
- // @formatter:off
- currentInputStream = ChecksumInputStream.builder()
- .setChecksum(new CRC32())
- .setInputStream(currentInputStream)
- .setCountThreshold(currentLocalFileHeader.originalSize)
- .setExpectedChecksumValue(currentLocalFileHeader.originalCrc32)
- .get();
- // @formatter:on
- }
- return new ArjArchiveEntry(currentLocalFileHeader);
- }
- currentInputStream = null;
- return null;
- }
- @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);
- }
- 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 int read8(final DataInputStream dataIn) throws IOException {
- final int value = dataIn.readUnsignedByte();
- count(1);
- return value;
- }
- 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);
- }
- }
- private byte[] readHeader() throws IOException {
- boolean found = false;
- byte[] basicHeaderBytes = null;
- do {
- int first;
- int second = read8(dis);
- do {
- first = second;
- second = read8(dis);
- } while (first != ARJ_MAGIC_1 && second != ARJ_MAGIC_2);
- final int basicHeaderSize = read16(dis);
- if (basicHeaderSize == 0) {
- // end of archive
- return null;
- }
- if (basicHeaderSize <= 2600) {
- basicHeaderBytes = readRange(dis, basicHeaderSize);
- final long basicHeaderCrc32 = read32(dis) & 0xFFFFFFFFL;
- final CRC32 crc32 = new CRC32();
- crc32.update(basicHeaderBytes);
- if (basicHeaderCrc32 == crc32.getValue()) {
- found = true;
- }
- }
- } while (!found);
- return basicHeaderBytes;
- }
- private LocalFileHeader readLocalFileHeader() throws IOException {
- final byte[] basicHeaderBytes = readHeader();
- if (basicHeaderBytes == null) {
- return null;
- }
- try (DataInputStream basicHeader = new DataInputStream(new ByteArrayInputStream(basicHeaderBytes))) {
- final int firstHeaderSize = basicHeader.readUnsignedByte();
- final byte[] firstHeaderBytes = readRange(basicHeader, firstHeaderSize - 1);
- pushedBackBytes(firstHeaderBytes.length);
- try (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(dis)) > 0) {
- final byte[] extendedHeaderBytes = readRange(dis, extendedHeaderSize);
- final long extendedHeaderCrc32 = 0xffffFFFFL & read32(dis);
- 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 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 = readRange(basicHeader, firstHeaderSize - 1);
- pushedBackBytes(firstHeaderBytes.length);
- 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 readRange
- 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(dis);
- if (extendedHeaderSize > 0) {
- hdr.extendedHeaderBytes = readRange(dis, extendedHeaderSize);
- final long extendedHeaderCrc32 = 0xffffFFFFL & read32(dis);
- final CRC32 crc32 = new CRC32();
- crc32.update(hdr.extendedHeaderBytes);
- if (extendedHeaderCrc32 != crc32.getValue()) {
- throw new IOException("Extended header CRC32 verification failure");
- }
- }
- return hdr;
- }
- private byte[] readRange(final InputStream in, final int len) throws IOException {
- final byte[] b = IOUtils.readRange(in, len);
- count(b.length);
- if (b.length < len) {
- throw new EOFException();
- }
- return b;
- }
- private String readString(final DataInputStream dataIn) throws IOException {
- try (ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
- int nextByte;
- while ((nextByte = dataIn.readUnsignedByte()) != 0) {
- buffer.write(nextByte);
- }
- return buffer.toString(getCharset().name());
- }
- }
- }