diff --git a/dissect/extfs/__init__.py b/dissect/extfs/__init__.py index bf54547..9ac4452 100644 --- a/dissect/extfs/__init__.py +++ b/dissect/extfs/__init__.py @@ -8,11 +8,11 @@ from dissect.extfs.journal import JDB2 __all__ = [ - "ExtFS", - "INode", "JDB2", "Error", + "ExtFS", "FileNotFoundError", + "INode", "NotADirectoryError", "NotASymlinkError", ] diff --git a/dissect/extfs/c_ext.py b/dissect/extfs/c_ext.py index fd6cdf8..1bfbb2c 100644 --- a/dissect/extfs/c_ext.py +++ b/dissect/extfs/c_ext.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import stat from dissect.cstruct import cstruct diff --git a/dissect/extfs/c_jdb2.py b/dissect/extfs/c_jdb2.py index d5d6e41..b51cea7 100644 --- a/dissect/extfs/c_jdb2.py +++ b/dissect/extfs/c_jdb2.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dissect.cstruct import cstruct jdb2_def = """ diff --git a/dissect/extfs/extfs.py b/dissect/extfs/extfs.py index d7e89ea..03e4b15 100644 --- a/dissect/extfs/extfs.py +++ b/dissect/extfs/extfs.py @@ -4,9 +4,8 @@ import logging import os import stat -from datetime import datetime from functools import lru_cache -from typing import BinaryIO, Iterator, Optional, Union +from typing import TYPE_CHECKING, BinaryIO from uuid import UUID from dissect.util import ts @@ -29,6 +28,10 @@ ) from dissect.extfs.journal import JDB2 +if TYPE_CHECKING: + from collections.abc import Iterator + from datetime import datetime + log = logging.getLogger(__name__) log.setLevel(os.getenv("DISSECT_LOG_EXTFS", "CRITICAL")) @@ -114,7 +117,7 @@ def journal(self) -> JDB2: return self._journal - def get(self, path: Union[str, int], node: Optional[INode] = None) -> INode: + def get(self, path: str | int, node: INode | None = None) -> INode: if isinstance(path, int): return self.get_inode(path) @@ -139,9 +142,9 @@ def get(self, path: Union[str, int], node: Optional[INode] = None) -> INode: def get_inode( self, inum: int, - filename: Optional[str] = None, - filetype: Optional[int] = None, - parent: Optional[INode] = None, + filename: str | None = None, + filetype: int | None = None, + parent: INode | None = None, lazy: bool = False, ) -> INode: if inum < c_ext.EXT2_BAD_INO or inum > self.sb.s_inodes_count: @@ -181,9 +184,9 @@ def __init__( self, extfs: ExtFS, inum: int, - filename: Optional[str] = None, - filetype: Optional[int] = None, - parent: Optional[INode] = None, + filename: str | None = None, + filetype: int | None = None, + parent: INode | None = None, ): self.extfs = extfs self.inum = inum @@ -248,10 +251,7 @@ def link_inode(self) -> INode: if not self._link_inode: # Relative lookups work because . and .. are actual directory entries link = self.link - if link.startswith("/"): - relnode = None - else: - relnode = self.parent + relnode = None if link.startswith("/") else self.parent self._link_inode = self.extfs.get(self.link, relnode) return self._link_inode @@ -320,14 +320,14 @@ def dtime(self) -> datetime: return ts.from_unix(self.inode.i_dtime) @property - def crtime(self) -> Optional[datetime]: + def crtime(self) -> datetime | None: time_ns = self.crtime_ns if time_ns is None: return None return ts.from_unix_ns(time_ns) @property - def crtime_ns(self) -> Optional[int]: + def crtime_ns(self) -> int | None: if self.extfs.sb.s_inode_size <= 128: return None @@ -370,7 +370,7 @@ def iterdir(self) -> Iterator[INode]: offset += direntry.rec_len buf.seek(offset) - def dataruns(self) -> list[tuple[Optional[int], int]]: + def dataruns(self) -> list[tuple[int | None, int]]: if not self._runlist: expected_runs = (self.size + self.extfs.block_size - 1) // self.extfs.block_size @@ -446,7 +446,7 @@ def dataruns(self) -> list[tuple[Optional[int], int]]: return self._runlist def open(self) -> BinaryIO: - if self.inode.i_flags & c_ext.EXT4_INLINE_DATA_FL or self.filetype == stat.S_IFLNK and self.size < 60: + if self.inode.i_flags & c_ext.EXT4_INLINE_DATA_FL or (self.filetype == stat.S_IFLNK and self.size < 60): buf = io.BytesIO(memoryview(self.inode.i_block)[: self.size]) # Need to add a size attribute to maintain compatibility with dissect streams buf.size = self.size @@ -478,21 +478,21 @@ def _parse_indirect(inode: INode, offset: int, num_blocks: int, level: int) -> l return [0] * read_blocks inode.extfs.fh.seek(offset * inode.extfs.block_size) return c_ext.uint32[read_blocks](inode.extfs.fh) - else: - blocks = [] - max_level_blocks = offsets_per_block**level - blocks_per_nest = max_level_blocks // offsets_per_block - read_blocks = (num_blocks + blocks_per_nest - 1) // blocks_per_nest - read_blocks = min(read_blocks, offsets_per_block) + blocks = [] - inode.extfs.fh.seek(offset * inode.extfs.block_size) - for addr in c_ext.uint32[read_blocks](inode.extfs.fh): - parsed_blocks = _parse_indirect(inode, addr, num_blocks, level - 1) - num_blocks -= len(parsed_blocks) - blocks.extend(parsed_blocks) + max_level_blocks = offsets_per_block**level + blocks_per_nest = max_level_blocks // offsets_per_block + read_blocks = (num_blocks + blocks_per_nest - 1) // blocks_per_nest + read_blocks = min(read_blocks, offsets_per_block) + + inode.extfs.fh.seek(offset * inode.extfs.block_size) + for addr in c_ext.uint32[read_blocks](inode.extfs.fh): + parsed_blocks = _parse_indirect(inode, addr, num_blocks, level - 1) + num_blocks -= len(parsed_blocks) + blocks.extend(parsed_blocks) - return blocks + return blocks def _parse_extents(inode: INode, buf: bytes) -> Iterator[c_ext.ext4_extent]: diff --git a/dissect/extfs/journal.py b/dissect/extfs/journal.py index 5903741..03f5dd6 100644 --- a/dissect/extfs/journal.py +++ b/dissect/extfs/journal.py @@ -2,13 +2,16 @@ import datetime import io -from typing import BinaryIO, Iterator, Optional +from typing import TYPE_CHECKING, BinaryIO from dissect.util.stream import RangeStream from dissect.extfs.c_jdb2 import c_jdb2 from dissect.extfs.exceptions import Error +if TYPE_CHECKING: + from collections.abc import Iterator + class JDB2: def __init__(self, fh: BinaryIO): @@ -132,7 +135,7 @@ def __init__( jdb2: JDB2, header: c_jdb2.commit_header, journal_block: int, - descriptors: Optional[list[DescriptorBlock]] = None, + descriptors: list[DescriptorBlock] | None = None, ): self.jdb2 = jdb2 self.header = header @@ -140,7 +143,7 @@ def __init__( self.descriptors = descriptors if descriptors else [] self.sequence = self.header.h_sequence - self.ts = datetime.datetime.utcfromtimestamp(self.header.h_commit_sec) + self.ts = datetime.datetime.fromtimestamp(self.header.h_commit_sec, tz=datetime.timezone.utc) self.ts += datetime.timedelta(microseconds=self.header.h_commit_nsec // 1000) def __repr__(self) -> str: diff --git a/pyproject.toml b/pyproject.toml index 2112d28..0ee46cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,13 +41,56 @@ dev = [ "dissect.util>=3.0.dev,<4.0.dev", ] -[tool.black] +[tool.ruff] line-length = 120 +required-version = ">=0.9.0" -[tool.isort] -profile = "black" -known_first_party = ["dissect.extfs"] -known_third_party = ["dissect"] +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] +select = [ + "F", + "E", + "W", + "I", + "UP", + "YTT", + "ANN", + "B", + "C4", + "DTZ", + "T10", + "FA", + "ISC", + "G", + "INP", + "PIE", + "PYI", + "PT", + "Q", + "RSE", + "RET", + "SLOT", + "SIM", + "TID", + "TCH", + "PTH", + "PLC", + "TRY", + "FLY", + "PERF", + "FURB", + "RUF", +] +ignore = ["E203", "B904", "UP024", "ANN002", "ANN003", "ANN204", "ANN401", "SIM105", "TRY003"] + +[tool.ruff.lint.per-file-ignores] +"tests/docs/**" = ["INP001"] + +[tool.ruff.lint.isort] +known-first-party = ["dissect.extfs"] +known-third-party = ["dissect"] [tool.setuptools] license-files = ["LICENSE", "COPYRIGHT"] diff --git a/tests/conftest.py b/tests/conftest.py index 186ae65..932285e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,15 +1,20 @@ +from __future__ import annotations + import gzip -import os -from typing import BinaryIO, Iterator +from pathlib import Path +from typing import TYPE_CHECKING, BinaryIO import pytest +if TYPE_CHECKING: + from collections.abc import Iterator + -def absolute_path(filename) -> str: - return os.path.join(os.path.dirname(__file__), filename) +def absolute_path(filename: str) -> Path: + return Path(__file__).parent / filename -def gzip_file(filename) -> Iterator[BinaryIO]: +def gzip_file(filename: str) -> Iterator[BinaryIO]: with gzip.GzipFile(absolute_path(filename), "rb") as fh: yield fh diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 6c5fe36..87c419b 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,10 +1,12 @@ +from __future__ import annotations + import pytest from dissect.extfs import exceptions @pytest.mark.parametrize( - "exc, std", + ("exc", "std"), [ (exceptions.FileNotFoundError, FileNotFoundError), (exceptions.IsADirectoryError, IsADirectoryError), diff --git a/tests/test_ext4.py b/tests/test_ext4.py index c945ca5..671506b 100644 --- a/tests/test_ext4.py +++ b/tests/test_ext4.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import datetime import gzip import stat from io import BytesIO -from logging import Logger -from typing import BinaryIO +from typing import TYPE_CHECKING, BinaryIO from unittest.mock import call, patch import pytest @@ -11,8 +12,11 @@ from dissect.extfs.c_ext import c_ext from dissect.extfs.extfs import EXT4, ExtFS, INode +if TYPE_CHECKING: + from logging import Logger + -def test_ext4(ext4_bin: BinaryIO): +def test_ext4(ext4_bin: BinaryIO) -> None: extfs = ExtFS(ext4_bin) assert extfs.type == EXT4 @@ -43,7 +47,7 @@ def test_ext4(ext4_bin: BinaryIO): assert len(list(extfs.journal.commits())) == 2 -def test_xattr(ext4_bin: BinaryIO): +def test_xattr(ext4_bin: BinaryIO) -> None: e = ExtFS(ext4_bin) inode = e.get("xattr_cap") @@ -56,7 +60,7 @@ def test_xattr(ext4_bin: BinaryIO): assert xattrs[1].value == b"\x01\x00\x00\x02\x00\x04@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" -def test_sparse(ext4_sparse_bin: BinaryIO): +def test_sparse(ext4_sparse_bin: BinaryIO) -> None: extfs = ExtFS(ext4_sparse_bin) sparse_start = extfs.get("sparse_start") @@ -84,11 +88,11 @@ def test_sparse(ext4_sparse_bin: BinaryIO): ("tests/data/ext4_symlink_test3.bin.gz"), ], ) -def test_symlinks(image_file: str): +def test_symlinks(image_file: str) -> None: path = "/path/to/dir/with/file.ext" expect = b"resolved!\n" - def resolve(node): + def resolve(node: INode) -> INode: while node.filetype == stat.S_IFLNK: node = node.link_inode return node @@ -100,7 +104,7 @@ def resolve(node): @patch("dissect.extfs.extfs.INode.open", return_value=BytesIO(b"\x00" * 16)) @patch("dissect.extfs.extfs.log", create=True, return_value=None) @patch("dissect.extfs.extfs.ExtFS") -def test_infinite_loop_protection(ExtFS: ExtFS, log: Logger, *args): +def test_infinite_loop_protection(ExtFS: ExtFS, log: Logger, *args) -> None: ExtFS.sb.s_inodes_count = 69 ExtFS._dirtype = c_ext.ext2_dir_entry_2 inode = INode(ExtFS, 1, filetype=stat.S_IFDIR) diff --git a/tox.ini b/tox.ini index bfcf133..e82fbf9 100644 --- a/tox.ini +++ b/tox.ini @@ -32,23 +32,17 @@ commands = [testenv:fix] package = skip deps = - black==23.1.0 - isort==5.11.4 + ruff==0.9.2 commands = - black dissect tests - isort dissect tests + ruff format dissect tests [testenv:lint] package = skip deps = - black==23.1.0 - flake8 - flake8-black - flake8-isort - isort==5.11.4 + ruff==0.9.2 vermin commands = - flake8 dissect tests + ruff check dissect tests vermin -t=3.9- --no-tips --lint dissect tests [flake8]