Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:
run: pip install emake
- name: Run lint
run: emake lint
env:
PYTHONUNBUFFERED: "1"

test-image:
name: Generate test ext4 image
Expand All @@ -49,6 +51,8 @@ jobs:
run: |
set -e
./_test_image.sh
env:
PYTHONUNBUFFERED: "1"
- uses: actions/upload-artifact@v6
with:
name: test.ext4
Expand Down Expand Up @@ -88,6 +92,8 @@ jobs:
- name: Run tests
shell: bash
run: emake test
env:
PYTHONUNBUFFERED: "1"

fuzz:
name: Fuzz
Expand All @@ -112,6 +118,8 @@ jobs:
- name: Run test
shell: bash
run: make fuzz
env:
PYTHONUNBUFFERED: "1"

build-sdist:
name: Build sdist
Expand All @@ -125,6 +133,8 @@ jobs:
- *install-emake
- name: Building sdist
run: emake build --sdist
env:
PYTHONUNBUFFERED: "1"
- uses: actions/upload-artifact@v6
with:
name: pip-sdist
Expand All @@ -148,6 +158,8 @@ jobs:
path: .
- name: Test wheel
run: emake test --wheel
env:
PYTHONUNBUFFERED: "1"
- uses: actions/upload-artifact@v6
with:
name: pip-wheel-none-any
Expand Down Expand Up @@ -188,6 +200,8 @@ jobs:
--arch ${{ matrix.arch }} \
--libc ${{ matrix.libc }} \
--python ${{ matrix.python }}
env:
PYTHONUNBUFFERED: "1"
- name: Download test.ext4
uses: actions/download-artifact@v8
with:
Expand All @@ -200,6 +214,8 @@ jobs:
--arch ${{ matrix.arch }} \
--libc ${{ matrix.libc }} \
--python ${{ matrix.python }}
env:
PYTHONUNBUFFERED: "1"
- uses: actions/upload-artifact@v6
with:
name: pip-wheel-${{ matrix.python }}-${{ matrix.arch }}-${{ matrix.libc }}
Expand Down
11 changes: 10 additions & 1 deletion ext4/block.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import errno
import io
import os
from typing import TYPE_CHECKING
from typing import (
TYPE_CHECKING,
final,
)

from ._compat import override

Expand All @@ -11,7 +14,10 @@
from .volume import Volume


@final
class BlockIOBlocks:
__slots__ = ("blockio", "_null_block")

def __init__(self, blockio: "BlockIO") -> None:
self.blockio: BlockIO = blockio
self._null_block: bytearray = bytearray(self.block_size)
Expand Down Expand Up @@ -41,7 +47,10 @@ def __getitem__(self, ee_block: int) -> bytearray | bytes:
return self._null_block


@final
class BlockIO(io.RawIOBase):
__slots__ = ("inode", "cursor", "blocks")

def __init__(self, inode: "Inode") -> None:
super().__init__()
self.inode: Inode = inode
Expand Down
2 changes: 2 additions & 0 deletions ext4/blockdescriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

@final
class BlockDescriptor(Ext4Struct):
__slots__ = ("bg_no",)

