diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5bd16d9..5c14a7b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -6,7 +6,7 @@ on: pull_request: workflow_dispatch: release: - types: [released] + types: [ released ] permissions: read-all jobs: lint: @@ -16,7 +16,6 @@ jobs: fail-fast: false matrix: python: &python-versions - - "3.10" - "3.11" - "3.12" - "3.13" @@ -61,7 +60,7 @@ jobs: test: name: Test on ${{ matrix.os }} python ${{ matrix.python }} runs-on: ${{ matrix.os }} - needs: [test-image] + needs: [ test-image ] strategy: fail-fast: false matrix: @@ -93,7 +92,6 @@ jobs: fail-fast: false matrix: python: - #- "3.10" # atheris appears to break - "3.11" - "3.12" - "3.13" @@ -112,7 +110,7 @@ jobs: build-sdist: name: Build pip package runs-on: ubuntu-latest - needs: [test, lint] + needs: [ test, lint ] steps: - name: Checkout the Git repository uses: actions/checkout@v6 @@ -133,7 +131,7 @@ jobs: build-any-wheel: name: Build wheel runs-on: ubuntu-latest - needs: [test, lint] + needs: [ test, lint ] steps: - name: Checkout the Git repository uses: actions/checkout@v6 @@ -153,8 +151,8 @@ jobs: if-no-files-found: error build-wheel: name: Build wheel - if: github.repository == 'Eeems/python-ext4' && github.event_name == 'release' && startsWith(github.ref, 'refs/tags') - needs: [lint, test] + # if: github.repository == 'Eeems/python-ext4' && github.event_name == 'release' && startsWith(github.ref, 'refs/tags') + needs: [ lint, test, test-image ] runs-on: ubuntu-latest strategy: fail-fast: false @@ -166,18 +164,31 @@ jobs: - ppc64le - aarch64 - armv7l + - riscv64 + - s390x libc: - glibc - # - musl + - musl steps: - name: Checkout the Git repository uses: actions/checkout@v6 - - name: Building package + - name: Building wheel run: ./wheel.sh env: python: ${{ matrix.python }} arch: ${{ matrix.arch }} libc: ${{ matrix.libc }} + - name: Download test.ext4 + uses: actions/download-artifact@v8 + with: + name: test.ext4 + path: . + - name: Testing wheel + run: ./test-wheel.sh + env: + python: ${{ matrix.python }} + arch: ${{ matrix.arch }} + libc: ${{ matrix.libc }} - uses: actions/upload-artifact@v6 with: name: pip-wheel-${{ matrix.python }}-${{ matrix.arch }}-${{ matrix.libc }} diff --git a/ext4/blockdescriptor.py b/ext4/blockdescriptor.py index 2e686a9..705f979 100644 --- a/ext4/blockdescriptor.py +++ b/ext4/blockdescriptor.py @@ -30,7 +30,7 @@ class BlockDescriptor(Ext4Struct): ("bg_free_blocks_count_lo", c_uint16), ("bg_free_inodes_count_lo", c_uint16), ("bg_used_dirs_count_lo", c_uint16), - ("bg_flags", EXT4_BG), + ("bg_flags", EXT4_BG.basetype), ("bg_exclude_bitmap_lo", c_uint32), ("bg_block_bitmap_csum_lo", c_uint16), ("bg_inode_bitmap_csum_lo", c_uint16), diff --git a/ext4/directory.py b/ext4/directory.py index c6f1508..f9f5511 100644 --- a/ext4/directory.py +++ b/ext4/directory.py @@ -73,13 +73,13 @@ class DirectoryEntry2(DirectoryEntryBase): ("inode", c_uint32), ("rec_len", c_uint16), ("name_len", c_uint8), - ("file_type", EXT4_FT), + ("file_type", EXT4_FT.basetype), ("name", c_char * EXT4_NAME_LEN), ] @DirectoryEntryBase.is_fake_entry.getter def is_fake_entry(self) -> bool: - file_type = assert_cast(self.file_type, EXT4_FT) # pyright: ignore[reportAny] + file_type = EXT4_FT(self.file_type) # pyright: ignore[reportAny] return super().is_fake_entry or file_type == EXT4_FT.DIR_CSUM diff --git a/ext4/enum.py b/ext4/enum.py index 3648f87..3378dd1 100644 --- a/ext4/enum.py +++ b/ext4/enum.py @@ -48,6 +48,7 @@ def __repr__(self) -> str: def TypedCEnumeration(_type: type["SimpleCData"]): # noqa: ANN201 class CEnumeration(_type, metaclass=TypedEnumerationType(_type)): # pyright: ignore[reportGeneralTypeIssues, reportUntypedBaseClass] # noqa: ANN201,PLW1641,PLW1641 _members_: dict[str, Any] = {} # pyright: ignore[reportExplicitAny] + basetype: type["SimpleCData"] = _type @override def __repr__(self) -> str: diff --git a/ext4/htree.py b/ext4/htree.py index afb8e60..b39d925 100644 --- a/ext4/htree.py +++ b/ext4/htree.py @@ -80,7 +80,7 @@ class DXRootInfo(LittleEndianStructure): # _anonymous_ = ("reserved_zero") _fields_ = [ ("reserved_zero", c_uint32), - ("hash_version", DX_HASH), + ("hash_version", DX_HASH.basetype), ("info_length", c_uint8), ("indirect_levels", c_uint8), ("unused_flags", c_uint8), diff --git a/ext4/inode.py b/ext4/inode.py index ad6a15d..a6a8e0d 100644 --- a/ext4/inode.py +++ b/ext4/inode.py @@ -7,7 +7,7 @@ from collections.abc import Generator from ctypes import ( LittleEndianStructure, - Union, + LittleEndianUnion, c_uint16, c_uint32, sizeof, @@ -15,7 +15,6 @@ from typing import ( TYPE_CHECKING, Any, - cast, final, ) @@ -103,7 +102,7 @@ class Masix1(LittleEndianStructure): @final -class Osd1(Union): +class Osd1(LittleEndianUnion): _pack_ = 1 _fields_ = [ ("linux1", Linux1), @@ -151,7 +150,7 @@ class Masix2(LittleEndianStructure): @final -class Osd2(Union): +class Osd2(LittleEndianUnion): _pack_ = 1 _fields_ = [ ("linux2", Linux2), @@ -165,7 +164,7 @@ class Inode(Ext4Struct): EXT2_GOOD_OLD_INODE_SIZE: int = 128 _pack_ = 1 # pyright: ignore[reportUnannotatedClassAttribute] _fields_ = [ # pyright: ignore[reportUnannotatedClassAttribute] - ("i_mode", MODE), + ("i_mode", MODE.basetype), ("i_uid", c_uint16), ("i_size_lo", c_uint32), ("i_atime", c_uint32), @@ -175,7 +174,7 @@ class Inode(Ext4Struct): ("i_gid", c_uint16), ("i_links_count", c_uint16), ("i_blocks_lo", c_uint32), - ("i_flags", EXT4_FL), + ("i_flags", EXT4_FL.basetype), ("osd1", Osd1), ("i_block", c_uint32 * 15), ("i_generation", c_uint32), @@ -197,12 +196,8 @@ class Inode(Ext4Struct): @classmethod def get_file_type(cls, volume: Volume, offset: int) -> EXT4_FT: _ = volume.seek(offset + Inode.i_mode.offset) - file_type = cast( - MODE, - Inode.field_type("i_mode").from_buffer_copy( # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType, reportOptionalMemberAccess] - volume.read(Inode.i_mode.size) - ) - & 0xF000, + file_type = MODE( + int.from_bytes(volume.read(Inode.i_mode.size), "little") & 0xF000 ) match file_type: case MODE.IFIFO: @@ -321,7 +316,7 @@ def seed(self) -> int: @Ext4Struct.checksum.getter def checksum(self) -> int | None: - s_creator_os: EXT4_OS = assert_cast(self.superblock.s_creator_os, EXT4_OS) # pyright: ignore[reportAny] + s_creator_os: EXT4_OS = EXT4_OS(self.superblock.s_creator_os) # pyright: ignore[reportAny] if s_creator_os != EXT4_OS.LINUX: return None @@ -358,7 +353,7 @@ def checksum(self) -> int | None: @Ext4Struct.expected_checksum.getter def expected_checksum(self) -> int | None: - s_creator_os = assert_cast(self.superblock.s_creator_os, EXT4_OS) # pyright: ignore[reportAny] + s_creator_os = EXT4_OS(self.superblock.s_creator_os) # pyright: ignore[reportAny] if s_creator_os != EXT4_OS.LINUX: return None @@ -378,7 +373,7 @@ def validate(self) -> None: self.tree.validate() def has_flag(self, flag: EXT4_FL | int) -> bool: - i_flags = assert_cast(self.i_flags, EXT4_FL) # pyright: ignore[reportAny] + i_flags = EXT4_FL(self.i_flags) # pyright: ignore[reportAny] return (i_flags & flag) != 0 @property @@ -610,7 +605,7 @@ def opendir( ) -> Generator[tuple[DirectoryEntry | DirectoryEntry2, EXT4_FT], Any, None]: # pyright: ignore[reportExplicitAny] for dirent in self._opendir(): if isinstance(dirent, DirectoryEntry2): - file_type = assert_cast(dirent.file_type, EXT4_FT) # pyright: ignore[reportAny] + file_type = EXT4_FT(dirent.file_type) # pyright: ignore[reportAny] if file_type == EXT4_FT.DIR_CSUM: continue diff --git a/ext4/superblock.py b/ext4/superblock.py index bae84c2..f63fc61 100644 --- a/ext4/superblock.py +++ b/ext4/superblock.py @@ -42,7 +42,7 @@ class Superblock(Ext4Struct): # "s_reserved_pad", # "s_reserved", # ) - _fields_ = [ # pyright: ignore[reportUnknownVariableType] + _fields_ = [ ("s_inodes_count", c_uint32), ("s_blocks_count_lo", c_uint32), ("s_r_blocks_count_lo", c_uint32), @@ -59,21 +59,21 @@ class Superblock(Ext4Struct): ("s_mnt_count", c_uint16), ("s_max_mnt_count", c_uint16), ("s_magic", c_uint16), # 0xEF53 - ("s_state", EXT4_FS), - ("s_errors", EXT4_ERRORS), + ("s_state", EXT4_FS.basetype), + ("s_errors", EXT4_ERRORS.basetype), ("s_minor_rev_level", c_uint16), ("s_lastcheck", c_uint32), ("s_checkinterval", c_uint32), - ("s_creator_os", EXT4_OS), - ("s_rev_level", EXT4_REV), + ("s_creator_os", EXT4_OS.basetype), + ("s_rev_level", EXT4_REV.basetype), ("s_def_resuid", c_uint16), ("s_def_resgid", c_uint16), ("s_first_ino", c_uint32), ("s_inode_size", c_uint16), ("s_block_group_nr", c_uint16), - ("s_feature_compat", EXT4_FEATURE_COMPAT), - ("s_feature_incompat", EXT4_FEATURE_INCOMPAT), - ("s_feature_ro_compat", EXT4_FEATURE_RO_COMPAT), + ("s_feature_compat", EXT4_FEATURE_COMPAT.basetype), + ("s_feature_incompat", EXT4_FEATURE_INCOMPAT.basetype), + ("s_feature_ro_compat", EXT4_FEATURE_RO_COMPAT.basetype), ("s_uuid", c_uint8 * 16), ("s_volume_name", c_ubyte * 16), ("s_last_mounted", c_ubyte * 64), @@ -86,10 +86,10 @@ class Superblock(Ext4Struct): ("s_journal_dev", c_uint32), ("s_last_orphan", c_uint32), ("s_hash_seed", c_uint32 * 4), - ("s_def_hash_version", DX_HASH), + ("s_def_hash_version", DX_HASH.basetype), ("s_jnl_backup_type", c_uint8), ("s_desc_size", c_uint16), - ("s_default_mount_opts", EXT4_DEFM), + ("s_default_mount_opts", EXT4_DEFM.basetype), ("s_first_meta_bg", c_uint32), ("s_mkfs_time", c_uint32), ("s_jnl_blocks", c_uint32 * 17), @@ -98,13 +98,13 @@ class Superblock(Ext4Struct): ("s_free_blocks_count_hi", c_uint32), ("s_min_extra_isize", c_uint16), ("s_want_extra_isize", c_uint16), - ("s_flags", EXT2_FLAGS), + ("s_flags", EXT2_FLAGS.basetype), ("s_raid_stride", c_uint16), ("s_mmp_interval", c_uint16), ("s_mmp_block", c_uint64), ("s_raid_stripe_width", c_uint32), ("s_log_groups_per_flex", c_uint8), - ("s_checksum_type", EXT4_CHKSUM), + ("s_checksum_type", EXT4_CHKSUM.basetype), ("s_reserved_pad", c_uint16), ("s_kbytes_written", c_uint64), ("s_snapshot_inum", c_uint32), @@ -122,12 +122,12 @@ class Superblock(Ext4Struct): ("s_last_error_line", c_uint32), ("s_last_error_block", c_uint64), ("s_last_error_func", c_uint8 * 32), - ("s_mount_opts", EXT4_MOUNT * 64), # pyright: ignore[reportOperatorIssue] + ("s_mount_opts", EXT4_MOUNT.basetype * 64), ("s_usr_quota_inum", c_uint32), ("s_grp_quota_inum", c_uint32), ("s_overhead_blocks", c_uint32), ("s_backup_bgs", c_uint32 * 2), - ("s_encrypt_algos", FS_ENCRYPTION_MODE * 4), # pyright: ignore[reportOperatorIssue] + ("s_encrypt_algos", FS_ENCRYPTION_MODE.basetype * 4), ("s_encrypt_pw_salt", c_uint8 * 16), ("s_lpf_ino", c_uint32), ("s_prj_quota_inum", c_uint32), @@ -206,21 +206,15 @@ def checksum(self) -> int | None: @property def feature_incompat(self) -> EXT4_FEATURE_INCOMPAT: - s_feature_incompat = assert_cast(self.s_feature_incompat, EXT4_FEATURE_INCOMPAT) # pyright: ignore[reportAny] - return s_feature_incompat + return EXT4_FEATURE_INCOMPAT(self.s_feature_incompat) # pyright: ignore[reportAny] @property def feature_compat(self) -> EXT4_FEATURE_COMPAT: - s_feature_compat = assert_cast(self.s_feature_compat, EXT4_FEATURE_COMPAT) # pyright: ignore[reportAny] - return s_feature_compat + return EXT4_FEATURE_COMPAT(self.s_feature_compat) # pyright: ignore[reportAny] @property def feature_ro_compat(self) -> EXT4_FEATURE_RO_COMPAT: - s_feature_ro_compat = assert_cast( - self.s_feature_ro_compat, # pyright: ignore[reportAny] - EXT4_FEATURE_RO_COMPAT, - ) - return s_feature_ro_compat + return EXT4_FEATURE_RO_COMPAT(self.s_feature_ro_compat) # pyright: ignore[reportAny] @property def seed(self) -> int: diff --git a/pyproject.toml b/pyproject.toml index aa19573..83b3a60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [project] name = "ext4" -version = "1.3.1" +version = "1.3.2" authors = [ { name="Eeems", email="eeems@eeems.email" }, ] description = "Library for read only interactions with an ext4 filesystem" -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", @@ -15,7 +15,6 @@ classifiers = [ "Operating System :: Microsoft :: Windows", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -37,9 +36,7 @@ dev = [ 'ruff', 'basedpyright', ] -test = [ - "pytest", -] +test = [] fuzz = [ "atheris", ] diff --git a/test-wheel.sh b/test-wheel.sh new file mode 100755 index 0000000..7bf853e --- /dev/null +++ b/test-wheel.sh @@ -0,0 +1,61 @@ +#!/bin/bash +set -e +libc=${libc:-glibc} +arch=${arch:-x86_64} +python=${python:-3.11} + +wheel="$(find wheelhouse -name "*_${arch}.whl" | head -n1)" +if [[ -z "$wheel" ]]; then + echo "No wheel found for architecture $arch" + exit 1 +fi +script=$( + cat < None: # pyr print(f" {debug()}", file=sys.stderr) +def _not_raises(source: str, debug: Callable[[], Any] | None = None) -> None: # pyright: ignore[reportExplicitAny] + global FAILED # noqa: PLW0603 + print(f"check {source} does not raise exception: ", end="") + try: + _ = eval(source) # noqa: S307 # pyright: ignore[reportAny] + print("pass") + + except Exception: + FAILED = True # pyright: ignore[reportConstantRedefinition] + print("fail") + if debug is not None: + print(f" {debug()}", file=sys.stderr) + + def test_magic_error(f: BufferedReader) -> None: global FAILED # noqa: PLW0603 try: @@ -189,7 +203,10 @@ def test_root_inode(volume: ext4.Volume) -> None: htree = volume.root.htree _assert("htree is not None") if htree is not None: - _assert("isinstance(htree.dot, ext4.DotDirectoryEntry2)", lambda: htree.dot) # pyright: ignore[reportOptionalMemberAccess, reportAny] + _assert( + "isinstance(htree.dot, ext4.DotDirectoryEntry2)", + lambda: htree.dot, # pyright: ignore[reportOptionalMemberAccess, reportAny] + ) _assert( "isinstance(htree.dotdot, ext4.DotDirectoryEntry2)", lambda: htree.dotdot, # pyright: ignore[reportOptionalMemberAccess, reportAny] @@ -208,7 +225,11 @@ def test_root_inode(volume: ext4.Volume) -> None: dx_root_info = htree.dx_root_info # pyright: ignore[reportAny] _assert( - "isinstance(dx_root_info.hash_version, ext4.DX_HASH)", + "isinstance(dx_root_info.hash_version, int)", + lambda: dx_root_info.hash_version, # pyright: ignore[reportAny] + ) + _not_raises( + "ext4.DX_HASH(dx_root_info.hash_version)", lambda: dx_root_info.hash_version, # pyright: ignore[reportAny] ) _assert("dx_root_info.info_length == 8", lambda: dx_root_info.info_length) # pyright: ignore[reportAny] diff --git a/wheel.sh b/wheel.sh index 91f67ff..85e6db2 100755 --- a/wheel.sh +++ b/wheel.sh @@ -23,6 +23,8 @@ if [[ "$libc" == "musl" ]]; then image="musllinux_1_2_$arch" elif [[ "$arch" == "armv7l" ]]; then image="manylinux_2_35_$arch" +elif [[ "$arch" == "riscv64" ]]; then + image="manylinux_2_39_$arch" else image="manylinux_2_34_$arch" fi