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());
+            }
+        }
     }
 }