diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 3e7f5463e12..93c680a6ed1 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -79,6 +79,8 @@ The type attribute can be add,update,fix,remove. ArArchiveInputStream.readGNUStringTable(byte[], int, int) now provides a better exception message, wrapping the underlying exception. ArArchiveInputStream.read(byte[], int, int) now throws ArchiveException instead of ArithmeticException. Simplify handling of special AR records in ArArchiveInputStream. + + Correct byte accounting and truncation errors in ARJ input stream. org.apache.commons.compress.harmony.unpack200 now throws Pack200Exception, IllegalArgumentException, and IllegalStateException instead of other runtime exceptions and Error. 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 index fa5654f4708..01984c253ad 100644 --- a/src/main/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStream.java +++ b/src/main/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStream.java @@ -20,7 +20,6 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; @@ -32,7 +31,7 @@ import org.apache.commons.compress.archivers.ArchiveException; import org.apache.commons.compress.archivers.ArchiveInputStream; import org.apache.commons.compress.utils.ArchiveUtils; -import org.apache.commons.compress.utils.IOUtils; +import org.apache.commons.io.EndianUtils; import org.apache.commons.io.input.BoundedInputStream; import org.apache.commons.io.input.ChecksumInputStream; @@ -99,14 +98,31 @@ 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 static void readExtraData(final int firstHeaderSize, final InputStream firstHeader, final LocalFileHeader localFileHeader) throws IOException { + if (firstHeaderSize >= 33) { + localFileHeader.extendedFilePosition = EndianUtils.readSwappedInteger(firstHeader); + if (firstHeaderSize >= 45) { + localFileHeader.dateTimeAccessed = EndianUtils.readSwappedInteger(firstHeader); + localFileHeader.dateTimeCreated = EndianUtils.readSwappedInteger(firstHeader); + localFileHeader.originalSizeEvenForVolumes = EndianUtils.readSwappedInteger(firstHeader); + } + } + } + + private static int readUnsignedByte(InputStream in) throws IOException { + final int value = in.read(); + if (value == -1) { + throw new EOFException(); + } + return value & 0xff; + } + private final MainHeader mainHeader; private LocalFileHeader currentLocalFileHeader; private InputStream currentInputStream; private ArjArchiveInputStream(final Builder builder) throws IOException { super(builder); - dis = new DataInputStream(in); mainHeader = readMainHeader(); if ((mainHeader.arjFlags & MainHeader.Flags.GARBLED) != 0) { throw new ArchiveException("Encrypted ARJ files are unsupported"); @@ -148,11 +164,6 @@ 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. * @@ -185,10 +196,22 @@ public ArjArchiveEntry getNextEntry() throws IOException { currentLocalFileHeader = readLocalFileHeader(); if (currentLocalFileHeader != null) { // @formatter:off + final long currentPosition = getBytesRead(); currentInputStream = BoundedInputStream.builder() - .setInputStream(dis) + .setInputStream(in) .setMaxCount(currentLocalFileHeader.compressedSize) .setPropagateClose(false) + .setAfterRead(read -> { + if (read < 0) { + throw new EOFException(String.format( + "Truncated ARJ archive: entry '%s' expected %,d bytes, but only %,d were read.", + currentLocalFileHeader.name, + currentLocalFileHeader.compressedSize, + getBytesRead() - currentPosition + )); + } + count(read); + }) .get(); // @formatter:on if (currentLocalFileHeader.method == LocalFileHeader.Methods.STORED) { @@ -222,63 +245,51 @@ public int read(final byte[] b, final int off, final int len) throws IOException 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 String readComment(final InputStream dataIn) throws IOException { + return new String(readString(dataIn).toByteArray(), getCharset()); } - 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 String readEntryName(final InputStream dataIn) throws IOException { + final ByteArrayOutputStream buffer = readString(dataIn); + ArchiveUtils.checkEntryNameLength(buffer.size(), getMaxEntryNameLength(), "ARJ"); + return new String(buffer.toByteArray(), getCharset()); } + /** + * Scans for the next valid ARJ header. + * + * @return The header bytes, or {@code null} if end of archive. + * @throws EOFException If the end of the stream is reached before a valid header is found. + * @throws IOException If an I/O error occurs. + */ private byte[] readHeader() throws IOException { - boolean found = false; - byte[] basicHeaderBytes = null; - do { + byte[] basicHeaderBytes; + // TODO: Explain why we are scanning for a valid ARJ header + // and don't throw, when an invalid/corrupted header is found, + // which might indicate a corrupted archive. + while (true) { int first; - int second = read8(dis); + int second = readUnsignedByte(); do { first = second; - second = read8(dis); + second = readUnsignedByte(); } while (first != ARJ_MAGIC_1 && second != ARJ_MAGIC_2); - final int basicHeaderSize = read16(dis); + final int basicHeaderSize = readSwappedUnsignedShort(); if (basicHeaderSize == 0) { // end of archive return null; - } - if (basicHeaderSize <= 2600) { - basicHeaderBytes = readRange(dis, basicHeaderSize); - final long basicHeaderCrc32 = read32(dis) & 0xFFFFFFFFL; + } else if (basicHeaderSize <= 2600) { + basicHeaderBytes = org.apache.commons.io.IOUtils.toByteArray(in, basicHeaderSize); + count(basicHeaderSize); + final long basicHeaderCrc32 = EndianUtils.readSwappedUnsignedInteger(in); + count(4); final CRC32 crc32 = new CRC32(); crc32.update(basicHeaderBytes); if (basicHeaderCrc32 == crc32.getValue()) { - found = true; + return basicHeaderBytes; } } - } while (!found); - return basicHeaderBytes; + } } private LocalFileHeader readLocalFileHeader() throws IOException { @@ -286,100 +297,96 @@ private LocalFileHeader readLocalFileHeader() throws IOException { 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(); + final LocalFileHeader localFileHeader = new LocalFileHeader(); + try (InputStream basicHeader = new ByteArrayInputStream(basicHeaderBytes)) { + + final int firstHeaderSize = readUnsignedByte(basicHeader); + try (InputStream firstHeader = BoundedInputStream.builder().setInputStream(basicHeader).setMaxCount(firstHeaderSize - 1).get()) { + + localFileHeader.archiverVersionNumber = readUnsignedByte(firstHeader); + localFileHeader.minVersionToExtract = readUnsignedByte(firstHeader); + localFileHeader.hostOS = readUnsignedByte(firstHeader); + localFileHeader.arjFlags = readUnsignedByte(firstHeader); + localFileHeader.method = readUnsignedByte(firstHeader); + localFileHeader.fileType = readUnsignedByte(firstHeader); + localFileHeader.reserved = readUnsignedByte(firstHeader); + localFileHeader.dateTimeModified = EndianUtils.readSwappedInteger(firstHeader); + localFileHeader.compressedSize = EndianUtils.readSwappedUnsignedInteger(firstHeader); + localFileHeader.originalSize = EndianUtils.readSwappedUnsignedInteger(firstHeader); + localFileHeader.originalCrc32 = EndianUtils.readSwappedUnsignedInteger(firstHeader); + localFileHeader.fileSpecPosition = EndianUtils.readSwappedShort(firstHeader); + localFileHeader.fileAccessMode = EndianUtils.readSwappedShort(firstHeader); + localFileHeader.firstChapter = readUnsignedByte(firstHeader); + localFileHeader.lastChapter = readUnsignedByte(firstHeader); readExtraData(firstHeaderSize, firstHeader, localFileHeader); + } - localFileHeader.name = readEntryName(basicHeader); - localFileHeader.comment = readComment(basicHeader); - - final ArrayList 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 ArchiveException("Extended header CRC32 verification failure"); - } - extendedHeaders.add(extendedHeaderBytes); - } - localFileHeader.extendedHeaders = extendedHeaders.toArray(new byte[0][]); + localFileHeader.name = readEntryName(basicHeader); + localFileHeader.comment = readComment(basicHeader); + } - return localFileHeader; + final ArrayList extendedHeaders = new ArrayList<>(); + int extendedHeaderSize; + while ((extendedHeaderSize = readSwappedUnsignedShort()) > 0) { + final byte[] extendedHeaderBytes = org.apache.commons.io.IOUtils.toByteArray(in, extendedHeaderSize); + count(extendedHeaderSize); + final long extendedHeaderCrc32 = EndianUtils.readSwappedUnsignedInteger(in); + count(4); + final CRC32 crc32 = new CRC32(); + crc32.update(extendedHeaderBytes); + if (extendedHeaderCrc32 != crc32.getValue()) { + throw new ArchiveException("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 ArchiveException("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 header = new MainHeader(); - header.archiverVersionNumber = firstHeader.readUnsignedByte(); - header.minVersionToExtract = firstHeader.readUnsignedByte(); - header.hostOS = firstHeader.readUnsignedByte(); - header.arjFlags = firstHeader.readUnsignedByte(); - header.securityVersion = firstHeader.readUnsignedByte(); - header.fileType = firstHeader.readUnsignedByte(); - header.reserved = firstHeader.readUnsignedByte(); - header.dateTimeCreated = read32(firstHeader); - header.dateTimeModified = read32(firstHeader); - header.archiveSize = 0xffffFFFFL & read32(firstHeader); - header.securityEnvelopeFilePosition = read32(firstHeader); - header.fileSpecPosition = read16(firstHeader); - header.securityEnvelopeLength = read16(firstHeader); - pushedBackBytes(20); // count has already counted them via readRange - header.encryptionVersion = firstHeader.readUnsignedByte(); - header.lastChapter = firstHeader.readUnsignedByte(); + try (InputStream basicHeader = new ByteArrayInputStream(basicHeaderBytes)) { + + final int firstHeaderSize = readUnsignedByte(basicHeader); + try (InputStream firstHeader = BoundedInputStream.builder().setInputStream(basicHeader).setMaxCount(firstHeaderSize - 1).get()) { + + header.archiverVersionNumber = readUnsignedByte(firstHeader); + header.minVersionToExtract = readUnsignedByte(firstHeader); + header.hostOS = readUnsignedByte(firstHeader); + header.arjFlags = readUnsignedByte(firstHeader); + header.securityVersion = readUnsignedByte(firstHeader); + header.fileType = readUnsignedByte(firstHeader); + header.reserved = readUnsignedByte(firstHeader); + header.dateTimeCreated = EndianUtils.readSwappedInteger(firstHeader); + header.dateTimeModified = EndianUtils.readSwappedInteger(firstHeader); + header.archiveSize = EndianUtils.readSwappedUnsignedInteger(firstHeader); + header.securityEnvelopeFilePosition = EndianUtils.readSwappedInteger(firstHeader); + header.fileSpecPosition = EndianUtils.readSwappedShort(firstHeader); + header.securityEnvelopeLength = EndianUtils.readSwappedShort(firstHeader); + header.encryptionVersion = readUnsignedByte(firstHeader); + header.lastChapter = readUnsignedByte(firstHeader); + + if (firstHeaderSize >= 33) { + header.arjProtectionFactor = readUnsignedByte(firstHeader); + header.arjFlags2 = readUnsignedByte(firstHeader); + readUnsignedByte(firstHeader); + readUnsignedByte(firstHeader); + } + } - if (firstHeaderSize >= 33) { - header.arjProtectionFactor = firstHeader.readUnsignedByte(); - header.arjFlags2 = firstHeader.readUnsignedByte(); - firstHeader.readUnsignedByte(); - firstHeader.readUnsignedByte(); + header.name = readEntryName(basicHeader); + header.comment = readComment(basicHeader); } - header.name = readEntryName(basicHeader); - header.comment = readComment(basicHeader); - - final int extendedHeaderSize = read16(dis); + final int extendedHeaderSize = readSwappedUnsignedShort(); if (extendedHeaderSize > 0) { - header.extendedHeaderBytes = readRange(dis, extendedHeaderSize); - final long extendedHeaderCrc32 = 0xffffFFFFL & read32(dis); + header.extendedHeaderBytes = org.apache.commons.io.IOUtils.toByteArray(in, extendedHeaderSize); + count(extendedHeaderSize); + final long extendedHeaderCrc32 = EndianUtils.readSwappedUnsignedInteger(in); + count(4); final CRC32 crc32 = new CRC32(); crc32.update(header.extendedHeaderBytes); if (extendedHeaderCrc32 != crc32.getValue()) { @@ -390,32 +397,25 @@ private MainHeader readMainHeader() throws IOException { return header; } - 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 readComment(DataInputStream dataIn) throws IOException { - return new String(readString(dataIn).toByteArray(), getCharset()); - } - - private String readEntryName(DataInputStream dataIn) throws IOException { - final ByteArrayOutputStream buffer = readString(dataIn); - ArchiveUtils.checkEntryNameLength(buffer.size(), getMaxEntryNameLength(), "ARJ"); - return new String(buffer.toByteArray(), getCharset()); - } - - private ByteArrayOutputStream readString(DataInputStream dataIn) throws IOException { + private ByteArrayOutputStream readString(final InputStream dataIn) throws IOException { try (ByteArrayOutputStream buffer = new ByteArrayOutputStream()) { int nextByte; - while ((nextByte = dataIn.readUnsignedByte()) != 0) { + while ((nextByte = readUnsignedByte(dataIn)) != 0) { buffer.write(nextByte); } return buffer; } } + + private int readSwappedUnsignedShort() throws IOException { + final int value = EndianUtils.readSwappedUnsignedShort(in); + count(2); + return value; + } + + private int readUnsignedByte() throws IOException { + final int value = readUnsignedByte(in); + count(1); + return value & 0xff; + } } diff --git a/src/test/java/org/apache/commons/compress/LegacyConstructorsTest.java b/src/test/java/org/apache/commons/compress/LegacyConstructorsTest.java index d82ef793fe5..40c11a81bf0 100644 --- a/src/test/java/org/apache/commons/compress/LegacyConstructorsTest.java +++ b/src/test/java/org/apache/commons/compress/LegacyConstructorsTest.java @@ -100,7 +100,6 @@ static Stream testZipConstructors() throws IOException { void testArjConstructor() throws Exception { try (InputStream inputStream = Files.newInputStream(getPath("bla.arj")); ArjArchiveInputStream archiveInputStream = new ArjArchiveInputStream(inputStream, "US-ASCII")) { - // Arj wraps the input stream in a DataInputStream assertEquals(inputStream, getNestedInputStream(archiveInputStream)); assertEquals(US_ASCII, archiveInputStream.getCharset()); } diff --git a/src/test/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStreamTest.java b/src/test/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStreamTest.java index f85abafaf8e..25a66622053 100644 --- a/src/test/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStreamTest.java +++ b/src/test/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStreamTest.java @@ -25,17 +25,22 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.nio.file.Files; +import java.nio.file.Path; import java.util.Calendar; import java.util.TimeZone; import org.apache.commons.compress.AbstractTest; import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.BoundedInputStream; import org.apache.commons.io.output.ByteArrayOutputStream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; /** * Tests {@link ArjArchiveInputStream}. @@ -106,6 +111,19 @@ void testForEach() throws Exception { assertEquals(expected.toString(), result.toString()); } + @ParameterizedTest + @ValueSource(strings = { "bla.arj", "bla.unix.arj" }) + void testGetBytesRead(final String resource) throws IOException { + final Path path = getPath(resource); + try (ArjArchiveInputStream in = ArjArchiveInputStream.builder().setPath(path).get()) { + while (in.getNextEntry() != null) { + // nop + } + final long expected = Files.size(path); + assertEquals(expected, in.getBytesRead(), "getBytesRead() did not return the expected value"); + } + } + @Test void testGetNextEntry() throws Exception { final StringBuilder expected = new StringBuilder(); @@ -266,4 +284,91 @@ void testSingleByteReadConsistentlyReturnsMinusOneAtEof() throws Exception { assertForEach(archive); } } + + /** + * Verifies that reading an ARJ header record cut short at various boundaries + * results in an {@link EOFException}. + * + *

The test archive is crafted so that the local file header of the first entry begins at + * byte offset {@code 0x0035}. Within that header:

+ *
    + *
  • Basic header size (2 bytes at offsets 0x02–0x03) = {@code 0x0039}.
  • + *
  • Fixed header size (aka {@code first_hdr_size}, 1 byte at 0x04) = {@code 0x2E}.
  • + *
  • The filename and comment C-strings follow the fixed header and complete the basic header.
  • + *
  • A 4-byte basic header CRC-32 follows the basic header.
  • + *
+ * + * @param maxCount absolute truncation point (number of readable bytes from the start of the file) + */ + @ParameterizedTest + @ValueSource(longs = { + // Before the local file header signature + 0x35, + // Immediately after the 2-byte signature + 0x35 + 0x02, + // Inside / after the basic-header size (2 bytes at 0x02–0x03) + 0x35 + 0x03, 0x35 + 0x04, + // Just after the fixed-header size (1 byte at 0x04) + 0x35 + 0x05, + // End of fixed header (0x04 + first_hdr_size == 0x32) + 0x35 + 0x32, + // End of basic header after filename/comment (0x04 + basic_hdr_size == 0x3d) + 0x35 + 0x3d, + // Inside / after the basic-header CRC-32 (4 bytes) + 0x35 + 0x3e, 0x35 + 0x41, + // Inside / after the extended-header length (2 bytes) + 0x35 + 0x42, 0x35 + 0x43, + // One byte before the first file’s data + 0x95 + }) + void testTruncatedLocalHeader(long maxCount) throws Exception { + try (InputStream input = BoundedInputStream.builder().setURI(getURI("bla.arj")).setMaxCount(maxCount).get(); + ArjArchiveInputStream archive = ArjArchiveInputStream.builder().setInputStream(input).get()) { + assertThrows(EOFException.class, () -> { + archive.getNextEntry(); + IOUtils.skip(archive, Long.MAX_VALUE); + }); + } + } + + /** + * Verifies that reading an ARJ header record cut short at various boundaries + * results in an {@link EOFException}. + * + *

The main archive header is at the beginning of the file. Within that header:

+ *
    + *
  • Basic header size (2 bytes at offsets 0x02–0x03) = {@code 0x002b}.
  • + *
  • Fixed header size (aka {@code first_hdr_size}, 1 byte at 0x04) = {@code 0x22}.
  • + *
  • The archive name and comment C-strings follow the fixed header and complete the basic header.
  • + *
  • A 4-byte basic header CRC-32 follows the basic header.
  • + *
+ * + * @param maxCount absolute truncation point (number of readable bytes from the start of the file) + */ + @ParameterizedTest + @ValueSource(longs = { + // Empty file. + 0, + // Immediately after the 2-byte signature + 0x02, + // Inside / after the basic-header size (2 bytes at 0x02–0x03) + 0x03, 0x04, + // Just after the fixed-header size (1 byte at 0x04) + 0x05, + // End of fixed header (0x04 + first_hdr_size == 0x26) + 0x26, + // End of basic header after filename/comment (0x04 + basic_hdr_size == 0x2f) + 0x2f, + // Inside / after the basic-header CRC-32 (4 bytes) + 0x30, 0x33, + // Inside the extended-header length (2 bytes) + 0x34}) + void testTruncatedMainHeader(long maxCount) throws Exception { + try (InputStream input = BoundedInputStream.builder() + .setURI(getURI("bla.arj")) + .setMaxCount(maxCount) + .get()) { + assertThrows(EOFException.class, () -> ArjArchiveInputStream.builder().setInputStream(input).get()); + } + } }