From f63407cfc6c4e045c75593abb9122ede78972423 Mon Sep 17 00:00:00 2001 From: Nathaniel van Diepen Date: Thu, 2 Apr 2026 16:00:26 -0600 Subject: [PATCH 1/5] Add symlink testing --- _test_image.sh | 1 + test.py | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/_test_image.sh b/_test_image.sh index 4aee969..814a428 100755 --- a/_test_image.sh +++ b/_test_image.sh @@ -28,6 +28,7 @@ trap "rm -r \"$tmp_dir\"" EXIT echo "[test] Using temporary directory: $tmp_dir" echo "[test] Generating files..." echo "hello world" >"$tmp_dir"/test.txt +ln -s test.txt "$tmp_dir"/symlink.txt for i in {1..1000}; do echo "echo "hello world" >>'$tmp_dir/test.txt'" done | xargs -P "$(nproc)" -I {} bash -c '{}' diff --git a/test.py b/test.py index 7f9011a..3aac1b5 100644 --- a/test.py +++ b/test.py @@ -28,8 +28,8 @@ def test_path_tuple(path: str | bytes, expected: tuple[bytes, ...]) -> None: except Exception as e: FAILED = True # pyright: ignore[reportConstantRedefinition] print("fail") - print(" ", end="") - print(e) + print(" ", end="", file=sys.stderr) + print(e, file=sys.stderr) def _eval_or_False(source: str) -> Any: # pyright: ignore[reportExplicitAny, reportAny] # noqa: ANN401 @@ -51,7 +51,7 @@ def _assert(source: str, debug: Callable[[], Any] | None = None) -> None: # pyr FAILED = True # pyright: ignore[reportConstantRedefinition] print("fail") if debug is not None: - print(f" {debug()}") + print(f" {debug()}", file=sys.stderr) def test_magic_error(f: BufferedReader) -> None: @@ -68,8 +68,8 @@ def test_magic_error(f: BufferedReader) -> None: except Exception as e: FAILED = True # pyright: ignore[reportConstantRedefinition] print("fail") - print(" ", end="") - print(e) + print(" ", end="", file=sys.stderr) + print(e, file=sys.stderr) def test_root_inode(volume: ext4.Volume) -> None: @@ -82,8 +82,8 @@ def test_root_inode(volume: ext4.Volume) -> None: except ext4.struct.ChecksumError as e: FAILED = True # pyright: ignore[reportConstantRedefinition] print("fail") - print(" ", end="") - print(e) + print(" ", end="", file=sys.stderr) + print(e, file=sys.stderr) print("check ext4.Volume stream validation: ", end="") @@ -98,8 +98,8 @@ def test_root_inode(volume: ext4.Volume) -> None: except Exception as e: FAILED = True # pyright: ignore[reportConstantRedefinition] print("fail") - print(" ", end="") - print(e) + print(" ", end="", file=sys.stderr) + print(e, file=sys.stderr) test_path_tuple("/", tuple()) test_path_tuple(b"/", tuple()) @@ -162,6 +162,10 @@ def test_root_inode(volume: ext4.Volume) -> None: _ = b.seek(0) _assert(f"b.read({x}) == {data[:x]}", lambda: b.seek(0) == 0 and b.read(x)) + inode = cast(ext4.SymbolicLink, volume.inode_at("/symlink.txt")) + _assert("isinstance(inode, ext4.SymbolicLink)") + _assert('inode.readlink() == b"test.txt"', inode.readlink) + img_file = "test_htree.ext4" print(f"Testing image: {img_file}") with open(img_file, "rb") as f: From c82f129bcaa052cefd02db0d7ef98d06b4f7a564 Mon Sep 17 00:00:00 2001 From: Nathaniel van Diepen Date: Thu, 2 Apr 2026 16:11:29 -0600 Subject: [PATCH 2/5] Handle fast symlinks --- ext4/block.py | 1 - ext4/blockdescriptor.py | 1 - ext4/directory.py | 1 - ext4/enum.py | 1 - ext4/extent.py | 1 - ext4/htree.py | 1 - ext4/inode.py | 12 ++++++++++-- ext4/struct.py | 1 - ext4/superblock.py | 1 - pyproject.toml | 1 + 10 files changed, 11 insertions(+), 10 deletions(-) diff --git a/ext4/block.py b/ext4/block.py index 07948d4..03c7e17 100644 --- a/ext4/block.py +++ b/ext4/block.py @@ -1,4 +1,3 @@ -# pyright: reportImportCycles=false import errno import io import os diff --git a/ext4/blockdescriptor.py b/ext4/blockdescriptor.py index cf87af3..2e686a9 100644 --- a/ext4/blockdescriptor.py +++ b/ext4/blockdescriptor.py @@ -1,4 +1,3 @@ -# pyright: reportImportCycles=false from ctypes import ( c_uint16, c_uint32, diff --git a/ext4/directory.py b/ext4/directory.py index 42b4782..c6f1508 100644 --- a/ext4/directory.py +++ b/ext4/directory.py @@ -1,4 +1,3 @@ -# pyright: reportImportCycles=false from ctypes import ( addressof, c_char, diff --git a/ext4/enum.py b/ext4/enum.py index aa11e24..3648f87 100644 --- a/ext4/enum.py +++ b/ext4/enum.py @@ -1,4 +1,3 @@ -# pyright: reportImportCycles=false from ctypes import ( c_uint8, c_uint16, diff --git a/ext4/extent.py b/ext4/extent.py index 397d5eb..148f4f6 100644 --- a/ext4/extent.py +++ b/ext4/extent.py @@ -1,4 +1,3 @@ -# pyright: reportImportCycles=false from collections.abc import Iterator from ctypes import ( c_uint16, diff --git a/ext4/htree.py b/ext4/htree.py index d418d95..afb8e60 100644 --- a/ext4/htree.py +++ b/ext4/htree.py @@ -1,4 +1,3 @@ -# pyright: reportImportCycles=false import warnings from collections.abc import Generator from ctypes import ( diff --git a/ext4/inode.py b/ext4/inode.py index ab2d52a..de60960 100644 --- a/ext4/inode.py +++ b/ext4/inode.py @@ -1,4 +1,3 @@ -# pyright: reportImportCycles=false from __future__ import annotations import errno @@ -482,8 +481,17 @@ def open( class SymbolicLink(Inode): + @property + def is_fast_symlink(self) -> bool: + i_blocks_lo = assert_cast(self.i_blocks_lo, int) # pyright: ignore[reportAny] + return i_blocks_lo == 0 and not self.is_inline + def readlink(self) -> bytes: - return self._open().read() + if not self.is_fast_symlink: + return self._open().read() + + _ = self.volume.seek(self.offset + Inode.i_block.offset) + return self.volume.read(self.i_size) class Directory(Inode): diff --git a/ext4/struct.py b/ext4/struct.py index 92e4883..1b80648 100644 --- a/ext4/struct.py +++ b/ext4/struct.py @@ -1,4 +1,3 @@ -# pyright: reportImportCycles=false import ctypes import errno import warnings diff --git a/ext4/superblock.py b/ext4/superblock.py index 635c54e..bae84c2 100644 --- a/ext4/superblock.py +++ b/ext4/superblock.py @@ -1,4 +1,3 @@ -# pyright: reportImportCycles=false from ctypes import ( c_ubyte, c_uint8, diff --git a/pyproject.toml b/pyproject.toml index bad0b07..5c88df0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,3 +91,4 @@ ignore = [ [tool.pyright] exclude = [".venv", "build"] reportMissingTypeStubs = false +reportImportCycles = false From f1434a85bc50f6fd3328d5c6b525b70a63188e21 Mon Sep 17 00:00:00 2001 From: Nathaniel van Diepen Date: Thu, 2 Apr 2026 16:12:08 -0600 Subject: [PATCH 3/5] Bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5c88df0..aa19573 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ext4" -version = "1.3" +version = "1.3.1" authors = [ { name="Eeems", email="eeems@eeems.email" }, ] From b251346224be993529adb8497303ad8d7f5aff9a Mon Sep 17 00:00:00 2001 From: Nathaniel van Diepen Date: Thu, 2 Apr 2026 16:27:08 -0600 Subject: [PATCH 4/5] Fix test image generation --- _test_image.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_test_image.sh b/_test_image.sh index 814a428..259afd2 100755 --- a/_test_image.sh +++ b/_test_image.sh @@ -44,7 +44,7 @@ done | xargs -P "$(nproc)" -I {} bash -c '{}' mkimage test32 "$tmp_dir" 20 -O ^64bit mkimage test64 "$tmp_dir" 20 -O 64bit -rm -f "$tmp_dir"/test*.txt +rm -f "$tmp_dir"/test*.txt "$tmp_dir"/symlink.txt echo "[test] Generating files..." echo "[test] Making image test_htree..." From faa70f078e76c5aeb665e0071b1767f4f397782d Mon Sep 17 00:00:00 2001 From: Nathaniel van Diepen Date: Thu, 2 Apr 2026 16:33:28 -0600 Subject: [PATCH 5/5] Handle when the i_size is malformed and reading a fast symlink --- ext4/inode.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ext4/inode.py b/ext4/inode.py index de60960..ad6a15d 100644 --- a/ext4/inode.py +++ b/ext4/inode.py @@ -73,6 +73,10 @@ class InodeError(Exception): pass +class MalformedInodeError(Exception): + pass + + @final class Linux1(LittleEndianStructure): _pack_ = 1 @@ -490,6 +494,11 @@ def readlink(self) -> bytes: if not self.is_fast_symlink: return self._open().read() + if self.i_size > Inode.i_block.size: + raise MalformedInodeError( + f"Fast symlink target too large: {self.i_size} > {Inode.i_block.size}" + ) + _ = self.volume.seek(self.offset + Inode.i_block.offset) return self.volume.read(self.i_size)