Skip to content

Latest commit

 

History

History
138 lines (109 loc) · 6.1 KB

CONTRIBUTING.md

File metadata and controls

138 lines (109 loc) · 6.1 KB

Contributing to RecoverKit

Thanks for considering it. Read SECURITY.md first — there is a short list of contributions this project declines on principle, and it is better to know before you write the code than after.

Setup

git clone https://github.com/recoverkit/recoverkit
cd recoverkit
uv sync --all-extras          # or: pip install -e ".[dev]"
uv run pytest                 # must pass with zero external binaries installed
uv run ruff check . && uv run ruff format --check .
uv run mypy src

The test suite deliberately requires no external tools. Every subprocess call goes through external.py, and tests/fakebin/ replays recorded output for adb, photorec, pymobiledevice3 and the rest. If a change to a wrapper cannot be tested without installing the real binary, the wrapper is doing too much — move the logic out of the subprocess boundary.

The one rule that shapes everything

acquire/ is the only code allowed to touch a device. Everything else operates on a case bundle on disk.

That is what makes the project testable without hardware, lets someone analyse an image acquired by a completely different tool, and keeps the fake-binary harness down to a single seam. A pull request that reaches for a device from recover/, artifacts/ or report/ will be sent back regardless of how well it works.

Writing an artifact module

Modules follow the iLEAPP __artifacts_v2__ convention exactly, on purpose. A module written here is a valid iLEAPP module and vice versa, which means we can adopt any of their ~700 parsers by copying one file, and our deleted-data modules can go upstream unchanged. Inventing our own format would buy nothing and cost that permanently. See docs/UPSTREAM.md.

One file per artifact, in src/recoverkit/artifacts/<platform>/:

__artifacts_v2__ = {
    "photos_trashed": {
        "name": "Photos - Recently Deleted",
        "description": "Assets flagged as trashed but not yet purged",
        "author": "@yourhandle",
        "creation_date": "2026-08-05",
        "last_update_date": "2026-08-05",
        "requirements": "none",
        "category": "Photos",
        "notes": "30 day retention. ZTRASHEDDATE drives the expiry countdown.",
        "paths": ("*/PhotoData/Photos.sqlite*",),
        "output_types": "standard",
        "artifact_icon": "trash-2",
    }
}


@artifact_processor
def photos_trashed(context):
    ...
    return data_headers, data_list, source_path

Requirements for a module to be merged:

  • Return headers that match RecoveredItem field names. The ledger maps by name; unknown headers land in raw rather than being dropped.
  • Set method on every row. The method is the provenance and it determines the confidence tier. Never set confidence yourself — it is derived. See docs/CONFIDENCE.md.
  • Pull .db, -wal and -shm together and never let SQLite checkpoint a database you are analysing. A checkpoint destroys exactly the evidence you came for. Use the read-only helpers in recoverkit.sqlite_ro, not bare sqlite3.connect.
  • Ship a golden test. tests/corpus/golden/<module>/input/ plus expected.json. CI fails a new module without one. This is the contract that keeps a repository of thirty modules maintainable.
  • Fail loudly, never quietly wrong. If a field cannot be parsed, report it as unparsed. A message body attributed to the wrong person is worse than a gap.

Style

  • Files 200–400 lines, 800 hard maximum. Functions under 50.
  • pathlib everywhere. Never join paths with strings, never assume /, never assume case sensitivity, never shell=True. Windows is a supported platform.
  • Type annotations on everything public; mypy --strict passes.
  • Comments explain constraints the code cannot show — a retention window, a schema quirk, why a database must not be checkpointed. Not what the next line does.
  • Prefer reusing an existing maintained tool over writing a new one, and say so in the pull request when you do. Half the value of this project's design is what it refuses to reimplement.

Adding a dependency

Answer these in the pull request:

  1. What does it do that we cannot do in a few lines of standard library?
  2. What is its licence, and is it maintained?
  3. Import or subprocess? Anything under GPL is subprocess-only — see the licence boundary section in docs/ARCHITECTURE.md. Adding a GPL package to dependencies breaks the project's licensing position.
  4. Does it need a compiler? If so it belongs in an extra, not the base install.

Then update THIRD_PARTY_LICENSES.md — CI checks it for drift.

Dependencies without a licence file are unusable regardless of quality, because "no licence" means all rights reserved. Two well-known tools in this space are affected; they are listed in THIRD_PARTY_LICENSES.md under "Deliberately not used" so nobody has to rediscover it.

Commits and pull requests

Conventional commits: feat:, fix:, docs:, refactor:, test:, chore:, perf:, ci:. Explain why in the body when the reason is not obvious.

Sign off your commits (git commit -s) to certify the Developer Certificate of Origin.

Never commit real evidence. Not a backup, not a database, not a screenshot of someone's messages. .gitignore blocks the common shapes, but it cannot catch everything. Fixtures must be synthetic and reproducible from a generator, or come with the script that built them and its hash — see docs/TEST_CORPUS.md.

Changing the confidence mapping

The method-to-confidence table is a claim that people may rely on outside this project, possibly in proceedings. Changing it can retroactively alter how an older report should be read.

So: it changes only in a versioned release, only with the rationale written into docs/CONFIDENCE.md, and only with a loud entry in CHANGELOG.md. It is frozen alongside the ledger schema_version. This is not bureaucracy for its own sake — it is the difference between a number that means something and a number that does not.