You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2026/07/31 15:20:55 UTC
(commons-compress) 11/19: Sort members
This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-compress.git
commit 7d9ab8e2a7601b79a846e792b49b347f9f4fed9a
Author: Gary Gregory <ga...@gmail.com>
AuthorDate: Fri Jul 31 10:30:57 2026 -0400
Sort members
---
.../archivers/lha/LhaArchiveInputStream.java | 814 ++++----
.../archivers/lha/LhaArchiveEntryTest.java | 30 +-
.../archivers/lha/LhaArchiveInputStreamTest.java | 2012 ++++++++++----------
3 files changed, 1428 insertions(+), 1428 deletions(-)
diff --git a/src/main/java/org/apache/commons/compress/archivers/lha/LhaArchiveInputStream.java b/src/main/java/org/apache/commons/compress/archivers/lha/LhaArchiveInputStream.java
index c201a89bc..f6c33619f 100644
--- a/src/main/java/org/apache/commons/compress/archivers/lha/LhaArchiveInputStream.java
+++ b/src/main/java/org/apache/commons/compress/archivers/lha/LhaArchiveInputStream.java
@@ -182,11 +182,6 @@ Builder setFileSeparatorChar(final char fileSeparatorChar) {
*/
private static final Charset DEFAULT_CHARSET = StandardCharsets.US_ASCII;
- private final char fileSeparatorChar;
- private LhaArchiveEntry currentEntry;
- private InputStream currentCompressedStream;
- private InputStream currentDecompressedStream;
-
/**
* Creates a new builder.
*
@@ -195,32 +190,47 @@ Builder setFileSeparatorChar(final char fileSeparatorChar) {
public static Builder builder() {
return new Builder();
}
-
- private LhaArchiveInputStream(final Builder builder) throws IOException {
- super(builder);
- this.fileSeparatorChar = builder.fileSeparatorChar;
- }
-
- @Override
- public boolean canReadEntryData(final ArchiveEntry archiveEntry) {
- return currentDecompressedStream != null;
- }
-
- @Override
- public int read(final byte[] buffer, final int offset, final int length) throws IOException {
- if (currentEntry == null) {
- throw new IllegalStateException("No current entry");
+ /**
+ * Get a byte array from the ByteBuffer at the specified position and length.
+ * This is needed until this repo has been updated to use Java 9+ where we
+ * can use buffer.get(position, dst) directly.
+ *
+ * @param buffer the buffer to get the byte array from
+ * @param position the position in the buffer to start reading from
+ * @param dst the destination byte array to fill
+ */
+ private static void byteBufferGet(final ByteBuffer buffer, final int position, final byte[] dst) {
+ for (int i = 0; i < dst.length; i++) {
+ dst[i] = buffer.get(position + i);
}
+ }
+ /**
+ * Gets the compression method from the header. It is always located at the same offset for all header levels.
+ *
+ * @param buffer the buffer containing the header data
+ * @return compression method, e.g. -lh5-
+ * @throws ArchiveException if the compression method is invalid
+ */
+ static String getCompressionMethod(final ByteBuffer buffer) throws ArchiveException {
+ final byte[] compressionMethodBuffer = new byte[5];
+ byteBufferGet(buffer, HEADER_GENERIC_OFFSET_COMPRESSION_METHOD, compressionMethodBuffer);
- if (currentDecompressedStream == null) {
- throw new ArchiveException("Unsupported compression method: %s", currentEntry.getCompressionMethod());
+ // Validate the compression method
+ if (compressionMethodBuffer[0] == '-' &&
+ Character.isLowerCase(compressionMethodBuffer[1]) &&
+ Character.isLowerCase(compressionMethodBuffer[2]) &&
+ (Character.isLowerCase(compressionMethodBuffer[3]) || Character.isDigit(compressionMethodBuffer[3])) &&
+ compressionMethodBuffer[4] == '-') {
+ return new String(compressionMethodBuffer, StandardCharsets.US_ASCII);
+ } else {
+ throw new ArchiveException("Invalid compression method: 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x",
+ compressionMethodBuffer[0],
+ compressionMethodBuffer[1],
+ compressionMethodBuffer[2],
+ compressionMethodBuffer[3],
+ compressionMethodBuffer[4]);
}
-
- final int read = currentDecompressedStream.read(buffer, offset, length);
- count(read);
- return read;
}
-
/**
* Checks if the signature matches what is expected for an LHA file. There is no specific
* signature for LHA files, so this method checks if the header level and the compression
@@ -254,326 +264,132 @@ public static boolean matches(final byte[] signature, final int length) {
return true;
}
- @Override
- public LhaArchiveEntry getNextEntry() throws IOException {
- if (this.currentCompressedStream != null) {
- // Consume the entire compressed stream to end up at the next entry
- IOUtils.consume(this.currentCompressedStream);
+ private final char fileSeparatorChar;
- this.currentCompressedStream = null;
- this.currentDecompressedStream = null;
- }
+ private LhaArchiveEntry currentEntry;
- this.currentEntry = readHeader();
+ private InputStream currentCompressedStream;
- return this.currentEntry;
+ private InputStream currentDecompressedStream;
+
+ private LhaArchiveInputStream(final Builder builder) throws IOException {
+ super(builder);
+ this.fileSeparatorChar = builder.fileSeparatorChar;
}
/**
- * Read the next LHA header from the input stream.
+ * Create a new ByteBuffer slice from the provided buffer at the specified position and length. This is needed until this
+ * repo has been updated to use Java 9+ where we can use buffer.position(position).slice().limit(length) directly.
*
- * @return the next header entry, or null if there are no more entries
- * @throws IOException
+ * @param buffer the buffer to slice from
+ * @param position the position in the buffer to start slicing from
+ * @param length the length of the slice
+ * @return a new ByteBuffer slice with the specified position and length
*/
- LhaArchiveEntry readHeader() throws IOException {
- // Header level is not known yet. Read the minimum length header.
- final byte[] buffer = new byte[HEADER_GENERIC_MINIMUM_HEADER_LENGTH];
- final int len = IOUtils.read(in, buffer);
- if (len == 0) {
- // EOF
- return null;
- } else if (len == 1 && buffer[0] == 0) {
- // Last byte of the file is zero indicating no more entries
- return null;
- } else if (len < HEADER_GENERIC_MINIMUM_HEADER_LENGTH) {
- throw new ArchiveException("Invalid header length");
- }
-
- final ByteBuffer header = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN);
-
- // Determine header level
- final byte headerLevel = header.get(HEADER_GENERIC_OFFSET_HEADER_LEVEL);
- if (headerLevel == 0) {
- return readHeaderLevel0(header);
- } else if (headerLevel == 1) {
- return readHeaderLevel1(header);
- } else if (headerLevel == 2) {
- return readHeaderLevel2(header);
- } else {
- throw new ArchiveException("Invalid header level: %d", headerLevel);
- }
+ private ByteBuffer byteBufferSlice(final ByteBuffer buffer, final int position, final int length) {
+ return ByteBuffer.wrap(buffer.array(), position, length);
}
/**
- * Read LHA header level 0.
+ * Calculate the CRC16 checksum of the provided buffers.
*
- * @param buffer the buffer containing the header data
- * @return the LhaArchiveEntry read from the buffer
- * @throws IOException
+ * @param buffers the buffers to calculate the CRC16 checksum for
+ * @return CRC16 checksum
*/
- LhaArchiveEntry readHeaderLevel0(ByteBuffer buffer) throws IOException {
- // Add two to the header size as the first two bytes are not included
- final int headerSize = Byte.toUnsignedInt(buffer.get(HEADER_LEVEL_0_OFFSET_HEADER_SIZE)) + 2;
- if (headerSize < HEADER_GENERIC_MINIMUM_HEADER_LENGTH) {
- throw new ArchiveException("Invalid header level 0 length: %d", headerSize);
+ private long calculateCRC16(final ByteBuffer... buffers) {
+ final Checksum crc = Crc16.arc();
+ for (ByteBuffer buffer : buffers) {
+ crc.update(buffer.array(), 0, buffer.limit());
}
- buffer = readRemainingHeaderData(buffer, headerSize);
-
- final int headerChecksum = Byte.toUnsignedInt(buffer.get(HEADER_LEVEL_0_OFFSET_HEADER_CHECKSUM));
-
- final String compressionMethod = getCompressionMethod(buffer);
+ return crc.getValue();
+ }
- final LhaArchiveEntry.Builder entryBuilder = new LhaArchiveEntry.Builder()
- .setCompressionMethod(compressionMethod)
- .setCompressedSize(Integer.toUnsignedLong(buffer.getInt(HEADER_LEVEL_0_OFFSET_COMPRESSED_SIZE)))
- .setSize(Integer.toUnsignedLong(buffer.getInt(HEADER_LEVEL_0_OFFSET_ORIGINAL_SIZE)))
- .setLastModifiedDate(new Date(ZipUtil.dosToJavaTime(Integer.toUnsignedLong(buffer.getInt(HEADER_LEVEL_0_OFFSET_LAST_MODIFIED_DATE_TIME)))));
+ /**
+ * Calculate the header sum for level 0 and 1 headers. The checksum is calculated by summing the
+ * value of all bytes in the header except for the first two bytes (header length and header checksum)
+ * and get the low 8 bits.
+ *
+ * @param buffer the buffer containing the header
+ * @return checksum
+ */
+ private int calculateHeaderChecksum(final ByteBuffer buffer) {
+ int sum = 0;
+ for (int i = 2; i < buffer.limit(); i++) {
+ sum += Byte.toUnsignedInt(buffer.get(i));
+ }
- final int filenameLength = Byte.toUnsignedInt(buffer.get(HEADER_LEVEL_0_OFFSET_FILENAME_LENGTH));
+ return sum & 0xff;
+ }
- // Make sure the filename is not overflowing into the CRC field
- if (filenameLength > (headerSize - HEADER_LEVEL_0_OFFSET_FILENAME - 2)) {
- throw new ArchiveException("Invalid pathname length");
- }
+ @Override
+ public boolean canReadEntryData(final ArchiveEntry archiveEntry) {
+ return currentDecompressedStream != null;
+ }
- buffer.position(HEADER_LEVEL_0_OFFSET_FILENAME);
- entryBuilder.setFilename(getPathname(buffer, filenameLength))
- .setDirectory(isDirectory(compressionMethod))
- .setCrcValue(Short.toUnsignedInt(buffer.getShort()));
+ @Override
+ public LhaArchiveEntry getNextEntry() throws IOException {
+ if (this.currentCompressedStream != null) {
+ // Consume the entire compressed stream to end up at the next entry
+ IOUtils.consume(this.currentCompressedStream);
- if (calculateHeaderChecksum(buffer) != headerChecksum) {
- throw new ArchiveException("Invalid header level 0 checksum");
+ this.currentCompressedStream = null;
+ this.currentDecompressedStream = null;
}
- final LhaArchiveEntry entry = entryBuilder.get();
-
- prepareDecompression(entry);
+ this.currentEntry = readHeader();
- return entry;
+ return this.currentEntry;
}
/**
- * Read LHA header level 1.
+ * Gets the pathname from the current position in the provided buffer. Any 0xFF bytes
+ * and '\' chars will be converted into the configured file path separator char.
+ * Any leading file path separator char will be removed to avoid extracting to
+ * absolute locations.
*
- * @param buffer the buffer containing the header data
- * @return the LhaArchiveEntry read from the buffer
- * @throws IOException
+ * @param buffer the buffer where to get the pathname from
+ * @param pathnameLength the length of the pathname
+ * @return pathname
+ * @throws ArchiveException if the pathname is too long
*/
- LhaArchiveEntry readHeaderLevel1(ByteBuffer buffer) throws IOException {
- // Add two to the header size as the first two bytes are not included
- final int baseHeaderSize = Byte.toUnsignedInt(buffer.get(HEADER_LEVEL_1_OFFSET_BASE_HEADER_SIZE)) + 2;
- if (baseHeaderSize < HEADER_GENERIC_MINIMUM_HEADER_LENGTH) {
- throw new ArchiveException("Invalid header level 1 length: %d", baseHeaderSize);
+ String getPathname(final ByteBuffer buffer, final int pathnameLength) throws ArchiveException {
+ // Check pathname length to ensure we don't allocate too much memory
+ if (pathnameLength > MAX_PATHNAME_LENGTH) {
+ throw new ArchiveException("Pathname is longer than the maximum allowed (%d > %d)", pathnameLength, MAX_PATHNAME_LENGTH);
+ } else if (pathnameLength < 0) {
+ throw new ArchiveException("Pathname length is negative");
+ } else if (pathnameLength > (buffer.limit() - buffer.position())) {
+ throw new ArchiveException("Invalid pathname length");
}
- buffer = readRemainingHeaderData(buffer, baseHeaderSize);
+ final byte[] pathnameBuffer = new byte[pathnameLength];
+ buffer.get(pathnameBuffer);
- final int baseHeaderChecksum = Byte.toUnsignedInt(buffer.get(HEADER_LEVEL_1_OFFSET_BASE_HEADER_CHECKSUM));
+ // Split the pathname into parts by 0xFF bytes
+ final StringBuilder pathnameStringBuilder = new StringBuilder();
+ int start = 0;
+ for (int i = 0; i < pathnameLength; i++) {
+ if (pathnameBuffer[i] == (byte) 0xFF) {
+ if (i > start) {
+ // Decode the path segment into a string using the specified charset and append it to the result
+ pathnameStringBuilder.append(new String(pathnameBuffer, start, i - start, getCharset())).append(fileSeparatorChar);
+ }
- final String compressionMethod = getCompressionMethod(buffer);
- long skipSize = Integer.toUnsignedLong(buffer.getInt(HEADER_LEVEL_1_OFFSET_SKIP_SIZE));
+ start = i + 1; // Move start to the next segment
+ }
+ }
- final LhaArchiveEntry.Builder entryBuilder = new LhaArchiveEntry.Builder()
- .setCompressionMethod(compressionMethod)
- .setSize(Integer.toUnsignedLong(buffer.getInt(HEADER_LEVEL_1_OFFSET_ORIGINAL_SIZE)))
- .setLastModifiedDate(new Date(ZipUtil.dosToJavaTime(Integer.toUnsignedLong(buffer.getInt(HEADER_LEVEL_1_OFFSET_LAST_MODIFIED_DATE_TIME)))));
+ // Append the last segment if it exists
+ if (start < pathnameLength) {
+ pathnameStringBuilder.append(new String(pathnameBuffer, start, pathnameLength - start, getCharset()));
+ }
- final int filenameLength = Byte.toUnsignedInt(buffer.get(HEADER_LEVEL_1_OFFSET_FILENAME_LENGTH));
+ String pathname = pathnameStringBuilder.toString();
- // Make sure the filename is not overflowing into the CRC, OS ID and first extended header length fields.
- // This check is not bulletproof because there might also be an extended area after the filename that
- // we cannot detect for corrupt archives.
- if (filenameLength > (baseHeaderSize - HEADER_LEVEL_1_OFFSET_FILENAME - 5)) {
- throw new ArchiveException("Invalid pathname length");
- }
-
- buffer.position(HEADER_LEVEL_1_OFFSET_FILENAME);
- entryBuilder.setFilename(getPathname(buffer, filenameLength))
- .setDirectory(isDirectory(compressionMethod))
- .setCrcValue(Short.toUnsignedInt(buffer.getShort()))
- .setOsId(Byte.toUnsignedInt(buffer.get()));
-
- if (calculateHeaderChecksum(buffer) != baseHeaderChecksum) {
- throw new ArchiveException("Invalid header level 1 checksum");
- }
-
- // Create a list to store base header and all extended headers
- // to be able to calculate the CRC of the full header
- final List<ByteBuffer> headerParts = new ArrayList<>();
- headerParts.add(buffer);
-
- buffer.position(baseHeaderSize - 2); // First extended header length is at the end of the base header
- int extendedHeaderSize = Short.toUnsignedInt(buffer.getShort());
- while (extendedHeaderSize > 0) {
- final ByteBuffer extendedHeaderBuffer = readExtendedHeader(extendedHeaderSize);
- skipSize -= extendedHeaderSize;
-
- parseExtendedHeader(extendedHeaderBuffer, entryBuilder);
-
- headerParts.add(extendedHeaderBuffer);
-
- extendedHeaderSize = Short.toUnsignedInt(extendedHeaderBuffer.getShort(extendedHeaderBuffer.limit() - 2));
- }
-
- // The compressed size is derived by subtracting the extended header sizes from the skip size. A
- // corrupt archive whose extended headers exceed the skip size would yield a negative compressed
- // size, which must be rejected as it would otherwise disable the per-entry read bound.
- if (skipSize < 0) {
- throw new ArchiveException("Invalid compressed size");
- }
-
- entryBuilder.setCompressedSize(skipSize);
-
- final LhaArchiveEntry entry = entryBuilder.get();
-
- if (entry.getHeaderCrc() != null) {
- // Calculate CRC16 of full header
- final long headerCrc = calculateCRC16(headerParts.toArray(new ByteBuffer[headerParts.size()]));
- if (headerCrc != entry.getHeaderCrc()) {
- throw new ArchiveException("Invalid header CRC expected=0x%04x found=0x%04x", headerCrc, entry.getHeaderCrc());
- }
- }
-
- prepareDecompression(entry);
-
- return entry;
- }
-
- /**
- * Read LHA header level 2.
- *
- * @param buffer the buffer containing the header data
- * @return the LhaArchiveEntry read from the buffer
- * @throws IOException
- */
- LhaArchiveEntry readHeaderLevel2(ByteBuffer buffer) throws IOException {
- final int headerSize = Short.toUnsignedInt(buffer.getShort(HEADER_LEVEL_2_OFFSET_HEADER_SIZE));
- if (headerSize < HEADER_LEVEL_2_MINIMUM_HEADER_LENGTH) {
- throw new ArchiveException("Invalid header level 2 length: %d", headerSize);
- }
-
- buffer = readRemainingHeaderData(buffer, headerSize);
-
- final String compressionMethod = getCompressionMethod(buffer);
-
- final LhaArchiveEntry.Builder entryBuilder = new LhaArchiveEntry.Builder()
- .setCompressionMethod(compressionMethod)
- .setCompressedSize(Integer.toUnsignedLong(buffer.getInt(HEADER_LEVEL_2_OFFSET_COMPRESSED_SIZE)))
- .setSize(Integer.toUnsignedLong(buffer.getInt(HEADER_LEVEL_2_OFFSET_ORIGINAL_SIZE)))
- .setLastModifiedDate(new Date(Integer.toUnsignedLong(buffer.getInt(HEADER_LEVEL_2_OFFSET_LAST_MODIFIED_DATE_TIME)) * 1000))
- .setDirectory(isDirectory(compressionMethod))
- .setCrcValue(Short.toUnsignedInt(buffer.getShort(HEADER_LEVEL_2_OFFSET_CRC)))
- .setOsId(Byte.toUnsignedInt(buffer.get(HEADER_LEVEL_2_OFFSET_OS_ID)));
-
- int extendedHeaderSize = Short.toUnsignedInt(buffer.getShort(HEADER_LEVEL_2_OFFSET_FIRST_EXTENDED_HEADER_SIZE));
- int extendedHeaderOffset = HEADER_LEVEL_2_OFFSET_FIRST_EXTENDED_HEADER_SIZE + 2;
- while (extendedHeaderSize > 0) {
- if ((extendedHeaderOffset + extendedHeaderSize) > buffer.limit()) {
- throw new ArchiveException("Invalid extended header length");
- }
-
- // Create new ByteBuffer as a slice from the full header. Set limit to the extended header length.
- final ByteBuffer extendedHeaderBuffer = byteBufferSlice(buffer, extendedHeaderOffset, extendedHeaderSize).order(ByteOrder.LITTLE_ENDIAN);
-
- extendedHeaderOffset += extendedHeaderSize;
-
- parseExtendedHeader(extendedHeaderBuffer, entryBuilder);
-
- extendedHeaderSize = Short.toUnsignedInt(extendedHeaderBuffer.getShort(extendedHeaderBuffer.limit() - 2));
- }
-
- final LhaArchiveEntry entry = entryBuilder.get();
-
- if (entry.getHeaderCrc() != null) {
- // Calculate CRC16 of full header
- final long headerCrc = calculateCRC16(buffer);
- if (headerCrc != entry.getHeaderCrc()) {
- throw new ArchiveException("Invalid header CRC expected=0x%04x found=0x%04x", headerCrc, entry.getHeaderCrc());
- }
- }
-
- prepareDecompression(entry);
-
- return entry;
- }
-
- /**
- * Gets the compression method from the header. It is always located at the same offset for all header levels.
- *
- * @param buffer the buffer containing the header data
- * @return compression method, e.g. -lh5-
- * @throws ArchiveException if the compression method is invalid
- */
- static String getCompressionMethod(final ByteBuffer buffer) throws ArchiveException {
- final byte[] compressionMethodBuffer = new byte[5];
- byteBufferGet(buffer, HEADER_GENERIC_OFFSET_COMPRESSION_METHOD, compressionMethodBuffer);
-
- // Validate the compression method
- if (compressionMethodBuffer[0] == '-' &&
- Character.isLowerCase(compressionMethodBuffer[1]) &&
- Character.isLowerCase(compressionMethodBuffer[2]) &&
- (Character.isLowerCase(compressionMethodBuffer[3]) || Character.isDigit(compressionMethodBuffer[3])) &&
- compressionMethodBuffer[4] == '-') {
- return new String(compressionMethodBuffer, StandardCharsets.US_ASCII);
- } else {
- throw new ArchiveException("Invalid compression method: 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x",
- compressionMethodBuffer[0],
- compressionMethodBuffer[1],
- compressionMethodBuffer[2],
- compressionMethodBuffer[3],
- compressionMethodBuffer[4]);
- }
- }
-
- /**
- * Gets the pathname from the current position in the provided buffer. Any 0xFF bytes
- * and '\' chars will be converted into the configured file path separator char.
- * Any leading file path separator char will be removed to avoid extracting to
- * absolute locations.
- *
- * @param buffer the buffer where to get the pathname from
- * @param pathnameLength the length of the pathname
- * @return pathname
- * @throws ArchiveException if the pathname is too long
- */
- String getPathname(final ByteBuffer buffer, final int pathnameLength) throws ArchiveException {
- // Check pathname length to ensure we don't allocate too much memory
- if (pathnameLength > MAX_PATHNAME_LENGTH) {
- throw new ArchiveException("Pathname is longer than the maximum allowed (%d > %d)", pathnameLength, MAX_PATHNAME_LENGTH);
- } else if (pathnameLength < 0) {
- throw new ArchiveException("Pathname length is negative");
- } else if (pathnameLength > (buffer.limit() - buffer.position())) {
- throw new ArchiveException("Invalid pathname length");
- }
-
- final byte[] pathnameBuffer = new byte[pathnameLength];
- buffer.get(pathnameBuffer);
-
- // Split the pathname into parts by 0xFF bytes
- final StringBuilder pathnameStringBuilder = new StringBuilder();
- int start = 0;
- for (int i = 0; i < pathnameLength; i++) {
- if (pathnameBuffer[i] == (byte) 0xFF) {
- if (i > start) {
- // Decode the path segment into a string using the specified charset and append it to the result
- pathnameStringBuilder.append(new String(pathnameBuffer, start, i - start, getCharset())).append(fileSeparatorChar);
- }
-
- start = i + 1; // Move start to the next segment
- }
- }
-
- // Append the last segment if it exists
- if (start < pathnameLength) {
- pathnameStringBuilder.append(new String(pathnameBuffer, start, pathnameLength - start, getCharset()));
- }
-
- String pathname = pathnameStringBuilder.toString();
-
- // If the path separator char is not '\', replace all '\' characters with the path separator char
- if (fileSeparatorChar != '\\') {
- pathname = pathname.replace('\\', fileSeparatorChar);
+ // If the path separator char is not '\', replace all '\' characters with the path separator char
+ if (fileSeparatorChar != '\\') {
+ pathname = pathname.replace('\\', fileSeparatorChar);
}
// Remove leading file separator chars to avoid extracting to absolute locations
@@ -585,38 +401,13 @@ String getPathname(final ByteBuffer buffer, final int pathnameLength) throws Arc
}
/**
- * Read the remaining part of the header and append it to the already loaded parts.
- *
- * @param currentHeader all header parts that have already been loaded into memory
- * @param headerSize total header size
- * @return header the complete header as a ByteBuffer
- * @throws IOException
- */
- private ByteBuffer readRemainingHeaderData(final ByteBuffer currentHeader, final int headerSize) throws IOException {
- final byte[] remainingData = new byte[headerSize - currentHeader.capacity()];
- final int len = IOUtils.read(in, remainingData);
- if (len != remainingData.length) {
- throw new ArchiveException("Error reading remaining header");
- }
-
- return ByteBuffer.allocate(currentHeader.capacity() + len).put(currentHeader.array()).put(remainingData).order(ByteOrder.LITTLE_ENDIAN);
- }
-
- /**
- * Read extended header from the input stream.
+ * Tests whether the compression method is a directory entry.
*
- * @param headerSize the size of the extended header to read
- * @return the extended header as a ByteBuffer
- * @throws IOException
+ * @param compressionMethod the compression method
+ * @return true if the compression method is a directory entry, false otherwise
*/
- private ByteBuffer readExtendedHeader(final int headerSize) throws IOException {
- final byte[] extensionHeader = new byte[headerSize];
- final int len = IOUtils.read(in, extensionHeader);
- if (len != extensionHeader.length) {
- throw new ArchiveException("Error reading extended header");
- }
-
- return ByteBuffer.wrap(extensionHeader).order(ByteOrder.LITTLE_ENDIAN);
+ private boolean isDirectory(final String compressionMethod) {
+ return COMPRESSION_METHOD_DIRECTORY.equals(compressionMethod);
}
/**
@@ -696,54 +487,12 @@ void parseExtendedHeader(final ByteBuffer extendedHeaderBuffer, final LhaArchive
// Ignore unknown extended header
}
- /**
- * Tests whether the compression method is a directory entry.
- *
- * @param compressionMethod the compression method
- * @return true if the compression method is a directory entry, false otherwise
- */
- private boolean isDirectory(final String compressionMethod) {
- return COMPRESSION_METHOD_DIRECTORY.equals(compressionMethod);
- }
-
- /**
- * Calculate the header sum for level 0 and 1 headers. The checksum is calculated by summing the
- * value of all bytes in the header except for the first two bytes (header length and header checksum)
- * and get the low 8 bits.
- *
- * @param buffer the buffer containing the header
- * @return checksum
- */
- private int calculateHeaderChecksum(final ByteBuffer buffer) {
- int sum = 0;
- for (int i = 2; i < buffer.limit(); i++) {
- sum += Byte.toUnsignedInt(buffer.get(i));
- }
-
- return sum & 0xff;
- }
-
- /**
- * Calculate the CRC16 checksum of the provided buffers.
- *
- * @param buffers the buffers to calculate the CRC16 checksum for
- * @return CRC16 checksum
- */
- private long calculateCRC16(final ByteBuffer... buffers) {
- final Checksum crc = Crc16.arc();
- for (ByteBuffer buffer : buffers) {
- crc.update(buffer.array(), 0, buffer.limit());
- }
-
- return crc.getValue();
- }
-
- private void prepareDecompression(final LhaArchiveEntry entry) throws IOException {
- // Make sure we never read more than the compressed size of the entry
- this.currentCompressedStream = BoundedInputStream.builder()
- .setInputStream(in)
- .setMaxCount(entry.getCompressedSize())
- .get();
+ private void prepareDecompression(final LhaArchiveEntry entry) throws IOException {
+ // Make sure we never read more than the compressed size of the entry
+ this.currentCompressedStream = BoundedInputStream.builder()
+ .setInputStream(in)
+ .setMaxCount(entry.getCompressedSize())
+ .get();
if (isDirectory(entry.getCompressionMethod())) {
// Directory entry
@@ -785,31 +534,282 @@ private void prepareDecompression(final LhaArchiveEntry entry) throws IOExceptio
}
}
+ @Override
+ public int read(final byte[] buffer, final int offset, final int length) throws IOException {
+ if (currentEntry == null) {
+ throw new IllegalStateException("No current entry");
+ }
+
+ if (currentDecompressedStream == null) {
+ throw new ArchiveException("Unsupported compression method: %s", currentEntry.getCompressionMethod());
+ }
+
+ final int read = currentDecompressedStream.read(buffer, offset, length);
+ count(read);
+ return read;
+ }
+
/**
- * Create a new ByteBuffer slice from the provided buffer at the specified position and length. This is needed until this
- * repo has been updated to use Java 9+ where we can use buffer.position(position).slice().limit(length) directly.
+ * Read extended header from the input stream.
*
- * @param buffer the buffer to slice from
- * @param position the position in the buffer to start slicing from
- * @param length the length of the slice
- * @return a new ByteBuffer slice with the specified position and length
+ * @param headerSize the size of the extended header to read
+ * @return the extended header as a ByteBuffer
+ * @throws IOException
*/
- private ByteBuffer byteBufferSlice(final ByteBuffer buffer, final int position, final int length) {
- return ByteBuffer.wrap(buffer.array(), position, length);
+ private ByteBuffer readExtendedHeader(final int headerSize) throws IOException {
+ final byte[] extensionHeader = new byte[headerSize];
+ final int len = IOUtils.read(in, extensionHeader);
+ if (len != extensionHeader.length) {
+ throw new ArchiveException("Error reading extended header");
+ }
+
+ return ByteBuffer.wrap(extensionHeader).order(ByteOrder.LITTLE_ENDIAN);
}
/**
- * Get a byte array from the ByteBuffer at the specified position and length.
- * This is needed until this repo has been updated to use Java 9+ where we
- * can use buffer.get(position, dst) directly.
+ * Read the next LHA header from the input stream.
*
- * @param buffer the buffer to get the byte array from
- * @param position the position in the buffer to start reading from
- * @param dst the destination byte array to fill
+ * @return the next header entry, or null if there are no more entries
+ * @throws IOException
*/
- private static void byteBufferGet(final ByteBuffer buffer, final int position, final byte[] dst) {
- for (int i = 0; i < dst.length; i++) {
- dst[i] = buffer.get(position + i);
+ LhaArchiveEntry readHeader() throws IOException {
+ // Header level is not known yet. Read the minimum length header.
+ final byte[] buffer = new byte[HEADER_GENERIC_MINIMUM_HEADER_LENGTH];
+ final int len = IOUtils.read(in, buffer);
+ if (len == 0) {
+ // EOF
+ return null;
+ } else if (len == 1 && buffer[0] == 0) {
+ // Last byte of the file is zero indicating no more entries
+ return null;
+ } else if (len < HEADER_GENERIC_MINIMUM_HEADER_LENGTH) {
+ throw new ArchiveException("Invalid header length");
+ }
+
+ final ByteBuffer header = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN);
+
+ // Determine header level
+ final byte headerLevel = header.get(HEADER_GENERIC_OFFSET_HEADER_LEVEL);
+ if (headerLevel == 0) {
+ return readHeaderLevel0(header);
+ } else if (headerLevel == 1) {
+ return readHeaderLevel1(header);
+ } else if (headerLevel == 2) {
+ return readHeaderLevel2(header);
+ } else {
+ throw new ArchiveException("Invalid header level: %d", headerLevel);
+ }
+ }
+
+ /**
+ * Read LHA header level 0.
+ *
+ * @param buffer the buffer containing the header data
+ * @return the LhaArchiveEntry read from the buffer
+ * @throws IOException
+ */
+ LhaArchiveEntry readHeaderLevel0(ByteBuffer buffer) throws IOException {
+ // Add two to the header size as the first two bytes are not included
+ final int headerSize = Byte.toUnsignedInt(buffer.get(HEADER_LEVEL_0_OFFSET_HEADER_SIZE)) + 2;
+ if (headerSize < HEADER_GENERIC_MINIMUM_HEADER_LENGTH) {
+ throw new ArchiveException("Invalid header level 0 length: %d", headerSize);
+ }
+
+ buffer = readRemainingHeaderData(buffer, headerSize);
+
+ final int headerChecksum = Byte.toUnsignedInt(buffer.get(HEADER_LEVEL_0_OFFSET_HEADER_CHECKSUM));
+
+ final String compressionMethod = getCompressionMethod(buffer);
+
+ final LhaArchiveEntry.Builder entryBuilder = new LhaArchiveEntry.Builder()
+ .setCompressionMethod(compressionMethod)
+ .setCompressedSize(Integer.toUnsignedLong(buffer.getInt(HEADER_LEVEL_0_OFFSET_COMPRESSED_SIZE)))
+ .setSize(Integer.toUnsignedLong(buffer.getInt(HEADER_LEVEL_0_OFFSET_ORIGINAL_SIZE)))
+ .setLastModifiedDate(new Date(ZipUtil.dosToJavaTime(Integer.toUnsignedLong(buffer.getInt(HEADER_LEVEL_0_OFFSET_LAST_MODIFIED_DATE_TIME)))));
+
+ final int filenameLength = Byte.toUnsignedInt(buffer.get(HEADER_LEVEL_0_OFFSET_FILENAME_LENGTH));
+
+ // Make sure the filename is not overflowing into the CRC field
+ if (filenameLength > (headerSize - HEADER_LEVEL_0_OFFSET_FILENAME - 2)) {
+ throw new ArchiveException("Invalid pathname length");
+ }
+
+ buffer.position(HEADER_LEVEL_0_OFFSET_FILENAME);
+ entryBuilder.setFilename(getPathname(buffer, filenameLength))
+ .setDirectory(isDirectory(compressionMethod))
+ .setCrcValue(Short.toUnsignedInt(buffer.getShort()));
+
+ if (calculateHeaderChecksum(buffer) != headerChecksum) {
+ throw new ArchiveException("Invalid header level 0 checksum");
+ }
+
+ final LhaArchiveEntry entry = entryBuilder.get();
+
+ prepareDecompression(entry);
+
+ return entry;
+ }
+
+ /**
+ * Read LHA header level 1.
+ *
+ * @param buffer the buffer containing the header data
+ * @return the LhaArchiveEntry read from the buffer
+ * @throws IOException
+ */
+ LhaArchiveEntry readHeaderLevel1(ByteBuffer buffer) throws IOException {
+ // Add two to the header size as the first two bytes are not included
+ final int baseHeaderSize = Byte.toUnsignedInt(buffer.get(HEADER_LEVEL_1_OFFSET_BASE_HEADER_SIZE)) + 2;
+ if (baseHeaderSize < HEADER_GENERIC_MINIMUM_HEADER_LENGTH) {
+ throw new ArchiveException("Invalid header level 1 length: %d", baseHeaderSize);
+ }
+
+ buffer = readRemainingHeaderData(buffer, baseHeaderSize);
+
+ final int baseHeaderChecksum = Byte.toUnsignedInt(buffer.get(HEADER_LEVEL_1_OFFSET_BASE_HEADER_CHECKSUM));
+
+ final String compressionMethod = getCompressionMethod(buffer);
+ long skipSize = Integer.toUnsignedLong(buffer.getInt(HEADER_LEVEL_1_OFFSET_SKIP_SIZE));
+
+ final LhaArchiveEntry.Builder entryBuilder = new LhaArchiveEntry.Builder()
+ .setCompressionMethod(compressionMethod)
+ .setSize(Integer.toUnsignedLong(buffer.getInt(HEADER_LEVEL_1_OFFSET_ORIGINAL_SIZE)))
+ .setLastModifiedDate(new Date(ZipUtil.dosToJavaTime(Integer.toUnsignedLong(buffer.getInt(HEADER_LEVEL_1_OFFSET_LAST_MODIFIED_DATE_TIME)))));
+
+ final int filenameLength = Byte.toUnsignedInt(buffer.get(HEADER_LEVEL_1_OFFSET_FILENAME_LENGTH));
+
+ // Make sure the filename is not overflowing into the CRC, OS ID and first extended header length fields.
+ // This check is not bulletproof because there might also be an extended area after the filename that
+ // we cannot detect for corrupt archives.
+ if (filenameLength > (baseHeaderSize - HEADER_LEVEL_1_OFFSET_FILENAME - 5)) {
+ throw new ArchiveException("Invalid pathname length");
+ }
+
+ buffer.position(HEADER_LEVEL_1_OFFSET_FILENAME);
+ entryBuilder.setFilename(getPathname(buffer, filenameLength))
+ .setDirectory(isDirectory(compressionMethod))
+ .setCrcValue(Short.toUnsignedInt(buffer.getShort()))
+ .setOsId(Byte.toUnsignedInt(buffer.get()));
+
+ if (calculateHeaderChecksum(buffer) != baseHeaderChecksum) {
+ throw new ArchiveException("Invalid header level 1 checksum");
+ }
+
+ // Create a list to store base header and all extended headers
+ // to be able to calculate the CRC of the full header
+ final List<ByteBuffer> headerParts = new ArrayList<>();
+ headerParts.add(buffer);
+
+ buffer.position(baseHeaderSize - 2); // First extended header length is at the end of the base header
+ int extendedHeaderSize = Short.toUnsignedInt(buffer.getShort());
+ while (extendedHeaderSize > 0) {
+ final ByteBuffer extendedHeaderBuffer = readExtendedHeader(extendedHeaderSize);
+ skipSize -= extendedHeaderSize;
+
+ parseExtendedHeader(extendedHeaderBuffer, entryBuilder);
+
+ headerParts.add(extendedHeaderBuffer);
+
+ extendedHeaderSize = Short.toUnsignedInt(extendedHeaderBuffer.getShort(extendedHeaderBuffer.limit() - 2));
+ }
+
+ // The compressed size is derived by subtracting the extended header sizes from the skip size. A
+ // corrupt archive whose extended headers exceed the skip size would yield a negative compressed
+ // size, which must be rejected as it would otherwise disable the per-entry read bound.
+ if (skipSize < 0) {
+ throw new ArchiveException("Invalid compressed size");
}
+
+ entryBuilder.setCompressedSize(skipSize);
+
+ final LhaArchiveEntry entry = entryBuilder.get();
+
+ if (entry.getHeaderCrc() != null) {
+ // Calculate CRC16 of full header
+ final long headerCrc = calculateCRC16(headerParts.toArray(new ByteBuffer[headerParts.size()]));
+ if (headerCrc != entry.getHeaderCrc()) {
+ throw new ArchiveException("Invalid header CRC expected=0x%04x found=0x%04x", headerCrc, entry.getHeaderCrc());
+ }
+ }
+
+ prepareDecompression(entry);
+
+ return entry;
+ }
+
+ /**
+ * Read LHA header level 2.
+ *
+ * @param buffer the buffer containing the header data
+ * @return the LhaArchiveEntry read from the buffer
+ * @throws IOException
+ */
+ LhaArchiveEntry readHeaderLevel2(ByteBuffer buffer) throws IOException {
+ final int headerSize = Short.toUnsignedInt(buffer.getShort(HEADER_LEVEL_2_OFFSET_HEADER_SIZE));
+ if (headerSize < HEADER_LEVEL_2_MINIMUM_HEADER_LENGTH) {
+ throw new ArchiveException("Invalid header level 2 length: %d", headerSize);
+ }
+
+ buffer = readRemainingHeaderData(buffer, headerSize);
+
+ final String compressionMethod = getCompressionMethod(buffer);
+
+ final LhaArchiveEntry.Builder entryBuilder = new LhaArchiveEntry.Builder()
+ .setCompressionMethod(compressionMethod)
+ .setCompressedSize(Integer.toUnsignedLong(buffer.getInt(HEADER_LEVEL_2_OFFSET_COMPRESSED_SIZE)))
+ .setSize(Integer.toUnsignedLong(buffer.getInt(HEADER_LEVEL_2_OFFSET_ORIGINAL_SIZE)))
+ .setLastModifiedDate(new Date(Integer.toUnsignedLong(buffer.getInt(HEADER_LEVEL_2_OFFSET_LAST_MODIFIED_DATE_TIME)) * 1000))
+ .setDirectory(isDirectory(compressionMethod))
+ .setCrcValue(Short.toUnsignedInt(buffer.getShort(HEADER_LEVEL_2_OFFSET_CRC)))
+ .setOsId(Byte.toUnsignedInt(buffer.get(HEADER_LEVEL_2_OFFSET_OS_ID)));
+
+ int extendedHeaderSize = Short.toUnsignedInt(buffer.getShort(HEADER_LEVEL_2_OFFSET_FIRST_EXTENDED_HEADER_SIZE));
+ int extendedHeaderOffset = HEADER_LEVEL_2_OFFSET_FIRST_EXTENDED_HEADER_SIZE + 2;
+ while (extendedHeaderSize > 0) {
+ if ((extendedHeaderOffset + extendedHeaderSize) > buffer.limit()) {
+ throw new ArchiveException("Invalid extended header length");
+ }
+
+ // Create new ByteBuffer as a slice from the full header. Set limit to the extended header length.
+ final ByteBuffer extendedHeaderBuffer = byteBufferSlice(buffer, extendedHeaderOffset, extendedHeaderSize).order(ByteOrder.LITTLE_ENDIAN);
+
+ extendedHeaderOffset += extendedHeaderSize;
+
+ parseExtendedHeader(extendedHeaderBuffer, entryBuilder);
+
+ extendedHeaderSize = Short.toUnsignedInt(extendedHeaderBuffer.getShort(extendedHeaderBuffer.limit() - 2));
+ }
+
+ final LhaArchiveEntry entry = entryBuilder.get();
+
+ if (entry.getHeaderCrc() != null) {
+ // Calculate CRC16 of full header
+ final long headerCrc = calculateCRC16(buffer);
+ if (headerCrc != entry.getHeaderCrc()) {
+ throw new ArchiveException("Invalid header CRC expected=0x%04x found=0x%04x", headerCrc, entry.getHeaderCrc());
+ }
+ }
+
+ prepareDecompression(entry);
+
+ return entry;
+ }
+
+ /**
+ * Read the remaining part of the header and append it to the already loaded parts.
+ *
+ * @param currentHeader all header parts that have already been loaded into memory
+ * @param headerSize total header size
+ * @return header the complete header as a ByteBuffer
+ * @throws IOException
+ */
+ private ByteBuffer readRemainingHeaderData(final ByteBuffer currentHeader, final int headerSize) throws IOException {
+ final byte[] remainingData = new byte[headerSize - currentHeader.capacity()];
+ final int len = IOUtils.read(in, remainingData);
+ if (len != remainingData.length) {
+ throw new ArchiveException("Error reading remaining header");
+ }
+
+ return ByteBuffer.allocate(currentHeader.capacity() + len).put(currentHeader.array()).put(remainingData).order(ByteOrder.LITTLE_ENDIAN);
}
}
diff --git a/src/test/java/org/apache/commons/compress/archivers/lha/LhaArchiveEntryTest.java b/src/test/java/org/apache/commons/compress/archivers/lha/LhaArchiveEntryTest.java
index 4242b0cc6..025c063d6 100644
--- a/src/test/java/org/apache/commons/compress/archivers/lha/LhaArchiveEntryTest.java
+++ b/src/test/java/org/apache/commons/compress/archivers/lha/LhaArchiveEntryTest.java
@@ -27,43 +27,43 @@
class LhaArchiveEntryTest {
@Test
- void testToStringMinimal() {
+ void testToStringAllFields() {
final LhaArchiveEntry entry = LhaArchiveEntry.builder()
.setFilename("test1.txt")
+ .setDirectoryName("dir1/")
.setDirectory(false)
.setSize(57)
.setLastModifiedDate(new Date(1754236942000L)) // 2025-08-03T16:02:22Z
.setCompressedSize(52)
.setCompressionMethod("-lh5-")
.setCrcValue(0x6496)
+ .setOsId(85)
+ .setUnixPermissionMode(0100644)
+ .setUnixGroupId(20)
+ .setUnixUserId(501)
+ .setMsdosFileAttributes(0x0010)
+ .setHeaderCrc(0xb772)
.get();
- assertEquals("LhaArchiveEntry[name=test1.txt,directory=false,size=57,lastModifiedDate=2025-08-03T16:02:22Z,compressedSize=52," +
- "compressionMethod=-lh5-,crcValue=0x6496]", entry.toString());
+ assertEquals(
+ "LhaArchiveEntry[name=dir1/test1.txt,directory=false,size=57,lastModifiedDate=2025-08-03T16:02:22Z,compressedSize=52," +
+ "compressionMethod=-lh5-,crcValue=0x6496,osId=85,unixPermissionMode=100644,msdosFileAttributes=0010,headerCrc=0xb772]",
+ entry.toString());
}
@Test
- void testToStringAllFields() {
+ void testToStringMinimal() {
final LhaArchiveEntry entry = LhaArchiveEntry.builder()
.setFilename("test1.txt")
- .setDirectoryName("dir1/")
.setDirectory(false)
.setSize(57)
.setLastModifiedDate(new Date(1754236942000L)) // 2025-08-03T16:02:22Z
.setCompressedSize(52)
.setCompressionMethod("-lh5-")
.setCrcValue(0x6496)
- .setOsId(85)
- .setUnixPermissionMode(0100644)
- .setUnixGroupId(20)
- .setUnixUserId(501)
- .setMsdosFileAttributes(0x0010)
- .setHeaderCrc(0xb772)
.get();
- assertEquals(
- "LhaArchiveEntry[name=dir1/test1.txt,directory=false,size=57,lastModifiedDate=2025-08-03T16:02:22Z,compressedSize=52," +
- "compressionMethod=-lh5-,crcValue=0x6496,osId=85,unixPermissionMode=100644,msdosFileAttributes=0010,headerCrc=0xb772]",
- entry.toString());
+ assertEquals("LhaArchiveEntry[name=test1.txt,directory=false,size=57,lastModifiedDate=2025-08-03T16:02:22Z,compressedSize=52," +
+ "compressionMethod=-lh5-,crcValue=0x6496]", entry.toString());
}
}
diff --git a/src/test/java/org/apache/commons/compress/archivers/lha/LhaArchiveInputStreamTest.java b/src/test/java/org/apache/commons/compress/archivers/lha/LhaArchiveInputStreamTest.java
index 159b33e00..2880ba2f4 100644
--- a/src/test/java/org/apache/commons/compress/archivers/lha/LhaArchiveInputStreamTest.java
+++ b/src/test/java/org/apache/commons/compress/archivers/lha/LhaArchiveInputStreamTest.java
@@ -102,430 +102,633 @@ class LhaArchiveInputStreamTest extends AbstractTest {
0xbb, 0xc1, 0x7c, 0x1d, 0x9a, 0x63, 0xaf, 0xc3, 0xe4, 0xaf, 0x7c, 0x00 // |..|..c....|.|
};
- @Test
- void testInvalidHeaderLevelLength() throws IOException {
- final byte[] data = new byte[] { 0x04, 0x00, 0x00, 0x00, 0x00, 0x00 };
-
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
- archive.getNextEntry();
- fail("Expected ArchiveException for invalid header length");
- } catch (ArchiveException e) {
- assertEquals("Invalid header length", e.getMessage());
+ private static byte[] toByteArray(final int... data) {
+ final byte[] bytes = new byte[data.length];
+ for (int i = 0; i < data.length; i++) {
+ bytes[i] = (byte) data[i];
}
+ return bytes;
}
- @Test
- void testInvalidHeaderLevel() throws IOException {
- final byte[] data = toByteArray(VALID_HEADER_LEVEL_0_FILE);
-
- data[20] = 4; // Change the header level to an invalid value
-
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
- archive.getNextEntry();
- fail("Expected ArchiveException for invalid header level");
- } catch (ArchiveException e) {
- assertEquals("Invalid header level: 4", e.getMessage());
- }
+ private static ByteBuffer toByteBuffer(final int... data) {
+ return ByteBuffer.wrap(toByteArray(data)).order(ByteOrder.LITTLE_ENDIAN);
}
- @Test
- void testUnsupportedCompressionMethod() throws IOException {
- final byte[] data = toByteArray(VALID_HEADER_LEVEL_0_FILE);
+ /**
+ * The timestamp used in header level 0 and 1 entries has no time zone information and is
+ * converted in the system default time zone. This method converts the date to UTC to verify
+ * the timestamp in unit tests.
+ *
+ * @param date the date to convert
+ * @return a ZonedDateTime in UTC
+ */
+ private ZonedDateTime convertSystemTimeZoneDateToUTC(final Date date) {
+ return date.toInstant().atZone(ZoneId.systemDefault()).withZoneSameLocal(ZoneOffset.UTC);
+ }
- data[1] = (byte) 0x9c; // Change the header checksum
- data[5] = 'a'; // Change the compression method to an unsupported value
+ private String getPathname(final LhaArchiveInputStream is, final int... filepathBuffer) throws ArchiveException, UnsupportedEncodingException {
+ return is.getPathname(ByteBuffer.wrap(toByteArray(filepathBuffer)), filepathBuffer.length);
+ }
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
- final LhaArchiveEntry entry = archive.getNextEntry();
- assertNotNull(entry);
- assertEquals("-lha-", entry.getCompressionMethod());
+ private InputStream newEmptyInputStream() {
+ return new ByteArrayInputStream(new byte[0]);
+ }
- assertFalse(archive.canReadEntryData(entry));
+ @Test
+ void testDecompressLh0() throws Exception {
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
+ .setInputStream(newInputStream("test-macos-l0.lha"))
+ .get()) {
- try {
- IOUtils.toByteArray(archive);
- fail("Expected ArchiveException for unsupported compression method");
- } catch (ArchiveException e) {
- assertEquals("Unsupported compression method: -lha-", e.getMessage());
- }
+ final List<String> files = new ArrayList<>();
+ files.add("dir1" + File.separatorChar);
+ files.add("dir1" + File.separatorChar + "dir1-1" + File.separatorChar);
+ files.add("dir1" + File.separatorChar + "dir1-1" + File.separatorChar + "test1.txt");
+ files.add("dir1" + File.separatorChar + "dir1-2" + File.separatorChar);
+ files.add("dir1" + File.separatorChar + "dir1-2" + File.separatorChar + "test2.txt");
+ checkArchiveContent(archive, files);
}
}
@Test
- void testReadDataBeforeEntry() throws IOException {
- final byte[] data = toByteArray(VALID_HEADER_LEVEL_0_FILE);
-
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
- try {
- IOUtils.toByteArray(archive);
- fail("Expected IllegalStateException for reading data before entry");
- } catch (IllegalStateException e) {
- assertEquals("No current entry", e.getMessage());
- }
+ void testDecompressLh4() throws Exception {
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newInputStream("test-amiga-l0-lh4.lha")).get()) {
+ final List<String> files = new ArrayList<>();
+ files.add("lorem-ipsum.txt");
+ checkArchiveContent(archive, files);
}
}
@Test
- void testParseHeaderLevel0File() throws IOException {
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
- .setInputStream(new ByteArrayInputStream(toByteArray(VALID_HEADER_LEVEL_0_FILE)))
- .get()) {
-
- // Entry should be parsed correctly
- final LhaArchiveEntry entry = archive.getNextEntry();
- assertNotNull(entry);
- assertEquals("test1.txt", entry.getName());
- assertFalse(entry.isDirectory());
- assertEquals(57, entry.getSize());
- assertEquals(1754236942000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
- assertEquals(ZonedDateTime.parse("2025-08-03T16:02:22Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
- assertEquals(52, entry.getCompressedSize());
- assertEquals("-lh5-", entry.getCompressionMethod());
- assertEquals(0x6496, entry.getCrcValue());
- assertNull(entry.getOsId());
- assertNull(entry.getUnixPermissionMode());
- assertNull(entry.getUnixGroupId());
- assertNull(entry.getUnixUserId());
- assertNull(entry.getMsdosFileAttributes());
- assertNull(entry.getHeaderCrc());
-
- // No more entries expected
- assertNull(archive.getNextEntry());
+ void testDecompressLh5() throws Exception {
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newInputStream("test-macos-l0-lh5.lha")).get()) {
+ final List<String> files = new ArrayList<>();
+ files.add("lorem-ipsum.txt");
+ checkArchiveContent(archive, files);
}
}
+ /**
+ * Test decompressing a file with lh5 compression that contains only one characters and thus is
+ * basically RLE encoded. The distance tree contains only one entry (root node).
+ */
@Test
- void testParseHeaderLevel0FileMacosUtf8() throws IOException {
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
- .setInputStream(new ByteArrayInputStream(toByteArray(VALID_HEADER_LEVEL_0_FILE_MACOS_UTF8)))
- .setCharset(StandardCharsets.UTF_8)
- .get()) {
-
- // Entry name should be parsed correctly
- final LhaArchiveEntry entry = archive.getNextEntry();
- assertNotNull(entry);
- assertEquals("test-\u00E5\u00E4\u00F6.txt", entry.getName());
-
- // No more entries expected
- assertNull(archive.getNextEntry());
+ void testDecompressLh5Rle() throws Exception {
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newInputStream("test-macos-l0-lh5-rle.lha")).get()) {
+ final List<String> files = new ArrayList<>();
+ files.add("rle.txt");
+ checkArchiveContent(archive, files);
}
}
@Test
- void testParseHeaderLevel0FileMsdosIso88591() throws IOException {
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
- .setInputStream(new ByteArrayInputStream(toByteArray(VALID_HEADER_LEVEL_0_FILE_MSDOS_ISO8859_1)))
- .setCharset(StandardCharsets.ISO_8859_1)
- .get()) {
+ void testDecompressLh6() throws Exception {
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newInputStream("test-macos-l0-lh6.lha")).get()) {
+ final List<String> files = new ArrayList<>();
+ files.add("lorem-ipsum.txt");
+ checkArchiveContent(archive, files);
+ }
+ }
- // Entry name should be parsed correctly
- final LhaArchiveEntry entry = archive.getNextEntry();
- assertNotNull(entry);
- assertEquals("test-\u00E5\u00E4\u00F6.txt", entry.getName());
+ @Test
+ void testDecompressLh7() throws Exception {
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newInputStream("test-macos-l0-lh7.lha")).get()) {
+ final List<String> files = new ArrayList<>();
+ files.add("lorem-ipsum.txt");
+ checkArchiveContent(archive, files);
+ }
+ }
- // No more entries expected
- assertNull(archive.getNextEntry());
+ @Test
+ void testDecompressLz4() throws Exception {
+ // This archive was created using LArc 3.33 on MS-DOS
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newInputStream("test-msdos-l0-lz4.lzs")).get()) {
+ final List<String> files = new ArrayList<>();
+ files.add("TEST1.TXT");
+ checkArchiveContent(archive, files);
}
}
@Test
- void testParseHeaderLevel0FileMsdosIso88591DefaultEncoding() throws IOException {
+ void testGetBytesReadReflectsDecompressedBytes() throws IOException {
try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
- .setInputStream(new ByteArrayInputStream(toByteArray(VALID_HEADER_LEVEL_0_FILE_MSDOS_ISO8859_1)))
+ .setInputStream(new ByteArrayInputStream(toByteArray(VALID_HEADER_LEVEL_0_FILE)))
.get()) {
- // First entry should be with replacement characters for unsupported characters
final LhaArchiveEntry entry = archive.getNextEntry();
assertNotNull(entry);
- assertEquals("test-\uFFFD\uFFFD\uFFFD.txt", entry.getName()); // Unicode replacement characters for unsupported characters
- // No more entries expected
- assertNull(archive.getNextEntry());
+ final byte[] content = IOUtils.toByteArray(archive);
+ assertEquals(entry.getSize(), content.length);
+ assertEquals(entry.getSize(), archive.getBytesRead());
}
}
@Test
- void testInvalidHeaderLevel0Length() throws IOException {
- final byte[] data = toByteArray(VALID_HEADER_LEVEL_0_FILE);
-
- data[0] = 0x10; // Change the first byte to an invalid length
+ void testGetCompressionMethod() throws IOException {
+ assertEquals("-lh0-", LhaArchiveInputStream.getCompressionMethod(ByteBuffer.wrap(toByteArray(0x00, 0x00, '-', 'l', 'h', '0', '-'))));
+ assertEquals("-lhd-", LhaArchiveInputStream.getCompressionMethod(ByteBuffer.wrap(toByteArray(0x00, 0x00, '-', 'l', 'h', 'd', '-'))));
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
- archive.getNextEntry();
- fail("Expected ArchiveException for invalid header length");
+ try {
+ LhaArchiveInputStream.getCompressionMethod(ByteBuffer.wrap(toByteArray(0x00, 0x00, '-', 'l', 'h', '0', 0xff)));
+ fail("Expected ArchiveException for invalid compression method");
} catch (ArchiveException e) {
- assertEquals("Invalid header level 0 length: 18", e.getMessage());
+ assertEquals("Invalid compression method: 0x2d 0x6c 0x68 0x30 0xff", e.getMessage());
}
}
@Test
- void testInvalidHeaderLevel0Checksum() throws IOException {
- final byte[] data = toByteArray(VALID_HEADER_LEVEL_0_FILE);
-
- data[1] = 0x55; // Change the second byte to an invalid header checksum
-
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
- archive.getNextEntry();
- fail("Expected ArchiveException for invalid header checksum");
- } catch (ArchiveException e) {
- assertEquals("Invalid header level 0 checksum", e.getMessage());
+ void testGetPathnameInvalidLength() throws IOException, UnsupportedEncodingException {
+ try (LhaArchiveInputStream is = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).get()) {
+ try {
+ final byte[] pathname = new byte[] { 'a', 'b', 'c' };
+ is.getPathname(ByteBuffer.wrap(pathname), pathname.length + 1);
+ fail("Expected ArchiveException for invalid pathname length");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid pathname length", e.getMessage());
+ }
}
}
@Test
- void testInvalidHeaderLevel0FilenameLength() throws IOException {
- final byte[] data = toByteArray(VALID_HEADER_LEVEL_0_FILE);
-
- data[21] = 22; // Change the length of the filename
-
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
- archive.getNextEntry();
- fail("Expected ArchiveException for invalid filename");
- } catch (ArchiveException e) {
- assertEquals("Invalid pathname length", e.getMessage());
+ void testGetPathnameNegativeLength() throws IOException, UnsupportedEncodingException {
+ try (LhaArchiveInputStream is = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).get()) {
+ try {
+ is.getPathname(ByteBuffer.wrap(new byte[0]), -1);
+ fail("Expected ArchiveException when pathname length is negative");
+ } catch (ArchiveException e) {
+ assertEquals("Pathname length is negative", e.getMessage());
+ }
}
}
@Test
- void testParseHeaderLevel0FileWithFoldersMacos() throws IOException {
- // The lha file was generated by LHa for UNIX version 1.14i-ac20211125 for Macos
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
- .setInputStream(newInputStream("test-macos-l0.lha"))
- .setFileSeparatorChar('/')
- .get()) {
-
- LhaArchiveEntry entry;
+ void testGetPathnameTooLong() throws IOException, UnsupportedEncodingException {
+ try (LhaArchiveInputStream is = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).get()) {
+ try {
+ final byte[] pathname = new byte[4097];
+ is.getPathname(ByteBuffer.wrap(pathname), pathname.length);
+ fail("Expected ArchiveException when pathname is longer than the maximum allowed");
+ } catch (ArchiveException e) {
+ assertEquals("Pathname is longer than the maximum allowed (4097 > 4096)", e.getMessage());
+ }
+ }
+ }
- // Check directory entry
- entry = archive.getNextEntry();
- assertNotNull(entry);
- assertEquals("dir1/", entry.getName());
- assertTrue(entry.isDirectory());
- assertEquals(0, entry.getSize());
- assertEquals(1755090690000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
- assertEquals(ZonedDateTime.parse("2025-08-13T13:11:30Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
- assertEquals(0, entry.getCompressedSize());
- assertEquals("-lhd-", entry.getCompressionMethod());
- assertEquals(0x0000, entry.getCrcValue());
- assertNull(entry.getOsId());
- assertNull(entry.getUnixPermissionMode());
- assertNull(entry.getUnixGroupId());
- assertNull(entry.getUnixUserId());
- assertNull(entry.getMsdosFileAttributes());
- assertNull(entry.getHeaderCrc());
+ @Test
+ void testGetPathnameUnixFileSeparatorCharDefaultEncoding() throws IOException, UnsupportedEncodingException {
+ try (LhaArchiveInputStream is = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).setFileSeparatorChar('/').get()) {
+ assertEquals("", getPathname(is));
+ assertEquals("", getPathname(is, 0xff));
+ assertEquals("a", getPathname(is, 'a'));
+ assertEquals("folder/", getPathname(is, 'f', 'o', 'l', 'd', 'e', 'r', 0xff));
+ assertEquals("folder/file.txt", getPathname(is, 'f', 'o', 'l', 'd', 'e', 'r', 0xff, 'f', 'i', 'l', 'e', '.', 't', 'x', 't'));
+ assertEquals("folder/file.txt", getPathname(is, 0xff, 'f', 'o', 'l', 'd', 'e', 'r', 0xff, 'f', 'i', 'l', 'e', '.', 't', 'x', 't'));
+ assertEquals("folder/file.txt", getPathname(is, '\\', 'f', 'o', 'l', 'd', 'e', 'r', '\\', 'f', 'i', 'l', 'e', '.', 't', 'x', 't'));
- // Check directory entry
- entry = archive.getNextEntry();
- assertNotNull(entry);
- assertEquals("dir1/dir1-1/", entry.getName());
- assertTrue(entry.isDirectory());
- assertEquals(0, entry.getSize());
- assertEquals(1755090728000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
- assertEquals(ZonedDateTime.parse("2025-08-13T13:12:08Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
- assertEquals(0, entry.getCompressedSize());
- assertEquals("-lhd-", entry.getCompressionMethod());
- assertEquals(0x0000, entry.getCrcValue());
- assertNull(entry.getOsId());
- assertNull(entry.getUnixPermissionMode());
- assertNull(entry.getUnixGroupId());
- assertNull(entry.getUnixUserId());
- assertNull(entry.getMsdosFileAttributes());
- assertNull(entry.getHeaderCrc());
+ // Unicode replacement characters for unsupported characters
+ assertEquals("\uFFFD/\uFFFD/\uFFFD.txt", getPathname(is, 0xe5, 0xff, 0xe4, 0xff, 0xf6, '.', 't', 'x', 't'));
+ assertEquals("\uFFFD/\uFFFD/\uFFFD.txt", getPathname(is, 0xe5, '\\', 0xe4, '\\', 0xf6, '.', 't', 'x', 't'));
+ }
+ }
- // Check file entry
- entry = archive.getNextEntry();
- assertNotNull(entry);
- assertEquals("dir1/dir1-1/test1.txt", entry.getName());
- assertFalse(entry.isDirectory());
- assertEquals(13, entry.getSize());
- assertEquals(1755090728000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
- assertEquals(ZonedDateTime.parse("2025-08-13T13:12:08Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
- assertEquals(13, entry.getCompressedSize());
- assertEquals("-lh0-", entry.getCompressionMethod());
- assertEquals(0x7757, entry.getCrcValue());
- assertNull(entry.getOsId());
- assertNull(entry.getUnixPermissionMode());
- assertNull(entry.getUnixGroupId());
- assertNull(entry.getUnixUserId());
- assertNull(entry.getMsdosFileAttributes());
- assertNull(entry.getHeaderCrc());
+ @Test
+ void testGetPathnameUnixFileSeparatorCharIso88591() throws IOException, UnsupportedEncodingException {
+ try (LhaArchiveInputStream is = LhaArchiveInputStream.builder()
+ .setInputStream(newEmptyInputStream())
+ .setCharset(StandardCharsets.ISO_8859_1)
+ .setFileSeparatorChar('/')
+ .get()) {
- // Check directory entry
- entry = archive.getNextEntry();
- assertNotNull(entry);
- assertEquals("dir1/dir1-2/", entry.getName());
- assertTrue(entry.isDirectory());
- assertEquals(0, entry.getSize());
- assertEquals(1755090812000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
- assertEquals(ZonedDateTime.parse("2025-08-13T13:13:32Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
- assertEquals(0, entry.getCompressedSize());
- assertEquals("-lhd-", entry.getCompressionMethod());
- assertEquals(0x0000, entry.getCrcValue());
- assertNull(entry.getOsId());
- assertNull(entry.getUnixPermissionMode());
- assertNull(entry.getUnixGroupId());
- assertNull(entry.getUnixUserId());
- assertNull(entry.getMsdosFileAttributes());
- assertNull(entry.getHeaderCrc());
+ assertEquals("\u00E5/\u00E4/\u00F6.txt", getPathname(is, 0xe5, 0xff, 0xe4, 0xff, 0xf6, '.', 't', 'x', 't'));
+ assertEquals("\u00E5/\u00E4/\u00F6.txt", getPathname(is, 0xe5, '\\', 0xe4, '\\', 0xf6, '.', 't', 'x', 't'));
+ }
+ }
- // Check file entry
- entry = archive.getNextEntry();
- assertNotNull(entry);
- assertEquals("dir1/dir1-2/test2.txt", entry.getName());
- assertFalse(entry.isDirectory());
- assertEquals(13, entry.getSize());
- assertEquals(1755090812000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
- assertEquals(ZonedDateTime.parse("2025-08-13T13:13:32Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
- assertEquals(13, entry.getCompressedSize());
- assertEquals("-lh0-", entry.getCompressionMethod());
- assertEquals(0x7757, entry.getCrcValue());
- assertNull(entry.getOsId());
- assertNull(entry.getUnixPermissionMode());
- assertNull(entry.getUnixGroupId());
- assertNull(entry.getUnixUserId());
- assertNull(entry.getMsdosFileAttributes());
- assertNull(entry.getHeaderCrc());
+ @Test
+ void testGetPathnameWindowsFileSeparatorCharDefaultEncoding() throws IOException, UnsupportedEncodingException {
+ try (LhaArchiveInputStream is = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).setFileSeparatorChar('\\').get()) {
+ assertEquals("folder\\", getPathname(is, 'f', 'o', 'l', 'd', 'e', 'r', 0xff));
+ assertEquals("folder\\file.txt", getPathname(is, 'f', 'o', 'l', 'd', 'e', 'r', 0xff, 'f', 'i', 'l', 'e', '.', 't', 'x', 't'));
+ assertEquals("folder\\file.txt", getPathname(is, 0xff, 'f', 'o', 'l', 'd', 'e', 'r', 0xff, 'f', 'i', 'l', 'e', '.', 't', 'x', 't'));
+ assertEquals("folder\\file.txt", getPathname(is, '\\', 'f', 'o', 'l', 'd', 'e', 'r', '\\', 'f', 'i', 'l', 'e', '.', 't', 'x', 't'));
- // No more entries expected
- assertNull(archive.getNextEntry());
+ // Unicode replacement characters for unsupported characters
+ assertEquals("\uFFFD\\\uFFFD\\\uFFFD.txt", getPathname(is, 0xe5, 0xff, 0xe4, 0xff, 0xf6, '.', 't', 'x', 't'));
+ assertEquals("\uFFFD\\\uFFFD\\\uFFFD.txt", getPathname(is, 0xe5, '\\', 0xe4, '\\', 0xf6, '.', 't', 'x', 't'));
}
}
@Test
- void testParseHeaderLevel0FileWithFoldersMsdos() throws IOException {
- // The lha file was generated by LHA32 v2.67.00 for Windows
+ void testGetPathnameWindowsFileSeparatorCharIso88591() throws IOException, UnsupportedEncodingException {
+ try (LhaArchiveInputStream is = LhaArchiveInputStream.builder()
+ .setInputStream(newEmptyInputStream())
+ .setCharset(StandardCharsets.ISO_8859_1)
+ .setFileSeparatorChar('\\')
+ .get()) {
+
+ assertEquals("\u00E5\\\u00E4\\\u00F6.txt", getPathname(is, 0xe5, 0xff, 0xe4, 0xff, 0xf6, '.', 't', 'x', 't'));
+ assertEquals("\u00E5\\\u00E4\\\u00F6.txt", getPathname(is, 0xe5, '\\', 0xe4, '\\', 0xf6, '.', 't', 'x', 't'));
+ }
+ }
+
+ @Test
+ void testInvalidHeaderLevel() throws IOException {
+ final byte[] data = toByteArray(VALID_HEADER_LEVEL_0_FILE);
+
+ data[20] = 4; // Change the header level to an invalid value
+
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
+ archive.getNextEntry();
+ fail("Expected ArchiveException for invalid header level");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid header level: 4", e.getMessage());
+ }
+ }
+
+ @Test
+ void testInvalidHeaderLevel0Checksum() throws IOException {
+ final byte[] data = toByteArray(VALID_HEADER_LEVEL_0_FILE);
+
+ data[1] = 0x55; // Change the second byte to an invalid header checksum
+
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
+ archive.getNextEntry();
+ fail("Expected ArchiveException for invalid header checksum");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid header level 0 checksum", e.getMessage());
+ }
+ }
+
+ @Test
+ void testInvalidHeaderLevel0FilenameLength() throws IOException {
+ final byte[] data = toByteArray(VALID_HEADER_LEVEL_0_FILE);
+
+ data[21] = 22; // Change the length of the filename
+
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
+ archive.getNextEntry();
+ fail("Expected ArchiveException for invalid filename");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid pathname length", e.getMessage());
+ }
+ }
+
+ @Test
+ void testInvalidHeaderLevel0Length() throws IOException {
+ final byte[] data = toByteArray(VALID_HEADER_LEVEL_0_FILE);
+
+ data[0] = 0x10; // Change the first byte to an invalid length
+
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
+ archive.getNextEntry();
+ fail("Expected ArchiveException for invalid header length");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid header level 0 length: 18", e.getMessage());
+ }
+ }
+
+ @Test
+ void testInvalidHeaderLevel1Checksum() throws IOException {
+ final byte[] data = toByteArray(VALID_HEADER_LEVEL_1_FILE);
+
+ data[1] = 0x55; // Change the second byte to an invalid header checksum
+
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
+ archive.getNextEntry();
+ fail("Expected ArchiveException for invalid header checksum");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid header level 1 checksum", e.getMessage());
+ }
+ }
+
+ @Test
+ void testInvalidHeaderLevel1Crc() throws IOException {
+ final byte[] data = toByteArray(VALID_HEADER_LEVEL_1_FILE_MSDOS_WITH_CHECKSUM_AND_CRC);
+
+ // Change header CRC to an invalid value
+ data[41] = 0x33;
+ data[42] = 0x22;
+
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
+ archive.getNextEntry();
+ fail("Expected ArchiveException for invalid header checksum");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid header CRC expected=0xb772 found=0x2233", e.getMessage());
+ }
+ }
+
+ @Test
+ void testInvalidHeaderLevel1FilenameLength() throws IOException {
+ final byte[] data = toByteArray(VALID_HEADER_LEVEL_1_FILE);
+
+ data[21] = 10; // Change the length of the filename
+
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
+ archive.getNextEntry();
+ fail("Expected ArchiveException for invalid filename");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid pathname length", e.getMessage());
+ }
+ }
+
+ @Test
+ void testInvalidHeaderLevel1Length() throws IOException {
+ final byte[] data = toByteArray(VALID_HEADER_LEVEL_1_FILE);
+
+ data[0] = 0x10; // Change the first byte to an invalid length
+
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
+ archive.getNextEntry();
+ fail("Expected ArchiveException for invalid header length");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid header level 1 length: 18", e.getMessage());
+ }
+ }
+
+ @Test
+ void testInvalidHeaderLevel1NegativeCompressedSize() throws IOException {
+ final byte[] data = toByteArray(VALID_HEADER_LEVEL_1_FILE);
+
+ // Zero out the 4-byte skip size field (offset 7) so that subtracting the extended header
+ // sizes underflows to a negative compressed size.
+ data[7] = 0x00;
+ data[8] = 0x00;
+ data[9] = 0x00;
+ data[10] = 0x00;
+
+ // Recompute the base header checksum so the header passes checksum validation and reaches
+ // the compressed size check.
+ final int baseHeaderSize = (data[0] & 0xff) + 2;
+ int sum = 0;
+ for (int i = 2; i < baseHeaderSize; i++) {
+ sum += data[i] & 0xff;
+ }
+ data[1] = (byte) (sum & 0xff);
+
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
+ archive.getNextEntry();
+ fail("Expected ArchiveException for negative compressed size");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid compressed size", e.getMessage());
+ }
+ }
+
+ @Test
+ void testInvalidHeaderLevel2Checksum() throws IOException {
+ final byte[] data = toByteArray(VALID_HEADER_LEVEL_2_FILE);
+
+ // Change header CRC to an invalid value
+ data[27] = 0x33;
+ data[28] = 0x22;
+
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
+ archive.getNextEntry();
+ fail("Expected ArchiveException for invalid header checksum");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid header CRC expected=0x01a5 found=0x2233", e.getMessage());
+ }
+ }
+
+ @Test
+ void testInvalidHeaderLevel2Length() throws IOException {
+ final byte[] data = toByteArray(VALID_HEADER_LEVEL_2_FILE);
+
+ data[0] = 25; // Change the first byte to an invalid length
+
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
+ archive.getNextEntry();
+ fail("Expected ArchiveException for invalid header length");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid header level 2 length: 25", e.getMessage());
+ }
+ }
+
+ @Test
+ void testInvalidHeaderLevelLength() throws IOException {
+ final byte[] data = new byte[] { 0x04, 0x00, 0x00, 0x00, 0x00, 0x00 };
+
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
+ archive.getNextEntry();
+ fail("Expected ArchiveException for invalid header length");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid header length", e.getMessage());
+ }
+ }
+
+ @Test
+ void testMatches() {
+ byte[] data;
+
+ assertTrue(LhaArchiveInputStream.matches(toByteArray(VALID_HEADER_LEVEL_0_FILE), VALID_HEADER_LEVEL_0_FILE.length));
+ assertTrue(LhaArchiveInputStream.matches(toByteArray(VALID_HEADER_LEVEL_1_FILE), VALID_HEADER_LEVEL_1_FILE.length));
+ assertTrue(LhaArchiveInputStream.matches(toByteArray(VALID_HEADER_LEVEL_2_FILE), VALID_HEADER_LEVEL_2_FILE.length));
+
+ // Header to short
+ data = toByteArray(0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09);
+ assertFalse(LhaArchiveInputStream.matches(data, data.length));
+
+ // Change the header level to an invalid value
+ data = toByteArray(VALID_HEADER_LEVEL_0_FILE);
+ data[20] = 3;
+ assertFalse(LhaArchiveInputStream.matches(data, data.length));
+
+ // Change the compression method to an invalid value
+ data = toByteArray(VALID_HEADER_LEVEL_0_FILE);
+ data[6] = 0x08;
+ assertFalse(LhaArchiveInputStream.matches(data, data.length));
+ }
+
+ @Test
+ void testParseExtendedHeaderCommon() throws IOException {
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).get()) {
+ // Valid
+ final LhaArchiveEntry.Builder entryBuilder = LhaArchiveEntry.builder();
+ archive.parseExtendedHeader(toByteBuffer(0x00, 0x22, 0x33, 0x00, 0x00), entryBuilder);
+ assertEquals(0x3322, entryBuilder.get().getHeaderCrc());
+
+ // Invalid length
+ try {
+ archive.parseExtendedHeader(toByteBuffer(0x00, 0x22, 0x00, 0x00), entryBuilder);
+ fail("Expected ArchiveException for invalid extended header length");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid extended header length", e.getMessage());
+ }
+ }
+ }
+
+ @Test
+ void testParseExtendedHeaderDirectoryName() throws IOException {
try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
- .setInputStream(newInputStream("test-msdos-l0.lha"))
+ .setInputStream(newEmptyInputStream())
.setFileSeparatorChar('/')
.get()) {
- LhaArchiveEntry entry;
- // Check directory entry
- entry = archive.getNextEntry();
- assertNotNull(entry);
- assertEquals("dir1/", entry.getName());
- assertTrue(entry.isDirectory());
- assertEquals(0, entry.getSize());
- assertEquals(1755081308000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
- assertEquals(ZonedDateTime.parse("2025-08-13T10:35:08Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
- assertEquals(0, entry.getCompressedSize());
- assertEquals("-lhd-", entry.getCompressionMethod());
- assertEquals(0x0000, entry.getCrcValue());
- assertNull(entry.getOsId());
- assertNull(entry.getUnixPermissionMode());
- assertNull(entry.getUnixGroupId());
- assertNull(entry.getUnixUserId());
- assertNull(entry.getMsdosFileAttributes());
- assertNull(entry.getHeaderCrc());
+ // Valid
+ final LhaArchiveEntry.Builder entryBuilder = LhaArchiveEntry.builder();
+ archive.parseExtendedHeader(toByteBuffer(0x02, 'd', 'i', 'r', '1', 0xff, 0x00, 0x00), entryBuilder);
+ assertEquals("dir1/", entryBuilder.get().getName());
- // Check directory entry
- entry = archive.getNextEntry();
- assertNotNull(entry);
- assertEquals("dir1/dir1-1/", entry.getName());
- assertTrue(entry.isDirectory());
- assertEquals(0, entry.getSize());
- assertEquals(1755081336000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
- assertEquals(ZonedDateTime.parse("2025-08-13T10:35:36Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
- assertEquals(0, entry.getCompressedSize());
- assertEquals("-lhd-", entry.getCompressionMethod());
- assertEquals(0x0000, entry.getCrcValue());
- assertNull(entry.getOsId());
- assertNull(entry.getUnixPermissionMode());
- assertNull(entry.getUnixGroupId());
- assertNull(entry.getUnixUserId());
- assertNull(entry.getMsdosFileAttributes());
- assertNull(entry.getHeaderCrc());
+ // Invalid length
+ try {
+ archive.parseExtendedHeader(toByteBuffer(0x02, 0x00), entryBuilder);
+ fail("Expected ArchiveException for invalid extended header length");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid extended header length", e.getMessage());
+ }
+ }
+ }
+
+ @Test
+ void testParseExtendedHeaderFilename() throws IOException {
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).get()) {
+ // Valid
+ final LhaArchiveEntry.Builder entryBuilder = LhaArchiveEntry.builder();
+ archive.parseExtendedHeader(toByteBuffer(0x01, 't', 'e', 's', 't', '.', 't', 'x', 't', 0x00, 0x00), entryBuilder);
+ assertEquals("test.txt", entryBuilder.get().getName());
+
+ // Invalid length
+ try {
+ archive.parseExtendedHeader(toByteBuffer(0x01, 0x00), entryBuilder);
+ fail("Expected ArchiveException for invalid extended header length");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid extended header length", e.getMessage());
+ }
+ }
+ }
+
+ @Test
+ void testParseExtendedHeaderFilenameAndDirectoryName() throws IOException {
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
+ .setInputStream(newEmptyInputStream())
+ .setFileSeparatorChar('/')
+ .get()) {
+
+ LhaArchiveEntry.Builder entryBuilder;
+
+ // Test filename and directory name order
+ entryBuilder = LhaArchiveEntry.builder();
+ archive.parseExtendedHeader(toByteBuffer(0x01, 't', 'e', 's', 't', '.', 't', 'x', 't', 0x00, 0x00), entryBuilder);
+ archive.parseExtendedHeader(toByteBuffer(0x02, 'd', 'i', 'r', '1', 0xff, 0x00, 0x00), entryBuilder);
+ assertEquals("dir1/test.txt", entryBuilder.get().getName());
+
+ // Test filename and directory name order, no trailing slash
+ entryBuilder = LhaArchiveEntry.builder();
+ archive.parseExtendedHeader(toByteBuffer(0x01, 't', 'e', 's', 't', '.', 't', 'x', 't', 0x00, 0x00), entryBuilder);
+ archive.parseExtendedHeader(toByteBuffer(0x02, 'd', 'i', 'r', '1', 0x00, 0x00), entryBuilder);
+ assertEquals("dir1/test.txt", entryBuilder.get().getName());
+
+ // Test directory name and filename order
+ entryBuilder = LhaArchiveEntry.builder();
+ archive.parseExtendedHeader(toByteBuffer(0x02, 'd', 'i', 'r', '1', 0xff, 0x00, 0x00), entryBuilder);
+ archive.parseExtendedHeader(toByteBuffer(0x01, 't', 'e', 's', 't', '.', 't', 'x', 't', 0x00, 0x00), entryBuilder);
+ assertEquals("dir1/test.txt", entryBuilder.get().getName());
+
+ // Test directory name and filename order, no trailing slash
+ entryBuilder = LhaArchiveEntry.builder();
+ archive.parseExtendedHeader(toByteBuffer(0x02, 'd', 'i', 'r', '1', 0x00, 0x00), entryBuilder);
+ archive.parseExtendedHeader(toByteBuffer(0x01, 't', 'e', 's', 't', '.', 't', 'x', 't', 0x00, 0x00), entryBuilder);
+ assertEquals("dir1/test.txt", entryBuilder.get().getName());
+
+ // Test empty directory name, no trailing slash
+ entryBuilder = LhaArchiveEntry.builder();
+ archive.parseExtendedHeader(toByteBuffer(0x02, 0x00, 0x00), entryBuilder);
+ archive.parseExtendedHeader(toByteBuffer(0x01, 't', 'e', 's', 't', '.', 't', 'x', 't', 0x00, 0x00), entryBuilder);
+ assertEquals("test.txt", entryBuilder.get().getName());
+ }
+ }
+
+ @Test
+ void testParseExtendedHeaderMSdosFileAttributes() throws IOException {
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).get()) {
+ // Valid
+ final LhaArchiveEntry.Builder entryBuilder = LhaArchiveEntry.builder();
+ archive.parseExtendedHeader(toByteBuffer(0x40, 0x10, 0x00, 0x00, 0x00), entryBuilder);
+ assertEquals(0x10, entryBuilder.get().getMsdosFileAttributes());
- // Check file entry
- entry = archive.getNextEntry();
- assertNotNull(entry);
- assertEquals("dir1/dir1-1/test1.txt", entry.getName());
- assertFalse(entry.isDirectory());
- assertEquals(14, entry.getSize());
- assertEquals(1755081276000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
- assertEquals(ZonedDateTime.parse("2025-08-13T10:34:36Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
- assertEquals(14, entry.getCompressedSize());
- assertEquals("-lh0-", entry.getCompressionMethod());
- assertEquals(0xc9b4, entry.getCrcValue());
- assertNull(entry.getOsId());
- assertNull(entry.getUnixPermissionMode());
- assertNull(entry.getUnixGroupId());
- assertNull(entry.getUnixUserId());
- assertNull(entry.getMsdosFileAttributes());
- assertNull(entry.getHeaderCrc());
+ // Invalid length
+ try {
+ archive.parseExtendedHeader(toByteBuffer(0x40, 0x10, 0x00, 0x00), entryBuilder);
+ fail("Expected ArchiveException for invalid extended header length");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid extended header length", e.getMessage());
+ }
+ }
+ }
- // Check directory entry
- entry = archive.getNextEntry();
- assertNotNull(entry);
- assertEquals("dir1/dir1-2/", entry.getName());
- assertTrue(entry.isDirectory());
- assertEquals(0, entry.getSize());
- assertEquals(1755081340000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
- assertEquals(ZonedDateTime.parse("2025-08-13T10:35:40Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
- assertEquals(0, entry.getCompressedSize());
- assertEquals("-lhd-", entry.getCompressionMethod());
- assertEquals(0x0000, entry.getCrcValue());
- assertNull(entry.getOsId());
- assertNull(entry.getUnixPermissionMode());
- assertNull(entry.getUnixGroupId());
- assertNull(entry.getUnixUserId());
- assertNull(entry.getMsdosFileAttributes());
- assertNull(entry.getHeaderCrc());
+ @Test
+ void testParseExtendedHeaderTooShort() throws IOException {
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).get()) {
+ final LhaArchiveEntry.Builder entryBuilder = LhaArchiveEntry.builder();
+ try {
+ archive.parseExtendedHeader(toByteBuffer(0x00, 0x00), entryBuilder);
+ fail("Expected ArchiveException for invalid extended header length");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid extended header length", e.getMessage());
+ }
+ }
+ }
- // Check file entry
- entry = archive.getNextEntry();
- assertNotNull(entry);
- assertEquals("dir1/dir1-2/test2.txt", entry.getName());
- assertFalse(entry.isDirectory());
- assertEquals(14, entry.getSize());
- assertEquals(1755081276000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
- assertEquals(ZonedDateTime.parse("2025-08-13T10:34:36Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
- assertEquals(14, entry.getCompressedSize());
- assertEquals("-lh0-", entry.getCompressionMethod());
- assertEquals(0xc9b4, entry.getCrcValue());
- assertNull(entry.getOsId());
- assertNull(entry.getUnixPermissionMode());
- assertNull(entry.getUnixGroupId());
- assertNull(entry.getUnixUserId());
- assertNull(entry.getMsdosFileAttributes());
- assertNull(entry.getHeaderCrc());
+ @Test
+ void testParseExtendedHeaderUnixPermission() throws IOException {
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).get()) {
+ // Valid
+ final LhaArchiveEntry.Builder entryBuilder = LhaArchiveEntry.builder();
+ archive.parseExtendedHeader(toByteBuffer(0x50, 0xa4, 0x81, 0x00, 0x00), entryBuilder);
+ assertEquals(0x81a4, entryBuilder.get().getUnixPermissionMode());
+ assertEquals(0100644, entryBuilder.get().getUnixPermissionMode());
- // No more entries expected
- assertNull(archive.getNextEntry());
+ // Invalid length
+ try {
+ archive.parseExtendedHeader(toByteBuffer(0x50, 0xa4, 0x00, 0x00), entryBuilder);
+ fail("Expected ArchiveException for invalid extended header length");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid extended header length", e.getMessage());
+ }
}
}
@Test
- void testParseHeaderLevel0Larc() throws IOException {
- // This archive was created using LArc 3.33 on MS-DOS
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newInputStream("test-msdos-l0-lz4.lzs")).get()) {
- // Check file entry
- final LhaArchiveEntry entry = archive.getNextEntry();
- assertNotNull(entry);
- assertEquals("TEST1.TXT", entry.getName());
- assertFalse(entry.isDirectory());
- assertEquals(14, entry.getSize());
- assertEquals(1757247072000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
- assertEquals(ZonedDateTime.parse("2025-09-07T12:11:12Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
- assertEquals(14, entry.getCompressedSize());
- assertEquals("-lz4-", entry.getCompressionMethod());
- assertEquals(0xc9b4, entry.getCrcValue());
- assertNull(entry.getOsId());
- assertNull(entry.getUnixPermissionMode());
- assertNull(entry.getUnixGroupId());
- assertNull(entry.getUnixUserId());
- assertNull(entry.getMsdosFileAttributes());
- assertNull(entry.getHeaderCrc());
+ void testParseExtendedHeaderUnixTimestamp() throws IOException {
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).get()) {
+ // Valid
+ final LhaArchiveEntry.Builder entryBuilder = LhaArchiveEntry.builder();
+ archive.parseExtendedHeader(toByteBuffer(0x54, 0x5c, 0x73, 0x9c, 0x68, 0x00, 0x00), entryBuilder);
+ assertEquals(0x689c735cL, entryBuilder.get().getLastModifiedDate().getTime() / 1000);
- // No more entries expected
- assertNull(archive.getNextEntry());
+ // Invalid length
+ try {
+ archive.parseExtendedHeader(toByteBuffer(0x54, 0x5c, 0x73, 0x9c, 0x00, 0x00), entryBuilder);
+ fail("Expected ArchiveException for invalid extended header length");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid extended header length", e.getMessage());
+ }
}
}
@Test
- void testParseHeaderLevel1File() throws IOException {
+ void testParseExtendedHeaderUnixUidGid() throws IOException {
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).get()) {
+ // Valid
+ final LhaArchiveEntry.Builder entryBuilder = LhaArchiveEntry.builder();
+ archive.parseExtendedHeader(toByteBuffer(0x51, 0x14, 0x00, 0xf5, 0x01, 0x00, 0x00), entryBuilder);
+ assertEquals(0x0014, entryBuilder.get().getUnixGroupId());
+ assertEquals(0x01f5, entryBuilder.get().getUnixUserId());
+
+ // Invalid length
+ try {
+ archive.parseExtendedHeader(toByteBuffer(0x51, 0x14, 0x00, 0xf5, 0x00, 0x00), entryBuilder);
+ fail("Expected ArchiveException for invalid extended header length");
+ } catch (ArchiveException e) {
+ assertEquals("Invalid extended header length", e.getMessage());
+ }
+ }
+ }
+
+ @Test
+ void testParseHeaderLevel0File() throws IOException {
try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
- .setInputStream(new ByteArrayInputStream(toByteArray(VALID_HEADER_LEVEL_1_FILE)))
+ .setInputStream(new ByteArrayInputStream(toByteArray(VALID_HEADER_LEVEL_0_FILE)))
.get()) {
// Entry should be parsed correctly
@@ -534,15 +737,15 @@ void testParseHeaderLevel1File() throws IOException {
assertEquals("test1.txt", entry.getName());
assertFalse(entry.isDirectory());
assertEquals(57, entry.getSize());
- assertEquals(1754229743000L, entry.getLastModifiedDate().getTime());
- assertEquals(ZonedDateTime.parse("2025-08-03T14:02:23Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(1754236942000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
+ assertEquals(ZonedDateTime.parse("2025-08-03T16:02:22Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
assertEquals(52, entry.getCompressedSize());
assertEquals("-lh5-", entry.getCompressionMethod());
assertEquals(0x6496, entry.getCrcValue());
- assertEquals(85, entry.getOsId());
- assertEquals(0100644, entry.getUnixPermissionMode());
- assertEquals(20, entry.getUnixGroupId());
- assertEquals(501, entry.getUnixUserId());
+ assertNull(entry.getOsId());
+ assertNull(entry.getUnixPermissionMode());
+ assertNull(entry.getUnixGroupId());
+ assertNull(entry.getUnixUserId());
assertNull(entry.getMsdosFileAttributes());
assertNull(entry.getHeaderCrc());
@@ -552,147 +755,60 @@ void testParseHeaderLevel1File() throws IOException {
}
@Test
- void testParseHeaderLevel1FileMsdosChecksumAndCrc() throws IOException {
- // The lha file was generated by LHA32 v2.67.00 for Windows
+ void testParseHeaderLevel0FileMacosUtf8() throws IOException {
try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
- .setInputStream(new ByteArrayInputStream(toByteArray(VALID_HEADER_LEVEL_1_FILE_MSDOS_WITH_CHECKSUM_AND_CRC)))
- .setFileSeparatorChar('/')
+ .setInputStream(new ByteArrayInputStream(toByteArray(VALID_HEADER_LEVEL_0_FILE_MACOS_UTF8)))
+ .setCharset(StandardCharsets.UTF_8)
.get()) {
- LhaArchiveEntry entry;
-
- // Check directory entry
- entry = archive.getNextEntry();
- assertNotNull(entry);
- assertEquals("dir1/", entry.getName());
- assertTrue(entry.isDirectory());
- assertEquals(0, entry.getSize());
- assertEquals(1755097078000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
- assertEquals(ZonedDateTime.parse("2025-08-13T14:57:58Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
- assertEquals(0, entry.getCompressedSize());
- assertEquals("-lhd-", entry.getCompressionMethod());
- assertEquals(0x0000, entry.getCrcValue());
- assertEquals(77, entry.getOsId());
- assertNull(entry.getUnixPermissionMode());
- assertNull(entry.getUnixGroupId());
- assertNull(entry.getUnixUserId());
- assertEquals(0x0010, entry.getMsdosFileAttributes());
- assertEquals(0xb772, entry.getHeaderCrc());
-
- // Check file entry
- entry = archive.getNextEntry();
+ // Entry name should be parsed correctly
+ final LhaArchiveEntry entry = archive.getNextEntry();
assertNotNull(entry);
- assertEquals("dir1/test1.txt", entry.getName());
- assertFalse(entry.isDirectory());
- assertEquals(14, entry.getSize());
- assertEquals(1755081276000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
- assertEquals(ZonedDateTime.parse("2025-08-13T10:34:36Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
- assertEquals(14, entry.getCompressedSize());
- assertEquals("-lh0-", entry.getCompressionMethod());
- assertEquals(0xc9b4, entry.getCrcValue());
- assertEquals(77, entry.getOsId());
- assertNull(entry.getUnixPermissionMode());
- assertNull(entry.getUnixGroupId());
- assertNull(entry.getUnixUserId());
- assertNull(entry.getMsdosFileAttributes());
- assertEquals(0x9b71, entry.getHeaderCrc());
-
- // No more entries expected
- assertNull(archive.getNextEntry());
- }
- }
-
- @Test
- void testInvalidHeaderLevel1Length() throws IOException {
- final byte[] data = toByteArray(VALID_HEADER_LEVEL_1_FILE);
-
- data[0] = 0x10; // Change the first byte to an invalid length
-
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
- archive.getNextEntry();
- fail("Expected ArchiveException for invalid header length");
- } catch (ArchiveException e) {
- assertEquals("Invalid header level 1 length: 18", e.getMessage());
- }
- }
-
- @Test
- void testInvalidHeaderLevel1Checksum() throws IOException {
- final byte[] data = toByteArray(VALID_HEADER_LEVEL_1_FILE);
-
- data[1] = 0x55; // Change the second byte to an invalid header checksum
-
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
- archive.getNextEntry();
- fail("Expected ArchiveException for invalid header checksum");
- } catch (ArchiveException e) {
- assertEquals("Invalid header level 1 checksum", e.getMessage());
- }
- }
-
- @Test
- void testInvalidHeaderLevel1Crc() throws IOException {
- final byte[] data = toByteArray(VALID_HEADER_LEVEL_1_FILE_MSDOS_WITH_CHECKSUM_AND_CRC);
-
- // Change header CRC to an invalid value
- data[41] = 0x33;
- data[42] = 0x22;
-
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
- archive.getNextEntry();
- fail("Expected ArchiveException for invalid header checksum");
- } catch (ArchiveException e) {
- assertEquals("Invalid header CRC expected=0xb772 found=0x2233", e.getMessage());
+ assertEquals("test-\u00E5\u00E4\u00F6.txt", entry.getName());
+
+ // No more entries expected
+ assertNull(archive.getNextEntry());
}
}
@Test
- void testInvalidHeaderLevel1FilenameLength() throws IOException {
- final byte[] data = toByteArray(VALID_HEADER_LEVEL_1_FILE);
+ void testParseHeaderLevel0FileMsdosIso88591() throws IOException {
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
+ .setInputStream(new ByteArrayInputStream(toByteArray(VALID_HEADER_LEVEL_0_FILE_MSDOS_ISO8859_1)))
+ .setCharset(StandardCharsets.ISO_8859_1)
+ .get()) {
- data[21] = 10; // Change the length of the filename
+ // Entry name should be parsed correctly
+ final LhaArchiveEntry entry = archive.getNextEntry();
+ assertNotNull(entry);
+ assertEquals("test-\u00E5\u00E4\u00F6.txt", entry.getName());
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
- archive.getNextEntry();
- fail("Expected ArchiveException for invalid filename");
- } catch (ArchiveException e) {
- assertEquals("Invalid pathname length", e.getMessage());
+ // No more entries expected
+ assertNull(archive.getNextEntry());
}
}
@Test
- void testInvalidHeaderLevel1NegativeCompressedSize() throws IOException {
- final byte[] data = toByteArray(VALID_HEADER_LEVEL_1_FILE);
-
- // Zero out the 4-byte skip size field (offset 7) so that subtracting the extended header
- // sizes underflows to a negative compressed size.
- data[7] = 0x00;
- data[8] = 0x00;
- data[9] = 0x00;
- data[10] = 0x00;
+ void testParseHeaderLevel0FileMsdosIso88591DefaultEncoding() throws IOException {
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
+ .setInputStream(new ByteArrayInputStream(toByteArray(VALID_HEADER_LEVEL_0_FILE_MSDOS_ISO8859_1)))
+ .get()) {
- // Recompute the base header checksum so the header passes checksum validation and reaches
- // the compressed size check.
- final int baseHeaderSize = (data[0] & 0xff) + 2;
- int sum = 0;
- for (int i = 2; i < baseHeaderSize; i++) {
- sum += data[i] & 0xff;
- }
- data[1] = (byte) (sum & 0xff);
+ // First entry should be with replacement characters for unsupported characters
+ final LhaArchiveEntry entry = archive.getNextEntry();
+ assertNotNull(entry);
+ assertEquals("test-\uFFFD\uFFFD\uFFFD.txt", entry.getName()); // Unicode replacement characters for unsupported characters
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
- archive.getNextEntry();
- fail("Expected ArchiveException for negative compressed size");
- } catch (ArchiveException e) {
- assertEquals("Invalid compressed size", e.getMessage());
+ // No more entries expected
+ assertNull(archive.getNextEntry());
}
}
@Test
- void testParseHeaderLevel1FileWithFoldersMacos() throws IOException {
+ void testParseHeaderLevel0FileWithFoldersMacos() throws IOException {
// The lha file was generated by LHa for UNIX version 1.14i-ac20211125 for Macos
try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
- .setInputStream(newInputStream("test-macos-l1.lha"))
+ .setInputStream(newInputStream("test-macos-l0.lha"))
.setFileSeparatorChar('/')
.get()) {
@@ -704,15 +820,15 @@ void testParseHeaderLevel1FileWithFoldersMacos() throws IOException {
assertEquals("dir1/", entry.getName());
assertTrue(entry.isDirectory());
assertEquals(0, entry.getSize());
- assertEquals(1755083490000L, entry.getLastModifiedDate().getTime());
- assertEquals(ZonedDateTime.parse("2025-08-13T11:11:30Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(1755090690000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
+ assertEquals(ZonedDateTime.parse("2025-08-13T13:11:30Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
assertEquals(0, entry.getCompressedSize());
assertEquals("-lhd-", entry.getCompressionMethod());
assertEquals(0x0000, entry.getCrcValue());
- assertEquals(85, entry.getOsId());
- assertEquals(040755, entry.getUnixPermissionMode());
- assertEquals(20, entry.getUnixGroupId());
- assertEquals(501, entry.getUnixUserId());
+ assertNull(entry.getOsId());
+ assertNull(entry.getUnixPermissionMode());
+ assertNull(entry.getUnixGroupId());
+ assertNull(entry.getUnixUserId());
assertNull(entry.getMsdosFileAttributes());
assertNull(entry.getHeaderCrc());
@@ -722,15 +838,15 @@ void testParseHeaderLevel1FileWithFoldersMacos() throws IOException {
assertEquals("dir1/dir1-1/", entry.getName());
assertTrue(entry.isDirectory());
assertEquals(0, entry.getSize());
- assertEquals(1755083529000L, entry.getLastModifiedDate().getTime());
- assertEquals(ZonedDateTime.parse("2025-08-13T11:12:09Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(1755090728000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
+ assertEquals(ZonedDateTime.parse("2025-08-13T13:12:08Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
assertEquals(0, entry.getCompressedSize());
assertEquals("-lhd-", entry.getCompressionMethod());
assertEquals(0x0000, entry.getCrcValue());
- assertEquals(85, entry.getOsId());
- assertEquals(040755, entry.getUnixPermissionMode());
- assertEquals(20, entry.getUnixGroupId());
- assertEquals(501, entry.getUnixUserId());
+ assertNull(entry.getOsId());
+ assertNull(entry.getUnixPermissionMode());
+ assertNull(entry.getUnixGroupId());
+ assertNull(entry.getUnixUserId());
assertNull(entry.getMsdosFileAttributes());
assertNull(entry.getHeaderCrc());
@@ -740,15 +856,15 @@ void testParseHeaderLevel1FileWithFoldersMacos() throws IOException {
assertEquals("dir1/dir1-1/test1.txt", entry.getName());
assertFalse(entry.isDirectory());
assertEquals(13, entry.getSize());
- assertEquals(1755083529000L, entry.getLastModifiedDate().getTime());
- assertEquals(ZonedDateTime.parse("2025-08-13T11:12:09Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(1755090728000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
+ assertEquals(ZonedDateTime.parse("2025-08-13T13:12:08Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
assertEquals(13, entry.getCompressedSize());
assertEquals("-lh0-", entry.getCompressionMethod());
assertEquals(0x7757, entry.getCrcValue());
- assertEquals(85, entry.getOsId());
- assertEquals(0100644, entry.getUnixPermissionMode());
- assertEquals(20, entry.getUnixGroupId());
- assertEquals(501, entry.getUnixUserId());
+ assertNull(entry.getOsId());
+ assertNull(entry.getUnixPermissionMode());
+ assertNull(entry.getUnixGroupId());
+ assertNull(entry.getUnixUserId());
assertNull(entry.getMsdosFileAttributes());
assertNull(entry.getHeaderCrc());
@@ -758,15 +874,15 @@ void testParseHeaderLevel1FileWithFoldersMacos() throws IOException {
assertEquals("dir1/dir1-2/", entry.getName());
assertTrue(entry.isDirectory());
assertEquals(0, entry.getSize());
- assertEquals(1755083612000L, entry.getLastModifiedDate().getTime());
- assertEquals(ZonedDateTime.parse("2025-08-13T11:13:32Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(1755090812000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
+ assertEquals(ZonedDateTime.parse("2025-08-13T13:13:32Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
assertEquals(0, entry.getCompressedSize());
assertEquals("-lhd-", entry.getCompressionMethod());
assertEquals(0x0000, entry.getCrcValue());
- assertEquals(85, entry.getOsId());
- assertEquals(040755, entry.getUnixPermissionMode());
- assertEquals(20, entry.getUnixGroupId());
- assertEquals(501, entry.getUnixUserId());
+ assertNull(entry.getOsId());
+ assertNull(entry.getUnixPermissionMode());
+ assertNull(entry.getUnixGroupId());
+ assertNull(entry.getUnixUserId());
assertNull(entry.getMsdosFileAttributes());
assertNull(entry.getHeaderCrc());
@@ -776,15 +892,15 @@ void testParseHeaderLevel1FileWithFoldersMacos() throws IOException {
assertEquals("dir1/dir1-2/test2.txt", entry.getName());
assertFalse(entry.isDirectory());
assertEquals(13, entry.getSize());
- assertEquals(1755083612000L, entry.getLastModifiedDate().getTime());
- assertEquals(ZonedDateTime.parse("2025-08-13T11:13:32Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(1755090812000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
+ assertEquals(ZonedDateTime.parse("2025-08-13T13:13:32Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
assertEquals(13, entry.getCompressedSize());
assertEquals("-lh0-", entry.getCompressionMethod());
assertEquals(0x7757, entry.getCrcValue());
- assertEquals(85, entry.getOsId());
- assertEquals(0100644, entry.getUnixPermissionMode());
- assertEquals(20, entry.getUnixGroupId());
- assertEquals(501, entry.getUnixUserId());
+ assertNull(entry.getOsId());
+ assertNull(entry.getUnixPermissionMode());
+ assertNull(entry.getUnixGroupId());
+ assertNull(entry.getUnixUserId());
assertNull(entry.getMsdosFileAttributes());
assertNull(entry.getHeaderCrc());
@@ -794,13 +910,12 @@ void testParseHeaderLevel1FileWithFoldersMacos() throws IOException {
}
@Test
- void testParseHeaderLevel1FileWithFoldersMsdos() throws IOException {
+ void testParseHeaderLevel0FileWithFoldersMsdos() throws IOException {
// The lha file was generated by LHA32 v2.67.00 for Windows
try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
- .setInputStream(newInputStream("test-msdos-l1.lha"))
+ .setInputStream(newInputStream("test-msdos-l0.lha"))
.setFileSeparatorChar('/')
.get()) {
-
LhaArchiveEntry entry;
// Check directory entry
@@ -814,12 +929,12 @@ void testParseHeaderLevel1FileWithFoldersMsdos() throws IOException {
assertEquals(0, entry.getCompressedSize());
assertEquals("-lhd-", entry.getCompressionMethod());
assertEquals(0x0000, entry.getCrcValue());
- assertEquals(77, entry.getOsId());
+ assertNull(entry.getOsId());
assertNull(entry.getUnixPermissionMode());
assertNull(entry.getUnixGroupId());
assertNull(entry.getUnixUserId());
- assertEquals(0x0010, entry.getMsdosFileAttributes());
- assertEquals(0xd458, entry.getHeaderCrc());
+ assertNull(entry.getMsdosFileAttributes());
+ assertNull(entry.getHeaderCrc());
// Check directory entry
entry = archive.getNextEntry();
@@ -832,12 +947,12 @@ void testParseHeaderLevel1FileWithFoldersMsdos() throws IOException {
assertEquals(0, entry.getCompressedSize());
assertEquals("-lhd-", entry.getCompressionMethod());
assertEquals(0x0000, entry.getCrcValue());
- assertEquals(77, entry.getOsId());
+ assertNull(entry.getOsId());
assertNull(entry.getUnixPermissionMode());
assertNull(entry.getUnixGroupId());
assertNull(entry.getUnixUserId());
- assertEquals(0x0010, entry.getMsdosFileAttributes());
- assertEquals(0x40de, entry.getHeaderCrc());
+ assertNull(entry.getMsdosFileAttributes());
+ assertNull(entry.getHeaderCrc());
// Check file entry
entry = archive.getNextEntry();
@@ -850,12 +965,12 @@ void testParseHeaderLevel1FileWithFoldersMsdos() throws IOException {
assertEquals(14, entry.getCompressedSize());
assertEquals("-lh0-", entry.getCompressionMethod());
assertEquals(0xc9b4, entry.getCrcValue());
- assertEquals(77, entry.getOsId());
+ assertNull(entry.getOsId());
assertNull(entry.getUnixPermissionMode());
assertNull(entry.getUnixGroupId());
assertNull(entry.getUnixUserId());
assertNull(entry.getMsdosFileAttributes());
- assertEquals(0x34b0, entry.getHeaderCrc());
+ assertNull(entry.getHeaderCrc());
// Check directory entry
entry = archive.getNextEntry();
@@ -868,12 +983,12 @@ void testParseHeaderLevel1FileWithFoldersMsdos() throws IOException {
assertEquals(0, entry.getCompressedSize());
assertEquals("-lhd-", entry.getCompressionMethod());
assertEquals(0x0000, entry.getCrcValue());
- assertEquals(77, entry.getOsId());
+ assertNull(entry.getOsId());
assertNull(entry.getUnixPermissionMode());
assertNull(entry.getUnixGroupId());
assertNull(entry.getUnixUserId());
- assertEquals(0x0010, entry.getMsdosFileAttributes());
- assertEquals(0x21b2, entry.getHeaderCrc());
+ assertNull(entry.getMsdosFileAttributes());
+ assertNull(entry.getHeaderCrc());
// Check file entry
entry = archive.getNextEntry();
@@ -886,12 +1001,12 @@ void testParseHeaderLevel1FileWithFoldersMsdos() throws IOException {
assertEquals(14, entry.getCompressedSize());
assertEquals("-lh0-", entry.getCompressionMethod());
assertEquals(0xc9b4, entry.getCrcValue());
- assertEquals(77, entry.getOsId());
+ assertNull(entry.getOsId());
assertNull(entry.getUnixPermissionMode());
assertNull(entry.getUnixGroupId());
assertNull(entry.getUnixUserId());
assertNull(entry.getMsdosFileAttributes());
- assertEquals(0x8f0c, entry.getHeaderCrc());
+ assertNull(entry.getHeaderCrc());
// No more entries expected
assertNull(archive.getNextEntry());
@@ -899,10 +1014,36 @@ void testParseHeaderLevel1FileWithFoldersMsdos() throws IOException {
}
@Test
- void testParseHeaderLevel2File() throws IOException {
+ void testParseHeaderLevel0Larc() throws IOException {
+ // This archive was created using LArc 3.33 on MS-DOS
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newInputStream("test-msdos-l0-lz4.lzs")).get()) {
+ // Check file entry
+ final LhaArchiveEntry entry = archive.getNextEntry();
+ assertNotNull(entry);
+ assertEquals("TEST1.TXT", entry.getName());
+ assertFalse(entry.isDirectory());
+ assertEquals(14, entry.getSize());
+ assertEquals(1757247072000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
+ assertEquals(ZonedDateTime.parse("2025-09-07T12:11:12Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
+ assertEquals(14, entry.getCompressedSize());
+ assertEquals("-lz4-", entry.getCompressionMethod());
+ assertEquals(0xc9b4, entry.getCrcValue());
+ assertNull(entry.getOsId());
+ assertNull(entry.getUnixPermissionMode());
+ assertNull(entry.getUnixGroupId());
+ assertNull(entry.getUnixUserId());
+ assertNull(entry.getMsdosFileAttributes());
+ assertNull(entry.getHeaderCrc());
+
+ // No more entries expected
+ assertNull(archive.getNextEntry());
+ }
+ }
+
+ @Test
+ void testParseHeaderLevel1File() throws IOException {
try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
- .setInputStream(new ByteArrayInputStream(toByteArray(VALID_HEADER_LEVEL_2_FILE)))
- .setFileSeparatorChar('/')
+ .setInputStream(new ByteArrayInputStream(toByteArray(VALID_HEADER_LEVEL_1_FILE)))
.get()) {
// Entry should be parsed correctly
@@ -921,7 +1062,7 @@ void testParseHeaderLevel2File() throws IOException {
assertEquals(20, entry.getUnixGroupId());
assertEquals(501, entry.getUnixUserId());
assertNull(entry.getMsdosFileAttributes());
- assertEquals(0x01a5, entry.getHeaderCrc());
+ assertNull(entry.getHeaderCrc());
// No more entries expected
assertNull(archive.getNextEntry());
@@ -929,82 +1070,50 @@ void testParseHeaderLevel2File() throws IOException {
}
@Test
- void testInvalidHeaderLevel2Length() throws IOException {
- final byte[] data = toByteArray(VALID_HEADER_LEVEL_2_FILE);
-
- data[0] = 25; // Change the first byte to an invalid length
-
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
- archive.getNextEntry();
- fail("Expected ArchiveException for invalid header length");
- } catch (ArchiveException e) {
- assertEquals("Invalid header level 2 length: 25", e.getMessage());
- }
- }
-
- @Test
- void testInvalidHeaderLevel2Checksum() throws IOException {
- final byte[] data = toByteArray(VALID_HEADER_LEVEL_2_FILE);
-
- // Change header CRC to an invalid value
- data[27] = 0x33;
- data[28] = 0x22;
-
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
- archive.getNextEntry();
- fail("Expected ArchiveException for invalid header checksum");
- } catch (ArchiveException e) {
- assertEquals("Invalid header CRC expected=0x01a5 found=0x2233", e.getMessage());
- }
- }
-
- @Test
- void testParseHeaderLevel2FileWithFoldersAmiga() throws IOException {
- // The lha file was generated by LhA 2.15 on Amiga
+ void testParseHeaderLevel1FileMsdosChecksumAndCrc() throws IOException {
+ // The lha file was generated by LHA32 v2.67.00 for Windows
try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
- .setInputStream(newInputStream("test-amiga-l2.lha"))
+ .setInputStream(new ByteArrayInputStream(toByteArray(VALID_HEADER_LEVEL_1_FILE_MSDOS_WITH_CHECKSUM_AND_CRC)))
.setFileSeparatorChar('/')
.get()) {
LhaArchiveEntry entry;
- // No -lhd- directory entries in Amiga LHA files, so we expect only file entries
-
- // Check file entry
+ // Check directory entry
entry = archive.getNextEntry();
assertNotNull(entry);
- assertEquals("dir1/dir1-1/test1.txt", entry.getName());
- assertFalse(entry.isDirectory());
- assertEquals(14, entry.getSize());
- assertEquals(1755081276000L, entry.getLastModifiedDate().getTime());
- assertEquals(ZonedDateTime.parse("2025-08-13T10:34:36Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
- assertEquals(14, entry.getCompressedSize());
- assertEquals("-lh0-", entry.getCompressionMethod());
- assertEquals(0xc9b4, entry.getCrcValue());
- assertEquals(65, entry.getOsId());
+ assertEquals("dir1/", entry.getName());
+ assertTrue(entry.isDirectory());
+ assertEquals(0, entry.getSize());
+ assertEquals(1755097078000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
+ assertEquals(ZonedDateTime.parse("2025-08-13T14:57:58Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
+ assertEquals(0, entry.getCompressedSize());
+ assertEquals("-lhd-", entry.getCompressionMethod());
+ assertEquals(0x0000, entry.getCrcValue());
+ assertEquals(77, entry.getOsId());
assertNull(entry.getUnixPermissionMode());
assertNull(entry.getUnixGroupId());
assertNull(entry.getUnixUserId());
- assertNull(entry.getMsdosFileAttributes());
- assertEquals(0xe1a5, entry.getHeaderCrc());
+ assertEquals(0x0010, entry.getMsdosFileAttributes());
+ assertEquals(0xb772, entry.getHeaderCrc());
// Check file entry
entry = archive.getNextEntry();
assertNotNull(entry);
- assertEquals("dir1/dir1-2/test2.txt", entry.getName());
+ assertEquals("dir1/test1.txt", entry.getName());
assertFalse(entry.isDirectory());
assertEquals(14, entry.getSize());
- assertEquals(1755081276000L, entry.getLastModifiedDate().getTime());
- assertEquals(ZonedDateTime.parse("2025-08-13T10:34:36Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(1755081276000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
+ assertEquals(ZonedDateTime.parse("2025-08-13T10:34:36Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
assertEquals(14, entry.getCompressedSize());
assertEquals("-lh0-", entry.getCompressionMethod());
assertEquals(0xc9b4, entry.getCrcValue());
- assertEquals(65, entry.getOsId());
+ assertEquals(77, entry.getOsId());
assertNull(entry.getUnixPermissionMode());
assertNull(entry.getUnixGroupId());
assertNull(entry.getUnixUserId());
assertNull(entry.getMsdosFileAttributes());
- assertEquals(0xd6b0, entry.getHeaderCrc());
+ assertEquals(0x9b71, entry.getHeaderCrc());
// No more entries expected
assertNull(archive.getNextEntry());
@@ -1012,10 +1121,10 @@ void testParseHeaderLevel2FileWithFoldersAmiga() throws IOException {
}
@Test
- void testParseHeaderLevel2FileWithFoldersMacos() throws IOException {
+ void testParseHeaderLevel1FileWithFoldersMacos() throws IOException {
// The lha file was generated by LHa for UNIX version 1.14i-ac20211125 for Macos
try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
- .setInputStream(newInputStream("test-macos-l2.lha"))
+ .setInputStream(newInputStream("test-macos-l1.lha"))
.setFileSeparatorChar('/')
.get()) {
@@ -1037,7 +1146,7 @@ void testParseHeaderLevel2FileWithFoldersMacos() throws IOException {
assertEquals(20, entry.getUnixGroupId());
assertEquals(501, entry.getUnixUserId());
assertNull(entry.getMsdosFileAttributes());
- assertEquals(0xf3f7, entry.getHeaderCrc());
+ assertNull(entry.getHeaderCrc());
// Check directory entry
entry = archive.getNextEntry();
@@ -1055,7 +1164,7 @@ void testParseHeaderLevel2FileWithFoldersMacos() throws IOException {
assertEquals(20, entry.getUnixGroupId());
assertEquals(501, entry.getUnixUserId());
assertNull(entry.getMsdosFileAttributes());
- assertEquals(0x50d3, entry.getHeaderCrc());
+ assertNull(entry.getHeaderCrc());
// Check file entry
entry = archive.getNextEntry();
@@ -1073,7 +1182,7 @@ void testParseHeaderLevel2FileWithFoldersMacos() throws IOException {
assertEquals(20, entry.getUnixGroupId());
assertEquals(501, entry.getUnixUserId());
assertNull(entry.getMsdosFileAttributes());
- assertEquals(0x589e, entry.getHeaderCrc());
+ assertNull(entry.getHeaderCrc());
// Check directory entry
entry = archive.getNextEntry();
@@ -1091,7 +1200,7 @@ void testParseHeaderLevel2FileWithFoldersMacos() throws IOException {
assertEquals(20, entry.getUnixGroupId());
assertEquals(501, entry.getUnixUserId());
assertNull(entry.getMsdosFileAttributes());
- assertEquals(0x126d, entry.getHeaderCrc());
+ assertNull(entry.getHeaderCrc());
// Check file entry
entry = archive.getNextEntry();
@@ -1109,7 +1218,7 @@ void testParseHeaderLevel2FileWithFoldersMacos() throws IOException {
assertEquals(20, entry.getUnixGroupId());
assertEquals(501, entry.getUnixUserId());
assertNull(entry.getMsdosFileAttributes());
- assertEquals(0xdbdd, entry.getHeaderCrc());
+ assertNull(entry.getHeaderCrc());
// No more entries expected
assertNull(archive.getNextEntry());
@@ -1117,10 +1226,10 @@ void testParseHeaderLevel2FileWithFoldersMacos() throws IOException {
}
@Test
- void testParseHeaderLevel2FileWithFoldersMsdos() throws IOException {
+ void testParseHeaderLevel1FileWithFoldersMsdos() throws IOException {
// The lha file was generated by LHA32 v2.67.00 for Windows
try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
- .setInputStream(newInputStream("test-msdos-l2.lha"))
+ .setInputStream(newInputStream("test-msdos-l1.lha"))
.setFileSeparatorChar('/')
.get()) {
@@ -1132,8 +1241,8 @@ void testParseHeaderLevel2FileWithFoldersMsdos() throws IOException {
assertEquals("dir1/", entry.getName());
assertTrue(entry.isDirectory());
assertEquals(0, entry.getSize());
- assertEquals(1755081308000L, entry.getLastModifiedDate().getTime());
- assertEquals(ZonedDateTime.parse("2025-08-13T10:35:08Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(1755081308000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
+ assertEquals(ZonedDateTime.parse("2025-08-13T10:35:08Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
assertEquals(0, entry.getCompressedSize());
assertEquals("-lhd-", entry.getCompressionMethod());
assertEquals(0x0000, entry.getCrcValue());
@@ -1142,7 +1251,7 @@ void testParseHeaderLevel2FileWithFoldersMsdos() throws IOException {
assertNull(entry.getUnixGroupId());
assertNull(entry.getUnixUserId());
assertEquals(0x0010, entry.getMsdosFileAttributes());
- assertEquals(0x496a, entry.getHeaderCrc());
+ assertEquals(0xd458, entry.getHeaderCrc());
// Check directory entry
entry = archive.getNextEntry();
@@ -1150,8 +1259,8 @@ void testParseHeaderLevel2FileWithFoldersMsdos() throws IOException {
assertEquals("dir1/dir1-1/", entry.getName());
assertTrue(entry.isDirectory());
assertEquals(0, entry.getSize());
- assertEquals(1755081336000L, entry.getLastModifiedDate().getTime());
- assertEquals(ZonedDateTime.parse("2025-08-13T10:35:36Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(1755081336000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
+ assertEquals(ZonedDateTime.parse("2025-08-13T10:35:36Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
assertEquals(0, entry.getCompressedSize());
assertEquals("-lhd-", entry.getCompressionMethod());
assertEquals(0x0000, entry.getCrcValue());
@@ -1160,7 +1269,7 @@ void testParseHeaderLevel2FileWithFoldersMsdos() throws IOException {
assertNull(entry.getUnixGroupId());
assertNull(entry.getUnixUserId());
assertEquals(0x0010, entry.getMsdosFileAttributes());
- assertEquals(0xebe7, entry.getHeaderCrc());
+ assertEquals(0x40de, entry.getHeaderCrc());
// Check file entry
entry = archive.getNextEntry();
@@ -1168,8 +1277,8 @@ void testParseHeaderLevel2FileWithFoldersMsdos() throws IOException {
assertEquals("dir1/dir1-1/test1.txt", entry.getName());
assertFalse(entry.isDirectory());
assertEquals(14, entry.getSize());
- assertEquals(1755081276000L, entry.getLastModifiedDate().getTime());
- assertEquals(ZonedDateTime.parse("2025-08-13T10:34:36Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(1755081276000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
+ assertEquals(ZonedDateTime.parse("2025-08-13T10:34:36Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
assertEquals(14, entry.getCompressedSize());
assertEquals("-lh0-", entry.getCompressionMethod());
assertEquals(0xc9b4, entry.getCrcValue());
@@ -1178,7 +1287,7 @@ void testParseHeaderLevel2FileWithFoldersMsdos() throws IOException {
assertNull(entry.getUnixGroupId());
assertNull(entry.getUnixUserId());
assertNull(entry.getMsdosFileAttributes());
- assertEquals(0x214a, entry.getHeaderCrc());
+ assertEquals(0x34b0, entry.getHeaderCrc());
// Check directory entry
entry = archive.getNextEntry();
@@ -1186,8 +1295,8 @@ void testParseHeaderLevel2FileWithFoldersMsdos() throws IOException {
assertEquals("dir1/dir1-2/", entry.getName());
assertTrue(entry.isDirectory());
assertEquals(0, entry.getSize());
- assertEquals(1755081341000L, entry.getLastModifiedDate().getTime());
- assertEquals(ZonedDateTime.parse("2025-08-13T10:35:41Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(1755081340000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
+ assertEquals(ZonedDateTime.parse("2025-08-13T10:35:40Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
assertEquals(0, entry.getCompressedSize());
assertEquals("-lhd-", entry.getCompressionMethod());
assertEquals(0x0000, entry.getCrcValue());
@@ -1196,7 +1305,7 @@ void testParseHeaderLevel2FileWithFoldersMsdos() throws IOException {
assertNull(entry.getUnixGroupId());
assertNull(entry.getUnixUserId());
assertEquals(0x0010, entry.getMsdosFileAttributes());
- assertEquals(0x74ca, entry.getHeaderCrc());
+ assertEquals(0x21b2, entry.getHeaderCrc());
// Check file entry
entry = archive.getNextEntry();
@@ -1204,486 +1313,377 @@ void testParseHeaderLevel2FileWithFoldersMsdos() throws IOException {
assertEquals("dir1/dir1-2/test2.txt", entry.getName());
assertFalse(entry.isDirectory());
assertEquals(14, entry.getSize());
- assertEquals(1755081276000L, entry.getLastModifiedDate().getTime());
- assertEquals(ZonedDateTime.parse("2025-08-13T10:34:36Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
- assertEquals(14, entry.getCompressedSize());
- assertEquals("-lh0-", entry.getCompressionMethod());
- assertEquals(0xc9b4, entry.getCrcValue());
- assertEquals(77, entry.getOsId());
- assertNull(entry.getUnixPermissionMode());
- assertNull(entry.getUnixGroupId());
- assertNull(entry.getUnixUserId());
- assertNull(entry.getMsdosFileAttributes());
- assertEquals(0x165f, entry.getHeaderCrc());
-
- // No more entries expected
- assertNull(archive.getNextEntry());
- }
- }
-
- @Test
- void testParseHeaderLevel2FileWithMsdosAttributes() throws IOException {
- // The lha file was generated by LHA32 v2.67.00 for Windows
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newInputStream("test-msdos-l2-attrib.lha")).get()) {
- // Check file entry
- final LhaArchiveEntry entry = archive.getNextEntry();
- assertNotNull(entry);
- assertEquals("test1.txt", entry.getName());
- assertFalse(entry.isDirectory());
- assertEquals(14, entry.getSize());
- assertEquals(1755081276000L, entry.getLastModifiedDate().getTime());
- assertEquals(ZonedDateTime.parse("2025-08-13T10:34:36Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(1755081276000L, convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()).toInstant().toEpochMilli());
+ assertEquals(ZonedDateTime.parse("2025-08-13T10:34:36Z"), convertSystemTimeZoneDateToUTC(entry.getLastModifiedDate()));
assertEquals(14, entry.getCompressedSize());
assertEquals("-lh0-", entry.getCompressionMethod());
assertEquals(0xc9b4, entry.getCrcValue());
assertEquals(77, entry.getOsId());
assertNull(entry.getUnixPermissionMode());
assertNull(entry.getUnixGroupId());
- assertNull(entry.getUnixUserId());
- assertEquals(0x0021, entry.getMsdosFileAttributes());
- assertEquals(0x14bb, entry.getHeaderCrc());
-
- // No more entries expected
- assertNull(archive.getNextEntry());
- }
- }
-
- @Test
- void testParseExtendedHeaderTooShort() throws IOException {
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).get()) {
- final LhaArchiveEntry.Builder entryBuilder = LhaArchiveEntry.builder();
- try {
- archive.parseExtendedHeader(toByteBuffer(0x00, 0x00), entryBuilder);
- fail("Expected ArchiveException for invalid extended header length");
- } catch (ArchiveException e) {
- assertEquals("Invalid extended header length", e.getMessage());
- }
- }
- }
-
- @Test
- void testParseExtendedHeaderCommon() throws IOException {
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).get()) {
- // Valid
- final LhaArchiveEntry.Builder entryBuilder = LhaArchiveEntry.builder();
- archive.parseExtendedHeader(toByteBuffer(0x00, 0x22, 0x33, 0x00, 0x00), entryBuilder);
- assertEquals(0x3322, entryBuilder.get().getHeaderCrc());
-
- // Invalid length
- try {
- archive.parseExtendedHeader(toByteBuffer(0x00, 0x22, 0x00, 0x00), entryBuilder);
- fail("Expected ArchiveException for invalid extended header length");
- } catch (ArchiveException e) {
- assertEquals("Invalid extended header length", e.getMessage());
- }
- }
- }
-
- @Test
- void testParseExtendedHeaderFilename() throws IOException {
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).get()) {
- // Valid
- final LhaArchiveEntry.Builder entryBuilder = LhaArchiveEntry.builder();
- archive.parseExtendedHeader(toByteBuffer(0x01, 't', 'e', 's', 't', '.', 't', 'x', 't', 0x00, 0x00), entryBuilder);
- assertEquals("test.txt", entryBuilder.get().getName());
-
- // Invalid length
- try {
- archive.parseExtendedHeader(toByteBuffer(0x01, 0x00), entryBuilder);
- fail("Expected ArchiveException for invalid extended header length");
- } catch (ArchiveException e) {
- assertEquals("Invalid extended header length", e.getMessage());
- }
- }
- }
-
- @Test
- void testParseExtendedHeaderDirectoryName() throws IOException {
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
- .setInputStream(newEmptyInputStream())
- .setFileSeparatorChar('/')
- .get()) {
-
- // Valid
- final LhaArchiveEntry.Builder entryBuilder = LhaArchiveEntry.builder();
- archive.parseExtendedHeader(toByteBuffer(0x02, 'd', 'i', 'r', '1', 0xff, 0x00, 0x00), entryBuilder);
- assertEquals("dir1/", entryBuilder.get().getName());
-
- // Invalid length
- try {
- archive.parseExtendedHeader(toByteBuffer(0x02, 0x00), entryBuilder);
- fail("Expected ArchiveException for invalid extended header length");
- } catch (ArchiveException e) {
- assertEquals("Invalid extended header length", e.getMessage());
- }
- }
- }
-
- @Test
- void testParseExtendedHeaderFilenameAndDirectoryName() throws IOException {
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
- .setInputStream(newEmptyInputStream())
- .setFileSeparatorChar('/')
- .get()) {
-
- LhaArchiveEntry.Builder entryBuilder;
-
- // Test filename and directory name order
- entryBuilder = LhaArchiveEntry.builder();
- archive.parseExtendedHeader(toByteBuffer(0x01, 't', 'e', 's', 't', '.', 't', 'x', 't', 0x00, 0x00), entryBuilder);
- archive.parseExtendedHeader(toByteBuffer(0x02, 'd', 'i', 'r', '1', 0xff, 0x00, 0x00), entryBuilder);
- assertEquals("dir1/test.txt", entryBuilder.get().getName());
-
- // Test filename and directory name order, no trailing slash
- entryBuilder = LhaArchiveEntry.builder();
- archive.parseExtendedHeader(toByteBuffer(0x01, 't', 'e', 's', 't', '.', 't', 'x', 't', 0x00, 0x00), entryBuilder);
- archive.parseExtendedHeader(toByteBuffer(0x02, 'd', 'i', 'r', '1', 0x00, 0x00), entryBuilder);
- assertEquals("dir1/test.txt", entryBuilder.get().getName());
-
- // Test directory name and filename order
- entryBuilder = LhaArchiveEntry.builder();
- archive.parseExtendedHeader(toByteBuffer(0x02, 'd', 'i', 'r', '1', 0xff, 0x00, 0x00), entryBuilder);
- archive.parseExtendedHeader(toByteBuffer(0x01, 't', 'e', 's', 't', '.', 't', 'x', 't', 0x00, 0x00), entryBuilder);
- assertEquals("dir1/test.txt", entryBuilder.get().getName());
-
- // Test directory name and filename order, no trailing slash
- entryBuilder = LhaArchiveEntry.builder();
- archive.parseExtendedHeader(toByteBuffer(0x02, 'd', 'i', 'r', '1', 0x00, 0x00), entryBuilder);
- archive.parseExtendedHeader(toByteBuffer(0x01, 't', 'e', 's', 't', '.', 't', 'x', 't', 0x00, 0x00), entryBuilder);
- assertEquals("dir1/test.txt", entryBuilder.get().getName());
-
- // Test empty directory name, no trailing slash
- entryBuilder = LhaArchiveEntry.builder();
- archive.parseExtendedHeader(toByteBuffer(0x02, 0x00, 0x00), entryBuilder);
- archive.parseExtendedHeader(toByteBuffer(0x01, 't', 'e', 's', 't', '.', 't', 'x', 't', 0x00, 0x00), entryBuilder);
- assertEquals("test.txt", entryBuilder.get().getName());
- }
- }
-
- @Test
- void testParseExtendedHeaderUnixPermission() throws IOException {
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).get()) {
- // Valid
- final LhaArchiveEntry.Builder entryBuilder = LhaArchiveEntry.builder();
- archive.parseExtendedHeader(toByteBuffer(0x50, 0xa4, 0x81, 0x00, 0x00), entryBuilder);
- assertEquals(0x81a4, entryBuilder.get().getUnixPermissionMode());
- assertEquals(0100644, entryBuilder.get().getUnixPermissionMode());
-
- // Invalid length
- try {
- archive.parseExtendedHeader(toByteBuffer(0x50, 0xa4, 0x00, 0x00), entryBuilder);
- fail("Expected ArchiveException for invalid extended header length");
- } catch (ArchiveException e) {
- assertEquals("Invalid extended header length", e.getMessage());
- }
- }
- }
-
- @Test
- void testParseExtendedHeaderUnixUidGid() throws IOException {
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).get()) {
- // Valid
- final LhaArchiveEntry.Builder entryBuilder = LhaArchiveEntry.builder();
- archive.parseExtendedHeader(toByteBuffer(0x51, 0x14, 0x00, 0xf5, 0x01, 0x00, 0x00), entryBuilder);
- assertEquals(0x0014, entryBuilder.get().getUnixGroupId());
- assertEquals(0x01f5, entryBuilder.get().getUnixUserId());
-
- // Invalid length
- try {
- archive.parseExtendedHeader(toByteBuffer(0x51, 0x14, 0x00, 0xf5, 0x00, 0x00), entryBuilder);
- fail("Expected ArchiveException for invalid extended header length");
- } catch (ArchiveException e) {
- assertEquals("Invalid extended header length", e.getMessage());
- }
- }
- }
-
- @Test
- void testParseExtendedHeaderUnixTimestamp() throws IOException {
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).get()) {
- // Valid
- final LhaArchiveEntry.Builder entryBuilder = LhaArchiveEntry.builder();
- archive.parseExtendedHeader(toByteBuffer(0x54, 0x5c, 0x73, 0x9c, 0x68, 0x00, 0x00), entryBuilder);
- assertEquals(0x689c735cL, entryBuilder.get().getLastModifiedDate().getTime() / 1000);
-
- // Invalid length
- try {
- archive.parseExtendedHeader(toByteBuffer(0x54, 0x5c, 0x73, 0x9c, 0x00, 0x00), entryBuilder);
- fail("Expected ArchiveException for invalid extended header length");
- } catch (ArchiveException e) {
- assertEquals("Invalid extended header length", e.getMessage());
- }
- }
- }
-
- @Test
- void testParseExtendedHeaderMSdosFileAttributes() throws IOException {
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).get()) {
- // Valid
- final LhaArchiveEntry.Builder entryBuilder = LhaArchiveEntry.builder();
- archive.parseExtendedHeader(toByteBuffer(0x40, 0x10, 0x00, 0x00, 0x00), entryBuilder);
- assertEquals(0x10, entryBuilder.get().getMsdosFileAttributes());
+ assertNull(entry.getUnixUserId());
+ assertNull(entry.getMsdosFileAttributes());
+ assertEquals(0x8f0c, entry.getHeaderCrc());
- // Invalid length
- try {
- archive.parseExtendedHeader(toByteBuffer(0x40, 0x10, 0x00, 0x00), entryBuilder);
- fail("Expected ArchiveException for invalid extended header length");
- } catch (ArchiveException e) {
- assertEquals("Invalid extended header length", e.getMessage());
- }
+ // No more entries expected
+ assertNull(archive.getNextEntry());
}
}
@Test
- void testDecompressLh0() throws Exception {
+ void testParseHeaderLevel2File() throws IOException {
try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
- .setInputStream(newInputStream("test-macos-l0.lha"))
+ .setInputStream(new ByteArrayInputStream(toByteArray(VALID_HEADER_LEVEL_2_FILE)))
+ .setFileSeparatorChar('/')
.get()) {
- final List<String> files = new ArrayList<>();
- files.add("dir1" + File.separatorChar);
- files.add("dir1" + File.separatorChar + "dir1-1" + File.separatorChar);
- files.add("dir1" + File.separatorChar + "dir1-1" + File.separatorChar + "test1.txt");
- files.add("dir1" + File.separatorChar + "dir1-2" + File.separatorChar);
- files.add("dir1" + File.separatorChar + "dir1-2" + File.separatorChar + "test2.txt");
- checkArchiveContent(archive, files);
- }
- }
-
- @Test
- void testDecompressLh4() throws Exception {
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newInputStream("test-amiga-l0-lh4.lha")).get()) {
- final List<String> files = new ArrayList<>();
- files.add("lorem-ipsum.txt");
- checkArchiveContent(archive, files);
- }
- }
-
- @Test
- void testDecompressLh5() throws Exception {
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newInputStream("test-macos-l0-lh5.lha")).get()) {
- final List<String> files = new ArrayList<>();
- files.add("lorem-ipsum.txt");
- checkArchiveContent(archive, files);
- }
- }
+ // Entry should be parsed correctly
+ final LhaArchiveEntry entry = archive.getNextEntry();
+ assertNotNull(entry);
+ assertEquals("test1.txt", entry.getName());
+ assertFalse(entry.isDirectory());
+ assertEquals(57, entry.getSize());
+ assertEquals(1754229743000L, entry.getLastModifiedDate().getTime());
+ assertEquals(ZonedDateTime.parse("2025-08-03T14:02:23Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(52, entry.getCompressedSize());
+ assertEquals("-lh5-", entry.getCompressionMethod());
+ assertEquals(0x6496, entry.getCrcValue());
+ assertEquals(85, entry.getOsId());
+ assertEquals(0100644, entry.getUnixPermissionMode());
+ assertEquals(20, entry.getUnixGroupId());
+ assertEquals(501, entry.getUnixUserId());
+ assertNull(entry.getMsdosFileAttributes());
+ assertEquals(0x01a5, entry.getHeaderCrc());
- /**
- * Test decompressing a file with lh5 compression that contains only one characters and thus is
- * basically RLE encoded. The distance tree contains only one entry (root node).
- */
- @Test
- void testDecompressLh5Rle() throws Exception {
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newInputStream("test-macos-l0-lh5-rle.lha")).get()) {
- final List<String> files = new ArrayList<>();
- files.add("rle.txt");
- checkArchiveContent(archive, files);
+ // No more entries expected
+ assertNull(archive.getNextEntry());
}
}
@Test
- void testDecompressLh6() throws Exception {
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newInputStream("test-macos-l0-lh6.lha")).get()) {
- final List<String> files = new ArrayList<>();
- files.add("lorem-ipsum.txt");
- checkArchiveContent(archive, files);
- }
- }
+ void testParseHeaderLevel2FileWithFoldersAmiga() throws IOException {
+ // The lha file was generated by LhA 2.15 on Amiga
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
+ .setInputStream(newInputStream("test-amiga-l2.lha"))
+ .setFileSeparatorChar('/')
+ .get()) {
- @Test
- void testDecompressLh7() throws Exception {
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newInputStream("test-macos-l0-lh7.lha")).get()) {
- final List<String> files = new ArrayList<>();
- files.add("lorem-ipsum.txt");
- checkArchiveContent(archive, files);
- }
- }
+ LhaArchiveEntry entry;
- @Test
- void testDecompressLz4() throws Exception {
- // This archive was created using LArc 3.33 on MS-DOS
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newInputStream("test-msdos-l0-lz4.lzs")).get()) {
- final List<String> files = new ArrayList<>();
- files.add("TEST1.TXT");
- checkArchiveContent(archive, files);
- }
- }
+ // No -lhd- directory entries in Amiga LHA files, so we expect only file entries
- @Test
- void testGetBytesReadReflectsDecompressedBytes() throws IOException {
- try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
- .setInputStream(new ByteArrayInputStream(toByteArray(VALID_HEADER_LEVEL_0_FILE)))
- .get()) {
+ // Check file entry
+ entry = archive.getNextEntry();
+ assertNotNull(entry);
+ assertEquals("dir1/dir1-1/test1.txt", entry.getName());
+ assertFalse(entry.isDirectory());
+ assertEquals(14, entry.getSize());
+ assertEquals(1755081276000L, entry.getLastModifiedDate().getTime());
+ assertEquals(ZonedDateTime.parse("2025-08-13T10:34:36Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(14, entry.getCompressedSize());
+ assertEquals("-lh0-", entry.getCompressionMethod());
+ assertEquals(0xc9b4, entry.getCrcValue());
+ assertEquals(65, entry.getOsId());
+ assertNull(entry.getUnixPermissionMode());
+ assertNull(entry.getUnixGroupId());
+ assertNull(entry.getUnixUserId());
+ assertNull(entry.getMsdosFileAttributes());
+ assertEquals(0xe1a5, entry.getHeaderCrc());
- final LhaArchiveEntry entry = archive.getNextEntry();
+ // Check file entry
+ entry = archive.getNextEntry();
assertNotNull(entry);
+ assertEquals("dir1/dir1-2/test2.txt", entry.getName());
+ assertFalse(entry.isDirectory());
+ assertEquals(14, entry.getSize());
+ assertEquals(1755081276000L, entry.getLastModifiedDate().getTime());
+ assertEquals(ZonedDateTime.parse("2025-08-13T10:34:36Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(14, entry.getCompressedSize());
+ assertEquals("-lh0-", entry.getCompressionMethod());
+ assertEquals(0xc9b4, entry.getCrcValue());
+ assertEquals(65, entry.getOsId());
+ assertNull(entry.getUnixPermissionMode());
+ assertNull(entry.getUnixGroupId());
+ assertNull(entry.getUnixUserId());
+ assertNull(entry.getMsdosFileAttributes());
+ assertEquals(0xd6b0, entry.getHeaderCrc());
- final byte[] content = IOUtils.toByteArray(archive);
- assertEquals(entry.getSize(), content.length);
- assertEquals(entry.getSize(), archive.getBytesRead());
+ // No more entries expected
+ assertNull(archive.getNextEntry());
}
}
@Test
- void testMatches() {
- byte[] data;
-
- assertTrue(LhaArchiveInputStream.matches(toByteArray(VALID_HEADER_LEVEL_0_FILE), VALID_HEADER_LEVEL_0_FILE.length));
- assertTrue(LhaArchiveInputStream.matches(toByteArray(VALID_HEADER_LEVEL_1_FILE), VALID_HEADER_LEVEL_1_FILE.length));
- assertTrue(LhaArchiveInputStream.matches(toByteArray(VALID_HEADER_LEVEL_2_FILE), VALID_HEADER_LEVEL_2_FILE.length));
-
- // Header to short
- data = toByteArray(0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09);
- assertFalse(LhaArchiveInputStream.matches(data, data.length));
+ void testParseHeaderLevel2FileWithFoldersMacos() throws IOException {
+ // The lha file was generated by LHa for UNIX version 1.14i-ac20211125 for Macos
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
+ .setInputStream(newInputStream("test-macos-l2.lha"))
+ .setFileSeparatorChar('/')
+ .get()) {
- // Change the header level to an invalid value
- data = toByteArray(VALID_HEADER_LEVEL_0_FILE);
- data[20] = 3;
- assertFalse(LhaArchiveInputStream.matches(data, data.length));
+ LhaArchiveEntry entry;
- // Change the compression method to an invalid value
- data = toByteArray(VALID_HEADER_LEVEL_0_FILE);
- data[6] = 0x08;
- assertFalse(LhaArchiveInputStream.matches(data, data.length));
- }
+ // Check directory entry
+ entry = archive.getNextEntry();
+ assertNotNull(entry);
+ assertEquals("dir1/", entry.getName());
+ assertTrue(entry.isDirectory());
+ assertEquals(0, entry.getSize());
+ assertEquals(1755083490000L, entry.getLastModifiedDate().getTime());
+ assertEquals(ZonedDateTime.parse("2025-08-13T11:11:30Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(0, entry.getCompressedSize());
+ assertEquals("-lhd-", entry.getCompressionMethod());
+ assertEquals(0x0000, entry.getCrcValue());
+ assertEquals(85, entry.getOsId());
+ assertEquals(040755, entry.getUnixPermissionMode());
+ assertEquals(20, entry.getUnixGroupId());
+ assertEquals(501, entry.getUnixUserId());
+ assertNull(entry.getMsdosFileAttributes());
+ assertEquals(0xf3f7, entry.getHeaderCrc());
- @Test
- void testGetCompressionMethod() throws IOException {
- assertEquals("-lh0-", LhaArchiveInputStream.getCompressionMethod(ByteBuffer.wrap(toByteArray(0x00, 0x00, '-', 'l', 'h', '0', '-'))));
- assertEquals("-lhd-", LhaArchiveInputStream.getCompressionMethod(ByteBuffer.wrap(toByteArray(0x00, 0x00, '-', 'l', 'h', 'd', '-'))));
+ // Check directory entry
+ entry = archive.getNextEntry();
+ assertNotNull(entry);
+ assertEquals("dir1/dir1-1/", entry.getName());
+ assertTrue(entry.isDirectory());
+ assertEquals(0, entry.getSize());
+ assertEquals(1755083529000L, entry.getLastModifiedDate().getTime());
+ assertEquals(ZonedDateTime.parse("2025-08-13T11:12:09Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(0, entry.getCompressedSize());
+ assertEquals("-lhd-", entry.getCompressionMethod());
+ assertEquals(0x0000, entry.getCrcValue());
+ assertEquals(85, entry.getOsId());
+ assertEquals(040755, entry.getUnixPermissionMode());
+ assertEquals(20, entry.getUnixGroupId());
+ assertEquals(501, entry.getUnixUserId());
+ assertNull(entry.getMsdosFileAttributes());
+ assertEquals(0x50d3, entry.getHeaderCrc());
- try {
- LhaArchiveInputStream.getCompressionMethod(ByteBuffer.wrap(toByteArray(0x00, 0x00, '-', 'l', 'h', '0', 0xff)));
- fail("Expected ArchiveException for invalid compression method");
- } catch (ArchiveException e) {
- assertEquals("Invalid compression method: 0x2d 0x6c 0x68 0x30 0xff", e.getMessage());
- }
- }
+ // Check file entry
+ entry = archive.getNextEntry();
+ assertNotNull(entry);
+ assertEquals("dir1/dir1-1/test1.txt", entry.getName());
+ assertFalse(entry.isDirectory());
+ assertEquals(13, entry.getSize());
+ assertEquals(1755083529000L, entry.getLastModifiedDate().getTime());
+ assertEquals(ZonedDateTime.parse("2025-08-13T11:12:09Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(13, entry.getCompressedSize());
+ assertEquals("-lh0-", entry.getCompressionMethod());
+ assertEquals(0x7757, entry.getCrcValue());
+ assertEquals(85, entry.getOsId());
+ assertEquals(0100644, entry.getUnixPermissionMode());
+ assertEquals(20, entry.getUnixGroupId());
+ assertEquals(501, entry.getUnixUserId());
+ assertNull(entry.getMsdosFileAttributes());
+ assertEquals(0x589e, entry.getHeaderCrc());
- @Test
- void testGetPathnameUnixFileSeparatorCharDefaultEncoding() throws IOException, UnsupportedEncodingException {
- try (LhaArchiveInputStream is = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).setFileSeparatorChar('/').get()) {
- assertEquals("", getPathname(is));
- assertEquals("", getPathname(is, 0xff));
- assertEquals("a", getPathname(is, 'a'));
- assertEquals("folder/", getPathname(is, 'f', 'o', 'l', 'd', 'e', 'r', 0xff));
- assertEquals("folder/file.txt", getPathname(is, 'f', 'o', 'l', 'd', 'e', 'r', 0xff, 'f', 'i', 'l', 'e', '.', 't', 'x', 't'));
- assertEquals("folder/file.txt", getPathname(is, 0xff, 'f', 'o', 'l', 'd', 'e', 'r', 0xff, 'f', 'i', 'l', 'e', '.', 't', 'x', 't'));
- assertEquals("folder/file.txt", getPathname(is, '\\', 'f', 'o', 'l', 'd', 'e', 'r', '\\', 'f', 'i', 'l', 'e', '.', 't', 'x', 't'));
+ // Check directory entry
+ entry = archive.getNextEntry();
+ assertNotNull(entry);
+ assertEquals("dir1/dir1-2/", entry.getName());
+ assertTrue(entry.isDirectory());
+ assertEquals(0, entry.getSize());
+ assertEquals(1755083612000L, entry.getLastModifiedDate().getTime());
+ assertEquals(ZonedDateTime.parse("2025-08-13T11:13:32Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(0, entry.getCompressedSize());
+ assertEquals("-lhd-", entry.getCompressionMethod());
+ assertEquals(0x0000, entry.getCrcValue());
+ assertEquals(85, entry.getOsId());
+ assertEquals(040755, entry.getUnixPermissionMode());
+ assertEquals(20, entry.getUnixGroupId());
+ assertEquals(501, entry.getUnixUserId());
+ assertNull(entry.getMsdosFileAttributes());
+ assertEquals(0x126d, entry.getHeaderCrc());
- // Unicode replacement characters for unsupported characters
- assertEquals("\uFFFD/\uFFFD/\uFFFD.txt", getPathname(is, 0xe5, 0xff, 0xe4, 0xff, 0xf6, '.', 't', 'x', 't'));
- assertEquals("\uFFFD/\uFFFD/\uFFFD.txt", getPathname(is, 0xe5, '\\', 0xe4, '\\', 0xf6, '.', 't', 'x', 't'));
+ // Check file entry
+ entry = archive.getNextEntry();
+ assertNotNull(entry);
+ assertEquals("dir1/dir1-2/test2.txt", entry.getName());
+ assertFalse(entry.isDirectory());
+ assertEquals(13, entry.getSize());
+ assertEquals(1755083612000L, entry.getLastModifiedDate().getTime());
+ assertEquals(ZonedDateTime.parse("2025-08-13T11:13:32Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(13, entry.getCompressedSize());
+ assertEquals("-lh0-", entry.getCompressionMethod());
+ assertEquals(0x7757, entry.getCrcValue());
+ assertEquals(85, entry.getOsId());
+ assertEquals(0100644, entry.getUnixPermissionMode());
+ assertEquals(20, entry.getUnixGroupId());
+ assertEquals(501, entry.getUnixUserId());
+ assertNull(entry.getMsdosFileAttributes());
+ assertEquals(0xdbdd, entry.getHeaderCrc());
+
+ // No more entries expected
+ assertNull(archive.getNextEntry());
}
}
@Test
- void testGetPathnameUnixFileSeparatorCharIso88591() throws IOException, UnsupportedEncodingException {
- try (LhaArchiveInputStream is = LhaArchiveInputStream.builder()
- .setInputStream(newEmptyInputStream())
- .setCharset(StandardCharsets.ISO_8859_1)
+ void testParseHeaderLevel2FileWithFoldersMsdos() throws IOException {
+ // The lha file was generated by LHA32 v2.67.00 for Windows
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder()
+ .setInputStream(newInputStream("test-msdos-l2.lha"))
.setFileSeparatorChar('/')
.get()) {
- assertEquals("\u00E5/\u00E4/\u00F6.txt", getPathname(is, 0xe5, 0xff, 0xe4, 0xff, 0xf6, '.', 't', 'x', 't'));
- assertEquals("\u00E5/\u00E4/\u00F6.txt", getPathname(is, 0xe5, '\\', 0xe4, '\\', 0xf6, '.', 't', 'x', 't'));
- }
- }
+ LhaArchiveEntry entry;
- @Test
- void testGetPathnameWindowsFileSeparatorCharDefaultEncoding() throws IOException, UnsupportedEncodingException {
- try (LhaArchiveInputStream is = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).setFileSeparatorChar('\\').get()) {
- assertEquals("folder\\", getPathname(is, 'f', 'o', 'l', 'd', 'e', 'r', 0xff));
- assertEquals("folder\\file.txt", getPathname(is, 'f', 'o', 'l', 'd', 'e', 'r', 0xff, 'f', 'i', 'l', 'e', '.', 't', 'x', 't'));
- assertEquals("folder\\file.txt", getPathname(is, 0xff, 'f', 'o', 'l', 'd', 'e', 'r', 0xff, 'f', 'i', 'l', 'e', '.', 't', 'x', 't'));
- assertEquals("folder\\file.txt", getPathname(is, '\\', 'f', 'o', 'l', 'd', 'e', 'r', '\\', 'f', 'i', 'l', 'e', '.', 't', 'x', 't'));
+ // Check directory entry
+ entry = archive.getNextEntry();
+ assertNotNull(entry);
+ assertEquals("dir1/", entry.getName());
+ assertTrue(entry.isDirectory());
+ assertEquals(0, entry.getSize());
+ assertEquals(1755081308000L, entry.getLastModifiedDate().getTime());
+ assertEquals(ZonedDateTime.parse("2025-08-13T10:35:08Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(0, entry.getCompressedSize());
+ assertEquals("-lhd-", entry.getCompressionMethod());
+ assertEquals(0x0000, entry.getCrcValue());
+ assertEquals(77, entry.getOsId());
+ assertNull(entry.getUnixPermissionMode());
+ assertNull(entry.getUnixGroupId());
+ assertNull(entry.getUnixUserId());
+ assertEquals(0x0010, entry.getMsdosFileAttributes());
+ assertEquals(0x496a, entry.getHeaderCrc());
- // Unicode replacement characters for unsupported characters
- assertEquals("\uFFFD\\\uFFFD\\\uFFFD.txt", getPathname(is, 0xe5, 0xff, 0xe4, 0xff, 0xf6, '.', 't', 'x', 't'));
- assertEquals("\uFFFD\\\uFFFD\\\uFFFD.txt", getPathname(is, 0xe5, '\\', 0xe4, '\\', 0xf6, '.', 't', 'x', 't'));
- }
- }
+ // Check directory entry
+ entry = archive.getNextEntry();
+ assertNotNull(entry);
+ assertEquals("dir1/dir1-1/", entry.getName());
+ assertTrue(entry.isDirectory());
+ assertEquals(0, entry.getSize());
+ assertEquals(1755081336000L, entry.getLastModifiedDate().getTime());
+ assertEquals(ZonedDateTime.parse("2025-08-13T10:35:36Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(0, entry.getCompressedSize());
+ assertEquals("-lhd-", entry.getCompressionMethod());
+ assertEquals(0x0000, entry.getCrcValue());
+ assertEquals(77, entry.getOsId());
+ assertNull(entry.getUnixPermissionMode());
+ assertNull(entry.getUnixGroupId());
+ assertNull(entry.getUnixUserId());
+ assertEquals(0x0010, entry.getMsdosFileAttributes());
+ assertEquals(0xebe7, entry.getHeaderCrc());
- @Test
- void testGetPathnameNegativeLength() throws IOException, UnsupportedEncodingException {
- try (LhaArchiveInputStream is = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).get()) {
- try {
- is.getPathname(ByteBuffer.wrap(new byte[0]), -1);
- fail("Expected ArchiveException when pathname length is negative");
- } catch (ArchiveException e) {
- assertEquals("Pathname length is negative", e.getMessage());
- }
+ // Check file entry
+ entry = archive.getNextEntry();
+ assertNotNull(entry);
+ assertEquals("dir1/dir1-1/test1.txt", entry.getName());
+ assertFalse(entry.isDirectory());
+ assertEquals(14, entry.getSize());
+ assertEquals(1755081276000L, entry.getLastModifiedDate().getTime());
+ assertEquals(ZonedDateTime.parse("2025-08-13T10:34:36Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(14, entry.getCompressedSize());
+ assertEquals("-lh0-", entry.getCompressionMethod());
+ assertEquals(0xc9b4, entry.getCrcValue());
+ assertEquals(77, entry.getOsId());
+ assertNull(entry.getUnixPermissionMode());
+ assertNull(entry.getUnixGroupId());
+ assertNull(entry.getUnixUserId());
+ assertNull(entry.getMsdosFileAttributes());
+ assertEquals(0x214a, entry.getHeaderCrc());
+
+ // Check directory entry
+ entry = archive.getNextEntry();
+ assertNotNull(entry);
+ assertEquals("dir1/dir1-2/", entry.getName());
+ assertTrue(entry.isDirectory());
+ assertEquals(0, entry.getSize());
+ assertEquals(1755081341000L, entry.getLastModifiedDate().getTime());
+ assertEquals(ZonedDateTime.parse("2025-08-13T10:35:41Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(0, entry.getCompressedSize());
+ assertEquals("-lhd-", entry.getCompressionMethod());
+ assertEquals(0x0000, entry.getCrcValue());
+ assertEquals(77, entry.getOsId());
+ assertNull(entry.getUnixPermissionMode());
+ assertNull(entry.getUnixGroupId());
+ assertNull(entry.getUnixUserId());
+ assertEquals(0x0010, entry.getMsdosFileAttributes());
+ assertEquals(0x74ca, entry.getHeaderCrc());
+
+ // Check file entry
+ entry = archive.getNextEntry();
+ assertNotNull(entry);
+ assertEquals("dir1/dir1-2/test2.txt", entry.getName());
+ assertFalse(entry.isDirectory());
+ assertEquals(14, entry.getSize());
+ assertEquals(1755081276000L, entry.getLastModifiedDate().getTime());
+ assertEquals(ZonedDateTime.parse("2025-08-13T10:34:36Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(14, entry.getCompressedSize());
+ assertEquals("-lh0-", entry.getCompressionMethod());
+ assertEquals(0xc9b4, entry.getCrcValue());
+ assertEquals(77, entry.getOsId());
+ assertNull(entry.getUnixPermissionMode());
+ assertNull(entry.getUnixGroupId());
+ assertNull(entry.getUnixUserId());
+ assertNull(entry.getMsdosFileAttributes());
+ assertEquals(0x165f, entry.getHeaderCrc());
+
+ // No more entries expected
+ assertNull(archive.getNextEntry());
}
}
@Test
- void testGetPathnameTooLong() throws IOException, UnsupportedEncodingException {
- try (LhaArchiveInputStream is = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).get()) {
- try {
- final byte[] pathname = new byte[4097];
- is.getPathname(ByteBuffer.wrap(pathname), pathname.length);
- fail("Expected ArchiveException when pathname is longer than the maximum allowed");
- } catch (ArchiveException e) {
- assertEquals("Pathname is longer than the maximum allowed (4097 > 4096)", e.getMessage());
- }
+ void testParseHeaderLevel2FileWithMsdosAttributes() throws IOException {
+ // The lha file was generated by LHA32 v2.67.00 for Windows
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(newInputStream("test-msdos-l2-attrib.lha")).get()) {
+ // Check file entry
+ final LhaArchiveEntry entry = archive.getNextEntry();
+ assertNotNull(entry);
+ assertEquals("test1.txt", entry.getName());
+ assertFalse(entry.isDirectory());
+ assertEquals(14, entry.getSize());
+ assertEquals(1755081276000L, entry.getLastModifiedDate().getTime());
+ assertEquals(ZonedDateTime.parse("2025-08-13T10:34:36Z"), entry.getLastModifiedDate().toInstant().atZone(ZoneOffset.UTC));
+ assertEquals(14, entry.getCompressedSize());
+ assertEquals("-lh0-", entry.getCompressionMethod());
+ assertEquals(0xc9b4, entry.getCrcValue());
+ assertEquals(77, entry.getOsId());
+ assertNull(entry.getUnixPermissionMode());
+ assertNull(entry.getUnixGroupId());
+ assertNull(entry.getUnixUserId());
+ assertEquals(0x0021, entry.getMsdosFileAttributes());
+ assertEquals(0x14bb, entry.getHeaderCrc());
+
+ // No more entries expected
+ assertNull(archive.getNextEntry());
}
}
@Test
- void testGetPathnameInvalidLength() throws IOException, UnsupportedEncodingException {
- try (LhaArchiveInputStream is = LhaArchiveInputStream.builder().setInputStream(newEmptyInputStream()).get()) {
+ void testReadDataBeforeEntry() throws IOException {
+ final byte[] data = toByteArray(VALID_HEADER_LEVEL_0_FILE);
+
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
try {
- final byte[] pathname = new byte[] { 'a', 'b', 'c' };
- is.getPathname(ByteBuffer.wrap(pathname), pathname.length + 1);
- fail("Expected ArchiveException for invalid pathname length");
- } catch (ArchiveException e) {
- assertEquals("Invalid pathname length", e.getMessage());
+ IOUtils.toByteArray(archive);
+ fail("Expected IllegalStateException for reading data before entry");
+ } catch (IllegalStateException e) {
+ assertEquals("No current entry", e.getMessage());
}
}
}
@Test
- void testGetPathnameWindowsFileSeparatorCharIso88591() throws IOException, UnsupportedEncodingException {
- try (LhaArchiveInputStream is = LhaArchiveInputStream.builder()
- .setInputStream(newEmptyInputStream())
- .setCharset(StandardCharsets.ISO_8859_1)
- .setFileSeparatorChar('\\')
- .get()) {
-
- assertEquals("\u00E5\\\u00E4\\\u00F6.txt", getPathname(is, 0xe5, 0xff, 0xe4, 0xff, 0xf6, '.', 't', 'x', 't'));
- assertEquals("\u00E5\\\u00E4\\\u00F6.txt", getPathname(is, 0xe5, '\\', 0xe4, '\\', 0xf6, '.', 't', 'x', 't'));
- }
- }
-
- private static byte[] toByteArray(final int... data) {
- final byte[] bytes = new byte[data.length];
- for (int i = 0; i < data.length; i++) {
- bytes[i] = (byte) data[i];
- }
- return bytes;
- }
+ void testUnsupportedCompressionMethod() throws IOException {
+ final byte[] data = toByteArray(VALID_HEADER_LEVEL_0_FILE);
- private static ByteBuffer toByteBuffer(final int... data) {
- return ByteBuffer.wrap(toByteArray(data)).order(ByteOrder.LITTLE_ENDIAN);
- }
+ data[1] = (byte) 0x9c; // Change the header checksum
+ data[5] = 'a'; // Change the compression method to an unsupported value
- private String getPathname(final LhaArchiveInputStream is, final int... filepathBuffer) throws ArchiveException, UnsupportedEncodingException {
- return is.getPathname(ByteBuffer.wrap(toByteArray(filepathBuffer)), filepathBuffer.length);
- }
+ try (LhaArchiveInputStream archive = LhaArchiveInputStream.builder().setInputStream(new ByteArrayInputStream(data)).get()) {
+ final LhaArchiveEntry entry = archive.getNextEntry();
+ assertNotNull(entry);
+ assertEquals("-lha-", entry.getCompressionMethod());
- private InputStream newEmptyInputStream() {
- return new ByteArrayInputStream(new byte[0]);
- }
+ assertFalse(archive.canReadEntryData(entry));
- /**
- * The timestamp used in header level 0 and 1 entries has no time zone information and is
- * converted in the system default time zone. This method converts the date to UTC to verify
- * the timestamp in unit tests.
- *
- * @param date the date to convert
- * @return a ZonedDateTime in UTC
- */
- private ZonedDateTime convertSystemTimeZoneDateToUTC(final Date date) {
- return date.toInstant().atZone(ZoneId.systemDefault()).withZoneSameLocal(ZoneOffset.UTC);
+ try {
+ IOUtils.toByteArray(archive);
+ fail("Expected ArchiveException for unsupported compression method");
+ } catch (ArchiveException e) {
+ assertEquals("Unsupported compression method: -lha-", e.getMessage());
+ }
+ }
}
}