diff --git a/poetry.lock b/poetry.lock index 39643e0323..e1db192a97 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1718,29 +1718,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.8.6" +version = "0.9.1" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.8.6-py3-none-linux_armv6l.whl", hash = "sha256:defed167955d42c68b407e8f2e6f56ba52520e790aba4ca707a9c88619e580e3"}, - {file = "ruff-0.8.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:54799ca3d67ae5e0b7a7ac234baa657a9c1784b48ec954a094da7c206e0365b1"}, - {file = "ruff-0.8.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e88b8f6d901477c41559ba540beeb5a671e14cd29ebd5683903572f4b40a9807"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0509e8da430228236a18a677fcdb0c1f102dd26d5520f71f79b094963322ed25"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a7ddb221779871cf226100e677b5ea38c2d54e9e2c8ed847450ebbdf99b32d"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:248b1fb3f739d01d528cc50b35ee9c4812aa58cc5935998e776bf8ed5b251e75"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:bc3c083c50390cf69e7e1b5a5a7303898966be973664ec0c4a4acea82c1d4315"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52d587092ab8df308635762386f45f4638badb0866355b2b86760f6d3c076188"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61323159cf21bc3897674e5adb27cd9e7700bab6b84de40d7be28c3d46dc67cf"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ae4478b1471fc0c44ed52a6fb787e641a2ac58b1c1f91763bafbc2faddc5117"}, - {file = "ruff-0.8.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0c000a471d519b3e6cfc9c6680025d923b4ca140ce3e4612d1a2ef58e11f11fe"}, - {file = "ruff-0.8.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9257aa841e9e8d9b727423086f0fa9a86b6b420fbf4bf9e1465d1250ce8e4d8d"}, - {file = "ruff-0.8.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45a56f61b24682f6f6709636949ae8cc82ae229d8d773b4c76c09ec83964a95a"}, - {file = "ruff-0.8.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:496dd38a53aa173481a7d8866bcd6451bd934d06976a2505028a50583e001b76"}, - {file = "ruff-0.8.6-py3-none-win32.whl", hash = "sha256:e169ea1b9eae61c99b257dc83b9ee6c76f89042752cb2d83486a7d6e48e8f764"}, - {file = "ruff-0.8.6-py3-none-win_amd64.whl", hash = "sha256:f1d70bef3d16fdc897ee290d7d20da3cbe4e26349f62e8a0274e7a3f4ce7a905"}, - {file = "ruff-0.8.6-py3-none-win_arm64.whl", hash = "sha256:7d7fc2377a04b6e04ffe588caad613d0c460eb2ecba4c0ccbbfe2bc973cbc162"}, - {file = "ruff-0.8.6.tar.gz", hash = "sha256:dcad24b81b62650b0eb8814f576fc65cfee8674772a6e24c9b747911801eeaa5"}, + {file = "ruff-0.9.1-py3-none-linux_armv6l.whl", hash = "sha256:84330dda7abcc270e6055551aca93fdde1b0685fc4fd358f26410f9349cf1743"}, + {file = "ruff-0.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3cae39ba5d137054b0e5b472aee3b78a7c884e61591b100aeb544bcd1fc38d4f"}, + {file = "ruff-0.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50c647ff96f4ba288db0ad87048257753733763b409b2faf2ea78b45c8bb7fcb"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0c8b149e9c7353cace7d698e1656ffcf1e36e50f8ea3b5d5f7f87ff9986a7ca"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:beb3298604540c884d8b282fe7625651378e1986c25df51dec5b2f60cafc31ce"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39d0174ccc45c439093971cc06ed3ac4dc545f5e8bdacf9f067adf879544d969"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69572926c0f0c9912288915214ca9b2809525ea263603370b9e00bed2ba56dbd"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:937267afce0c9170d6d29f01fcd1f4378172dec6760a9f4dface48cdabf9610a"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:186c2313de946f2c22bdf5954b8dd083e124bcfb685732cfb0beae0c47233d9b"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f94942a3bb767675d9a051867c036655fe9f6c8a491539156a6f7e6b5f31831"}, + {file = "ruff-0.9.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:728d791b769cc28c05f12c280f99e8896932e9833fef1dd8756a6af2261fd1ab"}, + {file = "ruff-0.9.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f312c86fb40c5c02b44a29a750ee3b21002bd813b5233facdaf63a51d9a85e1"}, + {file = "ruff-0.9.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ae017c3a29bee341ba584f3823f805abbe5fe9cd97f87ed07ecbf533c4c88366"}, + {file = "ruff-0.9.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5dc40a378a0e21b4cfe2b8a0f1812a6572fc7b230ef12cd9fac9161aa91d807f"}, + {file = "ruff-0.9.1-py3-none-win32.whl", hash = "sha256:46ebf5cc106cf7e7378ca3c28ce4293b61b449cd121b98699be727d40b79ba72"}, + {file = "ruff-0.9.1-py3-none-win_amd64.whl", hash = "sha256:342a824b46ddbcdddd3abfbb332fa7fcaac5488bf18073e841236aadf4ad5c19"}, + {file = "ruff-0.9.1-py3-none-win_arm64.whl", hash = "sha256:1cd76c7f9c679e6e8f2af8f778367dca82b95009bc7b1a85a47f1521ae524fa7"}, + {file = "ruff-0.9.1.tar.gz", hash = "sha256:fd2b25ecaf907d6458fa842675382c8597b3c746a2dde6717fe3415425df0c17"}, ] [[package]] @@ -2033,4 +2033,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "8e2d92ed223b35b464f9ae8f1e29bc41803262c6adec2aef3f82d4b6e29beff5" +content-hash = "885535ffe9562349c653c24d1dbbfe51041b3aa466826b5c22ba543931a5e8b8" diff --git a/pyproject.toml b/pyproject.toml index b09267bb00..a6d93127b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ pyright = "^1.1.349" pytest = "^8.0.0" pytest-cov = ">=3,<7" pyyaml = "^6.0.1" -ruff = "0.8.6" +ruff = "0.9.1" taplo = "^0.9.3" [tool.poetry.group.docs] @@ -90,12 +90,14 @@ select = [ "T10", # flake8-debugger "T20", # flake8-print "TC", # flake8-type-checking + "TID251", # flake8-tidy-imports; banned-api "TRY", # tryceratops "UP", # pyupgrade "W", # pycodestyle (warnings) ] ignore = [ + "A005", # stdlib-module-shadowing: E.g. `unblog.logging` collides with stdlib `logging`. It's okay for us "B027", # empty-method-without-abstract-decorator: It is okay to have empty methods in abstract classes "D1", # undocumented-*: We are not documenting every public symbol "D203", # one-blank-line-before-class: D211 (no-blank-line-before-class) is used instead @@ -143,6 +145,9 @@ fixture-parentheses = false mark-parentheses = false parametrize-names-type = "csv" +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"attr".msg = "Use `attrs` (with an 's') instead" + [tool.pytest.ini_options] addopts = "--cov=unblob --cov=tests --cov-branch --cov-fail-under=90" norecursedirs = """ diff --git a/tests/test_cli.py b/tests/test_cli.py index 29c5cafdbd..a0ccd1d2ba 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -377,9 +377,9 @@ def test_skip_extraction( assert result.exit_code == 0 process_file_mock.assert_called_once() - assert ( - process_file_mock.call_args.args[0].skip_extraction == skip_extraction - ), fail_message + assert process_file_mock.call_args.args[0].skip_extraction == skip_extraction, ( + fail_message + ) @pytest.mark.parametrize( diff --git a/tests/test_finder.py b/tests/test_finder.py index d798cd2754..525aa13a4a 100644 --- a/tests/test_finder.py +++ b/tests/test_finder.py @@ -1,4 +1,4 @@ -import attr +import attrs import pytest from pyperscan import Scan @@ -233,4 +233,4 @@ def test_search_chunks(content, expected_chunks, task_result): assert len(chunks) == len(expected_chunks) for expected_chunk, chunk in zip(expected_chunks, chunks): - assert attr.evolve(chunk, id="") == attr.evolve(expected_chunk, id="") + assert attrs.evolve(chunk, id="") == attrs.evolve(expected_chunk, id="") diff --git a/tests/test_processing.py b/tests/test_processing.py index 795d9bfd7c..1a141a3ca1 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -6,7 +6,7 @@ from statistics import mean from typing import Optional, TypeVar -import attr +import attrs import pytest from unblob import handlers @@ -54,7 +54,7 @@ def assert_same_chunks(expected, actual, explanation=None): """Assert ignoring the chunk.id-s.""" assert len(expected) == len(actual), explanation for e, a in zip(expected, actual): - assert attr.evolve(e, id="") == attr.evolve(a, id=""), explanation + assert attrs.evolve(e, id="") == attrs.evolve(a, id=""), explanation @pytest.mark.parametrize( diff --git a/unblob/cli.py b/unblob/cli.py index 696ae883ca..92c1a1a30b 100755 --- a/unblob/cli.py +++ b/unblob/cli.py @@ -177,7 +177,7 @@ def __init__( help=f"""Skip processing files with given magic prefix. The provided values are appended to unblob's own skip magic list unless --clear-skip-magic is provided. - [default: {', '.join(DEFAULT_SKIP_MAGIC)}] + [default: {", ".join(DEFAULT_SKIP_MAGIC)}] """, multiple=True, ) @@ -481,12 +481,12 @@ def print_report(reports: ProcessResult): chunks_distribution.items(), key=lambda item: item[1], reverse=True ): chunks_table.add_row( - handler.upper(), human_size(size), f"{(size/total_size) * 100:0.2f}%" + handler.upper(), human_size(size), f"{(size / total_size) * 100:0.2f}%" ) console.print(chunks_table) console.print( - f"Chunk identification ratio: [#00FFC8]{(valid_size/total_size) * 100:0.2f}%[/#00FFC8]" + f"Chunk identification ratio: [#00FFC8]{(valid_size / total_size) * 100:0.2f}%[/#00FFC8]" ) if len(reports.errors): diff --git a/unblob/dependencies.py b/unblob/dependencies.py index ed05a9aef6..e3e88be725 100644 --- a/unblob/dependencies.py +++ b/unblob/dependencies.py @@ -1,11 +1,11 @@ import shutil -import attr +import attrs from .models import DirectoryHandlers, Handlers -@attr.define +@attrs.define class Dependency: command: str is_installed: bool diff --git a/unblob/finder.py b/unblob/finder.py index 21498d6bab..e5f68c7f9d 100644 --- a/unblob/finder.py +++ b/unblob/finder.py @@ -6,7 +6,7 @@ from functools import lru_cache from typing import Optional -import attr +import attrs from pyperscan import Flag, Pattern, Scan, StreamDatabase from structlog import get_logger @@ -19,7 +19,7 @@ logger = get_logger() -@attr.define +@attrs.define class HyperscanMatchContext: file: File file_size: int diff --git a/unblob/handlers/archive/cpio.py b/unblob/handlers/archive/cpio.py index fbd9553550..35644c97e1 100644 --- a/unblob/handlers/archive/cpio.py +++ b/unblob/handlers/archive/cpio.py @@ -4,7 +4,7 @@ from pathlib import Path from typing import Optional -import attr +import attrs from structlog import get_logger from ...file_utils import ( @@ -110,7 +110,7 @@ """ -@attr.define +@attrs.define class CPIOEntry: start_offset: int size: int diff --git a/unblob/handlers/archive/qnap/qnap_nas.py b/unblob/handlers/archive/qnap/qnap_nas.py index fdadfef0b5..f8ab9fd2c5 100644 --- a/unblob/handlers/archive/qnap/qnap_nas.py +++ b/unblob/handlers/archive/qnap/qnap_nas.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import Optional -import attr +import attrs from pyperscan import Flag, Pattern, Scan, StreamDatabase from structlog import get_logger @@ -36,7 +36,7 @@ ] -@attr.define +@attrs.define class QTSSearchContext: start_offset: int file: File diff --git a/unblob/handlers/archive/tar.py b/unblob/handlers/archive/tar.py index 1c920dbb4e..cd0897e57f 100644 --- a/unblob/handlers/archive/tar.py +++ b/unblob/handlers/archive/tar.py @@ -206,7 +206,7 @@ def _padded_field(re_content_char, size, leftpad_re=" ", rightpad_re=r"[ \0x00]" field_regexes = [] for padsize in range(size): - content_re = f"{re_content_char}{{{size-padsize}}}" + content_re = f"{re_content_char}{{{size - padsize}}}" for leftpadsize in range(padsize + 1): rightpadsize = padsize - leftpadsize diff --git a/unblob/handlers/compression/_gzip_reader.py b/unblob/handlers/compression/_gzip_reader.py index 5cf76b6cbc..0a9ac66a8b 100644 --- a/unblob/handlers/compression/_gzip_reader.py +++ b/unblob/handlers/compression/_gzip_reader.py @@ -30,8 +30,7 @@ def read(self): break if buf == b"": raise EOFError( - "Compressed file ended before the " - "end-of-stream marker was reached" + "Compressed file ended before the end-of-stream marker was reached" ) self._add_read_data(uncompress) diff --git a/unblob/handlers/compression/bzip2.py b/unblob/handlers/compression/bzip2.py index 30646c802d..46bbb39c49 100644 --- a/unblob/handlers/compression/bzip2.py +++ b/unblob/handlers/compression/bzip2.py @@ -1,6 +1,6 @@ from typing import Optional -import attr +import attrs from pyperscan import Flag, Pattern, Scan, StreamDatabase from structlog import get_logger @@ -63,7 +63,7 @@ def build_stream_end_scan_db(pattern_list): parser = StructParser(C_DEFINITIONS) -@attr.define +@attrs.define class Bzip2SearchContext: start_offset: int file: File diff --git a/unblob/handlers/compression/xz.py b/unblob/handlers/compression/xz.py index f047a42886..9bf7d0408e 100644 --- a/unblob/handlers/compression/xz.py +++ b/unblob/handlers/compression/xz.py @@ -1,7 +1,7 @@ import io from typing import Optional -import attr +import attrs from pyperscan import Flag, Pattern, Scan, StreamDatabase from structlog import get_logger @@ -61,7 +61,7 @@ def build_stream_end_scan_db(pattern_list): hyperscan_stream_end_magic_db = build_stream_end_scan_db(STREAM_END_MAGIC_PATTERNS) -@attr.define +@attrs.define class XZSearchContext: start_offset: int file: File diff --git a/unblob/handlers/executable/elf.py b/unblob/handlers/executable/elf.py index 1af654ead5..4708ec1a61 100644 --- a/unblob/handlers/executable/elf.py +++ b/unblob/handlers/executable/elf.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import Optional -import attr +import attrs import lief from structlog import get_logger @@ -29,7 +29,7 @@ KERNEL_INIT_DATA_SECTION = ".init.data" -@attr.define(repr=False) +@attrs.define(repr=False) class ElfChunk(ValidChunk): def extract(self, inpath: Path, outdir: Path): # ELF file extraction is special in that in the general case no new files are extracted, thus diff --git a/unblob/handlers/filesystem/yaffs.py b/unblob/handlers/filesystem/yaffs.py index 1c2a9e82f8..fd242614af 100644 --- a/unblob/handlers/filesystem/yaffs.py +++ b/unblob/handlers/filesystem/yaffs.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import Optional -import attr +import attrs from structlog import get_logger from treelib import Tree from treelib.exceptions import NodeIDAbsentError @@ -141,7 +141,7 @@ class YaffsObjectType(IntEnum): SPECIAL = 5 -@attr.define +@attrs.define class YAFFSChunk: chunk_id: int offset: int @@ -149,7 +149,7 @@ class YAFFSChunk: object_id: int -@attr.define +@attrs.define class YAFFS1Chunk(YAFFSChunk): serial: int ecc: bytes @@ -157,12 +157,12 @@ class YAFFS1Chunk(YAFFSChunk): block_status: int -@attr.define +@attrs.define class YAFFS2Chunk(YAFFSChunk): seq_number: int -@attr.define +@attrs.define class YAFFSFileVar: file_size: int stored_size: int @@ -170,7 +170,7 @@ class YAFFSFileVar: top_level: int -@attr.define +@attrs.define class YAFFSConfig: endianness: Endian page_size: int @@ -178,22 +178,22 @@ class YAFFSConfig: ecc: bool -@attr.define +@attrs.define class YAFFSEntry: object_type: YaffsObjectType object_id: int parent_obj_id: int - sum_no_longer_used: int = attr.ib(default=0) - name: str = attr.ib(default="") - alias: str = attr.ib(default="") - equiv_id: int = attr.ib(default=0) - file_size: int = attr.ib(default=0) - st_mode: int = attr.ib(default=0) - st_uid: int = attr.ib(default=0) - st_gid: int = attr.ib(default=0) - st_atime: int = attr.ib(default=0) - st_mtime: int = attr.ib(default=0) - st_ctime: int = attr.ib(default=0) + sum_no_longer_used: int = attrs.field(default=0) + name: str = attrs.field(default="") + alias: str = attrs.field(default="") + equiv_id: int = attrs.field(default=0) + file_size: int = attrs.field(default=0) + st_mode: int = attrs.field(default=0) + st_uid: int = attrs.field(default=0) + st_gid: int = attrs.field(default=0) + st_atime: int = attrs.field(default=0) + st_mtime: int = attrs.field(default=0) + st_ctime: int = attrs.field(default=0) def __lt__(self, other): return self.object_id < other.object_id @@ -208,18 +208,18 @@ def __str__(self): return f"{self.object_id}: {self.name}" -@attr.define(kw_only=True) +@attrs.define(kw_only=True) class YAFFS2Entry(YAFFSEntry): - chksum: int = attr.ib(default=0) - st_rdev: int = attr.ib(default=0) - win_ctime: list[int] = attr.ib(default=[]) - win_mtime: list[int] = attr.ib(default=[]) - inband_shadowed_obj_id: int = attr.ib(default=0) - inband_is_shrink: int = attr.ib(default=0) - reserved: list[int] = attr.ib(default=[]) - shadows_obj: int = attr.ib(default=0) - is_shrink: int = attr.ib(default=0) - filehead: YAFFSFileVar = attr.ib(default=None) + chksum: int = attrs.field(default=0) + st_rdev: int = attrs.field(default=0) + win_ctime: list[int] = attrs.field(default=[]) + win_mtime: list[int] = attrs.field(default=[]) + inband_shadowed_obj_id: int = attrs.field(default=0) + inband_is_shrink: int = attrs.field(default=0) + reserved: list[int] = attrs.field(default=[]) + shadows_obj: int = attrs.field(default=0) + is_shrink: int = attrs.field(default=0) + filehead: YAFFSFileVar = attrs.field(default=None) def iterate_over_file( diff --git a/unblob/models.py b/unblob/models.py index b6227007e4..0bba100e1c 100644 --- a/unblob/models.py +++ b/unblob/models.py @@ -6,7 +6,6 @@ from pathlib import Path from typing import Optional, TypeVar -import attr import attrs from structlog import get_logger @@ -31,22 +30,22 @@ # -@attr.define(frozen=True) +@attrs.define(frozen=True) class Task: path: Path depth: int blob_id: str - is_multi_file: bool = attr.field(default=False) + is_multi_file: bool = attrs.field(default=False) -@attr.define +@attrs.define class Blob: - id: str = attr.field( + id: str = attrs.field( factory=new_id, ) -@attr.define +@attrs.define class Chunk(Blob): """File chunk, have start and end offset, but still can be invalid. @@ -56,10 +55,10 @@ class Chunk(Blob): b[c.start_offset:c.end_offset] """ - start_offset: int = attr.field(kw_only=True) + start_offset: int = attrs.field(kw_only=True) """The index of the first byte of the chunk""" - end_offset: int = attr.field(kw_only=True) + end_offset: int = attrs.field(kw_only=True) """The index of the first byte after the end of the chunk""" file: Optional[File] = None @@ -101,12 +100,12 @@ def __repr__(self) -> str: return self.range_hex -@attr.define(repr=False) +@attrs.define(repr=False) class ValidChunk(Chunk): """Known to be valid chunk of a File, can be extracted with an external program.""" - handler: "Handler" = attr.ib(init=False, eq=False) - is_encrypted: bool = attr.ib(default=False) + handler: "Handler" = attrs.field(init=False, eq=False) + is_encrypted: bool = attrs.field(default=False) def extract(self, inpath: Path, outdir: Path) -> Optional["ExtractResult"]: if self.is_encrypted: @@ -131,7 +130,7 @@ def as_report(self, extraction_reports: list[Report]) -> ChunkReport: ) -@attr.define(repr=False) +@attrs.define(repr=False) class UnknownChunk(Chunk): r"""Gaps between valid chunks or otherwise unknown chunks. @@ -152,7 +151,7 @@ def as_report(self, randomness: Optional[RandomnessReport]) -> UnknownChunkRepor ) -@attr.define(repr=False) +@attrs.define(repr=False) class PaddingChunk(Chunk): r"""Gaps between valid chunks or otherwise unknown chunks. @@ -177,10 +176,10 @@ def as_report( @attrs.define class MultiFile(Blob): - name: str = attr.field(kw_only=True) - paths: list[Path] = attr.field(kw_only=True) + name: str = attrs.field(kw_only=True) + paths: list[Path] = attrs.field(kw_only=True) - handler: "DirectoryHandler" = attr.ib(init=False, eq=False) + handler: "DirectoryHandler" = attrs.field(init=False, eq=False) def extract(self, outdir: Path) -> Optional["ExtractResult"]: return self.handler.extract(self.paths, outdir) @@ -198,11 +197,11 @@ def as_report(self, extraction_reports: list[Report]) -> MultiFileReport: ReportType = TypeVar("ReportType", bound=Report) -@attr.define +@attrs.define class TaskResult: task: Task - reports: list[Report] = attr.field(factory=list) - subtasks: list[Task] = attr.field(factory=list) + reports: list[Report] = attrs.field(factory=list) + subtasks: list[Task] = attrs.field(factory=list) def add_report(self, report: Report): self.reports.append(report) @@ -214,9 +213,9 @@ def filter_reports(self, report_class: type[ReportType]) -> list[ReportType]: return [report for report in self.reports if isinstance(report, report_class)] -@attr.define +@attrs.define class ProcessResult: - results: list[TaskResult] = attr.field(factory=list) + results: list[TaskResult] = attrs.field(factory=list) @property def errors(self) -> list[ErrorReport]: @@ -257,9 +256,9 @@ def get_output_dir(self) -> Optional[Path]: class _JSONEncoder(json.JSONEncoder): def default(self, obj): - if attr.has(type(obj)): + if attrs.has(type(obj)): extend_attr_output = True - attr_output = attr.asdict(obj, recurse=not extend_attr_output) + attr_output = attrs.asdict(obj, recurse=not extend_attr_output) attr_output["__typename__"] = obj.__class__.__name__ return attr_output @@ -295,7 +294,7 @@ def __init__(self, *reports: Report): self.reports: tuple[Report, ...] = reports -@attr.define(kw_only=True) +@attrs.define(kw_only=True) class ExtractResult: reports: list[Report] diff --git a/unblob/processing.py b/unblob/processing.py index bd349c50ec..7f271743b8 100644 --- a/unblob/processing.py +++ b/unblob/processing.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Optional, Union -import attr +import attrs import magic import plotext as plt from structlog import get_logger @@ -83,9 +83,9 @@ DEFAULT_SKIP_EXTENSION = (".rlib",) -@attr.define(kw_only=True) +@attrs.define(kw_only=True) class ExtractionConfig: - extract_root: Path = attr.field(converter=lambda value: value.resolve()) + extract_root: Path = attrs.field(converter=lambda value: value.resolve()) force_extract: bool = False randomness_depth: int randomness_plot: bool = False diff --git a/unblob/report.py b/unblob/report.py index 641b2d408b..b902903ce1 100644 --- a/unblob/report.py +++ b/unblob/report.py @@ -5,21 +5,21 @@ from pathlib import Path from typing import Optional, Union, final -import attr +import attrs -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class Report: """A common base class for different reports.""" def __attrs_post_init__(self): - for field in attr.fields(type(self)): + for field in attrs.fields(type(self)): value = getattr(self, field.name) if isinstance(value, int): object.__setattr__(self, field.name, int(value)) def asdict(self) -> dict: - return attr.asdict(self) + return attrs.asdict(self) class Severity(Enum): @@ -29,7 +29,7 @@ class Severity(Enum): WARNING = "WARNING" -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class ErrorReport(Report): severity: Severity @@ -43,12 +43,12 @@ def _convert_exception_to_str(obj: Union[str, Exception]) -> str: raise ValueError("Invalid exception object", obj) -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class UnknownError(ErrorReport): """Describes an exception raised during file processing.""" - severity: Severity = attr.field(default=Severity.ERROR) - exception: Union[str, Exception] = attr.field( # pyright: ignore[reportGeneralTypeIssues] + severity: Severity = attrs.field(default=Severity.ERROR) + exception: Union[str, Exception] = attrs.field( # pyright: ignore[reportGeneralTypeIssues] converter=_convert_exception_to_str ) """Exceptions are also formatted at construct time. @@ -59,7 +59,7 @@ class UnknownError(ErrorReport): """ -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class CalculateChunkExceptionReport(UnknownError): """Describes an exception raised during calculate_chunk execution.""" @@ -68,7 +68,7 @@ class CalculateChunkExceptionReport(UnknownError): handler: str -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class CalculateMultiFileExceptionReport(UnknownError): """Describes an exception raised during calculate_chunk execution.""" @@ -77,7 +77,7 @@ class CalculateMultiFileExceptionReport(UnknownError): handler: str -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class ExtractCommandFailedReport(ErrorReport): """Describes an error when failed to run the extraction command.""" @@ -88,13 +88,13 @@ class ExtractCommandFailedReport(ErrorReport): exit_code: int -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class OutputDirectoryExistsReport(ErrorReport): severity: Severity = Severity.ERROR path: Path -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class ExtractorDependencyNotFoundReport(ErrorReport): """Describes an error when the dependency of an extractor doesn't exist.""" @@ -102,7 +102,7 @@ class ExtractorDependencyNotFoundReport(ErrorReport): dependencies: list[str] -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class ExtractorTimedOut(ErrorReport): """Describes an error when the extractor execution timed out.""" @@ -111,7 +111,7 @@ class ExtractorTimedOut(ErrorReport): timeout: float -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class MaliciousSymlinkRemoved(ErrorReport): """Describes an error when malicious symlinks have been removed from disk.""" @@ -120,7 +120,7 @@ class MaliciousSymlinkRemoved(ErrorReport): target: str -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class MultiFileCollisionReport(ErrorReport): """Describes an error when MultiFiles collide on the same file.""" @@ -129,7 +129,7 @@ class MultiFileCollisionReport(ErrorReport): handler: str -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class StatReport(Report): path: Path size: int @@ -157,7 +157,7 @@ def from_path(cls, path: Path): ) -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class HashReport(Report): md5: str sha1: str @@ -183,13 +183,13 @@ def from_path(cls, path: Path): ) -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class FileMagicReport(Report): magic: str mime_type: str -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class RandomnessMeasurements: percentages: list[float] block_size: int @@ -204,14 +204,14 @@ def lowest(self): return min(self.percentages) -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class RandomnessReport(Report): shannon: RandomnessMeasurements chi_square: RandomnessMeasurements @final -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class ChunkReport(Report): id: str handler_name: str @@ -223,7 +223,7 @@ class ChunkReport(Report): @final -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class UnknownChunkReport(Report): id: str start_offset: int @@ -232,13 +232,13 @@ class UnknownChunkReport(Report): randomness: Optional[RandomnessReport] -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class CarveDirectoryReport(Report): carve_dir: Path @final -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class MultiFileReport(Report): id: str handler_name: str @@ -247,7 +247,7 @@ class MultiFileReport(Report): extraction_reports: list[Report] -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class ExtractionProblem(Report): """A non-fatal problem discovered during extraction. @@ -275,7 +275,7 @@ def log_with(self, logger): logger.warning(self.log_msg, path=self.path) -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class PathTraversalProblem(ExtractionProblem): extraction_path: str @@ -287,7 +287,7 @@ def log_with(self, logger): ) -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class LinkExtractionProblem(ExtractionProblem): link_path: str @@ -295,7 +295,7 @@ def log_with(self, logger): logger.warning(self.log_msg, path=self.path, link_path=self.link_path) -@attr.define(kw_only=True, frozen=True) +@attrs.define(kw_only=True, frozen=True) class SpecialFileExtractionProblem(ExtractionProblem): mode: int device: int diff --git a/unblob/testing.py b/unblob/testing.py index 6301ff9651..196261d76a 100644 --- a/unblob/testing.py +++ b/unblob/testing.py @@ -6,8 +6,8 @@ import subprocess from pathlib import Path +import attrs import pytest -from attr import dataclass from lark.lark import Lark from lark.visitors import Discard, Transformer from pytest_cov.embed import cleanup_on_sigterm @@ -48,9 +48,9 @@ def gather_integration_tests(test_data_path: Path): for input_dir, output_dir, test_id in zip( test_input_dirs, test_output_dirs, test_ids ): - assert ( - list(input_dir.iterdir()) != [] - ), f"Integration test input dir should contain at least 1 file: {input_dir}" + assert list(input_dir.iterdir()) != [], ( + f"Integration test input dir should contain at least 1 file: {input_dir}" + ) yield pytest.param(input_dir, output_dir, id=test_id) @@ -163,7 +163,7 @@ def unhex(hexdump: str) -> bytes: ) -@dataclass +@attrs.define class _HexdumpLine: offset: int data: bytes