_pack_ = 1
# _anonymous_ = ("bg_reserved",)
_fields_ = [
Expand Down
7 changes: 7 additions & 0 deletions ext4/directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@


class DirectoryEntryStruct(Ext4Struct):
__slots__: tuple[str, ...] = ("directory",)

def __init__(self, directory: "Directory", offset: int) -> None:
self.directory: Directory = directory
super().__init__(directory.volume, offset)
Expand All @@ -39,6 +41,7 @@ def read_from_volume(self) -> None:


class DirectoryEntryBase(DirectoryEntryStruct):
__slots__: tuple[str, ...] = ()
@property
def name_bytes(self) -> bytes:
return bytes(self.name)[: self.name_len] # pyright: ignore[reportAny]
Expand All @@ -55,6 +58,7 @@ def is_fake_entry(self) -> bool:

@final
class DirectoryEntry(DirectoryEntryBase):
__slots__ = ()
_pack_ = 1
# _anonymous_ = ("l_i_reserved",)
_fields_ = [
Expand All @@ -67,6 +71,7 @@ class DirectoryEntry(DirectoryEntryBase):

@final
class DirectoryEntry2(DirectoryEntryBase):
__slots__ = ()
_pack_ = 1
# _anonymous_ = ("l_i_reserved",)
_fields_ = [
Expand All @@ -85,6 +90,7 @@ def is_fake_entry(self) -> bool:

@final
class DirectoryEntryTail(DirectoryEntryStruct):
__slots__ = ()
_pack_ = 1
# _anonymous_ = ("det_reserved_zero1", "det_reserved_zero2",)
_fields_ = [
Expand All @@ -107,6 +113,7 @@ def expected_magic(self) -> int:

@final
class DirectoryEntryHash(DirectoryEntryStruct):
__slots__ = ()
_pack_ = 1
# _anonymous_ = ("det_reserved_zero1", "det_reserved_zero2",)
_fields_ = [
Expand Down
13 changes: 13 additions & 0 deletions ext4/extent.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
from .volume import Volume


@final
class ExtentBlocks:
__slots__: tuple[str, ...] = ("extent", "_null_block")

def __init__(self, extent: "Extent") -> None:
self.extent: Extent = extent
self._null_block: bytearray = bytearray(self.block_size)
Expand Down Expand Up @@ -74,6 +77,8 @@ def __len__(self) -> int:

@final
class ExtentHeader(Ext4Struct):
__slots__ = ("tree", "indices", "extents", "tail")

_pack_ = 1
# _anonymous_ = ()
_fields_ = [
Expand Down Expand Up @@ -153,6 +158,8 @@ def checksum(self) -> int | None:

@final
class ExtentIndex(Ext4Struct):
__slots__ = ("ei_no", "header")

_pack_ = 1
# _anonymous_ = ("ei_unused",)
_fields_ = [
Expand Down Expand Up @@ -184,6 +191,8 @@ def inode(self) -> "Inode":

@final
class Extent(Ext4Struct):
__slots__ = ("ee_no", "header", "blocks")

_pack_ = 1
# _anonymous_ = ("ei_unused",)
_fields_ = [
Expand Down Expand Up @@ -231,6 +240,8 @@ def read(self) -> bytes:

@final
class ExtentTail(Ext4Struct):
__slots__ = ("header",)

_pack_ = 1
_fields_ = [
("et_checksum", c_uint32),
Expand All @@ -250,6 +261,8 @@ def inode(self) -> "Inode":


class ExtentTree:
__slots__: tuple[str, ...] = ("inode", "headers")

def __init__(self, inode: "Inode") -> None:
self.inode: Inode = inode
self.headers: list[ExtentHeader] = []
Expand Down
16 changes: 16 additions & 0 deletions ext4/htree.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@


class LittleEndianStructureWithVolume(LittleEndianStructure):
__slots__: tuple[str, ...] = ("_volume",)

def __init__(self) -> None:
super().__init__()
self._volume: Volume | None = None
Expand All @@ -47,6 +49,8 @@ def volume(self, volume: "Volume") -> None:

@final
class DotDirectoryEntry2(LittleEndianStructureWithVolume):
__slots__ = ()

_pack_ = 1
# _anonymous_ = ()
_fields_ = [
Expand Down Expand Up @@ -88,6 +92,8 @@ class DXRootInfo(LittleEndianStructure):


class DXBase(Ext4Struct):
__slots__: tuple[str, ...] = ("directory",)

def __init__(self, directory: "Directory", offset: int) -> None:
self.directory: Directory = directory
super().__init__(directory.volume, offset)
Expand All @@ -102,6 +108,8 @@ def read_from_volume(self) -> None:

@final
class DXEntry(DXBase):
__slots__ = ("index", "parent")

_pack_ = 1
# _anonymous_ = ("")
_fields_ = [
Expand All @@ -119,6 +127,8 @@ def __init__(self, parent: "DXEntriesBase", index: int) -> None:


class DXEntriesBase(DXBase):
__slots__: tuple[str, ...] = ()

@override
def read_from_volume(self) -> None:
super().read_from_volume()
Expand All @@ -141,6 +151,8 @@ def info_length(self) -> int:

@final
class DXRoot(DXEntriesBase):
__slots__ = ()

_pack_ = 1
# _anonymous_ = ("")
_fields_ = [
Expand Down Expand Up @@ -178,6 +190,8 @@ def magic(self) -> int:

@final
class DXNode(DXEntriesBase):
__slots__ = ()

_pack_ = 1
# _anonymous_ = ("")
_fields_ = [
Expand All @@ -196,6 +210,8 @@ def __init__(self, directory: "Directory", offset: int) -> None:

@final
class DXTail(DXBase):
__slots__ = ("parent",)

_pack_ = 1
# _anonymous_ = ("dt_reserved")
_fields_ = [
Expand Down
23 changes: 18 additions & 5 deletions ext4/inode.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ class Osd2(LittleEndianUnion):
class Inode(Ext4Struct):
EXT4_GOOD_OLD_INODE_SIZE: int = 128
EXT2_GOOD_OLD_INODE_SIZE: int = 128
__slots__: tuple[str, ...] = ("i_no", "tree")
_pack_ = 1 # pyright: ignore[reportUnannotatedClassAttribute]
_fields_ = [ # pyright: ignore[reportUnannotatedClassAttribute]
("i_mode", MODE.basetype),
Expand Down Expand Up @@ -451,26 +452,32 @@ def xattrs(
raise


@final
class UnknownInode(Inode):
pass
__slots__ = ()


@final
class Fifo(Inode):
pass
__slots__ = ()


@final
class CharacterDevice(Inode):
pass
__slots__ = ()


@final
class BlockDevice(Inode):
pass
__slots__ = ()


@final
class Socket(Inode):
pass
__slots__ = ()


@final
class File(Inode):
@override
def open(
Expand All @@ -479,7 +486,10 @@ def open(
return self._open(mode, encoding, newline)


@final
class SymbolicLink(Inode):
__slots__ = ()

@property
def is_fast_symlink(self) -> bool:
i_blocks_lo = assert_cast(self.i_blocks_lo, int) # pyright: ignore[reportAny]
Expand All @@ -498,7 +508,10 @@ def readlink(self) -> bytes:
return self.volume.read(self.i_size)


@final
class Directory(Inode):
__slots__ = ("_inode_at_cache", "_dirents", "htree")

def __init__(self, volume: Volume, offset: int, i_no: int) -> None:
super().__init__(volume, offset, i_no)
self._inode_at_cache: LRUCache[str | bytes, Inode] = LRUCache(maxsize=32)
Expand Down
2 changes: 2 additions & 0 deletions ext4/struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ def to_hex(data: int | list[int] | bytes | None) -> str:


class Ext4Struct(LittleEndianStructure):
__slots__: tuple[str, ...] = ("volume", "offset")

def __init__(self, volume: "Volume", offset: int) -> None:
super().__init__()
self.volume: Volume = volume
Expand Down
2 changes: 2 additions & 0 deletions ext4/superblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@

@final
class Superblock(Ext4Struct):
__slots__ = ()

_pack_ = 1
# _anonymous_ = (
# "s_reserved_pad",
Expand Down
Loading